diff --git a/NzbDrone.Core.Test/EpisodeProviderTest.cs b/NzbDrone.Core.Test/EpisodeProviderTest.cs index 9ca954590..75c99977d 100644 --- a/NzbDrone.Core.Test/EpisodeProviderTest.cs +++ b/NzbDrone.Core.Test/EpisodeProviderTest.cs @@ -1355,5 +1355,71 @@ namespace NzbDrone.Core.Test episodes.First().ShouldHave().AllPropertiesBut(e => e.Series).EqualTo(fakeEpisode); mocker.VerifyAllMocks(); } + + [Test] + public void IsFirstOrLastEpisodeInSeason_false() + { + var db = MockLib.GetEmptyDatabase(); + var mocker = new AutoMoqer(); + mocker.SetConstant(db); + + var fakeEpisodes = Builder.CreateListOfSize(10) + .WhereAll() + .Have(c => c.SeriesId = 10) + .Have(c => c.SeasonNumber = 1) + .Build(); + + db.InsertMany(fakeEpisodes); + + //Act + var result = mocker.Resolve().IsFirstOrLastEpisodeOfSeason(10, 1, 5); + + //Assert + result.Should().BeFalse(); + } + + [Test] + public void IsFirstOrLastEpisodeInSeason_true_first() + { + var db = MockLib.GetEmptyDatabase(); + var mocker = new AutoMoqer(); + mocker.SetConstant(db); + + var fakeEpisodes = Builder.CreateListOfSize(10) + .WhereAll() + .Have(c => c.SeriesId = 10) + .Have(c => c.SeasonNumber = 1) + .Build(); + + db.InsertMany(fakeEpisodes); + + //Act + var result = mocker.Resolve().IsFirstOrLastEpisodeOfSeason(10, 1, 1); + + //Assert + result.Should().BeFalse(); + } + + [Test] + public void IsFirstOrLastEpisodeInSeason_true_last() + { + var db = MockLib.GetEmptyDatabase(); + var mocker = new AutoMoqer(); + mocker.SetConstant(db); + + var fakeEpisodes = Builder.CreateListOfSize(10) + .WhereAll() + .Have(c => c.SeriesId = 10) + .Have(c => c.SeasonNumber = 1) + .Build(); + + db.InsertMany(fakeEpisodes); + + //Act + var result = mocker.Resolve().IsFirstOrLastEpisodeOfSeason(10, 1, 10); + + //Assert + result.Should().BeFalse(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/InventoryProvider_IsAcceptableSizeTest.cs b/NzbDrone.Core.Test/InventoryProvider_IsAcceptableSizeTest.cs new file mode 100644 index 000000000..75a3b5272 --- /dev/null +++ b/NzbDrone.Core.Test/InventoryProvider_IsAcceptableSizeTest.cs @@ -0,0 +1,323 @@ +// ReSharper disable RedundantUsingDirective +using System; +using System.Collections.Generic; +using AutoMoq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Quality; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test +{ + [TestFixture] + // ReSharper disable InconsistentNaming + public class InventoryProvider_IsAcceptableSizeTest : TestBase + { + private EpisodeParseResult parseResultMulti; + private EpisodeParseResult parseResultSingle; + private Series series30minutes; + private Series series60minutes; + private QualityType qualityType; + + [SetUp] + public new void Setup() + { + parseResultMulti = new EpisodeParseResult + { + CleanTitle = "Title", + Language = LanguageType.English, + Quality = new Quality(QualityTypes.SDTV, true), + EpisodeNumbers = new List { 3, 4 }, + SeasonNumber = 12, + AirDate = DateTime.Now.AddDays(-12).Date + }; + + parseResultSingle = new EpisodeParseResult + { + CleanTitle = "Title", + Language = LanguageType.English, + Quality = new Quality(QualityTypes.SDTV, true), + EpisodeNumbers = new List { 3 }, + SeasonNumber = 12, + AirDate = DateTime.Now.AddDays(-12).Date + }; + + series30minutes = Builder.CreateNew() + .With(c => c.Monitored = true) + .With(d => d.CleanTitle = parseResultMulti.CleanTitle) + .With(c => c.Runtime = 30) + .Build(); + + series60minutes = Builder.CreateNew() + .With(c => c.Monitored = true) + .With(d => d.CleanTitle = parseResultMulti.CleanTitle) + .With(c => c.Runtime = 60) + .Build(); + + qualityType = Builder.CreateNew() + .With(q => q.MinSize = 0) + .With(q => q.MaxSize = 314572800) + .With(q => q.QualityTypeId = 1) + .Build(); + + base.Setup(); + } + + [Test] + public void IsAcceptableSize_true_single_episode_not_first_or_last_30_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultSingle.Series = series30minutes; + parseResultSingle.Size = 184572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultSingle); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void IsAcceptableSize_true_single_episode_not_first_or_last_60_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultSingle.Series = series60minutes; + parseResultSingle.Size = 368572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultSingle); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void IsAcceptableSize_false_single_episode_not_first_or_last_30_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultSingle.Series = series30minutes; + parseResultSingle.Size = 1184572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultSingle); + + //Assert + result.Should().BeFalse(); + } + + [Test] + public void IsAcceptableSize_false_single_episode_not_first_or_last_60_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultSingle.Series = series60minutes; + parseResultSingle.Size = 1368572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultSingle); + + //Assert + result.Should().BeFalse(); + } + + [Test] + public void IsAcceptableSize_true_multi_episode_not_first_or_last_30_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultMulti.Series = series30minutes; + parseResultMulti.Size = 184572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultMulti); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void IsAcceptableSize_true_multi_episode_not_first_or_last_60_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultMulti.Series = series60minutes; + parseResultMulti.Size = 368572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultMulti); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void IsAcceptableSize_false_multi_episode_not_first_or_last_30_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultMulti.Series = series30minutes; + parseResultMulti.Size = 1184572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultMulti); + + //Assert + result.Should().BeFalse(); + } + + [Test] + public void IsAcceptableSize_false_multi_episode_not_first_or_last_60_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultMulti.Series = series60minutes; + parseResultMulti.Size = 1368572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultMulti); + + //Assert + result.Should().BeFalse(); + } + + [Test] + public void IsAcceptableSize_true_single_episode_first_30_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultSingle.Series = series30minutes; + parseResultSingle.Size = 184572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(true); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultSingle); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void IsAcceptableSize_true_single_episode_first_60_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultSingle.Series = series60minutes; + parseResultSingle.Size = 368572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(true); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultSingle); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void IsAcceptableSize_false_single_episode_first_30_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultSingle.Series = series30minutes; + parseResultSingle.Size = 1184572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(true); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultSingle); + + //Assert + result.Should().BeFalse(); + } + + [Test] + public void IsAcceptableSize_false_single_episode_first_60_minute() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + parseResultSingle.Series = series60minutes; + parseResultSingle.Size = 1368572800; + + mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(true); + + //Act + bool result = mocker.Resolve().IsAcceptableSize(parseResultSingle); + + //Assert + result.Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core.Test/InventoryProvider_QualityNeededTest.cs b/NzbDrone.Core.Test/InventoryProvider_QualityNeededTest.cs index 455c71e33..d2202bc2a 100644 --- a/NzbDrone.Core.Test/InventoryProvider_QualityNeededTest.cs +++ b/NzbDrone.Core.Test/InventoryProvider_QualityNeededTest.cs @@ -135,6 +135,10 @@ namespace NzbDrone.Core.Test .Setup(p => p.GetEpisodesByParseResult(parseResultMulti, true)) .Returns(new List { episode, episode2 }); + mocker.GetMock() + .Setup(s => s.Get(It.IsAny())) + .Returns(new QualityType { MaxSize = 10.Gigabytes(), MinSize = 0 }); + episode.EpisodeFile.Quality = QualityTypes.Bluray720p; //Act @@ -145,7 +149,6 @@ namespace NzbDrone.Core.Test mocker.VerifyAllMocks(); } - [Test] public void IsQualityNeeded_file_in_history_should_be_skipped() { @@ -162,6 +165,14 @@ namespace NzbDrone.Core.Test .Setup(p => p.GetEpisodesByParseResult(parseResultSingle, true)) .Returns(new List { episode }); + mocker.GetMock() + .Setup(p => p.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + mocker.GetMock() + .Setup(s => s.Get(It.IsAny())) + .Returns(new QualityType { MaxSize = 10.Gigabytes(), MinSize = 0 }); + episode.EpisodeFile.Quality = QualityTypes.SDTV; //Act @@ -188,6 +199,14 @@ namespace NzbDrone.Core.Test .Setup(p => p.GetEpisodesByParseResult(parseResultSingle, true)) .Returns(new List { episode }); + mocker.GetMock() + .Setup(p => p.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + mocker.GetMock() + .Setup(s => s.Get(It.IsAny())) + .Returns(new QualityType { MaxSize = 10.Gigabytes(), MinSize = 0 }); + episode.EpisodeFile.Quality = QualityTypes.SDTV; //Act @@ -210,11 +229,18 @@ namespace NzbDrone.Core.Test .Setup(p => p.GetBestQualityInHistory(episode.EpisodeId)) .Returns(null); - mocker.GetMock() .Setup(p => p.GetEpisodesByParseResult(parseResultSingle, true)) .Returns(new List { episode }); + mocker.GetMock() + .Setup(p => p.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + + mocker.GetMock() + .Setup(s => s.Get(It.IsAny())) + .Returns(new QualityType { MaxSize = 10.Gigabytes(), MinSize = 0 }); + episode.EpisodeFile.Quality = QualityTypes.SDTV; //Act bool result = mocker.Resolve().IsQualityNeeded(parseResultSingle); diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 5fab03992..859d80822 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -90,6 +90,7 @@ + diff --git a/NzbDrone.Core/Providers/EpisodeProvider.cs b/NzbDrone.Core/Providers/EpisodeProvider.cs index af2cdda2a..aac994441 100644 --- a/NzbDrone.Core/Providers/EpisodeProvider.cs +++ b/NzbDrone.Core/Providers/EpisodeProvider.cs @@ -330,6 +330,21 @@ namespace NzbDrone.Core.Providers Logger.Info("Ignore flag for Episode:{0} successfully set to {1}", episodeId, isIgnored); } + public virtual bool IsFirstOrLastEpisodeOfSeason(int seriesId, int seasonNumber, int episodeNumber) + { + var episodes = GetEpisodesBySeason(seriesId, seasonNumber).OrderBy(e => e.EpisodeNumber); + + if (episodes.Count() == 0) + return false; + + //Ensure that this is either the first episode + //or is the last episode in a season that has 10 or more episodes + if (episodes.First().EpisodeNumber == episodeNumber || (episodes.Count() >= 10 && episodes.Last().EpisodeNumber == episodeNumber)) + return true; + + return false; + } + public IList AttachSeries(IList episodes) { if (episodes.Count == 0) return episodes; diff --git a/NzbDrone.Core/Providers/InventoryProvider.cs b/NzbDrone.Core/Providers/InventoryProvider.cs index fd9ada11b..6544831f2 100644 --- a/NzbDrone.Core/Providers/InventoryProvider.cs +++ b/NzbDrone.Core/Providers/InventoryProvider.cs @@ -14,15 +14,18 @@ namespace NzbDrone.Core.Providers private readonly SeriesProvider _seriesProvider; private readonly EpisodeProvider _episodeProvider; private readonly HistoryProvider _historyProvider; + private readonly QualityTypeProvider _qualityTypeProvider; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); [Inject] - public InventoryProvider(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, HistoryProvider historyProvider) + public InventoryProvider(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, + HistoryProvider historyProvider, QualityTypeProvider qualityTypeProvider) { _seriesProvider = seriesProvider; _episodeProvider = episodeProvider; _historyProvider = historyProvider; + _qualityTypeProvider = qualityTypeProvider; } public InventoryProvider() @@ -79,6 +82,9 @@ namespace NzbDrone.Core.Providers var cutoff = parsedReport.Series.QualityProfile.Cutoff; + if (!IsAcceptableSize(parsedReport)) + return false; + foreach (var episode in _episodeProvider.GetEpisodesByParseResult(parsedReport, true)) { //Checking File @@ -132,5 +138,39 @@ namespace NzbDrone.Core.Providers Logger.Debug("New item has better quality than existing item"); return true; } + + public virtual bool IsAcceptableSize(EpisodeParseResult parseResult) + { + var qualityType = _qualityTypeProvider.Get((int) parseResult.Quality.QualityType); + + //Need to determine if this is a 30 or 60 minute episode + //Is it a multi-episode release? + //Is it the first or last series of a season? + + var maxSize = qualityType.MaxSize; + var series = parseResult.Series; + + //If this is an hour long episode (between 50 and 65 minutes) then multiply by 2 + if (series.Runtime >= 50 && series.Runtime <= 65) + maxSize = maxSize * 2; + + //Multiply maxSize by the number of episodes parsed + maxSize = maxSize * parseResult.EpisodeNumbers.Count; + + //Check if there was only one episode parsed + //and it is the first or last episode of the season + if (parseResult.EpisodeNumbers.Count == 1 && + _episodeProvider.IsFirstOrLastEpisodeOfSeason(series.SeriesId, + parseResult.SeasonNumber, parseResult.EpisodeNumbers[0])) + { + maxSize = maxSize * 2; + } + + //If the parsed size is greater than maxSize we don't want it + if (parseResult.Size > maxSize) + return false; + + return true; + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Providers/QualityTypeProvider.cs b/NzbDrone.Core/Providers/QualityTypeProvider.cs index ae746723d..79c28354d 100644 --- a/NzbDrone.Core/Providers/QualityTypeProvider.cs +++ b/NzbDrone.Core/Providers/QualityTypeProvider.cs @@ -19,6 +19,11 @@ namespace NzbDrone.Core.Providers _database = database; } + public QualityTypeProvider() + { + + } + public virtual void Update(QualityType qualityType) { _database.Update(qualityType);