mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-18 21:34:28 -04:00
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:
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+65
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user