diff --git a/gulp/gulpfile.js b/gulp/gulpFile.js similarity index 100% rename from gulp/gulpfile.js rename to gulp/gulpFile.js diff --git a/src/NzbDrone.Api/Queue/QueueActionModule.cs b/src/NzbDrone.Api/Queue/QueueActionModule.cs index a02f2724e..c3b94bbda 100644 --- a/src/NzbDrone.Api/Queue/QueueActionModule.cs +++ b/src/NzbDrone.Api/Queue/QueueActionModule.cs @@ -72,7 +72,7 @@ private JsonResponse Import() var resource = Request.Body.FromJson(); var trackedDownload = GetTrackedDownload(resource.Id); - _completedDownloadService.Process(trackedDownload); + _completedDownloadService.Process(trackedDownload, true); return resource.AsResponse(); } diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs index 6bd561f75..17a3bb655 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -65,18 +66,24 @@ private void GivenNoGrabbedHistory() .Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId)) .Returns((History.History)null); } - - + private void GivenSuccessfulImport() { Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" })) }); } + private void GivenSeriesMatch() + { + Mocker.GetMock() + .Setup(s => s.GetSeries(It.IsAny())) + .Returns(_trackedDownload.RemoteEpisode.Series); + } + [TestCase(DownloadItemStatus.Downloading)] [TestCase(DownloadItemStatus.Failed)] [TestCase(DownloadItemStatus.Queued)] @@ -107,8 +114,9 @@ public void should_process_if_matching_history_is_not_found_but_category_specifi { _trackedDownload.DownloadItem.Category = "tv"; GivenNoGrabbedHistory(); + GivenSeriesMatch(); GivenSuccessfulImport(); - + Subject.Process(_trackedDownload); AssertCompletedDownload(); @@ -142,7 +150,7 @@ public void should_not_process_if_output_path_is_empty() public void should_not_mark_as_imported_if_all_files_were_rejected() { Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}, "Rejected!"),"Test Failure"), @@ -161,7 +169,7 @@ public void should_not_mark_as_imported_if_all_files_were_rejected() public void should_not_mark_as_imported_if_all_files_were_skipped() { Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}),"Test Failure"), @@ -177,6 +185,7 @@ public void should_not_mark_as_imported_if_all_files_were_skipped() [Test] public void should_mark_as_imported_if_all_episodes_were_imported_but_extra_files_were_not() { + GivenSeriesMatch(); _trackedDownload.RemoteEpisode.Episodes = new List { @@ -184,14 +193,13 @@ public void should_mark_as_imported_if_all_episodes_were_imported_but_extra_file }; Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})), new ImportResult(new ImportDecision(new LocalEpisode{Path = @"C:\TestPath\Droned.S01E01.mkv"}),"Test Failure") }); - Subject.Process(_trackedDownload); AssertCompletedDownload(); @@ -208,7 +216,7 @@ public void should_mark_as_failed_if_some_of_episodes_were_not_imported() }; Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})), @@ -222,11 +230,38 @@ public void should_mark_as_failed_if_some_of_episodes_were_not_imported() AssertNoCompletedDownload(); } + [Test] + public void should_not_import_when_there_is_a_title_mismatch() + { + Subject.Process(_trackedDownload); + + AssertNoCompletedDownload(); + } + + [Test] + public void should_mark_as_import_title_mismatch_if_ignore_warnings_is_true() + { + _trackedDownload.RemoteEpisode.Episodes = new List + { + new Episode() + }; + + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new List + { + new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})) + }); + + Subject.Process(_trackedDownload, true); + + AssertCompletedDownload(); + } private void AssertNoAttemptedImport() { Mocker.GetMock() - .Verify(v => v.ProcessPath(It.IsAny(), It.IsAny()), Times.Never()); + .Verify(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); AssertNoCompletedDownload(); } @@ -242,7 +277,7 @@ private void AssertNoCompletedDownload() private void AssertCompletedDownload() { Mocker.GetMock() - .Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, _trackedDownload.DownloadItem), Times.Once()); + .Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once()); _trackedDownload.State.Should().Be(TrackedDownloadStage.Imported); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesCommandServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesCommandServiceFixture.cs index e56a430f4..244cdd92e 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesCommandServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesCommandServiceFixture.cs @@ -19,7 +19,6 @@ public class DownloadedEpisodesCommandServiceFixture : CoreTest() .Setup(v => v.ProcessRootFolder(It.IsAny())) .Returns(new List()); - - Mocker.GetMock() - .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) - .Returns(new List()); - } [Test] @@ -48,7 +42,7 @@ public void should_process_dronefactory_if_path_is_not_specified() } [Test] - public void should_skip_import_if_dropfolder_doesnt_exist() + public void should_skip_import_if_dronefactory_doesnt_exist() { Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())).Returns(false); @@ -57,7 +51,6 @@ public void should_skip_import_if_dropfolder_doesnt_exist() Mocker.GetMock().Verify(c => c.ProcessRootFolder(It.IsAny()), Times.Never()); ExceptionVerification.ExpectedWarns(1); - } - + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index 61f9a3955..f32cb519a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -199,9 +199,12 @@ public void should_remove_unpack_from_folder_name(string prefix) [Test] public void should_return_importresult_on_unknown_series() { + Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) + .Returns(false); + var fileName = @"C:\folder\file.mkv".AsOsAgnostic(); - var result = Subject.ProcessFile(new FileInfo(fileName)); + var result = Subject.ProcessPath(fileName); result.Should().HaveCount(1); result.First().ImportDecision.Should().NotBeNull(); diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 316b5dac2..09cb3e6e4 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -9,12 +9,13 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; namespace NzbDrone.Core.Download { public interface ICompletedDownloadService { - void Process(TrackedDownload trackedDownload); + void Process(TrackedDownload trackedDownload, bool ignoreWarnings = false); } public class CompletedDownloadService : ICompletedDownloadService @@ -23,46 +24,64 @@ public class CompletedDownloadService : ICompletedDownloadService private readonly IEventAggregator _eventAggregator; private readonly IHistoryService _historyService; private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly IParsingService _parsingService; + private readonly Logger _logger; public CompletedDownloadService(IConfigService configService, IEventAggregator eventAggregator, IHistoryService historyService, - IDownloadedEpisodesImportService downloadedEpisodesImportService) + IDownloadedEpisodesImportService downloadedEpisodesImportService, + IParsingService parsingService, + Logger logger) { _configService = configService; _eventAggregator = eventAggregator; _historyService = historyService; _downloadedEpisodesImportService = downloadedEpisodesImportService; + _parsingService = parsingService; + _logger = logger; } - public void Process(TrackedDownload trackedDownload) + public void Process(TrackedDownload trackedDownload, bool ignoreWarnings = false) { if (trackedDownload.DownloadItem.Status != DownloadItemStatus.Completed) { return; } - var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId); - - if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) + if (!ignoreWarnings) { - trackedDownload.Warn("Download wasn't grabbed by Sonarr and not in a category, Skipping."); - return; - } + var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId); - var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath; + if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) + { + trackedDownload.Warn("Download wasn't grabbed by Sonarr and not in a category, Skipping."); + return; + } - if (downloadItemOutputPath.IsEmpty) - { - trackedDownload.Warn("Download doesn't contain intermediate path, Skipping."); - return; - } + var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath; - var downloadedEpisodesFolder = new OsPath(_configService.DownloadedEpisodesFolder); - if (downloadedEpisodesFolder.Contains(downloadItemOutputPath)) - { - trackedDownload.Warn("Intermediate Download path inside drone factory, Skipping."); - return; + if (downloadItemOutputPath.IsEmpty) + { + trackedDownload.Warn("Download doesn't contain intermediate path, Skipping."); + return; + } + + var downloadedEpisodesFolder = new OsPath(_configService.DownloadedEpisodesFolder); + + if (downloadedEpisodesFolder.Contains(downloadItemOutputPath)) + { + trackedDownload.Warn("Intermediate Download path inside drone factory, Skipping."); + return; + } + + var series = _parsingService.GetSeries(trackedDownload.DownloadItem.Title); + + if (series == null) + { + trackedDownload.Warn("Series title mismatch, automatic import is not possible."); + return; + } } Import(trackedDownload); @@ -71,7 +90,7 @@ public void Process(TrackedDownload trackedDownload) private void Import(TrackedDownload trackedDownload) { var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath; - var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, trackedDownload.DownloadItem); + var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem); if (importResults.Empty()) { diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs index b11ff9e4b..f359c1f3b 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs @@ -78,7 +78,7 @@ private List ProcessClientDownloads(IDownloadClient downloadCli trackedDownloads.AddRange(newItems); } - if (_configService.RemoveCompletedDownloads) + if (_configService.EnableCompletedDownloadHandling && _configService.RemoveCompletedDownloads) { RemoveCompletedDownloads(trackedDownloads); } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index e3c3ac547..d158b69a2 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -1,6 +1,8 @@ using System; +using System.Linq; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; using NzbDrone.Core.History; using NzbDrone.Core.Parser; @@ -59,9 +61,17 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do if (parsedEpisodeInfo == null) return null; var remoteEpisode = _parsingService.Map(parsedEpisodeInfo); + if (remoteEpisode.Series == null) { - return null; + var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId); + + if (historyItems.Empty()) + { + return null; + } + + remoteEpisode = _parsingService.Map(parsedEpisodeInfo, historyItems.First().SeriesId, historyItems.Select(h => h.EpisodeId)); } trackedDownload.RemoteEpisode = remoteEpisode; @@ -73,6 +83,7 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do } var historyItem = _historyService.MostRecentForDownloadId(downloadItem.DownloadId); + if (historyItem != null) { trackedDownload.State = GetStateFromHistory(historyItem.EventType); diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index e550964be..ba2e64705 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -22,6 +22,7 @@ public interface IHistoryService History MostRecentForDownloadId(string downloadId); History Get(int historyId); List Find(string downloadId, HistoryEventType eventType); + List FindByDownloadId(string downloadId); } public class HistoryService : IHistoryService, @@ -64,6 +65,10 @@ public List Find(string downloadId, HistoryEventType eventType) return _historyRepository.FindByDownloadId(downloadId).Where(c => c.EventType == eventType).ToList(); } + public List FindByDownloadId(string downloadId) + { + return _historyRepository.FindByDownloadId(downloadId); + } public QualityModel GetBestQualityInHistory(Profile profile, int episodeId) { diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index e1c9376a4..4730e5020 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -15,8 +15,7 @@ namespace NzbDrone.Core.MediaFiles public interface IDownloadedEpisodesImportService { List ProcessRootFolder(DirectoryInfo directoryInfo); - List ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null); - List ProcessPath(string path, DownloadClientItem downloadClientItem = null); + List ProcessPath(string path, Series series = null, DownloadClientItem downloadClientItem = null); } public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService @@ -31,13 +30,13 @@ public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService private readonly Logger _logger; public DownloadedEpisodesImportService(IDiskProvider diskProvider, - IDiskScanService diskScanService, - ISeriesService seriesService, - IParsingService parsingService, - IMakeImportDecision importDecisionMaker, - IImportApprovedEpisodes importApprovedEpisodes, - ISampleService sampleService, - Logger logger) + IDiskScanService diskScanService, + ISeriesService seriesService, + IParsingService parsingService, + IMakeImportDecision importDecisionMaker, + IImportApprovedEpisodes importApprovedEpisodes, + ISampleService sampleService, + Logger logger) { _diskProvider = diskProvider; _diskScanService = diskScanService; @@ -68,7 +67,27 @@ public List ProcessRootFolder(DirectoryInfo directoryInfo) return results; } - public List ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null) + public List ProcessPath(string path, Series series = null, DownloadClientItem downloadClientItem = null) + { + if (_diskProvider.FolderExists(path)) + { + if (series == null) + { + return ProcessFolder(new DirectoryInfo(path), downloadClientItem); + } + + return ProcessFolder(new DirectoryInfo(path), series, downloadClientItem); + } + + if (series == null) + { + return ProcessFile(new FileInfo(path), downloadClientItem); + } + + return ProcessFile(new FileInfo(path), series, downloadClientItem); + } + + private List ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null) { var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); var series = _parsingService.GetSeries(cleanedUpName); @@ -87,7 +106,7 @@ public List ProcessFolder(DirectoryInfo directoryInfo, DownloadCli } private List ProcessFolder(DirectoryInfo directoryInfo, Series series, - DownloadClientItem downloadClientItem = null) + DownloadClientItem downloadClientItem = null) { if (_seriesService.SeriesPathExists(directoryInfo.FullName)) { @@ -128,7 +147,7 @@ private List ProcessFolder(DirectoryInfo directoryInfo, Series ser return importResults; } - public List ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem = null) + private List ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem = null) { var series = _parsingService.GetSeries(Path.GetFileNameWithoutExtension(fileInfo.Name)); @@ -162,16 +181,6 @@ private List ProcessFile(FileInfo fileInfo, Series series, Downloa return _importApprovedEpisodes.Import(decisions, true, downloadClientItem); } - public List ProcessPath(string path, DownloadClientItem downloadClientItem = null) - { - if (_diskProvider.FolderExists(path)) - { - return ProcessFolder(new DirectoryInfo(path), downloadClientItem); - } - - return ProcessFile(new FileInfo(path), downloadClientItem); - } - private string GetCleanedUpFolderName(string folder) { folder = folder.Replace("_UNPACK_", "") diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 0e446845e..46e80a118 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -16,6 +16,7 @@ public interface IParsingService LocalEpisode GetLocalEpisode(string filename, Series series, bool sceneSource); Series GetSeries(string title); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 tvRageId = 0, SearchCriteriaBase searchCriteria = null); + RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 seriesId, IEnumerable episodeIds); List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvRageId, SearchCriteriaBase searchCriteria = null); } @@ -118,6 +119,16 @@ public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 tvRageId, Se return remoteEpisode; } + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable episodeIds) + { + return new RemoteEpisode + { + ParsedEpisodeInfo = parsedEpisodeInfo, + Series = _seriesService.GetSeries(seriesId), + Episodes = _episodeService.GetEpisodes(episodeIds) + }; + } + public List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null) { var result = new List();