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
This commit is contained in:
Mark McDowall
2019-10-24 21:42:41 +01:00
committed by ta264
parent a70e7e4778
commit 94ac75c6b7
40 changed files with 1108 additions and 497 deletions
@@ -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,
@@ -4,6 +4,5 @@ namespace NzbDrone.Core.Download
{
public class CheckForFinishedDownloadCommand : Command
{
public override bool RequiresDiskAccess => true;
}
}
@@ -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,75 +48,113 @@ 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())
var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId);
if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace())
{
trackedDownload.Warn("Download wasn't grabbed by Lidarr and not in a category, Skipping.");
return;
}
var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath;
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("Download wasn't grabbed by Lidarr and not in a category, Skipping.");
return;
artist = _artistService.GetArtist(historyItem.ArtistId);
}
var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath;
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)
{
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);
if (importResults.Empty())
{
trackedDownload.State = TrackedDownloadStage.ImportFailed;
trackedDownload.State = TrackedDownloadState.ImportFailed;
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
_eventAggregator.PublishEvent(new AlbumImportIncompleteEvent(trackedDownload));
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))
@@ -132,6 +162,7 @@ namespace NzbDrone.Core.Download
trackedDownload.Warn(statusMessages);
_eventAggregator.PublishEvent(new AlbumImportIncompleteEvent(trackedDownload));
return;
}
}
}
@@ -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<ProcessMonitoredDownloadsCommand>
{
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<TrackedDownload> 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());
}
}
}
@@ -0,0 +1,11 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Download
{
public class DownloadsProcessedEvent : IEvent
{
public DownloadsProcessedEvent()
{
}
}
}
@@ -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)
// Only process tracked downloads that are still downloading
if (trackedDownload.State != TrackedDownloadState.Downloading)
{
failure = "Encrypted download detected";
}
else if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed)
{
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,11 +74,41 @@ 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<History.History> historyItems, string message, TrackedDownload trackedDownload = null, bool skipReDownload = false)
{
var historyItem = historyItems.First();
@@ -0,0 +1,9 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Download
{
public class ProcessMonitoredDownloadsCommand : Command
{
public override bool RequiresDiskAccess => true;
}
}
@@ -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<DownloadFailedEvent>
public class RedownloadFailedDownloadService : IHandle<DownloadFailedEvent>
{
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)
{
@@ -0,0 +1,8 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Download
{
public class RefreshMonitoredDownloadsCommand : Command
{
}
}
@@ -11,9 +11,10 @@ using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Download.TrackedDownloads
{
public class DownloadMonitoringService : IExecute<CheckForFinishedDownloadCommand>,
public class DownloadMonitoringService : IExecute<RefreshMonitoredDownloadsCommand>,
IHandle<AlbumGrabbedEvent>,
IHandle<TrackImportedEvent>,
IHandle<DownloadsProcessedEvent>,
IHandle<TrackedDownloadsRemovedEvent>
{
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<TrackedDownload> ProcessClientDownloads(IDownloadClient downloadClient)
{
List<DownloadClientItem> downloadClientHistory = new List<DownloadClientItem>();
var downloadClientItems = new List<DownloadClientItem>();
var trackedDownloads = new List<TrackedDownload>();
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<TrackedDownload> 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<TrackedDownload> ProcessClientItems(IDownloadClient downloadClient, DownloadClientItem downloadItem)
{
var trackedDownloads = new List<TrackedDownload>();
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();
@@ -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
}
}
@@ -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<History.History> historyItems);
}
public class TrackedDownloadAlreadyImported : ITrackedDownloadAlreadyImported
{
public bool IsImported(TrackedDownload trackedDownload, List<History.History> 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;
}
}
}
@@ -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<TrackedDownload> _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<TrackedDownload>(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<List<TrackedDownloadStatusMessage>>(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)
+1 -1
View File
@@ -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(ApplicationUpdateCommand).FullName },
new ScheduledTask { Interval = 6 * 60, TypeName = typeof(CheckHealthCommand).FullName },
@@ -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));
}
}
@@ -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<LocalAlbumRelease>
{
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();
}
}
}
@@ -21,16 +21,12 @@ namespace NzbDrone.Core.Messaging.Events
private class EventSubscribers<TEvent>
where TEvent : class, IEvent
{
private IServiceFactory _serviceFactory;
public IHandle<TEvent>[] _syncHandlers;
public IHandleAsync<TEvent>[] _asyncHandlers;
public IHandleAsync<IEvent>[] _globalHandlers;
public EventSubscribers(IServiceFactory serviceFactory)
{
_serviceFactory = serviceFactory;
_syncHandlers = serviceFactory.BuildAll<IHandle<TEvent>>()
.OrderBy(GetEventHandleOrder)
.ToArray();
@@ -144,8 +140,7 @@ namespace NzbDrone.Core.Messaging.Events
internal static int GetEventHandleOrder<TEvent>(IHandle<TEvent> 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)
{
+2 -1
View File
@@ -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<TrackedDownloadStatusMessage> StatusMessages { get; set; }
public string DownloadId { get; set; }
public RemoteAlbum RemoteAlbum { get; set; }
+2 -1
View File
@@ -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,