From 345854d0fe9b65a561fdab12aac688782a420aa5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 22 Jan 2024 20:56:35 -0800 Subject: [PATCH] New: Optionally remove from queue by changing category to 'Post-Import Category' when configured Closes #6023 --- frontend/src/Activity/Queue/Queue.js | 13 +- frontend/src/Activity/Queue/QueueRow.js | 3 + ...temsModal.css => RemoveQueueItemModal.css} | 0 ...css.d.ts => RemoveQueueItemModal.css.d.ts} | 0 .../Activity/Queue/RemoveQueueItemModal.js | 171 ------------- .../Activity/Queue/RemoveQueueItemModal.tsx | 230 ++++++++++++++++++ .../Activity/Queue/RemoveQueueItemsModal.js | 174 ------------- .../src/Components/Form/FormInputGroup.js | 1 + frontend/src/Store/Actions/queueActions.js | 10 +- .../Download/Clients/Aria2/Aria2.cs | 2 +- .../Clients/Blackhole/TorrentBlackhole.cs | 2 +- .../Clients/Blackhole/UsenetBlackhole.cs | 2 +- .../Download/Clients/Deluge/Deluge.cs | 2 +- .../DownloadStation/TorrentDownloadStation.cs | 2 +- .../DownloadStation/UsenetDownloadStation.cs | 2 +- .../Download/Clients/Flood/Flood.cs | 2 +- .../FreeboxDownload/TorrentFreeboxDownload.cs | 2 +- .../Download/Clients/Hadouken/Hadouken.cs | 2 +- .../Download/Clients/NzbVortex/NzbVortex.cs | 2 +- .../Download/Clients/Nzbget/Nzbget.cs | 4 +- .../Download/Clients/Pneumatic/Pneumatic.cs | 2 +- .../Clients/QBittorrent/QBittorrent.cs | 2 +- .../Download/Clients/Sabnzbd/Sabnzbd.cs | 4 +- .../Clients/Transmission/TransmissionBase.cs | 2 +- .../Download/Clients/rTorrent/RTorrent.cs | 2 +- .../Download/Clients/uTorrent/UTorrent.cs | 2 +- .../Download/DownloadClientItem.cs | 6 +- src/NzbDrone.Core/Localization/Core/en.json | 25 +- src/NzbDrone.Core/Queue/Queue.cs | 1 + src/NzbDrone.Core/Queue/QueueService.cs | 3 +- src/Sonarr.Api.V3/Queue/QueueController.cs | 23 +- src/Sonarr.Api.V3/Queue/QueueResource.cs | 2 + 32 files changed, 317 insertions(+), 383 deletions(-) rename frontend/src/Activity/Queue/{RemoveQueueItemsModal.css => RemoveQueueItemModal.css} (100%) rename frontend/src/Activity/Queue/{RemoveQueueItemsModal.css.d.ts => RemoveQueueItemModal.css.d.ts} (100%) delete mode 100644 frontend/src/Activity/Queue/RemoveQueueItemModal.js create mode 100644 frontend/src/Activity/Queue/RemoveQueueItemModal.tsx delete mode 100644 frontend/src/Activity/Queue/RemoveQueueItemsModal.js diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js index 633357b7e..30f5260cb 100644 --- a/frontend/src/Activity/Queue/Queue.js +++ b/frontend/src/Activity/Queue/Queue.js @@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected'; import QueueFilterModal from './QueueFilterModal'; import QueueOptionsConnector from './QueueOptionsConnector'; import QueueRowConnector from './QueueRowConnector'; -import RemoveQueueItemsModal from './RemoveQueueItemsModal'; +import RemoveQueueItemModal from './RemoveQueueItemModal'; class Queue extends Component { @@ -305,9 +305,16 @@ class Queue extends Component { } - { + const item = items.find((i) => i.id === id); + + return !!(item && item.downloadClientHasPostImportCategory); + }) + )} canIgnore={isConfirmRemoveModalOpen && ( selectedIds.every((id) => { const item = items.find((i) => i.id === id); @@ -315,7 +322,7 @@ class Queue extends Component { return !!(item && item.seriesId && item.episodeId); }) )} - allPending={isConfirmRemoveModalOpen && ( + pending={isConfirmRemoveModalOpen && ( selectedIds.every((id) => { const item = items.find((i) => i.id === id); diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index 95ff2527e..f143ace3f 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -99,6 +99,7 @@ class QueueRow extends Component { indexer, outputPath, downloadClient, + downloadClientHasPostImportCategory, estimatedCompletionTime, added, timeleft, @@ -420,6 +421,7 @@ class QueueRow extends Component { { - this.setState({ remove: value }); - }; - - onBlocklistChange = ({ value }) => { - this.setState({ blocklist: value }); - }; - - onSkipRedownloadChange = ({ value }) => { - this.setState({ skipRedownload: value }); - }; - - onRemoveConfirmed = () => { - const state = this.state; - - this.resetState(); - this.props.onRemovePress(state); - }; - - onModalClose = () => { - this.resetState(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - const { - isOpen, - sourceTitle, - canIgnore, - isPending - } = this.props; - - const { remove, blocklist, skipRedownload } = this.state; - - return ( - - - - {translate('RemoveQueueItem', { sourceTitle })} - - - -
- {translate('RemoveQueueItemConfirmation', { sourceTitle })} -
- - { - isPending ? - null : - - {translate('RemoveFromDownloadClient')} - - - - } - - - {translate('BlocklistRelease')} - - - - - { - blocklist ? - - {translate('SkipRedownload')} - - : - null - } -
- - - - - - -
-
- ); - } -} - -RemoveQueueItemModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - sourceTitle: PropTypes.string.isRequired, - canIgnore: PropTypes.bool.isRequired, - isPending: PropTypes.bool.isRequired, - onRemovePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default RemoveQueueItemModal; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx b/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx new file mode 100644 index 000000000..4348f818c --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.tsx @@ -0,0 +1,230 @@ +import React, { useCallback, useMemo, useState } from 'react'; +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 Modal from 'Components/Modal/Modal'; +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, sizes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './RemoveQueueItemModal.css'; + +interface RemovePressProps { + remove: boolean; + changeCategory: boolean; + blocklist: boolean; + skipRedownload: boolean; +} + +interface RemoveQueueItemModalProps { + isOpen: boolean; + sourceTitle: string; + canChangeCategory: boolean; + canIgnore: boolean; + isPending: boolean; + selectedCount?: number; + onRemovePress(props: RemovePressProps): void; + onModalClose: () => void; +} + +type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore'; +type BlocklistMethod = + | 'doNotBlocklist' + | 'blocklistAndSearch' + | 'blocklistOnly'; + +function RemoveQueueItemModal(props: RemoveQueueItemModalProps) { + const { + isOpen, + sourceTitle, + canIgnore, + canChangeCategory, + isPending, + selectedCount, + onRemovePress, + onModalClose, + } = props; + + const multipleSelected = selectedCount && selectedCount > 1; + + const [removalMethod, setRemovalMethod] = + useState('removeFromClient'); + const [blocklistMethod, setBlocklistMethod] = + useState('doNotBlocklist'); + + const { title, message } = useMemo(() => { + if (!selectedCount) { + return { + title: translate('RemoveQueueItem', { sourceTitle }), + message: translate('RemoveQueueItemConfirmation', { sourceTitle }), + }; + } + + if (selectedCount === 1) { + return { + title: translate('RemoveSelectedItem'), + message: translate('RemoveSelectedItemQueueMessageText'), + }; + } + + return { + title: translate('RemoveSelectedItems'), + message: translate('RemoveSelectedItemsQueueMessageText', { + selectedCount, + }), + }; + }, [sourceTitle, selectedCount]); + + const removalMethodOptions = useMemo(() => { + return [ + { + key: 'removeFromClient', + value: translate('RemoveFromDownloadClient'), + hint: multipleSelected + ? translate('RemoveMultipleFromDownloadClientHint') + : translate('RemoveFromDownloadClientHint'), + }, + { + key: 'changeCategory', + value: translate('ChangeCategory'), + isDisabled: !canChangeCategory, + hint: multipleSelected + ? translate('ChangeCategoryMultipleHint') + : translate('ChangeCategoryHint'), + }, + { + key: 'ignore', + value: multipleSelected + ? translate('IgnoreDownloads') + : translate('IgnoreDownload'), + isDisabled: !canIgnore, + hint: multipleSelected + ? translate('IgnoreDownloadsHint') + : translate('IgnoreDownloadHint'), + }, + ]; + }, [canChangeCategory, canIgnore, multipleSelected]); + + const blocklistMethodOptions = useMemo(() => { + return [ + { + key: 'doNotBlocklist', + value: translate('DoNotBlocklist'), + hint: translate('DoNotBlocklistHint'), + }, + { + key: 'blocklistAndSearch', + value: translate('BlocklistAndSearch'), + hint: multipleSelected + ? translate('BlocklistAndSearchMultipleHint') + : translate('BlocklistAndSearchHint'), + }, + { + key: 'blocklistOnly', + value: translate('BlocklistOnly'), + hint: multipleSelected + ? translate('BlocklistMultipleOnlyHint') + : translate('BlocklistOnlyHint'), + }, + ]; + }, [multipleSelected]); + + const handleRemovalMethodChange = useCallback( + ({ value }: { value: RemovalMethod }) => { + setRemovalMethod(value); + }, + [setRemovalMethod] + ); + + const handleBlocklistMethodChange = useCallback( + ({ value }: { value: BlocklistMethod }) => { + setBlocklistMethod(value); + }, + [setBlocklistMethod] + ); + + const handleConfirmRemove = useCallback(() => { + onRemovePress({ + remove: removalMethod === 'removeFromClient', + changeCategory: removalMethod === 'changeCategory', + blocklist: blocklistMethod !== 'doNotBlocklist', + skipRedownload: blocklistMethod === 'blocklistOnly', + }); + + setRemovalMethod('removeFromClient'); + setBlocklistMethod('doNotBlocklist'); + }, [ + removalMethod, + blocklistMethod, + setRemovalMethod, + setBlocklistMethod, + onRemovePress, + ]); + + const handleModalClose = useCallback(() => { + setRemovalMethod('removeFromClient'); + setBlocklistMethod('doNotBlocklist'); + + onModalClose(); + }, [setRemovalMethod, setBlocklistMethod, onModalClose]); + + return ( + + + {title} + + +
{message}
+ + {isPending ? null : ( + + {translate('RemoveQueueItemRemovalMethod')} + + + + )} + + + + {multipleSelected + ? translate('BlocklistReleases') + : translate('BlocklistRelease')} + + + + +
+ + + + + + +
+
+ ); +} + +export default RemoveQueueItemModal; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js deleted file mode 100644 index 18ea39aea..000000000 --- a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js +++ /dev/null @@ -1,174 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -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 Modal from 'Components/Modal/Modal'; -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, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './RemoveQueueItemsModal.css'; - -class RemoveQueueItemsModal extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - remove: true, - blocklist: false, - skipRedownload: false - }; - } - - // - // Control - - resetState = function() { - this.setState({ - remove: true, - blocklist: false, - skipRedownload: false - }); - }; - - // - // Listeners - - onRemoveChange = ({ value }) => { - this.setState({ remove: value }); - }; - - onBlocklistChange = ({ value }) => { - this.setState({ blocklist: value }); - }; - - onSkipRedownloadChange = ({ value }) => { - this.setState({ skipRedownload: value }); - }; - - onRemoveConfirmed = () => { - const state = this.state; - - this.resetState(); - this.props.onRemovePress(state); - }; - - onModalClose = () => { - this.resetState(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - const { - isOpen, - selectedCount, - canIgnore, - allPending - } = this.props; - - const { remove, blocklist, skipRedownload } = this.state; - - return ( - - - - {selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')} - - - -
- {selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')} -
- - { - allPending ? - null : - - {translate('RemoveFromDownloadClient')} - - - - } - - - - {selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')} - - - - - - { - blocklist ? - - {translate('SkipRedownload')} - - : - null - } -
- - - - - - -
-
- ); - } -} - -RemoveQueueItemsModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - selectedCount: PropTypes.number.isRequired, - canIgnore: PropTypes.bool.isRequired, - allPending: PropTypes.bool.isRequired, - onRemovePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default RemoveQueueItemsModal; diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 49f08c90b..d3b3eb206 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -264,6 +264,7 @@ FormInputGroup.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.any, values: PropTypes.arrayOf(PropTypes.any), + isDisabled: PropTypes.bool, type: PropTypes.string.isRequired, kind: PropTypes.oneOf(kinds.all), min: PropTypes.number, diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index fa4c3c473..dff490d12 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -430,13 +430,14 @@ export const actionHandlers = handleThunks({ id, remove, blocklist, - skipRedownload + skipRedownload, + changeCategory } = payload; dispatch(updateItem({ section: paged, id, isRemoving: true })); const promise = createAjaxRequest({ - url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`, + url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`, method: 'DELETE' }).request; @@ -454,7 +455,8 @@ export const actionHandlers = handleThunks({ ids, remove, blocklist, - skipRedownload + skipRedownload, + changeCategory } = payload; dispatch(batchActions([ @@ -470,7 +472,7 @@ export const actionHandlers = handleThunks({ ])); const promise = createAjaxRequest({ - url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`, + url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`, method: 'DELETE', dataType: 'json', contentType: 'application/json', diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs index 621b8937e..970d09d35 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -134,7 +134,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2 CanMoveFiles = false, CanBeRemoved = torrent.Status == "complete", Category = null, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = torrent.InfoHash?.ToUpper(), IsEncrypted = false, Message = torrent.ErrorMessage, diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs index 282ededa1..8364a1fb2 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { yield return new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = Definition.Name + "_" + item.DownloadId, Category = "sonarr", Title = item.Title, diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs index 3a7105ba9..e1eb75905 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { yield return new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = Definition.Name + "_" + item.DownloadId, Category = "sonarr", Title = item.Title, diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 10716c699..3856e7a70 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -137,7 +137,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge item.Title = torrent.Name; item.Category = Settings.TvCategory; - item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace()); var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath)); item.OutputPath = outputPath + torrent.Name; diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index 612be692d..8ecda831e 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation var item = new DownloadClientItem() { Category = Settings.TvCategory, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = CreateDownloadId(torrent.Id, serialNumber), Title = torrent.Title, TotalSize = torrent.Size, diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index 6f89845a9..0571847e2 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation var item = new DownloadClientItem() { Category = Settings.TvCategory, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = CreateDownloadId(nzb.Id, serialNumber), Title = nzb.Title, TotalSize = nzb.Size, diff --git a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs index b770792e1..60b153441 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Download.Clients.Flood var item = new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = torrent.Key, Title = properties.Name, OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(properties.Directory)), diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs index 07f435a34..88248e4b5 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload Category = Settings.Category, Title = torrent.Name, TotalSize = torrent.Size, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), RemainingSize = (long)(torrent.Size * (double)(1 - ((double)torrent.ReceivedPrct / 10000))), RemainingTime = torrent.Eta <= 0 ? null : TimeSpan.FromSeconds(torrent.Eta), SeedRatio = torrent.StopRatio <= 0 ? 0 : torrent.StopRatio / 100, diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index a29be7f4c..59f28e34d 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken var item = new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = torrent.InfoHash.ToUpper(), OutputPath = outputPath + torrent.Name, RemainingSize = torrent.TotalSize - torrent.DownloadedBytes, diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs index dbdfdb7c4..2a12fc364 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex { var queueItem = new DownloadClientItem(); - queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); queueItem.DownloadId = vortexQueueItem.AddUUID ?? vortexQueueItem.Id.ToString(); queueItem.Category = vortexQueueItem.GroupName; queueItem.Title = vortexQueueItem.UiTitle; diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index d7956318e..fc14c1496 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget queueItem.Title = item.NzbName; queueItem.TotalSize = totalSize; queueItem.Category = item.Category; - queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); queueItem.CanMoveFiles = true; queueItem.CanBeRemoved = true; @@ -120,7 +120,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var historyItem = new DownloadClientItem(); var itemDir = item.FinalDir.IsNullOrWhiteSpace() ? item.DestDir : item.FinalDir; - historyItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + historyItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); historyItem.DownloadId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); historyItem.Title = item.Name; historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 920279263..6797c0b0e 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic var historyItem = new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = GetDownloadClientId(file), Title = title, diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index e523992ad..e8917a0c9 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -231,7 +231,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label, Title = torrent.Name, TotalSize = torrent.Size, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace()), RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)), RemainingTime = GetRemainingTime(torrent), SeedRatio = torrent.Ratio diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 5d9003849..a1c856cfb 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -65,7 +65,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } var queueItem = new DownloadClientItem(); - queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); queueItem.DownloadId = sabQueueItem.Id; queueItem.Category = sabQueueItem.Category; queueItem.Title = sabQueueItem.Title; @@ -120,7 +120,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd var historyItem = new DownloadClientItem { - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false), DownloadId = sabHistoryItem.Id, Category = sabHistoryItem.Category, Title = sabHistoryItem.Title, diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index 48a268275..59113cbab 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -74,7 +74,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission item.Category = Settings.TvCategory; item.Title = torrent.Name; - item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false); item.OutputPath = GetOutputPath(outputPath, torrent); item.TotalSize = torrent.TotalSize; diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index d1e129949..5705e33a3 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent } var item = new DownloadClientItem(); - item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace()); item.Title = torrent.Name; item.DownloadId = torrent.Hash; item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path)); diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index cecc76dd7..5b93a1d5d 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -122,7 +122,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent item.Title = torrent.Name; item.TotalSize = torrent.Size; item.Category = torrent.Label; - item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this); + item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace()); item.RemainingSize = torrent.Remaining; item.SeedRatio = torrent.Ratio; diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 671dae4ed..6dd1b6173 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -37,9 +37,10 @@ namespace NzbDrone.Core.Download public string Type { get; set; } public int Id { get; set; } public string Name { get; set; } + public bool HasPostImportCategory { get; set; } public static DownloadClientItemClientInfo FromDownloadClient( - DownloadClientBase downloadClient) + DownloadClientBase downloadClient, bool hasPostImportCategory) where TSettings : IProviderConfig, new() { return new DownloadClientItemClientInfo @@ -47,7 +48,8 @@ namespace NzbDrone.Core.Download Protocol = downloadClient.Protocol, Type = downloadClient.Name, Id = downloadClient.Definition.Id, - Name = downloadClient.Definition.Name + Name = downloadClient.Definition.Name, + HasPostImportCategory = hasPostImportCategory }; } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ad06b5924..f5e78bbd4 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -153,9 +153,15 @@ "BlackholeWatchFolder": "Watch Folder", "BlackholeWatchFolderHelpText": "Folder from which {appName} should import completed downloads", "Blocklist": "Blocklist", + "BlocklistAndSearch": "Blocklist and Search", + "BlocklistAndSearchHint": "Start a search for a replacement after blocklisting", + "BlocklistAndSearchMultipleHint": "Start searches for replacements after blocklisting", "BlocklistLoadError": "Unable to load blocklist", + "BlocklistMultipleOnlyHint": "Blocklist without searching for replacements", + "BlocklistOnly": "Blocklist Only", + "BlocklistOnlyHint": "Blocklist without searching for a replacement", "BlocklistRelease": "Blocklist Release", - "BlocklistReleaseSearchEpisodeAgainHelpText": "Starts a search for this episode again and prevents this release from being grabbed again", + "BlocklistReleaseHelpText": "Blocks this release from being redownloaded by {appName} via RSS or Automatic Search", "BlocklistReleases": "Blocklist Releases", "Branch": "Branch", "BranchUpdate": "Branch to use to update {appName}", @@ -188,6 +194,9 @@ "CertificateValidation": "Certificate Validation", "CertificateValidationHelpText": "Change how strict HTTPS certification validation is. Do not change unless you understand the risks.", "Certification": "Certification", + "ChangeCategory": "Change Category", + "ChangeCategoryHint": "Changes download to the 'Post-Import Category' from Download Client", + "ChangeCategoryMultipleHint": "Changes downloads to the 'Post-Import Category' from Download Client", "ChangeFileDate": "Change File Date", "ChangeFileDateHelpText": "Change file date on import/rescan", "CheckDownloadClientForDetails": "check download client for more details", @@ -377,6 +386,8 @@ "DisabledForLocalAddresses": "Disabled for Local Addresses", "Discord": "Discord", "DiskSpace": "Disk Space", + "DoNotBlocklist": "Do not Blocklist", + "DoNotBlocklistHint": "Remove without blocklisting", "DoNotPrefer": "Do not Prefer", "DoNotUpgradeAutomatically": "Do not Upgrade Automatically", "Docker": "Docker", @@ -754,6 +765,10 @@ "IconForFinalesHelpText": "Show icon for series/season finales based on available episode information", "IconForSpecials": "Icon for Specials", "IconForSpecialsHelpText": "Show icon for special episodes (season 0)", + "IgnoreDownload": "Ignore Download", + "IgnoreDownloads": "Ignore Downloads", + "IgnoreDownloadHint": "Stops {appName} from processing this download further", + "IgnoreDownloadsHint": "Stops {appName} from processing these downloads further", "Ignored": "Ignored", "IgnoredAddresses": "Ignored Addresses", "Images": "Images", @@ -1596,11 +1611,15 @@ "RemoveFailedDownloadsHelpText": "Remove failed downloads from download client history", "RemoveFilter": "Remove filter", "RemoveFromBlocklist": "Remove from Blocklist", - "RemoveFromDownloadClient": "Remove From Download Client", - "RemoveFromDownloadClientHelpTextWarning": "Removing will remove the download and the file(s) from the download client.", + "RemoveFromDownloadClient": "Remove from Download Client", + "RemoveFromDownloadClientHint": "Removes download and file(s) from download client", "RemoveFromQueue": "Remove from queue", + "RemoveMultipleFromDownloadClientHint": "Removes downloads and files from download client", "RemoveQueueItem": "Remove - {sourceTitle}", + "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the download and the file(s) from the download client.", "RemoveQueueItemConfirmation": "Are you sure you want to remove '{sourceTitle}' from the queue?", + "RemoveQueueItemRemovalMethod": "Removal Method", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the downloads and the files from the download client.", "RemoveRootFolder": "Remove root folder", "RemoveSelected": "Remove Selected", "RemoveSelectedBlocklistMessageText": "Are you sure you want to remove the selected items from the blocklist?", diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 15ff7948a..c5d2a123a 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -30,6 +30,7 @@ namespace NzbDrone.Core.Queue public RemoteEpisode RemoteEpisode { get; set; } public DownloadProtocol Protocol { get; set; } public string DownloadClient { get; set; } + public bool DownloadClientHasPostImportCategory { get; set; } public string Indexer { get; set; } public string OutputPath { get; set; } public string ErrorMessage { get; set; } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 6b4aadb4c..3d7078223 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -80,7 +80,8 @@ namespace NzbDrone.Core.Queue DownloadClient = trackedDownload.DownloadItem.DownloadClientInfo.Name, Indexer = trackedDownload.Indexer, OutputPath = trackedDownload.DownloadItem.OutputPath.ToString(), - Added = trackedDownload.Added + Added = trackedDownload.Added, + DownloadClientHasPostImportCategory = trackedDownload.DownloadItem.DownloadClientInfo.HasPostImportCategory }; queue.Id = HashConverter.GetHashInt31($"trackedDownload-{trackedDownload.DownloadClient}-{trackedDownload.DownloadItem.DownloadId}-ep{episode?.Id ?? 0}"); diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 744fedda3..8884ef4a6 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -71,7 +71,7 @@ namespace Sonarr.Api.V3.Queue } [RestDeleteById] - public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false) + public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false, bool changeCategory = false) { var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); @@ -89,12 +89,12 @@ namespace Sonarr.Api.V3.Queue throw new NotFoundException(); } - Remove(trackedDownload, removeFromClient, blocklist, skipRedownload); + Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory); _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); } [HttpDelete("bulk")] - public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false) + public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false, [FromQuery] bool changeCategory = false) { var trackedDownloadIds = new List(); var pendingToRemove = new List(); @@ -125,7 +125,7 @@ namespace Sonarr.Api.V3.Queue foreach (var trackedDownload in trackedToRemove.DistinctBy(t => t.DownloadItem.DownloadId)) { - Remove(trackedDownload, removeFromClient, blocklist, skipRedownload); + Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory); trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); } @@ -292,7 +292,7 @@ namespace Sonarr.Api.V3.Queue _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); } - private TrackedDownload Remove(TrackedDownload trackedDownload, bool removeFromClient, bool blocklist, bool skipRedownload) + private TrackedDownload Remove(TrackedDownload trackedDownload, bool removeFromClient, bool blocklist, bool skipRedownload, bool changeCategory) { if (removeFromClient) { @@ -305,13 +305,24 @@ namespace Sonarr.Api.V3.Queue downloadClient.RemoveItem(trackedDownload.DownloadItem, true); } + else if (changeCategory) + { + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + + if (downloadClient == null) + { + throw new BadRequestException(); + } + + downloadClient.MarkItemAsImported(trackedDownload.DownloadItem); + } if (blocklist) { _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipRedownload); } - if (!removeFromClient && !blocklist) + if (!removeFromClient && !blocklist && !changeCategory) { if (!_ignoredDownloadService.IgnoreDownload(trackedDownload)) { diff --git a/src/Sonarr.Api.V3/Queue/QueueResource.cs b/src/Sonarr.Api.V3/Queue/QueueResource.cs index 6aaf3b1ed..e5152f1d7 100644 --- a/src/Sonarr.Api.V3/Queue/QueueResource.cs +++ b/src/Sonarr.Api.V3/Queue/QueueResource.cs @@ -38,6 +38,7 @@ namespace Sonarr.Api.V3.Queue public string DownloadId { get; set; } public DownloadProtocol Protocol { get; set; } public string DownloadClient { get; set; } + public bool DownloadClientHasPostImportCategory { get; set; } public string Indexer { get; set; } public string OutputPath { get; set; } public bool EpisodeHasFile { get; set; } @@ -81,6 +82,7 @@ namespace Sonarr.Api.V3.Queue DownloadId = model.DownloadId, Protocol = model.Protocol, DownloadClient = model.DownloadClient, + DownloadClientHasPostImportCategory = model.DownloadClientHasPostImportCategory, Indexer = model.Indexer, OutputPath = model.OutputPath, EpisodeHasFile = model.Episode?.HasFile ?? false