From d83e20937d35f4ba6fa6e865bc4173525758a15c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 26 Jul 2020 21:27:30 +0100 Subject: [PATCH] New: Monitor and Process downloads separately New: Queue remains up to date while importing file from remote file system Fixed: Failed downloads still in queue won't result in failed search --- frontend/src/Activity/Queue/Queue.js | 6 +- frontend/src/Activity/Queue/QueueConnector.js | 8 +- frontend/src/Activity/Queue/QueueRow.js | 7 +- .../src/Activity/Queue/QueueStatusCell.js | 43 ++- frontend/src/Commands/commandNames.js | 2 +- frontend/src/Components/SignalRConnector.js | 2 +- .../src/System/Tasks/Queued/QueuedTaskRow.js | 2 +- src/Lidarr.Api.V1/Queue/QueueResource.cs | 7 +- src/Lidarr.Api.V1/Queue/QueueStatusModule.cs | 9 +- .../QueueSpecificationFixture.cs | 26 +- .../ImportFixture.cs} | 332 ++++++------------ .../ProcessFixture.cs | 181 ++++++++++ .../ProcessFailedFixture.cs | 101 ++++++ .../ProcessFixture.cs} | 49 +-- .../RedownloadFailedDownloadServiceFixture.cs | 10 +- .../TrackedDownloadAlreadyImportedFixture.cs | 127 +++++++ .../DownloadedAlbumsCommandServiceFixture.cs | 2 +- .../DownloadedTracksImportServiceFixture.cs | 2 +- .../Messaging/Commands/CommandQueueFixture.cs | 8 +- .../Commands/CommandQueueManagerFixture.cs | 2 +- .../Specifications/QueueSpecification.cs | 10 + .../CheckForFinishedDownloadCommand.cs | 1 - .../Download/CompletedDownloadService.cs | 131 ++++--- .../Download/DownloadProcessingService.cs | 66 ++++ .../Download/DownloadsProcessedEvent.cs | 11 + .../Download/FailedDownloadService.cs | 58 ++- .../ProcessMonitoredDownloadsCommand.cs | 9 + .../RedownloadFailedDownloadService.cs | 6 +- .../RefreshMonitoredDownloadsCommand.cs | 8 + .../DownloadMonitoringService.cs | 62 ++-- .../TrackedDownloads/TrackedDownload.cs | 9 +- .../TrackedDownloadAlreadyImported.cs | 42 +++ .../TrackedDownloadService.cs | 39 +- src/NzbDrone.Core/Jobs/TaskManager.cs | 2 +- .../TrackImport/Manual/ManualImportService.cs | 2 +- .../AlreadyImportedSpecification.cs | 65 ++++ .../Messaging/Events/EventAggregator.cs | 7 +- src/NzbDrone.Core/Queue/Queue.cs | 3 +- src/NzbDrone.Core/Queue/QueueService.cs | 3 +- src/NzbDrone.Host.Test/ContainerFixture.cs | 4 +- 40 files changed, 1037 insertions(+), 427 deletions(-) rename src/NzbDrone.Core.Test/Download/{CompletedDownloadServiceFixture.cs => CompletedDownloadServiceTests/ImportFixture.cs} (61%) create mode 100644 src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ProcessFixture.cs create mode 100644 src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFailedFixture.cs rename src/NzbDrone.Core.Test/Download/{FailedDownloadServiceFixture.cs => FailedDownloadServiceTests/ProcessFixture.cs} (72%) create mode 100644 src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadAlreadyImportedFixture.cs create mode 100644 src/NzbDrone.Core/Download/DownloadProcessingService.cs create mode 100644 src/NzbDrone.Core/Download/DownloadsProcessedEvent.cs create mode 100644 src/NzbDrone.Core/Download/ProcessMonitoredDownloadsCommand.cs create mode 100644 src/NzbDrone.Core/Download/RefreshMonitoredDownloadsCommand.cs create mode 100644 src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs create mode 100644 src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlreadyImportedSpecification.cs diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js index 4f27fa60d..6000e6ae9 100644 --- a/frontend/src/Activity/Queue/Queue.js +++ b/frontend/src/Activity/Queue/Queue.js @@ -132,7 +132,7 @@ class Queue extends Component { totalRecords, isGrabbing, isRemoving, - isCheckForFinishedDownloadExecuting, + isRefreshMonitoredDownloadsExecuting, onRefreshPress, ...otherProps } = this.props; @@ -145,7 +145,7 @@ class Queue extends Component { isPendingSelected } = this.state; - const isRefreshing = isFetching || isAlbumsFetching || isCheckForFinishedDownloadExecuting; + 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; @@ -279,7 +279,7 @@ Queue.propTypes = { totalRecords: PropTypes.number, isGrabbing: PropTypes.bool.isRequired, isRemoving: PropTypes.bool.isRequired, - isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired, + isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired, onRefreshPress: PropTypes.func.isRequired, onGrabSelectedPress: PropTypes.func.isRequired, onRemoveSelectedPress: PropTypes.func.isRequired diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js index ea571e8af..17052330e 100644 --- a/frontend/src/Activity/Queue/QueueConnector.js +++ b/frontend/src/Activity/Queue/QueueConnector.js @@ -18,13 +18,13 @@ function createMapStateToProps() { (state) => state.albums, (state) => state.queue.options, (state) => state.queue.paged, - createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD), - (albums, options, queue, isCheckForFinishedDownloadExecuting) => { + createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS), + (albums, options, queue, isRefreshMonitoredDownloadsExecuting) => { return { isAlbumsFetching: albums.isFetching, isAlbumsPopulated: albums.isPopulated, albumsError: albums.error, - isCheckForFinishedDownloadExecuting, + isRefreshMonitoredDownloadsExecuting, ...options, ...queue }; @@ -129,7 +129,7 @@ class QueueConnector extends Component { onRefreshPress = () => { this.props.executeCommand({ - name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD + name: commandNames.REFRESH_MONITORED_DOWNLOADS }); } diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index 06cf16f70..301c1a3c8 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -68,6 +68,7 @@ class QueueRow extends Component { title, status, trackedDownloadStatus, + trackedDownloadState, statusMessages, errorMessage, artist, @@ -100,8 +101,8 @@ class QueueRow extends Component { } = this.state; const progress = 100 - (sizeleft / size * 100); - const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning'; - const isPending = status === 'Delay' || status === 'DownloadClientUnavailable'; + const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning'; + const isPending = status === 'delay' || status === 'downloadClientUnavailable'; return ( @@ -129,6 +130,7 @@ class QueueRow extends Component { sourceTitle={title} status={status} trackedDownloadStatus={trackedDownloadStatus} + trackedDownloadState={trackedDownloadState} statusMessages={statusMessages} errorMessage={errorMessage} /> @@ -348,6 +350,7 @@ QueueRow.propTypes = { title: PropTypes.string.isRequired, status: PropTypes.string.isRequired, trackedDownloadStatus: PropTypes.string, + trackedDownloadState: PropTypes.string, statusMessages: PropTypes.arrayOf(PropTypes.object), errorMessage: PropTypes.string, artist: PropTypes.object, diff --git a/frontend/src/Activity/Queue/QueueStatusCell.js b/frontend/src/Activity/Queue/QueueStatusCell.js index 552fa1444..23d4874d3 100644 --- a/frontend/src/Activity/Queue/QueueStatusCell.js +++ b/frontend/src/Activity/Queue/QueueStatusCell.js @@ -37,13 +37,14 @@ function QueueStatusCell(props) { const { sourceTitle, status, - trackedDownloadStatus = 'Ok', + trackedDownloadStatus, + trackedDownloadState, statusMessages, errorMessage } = props; - const hasWarning = trackedDownloadStatus === 'Warning'; - const hasError = trackedDownloadStatus === 'Error'; + const hasWarning = trackedDownloadStatus === 'warning'; + const hasError = trackedDownloadStatus === 'error'; // status === 'downloading' let iconName = icons.DOWNLOADING; @@ -54,46 +55,58 @@ function QueueStatusCell(props) { iconKind = kinds.WARNING; } - if (status === 'Paused') { + if (status === 'paused') { iconName = icons.PAUSED; title = 'Paused'; } - if (status === 'Queued') { + if (status === 'queued') { iconName = icons.QUEUED; title = 'Queued'; } - if (status === 'Completed') { + if (status === 'completed') { iconName = icons.DOWNLOADED; title = 'Downloaded'; + + if (trackedDownloadState === 'importPending') { + title += ' - Waiting to Import'; + } + + if (trackedDownloadState === 'importing') { + title += ' - Importing'; + } + + if (trackedDownloadState === 'failedPending') { + title += ' - Waiting to Process'; + } } - if (status === 'Delay') { + if (status === 'delay') { iconName = icons.PENDING; title = 'Pending'; } - if (status === 'DownloadClientUnavailable') { + if (status === 'downloadClientUnavailable') { iconName = icons.PENDING; iconKind = kinds.WARNING; title = 'Pending - Download client is unavailable'; } - if (status === 'Failed') { + if (status === 'failed') { iconName = icons.DOWNLOADING; iconKind = kinds.DANGER; title = 'Download failed'; } - if (status === 'Warning') { + if (status === 'warning') { iconName = icons.DOWNLOADING; iconKind = kinds.WARNING; title = `Download warning: ${errorMessage || 'check download client for more details'}`; } if (hasError) { - if (status === 'Completed') { + if (status === 'completed') { iconName = icons.DOWNLOAD; iconKind = kinds.DANGER; title = `Import failed: ${sourceTitle}`; @@ -125,9 +138,15 @@ function QueueStatusCell(props) { QueueStatusCell.propTypes = { sourceTitle: PropTypes.string.isRequired, status: PropTypes.string.isRequired, - trackedDownloadStatus: PropTypes.string, + trackedDownloadStatus: PropTypes.string.isRequired, + trackedDownloadState: PropTypes.string.isRequired, statusMessages: PropTypes.arrayOf(PropTypes.object), errorMessage: PropTypes.string }; +QueueStatusCell.defaultProps = { + trackedDownloadStatus: 'ok', + trackedDownloadState: 'downloading' +}; + export default QueueStatusCell; diff --git a/frontend/src/Commands/commandNames.js b/frontend/src/Commands/commandNames.js index b3ea2f94e..c5159daa5 100644 --- a/frontend/src/Commands/commandNames.js +++ b/frontend/src/Commands/commandNames.js @@ -1,6 +1,6 @@ export const APPLICATION_UPDATE = 'ApplicationUpdate'; export const BACKUP = 'Backup'; -export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload'; +export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads'; export const CLEAR_BLACKLIST = 'ClearBlacklist'; export const CLEAR_LOGS = 'ClearLog'; export const CUTOFF_UNMET_ALBUM_SEARCH = 'CutoffUnmetAlbumSearch'; diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index 2b880aea6..c2bc3f46e 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -272,7 +272,7 @@ class SignalRConnector extends Component { } handleSystemTask = () => { - // No-op for now, we may want this later + this.props.dispatchFetchCommands(); } handleRootfolder = (body) => { diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js index 4aa6d76d6..1a64e6f84 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js @@ -168,7 +168,7 @@ class QueuedTaskRow extends Component { isCancelConfirmModalOpen } = this.state; - let triggerIcon = icons.UNKNOWN; + let triggerIcon = icons.QUICK; if (trigger === 'manual') { triggerIcon = icons.INTERACTIVE; diff --git a/src/Lidarr.Api.V1/Queue/QueueResource.cs b/src/Lidarr.Api.V1/Queue/QueueResource.cs index d5b11dde3..c34403642 100644 --- a/src/Lidarr.Api.V1/Queue/QueueResource.cs +++ b/src/Lidarr.Api.V1/Queue/QueueResource.cs @@ -4,6 +4,7 @@ using System.Linq; using Lidarr.Api.V1.Albums; using Lidarr.Api.V1.Artist; using Lidarr.Http.REST; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Indexers; using NzbDrone.Core.Qualities; @@ -23,7 +24,8 @@ namespace Lidarr.Api.V1.Queue public TimeSpan? Timeleft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } public string Status { get; set; } - public string TrackedDownloadStatus { get; set; } + public TrackedDownloadStatus? TrackedDownloadStatus { get; set; } + public TrackedDownloadState? TrackedDownloadState { get; set; } public List StatusMessages { get; set; } public string ErrorMessage { get; set; } public string DownloadId { get; set; } @@ -56,8 +58,9 @@ namespace Lidarr.Api.V1.Queue Sizeleft = model.Sizeleft, Timeleft = model.Timeleft, EstimatedCompletionTime = model.EstimatedCompletionTime, - Status = model.Status, + Status = model.Status.FirstCharToLower(), TrackedDownloadStatus = model.TrackedDownloadStatus, + TrackedDownloadState = model.TrackedDownloadState, StatusMessages = model.StatusMessages, ErrorMessage = model.ErrorMessage, DownloadId = model.DownloadId, diff --git a/src/Lidarr.Api.V1/Queue/QueueStatusModule.cs b/src/Lidarr.Api.V1/Queue/QueueStatusModule.cs index 34a2040f6..319c811c0 100644 --- a/src/Lidarr.Api.V1/Queue/QueueStatusModule.cs +++ b/src/Lidarr.Api.V1/Queue/QueueStatusModule.cs @@ -4,6 +4,7 @@ using Lidarr.Http; using NzbDrone.Common.TPL; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Queue; using NzbDrone.SignalR; @@ -45,10 +46,10 @@ namespace Lidarr.Api.V1.Queue TotalCount = queue.Count + pending.Count, Count = queue.Count(q => q.Artist != null) + pending.Count, UnknownCount = queue.Count(q => q.Artist == null), - Errors = queue.Any(q => q.Artist != null && q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)), - Warnings = queue.Any(q => q.Artist != null && q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase)), - UnknownErrors = queue.Any(q => q.Artist == null && q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)), - UnknownWarnings = queue.Any(q => q.Artist == null && q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase)) + Errors = queue.Any(q => q.Artist != null && q.TrackedDownloadStatus == TrackedDownloadStatus.Error), + Warnings = queue.Any(q => q.Artist != null && q.TrackedDownloadStatus == TrackedDownloadStatus.Warning), + UnknownErrors = queue.Any(q => q.Artist == null && q.TrackedDownloadStatus == TrackedDownloadStatus.Error), + UnknownWarnings = queue.Any(q => q.Artist == null && q.TrackedDownloadStatus == TrackedDownloadStatus.Warning) }; _broadcastDebounce.Resume(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs index 263bfa7a0..ca1bc44a7 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs @@ -4,6 +4,7 @@ using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Music; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Qualities; @@ -69,11 +70,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Returns(new List()); } - private void GivenQueue(IEnumerable remoteAlbums) + private void GivenQueue(IEnumerable remoteAlbums, TrackedDownloadState trackedDownloadState = TrackedDownloadState.Downloading) { var queue = remoteAlbums.Select(remoteAlbum => new Queue.Queue { - RemoteAlbum = remoteAlbum + RemoteAlbum = remoteAlbum, + TrackedDownloadState = trackedDownloadState }); Mocker.GetMock() @@ -308,5 +310,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests GivenQueue(new List { remoteAlbum }); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); } + + [Test] + public void should_return_true_if_everything_is_the_same_for_failed_pending() + { + _artist.QualityProfile.Value.Cutoff = Quality.FLAC.Id; + + var remoteAlbum = Builder.CreateNew() + .With(r => r.Artist = _artist) + .With(r => r.Albums = new List { _album }) + .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo + { + Quality = new QualityModel(Quality.MP3_008) + }) + .With(r => r.Release = _releaseInfo) + .Build(); + + GivenQueue(new List { remoteAlbum }, TrackedDownloadState.DownloadFailedPending); + + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs similarity index 61% rename from src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs rename to src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs index c4374e1bd..125d6ada5 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs @@ -9,7 +9,6 @@ using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; @@ -18,10 +17,10 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; -namespace NzbDrone.Core.Test.Download +namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests { [TestFixture] - public class CompletedDownloadServiceFixture : CoreTest + public class ImportFixture : CoreTest { private TrackedDownload _trackedDownload; @@ -37,7 +36,7 @@ namespace NzbDrone.Core.Test.Download var remoteAlbum = BuildRemoteAlbum(); _trackedDownload = Builder.CreateNew() - .With(c => c.State = TrackedDownloadStage.Downloading) + .With(c => c.State = TrackedDownloadState.Downloading) .With(c => c.DownloadItem = completed) .With(c => c.RemoteAlbum = remoteAlbum) .Build(); @@ -84,23 +83,6 @@ namespace NzbDrone.Core.Test.Download }; } - private void GivenNoGrabbedHistory() - { - Mocker.GetMock() - .Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId)) - .Returns((History.History)null); - } - - private void GivenSuccessfulImport() - { - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new List - { - new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })) - }); - } - private void GivenABadlyNamedDownload() { _trackedDownload.RemoteAlbum.Artist = null; @@ -126,116 +108,6 @@ namespace NzbDrone.Core.Test.Download .Returns(_trackedDownload.RemoteAlbum.Artist); } - [TestCase(DownloadItemStatus.Downloading)] - [TestCase(DownloadItemStatus.Failed)] - [TestCase(DownloadItemStatus.Queued)] - [TestCase(DownloadItemStatus.Paused)] - [TestCase(DownloadItemStatus.Warning)] - public void should_not_process_if_download_status_isnt_completed(DownloadItemStatus status) - { - _trackedDownload.DownloadItem.Status = status; - - Subject.Process(_trackedDownload); - - AssertNoAttemptedImport(); - } - - [Test] - public void should_not_process_if_matching_history_is_not_found_and_no_category_specified() - { - _trackedDownload.DownloadItem.Category = null; - GivenNoGrabbedHistory(); - - Subject.Process(_trackedDownload); - - AssertNoAttemptedImport(); - } - - [Test] - public void should_process_if_matching_history_is_not_found_but_category_specified() - { - _trackedDownload.DownloadItem.Category = "tv"; - GivenNoGrabbedHistory(); - GivenArtistMatch(); - GivenSuccessfulImport(); - - Subject.Process(_trackedDownload); - - AssertCompletedDownload(); - } - - [Test] - public void should_not_process_if_output_path_is_empty() - { - _trackedDownload.DownloadItem.OutputPath = default(OsPath); - - Subject.Process(_trackedDownload); - - AssertNoAttemptedImport(); - } - - [Test] - public void should_not_throw_if_remotealbum_is_null() - { - _trackedDownload.RemoteAlbum = null; - - Subject.Process(_trackedDownload); - - AssertNoAttemptedImport(); - } - - [Test] - public void should_mark_as_imported_if_all_tracks_were_imported() - { - _trackedDownload.RemoteAlbum.Albums = new List - { - CreateAlbum(1, 2) - }; - - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new List - { - new ImportResult( - new ImportDecision( - new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })), - - new ImportResult( - new ImportDecision( - new LocalTrack { Path = @"C:\TestPath\Droned.S01E02.mkv".AsOsAgnostic() })) - }); - - Subject.Process(_trackedDownload); - - AssertCompletedDownload(); - } - - [Test] - public void should_mark_as_imported_if_all_tracks_were_imported_but_album_incomplete() - { - _trackedDownload.RemoteAlbum.Albums = new List - { - CreateAlbum(1, 3) - }; - - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new List - { - new ImportResult( - new ImportDecision( - new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })), - - new ImportResult( - new ImportDecision( - new LocalTrack { Path = @"C:\TestPath\Droned.S01E02.mkv".AsOsAgnostic() })) - }); - - Subject.Process(_trackedDownload); - - AssertCompletedDownload(); - } - [Test] public void should_not_mark_as_imported_if_all_files_were_rejected() { @@ -252,12 +124,12 @@ namespace NzbDrone.Core.Test.Download new LocalTrack { Path = @"C:\TestPath\Droned.S01E02.mkv".AsOsAgnostic() }, new Rejection("Rejected!")), "Test Failure") }); - Subject.Process(_trackedDownload); + Subject.Import(_trackedDownload); Mocker.GetMock() .Verify(v => v.PublishEvent(It.IsAny()), Times.Never()); - AssertImportIncomplete(); + AssertNotImported(); } [Test] @@ -278,9 +150,9 @@ namespace NzbDrone.Core.Test.Download _trackedDownload.RemoteAlbum.Albums.Clear(); - Subject.Process(_trackedDownload); + Subject.Import(_trackedDownload); - AssertImportIncomplete(); + AssertNotImported(); } [Test] @@ -290,10 +162,9 @@ namespace NzbDrone.Core.Test.Download .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List()); - Subject.Process(_trackedDownload); + Subject.Import(_trackedDownload); - AssertNoCompletedDownload(); - _trackedDownload.State.Should().NotBe(TrackedDownloadStage.ImportFailed); + _trackedDownload.State.Should().Be(TrackedDownloadState.Importing); } [Test] @@ -307,9 +178,9 @@ namespace NzbDrone.Core.Test.Download new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }), "Test Failure") }); - Subject.Process(_trackedDownload); + Subject.Import(_trackedDownload); - AssertImportIncomplete(); + AssertNotImported(); } [Test] @@ -332,13 +203,13 @@ namespace NzbDrone.Core.Test.Download new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }), "Test Failure") }); - Subject.Process(_trackedDownload); + Subject.Import(_trackedDownload); - AssertCompletedDownload(); + AssertImported(); } [Test] - public void should_mark_as_failed_if_some_tracks_were_not_imported() + public void should_not_mark_as_imported_if_some_tracks_were_not_imported() { _trackedDownload.RemoteAlbum.Albums = new List { @@ -358,134 +229,163 @@ namespace NzbDrone.Core.Test.Download new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }), "Test Failure") }); - Subject.Process(_trackedDownload); + var history = Builder.CreateListOfSize(2) + .BuildList(); + + Mocker.GetMock() + .Setup(s => s.FindByDownloadId(It.IsAny())) + .Returns(history); + + Mocker.GetMock() + .Setup(s => s.IsImported(_trackedDownload, history)) + .Returns(true); + + Subject.Import(_trackedDownload); - AssertImportIncomplete(); + AssertNotImported(); } [Test] - public void should_mark_as_imported_if_the_download_can_be_tracked_using_the_source_seriesid() + public void should_not_mark_as_imported_if_some_of_episodes_were_not_imported_including_history() { - GivenABadlyNamedDownload(); + var tracks = Builder.CreateListOfSize(3).BuildList(); - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new List - { - new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })) - }); - - Mocker.GetMock() - .Setup(v => v.GetArtist(It.IsAny())) - .Returns(BuildRemoteAlbum().Artist); + var releases = Builder.CreateListOfSize(3).All().With(x => x.Monitored = true).With(x => x.TrackCount = 1).BuildList(); + releases[0].Tracks = new List { tracks[0] }; + releases[1].Tracks = new List { tracks[1] }; + releases[2].Tracks = new List { tracks[2] }; - Subject.Process(_trackedDownload); + var albums = Builder.CreateListOfSize(3).BuildList(); - AssertCompletedDownload(); - } + albums[0].AlbumReleases = new List { releases[0] }; + albums[1].AlbumReleases = new List { releases[1] }; + albums[2].AlbumReleases = new List { releases[2] }; - [Test] - public void should_not_mark_as_imported_if_the_download_cannot_be_tracked_using_the_source_title_as_it_was_initiated_externally() - { - GivenABadlyNamedDownload(); + _trackedDownload.RemoteAlbum.Albums = albums; Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new List - { - new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })) - }); - - Mocker.GetMock() - .Setup(s => s.MostRecentForDownloadId(It.Is(i => i == "1234"))); + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new List + { + new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv" })), + new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv" }), "Test Failure"), + new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv" }), "Test Failure") + }); - Subject.Process(_trackedDownload); + var history = Builder.CreateListOfSize(2) + .BuildList(); - AssertNoCompletedDownload(); - } + Mocker.GetMock() + .Setup(s => s.FindByDownloadId(It.IsAny())) + .Returns(history); - [Test] - public void should_not_import_when_there_is_a_title_mismatch() - { - _trackedDownload.RemoteAlbum.Artist = null; - Mocker.GetMock() - .Setup(s => s.GetArtist("Drone.S01E01.HDTV")) - .Returns((Artist)null); + Mocker.GetMock() + .Setup(s => s.IsImported(It.IsAny(), It.IsAny>())) + .Returns(false); - Subject.Process(_trackedDownload); + Subject.Import(_trackedDownload); - AssertNoCompletedDownload(); + AssertNotImported(); } [Test] - public void should_mark_as_import_title_mismatch_if_ignore_warnings_is_true() + public void should_mark_as_imported_if_all_tracks_were_imported() { _trackedDownload.RemoteAlbum.Albums = new List { - CreateAlbum(0, 1) + CreateAlbum(1, 2) }; Mocker.GetMock() .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { - new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })) + new ImportResult( + new ImportDecision( + new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })), + + new ImportResult( + new ImportDecision( + new LocalTrack { Path = @"C:\TestPath\Droned.S01E02.mkv".AsOsAgnostic() })) }); - Subject.Process(_trackedDownload, true); + Subject.Import(_trackedDownload); - AssertCompletedDownload(); + AssertImported(); } [Test] - public void should_warn_if_path_is_not_valid_for_windows() + public void should_mark_as_imported_if_all_episodes_were_imported_including_history() { - WindowsOnly(); + var track1 = new Track { Id = 1 }; + var track2 = new Track { Id = 2 }; - _trackedDownload.DownloadItem.OutputPath = new OsPath(@"/invalid/Windows/Path"); + var releases = Builder.CreateListOfSize(2).All().With(x => x.Monitored = true).With(x => x.TrackCount = 1).BuildList(); + releases[0].Tracks = new List { track1 }; + releases[1].Tracks = new List { track2 }; - Subject.Process(_trackedDownload); + var albums = Builder.CreateListOfSize(2).BuildList(); - AssertNoAttemptedImport(); - } + albums[0].AlbumReleases = new List { releases[0] }; + albums[1].AlbumReleases = new List { releases[1] }; - [Test] - public void should_warn_if_path_is_not_valid_for_linux() - { - PosixOnly(); + _trackedDownload.RemoteAlbum.Albums = albums; - _trackedDownload.DownloadItem.OutputPath = new OsPath(@"C:\Invalid\Mono\Path"); + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new List + { + new ImportResult( + new ImportDecision( + new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv", Tracks = new List { track1 } })), - Subject.Process(_trackedDownload); + new ImportResult( + new ImportDecision( + new LocalTrack { Path = @"C:\TestPath\Droned.S01E02.mkv", Tracks = new List { track2 } }), "Test Failure") + }); - AssertNoAttemptedImport(); - } + var history = Builder.CreateListOfSize(2) + .BuildList(); - private void AssertNoAttemptedImport() - { - Mocker.GetMock() - .Verify(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock() + .Setup(s => s.FindByDownloadId(It.IsAny())) + .Returns(history); - AssertNoCompletedDownload(); + Mocker.GetMock() + .Setup(s => s.IsImported(It.IsAny(), It.IsAny>())) + .Returns(true); + + Subject.Import(_trackedDownload); + + AssertImported(); } - private void AssertImportIncomplete() + [Test] + public void should_mark_as_imported_if_the_download_can_be_tracked_using_the_source_seriesid() { - Mocker.GetMock() - .Verify(v => v.PublishEvent(It.IsAny()), Times.Once()); + GivenABadlyNamedDownload(); + + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new List + { + new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })) + }); + + Subject.Import(_trackedDownload); - AssertNoCompletedDownload(); + AssertImported(); } - private void AssertNoCompletedDownload() + private void AssertNotImported() { Mocker.GetMock() .Verify(v => v.PublishEvent(It.IsAny()), Times.Never()); - _trackedDownload.State.Should().NotBe(TrackedDownloadStage.Imported); + _trackedDownload.State.Should().Be(TrackedDownloadState.ImportFailed); } - private void AssertCompletedDownload() + private void AssertImported() { Mocker.GetMock() .Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, ImportMode.Auto, _trackedDownload.RemoteAlbum.Artist, _trackedDownload.DownloadItem), Times.Once()); @@ -493,7 +393,7 @@ namespace NzbDrone.Core.Test.Download Mocker.GetMock() .Verify(v => v.PublishEvent(It.IsAny()), Times.Once()); - _trackedDownload.State.Should().Be(TrackedDownloadStage.Imported); + _trackedDownload.State.Should().Be(TrackedDownloadState.Imported); } } } diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ProcessFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ProcessFixture.cs new file mode 100644 index 000000000..2884fcd43 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ProcessFixture.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.History; +using NzbDrone.Core.Music; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests +{ + [TestFixture] + public class ProcessFixture : CoreTest + { + private TrackedDownload _trackedDownload; + + [SetUp] + public void Setup() + { + var completed = Builder.CreateNew() + .With(h => h.Status = DownloadItemStatus.Completed) + .With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic())) + .With(h => h.Title = "Drone.S01E01.HDTV") + .Build(); + + var remoteAlbum = BuildRemoteAlbum(); + + _trackedDownload = Builder.CreateNew() + .With(c => c.State = TrackedDownloadState.Downloading) + .With(c => c.DownloadItem = completed) + .With(c => c.RemoteAlbum = remoteAlbum) + .Build(); + + Mocker.GetMock() + .SetupGet(c => c.Definition) + .Returns(new DownloadClientDefinition { Id = 1, Name = "testClient" }); + + Mocker.GetMock() + .Setup(c => c.Get(It.IsAny())) + .Returns(Mocker.GetMock().Object); + + Mocker.GetMock() + .Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId)) + .Returns(new History.History()); + + Mocker.GetMock() + .Setup(s => s.GetArtist("Drone.S01E01.HDTV")) + .Returns(remoteAlbum.Artist); + } + + private RemoteAlbum BuildRemoteAlbum() + { + return new RemoteAlbum + { + Artist = new Artist(), + Albums = new List { new Album { Id = 1 } } + }; + } + + private void GivenNoGrabbedHistory() + { + Mocker.GetMock() + .Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId)) + .Returns((History.History)null); + } + + private void GivenArtistMatch() + { + Mocker.GetMock() + .Setup(s => s.GetArtist(It.IsAny())) + .Returns(_trackedDownload.RemoteAlbum.Artist); + } + + private void GivenABadlyNamedDownload() + { + _trackedDownload.DownloadItem.DownloadId = "1234"; + _trackedDownload.DownloadItem.Title = "Droned Pilot"; // Set a badly named download + Mocker.GetMock() + .Setup(s => s.MostRecentForDownloadId(It.Is(i => i == "1234"))) + .Returns(new History.History() { SourceTitle = "Droned S01E01" }); + + Mocker.GetMock() + .Setup(s => s.GetArtist(It.IsAny())) + .Returns((Artist)null); + + Mocker.GetMock() + .Setup(s => s.GetArtist("Droned S01E01")) + .Returns(BuildRemoteAlbum().Artist); + } + + [TestCase(DownloadItemStatus.Downloading)] + [TestCase(DownloadItemStatus.Failed)] + [TestCase(DownloadItemStatus.Queued)] + [TestCase(DownloadItemStatus.Paused)] + [TestCase(DownloadItemStatus.Warning)] + public void should_not_process_if_download_status_isnt_completed(DownloadItemStatus status) + { + _trackedDownload.DownloadItem.Status = status; + + Subject.Check(_trackedDownload); + + AssertNotReadyToImport(); + } + + [Test] + public void should_not_process_if_matching_history_is_not_found_and_no_category_specified() + { + _trackedDownload.DownloadItem.Category = null; + GivenNoGrabbedHistory(); + + Subject.Check(_trackedDownload); + + AssertNotReadyToImport(); + } + + [Test] + public void should_process_if_matching_history_is_not_found_but_category_specified() + { + _trackedDownload.DownloadItem.Category = "tv"; + GivenNoGrabbedHistory(); + GivenArtistMatch(); + + Subject.Check(_trackedDownload); + + AssertReadyToImport(); + } + + [Test] + public void should_not_process_if_output_path_is_empty() + { + _trackedDownload.DownloadItem.OutputPath = default; + + Subject.Check(_trackedDownload); + + AssertNotReadyToImport(); + } + + [Test] + public void should_not_process_if_the_download_cannot_be_tracked_using_the_source_title_as_it_was_initiated_externally() + { + GivenABadlyNamedDownload(); + _trackedDownload.RemoteAlbum.Artist = null; + + Mocker.GetMock() + .Setup(s => s.MostRecentForDownloadId(It.Is(i => i == "1234"))); + + Subject.Check(_trackedDownload); + + AssertNotReadyToImport(); + } + + [Test] + public void should_not_process_when_there_is_a_title_mismatch() + { + _trackedDownload.RemoteAlbum.Artist = null; + Mocker.GetMock() + .Setup(s => s.GetArtist("Drone.S01E01.HDTV")) + .Returns((Artist)null); + + Subject.Check(_trackedDownload); + + AssertNotReadyToImport(); + } + + private void AssertNotReadyToImport() + { + _trackedDownload.State.Should().NotBe(TrackedDownloadState.ImportPending); + } + + private void AssertReadyToImport() + { + _trackedDownload.State.Should().Be(TrackedDownloadState.ImportPending); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFailedFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFailedFixture.cs new file mode 100644 index 000000000..b18c78a26 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFailedFixture.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Download.FailedDownloadServiceTests +{ + [TestFixture] + public class ProcessFailedFixture : CoreTest + { + private TrackedDownload _trackedDownload; + private List _grabHistory; + + [SetUp] + public void Setup() + { + var completed = Builder.CreateNew() + .With(h => h.Status = DownloadItemStatus.Completed) + .With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic())) + .With(h => h.Title = "Drone.S01E01.HDTV") + .Build(); + + _grabHistory = Builder.CreateListOfSize(2).BuildList(); + + var remoteAlbum = new RemoteAlbum + { + Artist = new Artist(), + Albums = new List { new Album { Id = 1 } } + }; + + _trackedDownload = Builder.CreateNew() + .With(c => c.State = TrackedDownloadState.DownloadFailedPending) + .With(c => c.DownloadItem = completed) + .With(c => c.RemoteAlbum = remoteAlbum) + .Build(); + + Mocker.GetMock() + .Setup(s => s.Find(_trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed)) + .Returns(_grabHistory); + } + + [Test] + public void should_mark_failed_if_encrypted() + { + _trackedDownload.DownloadItem.IsEncrypted = true; + + Subject.ProcessFailed(_trackedDownload); + + AssertDownloadFailed(); + } + + [Test] + public void should_mark_failed_if_download_item_is_failed() + { + _trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed; + + Subject.ProcessFailed(_trackedDownload); + + AssertDownloadFailed(); + } + + [Test] + public void should_include_tracked_download_in_message() + { + _trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed; + + Subject.ProcessFailed(_trackedDownload); + + Mocker.GetMock() + .Verify(v => v.PublishEvent(It.Is(c => c.TrackedDownload != null)), Times.Once()); + + AssertDownloadFailed(); + } + + private void AssertDownloadNotFailed() + { + Mocker.GetMock() + .Verify(v => v.PublishEvent(It.IsAny()), Times.Never()); + + _trackedDownload.State.Should().NotBe(TrackedDownloadState.DownloadFailed); + } + + private void AssertDownloadFailed() + { + Mocker.GetMock() + .Verify(v => v.PublishEvent(It.IsAny()), Times.Once()); + + _trackedDownload.State.Should().Be(TrackedDownloadState.DownloadFailed); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFixture.cs similarity index 72% rename from src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs rename to src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFixture.cs index 36b61ce42..e728a80ee 100644 --- a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFixture.cs @@ -13,10 +13,10 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; -namespace NzbDrone.Core.Test.Download +namespace NzbDrone.Core.Test.Download.FailedDownloadServiceTests { [TestFixture] - public class FailedDownloadServiceFixture : CoreTest + public class ProcessFixture : CoreTest { private TrackedDownload _trackedDownload; private List _grabHistory; @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.Download }; _trackedDownload = Builder.CreateNew() - .With(c => c.State = TrackedDownloadStage.Downloading) + .With(c => c.State = TrackedDownloadState.Downloading) .With(c => c.DownloadItem = completed) .With(c => c.RemoteAlbum = remoteAlbum) .Build(); @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Test.Download { GivenNoGrabbedHistory(); - Subject.Process(_trackedDownload); + Subject.Check(_trackedDownload); AssertDownloadNotFailed(); } @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.Download _trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed; GivenNoGrabbedHistory(); - Subject.Process(_trackedDownload); + Subject.Check(_trackedDownload); _trackedDownload.StatusMessages.Should().NotBeEmpty(); } @@ -83,50 +83,17 @@ namespace NzbDrone.Core.Test.Download _trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed; GivenNoGrabbedHistory(); - Subject.Process(_trackedDownload); + Subject.Check(_trackedDownload); _trackedDownload.StatusMessages.Should().NotBeEmpty(); } - [Test] - public void should_mark_failed_if_encrypted() - { - _trackedDownload.DownloadItem.IsEncrypted = true; - - Subject.Process(_trackedDownload); - - AssertDownloadFailed(); - } - - [Test] - public void should_mark_failed_if_download_item_is_failed() - { - _trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed; - - Subject.Process(_trackedDownload); - - AssertDownloadFailed(); - } - - [Test] - public void should_include_tracked_download_in_message() - { - _trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed; - - Subject.Process(_trackedDownload); - - Mocker.GetMock() - .Verify(v => v.PublishEvent(It.Is(c => c.TrackedDownload != null)), Times.Once()); - - AssertDownloadFailed(); - } - private void AssertDownloadNotFailed() { Mocker.GetMock() .Verify(v => v.PublishEvent(It.IsAny()), Times.Never()); - _trackedDownload.State.Should().NotBe(TrackedDownloadStage.DownloadFailed); + _trackedDownload.State.Should().NotBe(TrackedDownloadState.DownloadFailed); } private void AssertDownloadFailed() @@ -134,7 +101,7 @@ namespace NzbDrone.Core.Test.Download Mocker.GetMock() .Verify(v => v.PublishEvent(It.IsAny()), Times.Once()); - _trackedDownload.State.Should().Be(TrackedDownloadStage.DownloadFailed); + _trackedDownload.State.Should().Be(TrackedDownloadState.DownloadFailed); } } } diff --git a/src/NzbDrone.Core.Test/Download/RedownloadFailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/RedownloadFailedDownloadServiceFixture.cs index a870f32f2..2bed4caf8 100644 --- a/src/NzbDrone.Core.Test/Download/RedownloadFailedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/RedownloadFailedDownloadServiceFixture.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.Download SkipReDownload = true }; - Subject.HandleAsync(failedEvent); + Subject.Handle(failedEvent); Mocker.GetMock() .Verify(x => x.Push(It.IsAny(), It.IsAny(), It.IsAny()), @@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.Download .Setup(x => x.AutoRedownloadFailed) .Returns(false); - Subject.HandleAsync(failedEvent); + Subject.Handle(failedEvent); Mocker.GetMock() .Verify(x => x.Push(It.IsAny(), It.IsAny(), It.IsAny()), @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.Download AlbumIds = new List { 2 } }; - Subject.HandleAsync(failedEvent); + Subject.Handle(failedEvent); Mocker.GetMock() .Verify(x => x.Push(It.Is(c => c.AlbumIds.Count == 1 && @@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.Download AlbumIds = new List { 2, 3 } }; - Subject.HandleAsync(failedEvent); + Subject.Handle(failedEvent); Mocker.GetMock() .Verify(x => x.Push(It.Is(c => c.AlbumIds.Count == 2 && @@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.Download AlbumIds = new List { 1, 2, 3 } }; - Subject.HandleAsync(failedEvent); + Subject.Handle(failedEvent); Mocker.GetMock() .Verify(x => x.Push(It.Is(c => c.ArtistId == failedEvent.ArtistId), diff --git a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadAlreadyImportedFixture.cs b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadAlreadyImportedFixture.cs new file mode 100644 index 000000000..0d6567585 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadAlreadyImportedFixture.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.History; +using NzbDrone.Core.Music; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Download.TrackedDownloads +{ + [TestFixture] + public class TrackedDownloadAlreadyImportedFixture : CoreTest + { + private List _albums; + private TrackedDownload _trackedDownload; + private List _historyItems; + + [SetUp] + public void Setup() + { + _albums = new List(); + + var remoteAlbum = Builder.CreateNew() + .With(r => r.Albums = _albums) + .Build(); + + _trackedDownload = Builder.CreateNew() + .With(t => t.RemoteAlbum = remoteAlbum) + .Build(); + + _historyItems = new List(); + } + + public void GivenEpisodes(int count) + { + _albums.AddRange(Builder.CreateListOfSize(count) + .BuildList()); + } + + public void GivenHistoryForEpisode(Album episode, params HistoryEventType[] eventTypes) + { + foreach (var eventType in eventTypes) + { + _historyItems.Add( + Builder.CreateNew() + .With(h => h.AlbumId = episode.Id) + .With(h => h.EventType = eventType) + .Build()); + } + } + + [Test] + public void should_return_false_if_there_is_no_history() + { + GivenEpisodes(1); + + Subject.IsImported(_trackedDownload, _historyItems) + .Should() + .BeFalse(); + } + + [Test] + public void should_return_false_if_single_episode_download_is_not_imported() + { + GivenEpisodes(1); + + GivenHistoryForEpisode(_albums[0], HistoryEventType.Grabbed); + + Subject.IsImported(_trackedDownload, _historyItems) + .Should() + .BeFalse(); + } + + [Test] + public void should_return_false_if_no_episode_in_multi_episode_download_is_imported() + { + GivenEpisodes(2); + + GivenHistoryForEpisode(_albums[0], HistoryEventType.Grabbed); + GivenHistoryForEpisode(_albums[1], HistoryEventType.Grabbed); + + Subject.IsImported(_trackedDownload, _historyItems) + .Should() + .BeFalse(); + } + + [Test] + public void should_should_return_false_if_only_one_episode_in_multi_episode_download_is_imported() + { + GivenEpisodes(2); + + GivenHistoryForEpisode(_albums[0], HistoryEventType.DownloadImported, HistoryEventType.Grabbed); + GivenHistoryForEpisode(_albums[1], HistoryEventType.Grabbed); + + Subject.IsImported(_trackedDownload, _historyItems) + .Should() + .BeFalse(); + } + + [Test] + public void should_return_true_if_single_episode_download_is_imported() + { + GivenEpisodes(1); + + GivenHistoryForEpisode(_albums[0], HistoryEventType.DownloadImported, HistoryEventType.Grabbed); + + Subject.IsImported(_trackedDownload, _historyItems) + .Should() + .BeTrue(); + } + + [Test] + public void should_return_true_if_multi_episode_download_is_imported() + { + GivenEpisodes(2); + + GivenHistoryForEpisode(_albums[0], HistoryEventType.DownloadImported, HistoryEventType.Grabbed); + GivenHistoryForEpisode(_albums[1], HistoryEventType.DownloadImported, HistoryEventType.Grabbed); + + Subject.IsImported(_trackedDownload, _historyItems) + .Should() + .BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs index e59c42f0d..9fac7f97a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.MediaFiles { DownloadItem = downloadItem, RemoteAlbum = remoteAlbum, - State = TrackedDownloadStage.Downloading + State = TrackedDownloadState.Downloading }; } diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs index 887eef837..113f2b32f 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Test.MediaFiles { DownloadItem = downloadItem, RemoteAlbum = remoteAlbum, - State = TrackedDownloadStage.Downloading + State = TrackedDownloadState.Downloading }; } diff --git a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueFixture.cs b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueFixture.cs index bd37c938f..e7598a914 100644 --- a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueFixture.cs +++ b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueFixture.cs @@ -17,8 +17,8 @@ namespace NzbDrone.Core.Test.Messaging.Commands { var commandModel = Builder .CreateNew() - .With(c => c.Name = "CheckForFinishedDownload") - .With(c => c.Body = new CheckForFinishedDownloadCommand()) + .With(c => c.Name = "ProcessMonitoredDownloads") + .With(c => c.Body = new ProcessMonitoredDownloadsCommand()) .With(c => c.Status = CommandStatus.Started) .Build(); @@ -56,8 +56,8 @@ namespace NzbDrone.Core.Test.Messaging.Commands var newCommandModel = Builder .CreateNew() - .With(c => c.Name = "CheckForFinishedDownload") - .With(c => c.Body = new CheckForFinishedDownloadCommand()) + .With(c => c.Name = "ProcessMonitoredDownloads") + .With(c => c.Body = new ProcessMonitoredDownloadsCommand()) .Build(); Subject.Add(newCommandModel); diff --git a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs index 68ec47951..d0bf4a26e 100644 --- a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs +++ b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.Messaging.Commands [Test] public void should_not_remove_commands_for_five_minutes_after_they_end() { - var command = Subject.Push(new CheckForFinishedDownloadCommand()); + var command = Subject.Push(new RefreshMonitoredDownloadsCommand()); // Start the command to mimic CommandQueue's behaviour command.StartedAt = DateTime.Now; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 997e299a8..e512faffb 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Releases; @@ -43,7 +44,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var remoteAlbum = queueItem.RemoteAlbum; var qualityProfile = subject.Artist.QualityProfile.Value; + // To avoid a race make sure it's not FailedPending (failed awaiting removal/search). + // Failed items (already searching for a replacement) won't be part of the queue since + // it's a copy, of the tracked download, not a reference. + if (queueItem.TrackedDownloadState == TrackedDownloadState.DownloadFailedPending) + { + continue; + } + _logger.Debug("Checking if existing release in queue meets cutoff. Queued quality is: {0}", remoteAlbum.ParsedAlbumInfo.Quality); + var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Artist, queueItem.Title); if (!_upgradableSpecification.CutoffNotMet(qualityProfile, diff --git a/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs b/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs index 71f7f3d5e..a038f1a97 100644 --- a/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs +++ b/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs @@ -4,6 +4,5 @@ namespace NzbDrone.Core.Download { public class CheckForFinishedDownloadCommand : Command { - public override bool RequiresDiskAccess => true; } } diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index ba4cddab2..1fc346c8e 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -1,10 +1,8 @@ using System; using System.IO; using System.Linq; -using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles; @@ -12,43 +10,37 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; -using NzbDrone.Core.Parser; namespace NzbDrone.Core.Download { public interface ICompletedDownloadService { - void Process(TrackedDownload trackedDownload, bool ignoreWarnings = false); + void Check(TrackedDownload trackedDownload); + void Import(TrackedDownload trackedDownload); } public class CompletedDownloadService : ICompletedDownloadService { - private readonly IConfigService _configService; private readonly IEventAggregator _eventAggregator; private readonly IHistoryService _historyService; private readonly IDownloadedTracksImportService _downloadedTracksImportService; - private readonly IParsingService _parsingService; - private readonly Logger _logger; private readonly IArtistService _artistService; + private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported; - public CompletedDownloadService(IConfigService configService, - IEventAggregator eventAggregator, + public CompletedDownloadService(IEventAggregator eventAggregator, IHistoryService historyService, IDownloadedTracksImportService downloadedTracksImportService, - IParsingService parsingService, IArtistService artistService, - Logger logger) + ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported) { - _configService = configService; _eventAggregator = eventAggregator; _historyService = historyService; _downloadedTracksImportService = downloadedTracksImportService; - _parsingService = parsingService; - _logger = logger; _artistService = artistService; + _trackedDownloadAlreadyImported = trackedDownloadAlreadyImported; } - public void Process(TrackedDownload trackedDownload, bool ignoreWarnings = false) + public void Check(TrackedDownload trackedDownload) { if (trackedDownload.DownloadItem.Status != DownloadItemStatus.Completed || trackedDownload.RemoteAlbum == null) @@ -56,53 +48,58 @@ namespace NzbDrone.Core.Download return; } - if (!ignoreWarnings) + // Only process tracked downloads that are still downloading + if (trackedDownload.State != TrackedDownloadState.Downloading) { - var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId); + return; + } - if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) - { - trackedDownload.Warn("Download wasn't grabbed by Lidarr and not in a category, Skipping."); - return; - } + var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId); - var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath; + if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) + { + trackedDownload.Warn("Download wasn't grabbed by Lidarr and not in a category, Skipping."); + return; + } - if (downloadItemOutputPath.IsEmpty) - { - trackedDownload.Warn("Download doesn't contain intermediate path, Skipping."); - return; - } + var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath; - if ((OsInfo.IsWindows && !downloadItemOutputPath.IsWindowsPath) || - (OsInfo.IsNotWindows && !downloadItemOutputPath.IsUnixPath)) + if (downloadItemOutputPath.IsEmpty) + { + trackedDownload.Warn("Download doesn't contain intermediate path, Skipping."); + return; + } + + if ((OsInfo.IsWindows && !downloadItemOutputPath.IsWindowsPath) || + (OsInfo.IsNotWindows && !downloadItemOutputPath.IsUnixPath)) + { + trackedDownload.Warn("[{0}] is not a valid local path. You may need a Remote Path Mapping.", downloadItemOutputPath); + return; + } + + var artist = trackedDownload.RemoteAlbum.Artist; + + if (artist == null) + { + if (historyItem != null) { - trackedDownload.Warn("[{0}] is not a valid local path. You may need a Remote Path Mapping.", downloadItemOutputPath); - return; + artist = _artistService.GetArtist(historyItem.ArtistId); } - var artist = trackedDownload.RemoteAlbum.Artist; - if (artist == null) { - if (historyItem != null) - { - artist = _artistService.GetArtist(historyItem.ArtistId); - } - - if (artist == null) - { - trackedDownload.Warn("Artist name mismatch, automatic import is not possible."); - return; - } + trackedDownload.Warn("Artist name mismatch, automatic import is not possible."); + return; } } - Import(trackedDownload); + trackedDownload.State = TrackedDownloadState.ImportPending; } - private void Import(TrackedDownload trackedDownload) + public void Import(TrackedDownload trackedDownload) { + trackedDownload.State = TrackedDownloadState.Importing; + var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath; var importResults = _downloadedTracksImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteAlbum.Artist, trackedDownload.DownloadItem); @@ -112,17 +109,50 @@ namespace NzbDrone.Core.Download return; } - if (importResults.All(c => c.Result == ImportResultType.Imported) - || importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteAlbum.Albums.Sum(x => x.AlbumReleases.Value.Where(y => y.Monitored).Sum(z => z.TrackCount)))) + var allTracksImported = importResults.All(c => c.Result == ImportResultType.Imported) || + importResults.Count(c => c.Result == ImportResultType.Imported) >= + Math.Max(1, trackedDownload.RemoteAlbum.Albums.Sum(x => x.AlbumReleases.Value.Where(y => y.Monitored).Sum(z => z.TrackCount))); + + Console.WriteLine($"allimported: {allTracksImported}"); + Console.WriteLine($"count: {importResults.Count(c => c.Result == ImportResultType.Imported)}"); + Console.WriteLine($"max: {Math.Max(1, trackedDownload.RemoteAlbum.Albums.Sum(x => x.AlbumReleases.Value.Where(y => y.Monitored).Sum(z => z.TrackCount)))}"); + + if (allTracksImported) { - trackedDownload.State = TrackedDownloadStage.Imported; + trackedDownload.State = TrackedDownloadState.Imported; _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); return; } + // Double check if all albums were imported by checking the history if at least one + // file was imported. This will allow the decision engine to reject already imported + // albums and still mark the download complete when all files are imported. + if (importResults.Any(c => c.Result == ImportResultType.Imported)) + { + var historyItems = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId) + .OrderByDescending(h => h.Date) + .ToList(); + + var allTracksImportedInHistory = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems); + + if (allTracksImportedInHistory) + { + trackedDownload.State = TrackedDownloadState.Imported; + _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); + return; + } + } + + trackedDownload.State = TrackedDownloadState.ImportPending; + + if (importResults.Empty()) + { + trackedDownload.Warn("No files found are eligible for import in {0}", outputPath); + } + if (importResults.Any(c => c.Result != ImportResultType.Imported)) { - trackedDownload.State = TrackedDownloadStage.ImportFailed; + trackedDownload.State = TrackedDownloadState.ImportFailed; var statusMessages = importResults .Where(v => v.Result != ImportResultType.Imported) .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.Item.Path), v.Errors)) @@ -130,6 +160,7 @@ namespace NzbDrone.Core.Download trackedDownload.Warn(statusMessages); _eventAggregator.PublishEvent(new AlbumImportIncompleteEvent(trackedDownload)); + return; } } } diff --git a/src/NzbDrone.Core/Download/DownloadProcessingService.cs b/src/NzbDrone.Core/Download/DownloadProcessingService.cs new file mode 100644 index 000000000..aaa8079e2 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadProcessingService.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Download +{ + public class DownloadProcessingService : IExecute + { + private readonly IConfigService _configService; + private readonly ICompletedDownloadService _completedDownloadService; + private readonly IFailedDownloadService _failedDownloadService; + private readonly ITrackedDownloadService _trackedDownloadService; + private readonly IEventAggregator _eventAggregator; + + public DownloadProcessingService(IConfigService configService, + ICompletedDownloadService completedDownloadService, + IFailedDownloadService failedDownloadService, + ITrackedDownloadService trackedDownloadService, + IEventAggregator eventAggregator) + { + _configService = configService; + _completedDownloadService = completedDownloadService; + _failedDownloadService = failedDownloadService; + _trackedDownloadService = trackedDownloadService; + _eventAggregator = eventAggregator; + } + + private void RemoveCompletedDownloads(List trackedDownloads) + { + foreach (var trackedDownload in trackedDownloads.Where(c => c.DownloadItem.CanBeRemoved && c.State == TrackedDownloadState.Imported)) + { + _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); + } + } + + public void Execute(ProcessMonitoredDownloadsCommand message) + { + var enableCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling; + var trackedDownloads = _trackedDownloadService.GetTrackedDownloads(); + + foreach (var trackedDownload in trackedDownloads) + { + if (trackedDownload.State == TrackedDownloadState.DownloadFailedPending) + { + _failedDownloadService.ProcessFailed(trackedDownload); + } + + if (enableCompletedDownloadHandling && trackedDownload.State == TrackedDownloadState.ImportPending) + { + _completedDownloadService.Import(trackedDownload); + } + } + + if (enableCompletedDownloadHandling && _configService.RemoveCompletedDownloads) + { + // Remove tracked downloads that are now complete + RemoveCompletedDownloads(trackedDownloads); + } + + _eventAggregator.PublishEvent(new DownloadsProcessedEvent()); + } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadsProcessedEvent.cs b/src/NzbDrone.Core/Download/DownloadsProcessedEvent.cs new file mode 100644 index 000000000..8d85fd8c5 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadsProcessedEvent.cs @@ -0,0 +1,11 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Download +{ + public class DownloadsProcessedEvent : IEvent + { + public DownloadsProcessedEvent() + { + } + } +} diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index a86374ae0..83f65935f 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -11,7 +11,8 @@ namespace NzbDrone.Core.Download { void MarkAsFailed(int historyId, bool skipReDownload = false); void MarkAsFailed(string downloadId, bool skipReDownload = false); - void Process(TrackedDownload trackedDownload); + void Check(TrackedDownload trackedDownload); + void ProcessFailed(TrackedDownload trackedDownload); } public class FailedDownloadService : IFailedDownloadService @@ -52,23 +53,20 @@ namespace NzbDrone.Core.Download } } - public void Process(TrackedDownload trackedDownload) + public void Check(TrackedDownload trackedDownload) { - string failure = null; - - if (trackedDownload.DownloadItem.IsEncrypted) - { - failure = "Encrypted download detected"; - } - else if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed) + // Only process tracked downloads that are still downloading + if (trackedDownload.State != TrackedDownloadState.Downloading) { - failure = trackedDownload.DownloadItem.Message ?? "Failed download detected"; + return; } - if (failure != null) + if (trackedDownload.DownloadItem.IsEncrypted || + trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed) { - var grabbedItems = _historyService.Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed) - .ToList(); + var grabbedItems = _historyService + .Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed) + .ToList(); if (grabbedItems.Empty()) { @@ -76,9 +74,39 @@ namespace NzbDrone.Core.Download return; } - trackedDownload.State = TrackedDownloadStage.DownloadFailed; - PublishDownloadFailedEvent(grabbedItems, failure, trackedDownload); + trackedDownload.State = TrackedDownloadState.DownloadFailedPending; + } + } + + public void ProcessFailed(TrackedDownload trackedDownload) + { + if (trackedDownload.State != TrackedDownloadState.DownloadFailedPending) + { + return; } + + var grabbedItems = _historyService + .Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed) + .ToList(); + + if (grabbedItems.Empty()) + { + return; + } + + var failure = "Failed download detected"; + + if (trackedDownload.DownloadItem.IsEncrypted) + { + failure = "Encrypted download detected"; + } + else if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed && trackedDownload.DownloadItem.Message.IsNotNullOrWhiteSpace()) + { + failure = trackedDownload.DownloadItem.Message; + } + + trackedDownload.State = TrackedDownloadState.DownloadFailed; + PublishDownloadFailedEvent(grabbedItems, failure, trackedDownload); } private void PublishDownloadFailedEvent(List historyItems, string message, TrackedDownload trackedDownload = null, bool skipReDownload = false) diff --git a/src/NzbDrone.Core/Download/ProcessMonitoredDownloadsCommand.cs b/src/NzbDrone.Core/Download/ProcessMonitoredDownloadsCommand.cs new file mode 100644 index 000000000..c3c934031 --- /dev/null +++ b/src/NzbDrone.Core/Download/ProcessMonitoredDownloadsCommand.cs @@ -0,0 +1,9 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Download +{ + public class ProcessMonitoredDownloadsCommand : Command + { + public override bool RequiresDiskAccess => true; + } +} diff --git a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs index 7c3b4ae6b..a091211f6 100644 --- a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs @@ -1,13 +1,14 @@ using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.Messaging; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; namespace NzbDrone.Core.Download { - public class RedownloadFailedDownloadService : IHandleAsync + public class RedownloadFailedDownloadService : IHandle { private readonly IConfigService _configService; private readonly IAlbumService _albumService; @@ -25,7 +26,8 @@ namespace NzbDrone.Core.Download _logger = logger; } - public void HandleAsync(DownloadFailedEvent message) + [EventHandleOrder(EventHandleOrder.Last)] + public void Handle(DownloadFailedEvent message) { if (message.SkipReDownload) { diff --git a/src/NzbDrone.Core/Download/RefreshMonitoredDownloadsCommand.cs b/src/NzbDrone.Core/Download/RefreshMonitoredDownloadsCommand.cs new file mode 100644 index 000000000..b4b516b61 --- /dev/null +++ b/src/NzbDrone.Core/Download/RefreshMonitoredDownloadsCommand.cs @@ -0,0 +1,8 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Download +{ + public class RefreshMonitoredDownloadsCommand : Command + { + } +} diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs index e8bdce3f6..63396f973 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs @@ -11,9 +11,10 @@ using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Download.TrackedDownloads { - public class DownloadMonitoringService : IExecute, + public class DownloadMonitoringService : IExecute, IHandle, IHandle, + IHandle, IHandle { private readonly IDownloadClientStatusService _downloadClientStatusService; @@ -52,7 +53,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads private void QueueRefresh() { - _manageCommandQueue.Push(new CheckForFinishedDownloadCommand()); + _manageCommandQueue.Push(new RefreshMonitoredDownloadsCommand()); } private void Refresh() @@ -73,6 +74,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads _trackedDownloadService.UpdateTrackable(trackedDownloads); _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads)); + _manageCommandQueue.Push(new ProcessMonitoredDownloadsCommand()); } finally { @@ -82,12 +84,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads private List ProcessClientDownloads(IDownloadClient downloadClient) { - List downloadClientHistory = new List(); + var downloadClientItems = new List(); var trackedDownloads = new List(); try { - downloadClientHistory = downloadClient.GetItems().ToList(); + downloadClientItems = downloadClient.GetItems().ToList(); _downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id); } @@ -97,59 +99,40 @@ namespace NzbDrone.Core.Download.TrackedDownloads _logger.Warn(ex, "Unable to retrieve queue and history items from " + downloadClient.Definition.Name); } - foreach (var downloadItem in downloadClientHistory) + foreach (var downloadItem in downloadClientItems) { - var newItems = ProcessClientItems(downloadClient, downloadItem); - trackedDownloads.AddRange(newItems); - } - - if (_configService.EnableCompletedDownloadHandling && _configService.RemoveCompletedDownloads) - { - RemoveCompletedDownloads(trackedDownloads); + var item = ProcessClientItem(downloadClient, downloadItem); + trackedDownloads.AddIfNotNull(item); } return trackedDownloads; } - private void RemoveCompletedDownloads(List trackedDownloads) + private TrackedDownload ProcessClientItem(IDownloadClient downloadClient, DownloadClientItem downloadItem) { - foreach (var trackedDownload in trackedDownloads.Where(c => c.DownloadItem.CanBeRemoved && c.State == TrackedDownloadStage.Imported)) - { - _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); - } - } - - private List ProcessClientItems(IDownloadClient downloadClient, DownloadClientItem downloadItem) - { - var trackedDownloads = new List(); try { var trackedDownload = _trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition, downloadItem); - if (trackedDownload != null && trackedDownload.State == TrackedDownloadStage.Downloading) + if (trackedDownload != null && trackedDownload.State == TrackedDownloadState.Downloading) { - _failedDownloadService.Process(trackedDownload); - - if (_configService.EnableCompletedDownloadHandling) - { - _completedDownloadService.Process(trackedDownload); - } + _failedDownloadService.Check(trackedDownload); + _completedDownloadService.Check(trackedDownload); } - trackedDownloads.AddIfNotNull(trackedDownload); + return trackedDownload; } catch (Exception e) { _logger.Error(e, "Couldn't process tracked download {0}", downloadItem.Title); } - return trackedDownloads; + return null; } private bool DownloadIsTrackable(TrackedDownload trackedDownload) { // If the download has already been imported or failed don't track it - if (trackedDownload.State == TrackedDownloadStage.DownloadFailed - || trackedDownload.State == TrackedDownloadStage.Imported) + if (trackedDownload.State == TrackedDownloadState.Imported || trackedDownload.State == TrackedDownloadState.DownloadFailed) { return false; } @@ -163,8 +146,14 @@ namespace NzbDrone.Core.Download.TrackedDownloads return true; } + public void Execute(RefreshMonitoredDownloadsCommand message) + { + Refresh(); + } + public void Execute(CheckForFinishedDownloadCommand message) { + _logger.Warn("A third party app used the deprecated CheckForFinishedDownload command, it should be updated RefreshMonitoredDownloads instead"); Refresh(); } @@ -178,6 +167,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads _refreshDebounce.Execute(); } + public void Handle(DownloadsProcessedEvent message) + { + var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList(); + + _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads)); + } + public void Handle(TrackedDownloadsRemovedEvent message) { var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList(); diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index 73294e8e8..d88fb5911 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads { public int DownloadClient { get; set; } public DownloadClientItem DownloadItem { get; set; } - public TrackedDownloadStage State { get; set; } + public TrackedDownloadState State { get; set; } public TrackedDownloadStatus Status { get; private set; } public RemoteAlbum RemoteAlbum { get; set; } public TrackedDownloadStatusMessage[] StatusMessages { get; private set; } @@ -33,10 +33,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads } } - public enum TrackedDownloadStage + public enum TrackedDownloadState { Downloading, DownloadFailed, + DownloadFailedPending, + ImportPending, Importing, ImportFailed, Imported @@ -45,6 +47,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads public enum TrackedDownloadStatus { Ok, - Warning + Warning, + Error } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs new file mode 100644 index 000000000..3a91ae657 --- /dev/null +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.History; + +namespace NzbDrone.Core.Download.TrackedDownloads +{ + public interface ITrackedDownloadAlreadyImported + { + bool IsImported(TrackedDownload trackedDownload, List historyItems); + } + + public class TrackedDownloadAlreadyImported : ITrackedDownloadAlreadyImported + { + public bool IsImported(TrackedDownload trackedDownload, List historyItems) + { + if (historyItems.Empty()) + { + return false; + } + + if (trackedDownload.RemoteAlbum == null || trackedDownload.RemoteAlbum.Albums == null) + { + return true; + } + + var allAlbumsImportedInHistory = trackedDownload.RemoteAlbum.Albums.All(album => + { + var lastHistoryItem = historyItems.FirstOrDefault(h => h.AlbumId == album.Id); + + if (lastHistoryItem == null) + { + return false; + } + + return new[] { HistoryEventType.DownloadImported, HistoryEventType.TrackFileImported }.Contains(lastHistoryItem.EventType); + }); + + return allAlbumsImportedInHistory; + } + } +} diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 745a1e8b2..6384c1992 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -28,6 +28,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads private readonly IParsingService _parsingService; private readonly IHistoryService _historyService; private readonly IEventAggregator _eventAggregator; + private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported; private readonly Logger _logger; private readonly ICached _cache; @@ -35,11 +36,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads ICacheManager cacheManager, IHistoryService historyService, IEventAggregator eventAggregator, + ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported, Logger logger) { _parsingService = parsingService; _historyService = historyService; _eventAggregator = eventAggregator; + _trackedDownloadAlreadyImported = trackedDownloadAlreadyImported; _cache = cacheManager.GetCache(GetType()); _logger = logger; } @@ -93,7 +96,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads { var existingItem = Find(downloadItem.DownloadId); - if (existingItem != null && existingItem.State != TrackedDownloadStage.Downloading) + if (existingItem != null && existingItem.State != TrackedDownloadState.Downloading) { LogItemChange(existingItem, existingItem.DownloadItem, downloadItem); @@ -114,7 +117,9 @@ namespace NzbDrone.Core.Download.TrackedDownloads try { var parsedAlbumInfo = Parser.Parser.ParseAlbumTitle(trackedDownload.DownloadItem.Title); - var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId); + var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId) + .OrderByDescending(h => h.Date) + .ToList(); if (parsedAlbumInfo != null) { @@ -123,8 +128,22 @@ namespace NzbDrone.Core.Download.TrackedDownloads if (historyItems.Any()) { - var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).First(); - trackedDownload.State = GetStateFromHistory(firstHistoryItem); + var firstHistoryItem = historyItems.First(); + var state = GetStateFromHistory(firstHistoryItem); + + // One potential issue here is if the latest is imported, but other episodes are ignored or never imported. + // It's unlikely that will happen, but could happen if additional episodes are added to season after it's already imported. + if (state == TrackedDownloadState.Imported) + { + var allImported = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems); + + trackedDownload.State = allImported ? TrackedDownloadState.Imported : TrackedDownloadState.Downloading; + } + else + { + trackedDownload.State = state; + } + if (firstHistoryItem.EventType == HistoryEventType.AlbumImportIncomplete) { var messages = Json.Deserialize>(firstHistoryItem?.Data["statusMessages"]).ToArray(); @@ -223,25 +242,25 @@ namespace NzbDrone.Core.Download.TrackedDownloads } } - private static TrackedDownloadStage GetStateFromHistory(NzbDrone.Core.History.History history) + private static TrackedDownloadState GetStateFromHistory(NzbDrone.Core.History.History history) { switch (history.EventType) { case HistoryEventType.AlbumImportIncomplete: - return TrackedDownloadStage.ImportFailed; + return TrackedDownloadState.ImportFailed; case HistoryEventType.DownloadImported: - return TrackedDownloadStage.Imported; + return TrackedDownloadState.Imported; case HistoryEventType.DownloadFailed: - return TrackedDownloadStage.DownloadFailed; + return TrackedDownloadState.DownloadFailed; } // Since DownloadComplete is a new event type, we can't assume it exists for old downloads if (history.EventType == HistoryEventType.TrackFileImported) { - return DateTime.UtcNow.Subtract(history.Date).TotalSeconds < 60 ? TrackedDownloadStage.Importing : TrackedDownloadStage.Imported; + return DateTime.UtcNow.Subtract(history.Date).TotalSeconds < 60 ? TrackedDownloadState.Importing : TrackedDownloadState.Imported; } - return TrackedDownloadStage.Downloading; + return TrackedDownloadState.Downloading; } public void Handle(AlbumDeletedEvent message) diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 65c2a8783..e2ed6f7d7 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Jobs { var defaultTasks = new[] { - new ScheduledTask { Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName }, + new ScheduledTask { Interval = 1, TypeName = typeof(RefreshMonitoredDownloadsCommand).FullName }, new ScheduledTask { Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName }, new ScheduledTask { Interval = 6 * 60, TypeName = typeof(ApplicationUpdateCheckCommand).FullName }, new ScheduledTask { Interval = 6 * 60, TypeName = typeof(CheckHealthCommand).FullName }, diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 29c3d0057..d002973a7 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -375,7 +375,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteAlbum.Albums.Count)) { - trackedDownload.State = TrackedDownloadStage.Imported; + trackedDownload.State = TrackedDownloadState.Imported; _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlreadyImportedSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlreadyImportedSpecification.cs new file mode 100644 index 000000000..3f564ea7f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlreadyImportedSpecification.cs @@ -0,0 +1,65 @@ +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.History; +using NzbDrone.Core.MediaFiles.TrackImport; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications +{ + public class AlreadyImportedSpecification : IImportDecisionEngineSpecification + { + private readonly IHistoryService _historyService; + private readonly Logger _logger; + + public AlreadyImportedSpecification(IHistoryService historyService, + Logger logger) + { + _historyService = historyService; + _logger = logger; + } + + public SpecificationPriority Priority => SpecificationPriority.Database; + + public Decision IsSatisfiedBy(LocalAlbumRelease localAlbumRelease, DownloadClientItem downloadClientItem) + { + if (downloadClientItem == null) + { + _logger.Debug("No download client information is available, skipping"); + return Decision.Accept(); + } + + var albumRelease = localAlbumRelease.AlbumRelease; + + if (!albumRelease.Tracks.Value.Any(x => x.HasFile)) + { + _logger.Debug("Skipping already imported check for album without files"); + return Decision.Accept(); + } + + var albumHistory = _historyService.GetByAlbum(albumRelease.AlbumId, null); + var lastImported = albumHistory.FirstOrDefault(h => h.EventType == HistoryEventType.DownloadImported); + var lastGrabbed = albumHistory.FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed); + + if (lastImported == null) + { + return Decision.Accept(); + } + + if (lastGrabbed != null && lastGrabbed.Date.After(lastImported.Date)) + { + return Decision.Accept(); + } + + if (lastImported.DownloadId == downloadClientItem.DownloadId) + { + _logger.Debug("Album previously imported at {0}", lastImported.Date); + return Decision.Reject("Album already imported at {0}", lastImported.Date); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs b/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs index 989a8687b..e492381de 100644 --- a/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs +++ b/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs @@ -21,16 +21,12 @@ namespace NzbDrone.Core.Messaging.Events private class EventSubscribers where TEvent : class, IEvent { - private IServiceFactory _serviceFactory; - public IHandle[] _syncHandlers; public IHandleAsync[] _asyncHandlers; public IHandleAsync[] _globalHandlers; public EventSubscribers(IServiceFactory serviceFactory) { - _serviceFactory = serviceFactory; - _syncHandlers = serviceFactory.BuildAll>() .OrderBy(GetEventHandleOrder) .ToArray(); @@ -144,8 +140,7 @@ namespace NzbDrone.Core.Messaging.Events internal static int GetEventHandleOrder(IHandle eventHandler) where TEvent : class, IEvent { - // TODO: Convert "Handle" to nameof(eventHandler.Handle) after .net 4.5 - var method = eventHandler.GetType().GetMethod("Handle", new Type[] { typeof(TEvent) }); + var method = eventHandler.GetType().GetMethod(nameof(eventHandler.Handle), new Type[] { typeof(TEvent) }); if (method == null) { diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 1c969c381..e8e994b37 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -20,7 +20,8 @@ namespace NzbDrone.Core.Queue public TimeSpan? Timeleft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } public string Status { get; set; } - public string TrackedDownloadStatus { get; set; } + public TrackedDownloadStatus? TrackedDownloadStatus { get; set; } + public TrackedDownloadState? TrackedDownloadState { get; set; } public List StatusMessages { get; set; } public string DownloadId { get; set; } public RemoteAlbum RemoteAlbum { get; set; } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 00401c7a2..9783c0fc0 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -79,7 +79,8 @@ namespace NzbDrone.Core.Queue Sizeleft = trackedDownload.DownloadItem.RemainingSize, Timeleft = trackedDownload.DownloadItem.RemainingTime, Status = trackedDownload.DownloadItem.Status.ToString(), - TrackedDownloadStatus = trackedDownload.Status.ToString(), + TrackedDownloadStatus = trackedDownload.Status, + TrackedDownloadState = trackedDownload.State, StatusMessages = trackedDownload.StatusMessages.ToList(), ErrorMessage = trackedDownload.DownloadItem.Message, RemoteAlbum = trackedDownload.RemoteAlbum, diff --git a/src/NzbDrone.Host.Test/ContainerFixture.cs b/src/NzbDrone.Host.Test/ContainerFixture.cs index 7da6a9fbc..af4383be3 100644 --- a/src/NzbDrone.Host.Test/ContainerFixture.cs +++ b/src/NzbDrone.Host.Test/ContainerFixture.cs @@ -89,8 +89,8 @@ namespace NzbDrone.App.Test [Test] public void should_return_same_instance_of_singletons_by_different_interfaces() { - var first = _container.ResolveAll>().OfType().Single(); - var second = (DownloadMonitoringService)_container.Resolve>(); + var first = _container.ResolveAll>().OfType().Single(); + var second = (DownloadMonitoringService)_container.Resolve>(); first.Should().BeSameAs(second); }