diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 2cee27fdc..66bd95291 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -169,7 +169,9 @@ + + diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs new file mode 100644 index 000000000..bad109bf9 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests +{ + [TestFixture] + public class GetSeriesFixture : CoreTest + { + [Test] + public void should_use_passed_in_title_when_it_cannot_be_parsed() + { + const string title = "30 Rock"; + + Subject.GetSeries(title); + + Mocker.GetMock() + .Verify(s => s.FindByTitle(title), Times.Once()); + } + + [Test] + public void should_use_parsed_series_title() + { + const string title = "30.Rock.S01E01.720p.hdtv"; + + Subject.GetSeries(title); + + Mocker.GetMock() + .Verify(s => s.FindByTitle(Parser.Parser.ParseTitle(title).SeriesTitle), Times.Once()); + } + + [Test] + public void should_fallback_to_title_without_year_and_year_when_title_lookup_fails() + { + const string title = "House.2004.S01E01.720p.hdtv"; + var parsedEpisodeInfo = Parser.Parser.ParseTitle(title); + + Subject.GetSeries(title); + + Mocker.GetMock() + .Verify(s => s.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, + parsedEpisodeInfo.SeriesTitleInfo.Year), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs new file mode 100644 index 000000000..5f2e00b9c --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + [TestFixture] + public class SeriesTitleInfoFixture : CoreTest + { + [Test] + public void should_have_year_zero_when_title_doesnt_have_a_year() + { + const string title = "House.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.Year.Should().Be(0); + } + + [Test] + public void should_have_same_title_for_title_and_title_without_year_when_title_doesnt_have_a_year() + { + const string title = "House.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.Title.Should().Be(result.TitleWithoutYear); + } + + [Test] + public void should_have_year_when_title_has_a_year() + { + const string title = "House.2004.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.Year.Should().Be(2004); + } + + [Test] + public void should_have_year_in_title_when_title_has_a_year() + { + const string title = "House.2004.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.Title.Should().Be("house2004"); + } + + [Test] + public void should_title_without_year_should_not_contain_year() + { + const string title = "House.2004.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.TitleWithoutYear.Should().Be("house"); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 7a7d5b516..9007ad74e 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -319,6 +319,7 @@ + diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index fe89f6dee..14fd53a80 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.Parser.Model public class ParsedEpisodeInfo { public string SeriesTitle { get; set; } + public SeriesTitleInfo SeriesTitleInfo { get; set; } public QualityModel Quality { get; set; } public int SeasonNumber { get; set; } public int[] EpisodeNumbers { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs b/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs new file mode 100644 index 000000000..5ced83c40 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Parser.Model +{ + public class SeriesTitleInfo + { + public string Title { get; set; } + public string TitleWithoutYear { get; set; } + public int Year { get; set; } + } +} diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index a58464c96..9122245ab 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -76,6 +76,9 @@ public static class Parser private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?ita|italian)|(?german\b)|(?flemish)|(?greek)|(?(?:\W|_)FR)(?:\W|_)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex YearInTitleRegex = new Regex(@"^(?.+?)(?:\W|_)?(?<year>\d{4})", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + public static ParsedEpisodeInfo ParsePath(string path) { var fileInfo = new FileInfo(path); @@ -139,6 +142,58 @@ public static ParsedEpisodeInfo ParseTitle(string title) return null; } + public static string ParseSeriesName(string title) + { + Logger.Trace("Parsing string '{0}'", title); + + var parseResult = ParseTitle(title); + + if (parseResult == null) + { + return CleanSeriesTitle(title); + } + + return parseResult.SeriesTitle; + } + + public static string CleanSeriesTitle(this string title) + { + long number = 0; + + //If Title only contains numbers return it as is. + if (Int64.TryParse(title, out number)) + return title; + + return NormalizeRegex.Replace(title, String.Empty).ToLower(); + } + + public static string CleanupEpisodeTitle(string title) + { + //this will remove (1),(2) from the end of multi part episodes. + return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); + } + + private static SeriesTitleInfo GetSeriesTitleInfo(string title) + { + var seriesTitleInfo = new SeriesTitleInfo(); + seriesTitleInfo.Title = title; + + var match = YearInTitleRegex.Match(title); + + if (!match.Success) + { + seriesTitleInfo.TitleWithoutYear = title; + } + + else + { + seriesTitleInfo.TitleWithoutYear = match.Groups["title"].Value; + seriesTitleInfo.Year = Convert.ToInt32(match.Groups["year"].Value); + } + + return seriesTitleInfo; + } + private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchCollection) { var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' '); @@ -168,10 +223,10 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle return null; result = new ParsedEpisodeInfo - { - SeasonNumber = seasons.First(), - EpisodeNumbers = new int[0], - }; + { + SeasonNumber = seasons.First(), + EpisodeNumbers = new int[0], + }; foreach (Match matchGroup in matchCollection) { @@ -226,32 +281,19 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle } result = new ParsedEpisodeInfo - { - AirDate = airDate.ToString(Episode.AIR_DATE_FORMAT), - }; + { + AirDate = airDate.ToString(Episode.AIR_DATE_FORMAT), + }; } result.SeriesTitle = CleanSeriesTitle(seriesName); + result.SeriesTitleInfo = GetSeriesTitleInfo(result.SeriesTitle); Logger.Trace("Episode Parsed. {0}", result); return result; } - public static string ParseSeriesName(string title) - { - Logger.Trace("Parsing string '{0}'", title); - - var parseResult = ParseTitle(title); - - if (parseResult == null) - { - return CleanSeriesTitle(title); - } - - return parseResult.SeriesTitle; - } - private static Language ParseLanguage(string title) { var lowerTitle = title.ToLower(); @@ -345,22 +387,5 @@ private static bool ValidateBeforeParsing(string title) return true; } - - public static string CleanSeriesTitle(this string title) - { - long number = 0; - - //If Title only contains numbers return it as is. - if (Int64.TryParse(title, out number)) - return title; - - return NormalizeRegex.Replace(title, String.Empty).ToLower(); - } - - public static string CleanupEpisodeTitle(string title) - { - //this will remove (1),(2) from the end of multi part episodes. - return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); - } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 134e507e3..3cd3aa407 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -68,15 +68,22 @@ public LocalEpisode GetEpisodes(string filename, Series series, bool sceneSource public Series GetSeries(string title) { - var searchTitle = title; var parsedEpisodeInfo = Parser.ParseTitle(title); - if (parsedEpisodeInfo != null) + if (parsedEpisodeInfo == null) { - searchTitle = parsedEpisodeInfo.SeriesTitle; + return _seriesService.FindByTitle(title); } - return _seriesService.FindByTitle(searchTitle); + var series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); + + if (series == null) + { + series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, + parsedEpisodeInfo.SeriesTitleInfo.Year); + } + + return series; } public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvRageId, SearchCriteriaBase searchCriteria = null) diff --git a/src/NzbDrone.Core/Tv/SeriesRepository.cs b/src/NzbDrone.Core/Tv/SeriesRepository.cs index 0c7d0288e..dbdc1c191 100644 --- a/src/NzbDrone.Core/Tv/SeriesRepository.cs +++ b/src/NzbDrone.Core/Tv/SeriesRepository.cs @@ -10,6 +10,7 @@ public interface ISeriesRepository : IBasicRepository<Series> { bool SeriesPathExists(string path); Series FindByTitle(string cleanTitle); + Series FindByTitle(string cleanTitle, int year); Series FindByTvdbId(int tvdbId); Series FindByTvRageId(int tvRageId); void SetSeriesType(int seriesId, SeriesTypes seriesTypes); @@ -32,6 +33,12 @@ public Series FindByTitle(string cleanTitle) return Query.SingleOrDefault(s => s.CleanTitle.Equals(cleanTitle, StringComparison.InvariantCultureIgnoreCase)); } + public Series FindByTitle(string cleanTitle, int year) + { + return Query.SingleOrDefault(s => s.CleanTitle.Equals(cleanTitle, StringComparison.InvariantCultureIgnoreCase) && + s.Year == year); + } + public Series FindByTvdbId(int tvdbId) { return Query.SingleOrDefault(s => s.TvdbId.Equals(tvdbId)); diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 7496a15e7..6f94faa84 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -19,6 +19,7 @@ public interface ISeriesService Series FindByTvdbId(int tvdbId); Series FindByTvRageId(int tvRageId); Series FindByTitle(string title); + Series FindByTitle(string title, int year); void SetSeriesType(int seriesId, SeriesTypes seriesTypes); void DeleteSeries(int seriesId, bool deleteFiles); List<Series> GetAllSeries(); @@ -100,6 +101,11 @@ public Series FindByTitle(string title) return _seriesRepository.FindByTitle(Parser.Parser.CleanSeriesTitle(title)); } + public Series FindByTitle(string title, int year) + { + return _seriesRepository.FindByTitle(title, year); + } + public void SetSeriesType(int seriesId, SeriesTypes seriesTypes) { _seriesRepository.SetSeriesType(seriesId, seriesTypes);