1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-12-14 11:23:42 +02:00

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 @@ public void should_parse_daily_episode(string postTitle, string title, int year,
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")]

View File

@ -86,7 +86,7 @@ public void should_get_daily_episode_episode_when_search_criteria_is_null()
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
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]
@ -98,7 +98,7 @@ public void should_use_search_criteria_episode_when_it_matches_daily()
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
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]
@ -110,7 +110,20 @@ public void should_fallback_to_daily_episode_lookup_when_search_criteria_episode
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
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]
@ -125,7 +138,7 @@ public void should_use_search_criteria_episode_when_it_matches_absolute()
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
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]

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 @@ public class ParsedEpisodeInfo
public string ReleaseHash { get; set; }
public int SeasonPart { get; set; }
public string ReleaseTokens { get; set; }
public int? DailyPart { get; set; }
public ParsedEpisodeInfo()
{

View File

@ -148,6 +148,10 @@ public static class Parser
new Regex(@"^(?<title>.+?)(?:[-._ ][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 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle
//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 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle
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);

View File

@ -113,7 +113,7 @@ public List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series ser
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 @@ private Series GetSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tv
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 @@ private Episode GetDailyEpisode(Series series, string airDate, SearchCriteriaBas
if (episodeInfo == null)
{
episodeInfo = _episodeService.FindEpisode(series.Id, airDate);
episodeInfo = _episodeService.FindEpisode(series.Id, airDate, part);
}
return episodeInfo;

View File

@ -3,7 +3,6 @@
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 @@ 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> 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 @@ public Episode Find(int seriesId, int absoluteEpisodeNumber)
.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)

View File

@ -2,7 +2,6 @@
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 @@ public interface IEpisodeService
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 @@ public List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsolut
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 @@ public void Handle(EpisodeFileAddedEvent message)
_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");
}
}
}