From caa6a3858bf55d4a3eea7fb236cb0e9006194fe5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 13 Sep 2014 16:13:00 -0700 Subject: [PATCH] Use DownloadClientIds to find matching series/episodes instead of relying solely on release name Fixed: Show a warning in Queue when drone is unable to import due to a name mismatch Fixed: Better UI messages for Queue when there is an error or warning --- src/NzbDrone.Api/Queue/QueueResource.cs | 5 +- .../NotInQueueSpecificationFixture.cs | 5 +- .../CompletedDownloadServiceFixture.cs | 40 +++++++---- .../DownloadClientFixtureBase.cs | 7 +- .../Download/FailedDownloadServiceFixture.cs | 38 +++++++--- .../Specifications/NotInQueueSpecification.cs | 2 +- .../Download/Clients/Nzbget/Nzbget.cs | 3 - .../Download/Clients/Pneumatic/Pneumatic.cs | 3 - .../Download/Clients/Sabnzbd/Sabnzbd.cs | 3 - .../UsenetBlackhole/UsenetBlackhole.cs | 6 -- .../Download/CompletedDownloadService.cs | 33 +++++---- .../Download/DownloadClientBase.cs | 14 ---- .../Download/DownloadClientItem.cs | 5 +- .../Download/DownloadItemStatus.cs | 7 +- .../Download/DownloadTrackingService.cs | 71 +++++++++++++++---- .../Download/FailedDownloadService.cs | 12 ++-- src/NzbDrone.Core/Download/TrackedDownload.cs | 34 ++++++++- .../Download/TrackedDownloadStatusMessage.cs | 23 ++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/NzbDrone.Core/Parser/ParsingService.cs | 17 ++++- src/NzbDrone.Core/Queue/Queue.cs | 5 +- src/NzbDrone.Core/Queue/QueueService.cs | 31 ++++---- src/NzbDrone.Core/Tv/EpisodeService.cs | 12 +++- src/UI/Content/icons.less | 4 ++ src/UI/History/Queue/QueueStatusCell.js | 52 +++++++++----- .../History/Queue/QueueStatusCellTemplate.hbs | 8 +++ 26 files changed, 292 insertions(+), 149 deletions(-) create mode 100644 src/NzbDrone.Core/Download/TrackedDownloadStatusMessage.cs create mode 100644 src/UI/History/Queue/QueueStatusCellTemplate.hbs diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index 0cc002347..ce4bd73bb 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using NzbDrone.Api.REST; +using NzbDrone.Core.Download; using NzbDrone.Core.Qualities; using NzbDrone.Api.Series; using NzbDrone.Api.Episodes; @@ -17,6 +19,7 @@ public class QueueResource : RestResource public TimeSpan? Timeleft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } public String Status { get; set; } - public String ErrorMessage { get; set; } + public String TrackedDownloadStatus { get; set; } + public List StatusMessages { get; set; } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs index be385ae90..36ace6346 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs @@ -69,10 +69,7 @@ private void GivenQueue(IEnumerable remoteEpisodes, TrackedDownlo queue.Add(new TrackedDownload { State = state, - DownloadItem = new DownloadClientItem - { - RemoteEpisode = remoteEpisode - } + RemoteEpisode = remoteEpisode }); } diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs index 2d841db69..cd680f7a0 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs @@ -9,11 +9,13 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; using NzbDrone.Core.History; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; -using NzbDrone.Test.Common; using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Test.Download @@ -27,15 +29,18 @@ public class CompletedDownloadServiceFixture : CoreTest public void Setup() { _completed = Builder.CreateListOfSize(1) - .All() - .With(h => h.Status = DownloadItemStatus.Completed) - .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) - .With(h => h.RemoteEpisode = new RemoteEpisode - { - Episodes = new List { new Episode { Id = 1 } } - }) - .Build() - .ToList(); + .All() + .With(h => h.Status = DownloadItemStatus.Completed) + .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) + .With(h => h.Title = "Drone.S01E01.HDTV") + .Build() + .ToList(); + + var remoteEpisode = new RemoteEpisode + { + Series = new Series(), + Episodes = new List {new Episode {Id = 1}} + }; Mocker.GetMock() .Setup(c => c.GetDownloadClients()) @@ -43,7 +48,7 @@ public void Setup() Mocker.GetMock() .SetupGet(c => c.Definition) - .Returns(new Core.Download.DownloadClientDefinition { Id = 1, Name = "testClient" }); + .Returns(new DownloadClientDefinition { Id = 1, Name = "testClient" }); Mocker.GetMock() .SetupGet(s => s.EnableCompletedDownloadHandling) @@ -56,6 +61,14 @@ public void Setup() Mocker.GetMock() .Setup(s => s.Failed()) .Returns(new List()); + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(remoteEpisode); + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), (SearchCriteriaBase)null)) + .Returns(remoteEpisode); Mocker.SetConstant(Mocker.Resolve()); } @@ -311,10 +324,7 @@ public void should_process_as_already_imported_if_drone_factory_import_history_e .All() .With(h => h.Status = DownloadItemStatus.Completed) .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) - .With(h => h.RemoteEpisode = new RemoteEpisode - { - Episodes = new List { new Episode { Id = 1 } } - }) + .With(h => h.Title = "Drone.S01E01.HDTV") .Build()); var grabbedHistory = Builder.CreateListOfSize(2) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs index 96f965b2f..9186072a1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using FluentAssertions; using NzbDrone.Common.Http; +using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser; @@ -30,7 +31,7 @@ public void SetupBase() .Returns(30); Mocker.GetMock() - .Setup(s => s.Map(It.IsAny(), It.IsAny(), null)) + .Setup(s => s.Map(It.IsAny(), It.IsAny(), (SearchCriteriaBase)null)) .Returns(() => CreateRemoteEpisode()); Mocker.GetMock() @@ -64,11 +65,7 @@ protected void VerifyIdentifiable(DownloadClientItem downloadClientItem) { downloadClientItem.DownloadClient.Should().Be(Subject.Definition.Name); downloadClientItem.DownloadClientId.Should().NotBeNullOrEmpty(); - downloadClientItem.Title.Should().NotBeNullOrEmpty(); - - downloadClientItem.RemoteEpisode.Should().NotBeNull(); - } protected void VerifyQueued(DownloadClientItem downloadClientItem) diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs index 0898c64aa..901c91195 100644 --- a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs @@ -7,8 +7,12 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; using NzbDrone.Core.History; +using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download @@ -23,16 +27,24 @@ public class FailedDownloadServiceFixture : CoreTest public void Setup() { _completed = Builder.CreateListOfSize(5) - .All() - .With(h => h.Status = DownloadItemStatus.Completed) - .Build() - .ToList(); + .All() + .With(h => h.Status = DownloadItemStatus.Completed) + .With(h => h.Title = "Drone.S01E01.HDTV") + .Build() + .ToList(); _failed = Builder.CreateListOfSize(1) - .All() - .With(h => h.Status = DownloadItemStatus.Failed) - .Build() - .ToList(); + .All() + .With(h => h.Status = DownloadItemStatus.Failed) + .With(h => h.Title = "Drone.S01E01.HDTV") + .Build() + .ToList(); + + var remoteEpisode = new RemoteEpisode + { + Series = new Series(), + Episodes = new List { new Episode { Id = 1 } } + }; Mocker.GetMock() .Setup(c => c.GetDownloadClients()) @@ -40,7 +52,7 @@ public void Setup() Mocker.GetMock() .SetupGet(c => c.Definition) - .Returns(new Core.Download.DownloadClientDefinition { Id = 1, Name = "testClient" }); + .Returns(new DownloadClientDefinition { Id = 1, Name = "testClient" }); Mocker.GetMock() .SetupGet(s => s.EnableFailedDownloadHandling) @@ -50,6 +62,14 @@ public void Setup() .Setup(s => s.Imported()) .Returns(new List()); + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(remoteEpisode); + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), (SearchCriteriaBase)null)) + .Returns(remoteEpisode); + Mocker.SetConstant(Mocker.Resolve()); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs index fa58e4119..3e53f7c91 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs @@ -34,7 +34,7 @@ public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriter { var queue = _downloadTrackingService.GetQueuedDownloads() .Where(v => v.State == TrackedDownloadState.Downloading) - .Select(q => q.DownloadItem.RemoteEpisode).ToList(); + .Select(q => q.RemoteEpisode).ToList(); if (IsInQueue(subject, queue)) { diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 1ea2c003c..800515156 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -201,9 +201,6 @@ public override IEnumerable GetItems() { if (downloadClientItem.Category == Settings.TvCategory) { - downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); - if (downloadClientItem.RemoteEpisode == null) continue; - yield return downloadClientItem; } } diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 5c19f2e70..d55e4e5dc 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -103,9 +103,6 @@ public override IEnumerable GetItems() historyItem.Status = DownloadItemStatus.Completed; } - historyItem.RemoteEpisode = GetRemoteEpisode(historyItem.Title); - if (historyItem.RemoteEpisode == null) continue; - yield return historyItem; } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 59142e1a6..35ca256d1 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -188,9 +188,6 @@ public override IEnumerable GetItems() { if (downloadClientItem.Category == Settings.TvCategory) { - downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); - if (downloadClientItem.RemoteEpisode == null) continue; - yield return downloadClientItem; } } diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs index 584dde186..bc760fb4c 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -90,9 +90,6 @@ public override IEnumerable GetItems() historyItem.RemainingTime = TimeSpan.Zero; } - historyItem.RemoteEpisode = GetRemoteEpisode(historyItem.Title); - if (historyItem.RemoteEpisode == null) continue; - yield return historyItem; } @@ -121,9 +118,6 @@ public override IEnumerable GetItems() historyItem.RemainingTime = TimeSpan.Zero; } - historyItem.RemoteEpisode = GetRemoteEpisode(historyItem.Title); - if (historyItem.RemoteEpisode == null) continue; - yield return historyItem; } } diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 6c774ea1e..f1dc1cfe1 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -8,7 +8,6 @@ using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; -using NzbDrone.Core.Messaging.Events; using System.IO; namespace NzbDrone.Core.Download @@ -20,21 +19,18 @@ public interface ICompletedDownloadService public class CompletedDownloadService : ICompletedDownloadService { - private readonly IEventAggregator _eventAggregator; private readonly IConfigService _configService; private readonly IDiskProvider _diskProvider; private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; private readonly IHistoryService _historyService; private readonly Logger _logger; - public CompletedDownloadService(IEventAggregator eventAggregator, - IConfigService configService, - IDiskProvider diskProvider, - IDownloadedEpisodesImportService downloadedEpisodesImportService, - IHistoryService historyService, - Logger logger) + public CompletedDownloadService(IConfigService configService, + IDiskProvider diskProvider, + IDownloadedEpisodesImportService downloadedEpisodesImportService, + IHistoryService historyService, + Logger logger) { - _eventAggregator = eventAggregator; _configService = configService; _diskProvider = diskProvider; _downloadedEpisodesImportService = downloadedEpisodesImportService; @@ -61,7 +57,7 @@ public void CheckForCompletedItem(IDownloadClient downloadClient, TrackedDownloa if (!grabbedItems.Any() && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) { - UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Download wasn't grabbed by drone or not in a category, ignoring download."); + UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download wasn't grabbed by drone or not in a category, ignoring download."); return; } @@ -73,10 +69,16 @@ public void CheckForCompletedItem(IDownloadClient downloadClient, TrackedDownloa UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Already added to history as imported."); } + else if (trackedDownload.Status != TrackedDownloadStatus.Ok) + { + _logger.Debug("Tracked download status is: {0}, skipping import.", trackedDownload.Status); + return; + } else { - string downloadedEpisodesFolder = _configService.DownloadedEpisodesFolder; - string downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath; + var downloadedEpisodesFolder = _configService.DownloadedEpisodesFolder; + var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath; + if (downloadItemOutputPath.IsNullOrWhiteSpace()) { UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download doesn't contain intermediate path, ignoring download."); @@ -105,7 +107,7 @@ public void CheckForCompletedItem(IDownloadClient downloadClient, TrackedDownloa { if (grabbedItems.Any()) { - var episodeIds = trackedDownload.DownloadItem.RemoteEpisode.Episodes.Select(v => v.Id).ToList(); + var episodeIds = trackedDownload.RemoteEpisode.Episodes.Select(v => v.Id).ToList(); // Check if we can associate it with a previous drone factory import. importedItems = importedHistory.Where(v => v.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID) == null && @@ -171,7 +173,7 @@ private void UpdateStatusMessage(TrackedDownload trackedDownload, LogLevel logLe if (trackedDownload.StatusMessage != statusMessage) { - trackedDownload.HasError = logLevel >= LogLevel.Warn; + trackedDownload.SetStatusLevel(logLevel); trackedDownload.StatusMessage = statusMessage; _logger.Log(logLevel, logMessage); } @@ -200,6 +202,9 @@ private void ProcessImportResults(TrackedDownload trackedDownload, List v.Errors.Aggregate(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), (a, r) => a + "\r\n- " + r)) .Aggregate("Failed to import:", (a, r) => a + "\r\n" + r); + trackedDownload.StatusMessages = importResults.Where(v => v.Result == ImportResultType.Skipped || v.Result == ImportResultType.Rejected) + .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors)).ToList(); + UpdateStatusMessage(trackedDownload, LogLevel.Error, errors); } } diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 3a7f6a6ab..b6a118580 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Collections.Generic; using NzbDrone.Common; using NzbDrone.Common.Disk; @@ -21,7 +20,6 @@ public abstract class DownloadClientBase : IDownloadClient { protected readonly IConfigService _configService; protected readonly IDiskProvider _diskProvider; - protected readonly IParsingService _parsingService; protected readonly IRemotePathMappingService _remotePathMappingService; protected readonly Logger _logger; @@ -55,7 +53,6 @@ protected DownloadClientBase(IConfigService configService, IDiskProvider diskPro { _configService = configService; _diskProvider = diskProvider; - _parsingService = parsingService; _remotePathMappingService = remotePathMappingService; _logger = logger; } @@ -76,17 +73,6 @@ public abstract DownloadProtocol Protocol public abstract String RetryDownload(string id); public abstract DownloadClientStatus GetStatus(); - protected RemoteEpisode GetRemoteEpisode(String title) - { - var parsedEpisodeInfo = Parser.Parser.ParseTitle(title); - if (parsedEpisodeInfo == null) return null; - - var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); - if (remoteEpisode.Series == null) return null; - - return remoteEpisode; - } - public ValidationResult Test() { var failures = new List(); diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 3ef9ca2b2..e82cde7bd 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -1,5 +1,4 @@ -using NzbDrone.Core.Parser.Model; -using System; +using System; namespace NzbDrone.Core.Download { @@ -21,7 +20,5 @@ public class DownloadClientItem public DownloadItemStatus Status { get; set; } public Boolean IsEncrypted { get; set; } public Boolean IsReadOnly { get; set; } - - public RemoteEpisode RemoteEpisode { get; set; } } } diff --git a/src/NzbDrone.Core/Download/DownloadItemStatus.cs b/src/NzbDrone.Core/Download/DownloadItemStatus.cs index 6e9f53eb4..6549839d9 100644 --- a/src/NzbDrone.Core/Download/DownloadItemStatus.cs +++ b/src/NzbDrone.Core/Download/DownloadItemStatus.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Download +namespace NzbDrone.Core.Download { public enum DownloadItemStatus { diff --git a/src/NzbDrone.Core/Download/DownloadTrackingService.cs b/src/NzbDrone.Core/Download/DownloadTrackingService.cs index f34e337ee..04e4b4c8e 100644 --- a/src/NzbDrone.Core/Download/DownloadTrackingService.cs +++ b/src/NzbDrone.Core/Download/DownloadTrackingService.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Parser; using NzbDrone.Core.Queue; namespace NzbDrone.Core.Download @@ -22,7 +23,10 @@ public interface IDownloadTrackingService void MarkAsFailed(Int32 historyId); } - public class DownloadTrackingService : IDownloadTrackingService, IExecute, IHandleAsync, IHandle + public class DownloadTrackingService : IDownloadTrackingService, + IExecute, + IHandleAsync, + IHandle { private readonly IProvideDownloadClient _downloadClientProvider; private readonly IHistoryService _historyService; @@ -30,6 +34,7 @@ public class DownloadTrackingService : IDownloadTrackingService, IExecute _trackedDownloadCache; @@ -44,6 +49,7 @@ public DownloadTrackingService(IProvideDownloadClient downloadClientProvider, ICacheManager cacheManager, IFailedDownloadService failedDownloadService, ICompletedDownloadService completedDownloadService, + IParsingService parsingService, Logger logger) { _downloadClientProvider = downloadClientProvider; @@ -52,6 +58,7 @@ public DownloadTrackingService(IProvideDownloadClient downloadClientProvider, _configService = configService; _failedDownloadService = failedDownloadService; _completedDownloadService = completedDownloadService; + _parsingService = parsingService; _logger = logger; _trackedDownloadCache = cacheManager.GetCache(GetType()); @@ -73,7 +80,7 @@ public TrackedDownload[] GetQueuedDownloads() { return _trackedDownloadCache.Get("queued", () => { - UpdateTrackedDownloads(); + UpdateTrackedDownloads(_historyService.Grabbed()); return FilterQueuedDownloads(GetTrackedDownloads()); @@ -119,7 +126,7 @@ private TrackedDownload[] FilterQueuedDownloads(IEnumerable tra .ToList(); } - private Boolean UpdateTrackedDownloads() + private Boolean UpdateTrackedDownloads(List grabbedHistory) { var downloadClients = _downloadClientProvider.GetDownloadClients(); @@ -140,13 +147,9 @@ private Boolean UpdateTrackedDownloads() if (!oldTrackedDownloads.TryGetValue(trackingId, out trackedDownload)) { - trackedDownload = new TrackedDownload - { - TrackingId = trackingId, - DownloadClient = downloadClient.Definition.Id, - StartedTracking = DateTime.UtcNow, - State = TrackedDownloadState.Unknown - }; + trackedDownload = GetTrackedDownload(trackingId, downloadClient.Definition.Id, downloadItem, grabbedHistory); + + if (trackedDownload == null) continue; _logger.Debug("[{0}] Started tracking download with id {1}.", downloadItem.Title, trackingId); stateChanged = true; @@ -182,9 +185,9 @@ private void ProcessTrackedDownloads() var failedHistory = _historyService.Failed(); var importedHistory = _historyService.Imported(); - var stateChanged = UpdateTrackedDownloads(); + var stateChanged = UpdateTrackedDownloads(grabbedHistory); - var downloadClients = _downloadClientProvider.GetDownloadClients(); + var downloadClients = _downloadClientProvider.GetDownloadClients().ToList(); var trackedDownloads = GetTrackedDownloads(); foreach (var trackedDownload in trackedDownloads) @@ -215,6 +218,50 @@ private void ProcessTrackedDownloads() } } + private TrackedDownload GetTrackedDownload(String trackingId, Int32 downloadClient, DownloadClientItem downloadItem, List grabbedHistory) + { + var trackedDownload = new TrackedDownload + { + TrackingId = trackingId, + DownloadClient = downloadClient, + DownloadItem = downloadItem, + StartedTracking = DateTime.UtcNow, + State = TrackedDownloadState.Unknown, + Status = TrackedDownloadStatus.Ok, + }; + + var historyItems = grabbedHistory.Where(h => + { + var downloadClientId = h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID); + + if (downloadClientId == null) return false; + + return downloadClientId.Equals(trackedDownload.DownloadItem.DownloadClientId); + }).ToList(); + + var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title); + if (parsedEpisodeInfo == null) return null; + + var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); + + if (remoteEpisode.Series == null) + { + if (historyItems.Empty()) return null; + + trackedDownload.Status = TrackedDownloadStatus.Warning; + trackedDownload.StatusMessages.Add(new TrackedDownloadStatusMessage( + trackedDownload.DownloadItem.Title, + "Series title mismatch, automatic import is not possible") + ); + + remoteEpisode = _parsingService.Map(parsedEpisodeInfo, historyItems.First().SeriesId, historyItems.Select(h => h.EpisodeId)); + } + + trackedDownload.RemoteEpisode = remoteEpisode; + + return trackedDownload; + } + public void Execute(CheckForFinishedDownloadCommand message) { ProcessTrackedDownloads(); diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index a7c9ddf5f..4ce2914dd 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -3,10 +3,8 @@ using System.Linq; using NLog; using NzbDrone.Common; -using NzbDrone.Common.Cache; using NzbDrone.Core.Configuration; using NzbDrone.Core.History; -using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Download @@ -68,7 +66,7 @@ public void CheckForFailedItem(IDownloadClient downloadClient, TrackedDownload t if (!grabbedItems.Any()) { - UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Download was not grabbed by drone, ignoring download"); + UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download was not grabbed by drone, ignoring download"); return; } @@ -92,7 +90,7 @@ public void CheckForFailedItem(IDownloadClient downloadClient, TrackedDownload t if (!grabbedItems.Any()) { - UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Download wasn't grabbed by drone or not in a category, ignoring download."); + UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download wasn't grabbed by drone or not in a category, ignoring download."); return; } @@ -244,8 +242,12 @@ private void UpdateStatusMessage(TrackedDownload trackedDownload, LogLevel logLe if (trackedDownload.StatusMessage != statusMessage) { - trackedDownload.HasError = logLevel >= LogLevel.Warn; + trackedDownload.SetStatusLevel(logLevel); trackedDownload.StatusMessage = statusMessage; + trackedDownload.StatusMessages = new List + { + new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, statusMessage) + }; _logger.Log(logLevel, logMessage); } else diff --git a/src/NzbDrone.Core/Download/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownload.cs index 215e0d036..c0cad8bb2 100644 --- a/src/NzbDrone.Core/Download/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownload.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using NLog; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download { @@ -9,12 +11,33 @@ public class TrackedDownload public Int32 DownloadClient { get; set; } public DownloadClientItem DownloadItem { get; set; } public TrackedDownloadState State { get; set; } + public TrackedDownloadStatus Status { get; set; } public DateTime StartedTracking { get; set; } public DateTime LastRetry { get; set; } public Int32 RetryCount { get; set; } - public Boolean HasError { get; set; } public String StatusMessage { get; set; } - public List StatusMessages { get; set; } + public RemoteEpisode RemoteEpisode { get; set; } + public List StatusMessages { get; set; } + + public TrackedDownload() + { + StatusMessages = new List(); + } + + public void SetStatusLevel(LogLevel logLevel) + { + if (logLevel == LogLevel.Warn) + { + Status = TrackedDownloadStatus.Warning; + } + + if (logLevel >= LogLevel.Error) + { + Status = TrackedDownloadStatus.Error; + } + + else Status = TrackedDownloadStatus.Ok; + } } public enum TrackedDownloadState @@ -25,4 +48,11 @@ public enum TrackedDownloadState DownloadFailed, Removed } + + public enum TrackedDownloadStatus + { + Ok, + Warning, + Error + } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloadStatusMessage.cs b/src/NzbDrone.Core/Download/TrackedDownloadStatusMessage.cs new file mode 100644 index 000000000..903a2553e --- /dev/null +++ b/src/NzbDrone.Core/Download/TrackedDownloadStatusMessage.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Download +{ + public class TrackedDownloadStatusMessage + { + public String Title { get; set; } + public List Messages { get; set; } + + public TrackedDownloadStatusMessage(String title, List messages) + { + Title = title; + Messages = messages; + } + + public TrackedDownloadStatusMessage(String title, String message) + { + Title = title; + Messages = new List{ message }; + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 15239f89c..c2a58b14c 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -317,6 +317,7 @@ + diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index d62e9e02e..d79fbdc77 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -16,7 +16,8 @@ public interface IParsingService { LocalEpisode GetLocalEpisode(string filename, Series series, bool sceneSource); Series GetSeries(string title); - RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvRageId, SearchCriteriaBase searchCriteria = null); + RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 tvRageId, SearchCriteriaBase searchCriteria = null); + RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 seriesId, IEnumerable episodeIds); List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvRageId, SearchCriteriaBase searchCriteria = null); ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, Series series); @@ -99,7 +100,7 @@ public Series GetSeries(string title) return series; } - public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvRageId, SearchCriteriaBase searchCriteria = null) + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 tvRageId, SearchCriteriaBase searchCriteria = null) { var remoteEpisode = new RemoteEpisode { @@ -120,6 +121,18 @@ public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvRageId, Sear return remoteEpisode; } + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 seriesId, IEnumerable episodeIds) + { + var remoteEpisode = new RemoteEpisode + { + ParsedEpisodeInfo = parsedEpisodeInfo, + Series = _seriesService.GetSeries(seriesId), + Episodes = _episodeService.GetEpisodes(episodeIds) + }; + + return remoteEpisode; + } + public List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null) { var result = new List(); diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index de889939e..837a05af0 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Download; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Parser.Model; @@ -17,7 +19,8 @@ public class Queue : ModelBase public TimeSpan? Timeleft { get; set; } public DateTime? EstimatedCompletionTime { get; set; } public String Status { get; set; } - public String ErrorMessage { get; set; } + public String TrackedDownloadStatus { get; set; } + public List StatusMessages { get; set; } public RemoteEpisode RemoteEpisode { get; set; } } } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 3289bb210..dda598798 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -28,33 +28,30 @@ public List GetQueue() return MapQueue(queueItems); } - private List MapQueue(IEnumerable queueItems) + private List MapQueue(IEnumerable trackedDownloads) { var queued = new List(); - foreach (var queueItem in queueItems) + foreach (var trackedDownload in trackedDownloads) { - foreach (var episode in queueItem.DownloadItem.RemoteEpisode.Episodes) + foreach (var episode in trackedDownload.RemoteEpisode.Episodes) { var queue = new Queue { - Id = episode.Id ^ (queueItem.DownloadItem.DownloadClientId.GetHashCode().GetHashCode() << 16), - Series = queueItem.DownloadItem.RemoteEpisode.Series, + Id = episode.Id ^ (trackedDownload.DownloadItem.DownloadClientId.GetHashCode().GetHashCode() << 16), + Series = trackedDownload.RemoteEpisode.Series, Episode = episode, - Quality = queueItem.DownloadItem.RemoteEpisode.ParsedEpisodeInfo.Quality, - Title = queueItem.DownloadItem.Title, - Size = queueItem.DownloadItem.TotalSize, - Sizeleft = queueItem.DownloadItem.RemainingSize, - Timeleft = queueItem.DownloadItem.RemainingTime, - Status = queueItem.DownloadItem.Status.ToString(), - RemoteEpisode = queueItem.DownloadItem.RemoteEpisode + Quality = trackedDownload.RemoteEpisode.ParsedEpisodeInfo.Quality, + Title = trackedDownload.DownloadItem.Title, + Size = trackedDownload.DownloadItem.TotalSize, + Sizeleft = trackedDownload.DownloadItem.RemainingSize, + Timeleft = trackedDownload.DownloadItem.RemainingTime, + Status = trackedDownload.DownloadItem.Status.ToString(), + RemoteEpisode = trackedDownload.RemoteEpisode, + TrackedDownloadStatus = trackedDownload.Status.ToString(), + StatusMessages = trackedDownload.StatusMessages }; - if (queueItem.HasError) - { - queue.ErrorMessage = queueItem.StatusMessage; - } - if (queue.Timeleft.HasValue) { queue.EstimatedCompletionTime = DateTime.UtcNow.Add(queue.Timeleft.Value); diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 922f25ffc..a9a08cf00 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.Tv public interface IEpisodeService { Episode GetEpisode(int id); + List GetEpisodes(IEnumerable ids); Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber); Episode FindEpisode(int seriesId, int absoluteEpisodeNumber); Episode FindEpisodeByName(int seriesId, int seasonNumber, string episodeTitle); @@ -37,9 +38,9 @@ public interface IEpisodeService } public class EpisodeService : IEpisodeService, - IHandle, - IHandle, - IHandleAsync + IHandle, + IHandle, + IHandleAsync { private readonly IEpisodeRepository _episodeRepository; private readonly IConfigService _configService; @@ -57,6 +58,11 @@ public Episode GetEpisode(int id) return _episodeRepository.Get(id); } + public List GetEpisodes(IEnumerable ids) + { + return _episodeRepository.Get(ids).ToList(); + } + public Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber) { return _episodeRepository.Find(seriesId, seasonNumber, episodeNumber); diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index 20bd93f99..79e8dd76e 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -204,3 +204,7 @@ .icon-nd-deleted:before { .icon(@trash); } + +.icon-nd-warning:before { + color: @brand-warning; +} diff --git a/src/UI/History/Queue/QueueStatusCell.js b/src/UI/History/Queue/QueueStatusCell.js index c4214b1f4..439c3c267 100644 --- a/src/UI/History/Queue/QueueStatusCell.js +++ b/src/UI/History/Queue/QueueStatusCell.js @@ -2,20 +2,26 @@ define( [ + 'marionette', 'Cells/NzbDroneCell' - ], function (NzbDroneCell) { + ], function (Marionette, NzbDroneCell) { return NzbDroneCell.extend({ - className: 'queue-status-cell', + className : 'queue-status-cell', + template : 'History/Queue/QueueStatusCellTemplate', render: function () { this.$el.empty(); if (this.cellValue) { var status = this.cellValue.get('status').toLowerCase(); - var errorMessage = (this.cellValue.get('errorMessage') || ''); + var trackedDownloadStatus = this.cellValue.get('trackedDownloadStatus').toLowerCase(); + var hasError = this.cellValue.get('hasError') || false; + var hasWarning = this.cellValue.get('hasWarning') || false; var icon = 'icon-nd-downloading'; var title = 'Downloading'; + var itemTitle = this.cellValue.get('title'); + var content = itemTitle; if (status === 'paused') { icon = 'icon-pause'; @@ -39,7 +45,7 @@ define( if (status === 'failed') { icon = 'icon-nd-download-failed'; - title = 'Download failed: check download client for more details'; + title = 'Download failed'; } if (status === 'warning') { @@ -47,29 +53,37 @@ define( title = 'Download warning: check download client for more details'; } - if (errorMessage !== '') { + if (trackedDownloadStatus === 'warning') { + icon += ' icon-nd-warning'; +// title = 'Download failed'; + + this.templateFunction = Marionette.TemplateCache.get(this.template); + content = this.templateFunction(this.cellValue.toJSON()); + } + + if (trackedDownloadStatus === 'error') { if (status === 'completed') { icon = 'icon-nd-import-failed'; - title = 'Import failed'; + title = 'Import failed: ' + itemTitle; } else { icon = 'icon-nd-download-failed'; title = 'Download failed'; } - this.$el.html(''.format(icon)); - - this.$el.popover({ - content : errorMessage.replace(new RegExp('\r\n', 'g'), '
'), - html : true, - trigger : 'hover', - title : title, - placement: 'right', - container: this.$el - }); - } - else { - this.$el.html(''.format(icon, title)); + + this.templateFunction = Marionette.TemplateCache.get(this.template); + content = this.templateFunction(this.cellValue.toJSON()); } + + this.$el.html(''.format(icon)); + this.$el.popover({ + content : content, + html : true, + trigger : 'hover', + title : title, + placement: 'right', + container: this.$el + }); } return this; diff --git a/src/UI/History/Queue/QueueStatusCellTemplate.hbs b/src/UI/History/Queue/QueueStatusCellTemplate.hbs new file mode 100644 index 000000000..9ef1ced00 --- /dev/null +++ b/src/UI/History/Queue/QueueStatusCellTemplate.hbs @@ -0,0 +1,8 @@ +{{#each statusMessages}} + {{title}} +
    + {{#each messages}} +
  • {{this}}
  • + {{/each}} +
+{{/each}} \ No newline at end of file