New: Parse multi-part episodes using date

Closes #381
This commit is contained in:
Mark McDowall 2020-03-22 22:44:14 -07:00
parent 84b9488cfb
commit 00821b7ad6
9 changed files with 180 additions and 106 deletions

View File

@ -44,6 +44,23 @@ namespace NzbDrone.Core.Test.ParserTests
result.FullSeason.Should().BeFalse(); 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("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 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")] [TestCase("The.Daily.Show.{year}.{month}.{day}.Johnny.Knoxville.iTouch-MW")]

View File

@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
Mocker.GetMock<IEpisodeService>() Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>()), Times.Once()); .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once());
} }
[Test] [Test]
@ -98,7 +98,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Mocker.GetMock<IEpisodeService>() Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>()), Times.Never()); .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
} }
[Test] [Test]
@ -110,7 +110,20 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Mocker.GetMock<IEpisodeService>() Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>()), Times.Once()); .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), 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<IEpisodeService>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), 1), Times.Once());
} }
[Test] [Test]
@ -125,7 +138,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria); Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Mocker.GetMock<IEpisodeService>() Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>()), Times.Never()); .Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
} }
[Test] [Test]

View File

@ -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<EpisodeRepository, Episode>
{
private const int SERIES_ID = 1;
private const string AIR_DATE = "2014-04-02";
private void GivenEpisode(int seasonNumber)
{
var episode = Builder<Episode>.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<InvalidOperationException>(() => Subject.Get(SERIES_ID, AIR_DATE));
Assert.Throws<InvalidOperationException>(() => Subject.Find(SERIES_ID, AIR_DATE));
}
[Test]
public void should_throw_when_get_finds_no_episode()
{
Assert.Throws<InvalidOperationException>(() => 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();
}
}
}

View File

@ -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<EpisodeService>
{
private const int SERIES_ID = 1;
private const string AIR_DATE = "2014-04-02";
private Episode CreateEpisode(int seasonNumber, int episodeNumber)
{
var episode = Builder<Episode>.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<IEpisodeRepository>()
.Setup(s => s.Find(It.IsAny<int>(), It.IsAny<string>()))
.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<InvalidOperationException>(() => 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);
}
}
}

View File

@ -26,6 +26,7 @@ namespace NzbDrone.Core.Parser.Model
public string ReleaseHash { get; set; } public string ReleaseHash { get; set; }
public int SeasonPart { get; set; } public int SeasonPart { get; set; }
public string ReleaseTokens { get; set; } public string ReleaseTokens { get; set; }
public int? DailyPart { get; set; }
public ParsedEpisodeInfo() public ParsedEpisodeInfo()
{ {

View File

@ -148,6 +148,10 @@ namespace NzbDrone.Core.Parser
new Regex(@"^(?<title>.+?)(?:[-._ ][e])(?<episode>\d{2,3}(?!\d+))(?:(?:\-?[e])(?<episode>\d{2,3}(?!\d+)))+", new Regex(@"^(?<title>.+?)(?:[-._ ][e])(?<episode>\d{2,3}(?!\d+))(?:(?:\-?[e])(?<episode>\d{2,3}(?!\d+)))+",
RegexOptions.IgnoreCase | RegexOptions.Compiled), 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 //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+)))+)", new Regex(@"^(?<title>.+?)(?:\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+)))+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
@ -808,7 +812,7 @@ namespace NzbDrone.Core.Parser
//Try to Parse as a daily show //Try to Parse as a daily show
var airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value); var airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value);
var airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value); var airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value);
//Swap day and month if month is bigger than 12 (scene fail) //Swap day and month if month is bigger than 12 (scene fail)
if (airmonth > 12) if (airmonth > 12)
{ {
@ -843,12 +847,23 @@ namespace NzbDrone.Core.Parser
ReleaseTitle = releaseTitle, ReleaseTitle = releaseTitle,
AirDate = airDate.ToString(Episode.AIR_DATE_FORMAT), 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) if (lastSeasonEpisodeStringIndex != releaseTitle.Length)
{
result.ReleaseTokens = releaseTitle.Substring(lastSeasonEpisodeStringIndex); result.ReleaseTokens = releaseTitle.Substring(lastSeasonEpisodeStringIndex);
}
else else
{
result.ReleaseTokens = releaseTitle; result.ReleaseTokens = releaseTitle;
}
result.SeriesTitle = seriesName; result.SeriesTitle = seriesName;
result.SeriesTitleInfo = GetSeriesTitleInfo(result.SeriesTitle); result.SeriesTitleInfo = GetSeriesTitleInfo(result.SeriesTitle);

View File

@ -113,7 +113,7 @@ namespace NzbDrone.Core.Parser
if (parsedEpisodeInfo.IsDaily) if (parsedEpisodeInfo.IsDaily)
{ {
var episodeInfo = GetDailyEpisode(series, parsedEpisodeInfo.AirDate, searchCriteria); var episodeInfo = GetDailyEpisode(series, parsedEpisodeInfo.AirDate, parsedEpisodeInfo.DailyPart, searchCriteria);
if (episodeInfo != null) if (episodeInfo != null)
{ {
@ -314,7 +314,7 @@ namespace NzbDrone.Core.Parser
return series; 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; Episode episodeInfo = null;
@ -326,7 +326,7 @@ namespace NzbDrone.Core.Parser
if (episodeInfo == null) if (episodeInfo == null)
{ {
episodeInfo = _episodeService.FindEpisode(series.Id, airDate); episodeInfo = _episodeService.FindEpisode(series.Id, airDate, part);
} }
return episodeInfo; return episodeInfo;

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Marr.Data.QGen; using Marr.Data.QGen;
using NLog; using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Messaging.Events; 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 season, int episodeNumber);
Episode Find(int seriesId, int absoluteEpisodeNumber); Episode Find(int seriesId, int absoluteEpisodeNumber);
Episode Get(int seriesId, string date); List<Episode> Find(int seriesId, string date);
Episode Find(int seriesId, string date);
List<Episode> GetEpisodes(int seriesId); List<Episode> GetEpisodes(int seriesId);
List<Episode> GetEpisodes(int seriesId, int seasonNumber); List<Episode> GetEpisodes(int seriesId, int seasonNumber);
List<Episode> GetEpisodeByFileId(int fileId); List<Episode> GetEpisodeByFileId(int fileId);
@ -61,21 +59,11 @@ namespace NzbDrone.Core.Tv
.SingleOrDefault(); .SingleOrDefault();
} }
public Episode Get(int seriesId, string date) public List<Episode> Find(int seriesId, string date)
{ {
var episode = FindOneByAirDate(seriesId, date); return Query.Where(s => s.SeriesId == seriesId)
.AndWhere(s => s.AirDate == date)
if (episode == null) .ToList();
{
throw new InvalidOperationException("Expected at one episode");
}
return episode;
}
public Episode Find(int seriesId, string date)
{
return FindOneByAirDate(seriesId, date);
} }
public List<Episode> GetEpisodes(int seriesId) public List<Episode> GetEpisodes(int seriesId)

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -21,8 +20,7 @@ namespace NzbDrone.Core.Tv
Episode FindEpisodeByTitle(int seriesId, int seasonNumber, string releaseTitle); Episode FindEpisodeByTitle(int seriesId, int seasonNumber, string releaseTitle);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber); List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber); List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
Episode GetEpisode(int seriesId, string date); Episode FindEpisode(int seriesId, string date, int? part);
Episode FindEpisode(int seriesId, string date);
List<Episode> GetEpisodeBySeries(int seriesId); List<Episode> GetEpisodeBySeries(int seriesId);
List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber); List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber);
List<Episode> EpisodesWithFiles(int seriesId); List<Episode> EpisodesWithFiles(int seriesId);
@ -85,14 +83,9 @@ namespace NzbDrone.Core.Tv
return _episodeRepository.FindEpisodesBySceneNumbering(seriesId, sceneAbsoluteEpisodeNumber); 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); return FindOneByAirDate(seriesId, date, part);
}
public Episode FindEpisode(int seriesId, string date)
{
return _episodeRepository.Find(seriesId, date);
} }
public List<Episode> GetEpisodeBySeries(int seriesId) public List<Episode> GetEpisodeBySeries(int seriesId)
@ -240,5 +233,34 @@ namespace NzbDrone.Core.Tv
_logger.Debug("Linking [{0}] > [{1}]", message.EpisodeFile.RelativePath, episode); _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");
}
} }
} }