Compare commits

...

104 Commits

Author SHA1 Message Date
Bogdan
8a891d07cf Test eligibility of the first request in AvistazBase 2023-06-17 14:22:57 +03:00
Bogdan
40a932cd28 Improved page loading errors 2023-06-17 03:36:40 +03:00
Mark McDowall
4a81630073 Fixed: Clearing logs not updating UI once complete
(cherry picked from commit 56b3acddc9f50f59c78c03ca072fe802752b88a7)
2023-06-17 02:27:45 +03:00
Bogdan
0ff0fe2e68 Prevent NullRef when deleting missing backups 2023-06-17 02:08:40 +03:00
Bogdan
51e33740b0 Update import path in CategoryLabel 2023-06-17 01:08:06 +03:00
Bogdan
119164f729 Show indexer privacy in search results 2023-06-16 05:23:07 +03:00
Bogdan
ef0f8e25fd Sort limits in IndexerCapabilities 2023-06-16 05:23:07 +03:00
Bogdan
d21debe77f Convert to 'using' declaration in Housekeeping Tasks 2023-06-16 02:40:47 +03:00
Bogdan
a3ccc3d0cf Close database connections in housekeeping tasks 2023-06-16 02:40:39 +03:00
Bogdan
46d930e903 Apply template text to switch cases in Cardigann 2023-06-16 00:06:11 +03:00
Bogdan
4561859c2b Fixed: (UI) Case-insensitive sorting for add indexer modal 2023-06-14 10:03:02 +03:00
Bogdan
83166fb0b5 Allow array of string as value in EnhancedSelectInput 2023-06-14 07:11:50 +03:00
Bogdan
b98f9a945d Fix use of TmdbId in NewznabRequestGenerator 2023-06-14 04:11:26 +03:00
Bogdan
e658e3fe48 Fixed: (Cardigann) Skip duplicated GET requests 2023-06-12 03:58:02 +03:00
Bogdan
9042525f22 Bump version to 1.6.1 2023-06-11 09:33:46 +03:00
Bogdan
7b551a0af1 Update Anidub description 2023-06-11 07:42:41 +03:00
Bogdan
31c2917bad Fixed: (Indexers) Allow RSS searches in HttpIndexerBase 2023-06-11 03:25:00 +03:00
Bogdan
419cce53f7 Fixed: (Transmission) Set seed limits in client 2023-06-11 03:02:11 +03:00
Bogdan
48cd1d9f6b Reset ContentSummary on redirect in HttpClient 2023-06-11 01:54:13 +03:00
Bogdan
8bd6a313b7 Fixed: (FreeboxDownload) Set seed limits in client 2023-06-10 23:32:06 +03:00
Bogdan
7cb465787e Use more specific styling for kinds in ProgressBar
(cherry picked from commit dd31c913d2a974d95f3be251714ce749cfd99a72)
2023-06-10 02:10:25 +03:00
Bogdan
0b610ff9c8 Cleanse messages for TL 24h RSS feed links 2023-06-09 17:29:49 +03:00
Servarr
5187460298 Automated API Docs update 2023-06-09 16:49:58 +03:00
Bogdan
f0d9b43480 Add some API attributes 2023-06-09 04:12:57 +03:00
Bogdan
a1081cc554 Bump NLog to 5.2.0 2023-06-09 04:12:20 +03:00
Bogdan
c4bb1ba69a Catch JsonReaderException when parsing JSON in Cardigann 2023-06-09 01:37:07 +03:00
Bogdan
3a4c8db98c Add all search types in TorrentRssIndexer
For apps who don't support all categories with normal search, eg. Sonarr
2023-06-08 17:54:16 +03:00
Bogdan
a522796798 Fixed: (Apps) Change the default sync level to Full Sync 2023-06-08 17:47:13 +03:00
Bogdan
e012eda0cf Use the default IndexerCapabilities for TorrentRssIndexer 2023-06-08 00:23:58 +03:00
Bogdan
72ab2b34c4 Cleanse /Users for Mac users 2023-06-07 08:20:14 +03:00
Bogdan
aaba5b7499 Add help link to finding cookies guide 2023-06-07 05:55:13 +03:00
Bogdan
455b76c45c New: Add TorrentRssIndexer
Co-authored-by: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com>
2023-06-07 03:57:37 +03:00
Bogdan
596d3297da Move seed configuration logic to TorrentClientBase 2023-06-07 01:32:03 +03:00
Bogdan
d05128ca33 Map seed configuration on release only when it's not null
Fixes #1720
2023-06-06 23:12:52 +03:00
Bogdan
f5b57db753 Check if release still exists in cache when grabbing release 2023-06-06 23:00:12 +03:00
Bogdan
f7d7cca982 Add extension only for known protocols in ReleaseResource 2023-06-06 21:52:44 +03:00
Bogdan
7c5409383e Fixed: (NzbIndex) Use UsenetIndexerBase 2023-06-06 20:10:01 +03:00
Bogdan
98db8f8bf8 Add default definitions for download clients 2023-06-06 19:56:35 +03:00
Bogdan
88e793d76d Fixed: (Cardigann) Allow empty inputs for login.method form/post 2023-06-06 05:59:17 +03:00
Bogdan
0f31af6b89 Fixed: (Cardigann) Allow empty inputs for login.method get 2023-06-06 01:17:27 +03:00
Bogdan
65adf30f59 Fixed: (UTorrent) Set seed limits in client 2023-06-05 17:36:20 +03:00
Bogdan
da75519524 Fixed: (Deluge) Set seed limits in client 2023-06-05 17:22:30 +03:00
Bogdan
ed1fb58242 Align QBittorrent with upstream 2023-06-05 16:41:06 +03:00
Bogdan
d5daf6791c New: Support for seed configuration in DownloadService 2023-06-05 16:41:06 +03:00
Weblate
1f1a345d25 Translations update from Servarr Weblate
Co-authored-by: MoowGlax <matthieu.derouet.pro@gmail.com>
Co-authored-by: Thodoris Kalatzis <teo.kal@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: emacsdias <emacs.dias@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: splifter <a.strahlke@gmail.com>
Co-authored-by: victor22265 <843427709@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-06-05 16:37:55 +03:00
Bogdan
76a2f51533 Fixed: (HDTorrents) Add login error message 2023-06-05 02:22:49 +03:00
Bogdan
8c0bc9ab4e Filter enabled indexer proxies in Active 2023-06-05 02:22:05 +03:00
Servarr
b0c2b9119b Automated API Docs update 2023-06-05 00:02:28 +03:00
Bogdan
87fdf17926 Add HelpTextWarning support in FieldDefinition 2023-06-04 23:46:02 +03:00
Qstick
0f1b466a19 Bump version to 1.6.0 2023-06-04 15:43:52 -05:00
Bogdan
ea635e685b Fix duplicate indexers with same name in add modal 2023-06-03 05:32:42 +03:00
Bogdan
73f23d56dc Use HelpText for non-URL values 2023-06-02 18:24:14 +03:00
Bogdan
f14ccebf3a Update magnet trackers 2023-06-02 15:16:56 +03:00
Qstick
9539e4d481 More mono cleanup 2023-06-02 00:20:18 -05:00
Bogdan
e40ccc49ad Fixed: (Apps) Prevent null reference exception on sync failure 2023-06-02 07:26:25 +03:00
Qstick
9fd3eb4d6b Extract useSelectState from SelectContext
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2023-05-31 19:41:55 -05:00
Qstick
78aab80703 New: Additional custom filter predicates for strings 2023-05-31 19:33:28 -05:00
Weblate
868394d588 Translations update from Servarr Weblate
Co-authored-by: Cc95459 <954591059@qq.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Jens <jensmahnke@me.com>
Co-authored-by: Thijs Waalen <contact@thijswaalen.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-06-01 01:58:57 +03:00
Bogdan
d5e5697db8 Remove Rarbg tracker from MagnetLinkBuilder 2023-05-31 21:06:14 +03:00
Bogdan
d1e39f206a New: (Rarbg) Obsolete due to tracker shutdown 2023-05-31 18:00:24 +03:00
Bogdan
b59d89f308 Fixed: Don't log handled exceptions in API 2023-05-31 06:53:51 +03:00
Bogdan
bf5855beb4 Revert "Fixed: Don't log handled exceptions in API"
This reverts commit 19ff73dad0.
2023-05-31 06:53:43 +03:00
Bogdan
2d36adf865 Fixed: (Cardigann): Use MissingAttributeEqualsNoResults for Search.Rows.Attribute 2023-05-29 17:35:37 +03:00
Bogdan
ef1ad59f59 Fixed: (Cardigann) Respect the categories from search paths 2023-05-29 00:57:12 +03:00
Servarr
59b6e8af27 Automated API Docs update 2023-05-28 22:31:36 +03:00
Bogdan
3ae1917d3b Fixed: Revert seed criteria validation to warnings
Closes #1692
2023-05-28 22:21:08 +03:00
Bogdan
5864a090e4 Fixed: Enforce validation warnings
(cherry picked from commit 48ee1158ad4213fd0690842e2672f52d08f7ad26)
2023-05-28 22:18:34 +03:00
Bogdan
fcfec1b859 Simplify ShouldHaveApiKey and HasErrors
(cherry picked from commit 7343616a47cd538bba4c9128d2c1094561f9b3a5)
2023-05-28 22:17:31 +03:00
Bogdan
65541017dd Fixed: (RuTracker) Update categories 2023-05-28 20:50:15 +03:00
SetekhZ
7fe9942c28 Fixed: (RuTracker) Use supported 200 categories per search request 2023-05-28 19:40:06 +03:00
Bogdan
360827708f Use 'var' instead of explicit type
(cherry picked from commit 12374f7f0038e5b25548f5ab3f71122410832393)
2023-05-28 18:52:10 +03:00
Bogdan
0509335387 Inline 'out' variable declarations
(cherry picked from commit 281add47de1d3940990156c841362125dea9cc7d)
2023-05-28 18:45:10 +03:00
Bogdan
f54212a809 Standardize variable declaration
(cherry picked from commit 909f2ded6b75998fa8e1addd0dcf849279e7b120)
2023-05-28 18:40:17 +03:00
Bogdan
ea0eb2efa7 Enforce rule IDE0005 on build
(cherry picked from commit 6b1e4ef81938d264a2ddc8b626b0502f799aa640)
2023-05-28 18:39:57 +03:00
Bogdan
ce430433e5 Bump version to 1.5.2 2023-05-28 18:21:49 +03:00
Matthew Strapp
5437aac346 Fixed: Use relative paths instead of absolute paths for webmanifest
(cherry picked from commit 8e771f95ade919a8f1ed7b48675f032a6c508cb2)
2023-05-28 17:33:23 +03:00
Bogdan
b02188acf4 Fixed: (HealthCheck) Check only enabled indexer proxies 2023-05-28 14:55:20 +03:00
Bogdan
6897ed0b3f Fixed: (Anidex) Search with all categories selected 2023-05-28 02:16:06 +03:00
Bogdan
b3ddf2f9cd Improve logging when no releases were found 2023-05-28 02:04:39 +03:00
Bogdan
d9ce9eb0b2 Add defaults definitions for indexer proxies 2023-05-28 01:52:38 +03:00
bakerboy448
29ab1801db Fixed a really important spelling mistake
(cherry picked from commit b510201b43f6bc5e6774119ebbd7b8a0d89ee487)
2023-05-27 13:08:16 +03:00
Mark McDowall
19ff73dad0 Fixed: Don't log handled exceptions in API
(cherry picked from commit 59f2e5b65dd7352aad92b33adefa6cf5ca79a0de)
2023-05-27 13:03:36 +03:00
Bogdan
c455f1a113 New: (BakaBT) Add freeleech only option 2023-05-26 20:45:23 +03:00
Qstick
b8793d8783 Remove mono process detection
(cherry picked from commit 5a046026725084bc880a7b63d7105dcf4d882128)
2023-05-26 16:51:33 +03:00
Bogdan
ce34940287 Ensuring backward compatibility with older versions on first sync 2023-05-26 09:54:51 +03:00
Bogdan
dcb19a66b0 New: Add minimum version checks for applications 2023-05-26 09:54:51 +03:00
Weblate
b3bc92e60e Translated using Weblate (Indonesian)
Currently translated at 3.5% (18 of 514 strings)

Co-authored-by: liimee <git.taaa@fedora.email>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translation: Servarr/Prowlarr
2023-05-25 13:38:21 +03:00
bakerboy448
1b17d38564 Fixed: (Animedia) Description Language 2023-05-24 23:17:34 -05:00
Bogdan
d8c7361205 Convert typeof to nameof 2023-05-24 19:25:08 +03:00
Bogdan
7a0dd0bc0d Fixed: (AnimeTorrents) Replace non-word chars with wildcard in search term 2023-05-24 00:15:13 +03:00
Mark McDowall
c02bfb5930 Fixed: Don't rollback file move if destination already exists
(cherry picked from commit f05405fe1ce4c78a8c75e27920c863c5b83686bd)
(cherry picked from commit 8ab040f612ee04dac4813a08cdeaddd446a64dc9)
2023-05-23 20:16:53 +03:00
Weblate
d0fbb1f49a Translated using Weblate (French)
Currently translated at 98.6% (507 of 514 strings)

Co-authored-by: foXaCe <foxace66@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translation: Servarr/Prowlarr
2023-05-23 10:36:59 +03:00
Bogdan
aafdefe2f0 Fixed: (RuTracker) Improve the error message for failed logins 2023-05-22 14:12:41 +03:00
Bogdan
96234c0fe1 Fixed: (SceneTime) Update categories 2023-05-21 22:13:11 +03:00
Bogdan
8b5648d7bd Fix spelling "Use languages from Torznab/Newznab attributes if given"
(cherry picked from commit de3bfb7c5ab03e527dca1be3ef4a664dce266db6)
2023-05-21 21:15:20 +03:00
Qstick
1fc79f9e9b New: Use languages from Torznab/Newznab attributes if given
(cherry picked from commit 9c5a07f62a6e32832c10c80813cd3b98c5859989)
2023-05-21 21:13:48 +03:00
S0me6uy
ec40761757 New: Signal Notifications
(cherry picked from commit 59dd3b11271a63ea16f0e32a596dba8e9b9d1096)
2023-05-21 21:01:46 +03:00
Bogdan
0a8e4eb092 New: Improve validation messages
(cherry picked from commit a117001de673e80abd90d54a34a7c86292b3a649)
2023-05-21 20:59:31 +03:00
Bogdan
ade961fad5 Minor CS improvements in NzbDroneValidation
(cherry picked from commit 6118afa339621509aad55caf27b05e89bd0b8c74)
2023-05-21 20:58:25 +03:00
Bogdan
81b1c0e445 Add tests and ignore 0 in GetFullImdbId 2023-05-21 11:53:06 +03:00
Bogdan
0fe54ed36a Fix tests in IndexerServiceFixture 2023-05-21 11:52:54 +03:00
Bogdan
337828ff9c Bump version to 1.5.1 2023-05-21 10:15:52 +03:00
Mark McDowall
fb34294d2e Fixed: Exception when request to SABnzbd times out
(cherry picked from commit f946d78153b85ad726a06a1140143c8beac8766d)
2023-05-21 10:10:17 +03:00
Mark McDowall
931e3cf42d Cleanup TorrentDownloadStation
Fixed: Don't move seeding torrents in Synology Download Station

(cherry picked from commit 3cd33d3f44097b4cb4fb291bca70a0aa53c4b844)
2023-05-21 10:09:21 +03:00
309 changed files with 4657 additions and 3956 deletions

View File

@@ -36,12 +36,18 @@ dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
# Prefer "var" everywhere
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
csharp_style_var_elsewhere = true
# Prefer "out" variables to be declared inline
csharp_style_inlined_variable_declaration = true
# Using directive is unnecessary.
dotnet_diagnostic.IDE0005.severity = error
# Use var instead of explicit type
dotnet_diagnostic.IDE0007.severity = error
# Inline variable declaration
dotnet_diagnostic.IDE0018.severity = error
# Stylecop Rules
dotnet_diagnostic.SA0001.severity = none

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.5.0'
majorVersion: '1.6.1'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'

View File

@@ -1,58 +1,28 @@
import { cloneDeep } from 'lodash';
import React, { useEffect } from 'react';
import areAllSelected from 'Utilities/Table/areAllSelected';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import React, { useCallback, useEffect } from 'react';
import useSelectState, { SelectState } from 'Helpers/Hooks/useSelectState';
import ModelBase from './ModelBase';
export enum SelectActionType {
Reset,
SelectAll,
UnselectAll,
ToggleSelected,
RemoveItem,
UpdateItems,
}
type SelectedState = Record<number, boolean>;
interface SelectState {
selectedState: SelectedState;
lastToggled: number | null;
allSelected: boolean;
allUnselected: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items: any[];
}
type SelectAction =
| { type: SelectActionType.Reset }
| { type: SelectActionType.SelectAll }
| { type: SelectActionType.UnselectAll }
export type SelectContextAction =
| { type: 'reset' }
| { type: 'selectAll' }
| { type: 'unselectAll' }
| {
type: SelectActionType.ToggleSelected;
type: 'toggleSelected';
id: number;
isSelected: boolean;
shiftKey: boolean;
}
| {
type: SelectActionType.RemoveItem;
type: 'removeItem';
id: number;
}
| {
type: SelectActionType.UpdateItems;
type: 'updateItems';
items: ModelBase[];
};
type Dispatch = (action: SelectAction) => void;
const initialState = {
selectedState: {},
lastToggled: null,
allSelected: false,
allUnselected: true,
items: [],
};
export type SelectDispatch = (action: SelectContextAction) => void;
interface SelectProviderOptions<T extends ModelBase> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -60,90 +30,40 @@ interface SelectProviderOptions<T extends ModelBase> {
items: Array<T>;
}
function getSelectedState(items: ModelBase[], existingState: SelectedState) {
return items.reduce((acc: SelectedState, item) => {
const id = item.id;
acc[id] = existingState[id] ?? false;
return acc;
}, {});
}
// TODO: Can this be reused?
const SelectContext = React.createContext<[SelectState, Dispatch] | undefined>(
cloneDeep(undefined)
);
function selectReducer(state: SelectState, action: SelectAction): SelectState {
const { items, selectedState } = state;
switch (action.type) {
case SelectActionType.Reset: {
return cloneDeep(initialState);
}
case SelectActionType.SelectAll: {
return {
items,
...selectAll(selectedState, true),
};
}
case SelectActionType.UnselectAll: {
return {
items,
...selectAll(selectedState, false),
};
}
case SelectActionType.ToggleSelected: {
const result = {
items,
...toggleSelected(
state,
items,
action.id,
action.isSelected,
action.shiftKey
),
};
return result;
}
case SelectActionType.UpdateItems: {
const nextSelectedState = getSelectedState(action.items, selectedState);
return {
...state,
...areAllSelected(nextSelectedState),
selectedState: nextSelectedState,
items: action.items,
};
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
const SelectContext = React.createContext<
[SelectState, SelectDispatch] | undefined
>(cloneDeep(undefined));
export function SelectProvider<T extends ModelBase>(
props: SelectProviderOptions<T>
) {
const { items } = props;
const selectedState = getSelectedState(items, {});
const [state, dispatch] = useSelectState();
const [state, dispatch] = React.useReducer(selectReducer, {
selectedState,
lastToggled: null,
allSelected: false,
allUnselected: true,
items,
});
const dispatchWrapper = useCallback(
(action: SelectContextAction) => {
switch (action.type) {
case 'reset':
case 'removeItem':
dispatch(action);
break;
const value: [SelectState, Dispatch] = [state, dispatch];
default:
dispatch({
...action,
items,
});
break;
}
},
[items, dispatch]
);
const value: [SelectState, SelectDispatch] = [state, dispatchWrapper];
useEffect(() => {
dispatch({ type: SelectActionType.UpdateItems, items });
}, [items]);
dispatch({ type: 'updateItems', items });
}, [items, dispatch]);
return (
<SelectContext.Provider value={value}>

View File

@@ -578,7 +578,7 @@ EnhancedSelectInput.propTypes = {
className: PropTypes.string,
disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,

View File

@@ -67,6 +67,7 @@ function ProviderFieldFormGroup(props) {
name,
label,
helpText,
helpTextWarning,
helpLink,
placeholder,
value,
@@ -100,6 +101,7 @@ function ProviderFieldFormGroup(props) {
name={name}
label={label}
helpText={helpText}
helpTextWarning={helpTextWarning}
helpLink={helpLink}
placeholder={placeholder}
value={value}
@@ -126,6 +128,7 @@ ProviderFieldFormGroup.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
helpText: PropTypes.string,
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.any,

View File

@@ -13,7 +13,7 @@ const messages = [
'Loading humorous message... Please Wait',
'I could\'ve been faster in Python',
'Don\'t forget to rewind your tracks',
'Congratulations! you are the 1000th visitor.',
'Congratulations! You are the 1000th visitor.',
'HELP! I\'m being held hostage and forced to write these stupid lines!',
'RE-calibrating the internet...',
'I\'ll be here all week',

View File

@@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
function PageSectionContent(props) {
const {
@@ -17,7 +19,7 @@ function PageSectionContent(props) {
);
} else if (!isFetching && !!error) {
return (
<div>{errorMessage}</div>
<Alert kind={kinds.DANGER}>{errorMessage}</Alert>
);
} else if (isPopulated && !error) {
return (

View File

@@ -16,6 +16,38 @@
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
color: var(--white);
transition: width 0.6s ease;
&.primary {
background-color: var(--primaryColor);
}
&.danger {
background-color: var(--dangerColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(var(--dangerColor) shade(5%)), color(var(--dangerColor) shade(5%)) 5px, color(var(--dangerColor) shade(15%)) 5px, color(var(--dangerColor) shade(15%)) 10px);
}
}
&.success {
background-color: var(--successColor);
}
&.purple {
background-color: var(--purple);
}
&.warning {
background-color: var(--warningColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--warningColor), var(--warningColor) 5px, color(var(--warningColor) tint(15%)) 5px, color(var(--warningColor) tint(15%)) 10px);
}
}
&.info {
background-color: var(--infoColor);
}
}
.frontTextContainer {
@@ -41,38 +73,6 @@
cursor: default;
}
.primary {
background-color: var(--primaryColor);
}
.danger {
background-color: var(--dangerColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(var(--dangerColor) shade(5%)), color(var(--dangerColor) shade(5%)) 5px, color(var(--dangerColor) shade(15%)) 5px, color(var(--dangerColor) shade(15%)) 10px);
}
}
.success {
background-color: var(--successColor);
}
.purple {
background-color: var(--purple);
}
.warning {
background-color: var(--warningColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--warningColor), var(--warningColor) 5px, color(var(--warningColor) tint(15%)) 5px, color(var(--warningColor) tint(15%)) 10px);
}
}
.info {
background-color: var(--infoColor);
}
.small {
height: $progressBarSmallHeight;

View File

@@ -38,7 +38,7 @@ function ProgressBar(props) {
{
showText && width ?
<div
className={styles.backTextContainer}
className={classNames(styles.backTextContainer, styles[kind])}
style={{ width: actualWidth }}
>
<div className={styles.backText}>
@@ -67,7 +67,7 @@ function ProgressBar(props) {
{
showText ?
<div
className={styles.frontTextContainer}
className={classNames(styles.frontTextContainer, styles[kind])}
style={{ width: progressPercent }}
>
<div

View File

@@ -1,18 +1,19 @@
{
"name": "",
"name": "Prowlarr",
"icons": [
{
"src": "/Content/Images/Icons/android-chrome-192x192.png",
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/Content/Images/Icons/android-chrome-512x512.png",
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "../../../../",
"theme_color": "#3a3f51",
"background_color": "#3a3f51",
"display": "standalone"
}
}

View File

@@ -0,0 +1,113 @@
import { cloneDeep } from 'lodash';
import { useReducer } from 'react';
import ModelBase from 'App/ModelBase';
import areAllSelected from 'Utilities/Table/areAllSelected';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
export type SelectedState = Record<number, boolean>;
export interface SelectState {
selectedState: SelectedState;
lastToggled: number | null;
allSelected: boolean;
allUnselected: boolean;
}
export type SelectAction =
| { type: 'reset' }
| { type: 'selectAll'; items: ModelBase[] }
| { type: 'unselectAll'; items: ModelBase[] }
| {
type: 'toggleSelected';
id: number;
isSelected: boolean;
shiftKey: boolean;
items: ModelBase[];
}
| {
type: 'removeItem';
id: number;
}
| {
type: 'updateItems';
items: ModelBase[];
};
export type Dispatch = (action: SelectAction) => void;
const initialState = {
selectedState: {},
lastToggled: null,
allSelected: false,
allUnselected: true,
items: [],
};
function getSelectedState(items: ModelBase[], existingState: SelectedState) {
return items.reduce((acc: SelectedState, item) => {
const id = item.id;
acc[id] = existingState[id] ?? false;
return acc;
}, {});
}
function selectReducer(state: SelectState, action: SelectAction): SelectState {
const { selectedState } = state;
switch (action.type) {
case 'reset': {
return cloneDeep(initialState);
}
case 'selectAll': {
return {
...selectAll(selectedState, true),
};
}
case 'unselectAll': {
return {
...selectAll(selectedState, false),
};
}
case 'toggleSelected': {
const result = {
...toggleSelected(
state,
action.items,
action.id,
action.isSelected,
action.shiftKey
),
};
return result;
}
case 'updateItems': {
const nextSelectedState = getSelectedState(action.items, selectedState);
return {
...state,
...areAllSelected(nextSelectedState),
selectedState: nextSelectedState,
};
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
export default function useSelectState(): [SelectState, Dispatch] {
const selectedState = getSelectedState([], {});
const [state, dispatch] = useReducer(selectReducer, {
selectedState,
lastToggled: null,
allSelected: false,
allUnselected: true,
});
return [state, dispatch];
}

View File

@@ -1,14 +1,18 @@
import * as filterTypes from './filterTypes';
export const ARRAY = 'array';
export const CONTAINS = 'contains';
export const DATE = 'date';
export const EQUAL = 'equal';
export const EXACT = 'exact';
export const NUMBER = 'number';
export const STRING = 'string';
export const all = [
ARRAY,
CONTAINS,
DATE,
EQUAL,
EXACT,
NUMBER,
STRING
@@ -20,6 +24,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
],
[CONTAINS]: [
{ key: filterTypes.CONTAINS, value: 'contains' }
],
[DATE]: [
{ key: filterTypes.LESS_THAN, value: 'is before' },
{ key: filterTypes.GREATER_THAN, value: 'is after' },
@@ -29,6 +37,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
],
[EQUAL]: [
{ key: filterTypes.EQUAL, value: 'is' }
],
[EXACT]: [
{ key: filterTypes.EQUAL, value: 'is' },
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
@@ -47,6 +59,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.CONTAINS, value: 'contains' },
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' },
{ key: filterTypes.EQUAL, value: 'equal' },
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
{ key: filterTypes.NOT_EQUAL, value: 'not equal' },
{ key: filterTypes.STARTS_WITH, value: 'starts with' },
{ key: filterTypes.NOT_STARTS_WITH, value: 'does not start with' },
{ key: filterTypes.ENDS_WITH, value: 'ends with' },
{ key: filterTypes.NOT_ENDS_WITH, value: 'does not end with' }
]
};

View File

@@ -39,6 +39,22 @@ const filterTypePredicates = {
[filterTypes.NOT_EQUAL]: function(itemValue, filterValue) {
return itemValue !== filterValue;
},
[filterTypes.STARTS_WITH]: function(itemValue, filterValue) {
return itemValue.toLowerCase().startsWith(filterValue.toLowerCase());
},
[filterTypes.NOT_STARTS_WITH]: function(itemValue, filterValue) {
return !itemValue.toLowerCase().startsWith(filterValue.toLowerCase());
},
[filterTypes.ENDS_WITH]: function(itemValue, filterValue) {
return itemValue.toLowerCase().endsWith(filterValue.toLowerCase());
},
[filterTypes.NOT_ENDS_WITH]: function(itemValue, filterValue) {
return !itemValue.toLowerCase().endsWith(filterValue.toLowerCase());
}
};

View File

@@ -10,6 +10,10 @@ export const LESS_THAN = 'lessThan';
export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual';
export const NOT_CONTAINS = 'notContains';
export const NOT_EQUAL = 'notEqual';
export const STARTS_WITH = 'startsWith';
export const NOT_STARTS_WITH = 'notStartsWith';
export const ENDS_WITH = 'endsWith';
export const NOT_ENDS_WITH = 'notEndsWith';
export const all = [
CONTAINS,
@@ -23,5 +27,9 @@ export const all = [
IN_LAST,
NOT_IN_LAST,
IN_NEXT,
NOT_IN_NEXT
NOT_IN_NEXT,
STARTS_WITH,
NOT_STARTS_WITH,
ENDS_WITH,
NOT_ENDS_WITH
];

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -121,9 +122,9 @@ class History extends Component {
{
!isFetchingAny && hasError &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadHistory')}
</div>
</Alert>
}
{
@@ -131,9 +132,9 @@ class History extends Component {
// wait for the episodes to populate because they are never coming.
isPopulated && !hasError && !items.length &&
<div>
No history found
</div>
<Alert kind={kinds.INFO}>
{translate('NoHistoryFound')}
</Alert>
}
{

View File

@@ -26,7 +26,7 @@ const columns = [
isVisible: true
},
{
name: 'name',
name: 'sortName',
label: translate('Name'),
isSortable: true,
isVisible: true
@@ -226,7 +226,7 @@ class AddIndexerModalContent extends Component {
{
filteredIndexers.map((indexer) => (
<SelectIndexerRowConnector
key={indexer.name}
key={`${indexer.implementation}-${indexer.name}`}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
@@ -25,9 +25,7 @@ function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {
const onPress = useCallback(() => {
selectDispatch({
type: allSelected
? SelectActionType.UnselectAll
: SelectActionType.SelectAll,
type: allSelected ? 'unselectAll' : 'selectAll',
});
}, [allSelected, selectDispatch]);

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
@@ -26,9 +26,7 @@ function IndexerIndexSelectAllMenuItem(
const onPressWrapper = useCallback(() => {
selectDispatch({
type: allSelected
? SelectActionType.UnselectAll
: SelectActionType.SelectAll,
type: allSelected ? 'unselectAll' : 'selectAll',
});
}, [allSelected, selectDispatch]);

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
@@ -111,7 +111,7 @@ function IndexerIndexSelectFooter() {
useEffect(() => {
if (!isDeleting && !deleteError) {
selectDispatch({ type: SelectActionType.UnselectAll });
selectDispatch({ type: 'unselectAll' });
}
}, [isDeleting, deleteError, selectDispatch]);

View File

@@ -1,6 +1,6 @@
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
interface IndexerIndexSelectModeButtonProps {
@@ -20,7 +20,7 @@ function IndexerIndexSelectModeButton(
const onPressWrapper = useCallback(() => {
if (isSelectMode) {
selectDispatch({
type: SelectActionType.Reset,
type: 'reset',
});
}

View File

@@ -1,6 +1,6 @@
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
interface IndexerIndexSelectModeMenuItemProps {
@@ -19,7 +19,7 @@ function IndexerIndexSelectModeMenuItem(
const onPressWrapper = useCallback(() => {
if (isSelectMode) {
selectDispatch({
type: SelectActionType.Reset,
type: 'reset',
});
}

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
@@ -90,7 +90,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
const onSelectedChange = useCallback(
({ id, value, shiftKey }) => {
selectDispatch({
type: SelectActionType.ToggleSelected,
type: 'toggleSelected',
id,
isSelected: value,
shiftKey,

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { SelectActionType, useSelect } from 'App/SelectContext';
import { useSelect } from 'App/SelectContext';
import IconButton from 'Components/Link/IconButton';
import Column from 'Components/Table/Column';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
@@ -47,7 +47,7 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) {
const onSelectAllChange = useCallback(
({ value }) => {
selectDispatch({
type: value ? SelectActionType.SelectAll : SelectActionType.UnselectAll,
type: value ? 'selectAll' : 'unselectAll',
});
},
[selectDispatch]

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import BarChart from 'Components/Chart/BarChart';
import DoughnutChart from 'Components/Chart/DoughnutChart';
import StackedBarChart from 'Components/Chart/StackedBarChart';
@@ -178,9 +179,9 @@ function Stats(props) {
{
!isFetching && !!error &&
<div className={styles.errorMessage}>
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load indexer stats from API')}
</div>
</Alert>
}
{

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -10,7 +11,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import NoIndexer from 'Indexer/NoIndexer';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
@@ -309,9 +310,9 @@ class SearchIndex extends Component {
{
!isFetching && !!error &&
<div className={styles.errorMessage}>
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load search results from API')}
</div>
</Alert>
}
{

View File

@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import Tooltip from 'Components/Tooltip/Tooltip';
import { kinds, tooltipPositions } from 'Helpers/Props';
import Tooltip from '../../Components/Tooltip/Tooltip';
function CategoryLabel({ categories }) {
const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -8,7 +9,7 @@ import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
@@ -49,9 +50,9 @@ class DevelopmentSettings extends Component {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadDevelopmentSettings')}
</div>
</Alert>
}
{

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -123,9 +124,9 @@ class GeneralSettings extends Component {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadGeneralSettings')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -8,7 +9,7 @@ import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import themes from 'Styles/Themes';
import titleCase from 'Utilities/String/titleCase';
@@ -80,9 +81,9 @@ class UISettings extends Component {
{
!isFetching && error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadUISettings')}
</div>
</Alert>
}
{

View File

@@ -32,9 +32,9 @@ function createSaveProviderHandler(section, url, options = {}) {
const params = { ...queryParams };
// If the user is re-saving the same provider without changes
// force it to be saved. Only applies to editing existing providers.
// force it to be saved.
if (id && _.isEqual(saveData, lastSaveData)) {
if (_.isEqual(saveData, lastSaveData)) {
params.forceSave = true;
}

View File

@@ -41,7 +41,7 @@ export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
sortKey: 'name',
sortKey: 'sortName',
sortDirection: sortDirections.ASCENDING,
items: []
}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -8,7 +9,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BackupRow from './BackupRow';
import RestoreBackupModalConnector from './RestoreBackupModalConnector';
@@ -107,16 +108,16 @@ class Backups extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadBackups')}
</div>
</Alert>
}
{
noBackups &&
<div>
<Alert kind={kinds.INFO}>
{translate('NoBackupsAreAvailable')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
@@ -11,7 +12,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import LogsTableRow from './LogsTableRow';
@@ -82,9 +83,9 @@ function LogsTable(props) {
{
isPopulated && !error && !items.length &&
<div>
<Alert kind={kinds.INFO}>
No events found
</div>
</Alert>
}
{

View File

@@ -96,7 +96,14 @@ class LogsTableConnector extends Component {
};
onClearLogsPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_LOGS });
this.props.executeCommand({
name: commandNames.CLEAR_LOGS,
commandFinished: this.onCommandFinished
});
};
onCommandFinished = () => {
this.props.gotoLogsFirstPage();
};
//

View File

@@ -11,7 +11,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import LogsNavMenu from '../LogsNavMenu';
import LogFilesTableRow from './LogFilesTableRow';
@@ -118,9 +118,9 @@ class LogFiles extends Component {
{
!isFetching && !items.length &&
<div>
<Alert kind={kinds.INFO}>
{translate('NoLogFiles')}
</div>
</Alert>
}
</PageContentBody>
</PageContent>

View File

@@ -50,12 +50,6 @@ class LogFilesConnector extends Component {
this.props.fetchLogFiles();
}
componentDidUpdate(prevProps) {
if (prevProps.deleteFilesExecuting && !this.props.deleteFilesExecuting) {
this.props.fetchLogFiles();
}
}
//
// Listeners
@@ -64,7 +58,14 @@ class LogFilesConnector extends Component {
};
onDeleteFilesPress = () => {
this.props.executeCommand({ name: commandNames.DELETE_LOG_FILES });
this.props.executeCommand({
name: commandNames.DELETE_LOG_FILES,
commandFinished: this.onCommandFinished
});
};
onCommandFinished = () => {
this.props.fetchLogFiles();
};
//

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import SpinnerButton from 'Components/Link/SpinnerButton';
@@ -61,9 +62,9 @@ class Updates extends Component {
{
noUpdates &&
<div>
<Alert kind={kinds.INFO}>
{translate('NoUpdatesAreAvailable')}
</div>
</Alert>
}
{

View File

@@ -30,6 +30,13 @@
<!-- A test project gets the test sdk packages automatically added -->
<TestProject>false</TestProject>
<TestProject Condition="$(MSBuildProjectName.EndsWith('.Test'))">true</TestProject>
<!-- XML documentation comments are needed to enforce rule IDE0005 on build -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!--
CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
-->
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
@@ -102,7 +109,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
</ItemGroup>

View File

@@ -67,7 +67,7 @@ namespace NzbDrone.Automation.Test
{
try
{
Screenshot image = ((ITakesScreenshot)driver).GetScreenshot();
var image = ((ITakesScreenshot)driver).GetScreenshot();
image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png);
}
catch (Exception ex)

View File

@@ -36,7 +36,7 @@ namespace NzbDrone.Automation.Test.PageModel
{
try
{
IWebElement element = d.FindElement(By.ClassName("followingBalls"));
var element = d.FindElement(By.ClassName("followingBalls"));
return !element.Displayed;
}
catch (NoSuchElementException)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using FluentAssertions;
using NUnit.Framework;
@@ -65,9 +65,9 @@ namespace NzbDrone.Common.Test.CacheTests
[Test]
public void should_store_null()
{
int hitCount = 0;
var hitCount = 0;
for (int i = 0; i < 10; i++)
for (var i = 0; i < 10; i++)
{
_cachedString.Get("key", () =>
{
@@ -83,10 +83,10 @@ namespace NzbDrone.Common.Test.CacheTests
[Platform(Exclude = "MacOsX")]
public void should_honor_ttl()
{
int hitCount = 0;
var hitCount = 0;
_cachedString = new Cached<string>();
for (int i = 0; i < 10; i++)
for (var i = 0; i < 10; i++)
{
_cachedString.Get("key",
() =>

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -142,7 +142,7 @@ namespace NzbDrone.Common.Test
[Test]
public void SaveDictionary_should_save_proper_value()
{
int port = 20555;
var port = 20555;
var dic = Subject.GetConfigDictionary();
dic["Port"] = 20555;
@@ -155,9 +155,9 @@ namespace NzbDrone.Common.Test
[Test]
public void SaveDictionary_should_only_save_specified_values()
{
int port = 20555;
int origSslPort = 20551;
int sslPort = 20552;
var port = 20555;
var origSslPort = 20551;
var sslPort = 20552;
var dic = Subject.GetConfigDictionary();
dic["Port"] = port;

View File

@@ -42,7 +42,7 @@ namespace NzbDrone.Common.Test.DiskTests
[Test]
public void should_not_contain_recycling_bin_for_root_of_drive()
{
string root = @"C:\".AsOsAgnostic();
var root = @"C:\".AsOsAgnostic();
SetupFolders(root);
Mocker.GetMock<IDiskProvider>()
@@ -55,7 +55,7 @@ namespace NzbDrone.Common.Test.DiskTests
[Test]
public void should_not_contain_system_volume_information()
{
string root = @"C:\".AsOsAgnostic();
var root = @"C:\".AsOsAgnostic();
SetupFolders(root);
Mocker.GetMock<IDiskProvider>()
@@ -68,7 +68,7 @@ namespace NzbDrone.Common.Test.DiskTests
[Test]
public void should_not_contain_recycling_bin_or_system_volume_information_for_root_of_drive()
{
string root = @"C:\".AsOsAgnostic();
var root = @"C:\".AsOsAgnostic();
SetupFolders(root);
Mocker.GetMock<IDiskProvider>()

View File

@@ -351,6 +351,26 @@ namespace NzbDrone.Common.Test.DiskTests
.Verify(v => v.DeleteFile(_targetPath), Times.Once());
}
[Test]
public void should_not_rollback_move_on_partial_if_destination_already_exists()
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.MoveFile(_sourcePath, _targetPath, false))
.Callback(() =>
{
WithExistingFile(_targetPath, true, 900);
});
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.MoveFile(_sourcePath, _targetPath, false))
.Throws(new FileAlreadyExistsException("File already exists", _targetPath));
Assert.Throws<FileAlreadyExistsException>(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move));
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.DeleteFile(_targetPath), Times.Never());
}
[Test]
public void should_log_error_if_rollback_partialmove_fails()
{

View File

@@ -788,7 +788,7 @@ namespace NzbDrone.Common.Test.Http
try
{
// the date is bad in the below - should be 13-Jul-2026
string malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Mon, 13-Jul-26 16:19:50 GMT; path=/; HttpOnly";
var malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Mon, 13-Jul-26 16:19:50 GMT; path=/; HttpOnly";
var requestSet = new HttpRequestBuilder($"https://{_httpBinHost}/response-headers")
.AddQueryParam("Set-Cookie", malformedCookie)
.Build();
@@ -822,7 +822,7 @@ namespace NzbDrone.Common.Test.Http
{
try
{
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}";
var url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}";
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false;

View File

@@ -10,7 +10,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Indexer Urls
[TestCase(@"https://iptorrents.com/torrents/rss?u=mySecret;tp=mySecret;l5;download")]
[TestCase(@"http://rss.torrentleech.org/mySecret")]
[TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/filename.torrent")]
[TestCase(@"https://rss24h.torrentleech.org/mySecret")]
[TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")]
[TestCase(@"https://www.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")]
[TestCase(@"http://www.bitmetv.org/rss.php?uid=mySecret&passkey=mySecret")]
[TestCase(@"https://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user=sonarr&api=mySecret&eng=1")]
[TestCase(@"https://dognzb.cr/fetch/2b51db35e1912ffc138825a12b9933d2/2b51db35e1910123321025a12b9933d2")]
@@ -79,6 +81,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Deluge
[TestCase(@",{""download_location"": ""C:\Users\\mySecret mySecret\\Downloads""}")]
[TestCase(@",{""download_location"": ""/home/mySecret/Downloads""}")]
[TestCase(@",{""download_location"": ""/Users/mySecret/Downloads""}")]
[TestCase(@"auth.login(""mySecret"")")]
// Download Station

View File

@@ -74,17 +74,17 @@ namespace NzbDrone.Common
continue; // Ignore directories
}
string entryFileName = zipEntry.Name;
var entryFileName = zipEntry.Name;
// to remove the folder from the entry:- entryFileName = Path.GetFileName(entryFileName);
// Optionally match entrynames against a selection list here to skip as desired.
// The unpacked length is available in the zipEntry.Size property.
byte[] buffer = new byte[4096]; // 4K is optimum
Stream zipStream = zipFile.GetInputStream(zipEntry);
var buffer = new byte[4096]; // 4K is optimum
var zipStream = zipFile.GetInputStream(zipEntry);
// Manipulate the output filename here as desired.
string fullZipToPath = Path.Combine(destination, entryFileName);
string directoryName = Path.GetDirectoryName(fullZipToPath);
var fullZipToPath = Path.Combine(destination, entryFileName);
var directoryName = Path.GetDirectoryName(fullZipToPath);
if (directoryName.Length > 0)
{
Directory.CreateDirectory(directoryName);
@@ -93,7 +93,7 @@ namespace NzbDrone.Common
// Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size
// of the file, but does not waste memory.
// The "using" will close the stream even if an exception occurs.
using (FileStream streamWriter = File.Create(fullZipToPath))
using (var streamWriter = File.Create(fullZipToPath))
{
StreamUtils.Copy(zipStream, streamWriter, buffer);
}
@@ -106,7 +106,7 @@ namespace NzbDrone.Common
Stream inStream = File.OpenRead(compressedFile);
Stream gzipStream = new GZipInputStream(inStream);
TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream, null);
var tarArchive = TarArchive.CreateInputTarArchive(gzipStream, null);
tarArchive.ExtractContents(destination);
tarArchive.Close();

View File

@@ -47,8 +47,7 @@ namespace NzbDrone.Common.Cache
public T Find(string key)
{
CacheItem cacheItem;
if (!_store.TryGetValue(key, out cacheItem))
if (!_store.TryGetValue(key, out var cacheItem))
{
return default(T);
}
@@ -76,8 +75,7 @@ namespace NzbDrone.Common.Cache
public void Remove(string key)
{
CacheItem value;
_store.TryRemove(key, out value);
_store.TryRemove(key, out _);
}
public int Count => _store.Count;
@@ -88,9 +86,7 @@ namespace NzbDrone.Common.Cache
lifeTime = lifeTime ?? _defaultLifeTime;
CacheItem cacheItem;
if (_store.TryGetValue(key, out cacheItem) && !cacheItem.IsExpired())
if (_store.TryGetValue(key, out var cacheItem) && !cacheItem.IsExpired())
{
if (_rollingExpiry && lifeTime.HasValue)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
@@ -86,9 +86,7 @@ namespace NzbDrone.Common.Cache
{
RefreshIfExpired();
TValue result;
if (!_items.TryGetValue(key, out result))
if (!_items.TryGetValue(key, out var result))
{
throw new KeyNotFoundException(string.Format("Item {0} not found in cache.", key));
}
@@ -100,9 +98,7 @@ namespace NzbDrone.Common.Cache
{
RefreshIfExpired();
TValue result;
_items.TryGetValue(key, out result);
_items.TryGetValue(key, out var result);
return result;
}
@@ -128,8 +124,7 @@ namespace NzbDrone.Common.Cache
public void Remove(string key)
{
TValue item;
_items.TryRemove(key, out item);
_items.TryRemove(key, out _);
}
}
}

View File

@@ -264,6 +264,11 @@ namespace NzbDrone.Common.Disk
protected virtual void MoveFileInternal(string source, string destination)
{
if (File.Exists(destination))
{
throw new FileAlreadyExistsException("File already exists", destination);
}
File.Move(source, destination);
}

View File

@@ -500,9 +500,13 @@ namespace NzbDrone.Common.Disk
throw new IOException(string.Format("File move incomplete, data loss may have occurred. [{0}] was {1} bytes long instead of the expected {2}.", targetPath, targetSize, originalSize));
}
}
catch
catch (Exception ex)
{
RollbackPartialMove(sourcePath, targetPath);
if (ex is not FileAlreadyExistsException)
{
RollbackPartialMove(sourcePath, targetPath);
}
throw;
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace NzbDrone.Common.Disk
{
public class FileAlreadyExistsException : Exception
{
public string Filename { get; set; }
public FileAlreadyExistsException(string message, string filename)
: base(message)
{
Filename = filename;
}
}
}

View File

@@ -255,7 +255,7 @@ namespace NzbDrone.Common.Disk
var stringComparison = (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
for (int i = 0; i < leftFragments.Length; i++)
for (var i = 0; i < leftFragments.Length; i++)
{
if (!string.Equals(leftFragments[i], rightFragments[i], stringComparison))
{
@@ -372,12 +372,12 @@ namespace NzbDrone.Common.Disk
var newFragments = new List<string>();
for (int j = i; j < rightFragments.Length; j++)
for (var j = i; j < rightFragments.Length; j++)
{
newFragments.Add("..");
}
for (int j = i; j < leftFragments.Length; j++)
for (var j = i; j < leftFragments.Length; j++)
{
newFragments.Add(leftFragments[j]);
}

View File

@@ -36,14 +36,14 @@ namespace NzbDrone.Common.Extensions
public static bool IsValidDate(this string dateTime)
{
DateTime.TryParse(dateTime, out DateTime result);
DateTime.TryParse(dateTime, out var result);
return !result.Equals(default(DateTime));
}
public static bool IsFutureDate(this string dateTime)
{
DateTime.TryParse(dateTime, out DateTime result);
DateTime.TryParse(dateTime, out var result);
return !result.Equals(default(DateTime)) && result.After(DateTime.Now);
}

View File

@@ -126,9 +126,9 @@ namespace NzbDrone.Common.Extensions
private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source, int n)
{
Queue<T> buffer = new Queue<T>(n + 1);
var buffer = new Queue<T>(n + 1);
foreach (T x in source)
foreach (var x in source)
{
buffer.Enqueue(x);

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Common.Extensions
return text.Length * costDelete;
}
int[] matrix = new int[other.Length + 1];
var matrix = new int[other.Length + 1];
for (var i = 1; i < matrix.Length; i++)
{
@@ -30,13 +30,13 @@ namespace NzbDrone.Common.Extensions
for (var i = 0; i < text.Length; i++)
{
int topLeft = matrix[0];
var topLeft = matrix[0];
matrix[0] = matrix[0] + costDelete;
for (var j = 0; j < other.Length; j++)
{
int top = matrix[j];
int left = matrix[j + 1];
var top = matrix[j];
var left = matrix[j + 1];
var sumIns = top + costInsert;
var sumDel = left + costDelete;

View File

@@ -198,13 +198,13 @@ namespace NzbDrone.Common.Extensions
public static string CleanFileName(this string name)
{
string result = name;
var result = name;
string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" };
string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" };
result = result.Replace(": ", " - ");
for (int i = 0; i < badCharacters.Length; i++)
for (var i = 0; i < badCharacters.Length; i++)
{
result = result.Replace(badCharacters[i], goodCharacters[i]);
}

View File

@@ -6,9 +6,7 @@ namespace NzbDrone.Common.Extensions
{
public static int? ParseInt32(this string source)
{
int result;
if (int.TryParse(source, out result))
if (int.TryParse(source, out var result))
{
return result;
}
@@ -18,9 +16,7 @@ namespace NzbDrone.Common.Extensions
public static long? ParseInt64(this string source)
{
long result;
if (long.TryParse(source, out result))
if (long.TryParse(source, out var result))
{
return result;
}
@@ -30,9 +26,7 @@ namespace NzbDrone.Common.Extensions
public static double? ParseDouble(this string source)
{
double result;
if (double.TryParse(source.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out result))
if (double.TryParse(source.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out var result))
{
return result;
}

View File

@@ -8,9 +8,9 @@ namespace NzbDrone.Common
{
public static string CalculateCrc(string input)
{
uint mCrc = 0xffffffff;
byte[] bytes = Encoding.UTF8.GetBytes(input);
foreach (byte myByte in bytes)
var mCrc = 0xffffffff;
var bytes = Encoding.UTF8.GetBytes(input);
foreach (var myByte in bytes)
{
mCrc ^= (uint)myByte << 24;
for (var i = 0; i < 8; i++)

View File

@@ -127,7 +127,7 @@ namespace NzbDrone.Common.Http.Dispatchers
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
CookieContainer responseCookies = new CookieContainer();
var responseCookies = new CookieContainer();
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
{

View File

@@ -91,6 +91,7 @@ namespace NzbDrone.Common.Http
{
request.Method = HttpMethod.Get;
request.ContentData = null;
request.ContentSummary = null;
}
// Save to add to final response

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Common.Http
public HttpUri(string scheme, string host, int? port, string path, string query, string fragment)
{
StringBuilder builder = new StringBuilder();
var builder = new StringBuilder();
if (scheme.IsNotNullOrWhiteSpace())
{

View File

@@ -31,7 +31,7 @@ namespace NzbDrone.Common.Http.Proxy
if (!string.IsNullOrWhiteSpace(BypassFilter))
{
var hostlist = BypassFilter.Split(',');
for (int i = 0; i < hostlist.Length; i++)
for (var i = 0; i < hostlist.Length; i++)
{
if (hostlist[i].StartsWith("*"))
{

View File

@@ -9,61 +9,61 @@ namespace NzbDrone.Common.Instrumentation
{
private static readonly Regex[] CleansingRules =
{
// Url
new Regex(@"(?<=[?&: ;])(apikey|api_key|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pid|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?<secret>[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Url
new (@"(?<=[?&: ;])(apikey|api_key|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pid|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"rss(24h)?\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?<secret>[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// UNIT3D
new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// UNIT3D
new (@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path
new (@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"""/(home|Users)/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
// NzbGet
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// NzbGet
new (@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Sabnzbd
new Regex(@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Sabnzbd
new (@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// uTorrent
new Regex(@"\[""[a-z._]*(username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// uTorrent
new (@"\[""[a-z._]*(username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Deluge
new Regex(@"auth.login\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Deluge
new (@"auth.login\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// BroadcastheNet (;torrent_pass|torrents_notify_ is for MTV)
new Regex(@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"getTorrents\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&|;|=)(authkey|torrent_pass|torrents_notify)[_=](?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// BroadcastheNet (;torrent_pass|torrents_notify_ is for MTV)
new (@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"getTorrents\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?<=\?|&|;|=)(authkey|torrent_pass|torrents_notify)[_=](?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Plex
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Plex
new (@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Indexer Responses
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""token"":""(?<secret>[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Indexer Responses
new (@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"""token"":""(?<secret>[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
};
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
public static string Cleanse(string message)
{
@@ -75,15 +75,15 @@ namespace NzbDrone.Common.Instrumentation
foreach (var regex in CleansingRules)
{
message = regex.Replace(message, m =>
{
var value = m.Value;
foreach (var capture in m.Groups["secret"].Captures.OfType<Capture>().Reverse())
{
var value = m.Value;
foreach (var capture in m.Groups["secret"].Captures.OfType<Capture>().Reverse())
{
value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)");
}
value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)");
}
return value;
});
return value;
});
}
message = CleanseRemoteIPRegex.Replace(message, CleanseRemoteIP);

View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Common.Instrumentation
@@ -16,7 +16,7 @@ namespace NzbDrone.Common.Instrumentation
}
}
foreach (JToken token in json)
foreach (var token in json)
{
Visit(token);
}

View File

@@ -94,7 +94,7 @@ namespace NzbDrone.Common.Instrumentation
private static void RegisterDebugger()
{
DebuggerTarget target = new DebuggerTarget();
var target = new DebuggerTarget();
target.Name = "debuggerLogger";
target.Layout = "[${level}] [${threadid}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
@@ -108,9 +108,10 @@ namespace NzbDrone.Common.Instrumentation
{
LogManager.Setup().LoadConfiguration(c =>
{
c.ForLogger("System.*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
c.ForLogger("System*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal);
});
}

View File

@@ -29,7 +29,7 @@ namespace NzbDrone.Common.OAuth
public virtual string Version { get; set; }
public virtual string SessionHandle { get; set; }
/// <seealso cref="http://oauth.net/core/1.0#request_urls"/>
/// <seealso href="http://oauth.net/core/1.0#request_urls"/>
public virtual string RequestUrl { get; set; }
#if !WINRT

View File

@@ -313,7 +313,7 @@ namespace NzbDrone.Common.Processes
processInfo = new ProcessInfo();
processInfo.Id = process.Id;
processInfo.Name = process.ProcessName;
processInfo.StartPath = GetExeFileName(process);
processInfo.StartPath = process.MainModule.FileName;
if (process.Id != GetCurrentProcessId() && process.HasExited)
{
@@ -328,28 +328,9 @@ namespace NzbDrone.Common.Processes
return processInfo;
}
private static string GetExeFileName(Process process)
{
if (process.MainModule.FileName != "mono.exe")
{
return process.MainModule.FileName;
}
return process.Modules.Cast<ProcessModule>().FirstOrDefault(module => module.ModuleName.ToLower().EndsWith(".exe")).FileName;
}
private List<Process> GetProcessesByName(string name)
{
//TODO: move this to an OS specific class
var monoProcesses = Process.GetProcessesByName("mono")
.Union(Process.GetProcessesByName("mono-sgen"))
.Where(process =>
process.Modules.Cast<ProcessModule>()
.Any(module =>
module.ModuleName.ToLower() == name.ToLower() + ".exe"));
var processes = Process.GetProcessesByName(name)
.Union(monoProcesses).ToList();
var processes = Process.GetProcessesByName(name).ToList();
_logger.Debug("Found {0} processes with the name: {1}", processes.Count, name);

View File

@@ -8,8 +8,8 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="NLog" Version="5.2.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" />
<PackageReference Include="Npgsql" Version="5.0.11" />
<PackageReference Include="Sentry" Version="3.29.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />

View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Linq;
namespace NzbDrone.Common.Serializer
{
@@ -60,7 +60,7 @@ namespace NzbDrone.Common.Serializer
public virtual void Visit(JArray json)
{
foreach (JToken token in json)
foreach (var token in json)
{
Visit(token);
}
@@ -72,7 +72,7 @@ namespace NzbDrone.Common.Serializer
public virtual void Visit(JObject json)
{
foreach (JProperty property in json.Properties())
foreach (var property in json.Properties())
{
Visit(property);
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Text;
using Newtonsoft.Json;
@@ -42,7 +42,7 @@ namespace NzbDrone.Common.Serializer
var enumText = value.ToString();
var builder = new StringBuilder(enumText.Length + 4);
builder.Append(char.ToLower(enumText[0]));
for (int i = 1; i < enumText.Length; i++)
for (var i = 1; i < enumText.Length; i++)
{
if (char.IsUpper(enumText[i]))
{

View File

@@ -18,7 +18,7 @@ namespace NzbDrone.Common.Serializer
{
try
{
Version v = new Version(reader.GetString());
var v = new Version(reader.GetString());
return v;
}
catch (Exception)

View File

@@ -137,7 +137,7 @@ namespace NzbDrone.Common.TPL
/// <returns>An enumerable of the tasks currently scheduled.</returns>
protected sealed override IEnumerable<Task> GetScheduledTasks()
{
bool lockTaken = false;
var lockTaken = false;
try
{
Monitor.TryEnter(_tasks, ref lockTaken);

View File

@@ -110,7 +110,7 @@ namespace NzbDrone.Console
}
System.Console.WriteLine("Non-recoverable failure, waiting for user intervention...");
for (int i = 0; i < 3600; i++)
for (var i = 0; i < 3600; i++)
{
System.Threading.Thread.Sleep(1000);
if (!System.Console.IsInputRedirected && System.Console.KeyAvailable)

View File

@@ -197,7 +197,7 @@ namespace NzbDrone.Core.Test.Datastore
Subject.SetFields(_basicList, x => x.Interval);
for (int i = 0; i < _basicList.Count; i++)
for (var i = 0; i < _basicList.Count; i++)
{
_basicList[i].LastExecution = executionBackup[i];
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
using FluentAssertions;
@@ -57,7 +57,7 @@ namespace NzbDrone.Core.Test
[Test]
public void ToBestDateTime_DayOfWeek()
{
for (int i = 2; i < 7; i++)
for (var i = 2; i < 7; i++)
{
var dateTime = DateTime.Today.AddDays(i);

View File

@@ -6,6 +6,7 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.IndexerTests
{
@@ -31,13 +32,15 @@ namespace NzbDrone.Core.Test.IndexerTests
Mocker.SetConstant<IIndexerRepository>(repo);
var existingIndexers = Builder<IndexerDefinition>.CreateNew().BuildNew();
existingIndexers.ConfigContract = typeof(NewznabSettings).Name;
existingIndexers.ConfigContract = nameof(NewznabSettings);
repo.Insert(existingIndexers);
Subject.Handle(new ApplicationStartedEvent());
AllStoredModels.Should().NotContain(c => c.Id == existingIndexers.Id);
ExceptionVerification.ExpectedWarns(1);
}
}
}

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
{
var authResponse = new PassThePopcornAuthResponse { Result = "Ok" };
System.IO.StringWriter authStream = new System.IO.StringWriter();
var authStream = new System.IO.StringWriter();
Json.Serialize(authResponse, authStream);
var responseJson = ReadAllText(fileName);

View File

@@ -17,6 +17,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
{
[Obsolete("Rarbg has shutdown 2023-05-31")]
[TestFixture]
public class RarbgFixture : CoreTest<Rarbg>
{

View File

@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.InstrumentationTests
public void write_long_log()
{
var message = string.Empty;
for (int i = 0; i < 100; i++)
for (var i = 0; i < 100; i++)
{
message += Guid.NewGuid();
}

View File

@@ -63,5 +63,22 @@ namespace NzbDrone.Core.Test.ParserTests
{
ParseUtil.GetLongFromString(original).Should().Be(parsedInt);
}
[TestCase("tt0183790", "tt0183790")]
[TestCase("0183790", "tt0183790")]
[TestCase("183790", "tt0183790")]
[TestCase("tt10001870", "tt10001870")]
[TestCase("10001870", "tt10001870")]
[TestCase("tt", null)]
[TestCase("tt0", null)]
[TestCase("abc", null)]
[TestCase("abc0", null)]
[TestCase("0", null)]
[TestCase("", null)]
[TestCase(null, null)]
public void should_parse_full_imdb_id_from_string(string input, string expected)
{
ParseUtil.GetFullImdbId(input).Should().Be(expected);
}
}
}

View File

@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Annotations
public string Label { get; set; }
public string Unit { get; set; }
public string HelpText { get; set; }
public string HelpTextWarning { get; set; }
public string HelpLink { get; set; }
public FieldType Type { get; set; }
public bool Advanced { get; set; }

View File

@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Applications
yield return new ApplicationDefinition
{
Name = GetType().Name,
SyncLevel = ApplicationSyncLevel.AddOnly,
SyncLevel = ApplicationSyncLevel.FullSync,
Implementation = GetType().Name,
Settings = config
};

View File

@@ -6,6 +6,6 @@ namespace NzbDrone.Core.Applications
{
public ApplicationSyncLevel SyncLevel { get; set; }
public override bool Enable => SyncLevel == ApplicationSyncLevel.AddOnly || SyncLevel == ApplicationSyncLevel.FullSync;
public override bool Enable => SyncLevel is ApplicationSyncLevel.AddOnly or ApplicationSyncLevel.FullSync;
}
}

View File

@@ -48,8 +48,7 @@ namespace NzbDrone.Core.Applications
foreach (var application in applications)
{
ApplicationStatus blockedApplicationStatus;
if (blockedApplications.TryGetValue(application.Definition.Id, out blockedApplicationStatus))
if (blockedApplications.TryGetValue(application.Definition.Id, out var blockedApplicationStatus))
{
_logger.Debug("Temporarily ignoring application {0} till {1} due to recent failures.", application.Definition.Name, blockedApplicationStatus.DisabledTill.Value.ToLocalTime());
continue;

View File

@@ -78,6 +78,14 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol);
var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
if (remoteIndexer == null)
{
_logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id);
return;
}
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
@@ -49,10 +48,10 @@ namespace NzbDrone.Core.Applications.Lidarr
{
failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
}
catch (WebException ex)
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Lidarr"));
failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Lidarr. {ex.Message}"));
}
return new ValidationResult(failures);
@@ -61,7 +60,7 @@ namespace NzbDrone.Core.Applications.Lidarr
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _lidarrV1Proxy.GetIndexers(Settings)
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
.Where(i => i.Implementation is "Newznab" or "Torznab");
var mappings = new List<AppIndexerMap>();
@@ -96,6 +95,14 @@ namespace NzbDrone.Core.Applications.Lidarr
var lidarrIndexer = BuildLidarrIndexer(indexer, indexer.Protocol);
var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings);
if (remoteIndexer == null)
{
_logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id);
return;
}
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
@@ -174,7 +181,13 @@ namespace NzbDrone.Core.Applications.Lidarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
if (id == 0)
{
// Ensuring backward compatibility with older versions on first sync
syncFields.AddRange(new List<string> { "earlyReleaseLimit", "additionalParameters" });
}
var newznab = schemas.First(i => i.Implementation == "Newznab");
var torznab = schemas.First(i => i.Implementation == "Torznab");

View File

@@ -23,8 +23,11 @@ namespace NzbDrone.Core.Applications.Lidarr
public class LidarrV1Proxy : ILidarrV1Proxy
{
private static Version MinimumApplicationVersion => new (1, 0, 2, 0);
private const string AppApiRoute = "/api/v1";
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
@@ -102,7 +105,17 @@ namespace NzbDrone.Core.Applications.Lidarr
try
{
Execute<LidarrIndexer>(request);
var applicationVersion = _httpClient.Post<LidarrIndexer>(request).Headers.GetSingleValue("X-Application-Version");
if (applicationVersion == null)
{
return new ValidationFailure(string.Empty, "Failed to fetch Lidarr version");
}
if (new Version(applicationVersion) < MinimumApplicationVersion)
{
return new ValidationFailure(string.Empty, $"Lidarr version should be at least {MinimumApplicationVersion.ToString(3)}. Version reported is {applicationVersion}", applicationVersion);
}
}
catch (HttpException ex)
{

View File

@@ -78,6 +78,14 @@ namespace NzbDrone.Core.Applications.Mylar
var mylarIndexer = BuildMylarIndexer(indexer, indexer.Protocol);
var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
if (remoteIndexer == null)
{
_logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id);
return;
}
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
@@ -49,10 +48,10 @@ namespace NzbDrone.Core.Applications.Radarr
{
failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
}
catch (WebException ex)
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Radarr"));
failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Radarr. {ex.Message}"));
}
return new ValidationResult(failures);
@@ -61,7 +60,7 @@ namespace NzbDrone.Core.Applications.Radarr
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _radarrV3Proxy.GetIndexers(Settings)
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
.Where(i => i.Implementation is "Newznab" or "Torznab");
var mappings = new List<AppIndexerMap>();
@@ -96,6 +95,14 @@ namespace NzbDrone.Core.Applications.Radarr
var radarrIndexer = BuildRadarrIndexer(indexer, indexer.Protocol);
var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings);
if (remoteIndexer == null)
{
_logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id);
return;
}
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
@@ -174,7 +181,13 @@ namespace NzbDrone.Core.Applications.Radarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
if (id == 0)
{
// Ensuring backward compatibility with older versions on first sync
syncFields.AddRange(new List<string> { "multiLanguages", "removeYear", "requiredFlags", "additionalParameters" });
}
var newznab = schemas.First(i => i.Implementation == "Newznab");
var torznab = schemas.First(i => i.Implementation == "Torznab");

View File

@@ -23,8 +23,12 @@ namespace NzbDrone.Core.Applications.Radarr
public class RadarrV3Proxy : IRadarrV3Proxy
{
private static Version MinimumApplicationV4Version => new (4, 0, 4, 0);
private static Version MinimumApplicationV3Version => new (3, 1, 1, 0);
private const string AppApiRoute = "/api/v3";
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
@@ -102,7 +106,29 @@ namespace NzbDrone.Core.Applications.Radarr
try
{
Execute<RadarrIndexer>(request);
var applicationVersion = _httpClient.Post<RadarrIndexer>(request).Headers.GetSingleValue("X-Application-Version");
if (applicationVersion == null)
{
return new ValidationFailure(string.Empty, "Failed to fetch Radarr version");
}
var version = new Version(applicationVersion);
if (version.Major == 3)
{
if (version < MinimumApplicationV3Version)
{
return new ValidationFailure(string.Empty, $"Radarr version should be at least {MinimumApplicationV3Version.ToString(3)}. Version reported is {applicationVersion}", applicationVersion);
}
}
else
{
if (version < MinimumApplicationV4Version)
{
return new ValidationFailure(string.Empty, $"Radarr version should be at least {MinimumApplicationV4Version.ToString(3)}. Version reported is {applicationVersion}", applicationVersion);
}
}
}
catch (HttpException ex)
{

View File

@@ -96,6 +96,14 @@ namespace NzbDrone.Core.Applications.Readarr
var readarrIndexer = BuildReadarrIndexer(indexer, indexer.Protocol);
var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings);
if (remoteIndexer == null)
{
_logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id);
return;
}
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
@@ -49,10 +48,10 @@ namespace NzbDrone.Core.Applications.Sonarr
{
failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
}
catch (WebException ex)
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Sonarr"));
failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Sonarr. {ex.Message}"));
}
return new ValidationResult(failures);
@@ -61,7 +60,7 @@ namespace NzbDrone.Core.Applications.Sonarr
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _sonarrV3Proxy.GetIndexers(Settings)
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
.Where(i => i.Implementation is "Newznab" or "Torznab");
var mappings = new List<AppIndexerMap>();
@@ -97,6 +96,14 @@ namespace NzbDrone.Core.Applications.Sonarr
var sonarrIndexer = BuildSonarrIndexer(indexer, indexer.Protocol);
var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings);
if (remoteIndexer == null)
{
_logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id);
return;
}
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
@@ -176,7 +183,13 @@ namespace NzbDrone.Core.Applications.Sonarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
if (id == 0)
{
// Ensuring backward compatibility with older versions on first sync
syncFields.AddRange(new List<string> { "animeStandardFormatSearch", "additionalParameters" });
}
var newznab = schemas.First(i => i.Implementation == "Newznab");
var torznab = schemas.First(i => i.Implementation == "Torznab");

View File

@@ -23,8 +23,11 @@ namespace NzbDrone.Core.Applications.Sonarr
public class SonarrV3Proxy : ISonarrV3Proxy
{
private static Version MinimumApplicationVersion => new (3, 0, 5, 0);
private const string AppApiRoute = "/api/v3";
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
@@ -102,7 +105,17 @@ namespace NzbDrone.Core.Applications.Sonarr
try
{
Execute<SonarrIndexer>(request);
var applicationVersion = _httpClient.Post<SonarrIndexer>(request).Headers.GetSingleValue("X-Application-Version");
if (applicationVersion == null)
{
return new ValidationFailure(string.Empty, "Failed to fetch Sonarr version");
}
if (new Version(applicationVersion) < MinimumApplicationVersion)
{
return new ValidationFailure(string.Empty, $"Sonarr version should be at least {MinimumApplicationVersion.ToString(3)}. Version reported is {applicationVersion}", applicationVersion);
}
}
catch (HttpException ex)
{

View File

@@ -96,6 +96,14 @@ namespace NzbDrone.Core.Applications.Whisparr
var whisparrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol);
var remoteIndexer = _whisparrV3Proxy.AddIndexer(whisparrIndexer, Settings);
if (remoteIndexer == null)
{
_logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id);
return;
}
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}

View File

@@ -121,8 +121,7 @@ namespace NzbDrone.Core.Configuration
continue;
}
object currentValue;
allWithDefaults.TryGetValue(configValue.Key, out currentValue);
allWithDefaults.TryGetValue(configValue.Key, out var currentValue);
if (currentValue == null)
{
continue;
@@ -145,7 +144,7 @@ namespace NzbDrone.Core.Configuration
{
const string defaultValue = "*";
string bindAddress = GetValue("BindAddress", defaultValue);
var bindAddress = GetValue("BindAddress", defaultValue);
if (string.IsNullOrWhiteSpace(bindAddress))
{
return defaultValue;

View File

@@ -53,8 +53,7 @@ namespace NzbDrone.Core.Configuration
foreach (var configValue in configValues)
{
object currentValue;
allWithDefaults.TryGetValue(configValue.Key, out currentValue);
allWithDefaults.TryGetValue(configValue.Key, out var currentValue);
if (currentValue == null || configValue.Value == null)
{
continue;
@@ -211,9 +210,7 @@ namespace NzbDrone.Core.Configuration
EnsureCache();
string dbValue;
if (_cache.TryGetValue(key, out dbValue) && dbValue != null && !string.IsNullOrEmpty(dbValue))
if (_cache.TryGetValue(key, out var dbValue) && dbValue != null && !string.IsNullOrEmpty(dbValue))
{
return dbValue;
}

View File

@@ -196,7 +196,7 @@ namespace NzbDrone.Core.Datastore
using (var conn = _database.OpenConnection())
{
using (IDbTransaction tran = conn.BeginTransaction(IsolationLevel.ReadCommitted))
using (var tran = conn.BeginTransaction(IsolationLevel.ReadCommitted))
{
foreach (var model in models)
{

View File

@@ -18,7 +18,7 @@ namespace NzbDrone.Core.Datastore.Converters
}
string contract;
using (JsonDocument body = JsonDocument.Parse(stringValue))
using (var body = JsonDocument.Parse(stringValue))
{
contract = body.RootElement.GetProperty("name").GetString();
}

View File

@@ -201,7 +201,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
public virtual IList<TableDefinition> ReadDbSchema()
{
IList<TableDefinition> tables = ReadTables();
var tables = ReadTables();
foreach (var table in tables)
{
table.Indexes = ReadIndexes(table.SchemaName, table.Name);
@@ -264,7 +264,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
protected virtual IList<IndexDefinition> ReadIndexes(string schemaName, string tableName)
{
var sqlCommand = string.Format(@"SELECT type, name, sql FROM sqlite_master WHERE tbl_name = '{0}' AND type = 'index' AND name NOT LIKE 'sqlite_auto%';", tableName);
DataTable table = Read(sqlCommand).Tables[0];
var table = Read(sqlCommand).Tables[0];
IList<IndexDefinition> indexes = new List<IndexDefinition>();

Some files were not shown because too many files have changed in this diff Show More