New: Use 45 minutes for runtime when episode aired within 24 hours of pilot episode

This commit is contained in:
Mark McDowall 2022-04-28 23:55:07 -07:00
parent 9fb29f42c4
commit cc9fc1e3c3
2 changed files with 181 additions and 58 deletions

View File

@ -20,38 +20,50 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
private RemoteEpisode parseResultMulti; private RemoteEpisode parseResultMulti;
private RemoteEpisode parseResultSingle; private RemoteEpisode parseResultSingle;
private Series series; private Series series;
private List<Episode> episodes;
private QualityDefinition qualityType; private QualityDefinition qualityType;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
series = Builder<Series>.CreateNew() series = Builder<Series>.CreateNew()
.Build(); .With(s => s.Seasons = Builder<Season>.CreateListOfSize(2).Build().ToList())
.Build();
episodes = Builder<Episode>.CreateListOfSize(10)
.All()
.With(s => s.SeasonNumber = 1)
.BuildList();
parseResultMultiSet = new RemoteEpisode parseResultMultiSet = new RemoteEpisode
{ {
Series = series, Series = series,
Release = new ReleaseInfo(), Release = new ReleaseInfo(),
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) }, ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
Episodes = new List<Episode> { new Episode(), new Episode(), new Episode(), new Episode(), new Episode(), new Episode() } Episodes = Builder<Episode>.CreateListOfSize(6).All().With(s => s.SeasonNumber = 1).BuildList()
}; };
parseResultMulti = new RemoteEpisode parseResultMulti = new RemoteEpisode
{ {
Series = series, Series = series,
Release = new ReleaseInfo(), Release = new ReleaseInfo(),
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) }, ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
Episodes = new List<Episode> { new Episode(), new Episode() } Episodes = Builder<Episode>.CreateListOfSize(2).All().With(s => s.SeasonNumber = 1).BuildList()
}; };
parseResultSingle = new RemoteEpisode parseResultSingle = new RemoteEpisode
{ {
Series = series, Series = series,
Release = new ReleaseInfo(), Release = new ReleaseInfo(),
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) }, ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
Episodes = new List<Episode> { new Episode() { Id = 2 } } Episodes = new List<Episode> {
Builder<Episode>.CreateNew()
.With(s => s.SeasonNumber = 1)
.With(s => s.EpisodeNumber = 1)
.Build()
}
}; };
Mocker.GetMock<IQualityDefinitionService>() Mocker.GetMock<IQualityDefinitionService>()
.Setup(v => v.Get(It.IsAny<Quality>())) .Setup(v => v.Get(It.IsAny<Quality>()))
@ -67,18 +79,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Mocker.GetMock<IEpisodeService>().Setup( Mocker.GetMock<IEpisodeService>().Setup(
s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>())) s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
.Returns(new List<Episode>() { .Returns(episodes);
new Episode(), new Episode(), new Episode(), new Episode(), new Episode(),
new Episode(), new Episode(), new Episode(), new Episode() { Id = 2 }, new Episode() });
}
private void GivenLastEpisode()
{
Mocker.GetMock<IEpisodeService>().Setup(
s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
.Returns(new List<Episode>() {
new Episode(), new Episode(), new Episode(), new Episode(), new Episode(),
new Episode(), new Episode(), new Episode(), new Episode(), new Episode() { Id = 2 } });
} }
[TestCase(30, 50, false)] [TestCase(30, 50, false)]
@ -92,6 +93,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
series.Runtime = runtime; series.Runtime = runtime;
parseResultSingle.Series = series; parseResultSingle.Series = series;
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes(); parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
parseResultSingle.Episodes.First().Id = 5;
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult); Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
} }
@ -100,13 +102,26 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[TestCase(30, 1000, false)] [TestCase(30, 1000, false)]
[TestCase(60, 1000, true)] [TestCase(60, 1000, true)]
[TestCase(60, 2000, false)] [TestCase(60, 2000, false)]
public void single_episode_first_or_last(int runtime, int sizeInMegaBytes, bool expectedResult) public void should_return_expected_result_for_first_episode_of_season(int runtime, int sizeInMegaBytes, bool expectedResult)
{ {
GivenLastEpisode();
series.Runtime = runtime; series.Runtime = runtime;
parseResultSingle.Series = series; parseResultSingle.Series = series;
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes(); parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
parseResultSingle.Episodes.First().Id = episodes.First().Id;
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
}
[TestCase(30, 500, true)]
[TestCase(30, 1000, false)]
[TestCase(60, 1000, true)]
[TestCase(60, 2000, false)]
public void should_return_expected_result_for_last_episode_of_season(int runtime, int sizeInMegaBytes, bool expectedResult)
{
series.Runtime = runtime;
parseResultSingle.Series = series;
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
parseResultSingle.Episodes.First().Id = episodes.Last().Id;
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult); Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
} }
@ -144,8 +159,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_return_true_if_size_is_zero() public void should_return_true_if_size_is_zero()
{ {
GivenLastEpisode();
series.Runtime = 30; series.Runtime = 30;
parseResultSingle.Series = series; parseResultSingle.Series = series;
parseResultSingle.Release.Size = 0; parseResultSingle.Release.Size = 0;
@ -158,8 +171,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_return_true_if_unlimited_30_minute() public void should_return_true_if_unlimited_30_minute()
{ {
GivenLastEpisode();
series.Runtime = 30; series.Runtime = 30;
parseResultSingle.Series = series; parseResultSingle.Series = series;
parseResultSingle.Release.Size = 18457280000; parseResultSingle.Release.Size = 18457280000;
@ -171,8 +182,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_return_true_if_unlimited_60_minute() public void should_return_true_if_unlimited_60_minute()
{ {
GivenLastEpisode();
series.Runtime = 60; series.Runtime = 60;
parseResultSingle.Series = series; parseResultSingle.Series = series;
parseResultSingle.Release.Size = 36857280000; parseResultSingle.Release.Size = 36857280000;
@ -184,8 +193,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_treat_daily_series_as_single_episode() public void should_treat_daily_series_as_single_episode()
{ {
GivenLastEpisode();
series.Runtime = 60; series.Runtime = 60;
parseResultSingle.Series = series; parseResultSingle.Series = series;
parseResultSingle.Series.SeriesType = SeriesTypes.Daily; parseResultSingle.Series.SeriesType = SeriesTypes.Daily;
@ -216,5 +223,94 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().BeTrue(); Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().BeTrue();
} }
[Test]
public void should_return_false_if_series_runtime_is_zero_and_single_episode_is_not_from_first_season()
{
series.Runtime = 0;
parseResultSingle.Series = series;
parseResultSingle.Episodes.First().Id = 5;
parseResultSingle.Release.Size = 200.Megabytes();
parseResultSingle.Episodes.First().SeasonNumber = 2;
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(false);
}
[Test]
public void should_return_false_if_series_runtime_is_zero_and_single_episode_aired_more_than_24_hours_after_first_aired_episode()
{
series.Runtime = 0;
parseResultSingle.Series = series;
parseResultSingle.Release.Size = 200.Megabytes();
parseResultSingle.Episodes.First().Id = 5;
parseResultSingle.Episodes.First().SeasonNumber = 1;
parseResultSingle.Episodes.First().EpisodeNumber = 2;
parseResultSingle.Episodes.First().AirDateUtc = episodes.First().AirDateUtc.Value.AddDays(7);
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(false);
}
[Test]
public void should_return_true_if_series_runtime_is_zero_and_single_episode_aired_less_than_24_hours_after_first_aired_episode()
{
series.Runtime = 0;
parseResultSingle.Series = series;
parseResultSingle.Release.Size = 200.Megabytes();
parseResultSingle.Episodes.First().Id = 5;
parseResultSingle.Episodes.First().SeasonNumber = 1;
parseResultSingle.Episodes.First().EpisodeNumber = 2;
parseResultSingle.Episodes.First().AirDateUtc = episodes.First().AirDateUtc.Value.AddHours(1);
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(true);
}
[Test]
public void should_return_false_if_series_runtime_is_zero_and_multi_episode_is_not_from_first_season()
{
series.Runtime = 0;
parseResultMulti.Series = series;
parseResultMulti.Release.Size = 200.Megabytes();
parseResultMulti.Episodes.ForEach(e => e.SeasonNumber = 2);
Subject.IsSatisfiedBy(parseResultMulti, null).Accepted.Should().Be(false);
}
[Test]
public void should_return_false_if_series_runtime_is_zero_and_multi_episode_aired_more_than_24_hours_after_first_aired_episode()
{
var airDateUtc = episodes.First().AirDateUtc.Value.AddDays(7);
series.Runtime = 0;
parseResultMulti.Series = series;
parseResultMulti.Release.Size = 200.Megabytes();
parseResultMulti.Episodes.ForEach(e =>
{
e.SeasonNumber = 1;
e.AirDateUtc = airDateUtc;
});
Subject.IsSatisfiedBy(parseResultMulti, null).Accepted.Should().Be(false);
}
[Test]
public void should_return_true_if_series_runtime_is_zero_and_multi_episode_aired_less_than_24_hours_after_first_aired_episode()
{
var airDateUtc = episodes.First().AirDateUtc.Value.AddHours(1);
series.Runtime = 0;
parseResultMulti.Series = series;
parseResultMulti.Release.Size = 200.Megabytes();
parseResultMulti.Episodes.ForEach(e =>
{
e.SeasonNumber = 1;
e.AirDateUtc = airDateUtc;
});
Subject.IsSatisfiedBy(parseResultMulti, null).Accepted.Should().Be(true);
}
} }
} }

View File

@ -1,3 +1,4 @@
using System;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -43,18 +44,47 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return Decision.Accept(); return Decision.Accept();
} }
var runtime = subject.Series.Runtime;
if (runtime == 0)
{
var firstSeasonNumber = subject.Series.Seasons.Where(s => s.SeasonNumber > 0).Min(s => s.SeasonNumber);
var pilotEpisode = _episodeService.GetEpisodesBySeason(subject.Series.Id, firstSeasonNumber).First();
if (subject.Episodes.First().SeasonNumber == pilotEpisode.SeasonNumber)
{
// If the first episode has an air date use it, otherwise use the release's publish date because like runtime it may not have updated yet.
var gracePeriodEnd = (pilotEpisode.AirDateUtc ?? subject.Release.PublishDate).AddHours(24);
// If episodes don't have an air date that is okay, otherwise make sure it's within 24 hours of the first episode airing.
if (subject.Episodes.All(e => !e.AirDateUtc.HasValue || e.AirDateUtc.Value.Before(gracePeriodEnd)))
{
_logger.Debug("Series runtime is 0, but all episodes in release aired within 24 hours of first episode in season, defaulting runtime to 45 minutes");
runtime = 45;
}
}
// Reject if the run time is still 0
if (runtime == 0)
{
_logger.Debug("Series runtime is 0, unable to validate size until it is available, rejecting");
return Decision.Reject("Series runtime is 0, unable to validate size until it is available");
}
}
var qualityDefinition = _qualityDefinitionService.Get(quality); var qualityDefinition = _qualityDefinitionService.Get(quality);
if (qualityDefinition.MinSize.HasValue) if (qualityDefinition.MinSize.HasValue)
{ {
var minSize = qualityDefinition.MinSize.Value.Megabytes(); var minSize = qualityDefinition.MinSize.Value.Megabytes();
//Multiply maxSize by Series.Runtime // Multiply maxSize by Series.Runtime
minSize = minSize * subject.Series.Runtime * subject.Episodes.Count; minSize = minSize * runtime * subject.Episodes.Count;
//If the parsed size is smaller than minSize we don't want it // If the parsed size is smaller than minSize we don't want it
if (subject.Release.Size < minSize) if (subject.Release.Size < minSize)
{ {
var runtimeMessage = subject.Episodes.Count == 1 ? $"{subject.Series.Runtime}min" : $"{subject.Episodes.Count}x {subject.Series.Runtime}min"; var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count}x {runtime}min";
_logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, minSize, runtimeMessage); _logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, minSize, runtimeMessage);
return Decision.Reject("{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage); return Decision.Reject("{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage);
@ -64,46 +94,31 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{ {
_logger.Debug("Max size is unlimited, skipping size check"); _logger.Debug("Max size is unlimited, skipping size check");
} }
else if (subject.Series.Runtime == 0)
{
_logger.Debug("Series runtime is 0, unable to validate size until it is available, rejecting");
return Decision.Reject("Series runtime is 0, unable to validate size until it is available");
}
else else
{ {
var maxSize = qualityDefinition.MaxSize.Value.Megabytes(); var maxSize = qualityDefinition.MaxSize.Value.Megabytes();
//Multiply maxSize by Series.Runtime // Multiply maxSize by Series.Runtime
maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count; maxSize = maxSize * runtime * subject.Episodes.Count;
if (subject.Episodes.Count == 1 && subject.Series.SeriesType == SeriesTypes.Standard) if (subject.Episodes.Count == 1 && subject.Series.SeriesType == SeriesTypes.Standard)
{ {
Episode episode = subject.Episodes.First(); var firstEpisode = subject.Episodes.First();
List<Episode> seasonEpisodes; var seasonEpisodes = GetSeasonEpisodes(subject, searchCriteria);
var seasonSearchCriteria = searchCriteria as SeasonSearchCriteria; // Ensure that this is either the first episode
if (seasonSearchCriteria != null && !seasonSearchCriteria.Series.UseSceneNumbering && seasonSearchCriteria.Episodes.Any(v => v.Id == episode.Id)) // or is the last episode in a season that has 10 or more episodes
{ if (seasonEpisodes.First().Id == firstEpisode.Id || (seasonEpisodes.Count() >= 10 && seasonEpisodes.Last().Id == firstEpisode.Id))
seasonEpisodes = seasonSearchCriteria.Episodes;
}
else
{
seasonEpisodes = _episodeService.GetEpisodesBySeason(episode.SeriesId, episode.SeasonNumber);
}
//Ensure that this is either the first episode
//or is the last episode in a season that has 10 or more episodes
if (seasonEpisodes.First().Id == episode.Id || (seasonEpisodes.Count() >= 10 && seasonEpisodes.Last().Id == episode.Id))
{ {
_logger.Debug("Possible double episode, doubling allowed size."); _logger.Debug("Possible double episode, doubling allowed size.");
maxSize = maxSize * 2; maxSize = maxSize * 2;
} }
} }
//If the parsed size is greater than maxSize we don't want it // If the parsed size is greater than maxSize we don't want it
if (subject.Release.Size > maxSize) if (subject.Release.Size > maxSize)
{ {
var runtimeMessage = subject.Episodes.Count == 1 ? $"{subject.Series.Runtime}min" : $"{subject.Episodes.Count}x {subject.Series.Runtime}min"; var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count}x {runtime}min";
_logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} for {3}), rejecting", subject, subject.Release.Size, maxSize, runtimeMessage); _logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} for {3}), rejecting", subject, subject.Release.Size, maxSize, runtimeMessage);
return Decision.Reject("{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), runtimeMessage); return Decision.Reject("{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), runtimeMessage);
@ -113,5 +128,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
_logger.Debug("Item: {0}, meets size constraints", subject); _logger.Debug("Item: {0}, meets size constraints", subject);
return Decision.Accept(); return Decision.Accept();
} }
private List<Episode> GetSeasonEpisodes(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
{
var firstEpisode = subject.Episodes.First();
if (searchCriteria is SeasonSearchCriteria seasonSearchCriteria && !seasonSearchCriteria.Series.UseSceneNumbering && seasonSearchCriteria.Episodes.Any(v => v.Id == firstEpisode.Id))
{
return seasonSearchCriteria.Episodes;
}
return _episodeService.GetEpisodesBySeason(firstEpisode.SeriesId, firstEpisode.SeasonNumber);
}
} }
} }