From 81e385bebfc33c949fbb8f2dbbbd94ac445fcf5c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 14 Apr 2018 13:01:24 -0700 Subject: [PATCH] New: Use media info during import to extract resolution for quality Closes #448 Closes #1105 --- .../Aggregators/AggregateEpisodesFixture.cs | 109 ++++++++ .../Aggregators/AggregateQualityFixture.cs | 93 +++++++ .../AugmentQualityFromMediaInfoFixture.cs | 68 +++++ .../ImportDecisionMakerFixture.cs | 252 ++---------------- .../FullSeasonSpecificationFixture.cs | 6 +- .../MatchesFolderSpecificationFixture.cs | 15 +- .../ImportApprovedEpisodesFixture.cs | 5 +- .../NzbDrone.Core.Test.csproj | 3 + .../ParserTests/ParserFixture.cs | 2 +- .../ParserTests/QualityParserFixture.cs | 4 +- .../Metadata/ExistingMetadataImporter.cs | 21 +- .../Others/ExistingOtherExtraImporter.cs | 24 +- .../Subtitles/ExistingSubtitleImporter.cs | 25 +- .../DownloadedEpisodesImportService.cs | 7 - .../Aggregation/AggregationFailedException.cs | 24 ++ .../Aggregation/AggregationService.cs | 65 +++++ .../Aggregators/AggregateEpisodes.cs | 54 ++++ .../Aggregators/AggregateQuality.cs | 78 ++++++ .../Aggregators/AggregateReleaseGroup.cs | 27 ++ .../AugmentQualityFromDownloadClientItem.cs | 23 ++ .../Quality/AugmentQualityFromFileName.cs | 28 ++ .../Quality/AugmentQualityFromFolder.cs | 23 ++ .../Quality/AugmentQualityFromMediaInfo.cs | 39 +++ .../Quality/AugmentQualityResult.cs | 36 +++ .../Augmenters/Quality/Confidence.cs | 10 + .../Augmenters/Quality/IAugmentQuality.cs | 9 + .../Aggregators/IAggregateLocalEpisode.cs | 9 + .../MediaFiles/EpisodeImport/DetectSample.cs | 1 + .../EpisodeImport/ImportApprovedEpisodes.cs | 2 +- .../EpisodeImport/ImportDecisionMaker.cs | 172 ++++++------ .../Manual/ManualImportService.cs | 4 +- .../Specifications/FullSeasonSpecification.cs | 4 +- .../MatchesFolderSpecification.cs | 6 +- .../MediaInfo/VideoFileInfoReader.cs | 4 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 14 + .../Parser/Model/LocalEpisode.cs | 6 +- src/NzbDrone.Core/Parser/ParsingService.cs | 63 +---- src/NzbDrone.Core/Parser/QualityParser.cs | 2 +- src/NzbDrone.Core/Qualities/Quality.cs | 55 ++-- .../Qualities/QualityDetectionSource.cs | 9 + src/NzbDrone.Core/Qualities/QualityModel.cs | 6 +- src/NzbDrone.Core/Qualities/QualitySource.cs | 12 +- 42 files changed, 967 insertions(+), 452 deletions(-) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodesFixture.cs create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQualityFixture.cs create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfoFixture.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationFailedException.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationService.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodes.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQuality.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseGroup.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromDownloadClientItem.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromFileName.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromFolder.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfo.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityResult.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/Confidence.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/IAugmentQuality.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/IAggregateLocalEpisode.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityDetectionSource.cs diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodesFixture.cs new file mode 100644 index 000000000..45d24727a --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodesFixture.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + [TestFixture] + public class AugmentEpisodesFixture : CoreTest + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew().Build(); + + var augmenters = new List> + { + new Mock() + }; + + Mocker.SetConstant(augmenters.Select(c => c.Object)); + } + + [Test] + public void should_not_use_folder_for_full_season() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, false); + + Mocker.GetMock() + .Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once()); + } + + [Test] + public void should_not_use_folder_when_it_contains_more_than_one_valid_video_file() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, true); + + Mocker.GetMock() + .Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once()); + } + + [Test] + public void should_not_use_folder_name_if_file_name_is_scene_name() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01E01\Series.Title.S01E01.720p.HDTV-Sonarr.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, false); + + Mocker.GetMock() + .Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once()); + } + + [Test] + public void should_use_folder_when_only_one_video_file() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01E01\Series.Title.S01E01.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, false); + + Mocker.GetMock() + .Verify(v => v.GetEpisodes(folderEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQualityFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQualityFixture.cs new file mode 100644 index 000000000..3b39c8526 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQualityFixture.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + [TestFixture] + public class AugmentQualityFixture : CoreTest + { + private Mock _mediaInfoAugmenter; + private Mock _fileExtensionAugmenter; + private Mock _nameAugmenter; + + private IEnumerable _qualityAugmenters; + + [SetUp] + public void Setup() + { + _mediaInfoAugmenter = new Mock(); + _fileExtensionAugmenter = new Mock(); + _nameAugmenter = new Mock(); + + _mediaInfoAugmenter.Setup(s => s.AugmentQuality(It.IsAny())) + .Returns(AugmentQualityResult.ResolutionOnly(1080, Confidence.MediaInfo)); + + _fileExtensionAugmenter.Setup(s => s.AugmentQuality(It.IsAny())) + .Returns(new AugmentQualityResult(QualitySource.Television, Confidence.Fallback, 720, Confidence.Fallback, new Revision())); + + _nameAugmenter.Setup(s => s.AugmentQuality(It.IsAny())) + .Returns(new AugmentQualityResult(QualitySource.Television, Confidence.Default, 480, Confidence.Default, new Revision())); + } + + private void GivenAugmenters(params Mock[] mocks) + { + Mocker.SetConstant>(mocks.Select(c => c.Object)); + } + + [Test] + public void should_return_HDTV720_from_extension_when_other_augments_are_null() + { + var nullMock = new Mock(); + nullMock.Setup(s => s.AugmentQuality(It.IsAny())) + .Returns(l => null); + + GivenAugmenters(_fileExtensionAugmenter, nullMock); + + var result = Subject.Aggregate(new LocalEpisode(), false); + + result.Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.Extension); + result.Quality.Quality.Should().Be(Quality.HDTV720p); + } + + [Test] + public void should_return_SDTV_when_HDTV720_came_from_extension() + { + GivenAugmenters(_fileExtensionAugmenter, _nameAugmenter); + + var result = Subject.Aggregate(new LocalEpisode(), false); + + result.Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.Name); + result.Quality.Quality.Should().Be(Quality.SDTV); + } + + [Test] + public void should_return_HDTV1080p_when_HDTV720_came_from_extension_and_mediainfo_indicates_1080() + { + GivenAugmenters(_fileExtensionAugmenter, _mediaInfoAugmenter); + + var result = Subject.Aggregate(new LocalEpisode(), false); + + result.Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.MediaInfo); + result.Quality.Quality.Should().Be(Quality.HDTV1080p); + } + + [Test] + public void should_return_HDTV1080p_when_SDTV_came_from_name_and_mediainfo_indicates_1080() + { + GivenAugmenters(_nameAugmenter, _mediaInfoAugmenter); + + var result = Subject.Aggregate(new LocalEpisode(), false); + + result.Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.MediaInfo); + result.Quality.Quality.Should().Be(Quality.HDTV1080p); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfoFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfoFixture.cs new file mode 100644 index 000000000..eabd186ac --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfoFixture.cs @@ -0,0 +1,68 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality +{ + [TestFixture] + public class AugmentQualityFromMediaInfoFixture : CoreTest + { + [Test] + public void should_return_null_if_media_info_is_null() + { + var localEpisode = Builder.CreateNew() + .With(l => l.MediaInfo = null) + .Build(); + + Subject.AugmentQuality(localEpisode).Should().Be(null); + } + + [Test] + public void should_return_null_if_media_info_width_is_zero() + { + var mediaInfo = Builder.CreateNew() + .With(m => m.Width = 0) + .Build(); + + var localEpisode = Builder.CreateNew() + .With(l => l.MediaInfo = mediaInfo) + .Build(); + + Subject.AugmentQuality(localEpisode).Should().Be(null); + } + + [TestCase(4096, 2160)] // True 4K + [TestCase(4000, 2160)] + [TestCase(3840, 2160)] // 4K UHD + [TestCase(3200, 2160)] + [TestCase(2000, 1080)] + [TestCase(1920, 1080)] // Full HD + [TestCase(1800, 1080)] + [TestCase(1490, 720)] + [TestCase(1280, 720)] // HD + [TestCase(1200, 720)] + [TestCase(800, 480)] + [TestCase(720, 480)] // SDTV + [TestCase(600, 480)] + [TestCase(100, 480)] + public void should_return_closest_resolution(int mediaInfoWidth, int expectedResolution) + { + var mediaInfo = Builder.CreateNew() + .With(m => m.Width = mediaInfoWidth) + .Build(); + + var localEpisode = Builder.CreateNew() + .With(l => l.MediaInfo = mediaInfo) + .Build(); + + var result = Subject.AugmentQuality(localEpisode); + + result.Should().NotBe(null); + result.Resolution.Should().Be(expectedResolution); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index 66d59cf97..09ee280ed 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; -using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; @@ -15,6 +14,7 @@ using NzbDrone.Core.Tv; using NzbDrone.Test.Common; using FizzWare.NBuilder; using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { @@ -67,10 +67,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" }; - Mocker.GetMock() - .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(_localEpisode); - GivenVideoFiles(new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() }); } @@ -88,20 +84,31 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport .Returns(_videoFiles); } + private void GivenAugmentationSuccess() + { + Mocker.GetMock() + .Setup(s => s.Augment(It.IsAny(), It.IsAny())) + .Callback((localEpisode, otherFiles) => + { + localEpisode.Episodes = _localEpisode.Episodes; + }); + } + [Test] public void should_call_all_specifications() { var downloadClientItem = Builder.CreateNew().Build(); + GivenAugmentationSuccess(); GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); Subject.GetImportDecisions(_videoFiles, new Series(), downloadClientItem, null, false); - _fail1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _fail2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _fail3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _pass1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _pass2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _pass3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); + _fail1.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); + _fail2.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); + _fail3.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); + _pass1.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); + _pass2.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); + _pass3.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); } [Test] @@ -125,8 +132,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport } [Test] - public void should_return_pass_if_all_specs_pass() + public void should_return_approved_if_all_specs_pass() { + GivenAugmentationSuccess(); GivenSpecifications(_pass1, _pass2, _pass3); var result = Subject.GetImportDecisions(_videoFiles, new Series()); @@ -137,6 +145,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport [Test] public void should_have_same_number_of_rejections_as_specs_that_failed() { + GivenAugmentationSuccess(); GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); var result = Subject.GetImportDecisions(_videoFiles, new Series()); @@ -148,8 +157,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { GivenSpecifications(_pass1); - Mocker.GetMock() - .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(c => c.Augment(It.IsAny(), It.IsAny())) .Throws(); _videoFiles = new List @@ -163,76 +172,17 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport Subject.GetImportDecisions(_videoFiles, _series); - Mocker.GetMock() - .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); + Mocker.GetMock() + .Verify(c => c.Augment(It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); ExceptionVerification.ExpectedErrors(3); } - [Test] - public void should_use_file_quality_if_folder_quality_is_null() - { - GivenSpecifications(_pass1, _pass2, _pass3); - var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - - var result = Subject.GetImportDecisions(_videoFiles, _series); - - result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); - } - - [Test] - public void should_use_file_quality_if_file_quality_was_determined_by_name() - { - GivenSpecifications(_pass1, _pass2, _pass3); - var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - - var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedEpisodeInfo{Quality = new QualityModel(Quality.SDTV)}, true); - - result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); - } - - [Test] - public void should_use_folder_quality_when_file_quality_was_determined_by_the_extension() - { - GivenSpecifications(_pass1, _pass2, _pass3); - GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() }); - - _localEpisode.Path = _videoFiles.Single(); - _localEpisode.Quality.QualitySource = QualitySource.Extension; - _localEpisode.Quality.Quality = Quality.HDTV720p; - - var expectedQuality = new QualityModel(Quality.SDTV); - - var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedEpisodeInfo { Quality = expectedQuality }, true); - - result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); - } - - [Test] - public void should_use_folder_quality_when_greater_than_file_quality() - { - GivenSpecifications(_pass1, _pass2, _pass3); - GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() }); - - _localEpisode.Path = _videoFiles.Single(); - _localEpisode.Quality.Quality = Quality.HDTV720p; - - var expectedQuality = new QualityModel(Quality.Bluray720p); - - var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedEpisodeInfo { Quality = expectedQuality }, true); - - result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); - } - [Test] public void should_not_throw_if_episodes_are_not_found() { GivenSpecifications(_pass1); - Mocker.GetMock() - .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new LocalEpisode() { Path = "test", ParsedEpisodeInfo = new ParsedEpisodeInfo { } }); - _videoFiles = new List { "The.Office.S03E115.DVDRip.XviD-OSiTV", @@ -244,162 +194,18 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport var decisions = Subject.GetImportDecisions(_videoFiles, _series); - Mocker.GetMock() - .Verify(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); + Mocker.GetMock() + .Verify(c => c.Augment(It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); decisions.Should().HaveCount(3); decisions.First().Rejections.Should().NotBeEmpty(); } - [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, null, 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, null, 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, null, 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.IsAny())) - .Returns((Series s, string path, bool special) => - { - if (path.Contains("sample")) - { - return DetectSampleResult.Sample; - } - - return DetectSampleResult.NotSample; - }); - - var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01"); - - Subject.GetImportDecisions(_videoFiles, _series, null, 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, null, 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()); - } - - [Test] - public void should_not_use_folder_quality_when_it_is_unknown() - { - GivenSpecifications(_pass1, _pass2, _pass3); - - _series.Profile = new Profile - { - Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.Unknown) - }; - - - var folderQuality = new QualityModel(Quality.Unknown); - - var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedEpisodeInfo { Quality = folderQuality}, true); - - result.Single().LocalEpisode.Quality.Should().Be(_quality); - } - [Test] public void should_return_a_decision_when_exception_is_caught() { - Mocker.GetMock() - .Setup(c => c.GetLocalEpisode(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(c => c.Augment(It.IsAny(), It.IsAny())) .Throws(); _videoFiles = new List diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecificationFixture.cs index 585f9cd40..e2704af6b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecificationFixture.cs @@ -1,4 +1,4 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications Path = @"C:\Test\30 Rock\30.rock.s01e01.avi".AsOsAgnostic(), Size = 100, Series = Builder.CreateNew().Build(), - ParsedEpisodeInfo = new ParsedEpisodeInfo + FileEpisodeInfo = new ParsedEpisodeInfo { FullSeason = false } @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications [Test] public void should_return_false_when_file_contains_the_full_season() { - _localEpisode.ParsedEpisodeInfo.FullSeason = true; + _localEpisode.FileEpisodeInfo.FullSeason = true; Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs index 9b34c62e3..00d3dae1a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecificationFixture.cs @@ -1,4 +1,4 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications { _localEpisode = Builder.CreateNew() .With(l => l.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E05.mkv".AsOsAgnostic()) - .With(l => l.ParsedEpisodeInfo = + .With(l => l.FileEpisodeInfo = Builder.CreateNew() .With(p => p.EpisodeNumbers = new[] {5}) .With(p => p.FullSeason = false) @@ -53,7 +53,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications [Test] public void should_be_accepted_if_file_and_folder_have_the_same_episode() { - _localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 }; + _localEpisode.FileEpisodeInfo.EpisodeNumbers = new[] { 1 }; + _localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1 }; _localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic(); Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); } @@ -61,7 +62,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications [Test] public void should_be_accepted_if_file_is_one_episode_in_folder() { - _localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 }; + _localEpisode.FileEpisodeInfo.EpisodeNumbers = new[] { 1 }; + _localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1 }; _localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic(); Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); } @@ -76,9 +78,10 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications [Test] public void should_be_rejected_if_file_and_folder_do_not_have_same_episodes() { - _localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 5, 6 }; + _localEpisode.FileEpisodeInfo.EpisodeNumbers = new[] { 5, 6 }; + _localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 }; _localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E05E06.mkv".AsOsAgnostic(); Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index f7be82b81..4b2bd7745 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -58,10 +58,7 @@ namespace NzbDrone.Core.Test.MediaFiles Episodes = new List { episode }, Path = Path.Combine(series.Path, "30 Rock - S01E01 - Pilot.avi"), Quality = new QualityModel(Quality.Bluray720p), - ParsedEpisodeInfo = new ParsedEpisodeInfo - { - ReleaseGroup = "DRONE" - } + ReleaseGroup = "DRONE" })); } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 78dafba84..ef0436711 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -301,6 +301,9 @@ + + + diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 061e4ecf5..4a4693a45 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Test.ParserTests public void should_parse_quality_from_extension(string title) { Parser.Parser.ParseTitle(title).Quality.Quality.Should().NotBe(Quality.Unknown); - Parser.Parser.ParseTitle(title).Quality.QualitySource.Should().Be(QualitySource.Extension); + Parser.Parser.ParseTitle(title).Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.Extension); } diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 14bd283da..304af0064 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -283,7 +283,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("White.Van.Man.2011.S02E01.WS.PDTV.x264-REPACK-TLA")] public void should_parse_quality_from_name(string title) { - QualityParser.ParseQuality(title).QualitySource.Should().Be(QualitySource.Name); + QualityParser.ParseQuality(title).QualityDetectionSource.Should().Be(QualityDetectionSource.Name); } [TestCase("Revolution.S01E02.Chained.Heat.mkv")] @@ -292,7 +292,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[CR] Sailor Moon - 004 [48CE2D0F].avi")] public void should_parse_quality_from_extension(string title) { - QualityParser.ParseQuality(title).QualitySource.Should().Be(QualitySource.Extension); + QualityParser.ParseQuality(title).QualityDetectionSource.Should().Be(QualityDetectionSource.Extension); } private void ParseAndVerifyQuality(string title, Quality quality, bool proper) diff --git a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs index fa271f575..b5250f6e6 100644 --- a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs +++ b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs @@ -6,7 +6,9 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Subtitles; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Metadata @@ -14,18 +16,18 @@ namespace NzbDrone.Core.Extras.Metadata public class ExistingMetadataImporter : ImportExistingExtraFilesBase { private readonly IExtraFileService _metadataFileService; - private readonly IParsingService _parsingService; + private readonly IAugmentingService _augmentingService; private readonly Logger _logger; private readonly List _consumers; public ExistingMetadataImporter(IExtraFileService metadataFileService, IEnumerable consumers, - IParsingService parsingService, + IAugmentingService augmentingService, Logger logger) : base(metadataFileService) { _metadataFileService = metadataFileService; - _parsingService = parsingService; + _augmentingService = augmentingService; _logger = logger; _consumers = consumers.ToList(); } @@ -60,9 +62,18 @@ namespace NzbDrone.Core.Extras.Metadata if (metadata.Type == MetadataType.EpisodeImage || metadata.Type == MetadataType.EpisodeMetadata) { - var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, series); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = Parser.Parser.ParsePath(possibleMetadataFile), + Series = series, + Path = possibleMetadataFile + }; - if (localEpisode == null) + try + { + _augmentingService.Augment(localEpisode, false); + } + catch (AugmentingFailedException ex) { _logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile); continue; diff --git a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs index 05afb5645..724f4968c 100644 --- a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs +++ b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs @@ -1,10 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Others @@ -12,16 +15,16 @@ namespace NzbDrone.Core.Extras.Others public class ExistingOtherExtraImporter : ImportExistingExtraFilesBase { private readonly IExtraFileService _otherExtraFileService; - private readonly IParsingService _parsingService; + private readonly IAugmentingService _augmentingService; private readonly Logger _logger; public ExistingOtherExtraImporter(IExtraFileService otherExtraFileService, - IParsingService parsingService, + IAugmentingService augmentingService, Logger logger) : base(otherExtraFileService) { _otherExtraFileService = otherExtraFileService; - _parsingService = parsingService; + _augmentingService = augmentingService; _logger = logger; } @@ -44,9 +47,18 @@ namespace NzbDrone.Core.Extras.Others continue; } - var localEpisode = _parsingService.GetLocalEpisode(possibleExtraFile, series); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = Parser.Parser.ParsePath(possibleExtraFile), + Series = series, + Path = possibleExtraFile + }; - if (localEpisode == null) + try + { + _augmentingService.Augment(localEpisode, false); + } + catch (AugmentingFailedException ex) { _logger.Debug("Unable to parse extra file: {0}", possibleExtraFile); continue; diff --git a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs index d3ae8d46b..c15732351 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs @@ -4,7 +4,9 @@ using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Subtitles @@ -12,16 +14,16 @@ namespace NzbDrone.Core.Extras.Subtitles public class ExistingSubtitleImporter : ImportExistingExtraFilesBase { private readonly IExtraFileService _subtitleFileService; - private readonly IParsingService _parsingService; + private readonly IAugmentingService _augmentingService; private readonly Logger _logger; public ExistingSubtitleImporter(IExtraFileService subtitleFileService, - IParsingService parsingService, + IAugmentingService augmentingService, Logger logger) : base (subtitleFileService) { _subtitleFileService = subtitleFileService; - _parsingService = parsingService; + _augmentingService = augmentingService; _logger = logger; } @@ -40,11 +42,20 @@ namespace NzbDrone.Core.Extras.Subtitles if (SubtitleFileExtensions.Extensions.Contains(extension)) { - var localEpisode = _parsingService.GetLocalEpisode(possibleSubtitleFile, series); - - if (localEpisode == null) + var localEpisode = new LocalEpisode { - _logger.Debug("Unable to parse subtitle file: {0}", possibleSubtitleFile); + FileEpisodeInfo = Parser.Parser.ParsePath(possibleSubtitleFile), + Series = series, + Path = possibleSubtitleFile + }; + + try + { + _augmentingService.Augment(localEpisode, false); + } + catch (AugmentingFailedException ex) + { + _logger.Debug("Unable to parse extra file: {0}", possibleSubtitleFile); continue; } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index aa7b4a2ec..7f77c1286 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -156,14 +156,7 @@ namespace NzbDrone.Core.MediaFiles return new List(); } - var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); - - if (folderInfo != null) - { - _logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality); - } - var videoFiles = _diskScanService.FilterFiles(directoryInfo.FullName, _diskScanService.GetVideoFiles(directoryInfo.FullName)); if (downloadClientItem == null) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationFailedException.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationFailedException.cs new file mode 100644 index 000000000..948df4580 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationFailedException.cs @@ -0,0 +1,24 @@ +using System; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation +{ + public class AugmentingFailedException : NzbDroneException + { + public AugmentingFailedException(string message, params object[] args) : base(message, args) + { + } + + public AugmentingFailedException(string message) : base(message) + { + } + + public AugmentingFailedException(string message, Exception innerException, params object[] args) : base(message, innerException, args) + { + } + + public AugmentingFailedException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationService.cs new file mode 100644 index 000000000..6e1740a07 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/AggregationService.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation +{ + public interface IAugmentingService + { + LocalEpisode Augment(LocalEpisode localEpisode, bool otherFiles); + } + + public class AugmentingService : IAugmentingService + { + private readonly IEnumerable _augmenters; + private readonly IDiskProvider _diskProvider; + private readonly IVideoFileInfoReader _videoFileInfoReader; + private readonly Logger _logger; + + public AugmentingService(IEnumerable augmenters, + IDiskProvider diskProvider, + IVideoFileInfoReader videoFileInfoReader, + Logger logger) + { + _augmenters = augmenters; + _diskProvider = diskProvider; + _videoFileInfoReader = videoFileInfoReader; + _logger = logger; + } + + public LocalEpisode Augment(LocalEpisode localEpisode, bool otherFiles) + { + if (localEpisode.DownloadClientEpisodeInfo == null && + localEpisode.FolderEpisodeInfo == null && + localEpisode.FileEpisodeInfo == null) + { + if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(localEpisode.Path))) + { + throw new AugmentingFailedException("Unable to parse episode info from path: {0}", localEpisode.Path); + } + } + + localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path); + localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(localEpisode.Path); + + foreach (var augmenter in _augmenters) + { + try + { + augmenter.Aggregate(localEpisode, otherFiles); + } + catch (Exception ex) + { + _logger.Warn(ex, ex.Message); + } + } + + return localEpisode; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodes.cs new file mode 100644 index 000000000..24b8b0e0a --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateEpisodes.cs @@ -0,0 +1,54 @@ +using System.IO; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + public class AggregateEpisodes : IAggregateLocalEpisode + { + private readonly IParsingService _parsingService; + + public AggregateEpisodes(IParsingService parsingService) + { + _parsingService = parsingService; + } + + public LocalEpisode Aggregate(LocalEpisode localEpisode, bool otherFiles) + { + var bestEpisodeInfoForEpisodes = GetBestEpisodeInfo(localEpisode, otherFiles); + + localEpisode.Episodes = _parsingService.GetEpisodes(bestEpisodeInfoForEpisodes, localEpisode.Series, localEpisode.SceneSource); + + return localEpisode; + } + + private ParsedEpisodeInfo GetBestEpisodeInfo(LocalEpisode localEpisode, bool otherFiles) + { + var parsedEpisodeInfo = localEpisode.FileEpisodeInfo; + var downloadClientEpisodeInfo = localEpisode.DownloadClientEpisodeInfo; + var folderEpisodeInfo = localEpisode.FolderEpisodeInfo; + + if (!otherFiles && !SceneChecker.IsSceneTitle(Path.GetFileNameWithoutExtension(localEpisode.Path))) + { + if (downloadClientEpisodeInfo != null && !downloadClientEpisodeInfo.FullSeason) + { + parsedEpisodeInfo = localEpisode.DownloadClientEpisodeInfo; + } + else if (folderEpisodeInfo != null && !folderEpisodeInfo.FullSeason) + { + parsedEpisodeInfo = localEpisode.FolderEpisodeInfo; + } + } + + if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode) + { + var title = Path.GetFileNameWithoutExtension(localEpisode.Path); + var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, title, localEpisode.Series); + + return specialEpisodeInfo; + } + + return parsedEpisodeInfo; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQuality.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQuality.cs new file mode 100644 index 000000000..c4cf27b5e --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateQuality.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + public class AggregateQuality : IAggregateLocalEpisode + { + private readonly IEnumerable _augmentQualities; + private readonly Logger _logger; + + public AggregateQuality(IEnumerable augmentQualities, + Logger logger) + { + _augmentQualities = augmentQualities; + _logger = logger; + } + + public LocalEpisode Aggregate(LocalEpisode localEpisode, bool otherFiles) + { + var augmentedQualities = _augmentQualities.Select(a => a.AugmentQuality(localEpisode)) + .Where(a => a != null) + .OrderBy(a => a.SourceConfidence); + + var source = QualitySource.Unknown; + var sourceConfidence = Confidence.Default; + var resolution = 0; + var resolutionConfidence = Confidence.Default; + var revison = new Revision(); + + foreach (var augmentedQuality in augmentedQualities) + { + if (augmentedQuality.Source > source || + augmentedQuality.SourceConfidence > sourceConfidence && augmentedQuality.Source != QualitySource.Unknown) + { + source = augmentedQuality.Source; + sourceConfidence = augmentedQuality.SourceConfidence; + } + + if (augmentedQuality.Resolution > resolution || + augmentedQuality.ResolutionConfidence > resolutionConfidence && augmentedQuality.Resolution > 0) + { + resolution = augmentedQuality.Resolution; + resolutionConfidence = augmentedQuality.ResolutionConfidence; + } + + if (augmentedQuality.Revision != null && augmentedQuality.Revision > revison) + { + revison = augmentedQuality.Revision; + } + } + + var quality = new QualityModel(Quality.FindBySourceAndResolution(source, resolution), revison); + + if (resolutionConfidence == Confidence.MediaInfo) + { + quality.QualityDetectionSource = QualityDetectionSource.MediaInfo; + } + else if (sourceConfidence == Confidence.Fallback || resolutionConfidence == Confidence.Fallback) + { + quality.QualityDetectionSource = QualityDetectionSource.Extension; + } + else + { + quality.QualityDetectionSource = QualityDetectionSource.Name; + } + + _logger.Debug("Using quality: {0}", quality); + + localEpisode.Quality = quality; + + return localEpisode; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseGroup.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseGroup.cs new file mode 100644 index 000000000..a919320b8 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseGroup.cs @@ -0,0 +1,27 @@ +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + public class AggregateReleaseGroup : IAggregateLocalEpisode + { + public LocalEpisode Aggregate(LocalEpisode localEpisode, bool otherFiles) + { + var releaseGroup = localEpisode.DownloadClientEpisodeInfo?.ReleaseGroup; + + if (releaseGroup.IsNullOrWhiteSpace()) + { + releaseGroup = localEpisode.FolderEpisodeInfo?.ReleaseGroup; + } + + if (releaseGroup.IsNullOrWhiteSpace()) + { + releaseGroup = localEpisode.FileEpisodeInfo?.ReleaseGroup; + } + + localEpisode.ReleaseGroup = releaseGroup; + + return localEpisode; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromDownloadClientItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromDownloadClientItem.cs new file mode 100644 index 000000000..13da77969 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromDownloadClientItem.cs @@ -0,0 +1,23 @@ +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality +{ + public class AugmentQualityFromDownloadClientItem : IAugmentQuality + { + public AugmentQualityResult AugmentQuality(LocalEpisode localEpisode) + { + var quality = localEpisode.DownloadClientEpisodeInfo?.Quality; + + if (quality == null) + { + return null; + } + + return new AugmentQualityResult(quality.Quality.Source, + Confidence.Tag, + quality.Quality.Resolution, + Confidence.Tag, + quality.Revision); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromFileName.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromFileName.cs new file mode 100644 index 000000000..53475b5bb --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromFileName.cs @@ -0,0 +1,28 @@ +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality +{ + public class AugmentQualityFromFileName : IAugmentQuality + { + public AugmentQualityResult AugmentQuality(LocalEpisode localEpisode) + { + var quality = localEpisode.FileEpisodeInfo?.Quality; + + if (quality == null) + { + return null; + } + + var confidence = quality.QualityDetectionSource == QualityDetectionSource.Extension + ? Confidence.Fallback + : Confidence.Tag; + + return new AugmentQualityResult(quality.Quality.Source, + confidence, + quality.Quality.Resolution, + confidence, + quality.Revision); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromFolder.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromFolder.cs new file mode 100644 index 000000000..e59f3dbdc --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromFolder.cs @@ -0,0 +1,23 @@ +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality +{ + public class AugmentQualityFromFolder : IAugmentQuality + { + public AugmentQualityResult AugmentQuality(LocalEpisode localEpisode) + { + var quality = localEpisode.FolderEpisodeInfo?.Quality; + + if (quality == null) + { + return null; + } + + return new AugmentQualityResult(quality.Quality.Source, + Confidence.Tag, + quality.Quality.Resolution, + Confidence.Tag, + quality.Revision); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfo.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfo.cs new file mode 100644 index 000000000..958e226f8 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityFromMediaInfo.cs @@ -0,0 +1,39 @@ +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality +{ + public class AugmentQualityFromMediaInfo : IAugmentQuality + { + public AugmentQualityResult AugmentQuality(LocalEpisode localEpisode) + { + if (localEpisode.MediaInfo == null) + { + return null; + } + + var width = localEpisode.MediaInfo.Width; + + if (width >= 3200) + { + return AugmentQualityResult.ResolutionOnly(2160, Confidence.MediaInfo); + } + + if (width >= 1800) + { + return AugmentQualityResult.ResolutionOnly(1080, Confidence.MediaInfo); + } + + if (width >= 1200) + { + return AugmentQualityResult.ResolutionOnly(720, Confidence.MediaInfo); + } + + if (width > 0) + { + return AugmentQualityResult.ResolutionOnly(480, Confidence.MediaInfo); + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityResult.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityResult.cs new file mode 100644 index 000000000..885d90cca --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/AugmentQualityResult.cs @@ -0,0 +1,36 @@ +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality +{ + public class AugmentQualityResult + { + public QualitySource Source { get; set; } + public Confidence SourceConfidence { get; set; } + public int Resolution { get; set; } + public Confidence ResolutionConfidence { get; set; } + public Revision Revision { get; set; } + + public AugmentQualityResult(QualitySource source, + Confidence sourceConfidence, + int resolution, + Confidence resolutionConfidence, + Revision revision) + { + Source = source; + SourceConfidence = sourceConfidence; + Resolution = resolution; + ResolutionConfidence = resolutionConfidence; + Revision = revision; + } + + public static AugmentQualityResult SourceOnly(QualitySource source, Confidence sourceConfidence) + { + return new AugmentQualityResult(source, sourceConfidence, 0, Confidence.Default, null); + } + + public static AugmentQualityResult ResolutionOnly(int resolution, Confidence resolutionConfidence) + { + return new AugmentQualityResult(QualitySource.Unknown, Confidence.Default, resolution, resolutionConfidence, null); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/Confidence.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/Confidence.cs new file mode 100644 index 000000000..2bb3f281f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/Confidence.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality +{ + public enum Confidence + { + Fallback, + Default, + Tag, + MediaInfo + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/IAugmentQuality.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/IAugmentQuality.cs new file mode 100644 index 000000000..6bf464321 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/Augmenters/Quality/IAugmentQuality.cs @@ -0,0 +1,9 @@ +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality +{ + public interface IAugmentQuality + { + AugmentQualityResult AugmentQuality(LocalEpisode localEpisode); + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/IAggregateLocalEpisode.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/IAggregateLocalEpisode.cs new file mode 100644 index 000000000..452cac2a8 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/IAggregateLocalEpisode.cs @@ -0,0 +1,9 @@ +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + public interface IAggregateLocalEpisode + { + LocalEpisode Aggregate(LocalEpisode localEpisode, bool otherFiles); + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs index 9c5e185b0..b3e45c183 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs @@ -44,6 +44,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return DetectSampleResult.NotSample; } + // TODO: Use MediaInfo from the import process, no need to re-process the file again here var runTime = _videoFileInfoReader.GetRunTime(path); if (!runTime.HasValue) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 2a50ff2bf..c894daaab 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -83,7 +83,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.MediaInfo = localEpisode.MediaInfo; episodeFile.SeasonNumber = localEpisode.SeasonNumber; episodeFile.Episodes = localEpisode.Episodes; - episodeFile.ReleaseGroup = localEpisode.ParsedEpisodeInfo.ReleaseGroup; + episodeFile.ReleaseGroup = localEpisode.ReleaseGroup; bool copyOnly; switch (importMode) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 642384275..7d066bf31 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -7,12 +7,9 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; -using NzbDrone.Core.Parser; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; -using NzbDrone.Core.MediaFiles.MediaInfo; - namespace NzbDrone.Core.MediaFiles.EpisodeImport { @@ -25,26 +22,23 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public class ImportDecisionMaker : IMakeImportDecision { private readonly IEnumerable _specifications; - private readonly IParsingService _parsingService; private readonly IMediaFileService _mediaFileService; + private readonly IAugmentingService _augmentingService; private readonly IDiskProvider _diskProvider; - private readonly IVideoFileInfoReader _videoFileInfoReader; private readonly IDetectSample _detectSample; private readonly Logger _logger; public ImportDecisionMaker(IEnumerable specifications, - IParsingService parsingService, IMediaFileService mediaFileService, + IAugmentingService augmentingService, IDiskProvider diskProvider, - IVideoFileInfoReader videoFileInfoReader, IDetectSample detectSample, Logger logger) { _specifications = specifications; - _parsingService = parsingService; _mediaFileService = mediaFileService; + _augmentingService = augmentingService; _diskProvider = diskProvider; - _videoFileInfoReader = videoFileInfoReader; _detectSample = detectSample; _logger = logger; } @@ -60,78 +54,81 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count()); - var shouldUseFolderName = ShouldUseFolderName(videoFiles, series, folderInfo); + ParsedEpisodeInfo downloadClientItemInfo = null; + + if (downloadClientItem != null) + { + downloadClientItemInfo = Parser.Parser.ParseTitle(downloadClientItem.Title); + } + + var nonSampleVideoFileCount = GetNonSampleVideoFileCount(newFiles, series, downloadClientItemInfo, folderInfo); + var decisions = new List(); foreach (var file in newFiles) { - decisions.AddIfNotNull(GetDecision(file, series, downloadClientItem, folderInfo, sceneSource, shouldUseFolderName)); + var localEpisode = new LocalEpisode + { + Series = series, + DownloadClientEpisodeInfo = downloadClientItemInfo, + FolderEpisodeInfo = folderInfo, + Path = file, + SceneSource = sceneSource + }; + + decisions.AddIfNotNull(GetDecision(localEpisode, downloadClientItem, nonSampleVideoFileCount > 1)); } return decisions; } - private ImportDecision GetDecision(string file, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName) + private ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem, bool otherFiles) { ImportDecision decision = null; + var fileEpisodeInfo = Parser.Parser.ParsePath(localEpisode.Path); + + localEpisode.FileEpisodeInfo = fileEpisodeInfo; + localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path); + try { - var localEpisode = _parsingService.GetLocalEpisode(file, series, shouldUseFolderName ? folderInfo : null, sceneSource); + _augmentingService.Augment(localEpisode, otherFiles); - if (localEpisode != null) + if (localEpisode.Episodes.Empty()) { - localEpisode.Quality = GetQuality(folderInfo, localEpisode.Quality, series); - 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) + if (IsPartialSeason(localEpisode)) { - localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); + decision = new ImportDecision(localEpisode, new Rejection("Partial season packs are not supported")); } - - if (localEpisode.Episodes.Empty()) + else if (IsSeasonExtra(localEpisode)) { - if (localEpisode.ParsedEpisodeInfo.IsPartialSeason) - { - decision = new ImportDecision(localEpisode, new Rejection("Partial season packs are not supported")); - } - else if (localEpisode.ParsedEpisodeInfo.IsSeasonExtra) - { - decision = new ImportDecision(localEpisode, new Rejection("Extras are not supported")); - } - else - { - decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); - } + decision = new ImportDecision(localEpisode, new Rejection("Extras are not supported")); } else { - decision = GetDecision(localEpisode, downloadClientItem); + decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); } } - else { - localEpisode = new LocalEpisode(); - localEpisode.Path = file; - - decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); + decision = GetDecision(localEpisode, downloadClientItem); } } - catch (Exception e) + catch (AugmentingFailedException) { - _logger.Error(e, "Couldn't import file. {0}", file); + decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); + } + catch (Exception ex) + { + _logger.Error(ex, "Couldn't import file. {0}", localEpisode.Path); - var localEpisode = new LocalEpisode { Path = file }; decision = new ImportDecision(localEpisode, new Rejection("Unexpected error processing file")); } if (decision == null) { - _logger.Error("Unable to make a decision on {0}", file); + _logger.Error("Unable to make a decision on {0}", localEpisode.Path); } else if (decision.Rejections.Any()) { @@ -175,65 +172,66 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return null; } - private bool ShouldUseFolderName(List videoFiles, Series series, ParsedEpisodeInfo folderInfo) + private int GetNonSampleVideoFileCount(List videoFiles, Series series, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo) { - if (folderInfo == null) - { - return false; - } - - if (folderInfo.FullSeason) - { - return false; - } + var isPossibleSpecialEpisode = downloadClientItemInfo?.IsPossibleSpecialEpisode ?? false; + // If we might already have a special, don't try to get it from the folder info. + isPossibleSpecialEpisode = isPossibleSpecialEpisode || (folderInfo?.IsPossibleSpecialEpisode ?? false); return videoFiles.Count(file => { - var sample = _detectSample.IsSample(series, file, folderInfo.IsPossibleSpecialEpisode); + var sample = _detectSample.IsSample(series, file, isPossibleSpecialEpisode); if (sample == DetectSampleResult.Sample) { return false; } - if (SceneChecker.IsSceneTitle(Path.GetFileName(file))) - { - return false; - } - return true; - }) == 1; + }); } - private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) + private bool IsPartialSeason(LocalEpisode localEpisode) { - if (UseFolderQuality(folderInfo, fileQuality, series)) - { - _logger.Debug("Using quality from folder: {0}", folderInfo.Quality); - return folderInfo.Quality; - } + var downloadClientEpisodeInfo = localEpisode.DownloadClientEpisodeInfo; + var folderEpisodeInfo = localEpisode.FolderEpisodeInfo; + var fileEpisodeInfo = localEpisode.FileEpisodeInfo; - return fileQuality; - } - - private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) - { - if (folderInfo == null) - { - return false; - } - - if (folderInfo.Quality.Quality == Quality.Unknown) - { - return false; - } - - if (fileQuality.QualitySource == QualitySource.Extension) + if (downloadClientEpisodeInfo != null && downloadClientEpisodeInfo.IsPartialSeason) { return true; } - if (new QualityModelComparer(series.Profile).Compare(folderInfo.Quality, fileQuality) > 0) + if (folderEpisodeInfo != null && folderEpisodeInfo.IsPartialSeason) + { + return true; + } + + if (fileEpisodeInfo != null && fileEpisodeInfo.IsPartialSeason) + { + return true; + } + + return false; + } + + private bool IsSeasonExtra(LocalEpisode localEpisode) + { + var downloadClientEpisodeInfo = localEpisode.DownloadClientEpisodeInfo; + var folderEpisodeInfo = localEpisode.FolderEpisodeInfo; + var fileEpisodeInfo = localEpisode.FileEpisodeInfo; + + if (downloadClientEpisodeInfo != null && downloadClientEpisodeInfo.IsSeasonExtra) + { + return true; + } + + if (folderEpisodeInfo != null && folderEpisodeInfo.IsSeasonExtra) + { + return true; + } + + if (fileEpisodeInfo != null && fileEpisodeInfo.IsSeasonExtra) { return true; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 032497670..198333441 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -245,7 +245,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual var file = message.Files[i]; var series = _seriesService.GetSeries(file.SeriesId); var episodes = _episodeService.GetEpisodes(file.EpisodeIds); - var parsedEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo(); + var fileEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo(); var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path); var existingFile = series.Path.IsParentPath(file.Path); @@ -254,7 +254,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual ExistingFile = false, Episodes = episodes, MediaInfo = mediaInfo, - ParsedEpisodeInfo = parsedEpisodeInfo, + FileEpisodeInfo = fileEpisodeInfo, Path = file.Path, Quality = file.Quality, Series = series, diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs index d307121eb..37ecbbf40 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; @@ -16,7 +16,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { - if (localEpisode.ParsedEpisodeInfo.FullSeason) + if (localEpisode.FileEpisodeInfo.FullSeason) { _logger.Debug("Single episode file detected as containing all episodes in the season"); return Decision.Reject("Single episode file contains all episodes in seasons"); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs index 3ec975cff..c9e907a96 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; @@ -32,7 +32,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } - var folderInfo = Parser.Parser.ParseTitle(dirInfo.Name); + var folderInfo = localEpisode.FileEpisodeInfo; if (folderInfo != null && folderInfo.IsPossibleSceneSeasonSpecial) { @@ -54,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } - var unexpected = localEpisode.ParsedEpisodeInfo.EpisodeNumbers.Where(f => !folderInfo.EpisodeNumbers.Contains(f)).ToList(); + var unexpected = localEpisode.FileEpisodeInfo.EpisodeNumbers.Where(f => !folderInfo.EpisodeNumbers.Contains(f)).ToList(); if (unexpected.Any()) { diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index b4d6d6543..7a34341f6 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.IO; using NLog; @@ -36,6 +36,8 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo MediaInfo mediaInfo = null; + // TODO: Cache media info by path, mtime and length so we don't need to read files multiple times + try { mediaInfo = new MediaInfo(); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 97a008f01..007a7813f 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -763,7 +763,20 @@ + + + + + + + + + + + + + @@ -953,6 +966,7 @@ + diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index d59e55958..94a0ee5cd 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -16,12 +16,16 @@ namespace NzbDrone.Core.Parser.Model public string Path { get; set; } public long Size { get; set; } - public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } + public ParsedEpisodeInfo FileEpisodeInfo { get; set; } + public ParsedEpisodeInfo DownloadClientEpisodeInfo { get; set; } + public ParsedEpisodeInfo FolderEpisodeInfo { get; set; } public Series Series { get; set; } public List Episodes { get; set; } public QualityModel Quality { get; set; } public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } + public bool SceneSource { get; set; } + public string ReleaseGroup { get; set; } public int SeasonNumber { diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index cbd38784b..66cf00e7c 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; -using System.IO; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -13,13 +11,12 @@ namespace NzbDrone.Core.Parser { public interface IParsingService { - LocalEpisode GetLocalEpisode(string filename, Series series); - LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); Series GetSeries(string title); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable episodeIds); List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); + ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, Series series); } public class ParsingService : IParsingService @@ -40,60 +37,6 @@ namespace NzbDrone.Core.Parser _logger = logger; } - public LocalEpisode GetLocalEpisode(string filename, Series series) - { - 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) - { - var title = Path.GetFileNameWithoutExtension(filename); - var specialEpisodeInfo = ParseSpecialEpisodeTitle(parsedEpisodeInfo, title, series); - - if (specialEpisodeInfo != null) - { - parsedEpisodeInfo = specialEpisodeInfo; - } - } - - if (parsedEpisodeInfo == null) - { - if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(filename))) - { - _logger.Warn("Unable to parse episode info from path {0}", filename); - } - - return null; - } - - var episodes = GetEpisodes(parsedEpisodeInfo, series, sceneSource); - - return new LocalEpisode - { - Series = series, - Quality = parsedEpisodeInfo.Quality, - Episodes = episodes, - Path = filename, - ParsedEpisodeInfo = parsedEpisodeInfo, - ExistingFile = series.Path.IsParentPath(filename) - }; - } - public Series GetSeries(string title) { var parsedEpisodeInfo = Parser.ParseTitle(title); @@ -225,7 +168,7 @@ namespace NzbDrone.Core.Parser return ParseSpecialEpisodeTitle(parsedEpisodeInfo, releaseTitle, series); } - private ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, Series series) + public ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, Series series) { // SxxE00 episodes are sometimes mapped via TheXEM, don't use episode title parsing in that case. if (parsedEpisodeInfo != null && parsedEpisodeInfo.IsPossibleSceneSeasonSpecial && series.UseSceneNumbering) diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 66a485910..c9f34b506 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -66,7 +66,7 @@ namespace NzbDrone.Core.Parser try { result.Quality = MediaFileExtensions.GetQualityForExtension(Path.GetExtension(name)); - result.QualitySource = QualitySource.Extension; + result.QualityDetectionSource = QualityDetectionSource.Extension; } catch (ArgumentException) { diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index d41a05d35..fedbd0b34 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore; @@ -9,15 +9,19 @@ namespace NzbDrone.Core.Qualities { public int Id { get; set; } public string Name { get; set; } + public QualitySource Source { get; set; } + public int Resolution { get; set; } public Quality() { } - private Quality(int id, string name) + private Quality(int id, string name, QualitySource source, int resolution) { Id = id; Name = name; + Source = source; + Resolution = resolution; } public override string ToString() @@ -55,26 +59,26 @@ namespace NzbDrone.Core.Qualities return !Equals(left, right); } - public static Quality Unknown => new Quality(0, "Unknown"); - public static Quality SDTV => new Quality(1, "SDTV"); - public static Quality DVD => new Quality(2, "DVD"); - public static Quality WEBDL1080p => new Quality(3, "WEBDL-1080p"); - public static Quality HDTV720p => new Quality(4, "HDTV-720p"); - public static Quality WEBDL720p => new Quality(5, "WEBDL-720p"); - public static Quality Bluray720p => new Quality(6, "Bluray-720p"); - public static Quality Bluray1080p => new Quality(7, "Bluray-1080p"); - public static Quality WEBDL480p => new Quality(8, "WEBDL-480p"); - public static Quality HDTV1080p => new Quality(9, "HDTV-1080p"); - public static Quality RAWHD => new Quality(10, "Raw-HD"); - //public static Quality HDTV480p { get { return new Quality(11, "HDTV-480p"); } } - //public static Quality WEBRip480p { get { return new Quality(12, "WEBRip-480p"); } } - //public static Quality Bluray480p { get { return new Quality(13, "Bluray-480p"); } } - //public static Quality WEBRip720p { get { return new Quality(14, "WEBRip-720p"); } } - //public static Quality WEBRip1080p { get { return new Quality(15, "WEBRip-1080p"); } } - public static Quality HDTV2160p => new Quality(16, "HDTV-2160p"); - //public static Quality WEBRip2160p { get { return new Quality(17, "WEBRip-2160p"); } } - public static Quality WEBDL2160p => new Quality(18, "WEBDL-2160p"); - public static Quality Bluray2160p => new Quality(19, "Bluray-2160p"); + public static Quality Unknown => new Quality(0, "Unknown", QualitySource.Unknown, 0); + public static Quality SDTV => new Quality(1, "SDTV", QualitySource.Television, 480); + public static Quality DVD => new Quality(2, "DVD", QualitySource.DVD, 480); + public static Quality WEBDL1080p => new Quality(3, "WEBDL-1080p", QualitySource.Web, 1080); + public static Quality HDTV720p => new Quality(4, "HDTV-720p", QualitySource.Television, 720); + public static Quality WEBDL720p => new Quality(5, "WEBDL-720p", QualitySource.Web, 720); + public static Quality Bluray720p => new Quality(6, "Bluray-720p", QualitySource.Bluray, 720); + public static Quality Bluray1080p => new Quality(7, "Bluray-1080p", QualitySource.Bluray, 1080); + public static Quality WEBDL480p => new Quality(8, "WEBDL-480p", QualitySource.Web, 480); + public static Quality HDTV1080p => new Quality(9, "HDTV-1080p", QualitySource.Television, 1080); + public static Quality RAWHD => new Quality(10, "Raw-HD", QualitySource.TelevisionRaw, 1080); + //public static Quality HDTV480p { get { return new Quality(11, "HDTV-480p", QualitySource.Television, 480); } } + //public static Quality WEBRip480p { get { return new Quality(12, "WEBRip-480p", QualitySource.WebRip, 480); } } + //public static Quality Bluray480p { get { return new Quality(13, "Bluray-480p", QualitySource.Bluray, 480); } } + //public static Quality WEBRip720p { get { return new Quality(14, "WEBRip-720p", QualitySource.WebRip, 720); } } + //public static Quality WEBRip1080p { get { return new Quality(15, "WEBRip-1080p", QualitySource.WebRip, 1080); } } + public static Quality HDTV2160p => new Quality(16, "HDTV-2160p", QualitySource.Television, 2160); + //public static Quality WEBRip2160p { get { return new Quality(17, "WEBRip-2160p", QualitySource.WebRip, 2160); } } + public static Quality WEBDL2160p => new Quality(18, "WEBDL-2160p", QualitySource.Web, 2160); + public static Quality Bluray2160p => new Quality(19, "Bluray-2160p", QualitySource.Bluray, 2160); static Quality() { @@ -148,5 +152,10 @@ namespace NzbDrone.Core.Qualities { return quality.Id; } + + public static Quality FindBySourceAndResolution(QualitySource source, int resolution) + { + return All.SingleOrDefault(q => q.Source == source && q.Resolution == resolution); + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Qualities/QualityDetectionSource.cs b/src/NzbDrone.Core/Qualities/QualityDetectionSource.cs new file mode 100644 index 000000000..3f7695214 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityDetectionSource.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Qualities +{ + public enum QualityDetectionSource + { + Name, + Extension, + MediaInfo + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs index a483d22c2..b4954207a 100644 --- a/src/NzbDrone.Core/Qualities/QualityModel.cs +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using Newtonsoft.Json; using NzbDrone.Core.Datastore; @@ -10,8 +10,8 @@ namespace NzbDrone.Core.Qualities public Revision Revision { get; set; } [JsonIgnore] - public QualitySource QualitySource { get; set; } - + public QualityDetectionSource QualityDetectionSource { get; set; } + public QualityModel() : this(Quality.Unknown, new Revision()) { diff --git a/src/NzbDrone.Core/Qualities/QualitySource.cs b/src/NzbDrone.Core/Qualities/QualitySource.cs index 5c0c2c81f..258de8813 100644 --- a/src/NzbDrone.Core/Qualities/QualitySource.cs +++ b/src/NzbDrone.Core/Qualities/QualitySource.cs @@ -1,9 +1,13 @@ -namespace NzbDrone.Core.Qualities +namespace NzbDrone.Core.Qualities { public enum QualitySource { - Name, - Extension, - MediaInfo + Unknown, + Television, + TelevisionRaw, + Web, + WebRip, + DVD, + Bluray } }