From c6ea7d7e630cbcff73b02fa36cbd97aa178246b9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 5 Aug 2019 00:25:49 -0700 Subject: [PATCH] Option to ignore items when removing from queue instead of removing from client New: Option to not remove item from download client when removing from queue Closes #1710 --- .../History/Details/HistoryDetails.js | 24 +++++++++ .../History/Details/HistoryDetailsModal.js | 2 + .../Activity/History/HistoryEventTypeCell.js | 4 ++ frontend/src/Activity/Queue/Queue.js | 14 +++-- frontend/src/Activity/Queue/QueueConnector.js | 4 +- frontend/src/Activity/Queue/QueueRow.js | 1 + .../src/Activity/Queue/QueueRowConnector.js | 4 +- .../Activity/Queue/RemoveQueueItemModal.css | 4 -- .../Activity/Queue/RemoveQueueItemModal.js | 52 +++++++++++++----- .../Activity/Queue/RemoveQueueItemsModal.js | 53 +++++++++++++++---- frontend/src/Helpers/Props/icons.js | 1 + frontend/src/Store/Actions/historyActions.js | 11 ++++ frontend/src/Store/Actions/queueActions.js | 6 ++- .../Download/DownloadIgnoredEvent.cs | 19 +++++++ .../Download/IgnoredDownloadService.cs | 53 +++++++++++++++++++ .../DownloadMonitoringService.cs | 6 ++- .../TrackedDownloads/TrackedDownload.cs | 3 +- .../TrackedDownloadService.cs | 2 + src/NzbDrone.Core/History/History.cs | 3 +- src/NzbDrone.Core/History/HistoryService.cs | 35 +++++++++--- src/Sonarr.Api.V3/Queue/QueueActionModule.cs | 35 ++++++++---- 21 files changed, 281 insertions(+), 55 deletions(-) delete mode 100644 frontend/src/Activity/Queue/RemoveQueueItemModal.css create mode 100644 src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs create mode 100644 src/NzbDrone.Core/Download/IgnoredDownloadService.cs diff --git a/frontend/src/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js index b511c2e00..361844b77 100644 --- a/frontend/src/Activity/History/Details/HistoryDetails.js +++ b/frontend/src/Activity/History/Details/HistoryDetails.js @@ -232,6 +232,30 @@ function HistoryDetails(props) { ); } + if (eventType === 'downloadIgnored') { + const { + message + } = data; + + return ( + + + + { + !!message && + + } + + ); + } + return ( { - this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist); + onRemoveSelectedConfirmed = (payload) => { + this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload }); this.setState({ isConfirmRemoveModalOpen: false }); } @@ -148,7 +148,8 @@ class Queue extends Component { const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting; const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId)); const hasError = error || episodesError; - const selectedCount = this.getSelectedIds().length; + const selectedIds = this.getSelectedIds(); + const selectedCount = selectedIds.length; const disableSelectedActions = selectedCount === 0; return ( @@ -259,6 +260,13 @@ class Queue extends Component { { + const item = items.find((i) => i.id === id); + + return !!(item && item.seriesId && item.episodeId); + }) + )} onRemovePress={this.onRemoveSelectedConfirmed} onModalClose={this.onConfirmRemoveModalClose} /> diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js index 5bb174333..4ee9e6e63 100644 --- a/frontend/src/Activity/Queue/QueueConnector.js +++ b/frontend/src/Activity/Queue/QueueConnector.js @@ -137,8 +137,8 @@ class QueueConnector extends Component { this.props.grabQueueItems({ ids }); } - onRemoveSelectedPress = (ids, blacklist) => { - this.props.removeQueueItems({ ids, blacklist }); + onRemoveSelectedPress = (payload) => { + this.props.removeQueueItems(payload); } // diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index 5a6f06b88..d04188ce0 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -352,6 +352,7 @@ class QueueRow extends Component { diff --git a/frontend/src/Activity/Queue/QueueRowConnector.js b/frontend/src/Activity/Queue/QueueRowConnector.js index 10442e30f..4c1193e70 100644 --- a/frontend/src/Activity/Queue/QueueRowConnector.js +++ b/frontend/src/Activity/Queue/QueueRowConnector.js @@ -43,8 +43,8 @@ class QueueRowConnector extends Component { this.props.grabQueueItem({ id: this.props.id }); } - onRemoveQueueItemPress = (blacklist) => { - this.props.removeQueueItem({ id: this.props.id, blacklist }); + onRemoveQueueItemPress = (payload) => { + this.props.removeQueueItem({ id: this.props.id, ...payload }); } // diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.css b/frontend/src/Activity/Queue/RemoveQueueItemModal.css deleted file mode 100644 index d7a643463..000000000 --- a/frontend/src/Activity/Queue/RemoveQueueItemModal.css +++ /dev/null @@ -1,4 +0,0 @@ -.messageRemove { - margin-bottom: 30px; - color: $dangerColor; -} diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.js b/frontend/src/Activity/Queue/RemoveQueueItemModal.js index 33208bb5f..04ad68a7c 100644 --- a/frontend/src/Activity/Queue/RemoveQueueItemModal.js +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.js @@ -10,7 +10,6 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalHeader from 'Components/Modal/ModalHeader'; import ModalBody from 'Components/Modal/ModalBody'; import ModalFooter from 'Components/Modal/ModalFooter'; -import styles from './RemoveQueueItemModal.css'; class RemoveQueueItemModal extends Component { @@ -21,26 +20,41 @@ class RemoveQueueItemModal extends Component { super(props, context); this.state = { + remove: true, blacklist: false }; } + // + // Control + + resetState = function() { + this.setState({ + remove: true, + blacklist: false + }); + } + // // Listeners + onRemoveChange = ({ value }) => { + this.setState({ remove: value }); + } + onBlacklistChange = ({ value }) => { this.setState({ blacklist: value }); } - onRemoveQueueItemConfirmed = () => { - const blacklist = this.state.blacklist; + onRemoveConfirmed = () => { + const state = this.state; - this.setState({ blacklist: false }); - this.props.onRemovePress(blacklist); + this.resetState(); + this.props.onRemovePress(state); } onModalClose = () => { - this.setState({ blacklist: false }); + this.resetState(); this.props.onModalClose(); } @@ -50,10 +64,11 @@ class RemoveQueueItemModal extends Component { render() { const { isOpen, - sourceTitle + sourceTitle, + canIgnore } = this.props; - const blacklist = this.state.blacklist; + const { remove, blacklist } = this.state; return ( -
- Removing will remove the download and the file(s) from the download client. -
+ + Remove From Download Client + + + Blacklist Release + @@ -97,7 +122,7 @@ class RemoveQueueItemModal extends Component { @@ -111,6 +136,7 @@ class RemoveQueueItemModal extends Component { RemoveQueueItemModal.propTypes = { isOpen: PropTypes.bool.isRequired, sourceTitle: PropTypes.string.isRequired, + canIgnore: PropTypes.bool.isRequired, onRemovePress: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js index 8e8009ab1..2fff599b7 100644 --- a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js +++ b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js @@ -21,26 +21,41 @@ class RemoveQueueItemsModal extends Component { super(props, context); this.state = { + remove: true, blacklist: false }; } // - // Listeners + // Control + + resetState = function() { + this.setState({ + remove: true, + blacklist: false + }); + } + + // + // Listeners + + onRemoveChange = ({ value }) => { + this.setState({ remove: value }); + } onBlacklistChange = ({ value }) => { this.setState({ blacklist: value }); } - onRemoveQueueItemConfirmed = () => { - const blacklist = this.state.blacklist; + onRemoveConfirmed = () => { + const state = this.state; - this.setState({ blacklist: false }); - this.props.onRemovePress(blacklist); + this.resetState(); + this.props.onRemovePress(state); } onModalClose = () => { - this.setState({ blacklist: false }); + this.resetState(); this.props.onModalClose(); } @@ -50,10 +65,11 @@ class RemoveQueueItemsModal extends Component { render() { const { isOpen, - selectedCount + selectedCount, + canIgnore } = this.props; - const blacklist = this.state.blacklist; + const { remove, blacklist } = this.state; return ( - Blacklist Release + Remove From Download Client + + + + + + + Blacklist Release{selectedCount > 1 ? 's' : ''} + + Remove @@ -107,6 +139,7 @@ class RemoveQueueItemsModal extends Component { RemoveQueueItemsModal.propTypes = { isOpen: PropTypes.bool.isRequired, selectedCount: PropTypes.number.isRequired, + canIgnore: PropTypes.bool.isRequired, onRemovePress: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 21553bc87..49fbc457d 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -145,6 +145,7 @@ export const HEALTH = fasMedkit; export const HEART = fasHeart; export const HISTORY = fasHistory; export const HOUSEKEEPING = fasHome; +export const IGNORE = fasTimesCircle; export const INFO = fasInfoCircle; export const INTERACTIVE = fasUser; export const KEYBOARD = farKeyboard; diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 496b7ed29..abe9c16bf 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -150,6 +150,17 @@ export const defaultState = { type: filterTypes.EQUAL } ] + }, + { + key: 'ignored', + label: 'Ignored', + filters: [ + { + key: 'eventType', + value: '7', + type: filterTypes.EQUAL + } + ] } ] diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index f2f774a4c..6334bc058 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -356,13 +356,14 @@ export const actionHandlers = handleThunks({ [REMOVE_QUEUE_ITEM]: function(getState, payload, dispatch) { const { id, + remove, blacklist } = payload; dispatch(updateItem({ section: paged, id, isRemoving: true })); const promise = createAjaxRequest({ - url: `/queue/${id}?blacklist=${blacklist}`, + url: `/queue/${id}?removeFromClient=${remove}&blacklist=${blacklist}`, method: 'DELETE' }).request; @@ -378,6 +379,7 @@ export const actionHandlers = handleThunks({ [REMOVE_QUEUE_ITEMS]: function(getState, payload, dispatch) { const { ids, + remove, blacklist } = payload; @@ -394,7 +396,7 @@ export const actionHandlers = handleThunks({ ])); const promise = createAjaxRequest({ - url: `/queue/bulk?blacklist=${blacklist}`, + url: `/queue/bulk?removeFromClient=${remove}&blacklist=${blacklist}`, method: 'DELETE', dataType: 'json', data: JSON.stringify({ ids }) diff --git a/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs b/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs new file mode 100644 index 000000000..7637caaf3 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Download +{ + public class DownloadIgnoredEvent : IEvent + { + public int SeriesId { get; set; } + public List EpisodeIds { get; set; } + public Language Language { get; set; } + public QualityModel Quality { get; set; } + public string SourceTitle { get; set; } + public string DownloadClient { get; set; } + public string DownloadId { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/IgnoredDownloadService.cs b/src/NzbDrone.Core/Download/IgnoredDownloadService.cs new file mode 100644 index 000000000..76bc1c69c --- /dev/null +++ b/src/NzbDrone.Core/Download/IgnoredDownloadService.cs @@ -0,0 +1,53 @@ +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Download +{ + public interface IIgnoredDownloadService + { + bool IgnoreDownload(TrackedDownload trackedDownload); + } + + public class IgnoredDownloadService : IIgnoredDownloadService + { + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public IgnoredDownloadService(IEventAggregator eventAggregator, + Logger logger) + { + _eventAggregator = eventAggregator; + _logger = logger; + } + + public bool IgnoreDownload(TrackedDownload trackedDownload) + { + var series = trackedDownload.RemoteEpisode.Series; + var episodes = trackedDownload.RemoteEpisode.Episodes; + + if (series == null || episodes.Empty()) + { + _logger.Warn("Unable to ignore download for unknown series/episode"); + return false; + } + + var downloadIgnoredEvent = new DownloadIgnoredEvent + { + SeriesId = series.Id, + EpisodeIds = episodes.Select(e => e.Id).ToList(), + Language = trackedDownload.RemoteEpisode.ParsedEpisodeInfo.Language, + Quality = trackedDownload.RemoteEpisode.ParsedEpisodeInfo.Quality, + SourceTitle = trackedDownload.DownloadItem.Title, + DownloadClient = trackedDownload.DownloadItem.DownloadClient, + DownloadId = trackedDownload.DownloadItem.DownloadId, + Message = "Manually ignored" + }; + + _eventAggregator.PublishEvent(downloadIgnoredEvent); + return true; + } + } +} diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs index 0ec687ec2..8bba2945a 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs @@ -133,8 +133,10 @@ private TrackedDownload ProcessClientItem(IDownloadClient downloadClient, Downlo private bool DownloadIsTrackable(TrackedDownload trackedDownload) { - // If the download has already been imported or failed don't track it - if (trackedDownload.State == TrackedDownloadState.Imported || trackedDownload.State == TrackedDownloadState.Failed) + // If the download has already been imported, failed or the user ignored it don't track it + if (trackedDownload.State == TrackedDownloadState.Imported || + trackedDownload.State == TrackedDownloadState.Failed || + trackedDownload.State == TrackedDownloadState.Ignored) { return false; } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index 2a93f8789..64e4a15f6 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -40,7 +40,8 @@ public enum TrackedDownloadState Importing, Imported, FailedPending, - Failed + Failed, + Ignored } public enum TrackedDownloadStatus diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 5525f6adb..89824958f 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -201,6 +201,8 @@ private static TrackedDownloadState GetStateFromHistory(HistoryEventType eventTy return TrackedDownloadState.Imported; case HistoryEventType.DownloadFailed: return TrackedDownloadState.Failed; + case HistoryEventType.DownloadIgnored: + return TrackedDownloadState.Ignored; default: return TrackedDownloadState.Downloading; } diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 1604a2822..3cd630130 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -39,6 +39,7 @@ public enum HistoryEventType DownloadFolderImported = 3, DownloadFailed = 4, EpisodeFileDeleted = 5, - EpisodeFileRenamed = 6 + EpisodeFileRenamed = 6, + DownloadIgnored = 7 } } \ No newline at end of file diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index d2c5719a8..dd80a139e 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Marr.Data.QGen; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; @@ -11,11 +10,7 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; -using NzbDrone.Core.Languages; -using NzbDrone.Core.Profiles.Qualities; -using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.History { @@ -39,7 +34,8 @@ public class HistoryService : IHistoryService, IHandle, IHandle, IHandle, - IHandle + IHandle, + IHandle { private readonly IHistoryRepository _historyRepository; private readonly Logger _logger; @@ -307,6 +303,33 @@ public void Handle(EpisodeFileRenamedEvent message) } } + public void Handle(DownloadIgnoredEvent message) + { + var historyToAdd = new List(); + + foreach (var episodeId in message.EpisodeIds) + { + var history = new History + { + EventType = HistoryEventType.DownloadIgnored, + Date = DateTime.UtcNow, + Quality = message.Quality, + SourceTitle = message.SourceTitle, + SeriesId = message.SeriesId, + EpisodeId = episodeId, + DownloadId = message.DownloadId, + Language = message.Language + }; + + history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("Message", message.Message); + + historyToAdd.Add(history); + } + + _historyRepository.InsertMany(historyToAdd); + } + public void Handle(SeriesDeletedEvent message) { _historyRepository.DeleteForSeries(message.Series.Id); diff --git a/src/Sonarr.Api.V3/Queue/QueueActionModule.cs b/src/Sonarr.Api.V3/Queue/QueueActionModule.cs index 120738124..ea6f77bb3 100644 --- a/src/Sonarr.Api.V3/Queue/QueueActionModule.cs +++ b/src/Sonarr.Api.V3/Queue/QueueActionModule.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Nancy; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; @@ -16,6 +17,7 @@ public class QueueActionModule : SonarrRestModule private readonly IQueueService _queueService; private readonly ITrackedDownloadService _trackedDownloadService; private readonly IFailedDownloadService _failedDownloadService; + private readonly IIgnoredDownloadService _ignoredDownloadService; private readonly IProvideDownloadClient _downloadClientProvider; private readonly IPendingReleaseService _pendingReleaseService; private readonly IDownloadService _downloadService; @@ -23,6 +25,7 @@ public class QueueActionModule : SonarrRestModule public QueueActionModule(IQueueService queueService, ITrackedDownloadService trackedDownloadService, IFailedDownloadService failedDownloadService, + IIgnoredDownloadService ignoredDownloadService, IProvideDownloadClient downloadClientProvider, IPendingReleaseService pendingReleaseService, IDownloadService downloadService) @@ -30,6 +33,7 @@ public QueueActionModule(IQueueService queueService, _queueService = queueService; _trackedDownloadService = trackedDownloadService; _failedDownloadService = failedDownloadService; + _ignoredDownloadService = ignoredDownloadService; _downloadClientProvider = downloadClientProvider; _pendingReleaseService = pendingReleaseService; _downloadService = downloadService; @@ -76,9 +80,10 @@ private object Grab() private object Remove(int id) { + var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true); var blacklist = Request.GetBooleanQueryParameter("blacklist"); - var trackedDownload = Remove(id, blacklist); + var trackedDownload = Remove(id, removeFromClient, blacklist); if (trackedDownload != null) { @@ -90,6 +95,7 @@ private object Remove(int id) private object Remove() { + var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true); var blacklist = Request.GetBooleanQueryParameter("blacklist"); var resource = Request.Body.FromJson(); @@ -97,7 +103,7 @@ private object Remove() foreach (var id in resource.Ids) { - var trackedDownload = Remove(id, blacklist); + var trackedDownload = Remove(id, removeFromClient, blacklist); if (trackedDownload != null) { @@ -110,7 +116,7 @@ private object Remove() return new object(); } - private TrackedDownload Remove(int id, bool blacklist) + private TrackedDownload Remove(int id, bool removeFromClient, bool blacklist) { var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); @@ -128,19 +134,30 @@ private TrackedDownload Remove(int id, bool blacklist) throw new NotFoundException(); } - var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); - - if (downloadClient == null) + if (removeFromClient) { - throw new BadRequestException(); - } + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); - downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true); + if (downloadClient == null) + { + throw new BadRequestException(); + } + + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true); + } if (blacklist) { _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId); } + + if (!removeFromClient && !blacklist) + { + if (!_ignoredDownloadService.IgnoreDownload(trackedDownload)) + { + return null; + } + } return trackedDownload; }