From d13b41313fe21f64bae5d925c53c517fd621a069 Mon Sep 17 00:00:00 2001 From: Cyberlane Date: Sun, 27 Oct 2013 22:50:15 +0000 Subject: [PATCH 1/7] Parser can parse absolute episode numbers --- .../ParserTests/ParserFixture.cs | 32 ++++++++++ .../Parser/Model/ParsedEpisodeInfo.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 62 ++++++++++++++++--- 3 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 5652f3b6c..cebd69bff 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -177,6 +177,38 @@ public void parse_daily_episodes(string postTitle, string title, int year, int m result.EpisodeNumbers.Should().BeNull(); } + [TestCase("[SubDESU]_High_School_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "High School DxD", 7, 0, 0)] + [TestCase("[Chihiro]_Working!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Working!!", 6, 0, 0)] + [TestCase("[Commie]_Senki_Zesshou_Symphogear_-_11_[65F220B4]", "Senki_Zesshou_Symphogear", 11, 0, 0)] + [TestCase("[Underwater]_Rinne_no_Lagrange_-_12_(720p)_[5C7BC4F9]", "Rinne_no_Lagrange", 12, 0, 0)] + [TestCase("[Commie]_Rinne_no_Lagrange_-_15_[E76552EA]", "Rinne_no_Lagrange", 15, 0, 0)] + [TestCase("[HorribleSubs]_Hunter_X_Hunter_-_33_[720p]", "Hunter_X_Hunter", 33, 0, 0)] + [TestCase("[HorribleSubs]_Fairy_Tail_-_145_[720p]", "Fairy_Tail", 145, 0, 0)] + [TestCase("[HorribleSubs] Tonari no Kaibutsu-kun - 13 [1080p].mkv", "Tonari no Kaibutsu-kun", 13, 0, 0)] + [TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F].mkv", "Yes.Pretty.Cure.5.Go.Go!", 31, 0, 0)] + [TestCase("[K-F] One Piece 214", "One Piece", 214, 0, 0)] + [TestCase("[K-F] One Piece S10E14 214", "One Piece", 214, 10, 14)] + [TestCase("[K-F] One Piece 10x14 214", "One Piece", 214, 10, 14)] + [TestCase("[K-F] One Piece 214 10x14", "One Piece", 214, 10, 14)] + [TestCase("One Piece S10E14 214", "One Piece", 214, 10, 14)] + [TestCase("One Piece 10x14 214", "One Piece", 214, 10, 14)] + [TestCase("One Piece 214 10x14", "One Piece", 214, 10, 14)] + [TestCase("214 One Piece 10x14", "One Piece", 214, 10, 14)] + [TestCase("Bleach - 031 - The Resolution to Kill [Lunar].avi", "Bleach", 31, 0, 0)] + [TestCase("Bleach - 031 - The Resolution to Kill [Lunar]", "Bleach", 31, 0, 0)] + [TestCase("[ACX]Hack Sign 01 Role Play [Kosaka] [9C57891E].mkv", "Hack Sign", 1, 0, 0)] + [TestCase("[SFW-sage] Bakuman S3 - 12 [720p][D07C91FC]", "Bakuman S3", 12, 0, 0)] + [TestCase("ducktales_e66_time_is_money_part_one_marking_time", "DuckTales", 66, 0, 0)] + public void parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Should().NotBeNull(); + result.AbsoluteEpisodeNumbers.First().Should().Be(absoluteEpisodeNumber); + result.SeasonNumber.Should().Be(seasonNumber); + result.EpisodeNumbers.FirstOrDefault().Should().Be(episodeNumber); + result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); + } + [TestCase("Conan {year} {month} {day} Emma Roberts HDTV XviD BFF")] [TestCase("The Tonight Show With Jay Leno {year} {month} {day} 1080i HDTV DD5 1 MPEG2 TrollHD")] diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 14fd53a80..48e9ec577 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -11,6 +11,7 @@ public class ParsedEpisodeInfo public QualityModel Quality { get; set; } public int SeasonNumber { get; set; } public int[] EpisodeNumbers { get; set; } + public int[] AbsoluteEpisodeNumbers { get; set; } public String AirDate { get; set; } public Language Language { get; set; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 9122245ab..419baa5c0 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -16,6 +16,26 @@ public static class Parser private static readonly Regex[] ReportTitleRegex = new[] { + //Anime - Absolute Episode Number + Title + Season+Episode + new Regex(@"^(?:(?\d{2,3})(?:_|-|\s|\.)+)+(?.+?)(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Anime - [SubGroup] Title Absolute Episode Number + Season+Episode + new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))?(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Anime - [SubGroup] Title Season+Episode + Absolute Episode Number + new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))?(?<title>.+?)(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:_|\s|\.)(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.|$)+)+", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Anime - [SubGroup] Title Absolute Episode Number + new Regex(@"^\[(?<subgroup>.+?)\](?:_|-|\s|\.)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,}))+", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Anime - Title Absolute Episode Number [SubGroup] + new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}))+(?:.+?)\[(?<subgroup>.+?)\](?:\.|$)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Episodes with airdate new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -226,25 +246,45 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle { SeasonNumber = seasons.First(), EpisodeNumbers = new int[0], + AbsoluteEpisodeNumbers = new int[0] }; foreach (Match matchGroup in matchCollection) { var episodeCaptures = matchGroup.Groups["episode"].Captures.Cast<Capture>().ToList(); + var absoluteEpisodeCaptures = matchGroup.Groups["absoluteepisode"].Captures.Cast<Capture>().ToList(); //Allows use to return a list of 0 episodes (We can handle that as a full season release) - if (episodeCaptures.Any()) + var eps = episodeCaptures.Any(); + var epsAbs = absoluteEpisodeCaptures.Any(); + if (eps || epsAbs) { - var first = Convert.ToInt32(episodeCaptures.First().Value); - var last = Convert.ToInt32(episodeCaptures.Last().Value); - - if (first > last) + if (eps) { - return null; - } + var first = Convert.ToInt32(episodeCaptures.First().Value); + var last = Convert.ToInt32(episodeCaptures.Last().Value); - var count = last - first + 1; - result.EpisodeNumbers = Enumerable.Range(first, count).ToArray(); + if (first > last) + { + return null; + } + + var count = last - first + 1; + result.EpisodeNumbers = Enumerable.Range(first, count).ToArray(); + } + if (epsAbs) + { + var first = Convert.ToInt32(absoluteEpisodeCaptures.First().Value); + var last = Convert.ToInt32(absoluteEpisodeCaptures.Last().Value); + + if (first > last) + { + return null; + } + + var count = last - first + 1; + result.AbsoluteEpisodeNumbers = Enumerable.Range(first, count).ToArray(); + } } else { @@ -256,6 +296,10 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle result.FullSeason = true; } } + if (result.AbsoluteEpisodeNumbers.Any() && !result.EpisodeNumbers.Any()) + { + result.SeasonNumber = 0; + } } else From 44c1bc632e6e55a03e4bfec4b3eff639892b431b Mon Sep 17 00:00:00 2001 From: Cyberlane <dogbertuk2000@gmail.com> Date: Fri, 8 Nov 2013 00:24:09 +0000 Subject: [PATCH 2/7] Parsing service code (and tests) for absolute numbered episodes --- .../ParsingServiceTests/GetEpisodesFixture.cs | 19 +++++++++++++++- .../FindEpisodeFixture.cs | 10 +++++++++ .../Parser/Model/ParsedEpisodeInfo.cs | 5 +++++ src/NzbDrone.Core/Parser/ParsingService.cs | 22 +++++++++++++++++++ src/NzbDrone.Core/Tv/EpisodeRepository.cs | 6 +++++ src/NzbDrone.Core/Tv/EpisodeService.cs | 6 +++++ 6 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs index b1d235dee..59f6d031f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs @@ -38,7 +38,8 @@ public void Setup() { SeriesTitle = _series.Title, SeasonNumber = 1, - EpisodeNumbers = new[] { 1 } + EpisodeNumbers = new[] { 1 }, + AbsoluteEpisodeNumbers = new int[0] }; _singleEpisodeSearchCriteria = new SingleEpisodeSearchCriteria @@ -69,6 +70,11 @@ private void GivenSceneNumberingSeries() _series.UseSceneNumbering = true; } + private void GivenAbsoluteNumberingSeries() + { + _parsedEpisodeInfo.AbsoluteEpisodeNumbers = new[] { 1 }; + } + [Test] public void should_get_daily_episode_episode_when_search_criteria_is_null() { @@ -105,6 +111,17 @@ public void should_fallback_to_daily_episode_lookup_when_search_criteria_episode .Verify(v => v.FindEpisode(It.IsAny<Int32>(), It.IsAny<String>()), Times.Once()); } + [Test] + public void should_use_search_criteria_episode_when_it_matches_absolute() + { + GivenAbsoluteNumberingSeries(); + + Subject.Map(_parsedEpisodeInfo, _series.TvRageId, _singleEpisodeSearchCriteria); + + Mocker.GetMock<IEpisodeService>() + .Verify(v => v.FindEpisode(It.IsAny<Int32>(), It.IsAny<String>()), Times.Never()); + } + [Test] public void should_use_scene_numbering_when_series_uses_scene_numbering() { diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/FindEpisodeFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/FindEpisodeFixture.cs index 21fa4c91a..217e23ab8 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/FindEpisodeFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/FindEpisodeFixture.cs @@ -20,6 +20,7 @@ public void Setup() .With(e => e.SeasonNumber = 1) .With(e => e.SceneSeasonNumber = 2) .With(e => e.EpisodeNumber = 3) + .With(e => e.AbsoluteEpisodeNumber = 3) .With(e => e.SceneEpisodeNumber = 4) .Build(); @@ -51,5 +52,14 @@ public void should_not_find_episode_that_does_not_exist() .Should() .BeNull(); } + + [Test] + public void should_find_episode_by_absolute_numbering() + { + Subject.Find(_episode.SeriesId, _episode.AbsoluteEpisodeNumber.Value) + .Id + .Should() + .Be(_episode.Id); + } } } diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 48e9ec577..ee9e8f94b 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -41,5 +41,10 @@ public bool IsDaily() { return !String.IsNullOrWhiteSpace(AirDate); } + + public bool IsAbsoluteNumbering() + { + return AbsoluteEpisodeNumbers.Length > 0; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 3cd3aa407..aa474954b 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -129,6 +129,28 @@ public List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series ser return result; } + if (parsedEpisodeInfo.IsAbsoluteNumbering()) + { + foreach (var absoluteEpisodeNumber in parsedEpisodeInfo.AbsoluteEpisodeNumbers) + { + var episodeInfo = _episodeService.FindEpisode(series.Id, absoluteEpisodeNumber); + + if (episodeInfo != null) + { + _logger.Info("Using absolute episode number {0} for: {1} - Scene: {2}x{3:00} - TVDB: {4}x{5:00}", + absoluteEpisodeNumber, + series.Title, + episodeInfo.SceneSeasonNumber, + episodeInfo.SceneEpisodeNumber, + episodeInfo.SeasonNumber, + episodeInfo.EpisodeNumber); + result.Add(episodeInfo); + } + } + + return result; + } + if (parsedEpisodeInfo.EpisodeNumbers == null) return result; diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index daba4d5e0..58099d8c3 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Tv public interface IEpisodeRepository : IBasicRepository<Episode> { Episode Find(int seriesId, int season, int episodeNumber); + Episode Find(int seriesId, int absoluteEpisodeNumber); Episode Get(int seriesId, String date); Episode Find(int seriesId, String date); List<Episode> GetEpisodes(int seriesId); @@ -39,6 +40,11 @@ public Episode Find(int seriesId, int season, int episodeNumber) return Query.SingleOrDefault(s => s.SeriesId == seriesId && s.SeasonNumber == season && s.EpisodeNumber == episodeNumber); } + public Episode Find(int seriesId, int absoluteEpisodeNumber) + { + return Query.SingleOrDefault(s => s.SeriesId == seriesId && s.AbsoluteEpisodeNumber == absoluteEpisodeNumber); + } + public Episode Get(int seriesId, String date) { return Query.Single(s => s.SeriesId == seriesId && s.AirDate == date); diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 9ef46761f..96bfb1e52 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -14,6 +14,7 @@ public interface IEpisodeService { Episode GetEpisode(int id); Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber, bool useScene = false); + Episode FindEpisode(int seriesId, int absoluteEpisodeNumber); Episode GetEpisode(int seriesId, String date); Episode FindEpisode(int seriesId, String date); List<Episode> GetEpisodeBySeries(int seriesId); @@ -62,6 +63,11 @@ public Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber, bo return _episodeRepository.Find(seriesId, seasonNumber, episodeNumber); } + public Episode FindEpisode(int seriesId, int absoluteEpisodeNumber) + { + return _episodeRepository.Find(seriesId, absoluteEpisodeNumber); + } + public Episode GetEpisode(int seriesId, String date) { return _episodeRepository.Get(seriesId, date); From 26d3d9dcd6fb746ed0e0c68a301b2d96eb2013af Mon Sep 17 00:00:00 2001 From: Cyberlane <dogbertuk2000@gmail.com> Date: Fri, 8 Nov 2013 19:03:01 +0000 Subject: [PATCH 3/7] Small tweaks based on feedback from @markus101 --- .../Parser/Model/ParsedEpisodeInfo.cs | 2 +- src/NzbDrone.Core/Parser/Parser.cs | 47 +++++++++---------- src/NzbDrone.Core/Parser/ParsingService.cs | 4 +- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index ee9e8f94b..7913742d7 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -44,7 +44,7 @@ public bool IsDaily() public bool IsAbsoluteNumbering() { - return AbsoluteEpisodeNumbers.Length > 0; + return AbsoluteEpisodeNumbers.Any(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 419baa5c0..17b1491d3 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -246,7 +246,7 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle { SeasonNumber = seasons.First(), EpisodeNumbers = new int[0], - AbsoluteEpisodeNumbers = new int[0] + AbsoluteEpisodeNumbers = new int[0] }; foreach (Match matchGroup in matchCollection) @@ -255,36 +255,31 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle var absoluteEpisodeCaptures = matchGroup.Groups["absoluteepisode"].Captures.Cast<Capture>().ToList(); //Allows use to return a list of 0 episodes (We can handle that as a full season release) - var eps = episodeCaptures.Any(); - var epsAbs = absoluteEpisodeCaptures.Any(); - if (eps || epsAbs) + if (episodeCaptures.Any()) { - if (eps) + var first = Convert.ToInt32(episodeCaptures.First().Value); + var last = Convert.ToInt32(episodeCaptures.Last().Value); + + if (first > last) { - var first = Convert.ToInt32(episodeCaptures.First().Value); - var last = Convert.ToInt32(episodeCaptures.Last().Value); - - if (first > last) - { - return null; - } - - var count = last - first + 1; - result.EpisodeNumbers = Enumerable.Range(first, count).ToArray(); + return null; } - if (epsAbs) + + var count = last - first + 1; + result.EpisodeNumbers = Enumerable.Range(first, count).ToArray(); + } + else if (absoluteEpisodeCaptures.Any()) + { + var first = Convert.ToInt32(absoluteEpisodeCaptures.First().Value); + var last = Convert.ToInt32(absoluteEpisodeCaptures.Last().Value); + + if (first > last) { - var first = Convert.ToInt32(absoluteEpisodeCaptures.First().Value); - var last = Convert.ToInt32(absoluteEpisodeCaptures.Last().Value); - - if (first > last) - { - return null; - } - - var count = last - first + 1; - result.AbsoluteEpisodeNumbers = Enumerable.Range(first, count).ToArray(); + return null; } + + var count = last - first + 1; + result.AbsoluteEpisodeNumbers = Enumerable.Range(first, count).ToArray(); } else { diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index aa474954b..ee544d789 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -137,11 +137,9 @@ public List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series ser if (episodeInfo != null) { - _logger.Info("Using absolute episode number {0} for: {1} - Scene: {2}x{3:00} - TVDB: {4}x{5:00}", + _logger.Info("Using absolute episode number {0} for: {1} - TVDB: {2}x{3:00}", absoluteEpisodeNumber, series.Title, - episodeInfo.SceneSeasonNumber, - episodeInfo.SceneEpisodeNumber, episodeInfo.SeasonNumber, episodeInfo.EpisodeNumber); result.Add(episodeInfo); From 9be6e68e7e932c2e43640dbbde48a5ce7546872c Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 10 Nov 2013 22:41:21 -0800 Subject: [PATCH 4/7] Fixed broken tests --- .../ParserTests/ParserFixture.cs | 2 +- src/NzbDrone.Core/Parser/Parser.cs | 51 +++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index cebd69bff..56522f3d3 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -120,7 +120,7 @@ public void PathParse_tests(string path, int season, int episode) ExceptionVerification.IgnoreWarns(); } - [TestCase("[DmonHiro] The Severing Crime Edge - Cut 02 - Portrait Of Heresy [BD, 720p] [BE36E9E0]")] + [TestCase("THIS SHOULD NEVER PARSE")] public void unparsable_title_should_log_warn_and_return_null(string title) { Parser.Parser.ParseTitle(title).Should().BeNull(); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 17b1491d3..cc9556702 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -16,30 +17,26 @@ public static class Parser private static readonly Regex[] ReportTitleRegex = new[] { - //Anime - Absolute Episode Number + Title + Season+Episode - new Regex(@"^(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.)+)+(?<title>.+?)(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Anime - [SubGroup] Title Absolute Episode Number + Season+Episode - new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))?(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Anime - [SubGroup] Title Season+Episode + Absolute Episode Number - new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))?(?<title>.+?)(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:_|\s|\.)(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.|$)+)+", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Anime - [SubGroup] Title Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\](?:_|-|\s|\.)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,}))+", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Anime - Title Absolute Episode Number [SubGroup] - new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}))+(?:.+?)\[(?<subgroup>.+?)\](?:\.|$)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - //Episodes with airdate new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Anime - Absolute Episode Number + Title + Season+Episode + new Regex(@"^(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.)+)+(?<title>.+?)(?:\W|_)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Anime - [SubGroup] Title Absolute Episode Number + Season+Episode + new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))?(?<title>.+?)(?:(?:\W|_)+(?<absoluteepisode>\d{2,3}))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Anime - [SubGroup] Title Season+Episode + Absolute Episode Number + new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))?(?<title>.+?)(?:\W|_)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:\s|\.)(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.|$)+)+", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Anime - [SubGroup] Title Absolute Episode Number + new Regex(@"^\[(?<subgroup>.+?)\](?:_|-|\s|\.)?(?<title>.+?)(?:(?:\W|_)+(?<absoluteepisode>\d{2,}))+", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Multi-Part episodes without a title (S01E05.S01E06) new Regex(@"^(?:\W*S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -64,6 +61,10 @@ public static class Parser new Regex(@"^(?<title>.*?)(?:\W?S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]){1,2}(?<episode>\d{1}))+)+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Anime - Title Absolute Episode Number [SubGroup] + new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}))+(?:.+?)\[(?<subgroup>.+?)\](?:\.|$)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Supports 103/113 naming new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+)\d{1})(?<episode>\d{2}(?!p|i|\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -82,7 +83,11 @@ public static class Parser //Supports Season only releases new Regex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{1,2}(?!\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled) + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Anime - Title Absolute Episode Number + new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+e(?<absoluteepisode>\d{2,3}))+", + RegexOptions.IgnoreCase | RegexOptions.Compiled), }; private static readonly Regex NormalizeRegex = new Regex(@"((^|\W|_)(a|an|the|and|or|of)($|\W|_))|\W|_|(?:(?<=[^0-9]+)|\b)(?!(?:19\d{2}|20\d{2}))\d+(?=[^0-9ip]+|\b)", @@ -134,6 +139,7 @@ public static ParsedEpisodeInfo ParseTitle(string title) if (match.Count != 0) { + Debug.WriteLine(regex); try { var result = ParseMatchCollection(match); @@ -268,7 +274,8 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle var count = last - first + 1; result.EpisodeNumbers = Enumerable.Range(first, count).ToArray(); } - else if (absoluteEpisodeCaptures.Any()) + + if (absoluteEpisodeCaptures.Any()) { var first = Convert.ToInt32(absoluteEpisodeCaptures.First().Value); var last = Convert.ToInt32(absoluteEpisodeCaptures.Last().Value); From 92cb702b9c98402daf4fe07da68d1969bb5b6e35 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 10 Nov 2013 22:56:15 -0800 Subject: [PATCH 5/7] Added more multi-episode tests and support for them --- src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs | 4 +++- src/NzbDrone.Core/Parser/Parser.cs | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 56522f3d3..0c7aca4a0 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -147,7 +147,9 @@ public void unparsable_title_should_log_warn_and_return_null(string title) [TestCase("Hell.on.Wheels.S02E09-E10.720p.HDTV.x264-EVOLVE", "Hell on Wheels", 2, new[] { 9, 10 })] [TestCase("Grey's Anatomy - 8x01_02 - Free Falling", "Grey's Anatomy", 8, new [] { 1,2 })] [TestCase("8x01_02 - Free Falling", "", 8, new[] { 1, 2 })] - [TestCase("Kaamelott.S01E91-E100", "Kaamelott", 1,new[] { 91, 92, 93, 94, 95, 96, 97, 98, 99, 100 })] + [TestCase("Kaamelott.S01E91-E100", "Kaamelott", 1, new[] { 91, 92, 93, 94, 95, 96, 97, 98, 99, 100 })] + [TestCase("Neighbours.S29E161-E165.PDTV.x264-FQM", "Neighbours", 29, new[] { 161, 162, 163, 164, 165 })] + [TestCase("Shortland.Street.S22E5363-E5366.HDTV.x264-FiHTV", "Shortland Street", 22, new[] { 5363, 5364, 5365, 5366 })] public void TitleParse_multi(string postTitle, string title, int season, int[] episodes) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index cc9556702..ef62d9034 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -74,7 +74,7 @@ public static class Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Supports 1103/1113 naming - new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+|\(|\[)\d{2})(?<episode>\d{2}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)", + new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)\d{2}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Supports Season 01 Episode 03 @@ -85,6 +85,15 @@ public static class Parser new Regex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{1,2}(?!\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + //4-digit episode number + //Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc) + new Regex(@"^(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)(\W+|_|$)(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) + new Regex(@"^(?<title>.+?)(?:(\W|_)+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Anime - Title Absolute Episode Number new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+e(?<absoluteepisode>\d{2,3}))+", RegexOptions.IgnoreCase | RegexOptions.Compiled), From bb65e8301251610d3da706a932bb0046561a2503 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 10 Nov 2013 23:01:56 -0800 Subject: [PATCH 6/7] Fixed broken mapping tests --- .../Parser/Model/ParsedEpisodeInfo.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 7913742d7..d4bbefaac 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -14,9 +14,24 @@ public class ParsedEpisodeInfo public int[] AbsoluteEpisodeNumbers { get; set; } public String AirDate { get; set; } public Language Language { get; set; } - public bool FullSeason { get; set; } + public ParsedEpisodeInfo() + { + EpisodeNumbers = new int[0]; + AbsoluteEpisodeNumbers = new int[0]; + } + + public bool IsDaily() + { + return !String.IsNullOrWhiteSpace(AirDate); + } + + public bool IsAbsoluteNumbering() + { + return AbsoluteEpisodeNumbers.Any(); + } + public override string ToString() { string episodeString = "[Unknown Episode]"; @@ -36,15 +51,5 @@ public override string ToString() return string.Format("{0} - {1} {2}", SeriesTitle, episodeString, Quality); } - - public bool IsDaily() - { - return !String.IsNullOrWhiteSpace(AirDate); - } - - public bool IsAbsoluteNumbering() - { - return AbsoluteEpisodeNumbers.Any(); - } } } \ No newline at end of file From 106f06c81f8fd120baa59262baa5f2a38d72db49 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 10 Nov 2013 23:08:52 -0800 Subject: [PATCH 7/7] Fixed daily parsing tests --- src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 0c7aca4a0..c80d578b9 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -176,7 +176,7 @@ public void parse_daily_episodes(string postTitle, string title, int year, int m result.Should().NotBeNull(); result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); result.AirDate.Should().Be(airDate.ToString(Episode.AIR_DATE_FORMAT)); - result.EpisodeNumbers.Should().BeNull(); + result.EpisodeNumbers.Should().BeEmpty(); } [TestCase("[SubDESU]_High_School_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "High School DxD", 7, 0, 0)]