From 7991ed0154c7ffadc586fe5e17b35737b6a9c4b3 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 27 Apr 2019 18:27:05 -0700 Subject: [PATCH] New: Reject multi-season releases Closes #683 --- .../MultiSeasonSpecificationFixture.cs | 61 +++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + .../ParserTests/SeasonParserFixture.cs | 13 ++++ .../Specifications/FullSeasonSpecification.cs | 1 - .../MultiSeasonSpecification.cs | 30 +++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Parser/Model/ParsedEpisodeInfo.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 14 ++++- 8 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 src/NzbDrone.Core.Test/DecisionEngineTests/MultiSeasonSpecificationFixture.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/MultiSeasonSpecification.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/MultiSeasonSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/MultiSeasonSpecificationFixture.cs new file mode 100644 index 000000000..dc499a3d6 --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/MultiSeasonSpecificationFixture.cs @@ -0,0 +1,61 @@ + +using System; +using NUnit.Framework; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using FizzWare.NBuilder; +using System.Linq; +using FluentAssertions; +using NzbDrone.Core.Tv; +using Moq; +using System.Collections.Generic; + +namespace NzbDrone.Core.Test.DecisionEngineTests +{ + [TestFixture] + public class MultiSeasonSpecificationFixture : CoreTest + { + private RemoteEpisode _remoteEpisode; + + [SetUp] + public void Setup() + { + var series = Builder.CreateNew().With(s => s.Id = 1234).Build(); + _remoteEpisode = new RemoteEpisode + { + ParsedEpisodeInfo = new ParsedEpisodeInfo + { + FullSeason = true, + IsMultiSeason = true + }, + Episodes = Builder.CreateListOfSize(3) + .All() + .With(s => s.SeriesId = series.Id) + .BuildList(), + Series = series, + Release = new ReleaseInfo + { + Title = "Series.Title.S01-05.720p.BluRay.X264-RlsGrp" + } + }; + + Mocker.GetMock().Setup(s => s.EpisodesBetweenDates(It.IsAny(), It.IsAny(), false)) + .Returns(new List()); + } + + [Test] + public void should_return_true_if_is_not_a_multi_season_release() + { + _remoteEpisode.ParsedEpisodeInfo.IsMultiSeason = false; + _remoteEpisode.Episodes.Last().AirDateUtc = DateTime.UtcNow.AddDays(+2); + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_is_a_multi_season_release() + { + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 396cb9554..8f7573989 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -159,6 +159,7 @@ + diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs index 7272d474b..70d1a12ca 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -76,5 +76,18 @@ namespace NzbDrone.Core.Test.ParserTests result.IsPartialSeason.Should().BeTrue(); result.SeasonPart.Should().Be(seasonPart); } + + [TestCase("The Wire S01-05 WS BDRip X264-REWARD-No Rars", "The Wire", 1)] + public void should_parse_multi_season_release(string postTitle, string title, int firstSeason) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.SeasonNumber.Should().Be(firstSeason); + result.SeriesTitle.Should().Be(title); + result.EpisodeNumbers.Should().BeEmpty(); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeTrue(); + result.IsPartialSeason.Should().BeFalse(); + result.IsMultiSeason.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs index c6349ee37..63ecdffc8 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs @@ -4,7 +4,6 @@ using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Common.Extensions; using System.Linq; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine.Specifications { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/MultiSeasonSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/MultiSeasonSpecification.cs new file mode 100644 index 000000000..1c6479436 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/MultiSeasonSpecification.cs @@ -0,0 +1,30 @@ +using NLog; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class MultiSeasonSpecification : IDecisionEngineSpecification + { + private readonly Logger _logger; + + public MultiSeasonSpecification(Logger logger) + { + _logger = logger; + } + + public SpecificationPriority Priority => SpecificationPriority.Default; + public RejectionType Type => RejectionType.Permanent; + + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + { + if (subject.ParsedEpisodeInfo.IsMultiSeason) + { + _logger.Debug("Multi-season release {0} rejected. Not supported", subject.Release.Title); + return Decision.Reject("Multi-season releases are not supported"); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 4eab897b5..d797b8121 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -145,6 +145,7 @@ + diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 5b2128160..fc43e6713 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Parser.Model public Language Language { get; set; } public bool FullSeason { get; set; } public bool IsPartialSeason { get; set; } + public bool IsMultiSeason { get; set; } public bool IsSeasonExtra { get; set; } public bool Special { get; set; } public string ReleaseGroup { get; set; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 807d4a8e7..fb9cd25e0 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -128,6 +128,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?.+?)(?:(?:[-_\W](?<![()\[!]))+(?<season>(?<!\d+)(?:\d{4})(?!\d+))(?:x|\Wx){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|x|\Wx|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)\W?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Multi-season pack + new Regex(@"^(?<title>.+?)[-_. ]+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))-(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Partial season pack new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<seasonpart>\d{1,2}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -684,9 +688,6 @@ namespace NzbDrone.Core.Parser //If no season was found it should be treated as a mini series and season 1 if (seasons.Count == 0) seasons.Add(1); - //If more than 1 season was parsed go to the next REGEX (A multi-season release is unlikely) - if (seasons.Distinct().Count() > 1) return null; - result = new ParsedEpisodeInfo { ReleaseTitle = releaseTitle, @@ -695,6 +696,13 @@ namespace NzbDrone.Core.Parser AbsoluteEpisodeNumbers = new int[0] }; + + //If more than 1 season was parsed set IsMultiSeason to true so it can be rejected later + if (seasons.Distinct().Count() > 1) + { + result.IsMultiSeason = true; + } + foreach (Match matchGroup in matchCollection) { var episodeCaptures = matchGroup.Groups["episode"].Captures.Cast<Capture>().ToList();