mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
148 Commits
v1.5.1.342
...
v1.7.1.368
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
193335e2a8 | ||
|
|
1c98727cf3 | ||
|
|
ab5b321385 | ||
|
|
96340909f1 | ||
|
|
bd6a37dc8c | ||
|
|
a663cebada | ||
|
|
2ce5618499 | ||
|
|
94c91d4c3f | ||
|
|
79fbb2d0d7 | ||
|
|
e2e52746bb | ||
|
|
21cc96d683 | ||
|
|
e68b45636e | ||
|
|
ce68fe4105 | ||
|
|
712404ddca | ||
|
|
826828e8ec | ||
|
|
252740519f | ||
|
|
062fd77e1b | ||
|
|
6769055b6b | ||
|
|
90e92c0b66 | ||
|
|
7eac11f57a | ||
|
|
02a3c1b224 | ||
|
|
57efa6d0b1 | ||
|
|
cee52147bc | ||
|
|
a1abcd6c93 | ||
|
|
18e2757d37 | ||
|
|
8790a6f06a | ||
|
|
4fafdb2cd2 | ||
|
|
bfc06fc8bc | ||
|
|
9f4f6a5726 | ||
|
|
d9ace9a862 | ||
|
|
95691c7476 | ||
|
|
90f2020e59 | ||
|
|
6afa1dc8ba | ||
|
|
e8139f2a5b | ||
|
|
45328db2c7 | ||
|
|
e55d6b827a | ||
|
|
34cd68fa07 | ||
|
|
aed3f9f887 | ||
|
|
6880e67507 | ||
|
|
e0e1b1494e | ||
|
|
20df31919d | ||
|
|
8785fe02e8 | ||
|
|
b2b877a8c3 | ||
|
|
0de302ad48 | ||
|
|
06391489cf | ||
|
|
8fcceb0702 | ||
|
|
f20319fff1 | ||
|
|
20bcc00662 | ||
|
|
c4af3e746f | ||
|
|
660a162b7e | ||
|
|
20a3cad7fb | ||
|
|
77fe3f78fe | ||
|
|
d777cb8e29 | ||
|
|
15e7cc7ea8 | ||
|
|
04cf061275 | ||
|
|
d4cdeac69a | ||
|
|
e60fe05ee0 | ||
|
|
9a4c23797a | ||
|
|
acfdb5bae3 | ||
|
|
e2e65627ee | ||
|
|
4b8906ea62 | ||
|
|
f0c5d8ceea | ||
|
|
427802a50e | ||
|
|
0c9eae244a | ||
|
|
75ff2f41d3 | ||
|
|
d1ba208243 | ||
|
|
4e03ebadc4 | ||
|
|
0155ff60fd | ||
|
|
f0915638f3 | ||
|
|
56eb58aed1 | ||
|
|
8a891d07cf | ||
|
|
40a932cd28 | ||
|
|
4a81630073 | ||
|
|
0ff0fe2e68 | ||
|
|
51e33740b0 | ||
|
|
119164f729 | ||
|
|
ef0f8e25fd | ||
|
|
d21debe77f | ||
|
|
a3ccc3d0cf | ||
|
|
46d930e903 | ||
|
|
4561859c2b | ||
|
|
83166fb0b5 | ||
|
|
b98f9a945d | ||
|
|
e658e3fe48 | ||
|
|
9042525f22 | ||
|
|
7b551a0af1 | ||
|
|
31c2917bad | ||
|
|
419cce53f7 | ||
|
|
48cd1d9f6b | ||
|
|
8bd6a313b7 | ||
|
|
7cb465787e | ||
|
|
0b610ff9c8 | ||
|
|
5187460298 | ||
|
|
f0d9b43480 | ||
|
|
a1081cc554 | ||
|
|
c4bb1ba69a | ||
|
|
3a4c8db98c | ||
|
|
a522796798 | ||
|
|
e012eda0cf | ||
|
|
72ab2b34c4 | ||
|
|
aaba5b7499 | ||
|
|
455b76c45c | ||
|
|
596d3297da | ||
|
|
d05128ca33 | ||
|
|
f5b57db753 | ||
|
|
f7d7cca982 | ||
|
|
7c5409383e | ||
|
|
98db8f8bf8 | ||
|
|
88e793d76d | ||
|
|
0f31af6b89 | ||
|
|
65adf30f59 | ||
|
|
da75519524 | ||
|
|
ed1fb58242 | ||
|
|
d5daf6791c | ||
|
|
1f1a345d25 | ||
|
|
76a2f51533 | ||
|
|
8c0bc9ab4e | ||
|
|
b0c2b9119b | ||
|
|
87fdf17926 | ||
|
|
0f1b466a19 | ||
|
|
ea635e685b | ||
|
|
73f23d56dc | ||
|
|
f14ccebf3a | ||
|
|
9539e4d481 | ||
|
|
e40ccc49ad | ||
|
|
9fd3eb4d6b | ||
|
|
78aab80703 | ||
|
|
868394d588 | ||
|
|
d5e5697db8 | ||
|
|
d1e39f206a | ||
|
|
b59d89f308 | ||
|
|
bf5855beb4 | ||
|
|
2d36adf865 | ||
|
|
ef1ad59f59 | ||
|
|
59b6e8af27 | ||
|
|
3ae1917d3b | ||
|
|
5864a090e4 | ||
|
|
fcfec1b859 | ||
|
|
65541017dd | ||
|
|
7fe9942c28 | ||
|
|
360827708f | ||
|
|
0509335387 | ||
|
|
f54212a809 | ||
|
|
ea0eb2efa7 | ||
|
|
ce430433e5 | ||
|
|
5437aac346 | ||
|
|
b02188acf4 | ||
|
|
6897ed0b3f |
@@ -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
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -74,7 +74,7 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
|
||||
description: Trace logs are generally required for all bug reports
|
||||
description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
|
||||
options:
|
||||
- label: I have followed the steps in the wiki link above and provided the required trace logs that are relevant and show this issue.
|
||||
- label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
|
||||
required: true
|
||||
|
||||
2
.github/label-actions.yml
vendored
2
.github/label-actions.yml
vendored
@@ -7,6 +7,7 @@
|
||||
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord)
|
||||
or [Subreddit](https://reddit.com/r/prowlarr)
|
||||
close: true
|
||||
close-reason: 'not planned'
|
||||
|
||||
'Type: Indexer Request':
|
||||
comment: >
|
||||
@@ -14,6 +15,7 @@
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a indexer request. Please use our Indexer request [site](https://requests.prowlarr.com/)
|
||||
close: true
|
||||
close-reason: 'not planned'
|
||||
|
||||
'Status: Logs Needed':
|
||||
comment: >
|
||||
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.5.1'
|
||||
majorVersion: '1.7.1'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
@@ -362,7 +362,7 @@ stages:
|
||||
- bash: |
|
||||
echo "Uploading source maps to sentry"
|
||||
curl -sL https://sentry.io/get-cli/ | bash
|
||||
RELEASENAME="${PROWLARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
|
||||
RELEASENAME="Prowlarr@${PROWLARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
|
||||
sentry-cli releases new --finalize -p prowlarr -p prowlarr-ui -p prowlarr-update "${RELEASENAME}"
|
||||
sentry-cli releases -p prowlarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
|
||||
sentry-cli releases set-commits --auto "${RELEASENAME}"
|
||||
@@ -1003,7 +1003,7 @@ stages:
|
||||
git add .
|
||||
if git status | grep -q modified
|
||||
then
|
||||
git commit -am 'Automated API Docs update'
|
||||
git commit -am 'Automated API Docs update [skip ci]'
|
||||
git push -f --set-upstream origin api-docs
|
||||
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
|
||||
else
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -23,7 +24,7 @@ function createMapStateToProps() {
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
value: translate('NoChange'),
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.downloadClients,
|
||||
(state, { includeAny }) => includeAny,
|
||||
(state, { protocol }) => protocol,
|
||||
(downloadClients, includeAny, protocolFilter) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = downloadClients;
|
||||
|
||||
const values = items
|
||||
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
|
||||
.sort(sortByName)
|
||||
.map((downloadClient) => ({
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name
|
||||
}));
|
||||
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchDownloadClients: fetchDownloadClients
|
||||
};
|
||||
|
||||
class DownloadClientSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchDownloadClients();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value: parseInt(value) });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadClientSelectInputConnector.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeAny: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dispatchFetchDownloadClients: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DownloadClientSelectInputConnector.defaultProps = {
|
||||
includeAny: false,
|
||||
protocol: 'torrent'
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);
|
||||
@@ -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,
|
||||
|
||||
@@ -10,6 +10,7 @@ import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
import DeviceInputConnector from './DeviceInputConnector';
|
||||
import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
@@ -72,6 +73,9 @@ function getComponent(type) {
|
||||
case inputTypes.CATEGORY_SELECT:
|
||||
return NewznabCategorySelectInputConnector;
|
||||
|
||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||
return DownloadClientSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||
return IndexerFlagsSelectInputConnector;
|
||||
|
||||
@@ -258,6 +262,8 @@ FormInputGroup.propTypes = {
|
||||
values: PropTypes.arrayOf(PropTypes.any),
|
||||
type: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
unit: PropTypes.string,
|
||||
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||
helpText: PropTypes.string,
|
||||
|
||||
@@ -12,7 +12,7 @@ function createMapStateToProps() {
|
||||
(state) => state.indexers,
|
||||
(value, indexers) => {
|
||||
const values = [];
|
||||
const groupedIndexers = _(indexers.items).groupBy((x) => x.protocol).map((val, key) => ({ protocol: key, indexers: val })).value();
|
||||
const groupedIndexers = _.map(_.groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
||||
|
||||
groupedIndexers.forEach((element) => {
|
||||
values.push({
|
||||
@@ -21,10 +21,11 @@ function createMapStateToProps() {
|
||||
});
|
||||
|
||||
if (element.indexers && element.indexers.length > 0) {
|
||||
element.indexers.forEach((subCat) => {
|
||||
element.indexers.forEach((indexer) => {
|
||||
values.push({
|
||||
key: subCat.id,
|
||||
value: subCat.name,
|
||||
key: indexer.id,
|
||||
value: indexer.name,
|
||||
isDisabled: !indexer.enable,
|
||||
parentKey: element.protocol === 'usenet' ? -1 : -2
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,8 @@ class VirtualTable extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0
|
||||
width: 0,
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
@@ -48,20 +49,25 @@ class VirtualTable extends Component {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
scrollIndex
|
||||
scrollIndex,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width
|
||||
width,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (this._grid &&
|
||||
(prevState.width !== width ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: scrollIndex,
|
||||
@@ -98,6 +104,7 @@ class VirtualTable extends Component {
|
||||
focusScroller,
|
||||
header,
|
||||
headerHeight,
|
||||
rowHeight,
|
||||
rowRenderer,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -141,6 +148,7 @@ class VirtualTable extends Component {
|
||||
{header}
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
{...otherProps}
|
||||
ref={this.setGridRef}
|
||||
autoContainerWidth={true}
|
||||
autoHeight={true}
|
||||
@@ -148,7 +156,7 @@ class VirtualTable extends Component {
|
||||
width={width}
|
||||
height={height}
|
||||
headerHeight={height - headerHeight}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowHeight={rowHeight}
|
||||
rowCount={items.length}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
@@ -162,7 +170,6 @@ class VirtualTable extends Component {
|
||||
className={styles.tableBodyContainer}
|
||||
style={gridStyle}
|
||||
containerStyle={containerStyle}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
</Scroller>
|
||||
@@ -180,16 +187,19 @@ VirtualTable.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
scrollIndex: PropTypes.number,
|
||||
scrollTop: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
focusScroller: PropTypes.bool.isRequired,
|
||||
header: PropTypes.node.isRequired,
|
||||
headerHeight: PropTypes.number.isRequired,
|
||||
rowRenderer: PropTypes.func.isRequired
|
||||
rowRenderer: PropTypes.func.isRequired,
|
||||
rowHeight: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
VirtualTable.defaultProps = {
|
||||
className: styles.tableContainer,
|
||||
headerHeight: 38,
|
||||
rowHeight: ROW_HEIGHT,
|
||||
focusScroller: true
|
||||
};
|
||||
|
||||
|
||||
@@ -67,8 +67,10 @@ function keyboardShortcuts(WrappedComponent) {
|
||||
};
|
||||
|
||||
unbindShortcut = (key) => {
|
||||
delete this._mousetrapBindings[key];
|
||||
this._mousetrap.unbind(key);
|
||||
if (this._mousetrap != null) {
|
||||
delete this._mousetrapBindings[key];
|
||||
this._mousetrap.unbind(key);
|
||||
}
|
||||
};
|
||||
|
||||
unbindAllShortcuts = () => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
11
frontend/src/Helpers/Hooks/usePrevious.tsx
Normal file
11
frontend/src/Helpers/Hooks/usePrevious.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
113
frontend/src/Helpers/Hooks/useSelectState.tsx
Normal file
113
frontend/src/Helpers/Hooks/useSelectState.tsx
Normal 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];
|
||||
}
|
||||
@@ -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' }
|
||||
]
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ export const KEY_VALUE_LIST = 'keyValueList';
|
||||
export const INFO = 'info';
|
||||
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
||||
export const CATEGORY_SELECT = 'newznabCategorySelect';
|
||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||
export const NUMBER = 'number';
|
||||
export const OAUTH = 'oauth';
|
||||
export const PASSWORD = 'password';
|
||||
|
||||
@@ -18,6 +18,8 @@ function HistoryDetails(props) {
|
||||
query,
|
||||
queryResults,
|
||||
categories,
|
||||
limit,
|
||||
offset,
|
||||
source,
|
||||
url
|
||||
} = data;
|
||||
@@ -31,43 +33,66 @@ function HistoryDetails(props) {
|
||||
/>
|
||||
|
||||
{
|
||||
!!indexer &&
|
||||
indexer ?
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer.name}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!data &&
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('QueryResults')}
|
||||
data={queryResults ? queryResults : '-'}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!data &&
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Categories')}
|
||||
data={categories ? categories : '-'}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!data &&
|
||||
limit ?
|
||||
<DescriptionListItem
|
||||
title={translate('Limit')}
|
||||
data={limit}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
offset ?
|
||||
<DescriptionListItem
|
||||
title={translate('Offset')}
|
||||
data={offset}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Source')}
|
||||
data={source}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!data &&
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Url')}
|
||||
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
@@ -76,42 +101,46 @@ function HistoryDetails(props) {
|
||||
if (eventType === 'releaseGrabbed') {
|
||||
const {
|
||||
source,
|
||||
title,
|
||||
grabTitle,
|
||||
url
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
{
|
||||
!!indexer &&
|
||||
indexer ?
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer.name}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!data &&
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Source')}
|
||||
data={source ? source : '-'}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!data &&
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Title')}
|
||||
data={title ? title : '-'}
|
||||
/>
|
||||
title={translate('GrabTitle')}
|
||||
data={grabTitle ? grabTitle : '-'}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!data &&
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Url')}
|
||||
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
@@ -124,11 +153,12 @@ function HistoryDetails(props) {
|
||||
title={translate('Auth')}
|
||||
>
|
||||
{
|
||||
!!indexer &&
|
||||
indexer ?
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer.name}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.parameters {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
.parametersContent {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
2
frontend/src/History/HistoryRow.css.d.ts
vendored
2
frontend/src/History/HistoryRow.css.d.ts
vendored
@@ -6,7 +6,7 @@ interface CssExports {
|
||||
'details': string;
|
||||
'elapsedTime': string;
|
||||
'indexer': string;
|
||||
'parameters': string;
|
||||
'parametersContent': string;
|
||||
'query': string;
|
||||
'releaseGroup': string;
|
||||
'source': string;
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
import * as historyDataTypes from './historyDataTypes';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import HistoryRowParameter from './HistoryRowParameter';
|
||||
import styles from './HistoryRow.css';
|
||||
|
||||
const historyParameters = [
|
||||
{ key: historyDataTypes.IMDB_ID, title: 'IMDb' },
|
||||
{ key: historyDataTypes.TMDB_ID, title: 'TMDb' },
|
||||
{ key: historyDataTypes.TVDB_ID, title: 'TVDb' },
|
||||
{ key: historyDataTypes.TRAKT_ID, title: 'Trakt' },
|
||||
{ key: historyDataTypes.R_ID, title: 'TvRage' },
|
||||
{ key: historyDataTypes.TVMAZE_ID, title: 'TvMaze' },
|
||||
{ key: historyDataTypes.SEASON, title: translate('Season') },
|
||||
{ key: historyDataTypes.EPISODE, title: translate('Episode') },
|
||||
{ key: historyDataTypes.ARTIST, title: translate('Artist') },
|
||||
{ key: historyDataTypes.ALBUM, title: translate('Album') },
|
||||
{ key: historyDataTypes.LABEL, title: translate('Label') },
|
||||
{ key: historyDataTypes.TRACK, title: translate('Track') },
|
||||
{ key: historyDataTypes.YEAR, title: translate('Year') },
|
||||
{ key: historyDataTypes.GENRE, title: translate('Genre') },
|
||||
{ key: historyDataTypes.AUTHOR, title: translate('Author') },
|
||||
{ key: historyDataTypes.TITLE, title: translate('Title') },
|
||||
{ key: historyDataTypes.PUBLISHER, title: translate('Publisher') }
|
||||
];
|
||||
|
||||
class HistoryRow extends Component {
|
||||
|
||||
//
|
||||
@@ -44,15 +66,52 @@ class HistoryRow extends Component {
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
const { query, queryType, limit, offset } = data;
|
||||
|
||||
let searchQuery = query;
|
||||
let categories = [];
|
||||
|
||||
if (data.categories) {
|
||||
categories = data.categories.split(',').map((item) => {
|
||||
return parseInt(item);
|
||||
});
|
||||
categories = data.categories.split(',').map((item) => parseInt(item));
|
||||
}
|
||||
|
||||
this.props.onSearchPress(data.query, indexer.id, categories);
|
||||
const searchParams = [
|
||||
historyDataTypes.IMDB_ID,
|
||||
historyDataTypes.TMDB_ID,
|
||||
historyDataTypes.TVDB_ID,
|
||||
historyDataTypes.TRAKT_ID,
|
||||
historyDataTypes.R_ID,
|
||||
historyDataTypes.TVMAZE_ID,
|
||||
historyDataTypes.SEASON,
|
||||
historyDataTypes.EPISODE,
|
||||
historyDataTypes.ARTIST,
|
||||
historyDataTypes.ALBUM,
|
||||
historyDataTypes.LABEL,
|
||||
historyDataTypes.TRACK,
|
||||
historyDataTypes.YEAR,
|
||||
historyDataTypes.GENRE,
|
||||
historyDataTypes.AUTHOR,
|
||||
historyDataTypes.TITLE,
|
||||
historyDataTypes.PUBLISHER
|
||||
]
|
||||
.reduce((acc, key) => {
|
||||
if (key in data && data[key].length > 0) {
|
||||
const value = data[key];
|
||||
|
||||
acc.push({ key, value });
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.map((item) => `{${item.key}:${item.value}}`)
|
||||
.join('')
|
||||
;
|
||||
|
||||
if (searchParams.length > 0) {
|
||||
searchQuery += `${searchParams}`;
|
||||
}
|
||||
|
||||
this.props.onSearchPress(searchQuery, indexer.id, categories, queryType, parseInt(limit), parseInt(offset));
|
||||
};
|
||||
|
||||
onDetailsPress = () => {
|
||||
@@ -84,6 +143,8 @@ class HistoryRow extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parameters = historyParameters.filter((parameter) => parameter.key in data && data[parameter.key]);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
@@ -133,162 +194,19 @@ class HistoryRow extends Component {
|
||||
|
||||
if (name === 'parameters') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.parameters}
|
||||
>
|
||||
{
|
||||
data.imdbId ?
|
||||
<HistoryRowParameter
|
||||
title='IMDb'
|
||||
value={data.imdbId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.tmdbId ?
|
||||
<HistoryRowParameter
|
||||
title='TMDb'
|
||||
value={data.tmdbId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.tvdbId ?
|
||||
<HistoryRowParameter
|
||||
title='TVDb'
|
||||
value={data.tvdbId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.traktId ?
|
||||
<HistoryRowParameter
|
||||
title='Trakt'
|
||||
value={data.traktId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.rId ?
|
||||
<HistoryRowParameter
|
||||
title='TvRage'
|
||||
value={data.rId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.tvMazeId ?
|
||||
<HistoryRowParameter
|
||||
title='TvMaze'
|
||||
value={data.tvMazeId}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.season ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Season')}
|
||||
value={data.season}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.episode ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Episode')}
|
||||
value={data.episode}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.artist ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Artist')}
|
||||
value={data.artist}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.album ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Album')}
|
||||
value={data.album}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.label ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Label')}
|
||||
value={data.label}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.track ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Track')}
|
||||
value={data.track}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.year ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Year')}
|
||||
value={data.year}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.genre ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Genre')}
|
||||
value={data.genre}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.author ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Author')}
|
||||
value={data.author}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.bookTitle ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Book')}
|
||||
value={data.bookTitle}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data.publisher ?
|
||||
<HistoryRowParameter
|
||||
title={translate('Publisher')}
|
||||
value={data.publisher}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
<TableRowCell key={name}>
|
||||
<div className={styles.parametersContent}>
|
||||
{parameters.map((parameter) => {
|
||||
return (
|
||||
<HistoryRowParameter
|
||||
key={parameter.key}
|
||||
title={parameter.title}
|
||||
value={data[parameter.key]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -300,8 +218,25 @@ class HistoryRow extends Component {
|
||||
className={styles.indexer}
|
||||
>
|
||||
{
|
||||
data.title ?
|
||||
data.title :
|
||||
data.grabTitle ?
|
||||
data.grabTitle :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'queryType') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.query}
|
||||
>
|
||||
{
|
||||
data.queryType ?
|
||||
<Label kind={kinds.INFO}>
|
||||
{data.queryType}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
@@ -377,6 +312,12 @@ class HistoryRow extends Component {
|
||||
key={name}
|
||||
className={styles.details}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
title={translate('HistoryDetails')}
|
||||
/>
|
||||
|
||||
{
|
||||
eventType === 'indexerQuery' ?
|
||||
<IconButton
|
||||
@@ -386,11 +327,6 @@ class HistoryRow extends Component {
|
||||
/> :
|
||||
null
|
||||
}
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
title={translate('HistoryDetails')}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { push } from 'connected-react-router';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -48,8 +49,15 @@ class HistoryRowConnector extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchPress = (term, indexerId, categories) => {
|
||||
this.props.setSearchDefault({ searchQuery: term, searchIndexerIds: [indexerId], searchCategories: categories });
|
||||
onSearchPress = (query, indexerId, categories, type, limit, offset) => {
|
||||
this.props.setSearchDefault(_.pickBy({
|
||||
searchQuery: query,
|
||||
searchIndexerIds: [indexerId],
|
||||
searchCategories: categories,
|
||||
searchType: type,
|
||||
searchLimit: limit,
|
||||
searchOffset: offset
|
||||
}));
|
||||
this.props.push(`${window.Prowlarr.urlBase}/search`);
|
||||
};
|
||||
|
||||
|
||||
17
frontend/src/History/historyDataTypes.js
Normal file
17
frontend/src/History/historyDataTypes.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export const IMDB_ID = 'imdbId';
|
||||
export const TMDB_ID = 'tmdbId';
|
||||
export const TVDB_ID = 'tvdbId';
|
||||
export const TRAKT_ID = 'traktId';
|
||||
export const R_ID = 'rId';
|
||||
export const TVMAZE_ID = 'tvMazeId';
|
||||
export const SEASON = 'season';
|
||||
export const EPISODE = 'episode';
|
||||
export const ARTIST = 'artist';
|
||||
export const ALBUM = 'album';
|
||||
export const LABEL = 'label';
|
||||
export const TRACK = 'track';
|
||||
export const YEAR = 'year';
|
||||
export const GENRE = 'genre';
|
||||
export const AUTHOR = 'author';
|
||||
export const TITLE = 'title';
|
||||
export const PUBLISHER = 'publisher';
|
||||
@@ -40,6 +40,7 @@
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
margin-right: 12px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.filterContainer:last-child {
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
|
||||
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -26,7 +27,7 @@ const columns = [
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
name: 'sortName',
|
||||
label: translate('Name'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
@@ -89,7 +90,8 @@ class AddIndexerModalContent extends Component {
|
||||
filter: '',
|
||||
filterProtocols: [],
|
||||
filterLanguages: [],
|
||||
filterPrivacyLevels: []
|
||||
filterPrivacyLevels: [],
|
||||
filterCategories: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,7 +123,13 @@ class AddIndexerModalContent extends Component {
|
||||
.map((language) => ({ key: language, value: language }));
|
||||
|
||||
const filteredIndexers = indexers.filter((indexer) => {
|
||||
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
||||
const {
|
||||
filter,
|
||||
filterProtocols,
|
||||
filterLanguages,
|
||||
filterPrivacyLevels,
|
||||
filterCategories
|
||||
} = this.state;
|
||||
|
||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
||||
return false;
|
||||
@@ -139,6 +147,18 @@ class AddIndexerModalContent extends Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterCategories.length) {
|
||||
const { categories = [] } = indexer.capabilities || {};
|
||||
const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)];
|
||||
const flatCategories = categories
|
||||
.filter((item) => item.id < 100000)
|
||||
.flatMap(flat);
|
||||
|
||||
if (!filterCategories.every((item) => flatCategories.includes(item))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -165,7 +185,7 @@ class AddIndexerModalContent extends Component {
|
||||
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>Protocol</label>
|
||||
<label className={styles.filterLabel}>{translate('Protocol')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerProtocols"
|
||||
value={this.state.filterProtocols}
|
||||
@@ -175,7 +195,7 @@ class AddIndexerModalContent extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>Language</label>
|
||||
<label className={styles.filterLabel}>{translate('Language')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerLanguages"
|
||||
value={this.state.filterLanguages}
|
||||
@@ -185,7 +205,7 @@ class AddIndexerModalContent extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>Privacy</label>
|
||||
<label className={styles.filterLabel}>{translate('Privacy')}</label>
|
||||
<EnhancedSelectInput
|
||||
name="indexerPrivacyLevels"
|
||||
value={this.state.filterPrivacyLevels}
|
||||
@@ -193,6 +213,15 @@ class AddIndexerModalContent extends Component {
|
||||
onChange={({ value }) => this.setState({ filterPrivacyLevels: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterContainer}>
|
||||
<label className={styles.filterLabel}>{translate('Categories')}</label>
|
||||
<NewznabCategorySelectInputConnector
|
||||
name="indexerCategories"
|
||||
value={this.state.filterCategories}
|
||||
onChange={({ value }) => this.setState({ filterCategories: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
@@ -212,7 +241,7 @@ class AddIndexerModalContent extends Component {
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
{
|
||||
error ? <div>{errorMessage}</div> : null
|
||||
error ? <Alert kind={kinds.DANGER}>{errorMessage}</Alert> : null
|
||||
}
|
||||
{
|
||||
isPopulated && !!indexers.length ?
|
||||
@@ -226,7 +255,7 @@ class AddIndexerModalContent extends Component {
|
||||
{
|
||||
filteredIndexers.map((indexer) => (
|
||||
<SelectIndexerRowConnector
|
||||
key={indexer.name}
|
||||
key={`${indexer.implementation}-${indexer.name}`}
|
||||
implementation={indexer.implementation}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
@@ -237,6 +266,15 @@ class AddIndexerModalContent extends Component {
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
{
|
||||
isPopulated && !!indexers.length && !filteredIndexers.length ?
|
||||
<Alert
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('NoIndexersFound')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ function EditIndexerModalContent(props) {
|
||||
isTesting,
|
||||
saveError,
|
||||
item,
|
||||
hasUsenetDownloadClients,
|
||||
hasTorrentDownloadClients,
|
||||
onInputChange,
|
||||
onFieldChange,
|
||||
onModalClose,
|
||||
@@ -48,10 +50,13 @@ function EditIndexerModalContent(props) {
|
||||
appProfileId,
|
||||
tags,
|
||||
fields,
|
||||
priority
|
||||
priority,
|
||||
protocol,
|
||||
downloadClientId
|
||||
} = item;
|
||||
|
||||
const indexerDisplayName = implementationName === definitionName ? implementationName : `${implementationName} (${definitionName})`;
|
||||
const showDownloadClientInput = downloadClientId.value > 0 || protocol.value === 'usenet' && hasUsenetDownloadClients || protocol.value === 'torrent' && hasTorrentDownloadClients;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -156,6 +161,25 @@ function EditIndexerModalContent(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{showDownloadClientInput ?
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('DownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
|
||||
name="downloadClientId"
|
||||
helpText={translate('IndexerDownloadClientHelpText')}
|
||||
{...downloadClientId}
|
||||
includeAny={true}
|
||||
protocol={protocol.value}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup> : null
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
@@ -222,6 +246,8 @@ EditIndexerModalContent.propTypes = {
|
||||
isTesting: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
hasUsenetDownloadClients: PropTypes.bool.isRequired,
|
||||
hasTorrentDownloadClients: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
|
||||
@@ -3,17 +3,23 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions';
|
||||
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchDownloadClients, toggleAdvancedSettings } from 'Store/Actions/settingsActions';
|
||||
import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector';
|
||||
import EditIndexerModalContent from './EditIndexerModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.downloadClients,
|
||||
createIndexerSchemaSelector(),
|
||||
(advancedSettings, indexer) => {
|
||||
(advancedSettings, downloadClients, indexer) => {
|
||||
const usenetDownloadClients = downloadClients.items.filter((downloadClient) => downloadClient.protocol === 'usenet');
|
||||
const torrentDownloadClients = downloadClients.items.filter((downloadClient) => downloadClient.protocol === 'torrent');
|
||||
|
||||
return {
|
||||
advancedSettings,
|
||||
hasUsenetDownloadClients: usenetDownloadClients.length > 0,
|
||||
hasTorrentDownloadClients: torrentDownloadClients.length > 0,
|
||||
...indexer
|
||||
};
|
||||
}
|
||||
@@ -25,7 +31,8 @@ const mapDispatchToProps = {
|
||||
setIndexerFieldValue,
|
||||
saveIndexer,
|
||||
testIndexer,
|
||||
toggleAdvancedSettings
|
||||
toggleAdvancedSettings,
|
||||
dispatchFetchDownloadClients: fetchDownloadClients
|
||||
};
|
||||
|
||||
class EditIndexerModalContentConnector extends Component {
|
||||
@@ -33,6 +40,10 @@ class EditIndexerModalContentConnector extends Component {
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchDownloadClients();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
@@ -90,7 +101,8 @@ EditIndexerModalContentConnector.propTypes = {
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
saveIndexer: PropTypes.func.isRequired,
|
||||
testIndexer: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
dispatchFetchDownloadClients: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector);
|
||||
|
||||
@@ -148,17 +148,17 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
);
|
||||
|
||||
const jumpBarItems = useMemo(() => {
|
||||
// Reset if not sorting by sortTitle
|
||||
if (sortKey !== 'sortTitle') {
|
||||
// Reset if not sorting by sortName
|
||||
if (sortKey !== 'sortName') {
|
||||
return {
|
||||
order: [],
|
||||
};
|
||||
}
|
||||
|
||||
const characters = items.reduce((acc, item) => {
|
||||
let char = item.sortTitle.charAt(0);
|
||||
let char = item.sortName.charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
if (!isNaN(Number(char))) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
label={
|
||||
isSelectMode
|
||||
? translate('StopSelecting')
|
||||
: translate('SelectIndexer')
|
||||
: translate('SelectIndexers')
|
||||
}
|
||||
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
|
||||
isSelectMode={isSelectMode}
|
||||
|
||||
@@ -9,6 +9,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { bulkDeleteIndexers } from 'Store/Actions/indexerIndexActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteIndexerModalContent.css';
|
||||
|
||||
interface DeleteIndexerModalContentProps {
|
||||
@@ -19,16 +20,16 @@ interface DeleteIndexerModalContentProps {
|
||||
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
const { indexerIds, onModalClose } = props;
|
||||
|
||||
const allIndexer = useSelector(createAllIndexersSelector());
|
||||
const allIndexers = useSelector(createAllIndexersSelector());
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const indexers = useMemo(() => {
|
||||
const selectedIndexers = useMemo(() => {
|
||||
const indexers = indexerIds.map((id) => {
|
||||
return allIndexer.find((s) => s.id === id);
|
||||
return allIndexers.find((s) => s.id === id);
|
||||
});
|
||||
|
||||
return orderBy(indexers, ['sortTitle']);
|
||||
}, [indexerIds, allIndexer]);
|
||||
return orderBy(indexers, ['sortName']);
|
||||
}, [indexerIds, allIndexers]);
|
||||
|
||||
const onDeleteIndexerConfirmed = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -42,17 +43,19 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Delete Selected Indexer</ModalHeader>
|
||||
<ModalHeader>{translate('DeleteSelectedIndexers')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{`Are you sure you want to delete ${indexers.length} selected indexers?`}
|
||||
{translate('DeleteSelectedIndexersMessageText', [
|
||||
selectedIndexers.length,
|
||||
])}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{indexers.map((s) => {
|
||||
{selectedIndexers.map((s) => {
|
||||
return (
|
||||
<li key={s.name}>
|
||||
<li key={s.id}>
|
||||
<span>{s.name}</span>
|
||||
</li>
|
||||
);
|
||||
@@ -61,10 +64,10 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>Cancel</Button>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={onDeleteIndexerConfirmed}>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -14,6 +14,7 @@ import styles from './EditIndexerModalContent.css';
|
||||
interface SavePayload {
|
||||
enable?: boolean;
|
||||
appProfileId?: number;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
interface EditIndexerModalContentProps {
|
||||
@@ -35,6 +36,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
|
||||
const [enable, setEnable] = useState(NO_CHANGE);
|
||||
const [appProfileId, setAppProfileId] = useState<string | number>(NO_CHANGE);
|
||||
const [priority, setPriority] = useState<null | string | number>(null);
|
||||
|
||||
const save = useCallback(() => {
|
||||
let hasChanges = false;
|
||||
@@ -50,12 +52,17 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
payload.appProfileId = appProfileId as number;
|
||||
}
|
||||
|
||||
if (priority !== null) {
|
||||
hasChanges = true;
|
||||
payload.priority = priority as number;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
onSavePress(payload);
|
||||
}
|
||||
|
||||
onModalClose();
|
||||
}, [enable, appProfileId, onSavePress, onModalClose]);
|
||||
}, [enable, appProfileId, priority, onSavePress, onModalClose]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }) => {
|
||||
@@ -66,8 +73,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
case 'appProfileId':
|
||||
setAppProfileId(value);
|
||||
break;
|
||||
case 'priority':
|
||||
setPriority(value);
|
||||
break;
|
||||
default:
|
||||
console.warn('EditIndexerModalContent Unknown Input');
|
||||
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||
}
|
||||
},
|
||||
[setEnable]
|
||||
@@ -81,7 +91,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('Edit Selected Indexer')}</ModalHeader>
|
||||
<ModalHeader>{translate('EditSelectedIndexers')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
@@ -108,18 +118,31 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Priority')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
value={priority}
|
||||
min={1}
|
||||
max={50}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div className={styles.selected}>
|
||||
{translate('{0} indexers selected', selectedCount.toString())}
|
||||
{translate('CountIndexersSelected', [selectedCount])}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button onPress={onSavePressWrapper}>
|
||||
{translate('Apply Changes')}
|
||||
{translate('ApplyChanges')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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 usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { saveIndexerEditor } from 'Store/Actions/indexerIndexActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -13,7 +14,7 @@ import EditIndexerModal from './Edit/EditIndexerModal';
|
||||
import TagsModal from './Tags/TagsModal';
|
||||
import styles from './IndexerIndexSelectFooter.css';
|
||||
|
||||
const seriesEditorSelector = createSelector(
|
||||
const indexersEditorSelector = createSelector(
|
||||
(state) => state.indexers,
|
||||
(indexers) => {
|
||||
const { isSaving, isDeleting, deleteError } = indexers;
|
||||
@@ -27,8 +28,9 @@ const seriesEditorSelector = createSelector(
|
||||
);
|
||||
|
||||
function IndexerIndexSelectFooter() {
|
||||
const { isSaving, isDeleting, deleteError } =
|
||||
useSelector(seriesEditorSelector);
|
||||
const { isSaving, isDeleting, deleteError } = useSelector(
|
||||
indexersEditorSelector
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -37,6 +39,7 @@ function IndexerIndexSelectFooter() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isSavingIndexer, setIsSavingIndexer] = useState(false);
|
||||
const [isSavingTags, setIsSavingTags] = useState(false);
|
||||
const previousIsDeleting = usePrevious(isDeleting);
|
||||
|
||||
const [selectState, selectDispatch] = useSelect();
|
||||
const { selectedState } = selectState;
|
||||
@@ -110,10 +113,10 @@ function IndexerIndexSelectFooter() {
|
||||
}, [isSaving]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDeleting && !deleteError) {
|
||||
selectDispatch({ type: SelectActionType.UnselectAll });
|
||||
if (previousIsDeleting && !isDeleting && !deleteError) {
|
||||
selectDispatch({ type: 'unselectAll' });
|
||||
}
|
||||
}, [isDeleting, deleteError, selectDispatch]);
|
||||
}, [previousIsDeleting, isDeleting, deleteError, selectDispatch]);
|
||||
|
||||
const anySelected = selectedCount > 0;
|
||||
|
||||
@@ -134,7 +137,7 @@ function IndexerIndexSelectFooter() {
|
||||
isDisabled={!anySelected}
|
||||
onPress={onTagsPress}
|
||||
>
|
||||
{translate('Set Tags')}
|
||||
{translate('SetTags')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
|
||||
@@ -151,7 +154,7 @@ function IndexerIndexSelectFooter() {
|
||||
</div>
|
||||
|
||||
<div className={styles.selected}>
|
||||
{translate('{0} indexers selected', selectedCount.toString())}
|
||||
{translate('CountIndexersSelected', [selectedCount])}
|
||||
</div>
|
||||
|
||||
<EditIndexerModal
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -59,14 +59,14 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
}, [tags, applyTags, onApplyTagsPress]);
|
||||
|
||||
const applyTagsOptions = [
|
||||
{ key: 'add', value: 'Add' },
|
||||
{ key: 'remove', value: 'Remove' },
|
||||
{ key: 'replace', value: 'Replace' },
|
||||
{ key: 'add', value: translate('Add') },
|
||||
{ key: 'remove', value: translate('Remove') },
|
||||
{ key: 'replace', value: translate('Replace') },
|
||||
];
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Tags</ModalHeader>
|
||||
<ModalHeader>{translate('Tags')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
@@ -119,8 +119,8 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
key={tag.id}
|
||||
title={
|
||||
removeTag
|
||||
? translate('RemoveTagRemovingTag')
|
||||
: translate('RemoveTagExistingTag')
|
||||
? translate('RemovingTag')
|
||||
: translate('ExistingTag')
|
||||
}
|
||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
@@ -159,10 +159,10 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>Cancel</Button>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||
Apply
|
||||
{translate('Apply')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -92,11 +92,9 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
|
||||
|
||||
const columns = useSelector(columnsSelector);
|
||||
const { showBanners } = useSelector(selectTableOptions);
|
||||
const listRef: React.MutableRefObject<List> = useRef();
|
||||
const listRef = useRef<List>(null);
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
const rowHeight = useMemo(() => {
|
||||
return showBanners ? 70 : 38;
|
||||
@@ -107,8 +105,8 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
|
||||
|
||||
if (isSmallScreen) {
|
||||
setSize({
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -121,14 +119,14 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
|
||||
|
||||
setSize({
|
||||
width: width - padding * 2,
|
||||
height: windowHeight,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
|
||||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
@@ -137,7 +135,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
|
||||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
@@ -166,8 +164,8 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
|
||||
scrollTop += offset;
|
||||
}
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -43,7 +43,7 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
|
||||
className={styles.statusIcon}
|
||||
kind={enabled ? enableKind : kinds.DEFAULT}
|
||||
name={enabled ? enableIcon : icons.BLOCKLIST}
|
||||
title={enabled ? enableTitle : translate('EnabledIndexerIsDisabled')}
|
||||
title={enabled ? enableTitle : translate('Disabled')}
|
||||
/>
|
||||
}
|
||||
{status ? (
|
||||
|
||||
@@ -13,6 +13,10 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
||||
@@ -149,6 +153,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('SearchCapabilities')}>
|
||||
<div>
|
||||
<DescriptionList>
|
||||
@@ -237,6 +242,54 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
{capabilities.categories !== null &&
|
||||
capabilities.categories.length > 0 ? (
|
||||
<FieldSet legend={translate('IndexerCategories')}>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
name: 'id',
|
||||
label: translate('Id'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: translate('Name'),
|
||||
isVisible: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{capabilities.categories
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((category) => {
|
||||
return (
|
||||
<TableBody key={category.id}>
|
||||
<TableRow key={category.id}>
|
||||
<TableRowCell>{category.id}</TableRowCell>
|
||||
<TableRowCell>{category.name}</TableRowCell>
|
||||
</TableRow>
|
||||
{category.subCategories !== null &&
|
||||
category.subCategories.length > 0
|
||||
? category.subCategories
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((subCategory) => {
|
||||
return (
|
||||
<TableRow key={subCategory.id}>
|
||||
<TableRowCell>{subCategory.id}</TableRowCell>
|
||||
<TableRowCell>
|
||||
{subCategory.name}
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</TableBody>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</FieldSet>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -252,7 +253,6 @@ Stats.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
data: PropTypes.object
|
||||
|
||||
@@ -27,7 +27,9 @@ class SearchFooter extends Component {
|
||||
defaultIndexerIds,
|
||||
defaultCategories,
|
||||
defaultSearchQuery,
|
||||
defaultSearchType
|
||||
defaultSearchType,
|
||||
defaultSearchLimit,
|
||||
defaultSearchOffset
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
@@ -38,8 +40,8 @@ class SearchFooter extends Component {
|
||||
searchQuery: defaultSearchQuery || '',
|
||||
searchIndexerIds: defaultIndexerIds,
|
||||
searchCategories: defaultCategories,
|
||||
searchLimit: 100,
|
||||
searchOffset: 0,
|
||||
searchLimit: defaultSearchLimit,
|
||||
searchOffset: defaultSearchOffset,
|
||||
newSearch: true
|
||||
};
|
||||
}
|
||||
@@ -55,7 +57,9 @@ class SearchFooter extends Component {
|
||||
this.onSearchPress();
|
||||
}
|
||||
|
||||
this.props.bindShortcut('enter', this.onSearchPress, { isGlobal: true });
|
||||
setTimeout(() => {
|
||||
this.props.bindShortcut('enter', this.onSearchPress, { isGlobal: true });
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -120,7 +124,6 @@ class SearchFooter extends Component {
|
||||
};
|
||||
|
||||
onSearchPress = () => {
|
||||
|
||||
const {
|
||||
searchLimit,
|
||||
searchOffset,
|
||||
@@ -188,10 +191,10 @@ class SearchFooter extends Component {
|
||||
icon = icons.SEARCH;
|
||||
}
|
||||
|
||||
let footerLabel = `Search ${searchIndexerIds.length === 0 ? 'all' : searchIndexerIds.length} Indexers`;
|
||||
let footerLabel = searchIndexerIds.length === 0 ? translate('SearchAllIndexers') : translate('SearchCountIndexers', [searchIndexerIds.length]);
|
||||
|
||||
if (isPopulated) {
|
||||
footerLabel = selectedCount === 0 ? `Found ${itemCount} releases` : `Selected ${selectedCount} of ${itemCount} releases`;
|
||||
footerLabel = selectedCount === 0 ? translate('FoundCountReleases', [itemCount]) : translate('SelectedCountOfCountReleases', [selectedCount, itemCount]);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -302,6 +305,8 @@ SearchFooter.propTypes = {
|
||||
defaultCategories: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
defaultSearchQuery: PropTypes.string.isRequired,
|
||||
defaultSearchType: PropTypes.string.isRequired,
|
||||
defaultSearchLimit: PropTypes.number.isRequired,
|
||||
defaultSearchOffset: PropTypes.number.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
itemCount: PropTypes.number.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -3,24 +3,58 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setSearchDefault } from 'Store/Actions/releaseActions';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
import SearchFooter from './SearchFooter';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.releases,
|
||||
(releases) => {
|
||||
(state) => state.router.location,
|
||||
(releases, location) => {
|
||||
const {
|
||||
searchQuery: defaultSearchQuery,
|
||||
searchIndexerIds: defaultIndexerIds,
|
||||
searchCategories: defaultCategories,
|
||||
searchType: defaultSearchType
|
||||
searchType: defaultSearchType,
|
||||
searchLimit: defaultSearchLimit,
|
||||
searchOffset: defaultSearchOffset
|
||||
} = releases.defaults;
|
||||
|
||||
const { params } = parseUrl(location.search);
|
||||
const defaultSearchQueryParams = {};
|
||||
|
||||
if (params.query && !defaultSearchQuery) {
|
||||
defaultSearchQueryParams.searchQuery = params.query;
|
||||
}
|
||||
|
||||
if (params.indexerIds && !defaultIndexerIds.length) {
|
||||
defaultSearchQueryParams.searchIndexerIds = params.indexerIds.split(',').filter(Boolean).map((id) => Number(id));
|
||||
}
|
||||
|
||||
if (params.categories && !defaultCategories.length) {
|
||||
defaultSearchQueryParams.searchCategories = params.categories.split(',').filter(Boolean).map((id) => Number(id));
|
||||
}
|
||||
|
||||
if (params.type && defaultSearchType === 'search') {
|
||||
defaultSearchQueryParams.searchType = params.type;
|
||||
}
|
||||
|
||||
if (params.limit && defaultSearchLimit === 100 && !isNaN(params.limit)) {
|
||||
defaultSearchQueryParams.searchLimit = Number(params.limit);
|
||||
}
|
||||
|
||||
if (params.offset && !defaultSearchOffset && !isNaN(params.offset)) {
|
||||
defaultSearchQueryParams.searchOffset = Number(params.offset);
|
||||
}
|
||||
|
||||
return {
|
||||
defaultSearchQuery,
|
||||
defaultIndexerIds,
|
||||
defaultCategories,
|
||||
defaultSearchType
|
||||
defaultSearchQueryParams,
|
||||
defaultSearchQuery: defaultSearchQueryParams.searchQuery ?? defaultSearchQuery,
|
||||
defaultIndexerIds: defaultSearchQueryParams.searchIndexerIds ?? defaultIndexerIds,
|
||||
defaultCategories: defaultSearchQueryParams.searchCategories ?? defaultCategories,
|
||||
defaultSearchType: defaultSearchQueryParams.searchType ?? defaultSearchType,
|
||||
defaultSearchLimit: defaultSearchQueryParams.searchLimit ?? defaultSearchLimit,
|
||||
defaultSearchOffset: defaultSearchQueryParams.searchOffset ?? defaultSearchOffset
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -32,6 +66,16 @@ const mapDispatchToProps = {
|
||||
|
||||
class SearchFooterConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
// Set defaults from query parameters
|
||||
Object.entries(this.props.defaultSearchQueryParams).forEach(([name, value]) => {
|
||||
this.onInputChange({ name, value });
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -43,9 +87,14 @@ class SearchFooterConnector extends Component {
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultSearchQueryParams,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SearchFooter
|
||||
{...this.props}
|
||||
{...otherProps}
|
||||
onInputChange={this.onInputChange}
|
||||
/>
|
||||
);
|
||||
@@ -53,6 +102,7 @@ class SearchFooterConnector extends Component {
|
||||
}
|
||||
|
||||
SearchFooterConnector.propTypes = {
|
||||
defaultSearchQueryParams: PropTypes.object.isRequired,
|
||||
setSearchDefault: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -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,9 @@ 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 AddIndexerModal from 'Indexer/Add/AddIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import NoIndexer from 'Indexer/NoIndexer';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
@@ -53,7 +56,9 @@ class SearchIndex extends Component {
|
||||
lastToggled: null,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
selectedState: {}
|
||||
selectedState: {},
|
||||
isAddIndexerModalOpen: false,
|
||||
isEditIndexerModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,7 +146,7 @@ class SearchIndex extends Component {
|
||||
} = this.props;
|
||||
|
||||
// Reset if not sorting by sortTitle
|
||||
if (sortKey !== 'title') {
|
||||
if (sortKey !== 'sortTitle') {
|
||||
this.setState({ jumpBarItems: { order: [] } });
|
||||
return;
|
||||
}
|
||||
@@ -149,7 +154,7 @@ class SearchIndex extends Component {
|
||||
const characters = _.reduce(items, (acc, item) => {
|
||||
let char = item.sortTitle.charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
if (!isNaN(Number(char))) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
@@ -180,6 +185,22 @@ class SearchIndex extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddIndexerPress = () => {
|
||||
this.setState({ isAddIndexerModalOpen: true });
|
||||
};
|
||||
|
||||
onAddIndexerModalClose = () => {
|
||||
this.setState({ isAddIndexerModalOpen: false });
|
||||
};
|
||||
|
||||
onAddIndexerSelectIndexer = () => {
|
||||
this.setState({ isEditIndexerModalOpen: true });
|
||||
};
|
||||
|
||||
onEditIndexerModalClose = () => {
|
||||
this.setState({ isEditIndexerModalOpen: false });
|
||||
};
|
||||
|
||||
onJumpBarItemPress = (jumpToCharacter) => {
|
||||
this.setState({ jumpToCharacter });
|
||||
};
|
||||
@@ -251,7 +272,9 @@ class SearchIndex extends Component {
|
||||
jumpToCharacter,
|
||||
selectedState,
|
||||
allSelected,
|
||||
allUnselected
|
||||
allUnselected,
|
||||
isAddIndexerModalOpen,
|
||||
isEditIndexerModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIndexerIds = this.getSelectedIds();
|
||||
@@ -309,9 +332,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>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -347,6 +370,17 @@ class SearchIndex extends Component {
|
||||
!error && !isFetching && hasIndexers && !items.length &&
|
||||
<NoSearchResults totalItems={totalItems} />
|
||||
}
|
||||
|
||||
<AddIndexerModal
|
||||
isOpen={isAddIndexerModalOpen}
|
||||
onModalClose={this.onAddIndexerModalClose}
|
||||
onSelectIndexer={this.onAddIndexerSelectIndexer}
|
||||
/>
|
||||
|
||||
<EditIndexerModalConnector
|
||||
isOpen={isEditIndexerModalOpen}
|
||||
onModalClose={this.onEditIndexerModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -17,7 +17,7 @@ function AdvancedSettingsButton(props) {
|
||||
return (
|
||||
<Link
|
||||
className={styles.button}
|
||||
title={advancedSettings ? translate('ShownClickToHide') : translate('HiddenClickToShow')}
|
||||
title={advancedSettings ? translate('AdvancedSettingsShownClickToHide') : translate('AdvancedSettingsHiddenClickToShow')}
|
||||
onPress={onAdvancedSettingsPress}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditApplicationModalContent.css';
|
||||
|
||||
@@ -38,6 +39,7 @@ function EditApplicationModalContent(props) {
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onDeleteApplicationPress,
|
||||
onAdvancedSettingsPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -149,6 +151,12 @@ function EditApplicationModalContent(props) {
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
@@ -188,7 +196,8 @@ EditApplicationModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onDeleteApplicationPress: PropTypes.func
|
||||
onDeleteApplicationPress: PropTypes.func,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditApplicationModalContent;
|
||||
|
||||
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveApplication, setApplicationFieldValue, setApplicationValue, testApplication } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveApplication,
|
||||
setApplicationFieldValue,
|
||||
setApplicationValue,
|
||||
testApplication,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditApplicationModalContent from './EditApplicationModalContent';
|
||||
|
||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
||||
setApplicationValue,
|
||||
setApplicationFieldValue,
|
||||
saveApplication,
|
||||
testApplication
|
||||
testApplication,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditApplicationModalContentConnector extends Component {
|
||||
@@ -56,6 +63,10 @@ class EditApplicationModalContentConnector extends Component {
|
||||
this.props.testApplication({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -67,6 +78,7 @@ class EditApplicationModalContentConnector extends Component {
|
||||
onTestPress={this.onTestPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -82,7 +94,8 @@ EditApplicationModalContentConnector.propTypes = {
|
||||
setApplicationFieldValue: PropTypes.func,
|
||||
saveApplication: PropTypes.func,
|
||||
testApplication: PropTypes.func,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditApplicationModalContentConnector);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,12 @@ export const defaultState = {
|
||||
isSortable: false,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'queryType',
|
||||
label: translate('QueryType'),
|
||||
isSortable: false,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
label: translate('Categories'),
|
||||
|
||||
@@ -29,6 +29,8 @@ export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
selectedSchema: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
@@ -41,7 +43,7 @@ export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
sortKey: 'name',
|
||||
sortKey: 'sortName',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
items: []
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export const defaultState = {
|
||||
columns: [
|
||||
{
|
||||
name: 'status',
|
||||
columnLabel: translate('ReleaseStatus'),
|
||||
columnLabel: translate('IndexerStatus'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
|
||||
@@ -31,16 +31,18 @@ export const defaultState = {
|
||||
error: null,
|
||||
grabError: null,
|
||||
items: [],
|
||||
sortKey: 'title',
|
||||
sortKey: 'age',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
secondarySortKey: 'title',
|
||||
secondarySortKey: 'sortTitle',
|
||||
secondarySortDirection: sortDirections.ASCENDING,
|
||||
|
||||
defaults: {
|
||||
searchType: 'search',
|
||||
searchQuery: '',
|
||||
searchIndexerIds: [],
|
||||
searchCategories: []
|
||||
searchCategories: [],
|
||||
searchLimit: 100,
|
||||
searchOffset: 0
|
||||
},
|
||||
|
||||
columns: [
|
||||
|
||||
@@ -9,12 +9,12 @@ function createUnoptimizedSelector(uiSection) {
|
||||
const items = indexers.items.map((s) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
sortName
|
||||
} = s;
|
||||
|
||||
return {
|
||||
id,
|
||||
sortTitle: name
|
||||
sortName
|
||||
};
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ const createMovieEqualSelector = createSelectorCreator(
|
||||
function createIndexerClientSideCollectionItemsSelector(uiSection) {
|
||||
return createMovieEqualSelector(
|
||||
createUnoptimizedSelector(uiSection),
|
||||
(movies) => movies
|
||||
(indexers) => indexers
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ function createUnoptimizedSelector(uiSection) {
|
||||
const items = releases.items.map((s) => {
|
||||
const {
|
||||
guid,
|
||||
title,
|
||||
sortTitle,
|
||||
indexerId
|
||||
} = s;
|
||||
|
||||
return {
|
||||
guid,
|
||||
sortTitle: title,
|
||||
sortTitle,
|
||||
indexerId
|
||||
};
|
||||
});
|
||||
@@ -40,7 +40,7 @@ const createMovieEqualSelector = createSelectorCreator(
|
||||
function createReleaseClientSideCollectionItemsSelector(uiSection) {
|
||||
return createMovieEqualSelector(
|
||||
createUnoptimizedSelector(uiSection),
|
||||
(movies) => movies
|
||||
(releases) => releases
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ module.exports = {
|
||||
inputHoverBackgroundColor: 'rgba(255, 255, 255, 0.20)',
|
||||
inputSelectedBackgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
advancedFormLabelColor: '#ff902b',
|
||||
disabledCheckInputColor: '#ddd',
|
||||
disabledCheckInputColor: '#999',
|
||||
disabledInputColor: '#808080',
|
||||
|
||||
//
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export default function getIndexOfFirstCharacter(items, character) {
|
||||
return items.findIndex((item) => {
|
||||
const firstCharacter = item.sortTitle.charAt(0);
|
||||
const firstCharacter = 'sortName' in item ? item.sortName.charAt(0) : item.sortTitle.charAt(0);
|
||||
|
||||
if (character === '#') {
|
||||
return !isNaN(firstCharacter);
|
||||
return !isNaN(Number(firstCharacter));
|
||||
}
|
||||
|
||||
return firstCharacter === character;
|
||||
|
||||
@@ -18,13 +18,14 @@ function getTranslations() {
|
||||
|
||||
const translations = getTranslations();
|
||||
|
||||
export default function translate(key, args = '') {
|
||||
export default function translate(key, args = []) {
|
||||
const translation = translations[key] || key;
|
||||
|
||||
if (args) {
|
||||
const translatedKey = translate(key);
|
||||
return translatedKey.replace(/\{(\d+)\}/g, (match, index) => {
|
||||
return translation.replace(/\{(\d+)\}/g, (match, index) => {
|
||||
return args[index];
|
||||
});
|
||||
}
|
||||
|
||||
return translations[key] || key;
|
||||
return translation;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-use-measure": "2.1.1",
|
||||
"react-virtualized": "9.22.3",
|
||||
"react-virtualized": "9.21.1",
|
||||
"react-window": "1.8.8",
|
||||
"redux": "4.2.1",
|
||||
"redux-actions": "2.6.5",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
() =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -46,7 +46,11 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
|
||||
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url)
|
||||
{
|
||||
Version = HttpVersion.Version20,
|
||||
VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
|
||||
};
|
||||
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
|
||||
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
|
||||
|
||||
@@ -127,7 +131,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))
|
||||
{
|
||||
@@ -148,7 +152,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
sw.Stop();
|
||||
|
||||
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode);
|
||||
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode, responseMessage.Version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +190,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
var client = new System.Net.Http.HttpClient(handler)
|
||||
{
|
||||
DefaultRequestVersion = HttpVersion.Version20,
|
||||
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
|
||||
Timeout = Timeout.InfiniteTimeSpan
|
||||
};
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
request.Method = HttpMethod.Get;
|
||||
request.ContentData = null;
|
||||
request.ContentSummary = null;
|
||||
}
|
||||
|
||||
// Save to add to final response
|
||||
|
||||
@@ -9,9 +9,9 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
public class HttpResponse
|
||||
{
|
||||
private static readonly Regex RegexRefresh = new Regex("^(.*?url)=(.*?)(?:;|$)", RegexOptions.Compiled);
|
||||
private static readonly Regex RegexRefresh = new ("^(.*?url)=(.*?)(?:;|$)", RegexOptions.Compiled);
|
||||
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null)
|
||||
{
|
||||
Request = request;
|
||||
Headers = headers;
|
||||
@@ -19,9 +19,10 @@ namespace NzbDrone.Common.Http
|
||||
ResponseData = binaryData;
|
||||
StatusCode = statusCode;
|
||||
ElapsedTime = elapsedTime;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, string content, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, string content, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null)
|
||||
{
|
||||
Request = request;
|
||||
Headers = headers;
|
||||
@@ -30,6 +31,7 @@ namespace NzbDrone.Common.Http
|
||||
_content = content;
|
||||
StatusCode = statusCode;
|
||||
ElapsedTime = elapsedTime;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public HttpRequest Request { get; private set; }
|
||||
@@ -37,6 +39,7 @@ namespace NzbDrone.Common.Http
|
||||
public CookieCollection Cookies { get; private set; }
|
||||
public HttpStatusCode StatusCode { get; private set; }
|
||||
public long ElapsedTime { get; private set; }
|
||||
public Version Version { get; private set; }
|
||||
public byte[] ResponseData { get; private set; }
|
||||
|
||||
private string _content;
|
||||
@@ -63,6 +66,8 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public bool HasHttpError => (int)StatusCode >= 400;
|
||||
|
||||
public bool HasHttpServerError => (int)StatusCode >= 500;
|
||||
|
||||
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
|
||||
StatusCode == HttpStatusCode.Found ||
|
||||
StatusCode == HttpStatusCode.SeeOther ||
|
||||
@@ -119,7 +124,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
|
||||
var result = $"Res: HTTP/{Version} [{Request.Method}] {Request.Url}: {(int)StatusCode}.{StatusCode} ({ResponseData?.Length ?? 0} bytes)";
|
||||
|
||||
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
@@ -134,7 +139,7 @@ namespace NzbDrone.Common.Http
|
||||
where T : new()
|
||||
{
|
||||
public HttpResponse(HttpResponse response)
|
||||
: base(response.Request, response.Headers, response.Cookies, response.ResponseData, response.ElapsedTime, response.StatusCode)
|
||||
: base(response.Request, response.Headers, response.Cookies, response.ResponseData, response.ElapsedTime, response.StatusCode, response.Version)
|
||||
{
|
||||
Resource = Json.Deserialize<T>(response.Content);
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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("*"))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user