From 0d918a0aa9329edda7d1c0fa8e41604843f570a0 Mon Sep 17 00:00:00 2001 From: Qstick Date: Fri, 9 Dec 2022 19:03:13 -0600 Subject: [PATCH] New: Define multiple mapped categories for Download Clients Fixes #170 --- .../src/Components/Form/FormInputGroup.js | 4 + .../NewznabCategorySelectInputConnector.js | 2 +- frontend/src/Helpers/Props/inputTypes.js | 2 + .../Categories/AddCategoryModal.js | 27 +++ .../Categories/AddCategoryModalConnector.js | 50 +++++ .../Categories/AddCategoryModalContent.css | 5 + .../Categories/AddCategoryModalContent.js | 111 ++++++++++++ .../AddCategoryModalContentConnector.js | 78 ++++++++ .../DownloadClients/Categories/Category.css | 32 ++++ .../DownloadClients/Categories/Category.js | 111 ++++++++++++ .../EditDownloadClientModalContent.js | 76 +++++++- ...EditDownloadClientModalContentConnector.js | 28 ++- .../Settings/downloadClientCategories.js | 171 ++++++++++++++++++ .../Store/Actions/Settings/downloadClients.js | 29 ++- frontend/src/Store/Actions/settingsActions.js | 5 + .../023_download_client_categories.cs | 15 ++ src/NzbDrone.Core/Datastore/TableMapping.cs | 4 +- .../Download/Clients/Aria2/Aria2.cs | 2 + .../Clients/Blackhole/TorrentBlackhole.cs | 2 + .../Clients/Blackhole/UsenetBlackhole.cs | 1 + .../Download/Clients/Deluge/Deluge.cs | 49 +++-- .../Download/Clients/Deluge/DelugeProxy.cs | 6 +- .../Download/Clients/Deluge/DelugeSettings.cs | 2 +- .../DownloadStationSettings.cs | 8 +- .../DownloadStation/TorrentDownloadStation.cs | 7 +- .../DownloadStation/UsenetDownloadStation.cs | 7 +- .../Download/Clients/Flood/Flood.cs | 12 +- .../Download/Clients/Hadouken/Hadouken.cs | 5 +- .../Clients/Hadouken/HadoukenProxy.cs | 14 +- .../Clients/Hadouken/HadoukenSettings.cs | 2 +- .../Download/Clients/NzbVortex/NzbVortex.cs | 22 ++- .../Clients/NzbVortex/NzbVortexProxy.cs | 6 +- .../Clients/NzbVortex/NzbVortexSettings.cs | 2 +- .../Download/Clients/Nzbget/Nzbget.cs | 17 +- .../Download/Clients/Nzbget/NzbgetSettings.cs | 2 +- .../Download/Clients/Pneumatic/Pneumatic.cs | 1 + .../Clients/QBittorrent/QBittorrent.cs | 37 ++-- .../QBittorrent/QBittorrentProxySelector.cs | 4 +- .../Clients/QBittorrent/QBittorrentProxyV1.cs | 12 +- .../Clients/QBittorrent/QBittorrentProxyV2.cs | 12 +- .../QBittorrent/QBittorrentSettings.cs | 2 +- .../Download/Clients/Sabnzbd/Sabnzbd.cs | 31 ++-- .../Clients/Sabnzbd/SabnzbdSettings.cs | 2 +- .../Clients/Transmission/Transmission.cs | 1 + .../Transmission/TransmissionSettings.cs | 2 +- .../Download/Clients/Vuze/Vuze.cs | 1 + .../Download/Clients/rTorrent/RTorrent.cs | 5 +- .../Clients/rTorrent/RTorrentSettings.cs | 2 +- .../Download/Clients/uTorrent/UTorrent.cs | 45 +---- .../Clients/uTorrent/UTorrentSettings.cs | 2 +- .../Download/DownloadClientBase.cs | 60 ++++++ .../Download/DownloadClientCategory.cs | 14 ++ .../Download/DownloadClientDefinition.cs | 10 +- .../Download/DownloadClientFactory.cs | 1 + src/NzbDrone.Core/Download/IDownloadClient.cs | 2 + src/NzbDrone.Core/Localization/Core/en.json | 4 + .../DownloadClient/DownloadClientResource.cs | 6 + 57 files changed, 1020 insertions(+), 152 deletions(-) create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModal.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalConnector.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.css create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContentConnector.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.css create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.js create mode 100644 frontend/src/Store/Actions/Settings/downloadClientCategories.js create mode 100644 src/NzbDrone.Core/Datastore/Migration/023_download_client_categories.cs create mode 100644 src/NzbDrone.Core/Download/DownloadClientCategory.cs diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 7bc031c9e..8e39cd36a 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -16,6 +16,7 @@ import FormInputHelpText from './FormInputHelpText'; import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector'; import InfoInput from './InfoInput'; import KeyValueListInput from './KeyValueListInput'; +import NewznabCategorySelectInputConnector from './NewznabCategorySelectInputConnector'; import NumberInput from './NumberInput'; import OAuthInputConnector from './OAuthInputConnector'; import PasswordInput from './PasswordInput'; @@ -68,6 +69,9 @@ function getComponent(type) { case inputTypes.PATH: return PathInputConnector; + case inputTypes.CATEGORY_SELECT: + return NewznabCategorySelectInputConnector; + case inputTypes.INDEXER_FLAGS_SELECT: return IndexerFlagsSelectInputConnector; diff --git a/frontend/src/Components/Form/NewznabCategorySelectInputConnector.js b/frontend/src/Components/Form/NewznabCategorySelectInputConnector.js index d9cd3d178..e63e2cae7 100644 --- a/frontend/src/Components/Form/NewznabCategorySelectInputConnector.js +++ b/frontend/src/Components/Form/NewznabCategorySelectInputConnector.js @@ -31,7 +31,7 @@ function createMapStateToProps() { }); return { - value, + value: value || [], values }; } diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index 4af772636..7a11bb0c7 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -8,6 +8,7 @@ export const DEVICE = 'device'; export const KEY_VALUE_LIST = 'keyValueList'; export const INFO = 'info'; export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect'; +export const CATEGORY_SELECT = 'newznabCategorySelect'; export const NUMBER = 'number'; export const OAUTH = 'oauth'; export const PASSWORD = 'password'; @@ -32,6 +33,7 @@ export const all = [ KEY_VALUE_LIST, INFO, MOVIE_MONITORED_SELECT, + CATEGORY_SELECT, NUMBER, OAUTH, PASSWORD, diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModal.js b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModal.js new file mode 100644 index 000000000..798077c4f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModal.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import AddCategoryModalContentConnector from './AddCategoryModalContentConnector'; + +function AddCategoryModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +AddCategoryModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddCategoryModal; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalConnector.js new file mode 100644 index 000000000..069dc0312 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalConnector.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import AddCategoryModal from './AddCategoryModal'; + +function createMapDispatchToProps(dispatch, props) { + const section = 'settings.downloadClientCategories'; + + return { + dispatchClearPendingChanges() { + dispatch(clearPendingChanges({ section })); + } + }; +} + +class AddCategoryModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.dispatchClearPendingChanges(); + this.props.onModalClose(); + }; + + // + // Render + + render() { + const { + dispatchClearPendingChanges, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +AddCategoryModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + dispatchClearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(null, createMapDispatchToProps)(AddCategoryModalConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.css new file mode 100644 index 000000000..a2b6014df --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.css @@ -0,0 +1,5 @@ +.deleteButton { + composes: button from '~Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.js new file mode 100644 index 000000000..71c51849c --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.js @@ -0,0 +1,111 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +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 { inputTypes, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './AddCategoryModalContent.css'; + +function AddCategoryModalContent(props) { + const { + advancedSettings, + item, + onInputChange, + onFieldChange, + onCancelPress, + onSavePress, + onDeleteSpecificationPress, + ...otherProps + } = props; + + const { + id, + clientCategory, + categories + } = item; + + return ( + + + {`${id ? 'Edit' : 'Add'} Category`} + + + +
+ + + {translate('DownloadClientCategory')} + + + + + + + + {translate('MappedCategories')} + + + + +
+
+ + { + id && + + } + + + + + {translate('Save')} + + +
+ ); +} + +AddCategoryModalContent.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onFieldChange: PropTypes.func.isRequired, + onCancelPress: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onDeleteSpecificationPress: PropTypes.func +}; + +export default AddCategoryModalContent; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContentConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContentConnector.js new file mode 100644 index 000000000..a585e5707 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContentConnector.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { clearDownloadClientCategoryPending, saveDownloadClientCategory, setDownloadClientCategoryFieldValue, setDownloadClientCategoryValue } from 'Store/Actions/settingsActions'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import AddCategoryModalContent from './AddCategoryModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createProviderSettingsSelector('downloadClientCategories'), + (advancedSettings, specification) => { + return { + advancedSettings, + ...specification + }; + } + ); +} + +const mapDispatchToProps = { + setDownloadClientCategoryValue, + setDownloadClientCategoryFieldValue, + saveDownloadClientCategory, + clearDownloadClientCategoryPending +}; + +class AddCategoryModalContentConnector extends Component { + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setDownloadClientCategoryValue({ name, value }); + }; + + onFieldChange = ({ name, value }) => { + this.props.setDownloadClientCategoryFieldValue({ name, value }); + }; + + onCancelPress = () => { + this.props.clearDownloadClientCategoryPending(); + this.props.onModalClose(); + }; + + onSavePress = () => { + this.props.saveDownloadClientCategory({ id: this.props.id }); + this.props.onModalClose(); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +AddCategoryModalContentConnector.propTypes = { + id: PropTypes.number, + item: PropTypes.object.isRequired, + setDownloadClientCategoryValue: PropTypes.func.isRequired, + setDownloadClientCategoryFieldValue: PropTypes.func.isRequired, + clearDownloadClientCategoryPending: PropTypes.func.isRequired, + saveDownloadClientCategory: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddCategoryModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.css b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.css new file mode 100644 index 000000000..52ce0edd4 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.css @@ -0,0 +1,32 @@ +.customFormat { + composes: card from '~Components/Card.css'; + + width: 300px; +} + +.nameContainer { + display: flex; + justify-content: space-between; +} + +.name { + @add-mixin truncate; + + margin-bottom: 5px; + font-weight: 300; + font-size: 20px; +} + +.labels { + display: flex; + flex-wrap: wrap; + margin-top: 5px; + pointer-events: all; +} + +.tooltipLabel { + composes: label from '~Components/Label.css'; + + margin: 0; + border: none; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.js b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.js new file mode 100644 index 000000000..6e0a25a2d --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.js @@ -0,0 +1,111 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import { kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import AddCategoryModalConnector from './AddCategoryModalConnector'; +import styles from './Category.css'; + +class Category extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditSpecificationModalOpen: false, + isDeleteSpecificationModalOpen: false + }; + } + + // + // Listeners + + onEditSpecificationPress = () => { + this.setState({ isEditSpecificationModalOpen: true }); + }; + + onEditSpecificationModalClose = () => { + this.setState({ isEditSpecificationModalOpen: false }); + }; + + onDeleteSpecificationPress = () => { + this.setState({ + isEditSpecificationModalOpen: false, + isDeleteSpecificationModalOpen: true + }); + }; + + onDeleteSpecificationModalClose = () => { + this.setState({ isDeleteSpecificationModalOpen: false }); + }; + + onConfirmDeleteSpecification = () => { + this.props.onConfirmDeleteSpecification(this.props.id); + }; + + // + // Lifecycle + + render() { + const { + id, + clientCategory, + categories + } = this.props; + + return ( + +
+
+ {clientCategory} +
+
+ + + + + + +
+ {translate('AreYouSureYouWantToDeleteCategory', [name])} +
+ + } + confirmLabel={translate('Delete')} + onConfirm={this.onConfirmDeleteSpecification} + onCancel={this.onDeleteSpecificationModalClose} + /> +
+ ); + } +} + +Category.propTypes = { + id: PropTypes.number.isRequired, + categories: PropTypes.arrayOf(PropTypes.number).isRequired, + clientCategory: PropTypes.string.isRequired, + onConfirmDeleteSpecification: PropTypes.func.isRequired +}; + +export default Category; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index 8aba1b678..2e63c2b5f 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -1,11 +1,14 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Alert from 'Components/Alert'; +import Card from 'Components/Card'; +import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import Icon from 'Components/Icon'; import Button from 'Components/Link/Button'; import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -13,12 +16,33 @@ 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 { inputTypes, kinds } from 'Helpers/Props'; +import { icons, inputTypes, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +import AddCategoryModalConnector from './Categories/AddCategoryModalConnector'; +import Category from './Categories/Category'; import styles from './EditDownloadClientModalContent.css'; class EditDownloadClientModalContent extends Component { + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddCategoryModalOpen: false + }; + } + + onAddCategoryPress = () => { + this.setState({ isAddCategoryModalOpen: true }); + }; + + onAddCategoryModalClose = () => { + this.setState({ isAddCategoryModalOpen: false }); + }; + // // Render @@ -27,6 +51,7 @@ class EditDownloadClientModalContent extends Component { advancedSettings, isFetching, error, + categories, isSaving, isTesting, saveError, @@ -37,19 +62,27 @@ class EditDownloadClientModalContent extends Component { onSavePress, onTestPress, onDeleteDownloadClientPress, + onConfirmDeleteCategory, ...otherProps } = this.props; + const { + isAddCategoryModalOpen + } = this.state; + const { id, implementationName, name, enable, priority, + supportsCategories, fields, message } = item; + console.log(supportsCategories); + return ( @@ -136,6 +169,43 @@ class EditDownloadClientModalContent extends Component { /> + { + supportsCategories.value ? +
+
+ { + categories.map((tag) => { + return ( + + ); + }) + } + + +
+ +
+
+
+
: + null + } + + + } @@ -185,13 +255,15 @@ EditDownloadClientModalContent.propTypes = { isSaving: PropTypes.bool.isRequired, saveError: PropTypes.object, isTesting: PropTypes.bool.isRequired, + categories: PropTypes.arrayOf(PropTypes.object), item: PropTypes.object.isRequired, onInputChange: PropTypes.func.isRequired, onFieldChange: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired, - onDeleteDownloadClientPress: PropTypes.func + onDeleteDownloadClientPress: PropTypes.func, + onConfirmDeleteCategory: PropTypes.func.isRequired }; export default EditDownloadClientModalContent; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js index f2d4ad6ff..d02d99ca3 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions'; +import { deleteDownloadClientCategory, fetchDownloadClientCategories, saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import EditDownloadClientModalContent from './EditDownloadClientModalContent'; @@ -10,10 +10,12 @@ function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, createProviderSettingsSelector('downloadClients'), - (advancedSettings, downloadClient) => { + (state) => state.settings.downloadClientCategories, + (advancedSettings, downloadClient, categories) => { return { advancedSettings, - ...downloadClient + ...downloadClient, + categories: categories.items }; } ); @@ -23,7 +25,9 @@ const mapDispatchToProps = { setDownloadClientValue, setDownloadClientFieldValue, saveDownloadClient, - testDownloadClient + testDownloadClient, + fetchDownloadClientCategories, + deleteDownloadClientCategory }; class EditDownloadClientModalContentConnector extends Component { @@ -31,6 +35,14 @@ class EditDownloadClientModalContentConnector extends Component { // // Lifecycle + componentDidMount() { + const { + id, + tagsFromId + } = this.props; + this.props.fetchDownloadClientCategories({ id: tagsFromId || id }); + } + componentDidUpdate(prevProps, prevState) { if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { this.props.onModalClose(); @@ -56,6 +68,10 @@ class EditDownloadClientModalContentConnector extends Component { this.props.testDownloadClient({ id: this.props.id }); }; + onConfirmDeleteCategory = (id) => { + this.props.deleteDownloadClientCategory({ id }); + }; + // // Render @@ -67,6 +83,7 @@ class EditDownloadClientModalContentConnector extends Component { onTestPress={this.onTestPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} + onConfirmDeleteCategory={this.onConfirmDeleteCategory} /> ); } @@ -74,10 +91,13 @@ class EditDownloadClientModalContentConnector extends Component { EditDownloadClientModalContentConnector.propTypes = { id: PropTypes.number, + tagsFromId: PropTypes.number, isFetching: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired, saveError: PropTypes.object, item: PropTypes.object.isRequired, + fetchDownloadClientCategories: PropTypes.func.isRequired, + deleteDownloadClientCategory: PropTypes.func.isRequired, setDownloadClientValue: PropTypes.func.isRequired, setDownloadClientFieldValue: PropTypes.func.isRequired, saveDownloadClient: PropTypes.func.isRequired, diff --git a/frontend/src/Store/Actions/Settings/downloadClientCategories.js b/frontend/src/Store/Actions/Settings/downloadClientCategories.js new file mode 100644 index 000000000..a57dc1858 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/downloadClientCategories.js @@ -0,0 +1,171 @@ +import { createAction } from 'redux-actions'; +import { batchActions } from 'redux-batched-actions'; +import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer'; +import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; +import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import { createThunk } from 'Store/thunks'; +import getNextId from 'Utilities/State/getNextId'; +import getProviderState from 'Utilities/State/getProviderState'; +import getSectionState from 'Utilities/State/getSectionState'; +import selectProviderSchema from 'Utilities/State/selectProviderSchema'; +import { removeItem, set, update, updateItem } from '../baseActions'; + +// +// Variables + +const section = 'settings.downloadClientCategories'; + +// +// Actions Types + +export const FETCH_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/fetchDownloadClientCategories'; +export const FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/fetchDownloadClientCategorySchema'; +export const SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/selectDownloadClientCategorySchema'; +export const SET_DOWNLOAD_CLIENT_CATEGORY_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryValue'; +export const SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryFieldValue'; +export const SAVE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/saveDownloadClientCategory'; +export const DELETE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteDownloadClientCategory'; +export const DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteAllDownloadClientCategory'; +export const CLEAR_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/clearDownloadClientCategories'; +export const CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING = 'settings/downloadClientCategories/clearDownloadClientCategoryPending'; +// +// Action Creators + +export const fetchDownloadClientCategories = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORIES); +export const fetchDownloadClientCategorySchema = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA); +export const selectDownloadClientCategorySchema = createAction(SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA); + +export const saveDownloadClientCategory = createThunk(SAVE_DOWNLOAD_CLIENT_CATEGORY); +export const deleteDownloadClientCategory = createThunk(DELETE_DOWNLOAD_CLIENT_CATEGORY); +export const deleteAllDownloadClientCategory = createThunk(DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY); + +export const setDownloadClientCategoryValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +export const setDownloadClientCategoryFieldValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +export const clearDownloadClientCategory = createAction(CLEAR_DOWNLOAD_CLIENT_CATEGORIES); + +export const clearDownloadClientCategoryPending = createThunk(CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING); + +// +// Details + +export default { + + // + // State + + defaultState: { + isPopulated: false, + error: null, + isSchemaFetching: false, + isSchemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_DOWNLOAD_CLIENT_CATEGORIES]: (getState, payload, dispatch) => { + let tags = []; + if (payload.id) { + const cfState = getSectionState(getState(), 'settings.downloadClients', true); + const cf = cfState.items[cfState.itemMap[payload.id]]; + tags = cf.categories.map((tag, i) => { + return { + id: i + 1, + ...tag + }; + }); + } + + dispatch(batchActions([ + update({ section, data: tags }), + set({ + section, + isPopulated: true + }) + ])); + }, + + [SAVE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => { + const { + id, + ...otherPayload + } = payload; + + const saveData = getProviderState({ id, ...otherPayload }, getState, section, false); + + console.log(saveData); + + // we have to set id since not actually posting to server yet + if (!saveData.id) { + saveData.id = getNextId(getState().settings.downloadClientCategories.items); + } + + dispatch(batchActions([ + updateItem({ section, ...saveData }), + set({ + section, + pendingChanges: {} + }) + ])); + }, + + [DELETE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => { + const id = payload.id; + return dispatch(removeItem({ section, id })); + }, + + [DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => { + return dispatch(set({ + section, + items: [] + })); + }, + + [CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING]: (getState, payload, dispatch) => { + return dispatch(set({ + section, + pendingChanges: {} + })); + } + }, + + // + // Reducers + + reducers: { + [SET_DOWNLOAD_CLIENT_CATEGORY_VALUE]: createSetSettingValueReducer(section), + [SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE]: createSetProviderFieldValueReducer(section), + + [SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA]: (state, { payload }) => { + return selectProviderSchema(state, section, payload, (selectedSchema) => { + return selectedSchema; + }); + }, + + [CLEAR_DOWNLOAD_CLIENT_CATEGORIES]: createClearReducer(section, { + isPopulated: false, + error: null, + items: [] + }) + } +}; diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js index c18b4db76..7e9292f24 100644 --- a/frontend/src/Store/Actions/Settings/downloadClients.js +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -9,6 +9,7 @@ import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/ import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; import selectProviderSchema from 'Utilities/State/selectProviderSchema'; +import { set } from '../baseActions'; // // Variables @@ -90,10 +91,34 @@ export default { [FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'), [FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'), - [SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler(section, '/downloadclient'), + [SAVE_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => { + // move the format tags in as a pending change + const state = getState(); + const pendingChanges = state.settings.downloadClients.pendingChanges; + pendingChanges.categories = state.settings.downloadClientCategories.items; + dispatch(set({ + section, + pendingChanges + })); + + createSaveProviderHandler(section, '/downloadclient')(getState, payload, dispatch); + }, + [CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section), [DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'), - [TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'), + + [TEST_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => { + const state = getState(); + const pendingChanges = state.settings.downloadClients.pendingChanges; + pendingChanges.categories = state.settings.downloadClientCategories.items; + dispatch(set({ + section, + pendingChanges + })); + + createTestProviderHandler(section, '/downloadclient')(getState, payload, dispatch); + }, + [CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section), [TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient') }, diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 6aa4ad6be..77baa5785 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -4,6 +4,7 @@ import createHandleActions from './Creators/createHandleActions'; import applications from './Settings/applications'; import appProfiles from './Settings/appProfiles'; import development from './Settings/development'; +import downloadClientCategories from './Settings/downloadClientCategories'; import downloadClients from './Settings/downloadClients'; import general from './Settings/general'; import indexerCategories from './Settings/indexerCategories'; @@ -11,6 +12,7 @@ import indexerProxies from './Settings/indexerProxies'; import notifications from './Settings/notifications'; import ui from './Settings/ui'; +export * from './Settings/downloadClientCategories'; export * from './Settings/downloadClients'; export * from './Settings/general'; export * from './Settings/indexerCategories'; @@ -32,6 +34,7 @@ export const section = 'settings'; export const defaultState = { advancedSettings: false, + downloadClientCategories: downloadClientCategories.defaultState, downloadClients: downloadClients.defaultState, general: general.defaultState, indexerCategories: indexerCategories.defaultState, @@ -61,6 +64,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS); // Action Handlers export const actionHandlers = handleThunks({ + ...downloadClientCategories.actionHandlers, ...downloadClients.actionHandlers, ...general.actionHandlers, ...indexerCategories.actionHandlers, @@ -81,6 +85,7 @@ export const reducers = createHandleActions({ return Object.assign({}, state, { advancedSettings: !state.advancedSettings }); }, + ...downloadClientCategories.reducers, ...downloadClients.reducers, ...general.reducers, ...indexerCategories.reducers, diff --git a/src/NzbDrone.Core/Datastore/Migration/023_download_client_categories.cs b/src/NzbDrone.Core/Datastore/Migration/023_download_client_categories.cs new file mode 100644 index 000000000..4b27c8661 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/023_download_client_categories.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(023)] + public class download_client_categories : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("DownloadClients") + .AddColumn("Categories").AsString().WithDefaultValue("[]"); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 566dba21e..b26c016e6 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -60,7 +60,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity("DownloadClients").RegisterModel() .Ignore(x => x.ImplementationName) - .Ignore(i => i.Protocol) + .Ignore(d => d.SupportsCategories) + .Ignore(d => d.Protocol) .Ignore(d => d.Tags); Mapper.Entity("Notifications").RegisterModel() @@ -115,6 +116,7 @@ namespace NzbDrone.Core.Datastore SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); SqlMapper.AddTypeHandler(new OsPathConverter()); SqlMapper.RemoveTypeMap(typeof(Guid)); SqlMapper.RemoveTypeMap(typeof(Guid?)); diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs index 7eac71b7f..902985371 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -18,6 +18,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2 public override string Name => "Aria2"; + public override bool SupportsCategories => false; + public Aria2(IAria2Proxy proxy, ITorrentFileInfoReader torrentFileInfoReader, IHttpClient httpClient, diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs index 3315d43e9..5f6dd7a9d 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs @@ -74,6 +74,8 @@ namespace NzbDrone.Core.Download.Clients.Blackhole public override string Name => "Torrent Blackhole"; + public override bool SupportsCategories => false; + protected override void Test(List failures) { failures.AddIfNotNull(TestFolder(Settings.TorrentFolder, "TorrentFolder")); diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs index 7f0dd9c0b..0f0364e61 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -45,6 +45,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole } public override string Name => "Usenet Blackhole"; + public override bool SupportsCategories => false; protected override void Test(List failures) { diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 82fb83402..9ea829e6a 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.RegularExpressions; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; @@ -38,9 +39,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge } // _proxy.SetTorrentSeedingConfiguration(actualHash, remoteMovie.SeedConfiguration, Settings); - if (Settings.Category.IsNotNullOrWhiteSpace()) + var category = GetCategoryForRelease(release) ?? Settings.Category; + if (category.IsNotNullOrWhiteSpace()) { - _proxy.SetTorrentLabel(actualHash, Settings.Category, Settings); + _proxy.SetTorrentLabel(actualHash, category, Settings); } if (Settings.Priority == (int)DelugePriority.First) @@ -61,9 +63,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge } // _proxy.SetTorrentSeedingConfiguration(actualHash, release.SeedConfiguration, Settings); - if (Settings.Category.IsNotNullOrWhiteSpace()) + var category = GetCategoryForRelease(release) ?? Settings.Category; + if (category.IsNotNullOrWhiteSpace()) { - _proxy.SetTorrentLabel(actualHash, Settings.Category, Settings); + _proxy.SetTorrentLabel(actualHash, category, Settings); } if (Settings.Priority == (int)DelugePriority.First) @@ -75,6 +78,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge } public override string Name => "Deluge"; + public override bool SupportsCategories => true; protected override void Test(List failures) { @@ -139,7 +143,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge private ValidationFailure TestCategory() { - if (Settings.Category.IsNullOrWhiteSpace()) + if (Categories.Count == 0) { return null; } @@ -156,23 +160,42 @@ namespace NzbDrone.Core.Download.Clients.Deluge var labels = _proxy.GetAvailableLabels(Settings); - if (Settings.Category.IsNotNullOrWhiteSpace() && !labels.Contains(Settings.Category)) - { - _proxy.AddLabel(Settings.Category, Settings); - labels = _proxy.GetAvailableLabels(Settings); + var categories = Categories.Select(c => c.ClientCategory).ToList(); + categories.Add(Settings.Category); - if (!labels.Contains(Settings.Category)) + foreach (var category in categories) + { + if (category.IsNotNullOrWhiteSpace() && !labels.Contains(category)) { - return new NzbDroneValidationFailure("Category", "Configuration of label failed") + _proxy.AddLabel(category, Settings); + labels = _proxy.GetAvailableLabels(Settings); + + if (!labels.Contains(category)) { - DetailedDescription = "Prowlarr was unable to add the label to Deluge." - }; + return new NzbDroneValidationFailure("Category", "Configuration of label failed") + { + DetailedDescription = "Prowlarr was unable to add the label to Deluge." + }; + } } } return null; } + protected override void ValidateCategories(List failures) + { + base.ValidateCategories(failures); + + foreach (var label in Categories) + { + if (!Regex.IsMatch(label.ClientCategory, "^[-a-z0-9]*$")) + { + failures.AddIfNotNull(new ValidationFailure(string.Empty, "Mapped Categories allowed characters a-z, 0-9 and -")); + } + } + } + private ValidationFailure TestGetTorrents() { try diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs index 5248e6f20..6396052c3 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge string[] GetAvailablePlugins(DelugeSettings settings); string[] GetEnabledPlugins(DelugeSettings settings); string[] GetAvailableLabels(DelugeSettings settings); - DelugeLabel GetLabelOptions(DelugeSettings settings); + DelugeLabel GetLabelOptions(DelugeSettings settings, string label); void SetTorrentLabel(string hash, string label, DelugeSettings settings); void SetTorrentConfiguration(string hash, string key, object value, DelugeSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, DelugeSettings settings); @@ -158,9 +158,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge return response; } - public DelugeLabel GetLabelOptions(DelugeSettings settings) + public DelugeLabel GetLabelOptions(DelugeSettings settings, string label) { - var response = ProcessRequest(settings, "label.get_options", settings.Category); + var response = ProcessRequest(settings, "label.get_options", label); return response; } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index d9b7edc3e..bf463eb81 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(5, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback Category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] public string Category { get; set; } [FieldDefinition(6, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing items")] diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs index 497fd2fa8..bc3e8ca1c 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs @@ -18,9 +18,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation .When(c => c.TvDirectory.IsNotNullOrWhiteSpace()) .WithMessage("Cannot start with /"); - RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -"); + RuleFor(c => c.Category).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -"); - RuleFor(c => c.TvCategory).Empty() + RuleFor(c => c.Category).Empty() .When(c => c.TvDirectory.IsNotNullOrWhiteSpace()) .WithMessage("Cannot use Category and Directory"); } @@ -45,8 +45,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] - public string TvCategory { get; set; } + [FieldDefinition(5, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] + public string Category { get; set; } [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")] public string TvDirectory { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index dc759e9e5..5d3d0729e 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -44,6 +44,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } public override string Name => "Download Station"; + public override bool SupportsCategories => false; public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning); @@ -198,7 +199,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (downloadDir != null) { var sharedFolder = downloadDir.Split('\\', '/')[0]; - var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory); + var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.Category); var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings); @@ -311,11 +312,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { return Settings.TvDirectory.TrimStart('/'); } - else if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + else if (Settings.Category.IsNotNullOrWhiteSpace()) { var destDir = GetDefaultDir(); - return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}"; + return $"{destDir.TrimEnd('/')}/{Settings.Category}"; } return null; diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index c5c51433d..2cf42704b 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -42,6 +42,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } public override string Name => "Download Station"; + public override bool SupportsCategories => false; public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning); @@ -101,7 +102,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (downloadDir != null) { var sharedFolder = downloadDir.Split('\\', '/')[0]; - var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory); + var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.Category); var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings); @@ -272,11 +273,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { return Settings.TvDirectory.TrimStart('/'); } - else if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + else if (Settings.Category.IsNotNullOrWhiteSpace()) { var destDir = GetDefaultDir(); - return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}"; + return $"{destDir.TrimEnd('/')}/{Settings.Category}"; } return null; diff --git a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs index 8698500ea..f292eaf53 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Download.Clients.Flood _proxy = proxy; } - private static IEnumerable HandleTags(ReleaseInfo release, FloodSettings settings) + private static IEnumerable HandleTags(ReleaseInfo release, FloodSettings settings, string mappedCategory) { var result = new HashSet(); @@ -36,6 +36,11 @@ namespace NzbDrone.Core.Download.Clients.Flood result.UnionWith(settings.Tags); } + if (mappedCategory != null) + { + result.Add(mappedCategory); + } + if (settings.AdditionalTags.Any()) { foreach (var additionalTag in settings.AdditionalTags) @@ -55,18 +60,19 @@ namespace NzbDrone.Core.Download.Clients.Flood } public override string Name => "Flood"; + public override bool SupportsCategories => true; public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to remove torrents that have finished seeding when using Flood", ProviderMessageType.Warning); protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { - _proxy.AddTorrentByFile(Convert.ToBase64String(fileContent), HandleTags(release, Settings), Settings); + _proxy.AddTorrentByFile(Convert.ToBase64String(fileContent), HandleTags(release, Settings, GetCategoryForRelease(release)), Settings); return hash; } protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { - _proxy.AddTorrentByUrl(magnetLink, HandleTags(release, Settings), Settings); + _proxy.AddTorrentByUrl(magnetLink, HandleTags(release, Settings, GetCategoryForRelease(release)), Settings); return hash; } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index bfe9bdebf..a5ca2e4fc 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -27,6 +27,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken } public override string Name => "Hadouken"; + public override bool SupportsCategories => true; protected override void Test(List failures) { @@ -41,14 +42,14 @@ namespace NzbDrone.Core.Download.Clients.Hadouken protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { - _proxy.AddTorrentUri(Settings, magnetLink); + _proxy.AddTorrentUri(Settings, magnetLink, GetCategoryForRelease(release) ?? Settings.Category); return hash.ToUpper(); } protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { - return _proxy.AddTorrentFile(Settings, fileContent).ToUpper(); + return _proxy.AddTorrentFile(Settings, fileContent, GetCategoryForRelease(release) ?? Settings.Category).ToUpper(); } private ValidationFailure TestConnection() diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs index b695ef1da..a9704432a 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using NLog; @@ -13,8 +13,8 @@ namespace NzbDrone.Core.Download.Clients.Hadouken HadoukenSystemInfo GetSystemInfo(HadoukenSettings settings); HadoukenTorrent[] GetTorrents(HadoukenSettings settings); IReadOnlyDictionary GetConfig(HadoukenSettings settings); - string AddTorrentFile(HadoukenSettings settings, byte[] fileContent); - void AddTorrentUri(HadoukenSettings settings, string torrentUrl); + string AddTorrentFile(HadoukenSettings settings, byte[] fileContent, string label); + void AddTorrentUri(HadoukenSettings settings, string torrentUrl, string label); void RemoveTorrent(HadoukenSettings settings, string downloadId); void RemoveTorrentAndData(HadoukenSettings settings, string downloadId); } @@ -47,14 +47,14 @@ namespace NzbDrone.Core.Download.Clients.Hadouken return ProcessRequest>(settings, "webui.getSettings"); } - public string AddTorrentFile(HadoukenSettings settings, byte[] fileContent) + public string AddTorrentFile(HadoukenSettings settings, byte[] fileContent, string label) { - return ProcessRequest(settings, "webui.addTorrent", "file", Convert.ToBase64String(fileContent), new { label = settings.Category }); + return ProcessRequest(settings, "webui.addTorrent", "file", Convert.ToBase64String(fileContent), new { label }); } - public void AddTorrentUri(HadoukenSettings settings, string torrentUrl) + public void AddTorrentUri(HadoukenSettings settings, string torrentUrl, string label) { - ProcessRequest(settings, "webui.addTorrent", "url", torrentUrl, new { label = settings.Category }); + ProcessRequest(settings, "webui.addTorrent", "url", torrentUrl, new { label }); } public void RemoveTorrent(HadoukenSettings settings, string downloadId) diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs index df8ecfe38..c8e6f7763 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox)] + [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release.")] public string Category { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs index f2d68823d..e1088780d 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -29,8 +29,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContents) { var priority = Settings.Priority; + var category = GetCategoryForRelease(release) ?? Settings.Category; - var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings); + var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings, category); if (response == null) { @@ -41,6 +42,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex } public override string Name => "NZBVortex"; + public override bool SupportsCategories => true; protected List GetGroups() { @@ -111,19 +113,27 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex private ValidationFailure TestCategory() { - var group = GetGroups().FirstOrDefault(c => c.GroupName == Settings.Category); + var groups = GetGroups(); - if (group == null) + foreach (var category in Categories) { - if (Settings.Category.IsNotNullOrWhiteSpace()) + if (!category.ClientCategory.IsNullOrWhiteSpace() && !groups.Any(v => v.GroupName == category.ClientCategory)) { - return new NzbDroneValidationFailure("Category", "Group does not exist") + return new NzbDroneValidationFailure(string.Empty, "Group does not exist") { - DetailedDescription = "The Group you entered doesn't exist in NzbVortex. Go to NzbVortex to create it." + DetailedDescription = "A mapped category you entered doesn't exist in NzbVortex. Go to NzbVortex to create it." }; } } + if (!Settings.Category.IsNullOrWhiteSpace() && !groups.Any(v => v.GroupName == Settings.Category)) + { + return new NzbDroneValidationFailure("Category", "Category does not exist") + { + DetailedDescription = "The category you entered doesn't exist in NzbVortex. Go to NzbVortex to create it." + }; + } + return null; } diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs index aeb2d7ac5..11bbf6213 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex { public interface INzbVortexProxy { - string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings); + string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings, string group); void Remove(int id, bool deleteData, NzbVortexSettings settings); NzbVortexVersionResponse GetVersion(NzbVortexSettings settings); NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings); @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex _authSessionIdCache = cacheManager.GetCache(GetType(), "authCache"); } - public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings) + public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings, string group) { var requestBuilder = BuildRequest(settings).Resource("nzb/add") .Post() @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex if (settings.Category.IsNotNullOrWhiteSpace()) { - requestBuilder.AddQueryParam("groupname", settings.Category); + requestBuilder.AddQueryParam("groupname", group); } requestBuilder.AddFormUpload("name", filename, nzbData, "application/x-nzb"); diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs index c6b03f0e7..2dae1ac49 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex [FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(4, Label = "Group", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(4, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] public string Category { get; set; } [FieldDefinition(5, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing items")] diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index ecdbcf1cd..0aa160cda 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContent) { - var category = Settings.Category; + var category = GetCategoryForRelease(release) ?? Settings.Category; var priority = Settings.Priority; @@ -50,7 +50,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget protected override string AddFromLink(ReleaseInfo release) { - var category = Settings.Category; + var category = GetCategoryForRelease(release) ?? Settings.Category; var priority = Settings.Priority; @@ -66,6 +66,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget } public override string Name => "NZBGet"; + public override bool SupportsCategories => true; protected IEnumerable GetCategories(Dictionary config) { @@ -139,6 +140,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var config = _proxy.GetConfig(Settings); var categories = GetCategories(config); + foreach (var category in Categories) + { + if (!category.ClientCategory.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == category.ClientCategory)) + { + return new NzbDroneValidationFailure(string.Empty, "Category does not exist") + { + InfoLink = _proxy.GetBaseUrl(Settings), + DetailedDescription = "A mapped category you entered doesn't exist in NZBGet. Go to NZBGet to create it." + }; + } + } + if (!Settings.Category.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == Settings.Category)) { return new NzbDroneValidationFailure("Category", "Category does not exist") diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index 21ba2167b..0e1cbc912 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] public string Category { get; set; } [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority for items added from Prowlarr")] diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 1d9a4963d..1e98734f7 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic } public override string Name => "Pneumatic"; + public override bool SupportsCategories => false; public override DownloadProtocol Protocol => DownloadProtocol.Usenet; diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 70d17228b..95dd9e30b 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -52,8 +52,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent //var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); var itemToTop = Settings.Priority == (int)QBittorrentPriority.First; var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart; + var category = GetCategoryForRelease(release) ?? Settings.Category; - Proxy.AddTorrentFromUrl(magnetLink, null, Settings); + Proxy.AddTorrentFromUrl(magnetLink, null, Settings, category); if (itemToTop || forceStart) { @@ -100,8 +101,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent //var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); var itemToTop = Settings.Priority == (int)QBittorrentPriority.First; var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart; + var category = GetCategoryForRelease(release) ?? Settings.Category; - Proxy.AddTorrentFromFile(filename, fileContent, null, Settings); + Proxy.AddTorrentFromFile(filename, fileContent, null, Settings, category); if (itemToTop || forceStart) { @@ -167,6 +169,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } public override string Name => "qBittorrent"; + public override bool SupportsCategories => true; protected override void Test(List failures) { @@ -197,7 +200,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent else if (version < Version.Parse("1.6")) { // API version 6 introduced support for labels - if (Settings.Category.IsNotNullOrWhiteSpace()) + if (Settings.Category.IsNotNullOrWhiteSpace() || Categories.Count > 0) { return new NzbDroneValidationFailure("Category", "Category is not supported") { @@ -205,15 +208,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent }; } } - else if (Settings.Category.IsNullOrWhiteSpace()) - { - // warn if labels are supported, but category is not provided - return new NzbDroneValidationFailure("Category", "Category is recommended") - { - IsWarning = true, - DetailedDescription = "Prowlarr will not attempt to import completed downloads without a category." - }; - } } catch (DownloadClientAuthenticationException ex) { @@ -251,7 +245,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent private ValidationFailure TestCategory() { - if (Settings.Category.IsNullOrWhiteSpace()) + if (Settings.Category.IsNullOrWhiteSpace() && Categories.Count == 0) { return null; } @@ -265,6 +259,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent Dictionary labels = Proxy.GetLabels(Settings); + foreach (var category in Categories) + { + if (category.ClientCategory.IsNotNullOrWhiteSpace() && !labels.ContainsKey(category.ClientCategory)) + { + Proxy.AddLabel(category.ClientCategory, Settings); + labels = Proxy.GetLabels(Settings); + + if (!labels.ContainsKey(category.ClientCategory)) + { + return new NzbDroneValidationFailure(string.Empty, "Configuration of label failed") + { + DetailedDescription = "Prowlarr was unable to add the label to qBittorrent." + }; + } + } + } + if (Settings.Category.IsNotNullOrWhiteSpace() && !labels.ContainsKey(Settings.Category)) { Proxy.AddLabel(Settings.Category, Settings); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs index 158db804e..2460b3239 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -18,8 +18,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings); List GetTorrentFiles(string hash, QBittorrentSettings settings); - void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); - void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); + void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category); + void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category); void RemoveTorrent(string hash, bool removeData, QBittorrentSettings settings); void SetTorrentLabel(string hash, string label, QBittorrentSettings settings); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index e8b1c2e14..8bf6c1878 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -113,15 +113,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return response; } - public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) + public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category) { var request = BuildRequest(settings).Resource("/command/download") .Post() .AddFormParameter("urls", torrentUrl); - if (settings.Category.IsNotNullOrWhiteSpace()) + if (category.IsNotNullOrWhiteSpace()) { - request.AddFormParameter("category", settings.Category); + request.AddFormParameter("category", category); } // Note: ForceStart is handled by separate api call @@ -143,15 +143,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) + public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category) { var request = BuildRequest(settings).Resource("/command/upload") .Post() .AddFormUpload("torrents", fileName, fileContent); - if (settings.Category.IsNotNullOrWhiteSpace()) + if (category.IsNotNullOrWhiteSpace()) { - request.AddFormParameter("category", settings.Category); + request.AddFormParameter("category", category); } // Note: ForceStart is handled by separate api call diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index ed5165e67..c1d789e2d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -119,14 +119,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return response; } - public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) + public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category) { var request = BuildRequest(settings).Resource("/api/v2/torrents/add") .Post() .AddFormParameter("urls", torrentUrl); - if (settings.Category.IsNotNullOrWhiteSpace()) + if (category.IsNotNullOrWhiteSpace()) { - request.AddFormParameter("category", settings.Category); + request.AddFormParameter("category", category); } // Note: ForceStart is handled by separate api call @@ -153,15 +153,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) + public void AddTorrentFromFile(string fileName, byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings, string category) { var request = BuildRequest(settings).Resource("/api/v2/torrents/add") .Post() .AddFormUpload("torrents", fileName, fileContent); - if (settings.Category.IsNotNullOrWhiteSpace()) + if (category.IsNotNullOrWhiteSpace()) { - request.AddFormParameter("category", settings.Category); + request.AddFormParameter("category", category); } // Note: ForceStart is handled by separate api call diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index a18d14c34..d0c46c0c5 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] public string Category { get; set; } [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing items")] diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index ff3f23136..584e392b2 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContent) { - var category = Settings.Category; + var category = GetCategoryForRelease(release) ?? Settings.Category; var priority = Settings.Priority; var response = _proxy.DownloadNzb(fileContent, filename, category, priority, Settings); @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd protected override string AddFromLink(ReleaseInfo release) { - var category = Settings.Category; + var category = GetCategoryForRelease(release) ?? Settings.Category; var priority = Settings.Priority; var response = _proxy.DownloadNzbByUrl(release.DownloadUrl, category, priority, Settings); @@ -62,6 +62,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } public override string Name => "SABnzbd"; + public override bool SupportsCategories => true; protected IEnumerable GetCategories(SabnzbdConfig config) { @@ -260,29 +261,27 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd private ValidationFailure TestCategory() { var config = _proxy.GetConfig(Settings); - var category = GetCategories(config).FirstOrDefault((SabnzbdCategory v) => v.Name == Settings.Category); + var categories = GetCategories(config); - if (category != null) + foreach (var category in Categories) { - if (category.Dir.EndsWith("*")) + if (!category.ClientCategory.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == category.ClientCategory)) { - return new NzbDroneValidationFailure("Category", "Enable Job folders") + return new NzbDroneValidationFailure(string.Empty, "Category does not exist") { - InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"), - DetailedDescription = "Prowlarr prefers each download to have a separate folder. With * appended to the Folder/Path SABnzbd will not create these job folders. Go to SABnzbd to fix it." + InfoLink = _proxy.GetBaseUrl(Settings), + DetailedDescription = "A mapped category you entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it." }; } } - else + + if (!Settings.Category.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == Settings.Category)) { - if (!Settings.Category.IsNullOrWhiteSpace()) + return new NzbDroneValidationFailure("Category", "Category does not exist") { - return new NzbDroneValidationFailure("Category", "Category does not exist") - { - InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"), - DetailedDescription = "The category you entered doesn't exist in SABnzbd. Go to SABnzbd to create it." - }; - } + InfoLink = _proxy.GetBaseUrl(Settings), + DetailedDescription = "The category you entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it." + }; } if (config.Misc.enable_tv_sorting && ContainsCategory(config.Misc.tv_categories, Settings.Category)) diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 024f55a81..37df94bd5 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -65,7 +65,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(6, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(7, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] public string Category { get; set; } [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing items")] diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs index a6f8ec3a1..fac544a20 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs @@ -38,5 +38,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission } public override string Name => "Transmission"; + public override bool SupportsCategories => false; } } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 0759cf1ea..4d2682ee6 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] + [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] public string Category { get; set; } [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")] diff --git a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs index d52b3eb46..bd1f4c37b 100644 --- a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs +++ b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs @@ -58,5 +58,6 @@ namespace NzbDrone.Core.Download.Clients.Vuze } public override string Name => "Vuze"; + public override bool SupportsCategories => false; } } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index 091ac1242..9f7505cfd 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent { var priority = (RTorrentPriority)Settings.Priority; - _proxy.AddTorrentFromUrl(magnetLink, Settings.Category, priority, Settings.Directory, Settings); + _proxy.AddTorrentFromUrl(magnetLink, GetCategoryForRelease(release) ?? Settings.Category, priority, Settings.Directory, Settings); var tries = 10; var retryDelay = 500; @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent { var priority = (RTorrentPriority)Settings.Priority; - _proxy.AddTorrentFromFile(filename, fileContent, Settings.Category, priority, Settings.Directory, Settings); + _proxy.AddTorrentFromFile(filename, fileContent, GetCategoryForRelease(release) ?? Settings.Category, priority, Settings.Directory, Settings); var tries = 10; var retryDelay = 500; @@ -73,6 +73,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent } public override string Name => "rTorrent"; + public override bool SupportsCategories => true; public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to remove torrents that have finished seeding when using rTorrent", ProviderMessageType.Warning); diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index 73fbd43bf..e2ab37fe2 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional.")] + [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional.")] public string Category { get; set; } [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")] diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index 47951fe70..9b92312d5 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -38,9 +38,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent _proxy.AddTorrentFromUrl(magnetLink, Settings); //_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); - if (Settings.Category.IsNotNullOrWhiteSpace()) + var category = GetCategoryForRelease(release) ?? Settings.Category; + if (GetCategoryForRelease(release).IsNotNullOrWhiteSpace()) { - _proxy.SetTorrentLabel(hash, Settings.Category, Settings); + _proxy.SetTorrentLabel(hash, category, Settings); } if (Settings.Priority == (int)UTorrentPriority.First) @@ -58,9 +59,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent _proxy.AddTorrentFromFile(filename, fileContent, Settings); //_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); - if (Settings.Category.IsNotNullOrWhiteSpace()) + var category = GetCategoryForRelease(release) ?? Settings.Category; + if (category.IsNotNullOrWhiteSpace()) { - _proxy.SetTorrentLabel(hash, Settings.Category, Settings); + _proxy.SetTorrentLabel(hash, category, Settings); } if (Settings.Priority == (int)UTorrentPriority.First) @@ -74,40 +76,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } public override string Name => "uTorrent"; - - private List GetTorrents() - { - List torrents; - - var cacheKey = string.Format("{0}:{1}:{2}", Settings.Host, Settings.Port, Settings.Category); - var cache = _torrentCache.Find(cacheKey); - - var response = _proxy.GetTorrents(cache == null ? null : cache.CacheID, Settings); - - if (cache != null && response.Torrents == null) - { - var removedAndUpdated = new HashSet(response.TorrentsChanged.Select(v => v.Hash).Concat(response.TorrentsRemoved)); - - torrents = cache.Torrents - .Where(v => !removedAndUpdated.Contains(v.Hash)) - .Concat(response.TorrentsChanged) - .ToList(); - } - else - { - torrents = response.Torrents; - } - - cache = new UTorrentTorrentCache - { - CacheID = response.CacheNumber, - Torrents = torrents - }; - - _torrentCache.Set(cacheKey, cache, TimeSpan.FromMinutes(15)); - - return torrents; - } + public override bool SupportsCategories => true; protected override void Test(List failures) { diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index 19388bf5d..d3b72c04d 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] public string Category { get; set; } [FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing items")] diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 395cd491b..9b3741587 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -1,14 +1,17 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; +using Org.BouncyCastle.Crypto.Tls; namespace NzbDrone.Core.Download { @@ -50,6 +53,9 @@ namespace NzbDrone.Core.Download return GetType().Name; } + protected List Categories => ((DownloadClientDefinition)Definition).Categories; + public abstract bool SupportsCategories { get; } + public abstract DownloadProtocol Protocol { get; @@ -57,12 +63,54 @@ namespace NzbDrone.Core.Download public abstract Task Download(ReleaseInfo release, bool redirect, IIndexer indexer); + protected string GetCategoryForRelease(ReleaseInfo release) + { + var categories = ((DownloadClientDefinition)Definition).Categories; + if (categories.Count == 0) + { + return null; + } + + // Check for direct mapping + var category = categories.FirstOrDefault(x => x.Categories.Intersect(release.Categories.Select(c => c.Id)).Any())?.ClientCategory; + + // Check for parent mapping + if (category == null) + { + foreach (var cat in categories) + { + var mappedCat = NewznabStandardCategory.AllCats.Where(x => cat.Categories.Contains(x.Id)); + var subCats = mappedCat.SelectMany(x => x.SubCategories); + + if (subCats.Intersect(release.Categories).Any()) + { + category = cat.ClientCategory; + break; + } + } + } + + return category; + } + + protected virtual void ValidateCategories(List failures) + { + foreach (var category in ((DownloadClientDefinition)Definition).Categories) + { + if (category.ClientCategory.IsNullOrWhiteSpace()) + { + failures.AddIfNotNull(new ValidationFailure(string.Empty, "Category can not be empty")); + } + } + } + public ValidationResult Test() { var failures = new List(); try { + ValidateCategories(failures); Test(failures); } catch (Exception ex) @@ -97,5 +145,17 @@ namespace NzbDrone.Core.Download return null; } + + private bool HasConcreteImplementation(string methodName) + { + var method = GetType().GetMethod(methodName); + + if (method == null) + { + throw new MissingMethodException(GetType().Name, Name); + } + + return !method.DeclaringType.IsAbstract; + } } } diff --git a/src/NzbDrone.Core/Download/DownloadClientCategory.cs b/src/NzbDrone.Core/Download/DownloadClientCategory.cs new file mode 100644 index 000000000..b99e6fa4f --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientCategory.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NzbDrone.Core.Download +{ + public class DownloadClientCategory + { + public string ClientCategory { get; set; } + public List Categories { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientDefinition.cs b/src/NzbDrone.Core/Download/DownloadClientDefinition.cs index 1c0dfa927..ed2add70d 100644 --- a/src/NzbDrone.Core/Download/DownloadClientDefinition.cs +++ b/src/NzbDrone.Core/Download/DownloadClientDefinition.cs @@ -1,10 +1,18 @@ -using NzbDrone.Core.Indexers; +using System.Collections.Generic; +using NzbDrone.Core.Indexers; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Download { public class DownloadClientDefinition : ProviderDefinition { + public DownloadClientDefinition() + { + Categories = new List(); + } + + public List Categories { get; set; } + public bool SupportsCategories { get; set; } public DownloadProtocol Protocol { get; set; } public int Priority { get; set; } = 1; } diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index d69374794..f5cd9f005 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -40,6 +40,7 @@ namespace NzbDrone.Core.Download base.SetProviderCharacteristics(provider, definition); definition.Protocol = provider.Protocol; + definition.SupportsCategories = provider.SupportsCategories; } public List DownloadHandlingEnabled(bool filterBlockedClients = true) diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index 0b6d0664f..6c7bcf862 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; @@ -7,6 +8,7 @@ namespace NzbDrone.Core.Download { public interface IDownloadClient : IProvider { + bool SupportsCategories { get; } DownloadProtocol Protocol { get; } Task Download(ReleaseInfo release, bool redirect, IIndexer indexer); } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 56cb6cc95..5a2bf2838 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -39,6 +39,7 @@ "AppProfileSelectHelpText": "App profiles are used to control RSS, Automatic Search and Interactive Search settings on application sync", "Apps": "Apps", "AppSettingsSummary": "Applications and settings to configure how Prowlarr interacts with your PVR programs", + "AreYouSureYouWantToDeleteCategory": "Are you sure you want to delete mapped category?", "AreYouSureYouWantToResetYourAPIKey": "Are you sure you want to reset your API Key?", "AudioSearch": "Audio Search", "Auth": "Auth", @@ -97,6 +98,7 @@ "DeleteAppProfile": "Delete App Profile", "DeleteBackup": "Delete Backup", "DeleteBackupMessageText": "Are you sure you want to delete the backup '{0}'?", + "DeleteClientCategory": "Delete Download Client Category", "DeleteDownloadClient": "Delete Download Client", "DeleteDownloadClientMessageText": "Are you sure you want to delete the download client '{0}'?", "DeleteIndexerProxy": "Delete Indexer Proxy", @@ -113,6 +115,7 @@ "Docker": "Docker", "Donations": "Donations", "DownloadClient": "Download Client", + "DownloadClientCategory": "Download Client Category", "DownloadClients": "Download Clients", "DownloadClientSettings": "Download Client Settings", "DownloadClientsSettingsSummary": "Download clients configuration for integration into Prowlarr UI search", @@ -226,6 +229,7 @@ "Logs": "Logs", "MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details", "Manual": "Manual", + "MappedCategories": "Mapped Categories", "MappedDrivesRunningAsService": "Mapped network drives are not available when running as a Windows Service. Please see the FAQ for more information", "MassEditor": "Mass Editor", "Mechanism": "Mechanism", diff --git a/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs b/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs index 63de27da2..7376d6fe4 100644 --- a/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs +++ b/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; @@ -8,6 +9,8 @@ namespace Prowlarr.Api.V1.DownloadClient public bool Enable { get; set; } public DownloadProtocol Protocol { get; set; } public int Priority { get; set; } + public List Categories { get; set; } + public bool SupportsCategories { get; set; } } public class DownloadClientResourceMapper : ProviderResourceMapper @@ -24,6 +27,8 @@ namespace Prowlarr.Api.V1.DownloadClient resource.Enable = definition.Enable; resource.Protocol = definition.Protocol; resource.Priority = definition.Priority; + resource.Categories = definition.Categories; + resource.SupportsCategories = definition.SupportsCategories; return resource; } @@ -40,6 +45,7 @@ namespace Prowlarr.Api.V1.DownloadClient definition.Enable = resource.Enable; definition.Protocol = resource.Protocol; definition.Priority = resource.Priority; + definition.Categories = resource.Categories; return definition; }