New: The History->Queue UI now show some elementary error information for failed imports.

pull/92/head
Taloth Saldono 10 years ago
parent 981ffb09fc
commit 47089d360d

@ -37,6 +37,7 @@ module.exports = function (grunt) {
'Content/theme.less',
'Content/overrides.less',
'Series/series.less',
'History/history.less',
'AddSeries/addSeries.less',
'Calendar/calendar.less',
'Cells/cells.less',

@ -17,5 +17,6 @@ namespace NzbDrone.Api.Queue
public Decimal Sizeleft { get; set; }
public TimeSpan? Timeleft { get; set; }
public String Status { get; set; }
public String ErrorMessage { get; set; }
}
}

@ -12,6 +12,7 @@ using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Test.Common;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Parser.Model;
@ -103,14 +104,20 @@ namespace NzbDrone.Core.Test.Download
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<Core.MediaFiles.EpisodeImport.ImportDecision>() { new Core.MediaFiles.EpisodeImport.ImportDecision(null) });
.Returns(new List<ImportDecision>()
{
new ImportDecision(null)
});
}
private void GivenFailedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<Core.MediaFiles.EpisodeImport.ImportDecision>());
.Returns(new List<ImportDecision>()
{
new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }, "Test Failure")
});
}
private void VerifyNoImports()
@ -265,6 +272,8 @@ namespace NzbDrone.Core.Test.Download
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoImports();
ExceptionVerification.IgnoreErrors();
}
[Test]
@ -289,6 +298,8 @@ namespace NzbDrone.Core.Test.Download
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoImports();
ExceptionVerification.IgnoreWarns();
}
[Test]
@ -412,6 +423,8 @@ namespace NzbDrone.Core.Test.Download
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never());
ExceptionVerification.IgnoreErrors();
}
[Test]

@ -67,7 +67,7 @@ namespace NzbDrone.Core.Download
if (!grabbedItems.Any() && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace())
{
_logger.Trace("Ignoring download that wasn't grabbed by drone: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Download wasn't grabbed by drone or not in a category, ignoring download.");
return;
}
@ -77,7 +77,7 @@ namespace NzbDrone.Core.Download
{
trackedDownload.State = TrackedDownloadState.Imported;
_logger.Trace("Already added to history as imported: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Already added to history as imported.");
}
else
{
@ -85,13 +85,13 @@ namespace NzbDrone.Core.Download
string downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath;
if (downloadItemOutputPath.IsNullOrWhiteSpace())
{
_logger.Trace("Storage path not specified: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download doesn't contain intermediate path, ignoring download.");
return;
}
if (!downloadedEpisodesFolder.IsNullOrWhiteSpace() && (downloadedEpisodesFolder.PathEquals(downloadItemOutputPath) || downloadedEpisodesFolder.IsParentPath(downloadItemOutputPath)))
{
_logger.Trace("Storage path inside drone factory, ignoring download: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Intermediate Download path inside drone factory, ignoring download.");
return;
}
@ -99,19 +99,49 @@ namespace NzbDrone.Core.Download
{
var decisions = _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem);
if (decisions.Any())
if (!decisions.Any())
{
UpdateStatusMessage(trackedDownload, LogLevel.Error, "No files found eligible for import in {0}", trackedDownload.DownloadItem.OutputPath);
}
else if (decisions.Any(v => v.Approved))
{
UpdateStatusMessage(trackedDownload, LogLevel.Info, "Imported {0} files.", decisions.Count(v => v.Approved));
trackedDownload.State = TrackedDownloadState.Imported;
}
else
{
var rejections = decisions
.Where(v => !v.Approved)
.Select(v => v.Rejections.Aggregate(Path.GetFileName(v.LocalEpisode.Path), (a, r) => a + "\r\n- " + r))
.Aggregate("Failed to import:", (a, r) => a + "\r\n" + r);
UpdateStatusMessage(trackedDownload, LogLevel.Error, rejections);
}
}
else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath))
{
var decisions = _downloadedEpisodesImportService.ProcessFile(new FileInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem);
if (decisions.Any())
if (!decisions.Any())
{
UpdateStatusMessage(trackedDownload, LogLevel.Error, "No files found eligible for import in {0}", trackedDownload.DownloadItem.OutputPath);
}
else if (decisions.Any(v => v.Approved))
{
UpdateStatusMessage(trackedDownload, LogLevel.Info, "Imported {0} files.", decisions.Count(v => v.Approved));
trackedDownload.State = TrackedDownloadState.Imported;
}
else
{
var rejections = decisions
.Where(v => !v.Approved)
.Select(v => v.Rejections.Aggregate(Path.GetFileName(v.LocalEpisode.Path), (a, r) => a + "\r\n- " + r))
.Aggregate("Failed to import:", (a, r) => a + "\r\n" + r);
UpdateStatusMessage(trackedDownload, LogLevel.Error, rejections);
}
}
else
{
@ -137,13 +167,13 @@ namespace NzbDrone.Core.Download
importedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT_ID] = grabbedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT_ID];
_historyService.UpdateHistoryData(importedItems.First().Id, importedItems.First().Data);
_logger.Trace("Storage path does not exist, but found probable drone factory ImportEvent: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Intermediate Download path does not exist, but found probable drone factory ImportEvent.");
return;
}
}
}
_logger.Trace("Storage path does not exist: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(trackedDownload, LogLevel.Error, "Intermediate Download path does not exist: {0}", trackedDownload.DownloadItem.OutputPath);
return;
}
}
@ -153,17 +183,17 @@ namespace NzbDrone.Core.Download
{
try
{
_logger.Info("Removing completed download from history: {0}", trackedDownload.DownloadItem.Title);
_logger.Debug("[{0}] Removing completed download from history.", trackedDownload.DownloadItem.Title);
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId);
if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath))
{
_logger.Info("Removing completed download directory: {0}", trackedDownload.DownloadItem.OutputPath);
_logger.Debug("Removing completed download directory: {0}", trackedDownload.DownloadItem.OutputPath);
_diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath, true);
}
else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath))
{
_logger.Info("Removing completed download file: {0}", trackedDownload.DownloadItem.OutputPath);
_logger.Debug("Removing completed download file: {0}", trackedDownload.DownloadItem.OutputPath);
_diskProvider.DeleteFile(trackedDownload.DownloadItem.OutputPath);
}
@ -171,9 +201,26 @@ namespace NzbDrone.Core.Download
}
catch (NotSupportedException)
{
_logger.Debug("Removing item not supported by your download client");
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Removing item not supported by your download client.");
}
}
}
private void UpdateStatusMessage(TrackedDownload trackedDownload, LogLevel logLevel, String message, params object[] args)
{
var statusMessage = String.Format(message, args);
var logMessage = String.Format("[{0}] {1}", trackedDownload.DownloadItem.Title, statusMessage);
if (trackedDownload.StatusMessage != statusMessage)
{
trackedDownload.HasError = logLevel >= LogLevel.Warn;
trackedDownload.StatusMessage = statusMessage;
_logger.Log(logLevel, logMessage);
}
else
{
_logger.Debug(logMessage);
}
}
}
}

@ -20,7 +20,7 @@ namespace NzbDrone.Core.Download
TrackedDownload[] GetQueuedDownloads();
}
public class DownloadTrackingService : IDownloadTrackingService, IExecute<CheckForFinishedDownloadCommand>, IHandle<ApplicationStartedEvent>, IHandle<EpisodeGrabbedEvent>
public class DownloadTrackingService : IDownloadTrackingService, IExecute<CheckForFinishedDownloadCommand>, IHandleAsync<ApplicationStartedEvent>, IHandle<EpisodeGrabbedEvent>
{
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly IHistoryService _historyService;
@ -201,7 +201,7 @@ namespace NzbDrone.Core.Download
ProcessTrackedDownloads();
}
public void Handle(ApplicationStartedEvent message)
public void HandleAsync(ApplicationStartedEvent message)
{
ProcessTrackedDownloads();
}

@ -54,7 +54,7 @@ namespace NzbDrone.Core.Download
if (!grabbedItems.Any())
{
_logger.Trace("Download was not grabbed by drone, ignoring download: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(LogLevel.Debug, trackedDownload, "Download was not grabbed by drone, ignoring download");
return;
}
@ -64,7 +64,7 @@ namespace NzbDrone.Core.Download
if (failedItems.Any())
{
_logger.Trace("Already added to history as failed: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(LogLevel.Debug, trackedDownload, "Already added to history as failed");
}
else
{
@ -78,7 +78,7 @@ namespace NzbDrone.Core.Download
if (!grabbedItems.Any())
{
_logger.Trace("Download was not grabbed by drone, ignoring download: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(LogLevel.Debug, trackedDownload, "Download wasn't grabbed by drone or not in a category, ignoring download.");
return;
}
@ -86,13 +86,13 @@ namespace NzbDrone.Core.Download
if (trackedDownload.DownloadItem.Message.Equals("Unpacking failed, write error or disk is full?",
StringComparison.InvariantCultureIgnoreCase))
{
_logger.Trace("Failed due to lack of disk space, do not blacklist: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(LogLevel.Error, trackedDownload, "Download failed due to lack of disk space, not blacklisting.");
return;
}
if (FailedDownloadForRecentRelease(downloadClient, trackedDownload, grabbedItems))
{
_logger.Trace("Recent release Failed, do not blacklist: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(LogLevel.Debug, trackedDownload, "Recent release Failed, do not blacklist.");
return;
}
@ -102,7 +102,7 @@ namespace NzbDrone.Core.Download
if (failedItems.Any())
{
_logger.Trace("Already added to history as failed: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(LogLevel.Debug, trackedDownload, "Already added to history as failed.");
}
else
{
@ -117,7 +117,7 @@ namespace NzbDrone.Core.Download
if (grabbedItems.Any() && failedItems.Any())
{
_logger.Trace("Already added to history as failed, updating tracked state: " + trackedDownload.DownloadItem.Title);
UpdateStatusMessage(LogLevel.Debug, trackedDownload, "Already added to history as failed, updating tracked state.");
trackedDownload.State = TrackedDownloadState.DownloadFailed;
}
}
@ -126,14 +126,14 @@ namespace NzbDrone.Core.Download
{
try
{
_logger.Info("Removing failed download from client: {0}", trackedDownload.DownloadItem.Title);
_logger.Debug("[{0}] Removing failed download from client", trackedDownload.DownloadItem.Title);
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId);
trackedDownload.State = TrackedDownloadState.Removed;
}
catch (NotSupportedException)
{
_logger.Trace("Removing item not supported by your download client");
UpdateStatusMessage(LogLevel.Debug, trackedDownload, "Removing item not supported by your download client.");
}
}
}
@ -144,25 +144,25 @@ namespace NzbDrone.Core.Download
if (!Double.TryParse(matchingHistoryItems.First().Data.GetValueOrDefault("ageHours"), out ageHours))
{
_logger.Debug("Unable to determine age of failed download: " + trackedDownload.DownloadItem.Title);
_logger.Info("[{0}] Unable to determine age of failed download.", trackedDownload.DownloadItem.Title);
return false;
}
if (ageHours > _configService.BlacklistGracePeriod)
{
_logger.Debug("Failed download is older than the grace period: " + trackedDownload.DownloadItem.Title);
_logger.Info("[{0}] Failed download is older than the grace period.", trackedDownload.DownloadItem.Title);
return false;
}
if (trackedDownload.RetryCount >= _configService.BlacklistRetryLimit)
{
_logger.Debug("Retry limit reached: " + trackedDownload.DownloadItem.Title);
_logger.Info("[{0}] Retry limit reached.", trackedDownload.DownloadItem.Title);
return false;
}
if (trackedDownload.RetryCount == 0 || trackedDownload.LastRetry.AddMinutes(_configService.BlacklistRetryInterval) < DateTime.UtcNow)
{
_logger.Debug("Retrying failed release: " + trackedDownload.DownloadItem.Title);
_logger.Info("[{0}] Retrying failed release.", trackedDownload.DownloadItem.Title);
trackedDownload.LastRetry = DateTime.UtcNow;
trackedDownload.RetryCount++;
@ -205,5 +205,22 @@ namespace NzbDrone.Core.Download
_eventAggregator.PublishEvent(downloadFailedEvent);
}
private void UpdateStatusMessage(LogLevel logLevel, TrackedDownload trackedDownload, String message, params object[] args)
{
var statusMessage = String.Format(message, args);
var logMessage = String.Format("[{0}] {1}", trackedDownload.DownloadItem.Title, message);
if (trackedDownload.StatusMessage != statusMessage)
{
trackedDownload.HasError = logLevel >= LogLevel.Warn;
trackedDownload.StatusMessage = statusMessage;
_logger.Log(logLevel, statusMessage);
}
else
{
_logger.Debug(logMessage);
}
}
}
}

@ -12,6 +12,8 @@ namespace NzbDrone.Core.Download
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 enum TrackedDownloadState

@ -82,7 +82,7 @@ namespace NzbDrone.Core.MediaFiles
_diskProvider.DeleteFolder(directoryInfo.FullName, true);
}
return importedDecisions;
return importedDecisions.Union(decisions).ToList();
}
public List<ImportDecision> ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem)
@ -99,7 +99,7 @@ namespace NzbDrone.Core.MediaFiles
var importedDecisions = _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
return importedDecisions;
return importedDecisions.Union(decisions).ToList();
}
private void ProcessDownloadedEpisodesFolder()

@ -16,6 +16,7 @@ namespace NzbDrone.Core.Queue
public Decimal Sizeleft { get; set; }
public TimeSpan? Timeleft { get; set; }
public String Status { get; set; }
public String ErrorMessage { get; set; }
public RemoteEpisode RemoteEpisode { get; set; }
}
}

@ -25,32 +25,37 @@ namespace NzbDrone.Core.Queue
public List<Queue> GetQueue()
{
var queueItems = _downloadTrackingService.GetQueuedDownloads()
.Select(v => v.DownloadItem)
.OrderBy(v => v.RemainingTime)
.OrderBy(v => v.DownloadItem.RemainingTime)
.ToList();
return MapQueue(queueItems);
}
private List<Queue> MapQueue(IEnumerable<DownloadClientItem> queueItems)
private List<Queue> MapQueue(IEnumerable<TrackedDownload> queueItems)
{
var queued = new List<Queue>();
foreach (var queueItem in queueItems)
{
foreach (var episode in queueItem.RemoteEpisode.Episodes)
foreach (var episode in queueItem.DownloadItem.RemoteEpisode.Episodes)
{
var queue = new Queue();
queue.Id = queueItem.DownloadClientId.GetHashCode() + episode.Id;
queue.Series = queueItem.RemoteEpisode.Series;
queue.Id = queueItem.DownloadItem.DownloadClientId.GetHashCode() + episode.Id;
queue.Series = queueItem.DownloadItem.RemoteEpisode.Series;
queue.Episode = episode;
queue.Quality = queueItem.RemoteEpisode.ParsedEpisodeInfo.Quality;
queue.Title = queueItem.Title;
queue.Size = queueItem.TotalSize;
queue.Sizeleft = queueItem.RemainingSize;
queue.Timeleft = queueItem.RemainingTime;
queue.Status = queueItem.Status.ToString();
queue.RemoteEpisode = queueItem.RemoteEpisode;
queue.Quality = queueItem.DownloadItem.RemoteEpisode.ParsedEpisodeInfo.Quality;
queue.Title = queueItem.DownloadItem.Title;
queue.Size = queueItem.DownloadItem.TotalSize;
queue.Sizeleft = queueItem.DownloadItem.RemainingSize;
queue.Timeleft = queueItem.DownloadItem.RemainingTime;
queue.Status = queueItem.DownloadItem.Status.ToString();
queue.RemoteEpisode = queueItem.DownloadItem.RemoteEpisode;
if (queueItem.HasError)
{
queue.ErrorMessage = queueItem.StatusMessage;
}
queued.Add(queue);
}
}

@ -163,6 +163,11 @@
color : purple;
}
.icon-nd-import-failed:before {
.icon(@download-alt);
color: @brand-danger;
}
.icon-nd-download-failed:before {
.icon(@cloud-download);
color: @brand-danger;

@ -13,6 +13,7 @@ define(
if (this.cellValue) {
var status = this.cellValue.get('status').toLowerCase();
var errorMessage = (this.cellValue.get('errorMessage') || '');
var icon = 'icon-nd-downloading';
var title = 'Downloading';
@ -31,7 +32,29 @@ define(
title = 'Downloaded';
}
this.$el.html('<i class="{0}" title="{1}"></i>'.format(icon, title));
if (errorMessage !== '') {
if (status === 'completed') {
icon = 'icon-nd-import-failed';
title = "Import failed";
}
else {
icon = 'icon-nd-download-failed';
title = "Download failed";
}
this.$el.html('<i class="{0}"></i>'.format(icon));
this.$el.popover({
content : errorMessage.replace(new RegExp('\r\n', 'g'), '<br/>'),
html : true,
trigger : 'hover',
title : title,
placement: 'right',
container: this.$el
});
}
else {
this.$el.html('<i class="{0}" title="{1}"></i>'.format(icon, title));
}
}
return this;

@ -22,9 +22,8 @@ define(
this.$el.html("-");
}
else {
this.$el.html(timeleft);
this.$el.html('<span title="{1} / {2}">{0}</span>'.format(timeleft, remainingSize, totalSize));
}
this.$el.attr('title', '{0} / {1}'.format(remainingSize, totalSize));
}
return this;

@ -0,0 +1,4 @@
.queue-status-cell .popover {
max-width: 800px;
}

@ -11,8 +11,7 @@ define(
render: function () {
var date = Moment(this._getValue());
this.$el.html(date.format('LT'));
this.$el.attr('title', date.format('LLLL'));
this.$el.html('<span title="{1}">{0}</span>'.format(date.format('LT'), date.format('LLLL')));
return this;
}

@ -12,6 +12,7 @@
<link href="/Content/theme.css" rel='stylesheet' type='text/css'/>
<link href="/Cells/cells.css" rel='stylesheet' type='text/css'>
<link href="/Series/series.css" rel='stylesheet' type='text/css'/>
<link href="/History/history.css" rel='stylesheet' type='text/css'/>
<link href="/System/Logs/logs.css" rel='stylesheet' type='text/css'/>
<link href="/Settings/settings.css" rel='stylesheet' type='text/css'/>
<link href="/AddSeries/addSeries.css" rel='stylesheet' type='text/css'/>

Loading…
Cancel
Save