diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index 245fee1a6..075d1758d 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -211,5 +211,19 @@ namespace NzbDrone.Core.Test.ParserTests result.FullSeason.Should().BeFalse(); result.Special.Should().BeTrue(); } + + [TestCase("Series.Title.S06E01b.Fade.Out.Fade.in.Part.2.1080p.DSNP.WEB-DL.AAC2.0.H.264-FLUX", "Series Title", 6, 1)] + public void should_parse_split_episode(string postTitle, string title, int seasonNumber, int episodeNumber) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Should().NotBeNull(); + result.EpisodeNumbers.Should().HaveCount(1); + result.SeasonNumber.Should().Be(seasonNumber); + result.EpisodeNumbers.First().Should().Be(episodeNumber); + result.SeriesTitle.Should().Be(title); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeFalse(); + result.IsSplitEpisode.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs new file mode 100644 index 000000000..fef3be741 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SplitEpisodeSpecification.cs @@ -0,0 +1,30 @@ +using NLog; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class SplitEpisodeSpecification : IDecisionEngineSpecification + { + private readonly Logger _logger; + + public SplitEpisodeSpecification(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.IsSplitEpisode) + { + _logger.Debug("Split episode release {0} rejected. Not supported", subject.Release.Title); + return Decision.Reject("Split episode releases are not supported"); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs new file mode 100644 index 000000000..8a84b0b86 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SplitEpisodeSpecification.cs @@ -0,0 +1,33 @@ +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications +{ + public class SplitEpisodeSpecification : IImportDecisionEngineSpecification + { + private readonly Logger _logger; + + public SplitEpisodeSpecification(Logger logger) + { + _logger = logger; + } + + public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + { + if (localEpisode.FileEpisodeInfo == null) + { + return Decision.Accept(); + } + + if (localEpisode.FileEpisodeInfo.IsSplitEpisode) + { + _logger.Debug("Single episode split into multiple files"); + return Decision.Reject("Single episode split into multiple files"); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 9423bd5ca..3833199bb 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Parser.Model public bool IsPartialSeason { get; set; } public bool IsMultiSeason { get; set; } public bool IsSeasonExtra { get; set; } + public bool IsSplitEpisode { get; set; } public bool Special { get; set; } public string ReleaseGroup { get; set; } public string ReleaseHash { get; set; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 836101e14..fc369f6e7 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -74,6 +74,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?:S?(?(?\d{2,3}(?!\d+))){2,})", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Split episodes (S01E05a, S01E05b, etc) + new Regex(@"^(?.+?)(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_ ]?[ex])(?<episode>\d{2,3}(?!\d+))(?<splitepisode>[a-d])(?:[ _.])))", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Episodes without a title, Single (S01E05, 1x05) new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_ ]?[ex])(?<episode>\d{2,3}(?!\d+))))", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -966,6 +970,11 @@ namespace NzbDrone.Core.Parser { result.Special = true; } + + if (matchGroup.Groups["splitepisode"].Success) + { + result.IsSplitEpisode = true; + } } if (absoluteEpisodeCaptures.Any())