Fixed: Don't mark a download as successful unless all valid files are imported

This commit is contained in:
Mark McDowall 2014-08-21 17:57:23 -07:00
parent 059028da02
commit 2651cd63a8
11 changed files with 155 additions and 91 deletions

View File

@ -9,7 +9,6 @@ using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
@ -104,9 +103,9 @@ namespace NzbDrone.Core.Test.Download
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportDecision>()
.Returns(new List<ImportResult>
{
new ImportDecision(null)
new ImportResult(null)
});
}
@ -114,9 +113,9 @@ namespace NzbDrone.Core.Test.Download
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportDecision>()
.Returns(new List<ImportResult>()
{
new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }, "Test Failure")
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }, "Test Failure"))
});
}
@ -448,5 +447,37 @@ namespace NzbDrone.Core.Test.Download
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Once());
}
[Test]
public void should_not_mark_as_successful_if_no_files_were_imported()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(new LocalEpisode() {Path = @"C:\TestPath\Droned.S01E01.mkv"}),
"Test Failure")
});
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never());
ExceptionVerification.ExpectedErrors(1);
}
}
}

View File

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles;
@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Returns(new List<ImportDecision>());
.Returns(new List<ImportResult>());
}
private void GivenValidSeries()
@ -125,7 +125,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false, null))
.Returns(new List<ImportDecision>());
.Returns(new List<ImportResult>());
Subject.Execute(new DownloadedEpisodesScanCommand());
@ -149,7 +149,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Returns(imported);
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Subject.Execute(new DownloadedEpisodesScanCommand());
@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Returns(imported);
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Mocker.GetMock<ISampleService>()
.Setup(s => s.IsSample(It.IsAny<Series>(),

View File

@ -65,9 +65,11 @@ namespace NzbDrone.Core.Test.MediaFiles
}
[Test]
public void should_return_empty_list_if_there_are_no_approved_decisions()
public void should_not_import_any_if_there_are_no_approved_decisions()
{
Subject.Import(_rejectedDecisions, false).Should().BeEmpty();
Subject.Import(_rejectedDecisions, false).Where(i => i.Result == ImportResultType.Imported).Should().BeEmpty();
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.IsAny<EpisodeFile>()), Times.Never());
}
[Test]
@ -83,7 +85,10 @@ namespace NzbDrone.Core.Test.MediaFiles
all.AddRange(_rejectedDecisions);
all.AddRange(_approvedDecisions);
Subject.Import(all, false).Should().HaveCount(5);
var result = Subject.Import(all, false);
result.Should().HaveCount(all.Count);
result.Where(i => i.Result == ImportResultType.Imported).Should().HaveCount(_approvedDecisions.Count);
}
[Test]
@ -93,7 +98,9 @@ namespace NzbDrone.Core.Test.MediaFiles
all.AddRange(_approvedDecisions);
all.Add(new ImportDecision(_approvedDecisions.First().LocalEpisode));
Subject.Import(all, false).Should().HaveCount(5);
var result = Subject.Import(all, false);
result.Where(i => i.Result == ImportResultType.Imported).Should().HaveCount(_approvedDecisions.Count);
}
[Test]
@ -136,7 +143,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{
Series = fileDecision.LocalEpisode.Series,
Episodes = new List<Episode> {fileDecision.LocalEpisode.Episodes.First()},
Path = @"C:\Test\TV\30 Rock\30 Rock - S01E01 - Pilit.avi".AsOsAgnostic(),
Path = @"C:\Test\TV\30 Rock\30 Rock - S01E01 - Pilot.avi".AsOsAgnostic(),
Quality = new QualityModel(Quality.Bluray720p),
Size = 80.Megabytes()
});
@ -148,8 +155,9 @@ namespace NzbDrone.Core.Test.MediaFiles
var results = Subject.Import(all, false);
results.Should().HaveCount(1);
results.Should().ContainSingle(d => d.LocalEpisode.Size == fileDecision.LocalEpisode.Size);
results.Should().HaveCount(all.Count);
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported);
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported && d.ImportDecision.LocalEpisode.Size == fileDecision.LocalEpisode.Size);
}
}
}

View File

@ -3,19 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using System.IO;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Download
{
@ -97,51 +91,15 @@ namespace NzbDrone.Core.Download
if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath))
{
var decisions = _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem);
var importResults = _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem);
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);
}
ProcessImportResults(trackedDownload, importResults);
}
else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath))
{
var decisions = _downloadedEpisodesImportService.ProcessFile(new FileInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem);
var importResults = _downloadedEpisodesImportService.ProcessFile(new FileInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem);
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);
}
ProcessImportResults(trackedDownload, importResults);
}
else
{
@ -222,5 +180,28 @@ namespace NzbDrone.Core.Download
_logger.Debug(logMessage);
}
}
private void ProcessImportResults(TrackedDownload trackedDownload, List<ImportResult> importResults)
{
if (importResults.Empty())
{
UpdateStatusMessage(trackedDownload, LogLevel.Error, "No files found are eligible for import in {0}", trackedDownload.DownloadItem.OutputPath);
}
else if (importResults.All(v => v.Result == ImportResultType.Imported || v.Result == ImportResultType.Rejected))
{
UpdateStatusMessage(trackedDownload, LogLevel.Info, "Imported {0} files.", importResults.Count(v => v.Result == ImportResultType.Imported));
trackedDownload.State = TrackedDownloadState.Imported;
}
else
{
var errors = importResults
.Where(v => v.Result == ImportResultType.Skipped || v.Result == ImportResultType.Rejected)
.Select(v => 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);
UpdateStatusMessage(trackedDownload, LogLevel.Error, errors);
}
}
}
}

View File

@ -14,6 +14,7 @@ namespace NzbDrone.Core.Download
public Int32 RetryCount { get; set; }
public Boolean HasError { get; set; }
public String StatusMessage { get; set; }
public List<String> StatusMessages { get; set; }
}
public enum TrackedDownloadState

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using NLog;
@ -12,7 +11,6 @@ using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Download;
@ -20,8 +18,8 @@ namespace NzbDrone.Core.MediaFiles
{
public interface IDownloadedEpisodesImportService
{
List<ImportDecision> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem);
List<ImportDecision> ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem);
List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem);
List<ImportResult> ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem);
}
public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService, IExecute<DownloadedEpisodesScanCommand>
@ -57,7 +55,7 @@ namespace NzbDrone.Core.MediaFiles
_logger = logger;
}
public List<ImportDecision> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem)
public List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem)
{
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var series = _parsingService.GetSeries(cleanedUpName);
@ -67,39 +65,35 @@ namespace NzbDrone.Core.MediaFiles
if (series == null)
{
_logger.Debug("Unknown Series {0}", cleanedUpName);
return new List<ImportDecision>();
return new List<ImportResult>();
}
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality);
var importedDecisions = _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
if (!downloadClientItem.IsReadOnly && importedDecisions.Any() && ShouldDeleteFolder(directoryInfo))
if (!downloadClientItem.IsReadOnly && importResults.Any() && ShouldDeleteFolder(directoryInfo))
{
_logger.Debug("Deleting folder after importing valid files");
_diskProvider.DeleteFolder(directoryInfo.FullName, true);
}
return importedDecisions.Union(decisions).ToList();
return importResults;
}
public List<ImportDecision> ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem)
public List<ImportResult> ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem)
{
var series = _parsingService.GetSeries(Path.GetFileNameWithoutExtension(fileInfo.Name));
if (series == null)
{
_logger.Debug("Unknown Series for file: {0}", fileInfo.Name);
return new List<ImportDecision>();
return new List<ImportResult>();
}
var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { fileInfo.FullName }, series, true, null);
var importedDecisions = _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
return importedDecisions.Union(decisions).ToList();
var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { fileInfo.FullName }, series, true);
return _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
}
private void ProcessDownloadedEpisodesFolder()
@ -136,7 +130,7 @@ namespace NzbDrone.Core.MediaFiles
}
}
private List<ImportDecision> ProcessFolder(DirectoryInfo directoryInfo)
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo)
{
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var series = _parsingService.GetSeries(cleanedUpName);
@ -146,7 +140,7 @@ namespace NzbDrone.Core.MediaFiles
if (series == null)
{
_logger.Debug("Unknown Series {0}", cleanedUpName);
return new List<ImportDecision>();
return new List<ImportResult>();
}
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
@ -156,7 +150,7 @@ namespace NzbDrone.Core.MediaFiles
if (_diskProvider.IsFileLocked(videoFile))
{
_logger.Debug("[{0}] is currently locked by another process, skipping", videoFile);
return new List<ImportDecision>();
return new List<ImportResult>();
}
}

View File

@ -8,7 +8,6 @@ using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Download;
@ -16,7 +15,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public interface IImportApprovedEpisodes
{
List<ImportDecision> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem historyItem = null);
List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem historyItem = null);
}
public class ImportApprovedEpisodes : IImportApprovedEpisodes
@ -40,7 +39,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
_logger = logger;
}
public List<ImportDecision> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem historyItem = null)
public List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem historyItem = null)
{
var qualifiedImports = decisions.Where(c => c.Approved)
.GroupBy(c => c.LocalEpisode.Series.Id, (i, s) => s
@ -49,7 +48,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
.SelectMany(c => c)
.ToList();
var imported = new List<ImportDecision>();
var importResults = new List<ImportResult>();
foreach (var importDecision in qualifiedImports.OrderByDescending(e => e.LocalEpisode.Size))
{
@ -59,11 +58,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
try
{
//check if already imported
if (imported.SelectMany(r => r.LocalEpisode.Episodes)
if (importResults.SelectMany(r => r.ImportDecision.LocalEpisode.Episodes)
.Select(e => e.Id)
.Intersect(localEpisode.Episodes.Select(e => e.Id))
.Any())
{
importResults.Add(new ImportResult(importDecision, "Episode has already been imported"));
continue;
}
@ -92,7 +92,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
}
_mediaFileService.Add(episodeFile);
imported.Add(importDecision);
importResults.Add(new ImportResult(importDecision));
if (historyItem != null)
{
@ -111,10 +111,15 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
catch (Exception e)
{
_logger.WarnException("Couldn't import episode " + localEpisode, e);
importResults.Add(new ImportResult(importDecision, "Failed to import episode"));
}
}
return imported;
//Adding all the rejected decisions
importResults.AddRange(decisions.Where(c => !c.Approved)
.Select(d => new ImportResult(d, d.Rejections.ToArray())));
return importResults;
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public class ImportResult
{
public ImportDecision ImportDecision { get; private set; }
public List<String> Errors { get; private set; }
public ImportResultType Result
{
get
{
//Approved and imported
if (Errors.Empty()) return ImportResultType.Imported;
//Decision was approved, but it was not imported
if (ImportDecision.Approved) return ImportResultType.Skipped;
//Decision was rejected
return ImportResultType.Rejected;
}
}
public ImportResult(ImportDecision importDecision, params String[] errors)
{
ImportDecision = importDecision;
Errors = errors.ToList();
}
}
}

View File

@ -0,0 +1,9 @@
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public enum ImportResultType
{
Imported,
Rejected,
Skipped
}
}

View File

@ -468,10 +468,12 @@
<Compile Include="MediaFiles\EpisodeFile.cs" />
<Compile Include="MediaFiles\EpisodeFileMoveResult.cs" />
<Compile Include="MediaFiles\EpisodeFileMovingService.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportResult.cs" />
<Compile Include="MediaFiles\EpisodeImport\IImportDecisionEngineSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportApprovedEpisodes.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportDecision.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportResultType.cs" />
<Compile Include="MediaFiles\EpisodeImport\SampleService.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />

View File

@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Collections.Generic;
using NLog;
using NzbDrone.Core.Download;
namespace NzbDrone.Core.Queue