From 20782bbbc122ef835034d87e3d750f4f5ea7d730 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 26 Jan 2015 21:57:07 -0800 Subject: [PATCH] Episode import improvements Fixed: Use folder name when file name is not parsable on import --- .../Extensions}/ObjectExtensions.cs | 2 +- src/NzbDrone.Common/NzbDrone.Common.csproj | 1 + .../CompletedDownloadServiceFixture.cs | 10 +- .../PendingReleaseServiceTests/AddFixture.cs | 2 +- .../RemoveGrabbedFixture.cs | 3 +- .../RemoveRejectedFixture.cs | 2 +- .../DiskScanServiceTests/ScanFixture.cs | 8 +- .../DownloadedEpisodesImportServiceFixture.cs | 12 +- .../ImportDecisionMakerFixture.cs | 198 ++++++++++++++---- .../EpisodeImport/SampleServiceFixture.cs | 2 +- .../FreeSpaceSpecificationFixture.cs | 18 +- .../FullSeasonSpecificationFixture.cs | 4 +- .../MatchesFolderSpecificationFixture.cs | 84 ++++++++ .../NotSampleSpecificationFixture.cs | 2 +- .../NotUnpackingSpecificationFixture.cs | 8 +- .../UpgradeSpecificationFixture.cs | 14 +- .../ImportApprovedEpisodesFixture.cs | 7 +- .../NzbDrone.Core.Test.csproj | 1 + .../ParserTests/CrapParserFixture.cs | 7 + .../TvTests/RefreshEpisodeServiceFixture.cs | 2 +- .../TvTests/RefreshSeriesServiceFixture.cs | 2 +- .../MediaFiles/DiskScanService.cs | 2 +- .../DownloadedEpisodesImportService.cs | 26 ++- .../{SampleService.cs => DetectSample.cs} | 6 +- .../IImportDecisionEngineSpecification.cs | 4 +- .../EpisodeImport/ImportApprovedEpisodes.cs | 2 +- .../EpisodeImport/ImportDecision.cs | 5 +- .../EpisodeImport/ImportDecisionMaker.cs | 164 ++++++++++----- .../MediaFiles/EpisodeImport/ImportResult.cs | 7 +- .../Specifications/FreeSpaceSpecification.cs | 15 +- .../Specifications/FullSeasonSpecification.cs | 9 +- .../MatchesFolderSpecification.cs | 54 +++++ .../Specifications/NotSampleSpecification.cs | 30 +-- .../NotUnpackingSpecification.cs | 13 +- .../Specifications/UpgradeSpecification.cs | 9 +- .../Metadata/ExistingMetadataService.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 +- src/NzbDrone.Core/Parser/Parser.cs | 2 +- src/NzbDrone.Core/Parser/ParsingService.cs | 23 +- src/NzbDrone.Core/Parser/SceneChecker.cs | 16 +- .../NzbDrone.Test.Common.csproj | 1 - 41 files changed, 562 insertions(+), 220 deletions(-) rename src/{NzbDrone.Test.Common => NzbDrone.Common/Extensions}/ObjectExtensions.cs (87%) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs rename src/NzbDrone.Core/MediaFiles/EpisodeImport/{SampleService.cs => DetectSample.cs} (95%) create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs diff --git a/src/NzbDrone.Test.Common/ObjectExtensions.cs b/src/NzbDrone.Common/Extensions/ObjectExtensions.cs similarity index 87% rename from src/NzbDrone.Test.Common/ObjectExtensions.cs rename to src/NzbDrone.Common/Extensions/ObjectExtensions.cs index 702797269..98f694090 100644 --- a/src/NzbDrone.Test.Common/ObjectExtensions.cs +++ b/src/NzbDrone.Common/Extensions/ObjectExtensions.cs @@ -1,6 +1,6 @@ using NzbDrone.Common.Serializer; -namespace NzbDrone.Test.Common +namespace NzbDrone.Common.Extensions { public static class ObjectExtensions { diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index d7966efc0..8db2ba470 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -134,6 +134,7 @@ + diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs index 17a3bb655..9bd22d81e 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs @@ -5,6 +5,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.History; @@ -153,8 +154,13 @@ namespace NzbDrone.Core.Test.Download .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"), - new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"}, "Rejected!"),"Test Failure") + new ImportResult( + new ImportDecision( + new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}, new Rejection("Rejected!")), "Test Failure"), + + new ImportResult( + new ImportDecision( + new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"},new Rejection("Rejected!")), "Test Failure") }); Subject.Process(_trackedDownload); diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs index 6dac469ab..6ec615982 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs @@ -4,6 +4,7 @@ using FizzWare.NBuilder; using Marr.Data; using Moq; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Parser; @@ -12,7 +13,6 @@ using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; -using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests { diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs index 89776300a..323394532 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs @@ -4,17 +4,16 @@ using FizzWare.NBuilder; using Marr.Data; using Moq; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; -using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; -using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests { diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs index 36e520a2d..9d9aac486 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs @@ -4,6 +4,7 @@ using FizzWare.NBuilder; using Marr.Data; using Moq; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; @@ -14,7 +15,6 @@ using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; -using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests { diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs index 8982e441d..4e3a41f30 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs @@ -101,7 +101,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(_series); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _series, false, (QualityModel)null), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _series), Times.Once()); } [Test] @@ -119,7 +119,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(_series); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _series, false, (QualityModel)null), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _series), Times.Once()); } [Test] @@ -141,7 +141,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(_series); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 4), _series, false, (QualityModel)null), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 4), _series), Times.Once()); } [Test] @@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(_series); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _series, false, (QualityModel)null), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _series), Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index f32cb519a..89f00416c 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); Mocker.GetMock() - .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), + .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); VerifyNoImport(); @@ -129,7 +129,7 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), true, null)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null, true)) .Returns(imported); Mocker.GetMock() @@ -155,14 +155,14 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), true, null)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null, true)) .Returns(imported); Mocker.GetMock() .Setup(s => s.Import(It.IsAny>(), true, null)) .Returns(imported.Select(i => new ImportResult(i)).ToList()); - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.IsSample(It.IsAny(), It.IsAny(), It.IsAny(), @@ -224,14 +224,14 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), true, null)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null, true)) .Returns(imported); Mocker.GetMock() .Setup(s => s.Import(It.IsAny>(), true, null)) .Returns(imported.Select(i => new ImportResult(i)).ToList()); - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.IsSample(It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index 6f887ad37..e5e3e0985 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -45,31 +45,20 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _fail2 = new Mock(); _fail3 = new Mock(); - _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(true); - _pass1.Setup(c => c.RejectionReason).Returns("_pass1"); + _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(Decision.Accept()); + _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(Decision.Accept()); + _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(Decision.Accept()); - _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(true); - _pass2.Setup(c => c.RejectionReason).Returns("_pass2"); + _fail1.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(Decision.Reject("_fail1")); + _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(Decision.Reject("_fail2")); + _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(Decision.Reject("_fail3")); - _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(true); - _pass3.Setup(c => c.RejectionReason).Returns("_pass3"); - - - _fail1.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(false); - _fail1.Setup(c => c.RejectionReason).Returns("_fail1"); - - _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(false); - _fail2.Setup(c => c.RejectionReason).Returns("_fail2"); - - _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny())).Returns(false); - _fail3.Setup(c => c.RejectionReason).Returns("_fail3"); - - _videoFiles = new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" }; _series = Builder.CreateNew() .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _quality = new QualityModel(Quality.DVD); + _localEpisode = new LocalEpisode { Series = _series, @@ -78,17 +67,24 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport }; Mocker.GetMock() - .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_localEpisode); - Mocker.GetMock() - .Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny())) - .Returns(_videoFiles); + GivenVideoFiles(new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() }); } private void GivenSpecifications(params Mock[] mocks) { - Mocker.SetConstant>(mocks.Select(c => c.Object)); + Mocker.SetConstant(mocks.Select(c => c.Object)); + } + + private void GivenVideoFiles(IEnumerable videoFiles) + { + _videoFiles = videoFiles.ToList(); + + Mocker.GetMock() + .Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny())) + .Returns(_videoFiles); } [Test] @@ -96,7 +92,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); - Subject.GetImportDecisions(_videoFiles, new Series(), false); + Subject.GetImportDecisions(_videoFiles, new Series(), null, false); _fail1.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once()); _fail2.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once()); @@ -111,7 +107,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { GivenSpecifications(_fail1); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false); + var result = Subject.GetImportDecisions(_videoFiles, new Series()); result.Single().Approved.Should().BeFalse(); } @@ -121,7 +117,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { GivenSpecifications(_pass1, _fail1, _pass2, _pass3); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false); + var result = Subject.GetImportDecisions(_videoFiles, new Series()); result.Single().Approved.Should().BeFalse(); } @@ -131,7 +127,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { GivenSpecifications(_pass1, _pass2, _pass3); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false); + var result = Subject.GetImportDecisions(_videoFiles, new Series()); result.Single().Approved.Should().BeTrue(); } @@ -141,7 +137,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false); + var result = Subject.GetImportDecisions(_videoFiles, new Series()); result.Single().Rejections.Should().HaveCount(3); } @@ -151,7 +147,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1); Mocker.GetMock() - .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Throws(); _videoFiles = new List @@ -161,14 +157,12 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport "The.Office.S03E115.DVDRip.XviD-OSiTV" }; - Mocker.GetMock() - .Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny())) - .Returns(_videoFiles); + GivenVideoFiles(_videoFiles); - Subject.GetImportDecisions(_videoFiles, _series, false); + Subject.GetImportDecisions(_videoFiles, _series); Mocker.GetMock() - .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); ExceptionVerification.ExpectedErrors(3); } @@ -179,7 +173,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1, _pass2, _pass3); var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - var result = Subject.GetImportDecisions(_videoFiles, _series, false, null); + var result = Subject.GetImportDecisions(_videoFiles, _series); result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); } @@ -190,7 +184,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1, _pass2, _pass3); var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - var result = Subject.GetImportDecisions(_videoFiles, _series, false, new QualityModel(Quality.SDTV)); + var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo{Quality = new QualityModel(Quality.SDTV)}, true); result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); } @@ -201,7 +195,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1, _pass2, _pass3); var expectedQuality = new QualityModel(Quality.Bluray1080p); - var result = Subject.GetImportDecisions(_videoFiles, _series, false, expectedQuality); + var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo { Quality = expectedQuality }, true); result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); } @@ -212,7 +206,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1); Mocker.GetMock() - .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new EpisodeNotFoundException("Episode not found")); _videoFiles = new List @@ -222,14 +216,130 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport "The.Office.S03E115.DVDRip.XviD-OSiTV" }; - Mocker.GetMock() - .Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny())) - .Returns(_videoFiles); + GivenVideoFiles(_videoFiles); - Subject.GetImportDecisions(_videoFiles, _series, false); + Subject.GetImportDecisions(_videoFiles, _series); Mocker.GetMock() - .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); + } + + [Test] + public void should_not_use_folder_for_full_season() + { + var videoFiles = new[] + { + @"C:\Test\Unsorted\Series.Title.S01\S01E01.mkv".AsOsAgnostic(), + @"C:\Test\Unsorted\Series.Title.S01\S01E02.mkv".AsOsAgnostic(), + @"C:\Test\Unsorted\Series.Title.S01\S01E03.mkv".AsOsAgnostic() + }; + + GivenSpecifications(_pass1); + GivenVideoFiles(videoFiles); + + var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01"); + + Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), null, true), Times.Exactly(3)); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.Is(p => p != null), true), Times.Never()); + } + + [Test] + public void should_not_use_folder_when_it_contains_more_than_one_valid_video_file() + { + var videoFiles = new[] + { + @"C:\Test\Unsorted\Series.Title.S01E01\S01E01.mkv".AsOsAgnostic(), + @"C:\Test\Unsorted\Series.Title.S01E01\1x01.mkv".AsOsAgnostic() + }; + + GivenSpecifications(_pass1); + GivenVideoFiles(videoFiles); + + var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); + + Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), null, true), Times.Exactly(2)); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.Is(p => p != null), true), Times.Never()); + } + + [Test] + public void should_use_folder_when_only_one_video_file() + { + var videoFiles = new[] + { + @"C:\Test\Unsorted\Series.Title.S01E01\S01E01.mkv".AsOsAgnostic() + }; + + GivenSpecifications(_pass1); + GivenVideoFiles(videoFiles); + + var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); + + Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), true), Times.Exactly(1)); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), null, true), Times.Never()); + } + + [Test] + public void should_use_folder_when_only_one_video_file_and_a_sample() + { + var videoFiles = new[] + { + @"C:\Test\Unsorted\Series.Title.S01E01\S01E01.mkv".AsOsAgnostic(), + @"C:\Test\Unsorted\Series.Title.S01E01\S01E01.sample.mkv".AsOsAgnostic() + }; + + GivenSpecifications(_pass1); + GivenVideoFiles(videoFiles.ToList()); + + Mocker.GetMock() + .Setup(s => s.IsSample(_series, It.IsAny(), It.Is(c => c.Contains("sample")), It.IsAny(), It.IsAny())) + .Returns(true); + + var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); + + Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), true), Times.Exactly(2)); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), null, true), Times.Never()); + } + + [Test] + public void should_not_use_folder_name_if_file_name_is_scene_name() + { + var videoFiles = new[] + { + @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-LOL\Series.Title.S01E01.720p.HDTV-LOL.mkv".AsOsAgnostic() + }; + + GivenSpecifications(_pass1); + GivenVideoFiles(videoFiles); + + var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01.720p.HDTV-LOL"); + + Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), null, true), Times.Exactly(1)); + + Mocker.GetMock() + .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.Is(p => p != null), true), Times.Never()); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs index dbb860d8e..813ef747b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs @@ -14,7 +14,7 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { [TestFixture] - public class SampleServiceFixture : CoreTest + public class SampleServiceFixture : CoreTest { private Series _series; private LocalEpisode _localEpisode; diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecificationFixture.cs index 007a52976..5cdaa8590 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecificationFixture.cs @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenFileSize(100.Megabytes()); GivenFreeSpace(80.Megabytes()); - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); ExceptionVerification.ExpectedWarns(1); } @@ -74,7 +74,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenFileSize(100.Megabytes()); GivenFreeSpace(150.Megabytes()); - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); ExceptionVerification.ExpectedWarns(1); } @@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenFileSize(100.Megabytes()); GivenFreeSpace(1.Gigabytes()); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } [Test] @@ -93,7 +93,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenFileSize(100.Megabytes()); GivenFreeSpace(1.Gigabytes()); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); Mocker.GetMock() .Verify(v => v.GetAvailableSpace(_rootFolder), Times.Once()); @@ -105,7 +105,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenFileSize(100.Megabytes()); GivenFreeSpace(null); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } [Test] @@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Setup(s => s.GetAvailableSpace(It.IsAny())) .Throws(new TestException()); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); ExceptionVerification.ExpectedErrors(1); } @@ -126,7 +126,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications { _localEpisode.ExistingFile = true; - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); Mocker.GetMock() .Verify(s => s.GetAvailableSpace(It.IsAny()), Times.Never()); @@ -141,7 +141,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Setup(s => s.GetAvailableSpace(It.IsAny())) .Returns(freeSpace); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } [Test] @@ -151,7 +151,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Setup(s => s.SkipFreeSpaceCheckWhenImporting) .Returns(true); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecificationFixture.cs index cb657d47e..d8dced788 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecificationFixture.cs @@ -34,13 +34,13 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications { _localEpisode.ParsedEpisodeInfo.FullSeason = true; - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); } [Test] public void should_return_true_when_file_does_not_contain_the_full_season() { - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs new file mode 100644 index 000000000..71ff631a1 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs @@ -0,0 +1,84 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications +{ + [TestFixture] + public class MatchesFolderSpecificationFixture : CoreTest + { + private LocalEpisode _localEpisode; + + [SetUp] + public void Setup() + { + _localEpisode = Builder.CreateNew() + .With(l => l.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E05.mkv".AsOsAgnostic()) + .With(l => l.ParsedEpisodeInfo = + Builder.CreateNew() + .With(p => p.EpisodeNumbers = new[] {5}) + .With(p => p.FullSeason = false) + .Build()) + .Build(); + } + + [Test] + public void should_be_accepted_for_existing_file() + { + _localEpisode.ExistingFile = true; + + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_accepted_if_folder_name_is_not_parseable() + { + _localEpisode.Path = @"C:\Test\Unsorted\Series.Title\S01E01.mkv".AsOsAgnostic(); + + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); + } + + [Test] + public void should_should_be_accepted_for_full_season() + { + _localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01\S01E01.mkv".AsOsAgnostic(); + + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_accepted_if_file_and_folder_have_the_same_episode() + { + _localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 }; + _localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_accepted_if_file_is_one_episode_in_folder() + { + _localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 }; + _localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_rejected_if_file_and_folder_do_not_have_same_episode() + { + _localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E05.mkv".AsOsAgnostic(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); + } + + [Test] + public void should_be_rejected_if_file_and_folder_do_not_have_same_episodes() + { + _localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 5, 6 }; + _localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E05E06.mkv".AsOsAgnostic(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs index 3c4594c52..1f3492205 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications public void should_return_true_for_existing_file() { _localEpisode.ExistingFile = true; - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs index a0968314b..ad27e402f 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications [Test] public void should_return_true_if_not_in_working_folder() { - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } [Test] @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenInWorkingFolder(); GivenLastWriteTimeUtc(DateTime.UtcNow.AddHours(-1)); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } [Test] @@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenInWorkingFolder(); GivenLastWriteTimeUtc(DateTime.UtcNow); - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); } [Test] @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenInWorkingFolder(); GivenLastWriteTimeUtc(DateTime.UtcNow.AddDays(-5)); - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs index bfbab5472..f55cdcce2 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Build() .ToList(); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } [Test] @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Build() .ToList(); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } [Test] @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Build() .ToList(); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } [Test] @@ -92,7 +92,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Build() .ToList(); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue(); } [Test] @@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Build() .ToList(); - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); } [Test] @@ -126,7 +126,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Build() .ToList(); - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); } [Test] @@ -150,7 +150,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Build() .ToList(); - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse(); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 2961a257a..e51b8280a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -6,6 +6,7 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; @@ -44,9 +45,9 @@ namespace NzbDrone.Core.Test.MediaFiles - _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), "Rejected!")); - _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), "Rejected!")); - _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), "Rejected!")); + _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); + _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); + _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); foreach (var episode in episodes) { diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 9bf60de39..a2df9a896 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -227,6 +227,7 @@ + diff --git a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs index 98b74c6ff..ee32d1fc3 100644 --- a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("08bbc153931ce3ca5fcafe1b92d3297285feb061.mkv")] [TestCase("185d86a343e39f3341e35c4dad3ff159")] [TestCase("ah63jka93jf0jh26ahjas961.mkv")] + [TestCase("qrdSD3rYzWb7cPdVIGSn4E7")] public void should_not_parse_crap(string title) { Parser.Parser.ParseTitle(title).Should().BeNull(); @@ -82,5 +83,11 @@ namespace NzbDrone.Core.Test.ParserTests success.Should().Be(repetitions); } + + [TestCase("thebiggestloser1618finale")] + public void should_not_parse_file_name_without_proper_spacing(string fileName) + { + Parser.Parser.ParseTitle(fileName).Should().BeNull(); + } } } diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index 053b56839..392f6344f 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs @@ -5,10 +5,10 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.TvTests { diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs index 200aa1faf..d847c2ce7 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs @@ -4,11 +4,11 @@ using System.Linq; using FizzWare.NBuilder; using Moq; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Commands; -using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.TvTests { diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index f11591d54..e4c54c2a0 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -103,7 +103,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Trace("Finished getting episode files for: {0} [{1}]", series, videoFilesStopwatch.Elapsed); var decisionsStopwatch = Stopwatch.StartNew(); - var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series, false); + var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series); decisionsStopwatch.Stop(); _logger.Trace("Import decisions complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed); diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 4730e5020..73de1ad9e 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Parser; using NzbDrone.Core.Tv; @@ -26,7 +27,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IParsingService _parsingService; private readonly IMakeImportDecision _importDecisionMaker; private readonly IImportApprovedEpisodes _importApprovedEpisodes; - private readonly ISampleService _sampleService; + private readonly IDetectSample _detectSample; private readonly Logger _logger; public DownloadedEpisodesImportService(IDiskProvider diskProvider, @@ -35,7 +36,7 @@ namespace NzbDrone.Core.MediaFiles IParsingService parsingService, IMakeImportDecision importDecisionMaker, IImportApprovedEpisodes importApprovedEpisodes, - ISampleService sampleService, + IDetectSample detectSample, Logger logger) { _diskProvider = diskProvider; @@ -44,7 +45,7 @@ namespace NzbDrone.Core.MediaFiles _parsingService = parsingService; _importDecisionMaker = importDecisionMaker; _importApprovedEpisodes = importApprovedEpisodes; - _sampleService = sampleService; + _detectSample = detectSample; _logger = logger; } @@ -115,9 +116,12 @@ namespace NzbDrone.Core.MediaFiles } var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); - var quality = QualityParser.ParseQuality(cleanedUpName); + var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); - _logger.Debug("{0} folder quality: {1}", cleanedUpName, quality); + if (folderInfo != null) + { + _logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality); + } var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); @@ -135,7 +139,7 @@ namespace NzbDrone.Core.MediaFiles } } - var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality); + var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, folderInfo, true); var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem); if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) && importResults.Any() && ShouldDeleteFolder(directoryInfo, series)) @@ -177,7 +181,9 @@ namespace NzbDrone.Core.MediaFiles } } - var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, series, true); + var folderInfo = Parser.Parser.ParseTitle(fileInfo.DirectoryName); + var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, series, folderInfo, true); + return _importApprovedEpisodes.Import(decisions, true, downloadClientItem); } @@ -207,7 +213,7 @@ namespace NzbDrone.Core.MediaFiles var size = _diskProvider.GetFileSize(videoFile); var quality = QualityParser.ParseQuality(videoFile); - if (!_sampleService.IsSample(series, quality, videoFile, size, + if (!_detectSample.IsSample(series, quality, videoFile, size, episodeParseResult.SeasonNumber)) { _logger.Warn("Non-sample file detected: [{0}]", videoFile); @@ -227,14 +233,14 @@ namespace NzbDrone.Core.MediaFiles private ImportResult FileIsLockedResult(string videoFile) { _logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); - return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, "Locked file, try again later"), "Locked file, try again later"); + return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new Rejection("Locked file, try again later")), "Locked file, try again later"); } private ImportResult UnknownSeriesResult(string message, string videoFile = null) { var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile }; - return new ImportResult(new ImportDecision(localEpisode, "Unknown Series"), message); + return new ImportResult(new ImportDecision(localEpisode, new Rejection("Unknown Series")), message); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs similarity index 95% rename from src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs rename to src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs index 2b93406e4..d83040965 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs @@ -8,19 +8,19 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport { - public interface ISampleService + public interface IDetectSample { bool IsSample(Series series, QualityModel quality, string path, long size, int seasonNumber); } - public class SampleService : ISampleService + public class DetectSample : IDetectSample { private readonly IVideoFileInfoReader _videoFileInfoReader; private readonly Logger _logger; private static List _largeSampleSizeQualities = new List { Quality.HDTV1080p, Quality.WEBDL1080p, Quality.Bluray1080p }; - public SampleService(IVideoFileInfoReader videoFileInfoReader, Logger logger) + public DetectSample(IVideoFileInfoReader videoFileInfoReader, Logger logger) { _videoFileInfoReader = videoFileInfoReader; _logger = logger; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs index 3759ea6d0..86abb87b7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs @@ -3,8 +3,8 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.EpisodeImport { - public interface IImportDecisionEngineSpecification : IRejectWithReason + public interface IImportDecisionEngineSpecification { - bool IsSatisfiedBy(LocalEpisode localEpisode); + Decision IsSatisfiedBy(LocalEpisode localEpisode); } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index ea106f0d9..de2fba4b8 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -120,7 +120,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport //Adding all the rejected decisions importResults.AddRange(decisions.Where(c => !c.Approved) - .Select(d => new ImportResult(d, d.Rejections.ToArray()))); + .Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray()))); return importResults; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs index da7e8d092..ba55a4aec 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.EpisodeImport @@ -8,7 +9,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public class ImportDecision { public LocalEpisode LocalEpisode { get; private set; } - public IEnumerable Rejections { get; private set; } + public IEnumerable Rejections { get; private set; } public bool Approved { @@ -18,7 +19,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport } } - public ImportDecision(LocalEpisode localEpisode, params string[] rejections) + public ImportDecision(LocalEpisode localEpisode, params Rejection[] rejections) { LocalEpisode = localEpisode; Rejections = rejections.ToList(); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index a1bcd9b5e..871ec8c1a 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -15,23 +17,26 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { public interface IMakeImportDecision { - List GetImportDecisions(List videoFiles, Series series, bool sceneSource, QualityModel quality = null); + List GetImportDecisions(List videoFiles, Series series); + List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); } public class ImportDecisionMaker : IMakeImportDecision { - private readonly IEnumerable _specifications; + private readonly IEnumerable _specifications; private readonly IParsingService _parsingService; private readonly IMediaFileService _mediaFileService; private readonly IDiskProvider _diskProvider; private readonly IVideoFileInfoReader _videoFileInfoReader; + private readonly IDetectSample _detectSample; private readonly Logger _logger; - public ImportDecisionMaker(IEnumerable specifications, + public ImportDecisionMaker(IEnumerable specifications, IParsingService parsingService, IMediaFileService mediaFileService, IDiskProvider diskProvider, IVideoFileInfoReader videoFileInfoReader, + IDetectSample detectSample, Logger logger) { _specifications = specifications; @@ -39,98 +44,96 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _mediaFileService = mediaFileService; _diskProvider = diskProvider; _videoFileInfoReader = videoFileInfoReader; + _detectSample = detectSample; _logger = logger; } - public List GetImportDecisions(List videoFiles, Series series, bool sceneSource, QualityModel quality = null) + public List GetImportDecisions(List videoFiles, Series series) + { + return GetImportDecisions(videoFiles, series, null, false); + } + + public List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource) { var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series); _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count()); - return GetDecisions(newFiles, series, sceneSource, quality).ToList(); + var shouldUseFolderName = ShouldUseFolderName(videoFiles, series, folderInfo); + var decisions = new List(); + + foreach (var file in newFiles) + { + decisions.AddIfNotNull(GetDecision(file, series, folderInfo, sceneSource, shouldUseFolderName)); + } + + return decisions; } - private IEnumerable GetDecisions(IEnumerable videoFiles, Series series, bool sceneSource, QualityModel quality = null) + private ImportDecision GetDecision(string file, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName) { - foreach (var file in videoFiles) + ImportDecision decision = null; + + try { - ImportDecision decision = null; + var localEpisode = _parsingService.GetLocalEpisode(file, series, shouldUseFolderName ? folderInfo : null, sceneSource); - try + if (localEpisode != null) { - var localEpisode = _parsingService.GetLocalEpisode(file, series, sceneSource); + localEpisode.Quality = GetQuality(folderInfo, localEpisode.Quality, series); + localEpisode.Size = _diskProvider.GetFileSize(file); - if (localEpisode != null) + _logger.Debug("Size: {0}", localEpisode.Size); + + //TODO: make it so media info doesn't ruin the import process of a new series + if (sceneSource) { - if (quality != null && - new QualityModelComparer(localEpisode.Series.Profile).Compare(quality, - localEpisode.Quality) > 0) - { - _logger.Debug("Using quality from folder: {0}", quality); - localEpisode.Quality = quality; - } - - localEpisode.Size = _diskProvider.GetFileSize(file); - _logger.Debug("Size: {0}", localEpisode.Size); - - //TODO: make it so media info doesn't ruin the import process of a new series - if (sceneSource) - { - localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); - } - - decision = GetDecision(localEpisode); + localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); } - else - { - localEpisode = new LocalEpisode(); - localEpisode.Path = file; - - decision = new ImportDecision(localEpisode, "Unable to parse file"); - } + decision = GetDecision(localEpisode); } - catch (EpisodeNotFoundException e) + + else { - var localEpisode = new LocalEpisode(); + localEpisode = new LocalEpisode(); localEpisode.Path = file; - decision = new ImportDecision(localEpisode, e.Message); - } - catch (Exception e) - { - _logger.ErrorException("Couldn't import file. " + file, e); - } - - if (decision != null) - { - yield return decision; + decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); } } + catch (EpisodeNotFoundException e) + { + var localEpisode = new LocalEpisode(); + localEpisode.Path = file; + + decision = new ImportDecision(localEpisode, new Rejection(e.Message)); + } + catch (Exception e) + { + _logger.ErrorException("Couldn't import file. " + file, e); + } + + return decision; } private ImportDecision GetDecision(LocalEpisode localEpisode) { var reasons = _specifications.Select(c => EvaluateSpec(c, localEpisode)) - .Where(c => !string.IsNullOrWhiteSpace(c)); + .Where(c => c != null); return new ImportDecision(localEpisode, reasons.ToArray()); } - private string EvaluateSpec(IRejectWithReason spec, LocalEpisode localEpisode) + private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode) { try { - if (string.IsNullOrWhiteSpace(spec.RejectionReason)) - { - throw new InvalidOperationException("[Need Rejection Text]"); - } + var result = spec.IsSatisfiedBy(localEpisode); - var generalSpecification = spec as IImportDecisionEngineSpecification; - if (generalSpecification != null && !generalSpecification.IsSatisfiedBy(localEpisode)) + if (!result.Accepted) { - return spec.RejectionReason; + return new Rejection(result.Reason); } } catch (Exception e) @@ -138,10 +141,55 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport //e.Data.Add("report", remoteEpisode.Report.ToJson()); //e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); _logger.ErrorException("Couldn't evaluate decision on " + localEpisode.Path, e); - return string.Format("{0}: {1}", spec.GetType().Name, e.Message); + return new Rejection(String.Format("{0}: {1}", spec.GetType().Name, e.Message)); } return null; } + + private bool ShouldUseFolderName(List videoFiles, Series series, ParsedEpisodeInfo folderInfo) + { + if (folderInfo == null) + { + return false; + } + + if (folderInfo.FullSeason) + { + return false; + } + + return videoFiles.Count(file => + { + var size = _diskProvider.GetFileSize(file); + var fileQuality = QualityParser.ParseQuality(file); + var sample = _detectSample.IsSample(series, GetQuality(folderInfo, fileQuality, series), file, size, folderInfo.SeasonNumber); + + if (sample) + { + return false; + } + + if (SceneChecker.IsSceneTitle(Path.GetFileName(file))) + { + return false; + } + + return true; + }) == 1; + } + + private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) + { + if (folderInfo != null && + new QualityModelComparer(series.Profile).Compare(folderInfo.Quality, + fileQuality) > 0) + { + _logger.Debug("Using quality from folder: {0}", folderInfo.Quality); + return folderInfo.Quality; + } + + return fileQuality; + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs index 64ccb47b2..a0d989335 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportResult.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.EnsureThat; @@ -8,7 +7,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public class ImportResult { public ImportDecision ImportDecision { get; private set; } - public List Errors { get; private set; } + public List Errors { get; private set; } public ImportResultType Result { @@ -28,7 +27,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport } } - public ImportResult(ImportDecision importDecision, params String[] errors) + public ImportResult(ImportDecision importDecision, params string[] errors) { Ensure.That(importDecision, () => importDecision).IsNotNull(); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs index 1256a5207..c3fe8550e 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs @@ -3,6 +3,7 @@ using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications @@ -20,14 +21,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public string RejectionReason { get { return "Not enough free space"; } } - - public bool IsSatisfiedBy(LocalEpisode localEpisode) + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (_configService.SkipFreeSpaceCheckWhenImporting) { _logger.Debug("Skipping free space check when importing"); - return true; + return Decision.Accept(); } try @@ -35,7 +34,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (localEpisode.ExistingFile) { _logger.Debug("Skipping free space check for existing episode"); - return true; + return Decision.Accept(); } var path = Directory.GetParent(localEpisode.Series.Path); @@ -44,13 +43,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (!freeSpace.HasValue) { _logger.Debug("Free space check returned an invalid result for: {0}", path); - return true; + return Decision.Accept(); } if (freeSpace < localEpisode.Size + 100.Megabytes()) { _logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localEpisode, localEpisode.Size); - return false; + return Decision.Reject("Not enough free space"); } } catch (DirectoryNotFoundException ex) @@ -62,7 +61,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger.ErrorException("Unable to check free disk space while importing: " + localEpisode.Path, ex); } - return true; + return Decision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs index 1c3c8bf79..7397c13e7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs @@ -1,4 +1,5 @@ using NLog; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications @@ -12,17 +13,15 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public string RejectionReason { get { return "Full season file"; } } - - public bool IsSatisfiedBy(LocalEpisode localEpisode) + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ParsedEpisodeInfo.FullSeason) { _logger.Debug("Single episode file detected as containing all episodes in the season"); - return false; + return Decision.Reject("Single episode file contains all episodes in seasons"); } - return true; + return Decision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs new file mode 100644 index 000000000..982ffe94f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications +{ + public class MatchesFolderSpecification : IImportDecisionEngineSpecification + { + private readonly Logger _logger; + + public MatchesFolderSpecification(Logger logger) + { + _logger = logger; + } + public Decision IsSatisfiedBy(LocalEpisode localEpisode) + { + if (localEpisode.ExistingFile) + { + return Decision.Accept(); + } + + var folderInfo = Parser.Parser.ParseTitle(new FileInfo(localEpisode.Path).DirectoryName); + + if (folderInfo == null) + { + return Decision.Accept(); + } + + if (folderInfo.FullSeason) + { + return Decision.Accept(); + } + + var unexpected = localEpisode.ParsedEpisodeInfo.EpisodeNumbers.Where(f => !folderInfo.EpisodeNumbers.Contains(f)).ToList(); + + if (unexpected.Any()) + { + _logger.Debug("Unexpected episode number(s) in file: {0}", unexpected); + + if (unexpected.Count == 1) + { + return Decision.Reject("Episode Number {0} was unexpected", unexpected.First()); + } + + return Decision.Reject("Episode Numbers {0} were unexpected", String.Join(", ", unexpected)); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs index 7d32566f8..3b9e072ae 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs @@ -1,35 +1,41 @@ using NLog; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { public class NotSampleSpecification : IImportDecisionEngineSpecification { - private readonly ISampleService _sampleService; + private readonly IDetectSample _detectSample; private readonly Logger _logger; - public NotSampleSpecification(ISampleService sampleService, + public NotSampleSpecification(IDetectSample detectSample, Logger logger) { - _sampleService = sampleService; + _detectSample = detectSample; _logger = logger; } - public string RejectionReason { get { return "Sample"; } } - - public bool IsSatisfiedBy(LocalEpisode localEpisode) + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ExistingFile) { _logger.Debug("Existing file, skipping sample check"); - return true; + return Decision.Accept(); } - return !_sampleService.IsSample(localEpisode.Series, - localEpisode.Quality, - localEpisode.Path, - localEpisode.Size, - localEpisode.SeasonNumber); + var sample = _detectSample.IsSample(localEpisode.Series, + localEpisode.Quality, + localEpisode.Path, + localEpisode.Size, + localEpisode.SeasonNumber); + + if (sample) + { + return Decision.Reject("Sample"); + } + + return Decision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs index c18f96466..f457ddbf2 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs @@ -4,6 +4,7 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications @@ -21,14 +22,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public string RejectionReason { get { return "File is still being unpacked"; } } - - public bool IsSatisfiedBy(LocalEpisode localEpisode) + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ExistingFile) { _logger.Debug("{0} is in series folder, unpacking check", localEpisode.Path); - return true; + return Decision.Accept(); } foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|')) @@ -41,13 +40,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (OsInfo.IsNotWindows) { _logger.Debug("{0} is still being unpacked", localEpisode.Path); - return false; + return Decision.Reject("File is still being unpacked"); } if (_diskProvider.FileGetLastWrite(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) { _logger.Debug("{0} appears to be unpacking still", localEpisode.Path); - return false; + return Decision.Reject("File is still being unpacked"); } } @@ -55,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications } } - return true; + return Decision.Accept(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index aa567571f..3d07306af 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -1,5 +1,6 @@ using System.Linq; using NLog; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; @@ -14,18 +15,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } - public string RejectionReason { get { return "Not an upgrade for existing episode file(s)"; } } - - public bool IsSatisfiedBy(LocalEpisode localEpisode) + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { var qualityComparer = new QualityModelComparer(localEpisode.Series.Profile); if (localEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && qualityComparer.Compare(e.EpisodeFile.Value.Quality, localEpisode.Quality) > 0)) { _logger.Debug("This file isn't an upgrade for all episodes. Skipping {0}", localEpisode.Path); - return false; + return Decision.Reject("Not an upgrade for existing episode file(s)"); } - return true; + return Decision.Accept(); } } } diff --git a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs index 177f1da91..e89b35be6 100644 --- a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs +++ b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Metadata { try { - var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, message.Series, false); + var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, message.Series); if (localEpisode == null) { diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 2c4a2f698..04736e507 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -561,8 +561,9 @@ - + + diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index ba59179a4..ab684792b 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -104,7 +104,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes with single digit episode number (S01E1, S01E5E6, etc) - new Regex(@"^(?.*?)(?:\W?S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]){1,2}(?<episode>\d{1}))+)+(\W+|_|$)(?!\\)", + new Regex(@"^(?<title>.*?)(?:(?:_|-|\s|\.)S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]){1,2}(?<episode>\d{1}))+)+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Anime - Title Absolute Episode Number (e66) diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 46e80a118..14d4f2de4 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -13,7 +13,8 @@ namespace NzbDrone.Core.Parser { public interface IParsingService { - LocalEpisode GetLocalEpisode(string filename, Series series, bool sceneSource); + LocalEpisode GetLocalEpisode(string filename, Series series); + LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); Series GetSeries(string title); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 tvRageId = 0, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 seriesId, IEnumerable<Int32> episodeIds); @@ -39,9 +40,25 @@ namespace NzbDrone.Core.Parser _logger = logger; } - public LocalEpisode GetLocalEpisode(string filename, Series series, bool sceneSource) + public LocalEpisode GetLocalEpisode(string filename, Series series) { - var parsedEpisodeInfo = Parser.ParsePath(filename); + return GetLocalEpisode(filename, series, null, false); + } + + public LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource) + { + ParsedEpisodeInfo parsedEpisodeInfo; + + if (folderInfo != null) + { + parsedEpisodeInfo = folderInfo.JsonClone(); + parsedEpisodeInfo.Quality = QualityParser.ParseQuality(Path.GetFileName(filename)); + } + + else + { + parsedEpisodeInfo = Parser.ParsePath(filename); + } if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode) { diff --git a/src/NzbDrone.Core/Parser/SceneChecker.cs b/src/NzbDrone.Core/Parser/SceneChecker.cs index 75fa4595c..339f6b17c 100644 --- a/src/NzbDrone.Core/Parser/SceneChecker.cs +++ b/src/NzbDrone.Core/Parser/SceneChecker.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Core.Parser +using System; + +namespace NzbDrone.Core.Parser { public static class SceneChecker { @@ -10,10 +12,14 @@ if (title.Contains(" ")) return false; var parsedTitle = Parser.ParseTitle(title); - if (parsedTitle == null - || parsedTitle.ReleaseGroup == null - || parsedTitle.Quality.Quality == Qualities.Quality.Unknown - || string.IsNullOrWhiteSpace(parsedTitle.SeriesTitle)) return false; + + if (parsedTitle == null || + parsedTitle.ReleaseGroup == null || + parsedTitle.Quality.Quality == Qualities.Quality.Unknown || + String.IsNullOrWhiteSpace(parsedTitle.SeriesTitle)) + { + return false; + } return true; } diff --git a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj index 105bf8b55..3500ef1ad 100644 --- a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj @@ -91,7 +91,6 @@ <Compile Include="LoggingTest.cs" /> <Compile Include="MockerExtensions.cs" /> <Compile Include="NzbDroneRunner.cs" /> - <Compile Include="ObjectExtensions.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="ReflectionExtensions.cs" /> <Compile Include="StringExtensions.cs" />