diff --git a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs index 912b60335..0488e916e 100644 --- a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs @@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads .Returns(remoteEpisode); Mocker.GetMock() - .Setup(s => s.ParseSpecialEpisodeTitle(It.IsAny(), It.IsAny(), It.IsAny(), null)) + .Setup(s => s.ParseSpecialEpisodeTitle(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null)) .Returns(remoteEpisode.ParsedEpisodeInfo); var client = new DownloadClientDefinition() diff --git a/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs b/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs index 11f68da85..0db07c03f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs @@ -39,5 +39,12 @@ namespace NzbDrone.Core.Test.ParserTests { Parser.Parser.ParseTitle(title).IsPossibleSpecialEpisode.Should().BeTrue(); } + + + [TestCase("Dr.S11E00.A.Christmas.Carol.Special.720p.HDTV-FieldOfView")] + public void IsPossibleSpecialEpisode_should_be_true_if_e00_special(string title) + { + Parser.Parser.ParseTitle(title).IsPossibleSpecialEpisode.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/FindEpisodeByTitleFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/FindEpisodeByTitleFixture.cs index 46fafec3c..77f804383 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/FindEpisodeByTitleFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/FindEpisodeByTitleFixture.cs @@ -67,5 +67,31 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests .Should() .BeNull(); } + + [Test] + public void should_handle_e00_specials() + { + const string expectedTitle = "Inside The Walking Dead: Walker University"; + GivenEpisodesWithTitles("Inside The Walking Dead", expectedTitle, "Inside The Walking Dead Walker University 2"); + + Subject.FindEpisodeByTitle(1, 1, "The.Walking.Dead.S04E00.Inside.The.Walking.Dead.Walker.University.720p.HDTV.x264-W4F") + .Title + .Should() + .Be(expectedTitle); + } + + [TestCase("Dead.Man.Walking.S04E00.Inside.The.Walking.Dead.Walker.University.720p.HDTV.x264-W4F", "Inside The Walking Dead: Walker University", new[] { "Inside The Walking Dead", "Inside The Walking Dead Walker University 2" })] + [TestCase("Who.1999.S11E00.Twice.Upon.A.Time.1080p.AMZN.WEB-DL.DDP5.1.H.264-NTb", "Twice Upon A Time", new[] { "Last Christmas" })] + [TestCase("Who.1999.S11E00.Twice.Upon.A.Time.Christmas.Special.720p.HDTV.x264-FoV", "Twice Upon A Time", new[] { "Last Christmas" })] + [TestCase("Who.1999.S10E00.Christmas.Special.The.Return.Of.Doctor.Mysterio.1080p.BluRay.x264-OUIJA", "The Return Of Doctor Mysterio", new[] { "Doctor Mysterio" })] + public void should_handle_special(string releaseTitle, string expectedTitle, string[] rejectedTitles) + { + GivenEpisodesWithTitles(rejectedTitles.Concat(new[] { expectedTitle }).ToArray()); + + var episode = Subject.FindEpisodeByTitle(1, 0, releaseTitle); + + episode.Should().NotBeNull(); + episode.Title.Should().Be(expectedTitle); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 343280208..725735307 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -65,7 +65,7 @@ namespace NzbDrone.Core.DecisionEngine if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode) { - var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(report.Title, report.TvdbId, report.TvRageId, searchCriteria); + var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, report.Title, report.TvdbId, report.TvRageId, searchCriteria); if (specialEpisodeInfo != null) { diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index da92d1628..4bc95903a 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads { // Try parsing the original source title and if that fails, try parsing it as a special // TODO: Pass the TVDB ID and TVRage IDs in as well so we have a better chance for finding the item - parsedEpisodeInfo = Parser.Parser.ParseTitle(firstHistoryItem.SourceTitle) ?? _parsingService.ParseSpecialEpisodeTitle(firstHistoryItem.SourceTitle, 0, 0); + parsedEpisodeInfo = Parser.Parser.ParseTitle(firstHistoryItem.SourceTitle) ?? _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, firstHistoryItem.SourceTitle, 0, 0); if (parsedEpisodeInfo != null) { diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 73a9dfbaf..6dc338dcf 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -57,17 +57,29 @@ namespace NzbDrone.Core.Parser.Model { get { - // if we don't have eny episode numbers we are likely a special episode and need to do a search by episode title + // if we don't have any episode numbers we are likely a special episode and need to do a search by episode title return (AirDate.IsNullOrWhiteSpace() && SeriesTitle.IsNullOrWhiteSpace() && - (EpisodeNumbers.Length == 0 || SeasonNumber == 0) || - !SeriesTitle.IsNullOrWhiteSpace() && Special); + (EpisodeNumbers.Length == 0 || SeasonNumber == 0) || !SeriesTitle.IsNullOrWhiteSpace() && Special) || + EpisodeNumbers.Length == 1 && EpisodeNumbers[0] == 0; } //This prevents manually downloading a release from blowing up in mono //TODO: Is there a better way? private set {} } + public bool IsPossibleSceneSeasonSpecial + { + get + { + // SxxE00 episodes + return SeasonNumber != 0 && EpisodeNumbers.Length == 1 && EpisodeNumbers[0] == 0; + } + + //This prevents manually downloading a release from blowing up in mono + //TODO: Is there a better way? + private set { } + } public override string ToString() { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 61e4b97cc..dbeb21c1d 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -213,6 +213,16 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled) }; + + private static readonly Regex[] SpecialEpisodeTitleRegex = new Regex[] + { + new Regex(@"\.S\d+E00\.(?.+?)(?:\.(?:720p|1080p|HDTV|WEB|WEBRip|WEB-DL)\.|$)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + new Regex(@"\.S\d+\.Special\.(?.+?)(?:\.(?:720p|1080p|HDTV|WEB|WEBRip|WEB-DL)\.|$)", + RegexOptions.IgnoreCase | RegexOptions.Compiled) + }; + private static readonly Regex[] RejectHashedReleasesRegex = new Regex[] { // Generic match for md5 and mixed-case hashes. @@ -456,7 +466,19 @@ namespace NzbDrone.Core.Parser public static string NormalizeEpisodeTitle(string title) { - title = SpecialEpisodeWordRegex.Replace(title, string.Empty); + var match = SpecialEpisodeTitleRegex + .Select(v => v.Match(title)) + .Where(v => v.Success) + .FirstOrDefault(); + + if (match != null) + { + title = match.Groups["episodetitle"].Value; + } + + // Disabled, Until we run into specific testcases for the removal of these words. + //title = SpecialEpisodeWordRegex.Replace(title, string.Empty); + title = PunctuationRegex.Replace(title, " "); title = DuplicateSpacesRegex.Replace(title, " "); diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index eae9bb3d6..dd5e1d313 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Parser RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable episodeIds); List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); - ParsedEpisodeInfo ParseSpecialEpisodeTitle(string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); + ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); } public class ParsingService : IParsingService @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Parser if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode) { var title = Path.GetFileNameWithoutExtension(filename); - var specialEpisodeInfo = ParseSpecialEpisodeTitle(title, series); + var specialEpisodeInfo = ParseSpecialEpisodeTitle(parsedEpisodeInfo, title, series); if (specialEpisodeInfo != null) { @@ -184,18 +184,18 @@ namespace NzbDrone.Core.Parser return GetStandardEpisodes(series, parsedEpisodeInfo, sceneSource, searchCriteria); } - public ParsedEpisodeInfo ParseSpecialEpisodeTitle(string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) + public ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) { if (searchCriteria != null) { if (tvdbId != 0 && tvdbId == searchCriteria.Series.TvdbId) { - return ParseSpecialEpisodeTitle(releaseTitle, searchCriteria.Series); + return ParseSpecialEpisodeTitle(parsedEpisodeInfo, releaseTitle, searchCriteria.Series); } if (tvRageId != 0 && tvRageId == searchCriteria.Series.TvRageId) { - return ParseSpecialEpisodeTitle(releaseTitle, searchCriteria.Series); + return ParseSpecialEpisodeTitle(parsedEpisodeInfo, releaseTitle, searchCriteria.Series); } } @@ -222,11 +222,20 @@ namespace NzbDrone.Core.Parser return null; } - return ParseSpecialEpisodeTitle(releaseTitle, series); + return ParseSpecialEpisodeTitle(parsedEpisodeInfo, releaseTitle, series); } - private ParsedEpisodeInfo ParseSpecialEpisodeTitle(string releaseTitle, Series series) + private ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, Series series) { + // SxxE00 episodes are sometimes mapped via TheXEM, don't use episode title parsing in that case. + if (parsedEpisodeInfo != null && parsedEpisodeInfo.IsPossibleSceneSeasonSpecial && series.UseSceneNumbering) + { + if (_episodeService.FindEpisodesBySceneNumbering(series.Id, parsedEpisodeInfo.SeasonNumber, 0).Any()) + { + return parsedEpisodeInfo; + } + } + // find special episode in series season 0 var episode = _episodeService.FindEpisodeByTitle(series.Id, 0, releaseTitle); diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 32a46ec45..8f6dd9a67 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -102,11 +102,11 @@ namespace NzbDrone.Core.Tv { return _episodeRepository.GetEpisodes(seriesId, seasonNumber); } - - public Episode FindEpisodeByTitle(int seriesId, int seasonNumber, string releaseTitle) + + public Episode FindEpisodeByTitle(int seriesId, int seasonNumber, string releaseTitle) { // TODO: can replace this search mechanism with something smarter/faster/better - var normalizedReleaseTitle = Parser.Parser.NormalizeEpisodeTitle(releaseTitle).Replace(".", " "); + var normalizedReleaseTitle = Parser.Parser.NormalizeEpisodeTitle(releaseTitle); var episodes = _episodeRepository.GetEpisodes(seriesId, seasonNumber); var matches = episodes.Select( @@ -222,4 +222,4 @@ namespace NzbDrone.Core.Tv } } } -} \ No newline at end of file +}