From 48750780febbbe19a30a2cf0e3d0de11c2cc96f1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 26 Jul 2020 21:27:31 +0100 Subject: [PATCH] New: Option to ignore items when removing from queue instead of removing from client --- .../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 | 59 ++++++++++++------ .../Activity/Queue/RemoveQueueItemsModal.js | 62 +++++++++++++------ frontend/src/Helpers/Props/icons.js | 1 + frontend/src/Store/Actions/historyActions.js | 11 ++++ frontend/src/Store/Actions/queueActions.js | 6 +- src/Lidarr.Api.V1/Queue/QueueActionModule.cs | 35 ++++++++--- .../Download/DownloadIgnoredEvent.cs | 17 +++++ .../Download/IgnoredDownloadService.cs | 52 ++++++++++++++++ .../DownloadMonitoringService.cs | 6 +- .../TrackedDownloads/TrackedDownload.cs | 3 +- .../TrackedDownloadService.cs | 2 + src/NzbDrone.Core/History/History.cs | 3 +- src/NzbDrone.Core/History/HistoryService.cs | 28 ++++++++- 21 files changed, 277 insertions(+), 65 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 528b65907..28e510145 100644 --- a/frontend/src/Activity/History/Details/HistoryDetails.js +++ b/frontend/src/Activity/History/Details/HistoryDetails.js @@ -400,6 +400,30 @@ function HistoryDetails(props) { ); } + if (eventType === 'downloadIgnored') { + const { + message + } = data; + + return ( + + + + { + !!message && + + } + + ); + } + return ( { - this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist, skipredownload); + onRemoveSelectedConfirmed = (payload) => { + this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload }); this.setState({ isConfirmRemoveModalOpen: false }); } @@ -148,7 +148,8 @@ class Queue extends Component { const isRefreshing = isFetching || isAlbumsFetching || isRefreshMonitoredDownloadsExecuting; const isAllPopulated = isPopulated && (isAlbumsPopulated || !items.length || items.every((e) => !e.albumId)); const hasError = error || albumsError; - 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.artistId && item.albumId); + }) + )} onRemovePress={this.onRemoveSelectedConfirmed} onModalClose={this.onConfirmRemoveModalClose} /> diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js index 17052330e..8f8aa6ca4 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, skipredownload) => { - this.props.removeQueueItems({ ids, blacklist, skipredownload }); + onRemoveSelectedPress = (payload) => { + this.props.removeQueueItems(payload); } // diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index 301c1a3c8..bdd335d5e 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -335,6 +335,7 @@ class QueueRow extends Component { diff --git a/frontend/src/Activity/Queue/QueueRowConnector.js b/frontend/src/Activity/Queue/QueueRowConnector.js index 6bbbde361..4445ca88b 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, skipredownload) => { - this.props.removeQueueItem({ id: this.props.id, blacklist, skipredownload }); + 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 d2f929aee..d0686eba2 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,14 +20,30 @@ class RemoveQueueItemModal extends Component { super(props, context); this.state = { + remove: true, blacklist: false, skipredownload: false }; } + // + // Control + + resetState = function() { + this.setState({ + remove: true, + blacklist: false, + skipredownload: false + }); + } + // // Listeners + onRemoveChange = ({ value }) => { + this.setState({ remove: value }); + } + onBlacklistChange = ({ value }) => { this.setState({ blacklist: value }); } @@ -37,22 +52,15 @@ class RemoveQueueItemModal extends Component { this.setState({ skipredownload: value }); } - onRemoveQueueItemConfirmed = () => { - const blacklist = this.state.blacklist; - const skipredownload = this.state.skipredownload; + onRemoveConfirmed = () => { + const state = this.state; - this.setState({ - blacklist: false, - skipredownload: false - }); - this.props.onRemovePress(blacklist, skipredownload); + this.resetState(); + this.props.onRemovePress(state); } onModalClose = () => { - this.setState({ - blacklist: false, - skipredownload: false - }); + this.resetState(); this.props.onModalClose(); } @@ -62,11 +70,11 @@ class RemoveQueueItemModal extends Component { render() { const { isOpen, - sourceTitle + sourceTitle, + canIgnore } = this.props; - const blacklist = this.state.blacklist; - const skipredownload = this.state.skipredownload; + const { remove, blacklist, skipredownload } = this.state; return ( -
- Removing will remove the download and the file(s) from the download client. -
+ + Remove From Download Client + + + Blacklist Release + Remove @@ -138,6 +156,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 7d8d8d294..074af95cd 100644 --- a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js +++ b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js @@ -21,13 +21,29 @@ class RemoveQueueItemsModal extends Component { super(props, context); this.state = { + remove: true, blacklist: false, skipredownload: false }; } // - // Listeners + // Control + + resetState = function() { + this.setState({ + remove: true, + blacklist: false, + skipredownload: false + }); + } + + // + // Listeners + + onRemoveChange = ({ value }) => { + this.setState({ remove: value }); + } onBlacklistChange = ({ value }) => { this.setState({ blacklist: value }); @@ -37,22 +53,15 @@ class RemoveQueueItemsModal extends Component { this.setState({ skipredownload: value }); } - onRemoveQueueItemConfirmed = () => { - const blacklist = this.state.blacklist; - const skipredownload = this.state.skipredownload; + onRemoveConfirmed = () => { + const state = this.state; - this.setState({ - blacklist: false, - skipredownload: false - }); - this.props.onRemovePress(blacklist, skipredownload); + this.resetState(); + this.props.onRemovePress(state); } onModalClose = () => { - this.setState({ - blacklist: false, - skipredownload: false - }); + this.resetState(); this.props.onModalClose(); } @@ -62,11 +71,11 @@ class RemoveQueueItemsModal extends Component { render() { const { isOpen, - selectedCount + selectedCount, + canIgnore } = this.props; - const blacklist = this.state.blacklist; - const skipredownload = this.state.skipredownload; + const { remove, blacklist, skipredownload } = this.state; return ( - Blacklist Release + Remove From Download Client + + + + + + + Blacklist Release{selectedCount > 1 ? 's' : ''} + + Remove @@ -134,6 +159,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 6643cbc73..122c1e8e3 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -157,6 +157,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 d7552b938..8862464e7 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -179,6 +179,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 85c301c7d..1ecc1d978 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -345,6 +345,7 @@ export const actionHandlers = handleThunks({ [REMOVE_QUEUE_ITEM]: function(getState, payload, dispatch) { const { id, + remove, blacklist, skipredownload } = payload; @@ -352,7 +353,7 @@ export const actionHandlers = handleThunks({ dispatch(updateItem({ section: paged, id, isRemoving: true })); const promise = createAjaxRequest({ - url: `/queue/${id}?blacklist=${blacklist}&skipredownload=${skipredownload}`, + url: `/queue/${id}?removeFromClient=${remove}&blacklist=${blacklist}&skipredownload=${skipredownload}`, method: 'DELETE' }).request; @@ -368,6 +369,7 @@ export const actionHandlers = handleThunks({ [REMOVE_QUEUE_ITEMS]: function(getState, payload, dispatch) { const { ids, + remove, blacklist, skipredownload } = payload; @@ -385,7 +387,7 @@ export const actionHandlers = handleThunks({ ])); const promise = createAjaxRequest({ - url: `/queue/bulk?blacklist=${blacklist}&skipredownload=${skipredownload}`, + url: `/queue/bulk?removeFromClient=${remove}&blacklist=${blacklist}&skipredownload=${skipredownload}`, method: 'DELETE', dataType: 'json', data: JSON.stringify({ ids }) diff --git a/src/Lidarr.Api.V1/Queue/QueueActionModule.cs b/src/Lidarr.Api.V1/Queue/QueueActionModule.cs index de1c546df..fe2f29b18 100644 --- a/src/Lidarr.Api.V1/Queue/QueueActionModule.cs +++ b/src/Lidarr.Api.V1/Queue/QueueActionModule.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Lidarr.Http; using Lidarr.Http.Extensions; using Lidarr.Http.REST; @@ -15,6 +16,7 @@ namespace Lidarr.Api.V1.Queue 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; @@ -22,6 +24,7 @@ namespace Lidarr.Api.V1.Queue public QueueActionModule(IQueueService queueService, ITrackedDownloadService trackedDownloadService, IFailedDownloadService failedDownloadService, + IIgnoredDownloadService ignoredDownloadService, IProvideDownloadClient downloadClientProvider, IPendingReleaseService pendingReleaseService, IDownloadService downloadService) @@ -29,6 +32,7 @@ namespace Lidarr.Api.V1.Queue _queueService = queueService; _trackedDownloadService = trackedDownloadService; _failedDownloadService = failedDownloadService; + _ignoredDownloadService = ignoredDownloadService; _downloadClientProvider = downloadClientProvider; _pendingReleaseService = pendingReleaseService; _downloadService = downloadService; @@ -75,10 +79,11 @@ namespace Lidarr.Api.V1.Queue private object Remove(int id) { + var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true); var blacklist = Request.GetBooleanQueryParameter("blacklist"); var skipReDownload = Request.GetBooleanQueryParameter("skipredownload"); - var trackedDownload = Remove(id, blacklist, skipReDownload); + var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload); if (trackedDownload != null) { @@ -90,6 +95,7 @@ namespace Lidarr.Api.V1.Queue private object Remove() { + var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true); var blacklist = Request.GetBooleanQueryParameter("blacklist"); var skipReDownload = Request.GetBooleanQueryParameter("skipredownload"); @@ -98,7 +104,7 @@ namespace Lidarr.Api.V1.Queue foreach (var id in resource.Ids) { - var trackedDownload = Remove(id, blacklist, skipReDownload); + var trackedDownload = Remove(id, removeFromClient, blacklist, skipReDownload); if (trackedDownload != null) { @@ -111,7 +117,7 @@ namespace Lidarr.Api.V1.Queue return new object(); } - private TrackedDownload Remove(int id, bool blacklist, bool skipReDownload) + private TrackedDownload Remove(int id, bool removeFromClient, bool blacklist, bool skipReDownload) { var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); @@ -129,20 +135,31 @@ namespace Lidarr.Api.V1.Queue throw new NotFoundException(); } - var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); - - if (downloadClient == null) + if (removeFromClient) { - throw new BadRequestException(); - } + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + + if (downloadClient == null) + { + throw new BadRequestException(); + } - downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true); + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true); + } if (blacklist) { _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipReDownload); } + if (!removeFromClient && !blacklist) + { + if (!_ignoredDownloadService.IgnoreDownload(trackedDownload)) + { + return null; + } + } + return trackedDownload; } diff --git a/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs b/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs new file mode 100644 index 000000000..7373266a7 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Download +{ + public class DownloadIgnoredEvent : IEvent + { + public int ArtistId { get; set; } + public List AlbumIds { 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; } + } +} diff --git a/src/NzbDrone.Core/Download/IgnoredDownloadService.cs b/src/NzbDrone.Core/Download/IgnoredDownloadService.cs new file mode 100644 index 000000000..ecc051162 --- /dev/null +++ b/src/NzbDrone.Core/Download/IgnoredDownloadService.cs @@ -0,0 +1,52 @@ +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 artist = trackedDownload.RemoteAlbum.Artist; + var albums = trackedDownload.RemoteAlbum.Albums; + + if (artist == null || albums.Empty()) + { + _logger.Warn("Unable to ignore download for unknown artist/album"); + return false; + } + + var downloadIgnoredEvent = new DownloadIgnoredEvent + { + ArtistId = artist.Id, + AlbumIds = albums.Select(e => e.Id).ToList(), + Quality = trackedDownload.RemoteAlbum.ParsedAlbumInfo.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 63396f973..2fb243335 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs @@ -131,8 +131,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads 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.DownloadFailed) + // If the download has already been imported or failed or the user ignored it don't track it + if (trackedDownload.State == TrackedDownloadState.Imported || + trackedDownload.State == TrackedDownloadState.DownloadFailed || + 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 d88fb5911..46c46d529 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -41,7 +41,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads ImportPending, Importing, ImportFailed, - Imported + Imported, + Ignored } public enum TrackedDownloadStatus diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 6384c1992..2d7f81281 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -252,6 +252,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads return TrackedDownloadState.Imported; case HistoryEventType.DownloadFailed: return TrackedDownloadState.DownloadFailed; + case HistoryEventType.DownloadIgnored: + return TrackedDownloadState.Ignored; } // Since DownloadComplete is a new event type, we can't assume it exists for old downloads diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 2949aae28..8d9592361 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -41,6 +41,7 @@ namespace NzbDrone.Core.History TrackFileRenamed = 6, AlbumImportIncomplete = 7, DownloadImported = 8, - TrackFileRetagged = 9 + TrackFileRetagged = 9, + DownloadIgnored = 10 } } diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index e4313e635..d4e7d8dc1 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -39,7 +39,8 @@ namespace NzbDrone.Core.History IHandle, IHandle, IHandle, - IHandle + IHandle, + IHandle { private readonly IHistoryRepository _historyRepository; private readonly Logger _logger; @@ -369,6 +370,31 @@ namespace NzbDrone.Core.History _historyRepository.DeleteForArtist(message.Artist.Id); } + public void Handle(DownloadIgnoredEvent message) + { + var historyToAdd = new List(); + foreach (var albumId in message.AlbumIds) + { + var history = new History + { + EventType = HistoryEventType.DownloadIgnored, + Date = DateTime.UtcNow, + Quality = message.Quality, + SourceTitle = message.SourceTitle, + ArtistId = message.ArtistId, + AlbumId = albumId, + DownloadId = message.DownloadId + }; + + history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("Message", message.Message); + + historyToAdd.Add(history); + } + + _historyRepository.InsertMany(historyToAdd); + } + public List Since(DateTime date, HistoryEventType? eventType) { return _historyRepository.Since(date, eventType);