diff --git a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs index 8ef1f9ba3..d5a397132 100644 --- a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs @@ -44,6 +44,23 @@ namespace NzbDrone.Core.Test.ParserTests result.FullSeason.Should().BeFalse(); } + [TestCase("Series.Title.2015.09.07.Part1.720p.HULU.WEBRip.AAC2.0.H.264-Sonarr", "Series Title", 2015, 9, 7, 1)] + [TestCase("Series.Title.2015.09.07.Part2.720p.HULU.WEBRip.AAC2.0.H.264-Sonarr", "Series Title", 2015, 9, 7, 2)] + [TestCase("Series.Title.2015.09.07.Part.1.720p.HULU.WEBRip.AAC2.0.H.264-Sonarr", "Series Title", 2015, 9, 7, 1)] + [TestCase("Series.Title.2015.09.07.Part.2.720p.HULU.WEBRip.AAC2.0.H.264-Sonarr", "Series Title", 2015, 9, 7, 2)] + public void should_parse_daily_episode_with_multiple_parts(string postTitle, string title, int year, int month, int day, int part) + { + var result = Parser.Parser.ParseTitle(postTitle); + var airDate = new DateTime(year, month, day); + result.Should().NotBeNull(); + result.SeriesTitle.Should().Be(title); + result.AirDate.Should().Be(airDate.ToString(Episode.AIR_DATE_FORMAT)); + result.EpisodeNumbers.Should().BeEmpty(); + result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + result.FullSeason.Should().BeFalse(); + result.DailyPart.Should().Be(part); + } + [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")] [TestCase("The.Daily.Show.{year}.{month}.{day}.Johnny.Knoxville.iTouch-MW")] diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs index 8103cb483..dec49dfe8 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs @@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); Mocker.GetMock() - .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Once()); + .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny(), null), Times.Once()); } [Test] @@ -98,7 +98,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); Mocker.GetMock() - .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Never()); + .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny(), null), Times.Never()); } [Test] @@ -110,7 +110,20 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); Mocker.GetMock() - .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Once()); + .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny(), null), Times.Once()); + } + + [Test] + public void should_get_daily_episode_episode_should_lookup_including_daily_part() + { + GivenDailySeries(); + GivenDailyParseResult(); + _parsedEpisodeInfo.DailyPart = 1; + + Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); + + Mocker.GetMock() + .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny(), 1), Times.Once()); } [Test] @@ -125,7 +138,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); Mocker.GetMock() - .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Never()); + .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny(), null), Times.Never()); } [Test] diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/ByAirDateFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/ByAirDateFixture.cs deleted file mode 100644 index 2f6c0cef5..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/ByAirDateFixture.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests -{ - [TestFixture] - public class ByAirDateFixture : DbTest - { - private const int SERIES_ID = 1; - private const string AIR_DATE = "2014-04-02"; - - private void GivenEpisode(int seasonNumber) - { - var episode = Builder.CreateNew() - .With(e => e.SeriesId = 1) - .With(e => e.SeasonNumber = seasonNumber) - .With(e => e.AirDate = AIR_DATE) - .BuildNew(); - - Db.Insert(episode); - } - - [Test] - public void should_throw_when_multiple_regular_episodes_are_found() - { - GivenEpisode(1); - GivenEpisode(2); - - Assert.Throws(() => Subject.Get(SERIES_ID, AIR_DATE)); - Assert.Throws(() => Subject.Find(SERIES_ID, AIR_DATE)); - } - - [Test] - public void should_throw_when_get_finds_no_episode() - { - Assert.Throws(() => Subject.Get(SERIES_ID, AIR_DATE)); - } - - [Test] - public void should_get_episode_when_single_episode_exists_for_air_date() - { - GivenEpisode(1); - - Subject.Get(SERIES_ID, AIR_DATE).Should().NotBeNull(); - Subject.Find(SERIES_ID, AIR_DATE).Should().NotBeNull(); - } - - [Test] - public void should_get_episode_when_regular_episode_and_special_share_the_same_air_date() - { - GivenEpisode(1); - GivenEpisode(0); - - Subject.Get(SERIES_ID, AIR_DATE).Should().NotBeNull(); - Subject.Find(SERIES_ID, AIR_DATE).Should().NotBeNull(); - } - - [Test] - public void should_get_special_when_its_the_only_episode_for_the_date_provided() - { - GivenEpisode(0); - - Subject.Get(SERIES_ID, AIR_DATE).Should().NotBeNull(); - Subject.Find(SERIES_ID, AIR_DATE).Should().NotBeNull(); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/ByAirDateFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/ByAirDateFixture.cs new file mode 100644 index 000000000..f470316f5 --- /dev/null +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/ByAirDateFixture.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests +{ + [TestFixture] + public class ByAirDateFixture : CoreTest + { + private const int SERIES_ID = 1; + private const string AIR_DATE = "2014-04-02"; + + private Episode CreateEpisode(int seasonNumber, int episodeNumber) + { + var episode = Builder.CreateNew() + .With(e => e.SeriesId = 1) + .With(e => e.SeasonNumber = seasonNumber) + .With(e => e.EpisodeNumber = episodeNumber) + .With(e => e.AirDate = AIR_DATE) + .BuildNew(); + + return episode; + } + + private void GivenEpisodes(params Episode[] episodes) + { + Mocker.GetMock() + .Setup(s => s.Find(It.IsAny(), It.IsAny())) + .Returns(episodes.ToList()); + } + + [Test] + public void should_throw_when_multiple_regular_episodes_are_found_and_not_part_provided() + { + GivenEpisodes(CreateEpisode(1, 1), CreateEpisode(2, 1)); + + Assert.Throws(() => Subject.FindEpisode(SERIES_ID, AIR_DATE, null)); + } + + [Test] + public void should_return_null_when_finds_no_episode() + { + GivenEpisodes(); + + Subject.FindEpisode(SERIES_ID, AIR_DATE, null).Should().BeNull(); + } + + [Test] + public void should_get_episode_when_single_episode_exists_for_air_date() + { + GivenEpisodes(CreateEpisode(1, 1)); + + Subject.FindEpisode(SERIES_ID, AIR_DATE, null).Should().NotBeNull(); + } + + [Test] + public void should_get_episode_when_regular_episode_and_special_share_the_same_air_date() + { + GivenEpisodes(CreateEpisode(1, 1), CreateEpisode(0, 1)); + + Subject.FindEpisode(SERIES_ID, AIR_DATE, null).Should().NotBeNull(); + } + + [Test] + public void should_get_special_when_its_the_only_episode_for_the_date_provided() + { + GivenEpisodes(CreateEpisode(0, 1)); + + Subject.FindEpisode(SERIES_ID, AIR_DATE, null).Should().NotBeNull(); + } + + [Test] + public void should_get_episode_when_two_regular_episodes_share_the_same_air_date_and_part_is_provided() + { + var episode1 = CreateEpisode(1, 1); + var episode2 = CreateEpisode(1, 2); + + GivenEpisodes(episode1, episode2); + + Subject.FindEpisode(SERIES_ID, AIR_DATE, 1).Should().Be(episode1); + Subject.FindEpisode(SERIES_ID, AIR_DATE, 2).Should().Be(episode2); + } + } +} diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index fc43e6713..b2c409419 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.Parser.Model public string ReleaseHash { get; set; } public int SeasonPart { get; set; } public string ReleaseTokens { get; set; } + public int? DailyPart { get; set; } public ParsedEpisodeInfo() { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 4b718f6ff..39dd62458 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -148,6 +148,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?.+?)(?:[-._ ][e])(?<episode>\d{2,3}(?!\d+))(?:(?:\-?[e])(?<episode>\d{2,3}(?!\d+)))+", RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Episodes with airdate and part (2018.04.28.Part.2) + new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})[-_. ]+(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])(?![-_. ]+[0-3][0-9])[-_. ]+Part[-_. ]?(?<part>[1-9])", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Mini-Series, treated as season 1, episodes are labelled as Part01, Part 01, Part.1 new Regex(@"^(?<title>.+?)(?:\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -808,7 +812,7 @@ namespace NzbDrone.Core.Parser //Try to Parse as a daily show var airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value); var airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value); - + //Swap day and month if month is bigger than 12 (scene fail) if (airmonth > 12) { @@ -843,12 +847,23 @@ namespace NzbDrone.Core.Parser ReleaseTitle = releaseTitle, AirDate = airDate.ToString(Episode.AIR_DATE_FORMAT), }; + + var partMatch = matchCollection[0].Groups["part"]; + + if (partMatch.Success) + { + result.DailyPart = Convert.ToInt32(partMatch.Value); + } } if (lastSeasonEpisodeStringIndex != releaseTitle.Length) + { result.ReleaseTokens = releaseTitle.Substring(lastSeasonEpisodeStringIndex); + } else + { result.ReleaseTokens = releaseTitle; + } result.SeriesTitle = seriesName; result.SeriesTitleInfo = GetSeriesTitleInfo(result.SeriesTitle); diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index a7920ec4a..e357cc0c4 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -113,7 +113,7 @@ namespace NzbDrone.Core.Parser if (parsedEpisodeInfo.IsDaily) { - var episodeInfo = GetDailyEpisode(series, parsedEpisodeInfo.AirDate, searchCriteria); + var episodeInfo = GetDailyEpisode(series, parsedEpisodeInfo.AirDate, parsedEpisodeInfo.DailyPart, searchCriteria); if (episodeInfo != null) { @@ -314,7 +314,7 @@ namespace NzbDrone.Core.Parser return series; } - private Episode GetDailyEpisode(Series series, string airDate, SearchCriteriaBase searchCriteria) + private Episode GetDailyEpisode(Series series, string airDate, int? part, SearchCriteriaBase searchCriteria) { Episode episodeInfo = null; @@ -326,7 +326,7 @@ namespace NzbDrone.Core.Parser if (episodeInfo == null) { - episodeInfo = _episodeService.FindEpisode(series.Id, airDate); + episodeInfo = _episodeService.FindEpisode(series.Id, airDate, part); } return episodeInfo; diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index 0fdda27d1..a1365c5cb 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Marr.Data.QGen; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Messaging.Events; @@ -17,8 +16,7 @@ namespace NzbDrone.Core.Tv { 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> Find(int seriesId, string date); List<Episode> GetEpisodes(int seriesId); List<Episode> GetEpisodes(int seriesId, int seasonNumber); List<Episode> GetEpisodeByFileId(int fileId); @@ -61,21 +59,11 @@ namespace NzbDrone.Core.Tv .SingleOrDefault(); } - public Episode Get(int seriesId, string date) + public List<Episode> Find(int seriesId, string date) { - var episode = FindOneByAirDate(seriesId, date); - - if (episode == null) - { - throw new InvalidOperationException("Expected at one episode"); - } - - return episode; - } - - public Episode Find(int seriesId, string date) - { - return FindOneByAirDate(seriesId, date); + return Query.Where(s => s.SeriesId == seriesId) + .AndWhere(s => s.AirDate == date) + .ToList(); } public List<Episode> GetEpisodes(int seriesId) diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index fe1ebc404..71565ef9d 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; @@ -21,8 +20,7 @@ namespace NzbDrone.Core.Tv Episode FindEpisodeByTitle(int seriesId, int seasonNumber, string releaseTitle); List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber); List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber); - Episode GetEpisode(int seriesId, string date); - Episode FindEpisode(int seriesId, string date); + Episode FindEpisode(int seriesId, string date, int? part); List<Episode> GetEpisodeBySeries(int seriesId); List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber); List<Episode> EpisodesWithFiles(int seriesId); @@ -85,14 +83,9 @@ namespace NzbDrone.Core.Tv return _episodeRepository.FindEpisodesBySceneNumbering(seriesId, sceneAbsoluteEpisodeNumber); } - public Episode GetEpisode(int seriesId, string date) + public Episode FindEpisode(int seriesId, string date, int? part) { - return _episodeRepository.Get(seriesId, date); - } - - public Episode FindEpisode(int seriesId, string date) - { - return _episodeRepository.Find(seriesId, date); + return FindOneByAirDate(seriesId, date, part); } public List<Episode> GetEpisodeBySeries(int seriesId) @@ -240,5 +233,34 @@ namespace NzbDrone.Core.Tv _logger.Debug("Linking [{0}] > [{1}]", message.EpisodeFile.RelativePath, episode); } } + + private Episode FindOneByAirDate(int seriesId, string date, int? part) + { + var episodes = _episodeRepository.Find(seriesId, date); + + if (!episodes.Any()) return null; + + if (episodes.Count == 1) return episodes.First(); + + _logger.Debug("Multiple episodes with the same air date were found, will exclude specials"); + + var regularEpisodes = episodes.Where(e => e.SeasonNumber > 0).ToList(); + + if (regularEpisodes.Count == 1 && !part.HasValue) + { + _logger.Debug("Left with one episode after excluding specials"); + return regularEpisodes.First(); + } + else if (part.HasValue && part.Value <= regularEpisodes.Count) + { + var sortedEpisodes = regularEpisodes.OrderBy(e => e.SeasonNumber) + .ThenBy(e => e.EpisodeNumber) + .ToList(); + + return sortedEpisodes[part.Value - 1]; + } + + throw new InvalidOperationException("Multiple episodes with the same air date found"); + } } }