diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/FullSeasonSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/FullSeasonSpecificationFixture.cs index 6a66d957d..a751d2a6a 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/FullSeasonSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/FullSeasonSpecificationFixture.cs @@ -49,27 +49,27 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { _remoteEpisode.ParsedEpisodeInfo.FullSeason = false; _remoteEpisode.Episodes.Last().AirDateUtc = DateTime.UtcNow.AddDays(+2); - Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); } [Test] public void should_return_true_if_all_episodes_have_aired() { - Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); } [Test] public void should_return_false_if_one_episode_has_not_aired() { _remoteEpisode.Episodes.Last().AirDateUtc = DateTime.UtcNow.AddDays(+2); - Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); } [Test] public void should_return_false_if_an_episode_does_not_have_an_air_date() { _remoteEpisode.Episodes.Last().AirDateUtc = null; - Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs index 7a4ed0b9f..8d7ba558e 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Test.Framework; @@ -53,5 +53,18 @@ namespace NzbDrone.Core.Test.ParserTests result.Should().BeNull(); } + + [TestCase("The.Ranch.2016.S02.Part.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Ranch 2016", 2, 1)] + public void should_parse_partial_season_release(string postTitle, string title, int season, int seasonPart) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.SeasonNumber.Should().Be(season); + result.SeriesTitle.Should().Be(title); + result.EpisodeNumbers.Should().BeEmpty(); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeFalse(); + result.IsPartialSeason.Should().BeTrue(); + result.SeasonPart.Should().Be(seasonPart); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 859c8111d..aaa015915 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -94,7 +94,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (localEpisode.Episodes.Empty()) { - decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); + if (localEpisode.ParsedEpisodeInfo.IsPartialSeason) + { + decision = new ImportDecision(localEpisode, new Rejection("Partial season packs are not supported")); + } + else + { + decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); + } } else { diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index d38001715..35bc49d7d 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Qualities; @@ -16,9 +16,11 @@ namespace NzbDrone.Core.Parser.Model public string AirDate { get; set; } public Language Language { get; set; } public bool FullSeason { get; set; } + public bool IsPartialSeason { get; set; } public bool Special { get; set; } public string ReleaseGroup { get; set; } public string ReleaseHash { get; set; } + public int SeasonPart { get; set; } public ParsedEpisodeInfo() { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 3a20f7d09..0cd5fb6d1 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -94,6 +94,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), + // 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), + //Mini-Series with year in title, treated as season 1, episodes are labelled as Part01, Part 01, Part.1 new Regex(@"^(?<title>.+?\d{4})(?:\W+(?:(?:Part\W?|e)(?<episode>\d{1,2}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -602,7 +606,19 @@ namespace NzbDrone.Core.Parser //Todo: Set a "Extras" flag in EpisodeParseResult if we want to download them ever if (!matchCollection[0].Groups["extras"].Value.IsNullOrWhiteSpace()) return null; - result.FullSeason = true; + // Partial season packs will have a seasonpart group so they can be differentiated + // from a full season/single episode release + var seasonPart = matchCollection[0].Groups["seasonpart"].Value; + + if (seasonPart.IsNotNullOrWhiteSpace()) + { + result.SeasonPart = Convert.ToInt32(seasonPart); + result.IsPartialSeason = true; + } + else + { + result.FullSeason = true; + } } }