diff --git a/src/NzbDrone.Api/Parse/ParseModule.cs b/src/NzbDrone.Api/Parse/ParseModule.cs index 0dec532ae..8140cf93f 100644 --- a/src/NzbDrone.Api/Parse/ParseModule.cs +++ b/src/NzbDrone.Api/Parse/ParseModule.cs @@ -3,6 +3,7 @@ using NzbDrone.Api.Series; using NzbDrone.Common.Extensions; using NzbDrone.Core.Parser; using Sonarr.Http; +using Sonarr.Http.REST; namespace NzbDrone.Api.Parse { @@ -21,6 +22,12 @@ namespace NzbDrone.Api.Parse { var title = Request.Query.Title.Value as string; var path = Request.Query.Path.Value as string; + + if (path.IsNullOrWhiteSpace() && title.IsNullOrWhiteSpace()) + { + throw new BadRequestException("title or path is missing"); + } + var parsedEpisodeInfo = path.IsNotNullOrWhiteSpace() ? Parser.ParsePath(path) : Parser.ParseTitle(title); if (parsedEpisodeInfo == null) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs index bf4b399b5..b9ddd0e4f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs @@ -1,5 +1,7 @@ -using Moq; +using FluentAssertions; +using Moq; using NUnit.Framework; +using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Parser; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -43,5 +45,18 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests .Verify(s => s.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, parsedEpisodeInfo.SeriesTitleInfo.Year), Times.Once()); } + + [Test] + public void should_parse_concatenated_title() + { + var series = new Series { TvdbId = 100 }; + Mocker.GetMock().Setup(v => v.FindByTitle("Welcome")).Returns(series); + Mocker.GetMock().Setup(v => v.FindTvdbId("Mairimashita", It.IsAny())).Returns(100); + + var result = Subject.GetSeries("Welcome (Mairimashita).S01E01.720p.WEB-DL-Viva"); + + result.Should().NotBeNull(); + result.TvdbId.Should().Be(100); + } } } diff --git a/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs b/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs index e9befbf39..086c80f99 100644 --- a/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs @@ -5,5 +5,6 @@ public string Title { get; set; } public string TitleWithoutYear { get; set; } public int Year { get; set; } + public string[] AllTitles { get; set; } } } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 53a10cdbb..daa4ccf56 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -385,6 +385,9 @@ namespace NzbDrone.Core.Parser private static readonly Regex YearInTitleRegex = new Regex(@"^(?.+?)(?:\W|_)?(?<year>\d{4})", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex TitleComponentsRegex = new Regex(@"^(?<title>.+?) \((?<title>.+?)\)$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|\|)+", RegexOptions.Compiled); private static readonly Regex PunctuationRegex = new Regex(@"[^\w\s]", RegexOptions.Compiled); private static readonly Regex CommonWordRegex = new Regex(@"\b(a|an|the|and|or|of)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -670,13 +673,19 @@ namespace NzbDrone.Core.Parser { seriesTitleInfo.TitleWithoutYear = title; } - else { seriesTitleInfo.TitleWithoutYear = match.Groups["title"].Value; seriesTitleInfo.Year = Convert.ToInt32(match.Groups["year"].Value); } + var matchComponents = TitleComponentsRegex.Match(seriesTitleInfo.TitleWithoutYear); + + if (matchComponents.Success) + { + seriesTitleInfo.AllTitles = matchComponents.Groups["title"].Captures.OfType<Capture>().Select(v => v.Value).ToArray(); + } + return seriesTitleInfo; } diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index fc9b28b35..f0862a5e0 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -57,6 +57,11 @@ namespace NzbDrone.Core.Parser var series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); + if (series == null && parsedEpisodeInfo.SeriesTitleInfo.AllTitles != null) + { + series = GetSeriesByAllTitles(parsedEpisodeInfo); + } + if (series == null) { series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, @@ -66,6 +71,49 @@ namespace NzbDrone.Core.Parser return series; } + private Series GetSeriesByAllTitles(ParsedEpisodeInfo parsedEpisodeInfo) + { + Series foundSeries = null; + int? foundTvdbId = null; + + // Match each title individually, they must all resolve to the same tvdbid + foreach (var title in parsedEpisodeInfo.SeriesTitleInfo.AllTitles) + { + var series = _seriesService.FindByTitle(title); + var tvdbId = series?.TvdbId; + + if (series == null) + { + tvdbId = _sceneMappingService.FindTvdbId(title, parsedEpisodeInfo.ReleaseTitle); + } + + if (!tvdbId.HasValue) + { + _logger.Trace("Title {0} not matching any series.", title); + return null; + } + + if (foundTvdbId.HasValue && tvdbId != foundTvdbId) + { + _logger.Trace("Title {0} both matches tvdbid {1} and {2}, no series selected.", parsedEpisodeInfo.SeriesTitle, foundTvdbId, tvdbId); + return null; + } + + if (foundSeries == null) + { + foundSeries = series; + } + foundTvdbId = tvdbId; + } + + if (foundSeries == null && foundTvdbId.HasValue) + { + foundSeries = _seriesService.FindByTvdbId(foundTvdbId.Value); + } + + return foundSeries; + } + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) { var remoteEpisode = new RemoteEpisode @@ -270,6 +318,11 @@ namespace NzbDrone.Core.Parser series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); + if (series == null && parsedEpisodeInfo.SeriesTitleInfo.AllTitles != null) + { + series = GetSeriesByAllTitles(parsedEpisodeInfo); + } + if (series == null && parsedEpisodeInfo.SeriesTitleInfo.Year > 0) { series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, parsedEpisodeInfo.SeriesTitleInfo.Year); diff --git a/src/Sonarr.Api.V3/Parse/ParseModule.cs b/src/Sonarr.Api.V3/Parse/ParseModule.cs index 05cef8534..04f955758 100644 --- a/src/Sonarr.Api.V3/Parse/ParseModule.cs +++ b/src/Sonarr.Api.V3/Parse/ParseModule.cs @@ -3,6 +3,7 @@ using NzbDrone.Core.Parser; using Sonarr.Api.V3.Episodes; using Sonarr.Api.V3.Series; using Sonarr.Http; +using Sonarr.Http.REST; namespace Sonarr.Api.V3.Parse { @@ -21,6 +22,12 @@ namespace Sonarr.Api.V3.Parse { var title = Request.Query.Title.Value as string; var path = Request.Query.Path.Value as string; + + if (path.IsNullOrWhiteSpace() && title.IsNullOrWhiteSpace()) + { + throw new BadRequestException("title or path is missing"); + } + var parsedEpisodeInfo = path.IsNotNullOrWhiteSpace() ? Parser.ParsePath(path) : Parser.ParseTitle(title); if (parsedEpisodeInfo == null)