diff --git a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs index 03bc64fa5..8ef926316 100644 --- a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs @@ -29,6 +29,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase(@"C:\Test\Series\Season 01\1 Pilot (1080p HD).mkv", 1, 1)] [TestCase(@"C:\Test\Series\Season 1\02 Honor Thy Father (1080p HD).m4v", 1, 2)] [TestCase(@"C:\Test\Series\Season 1\2 Honor Thy Developer (1080p HD).m4v", 1, 2)] + [TestCase(@"C:\Test\Series\Season 2 - Total Series Action\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)] + [TestCase(@"C:\Test\Series\Season 2\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)] // [TestCase(@"C:\series.state.S02E04.720p.WEB-DL.DD5.1.H.264\73696S02-04.mkv", 2, 4)] //Gets treated as S01E04 (because it gets parsed as anime); 2020-01 broken test case: Expected result.EpisodeNumbers to contain 1 item(s), but found 0 public void should_parse_from_path(string path, int season, int episode) @@ -45,6 +47,7 @@ namespace NzbDrone.Core.Test.ParserTests } [TestCase("01-03\\The Series Title (2010) - 1x01-02-03 - Episode Title HDTV-720p Proper", "The Series Title (2010)", 1, new[] { 1, 2, 3 })] + [TestCase("Season 2\\E05-06 - Episode Title HDTV-720p Proper", "", 2, new[] { 5, 6 })] public void should_parse_multi_episode_from_path(string path, string title, int season, int[] episodes) { var result = Parser.Parser.ParsePath(path.AsOsAgnostic()); diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 7e03172ef..ab532f0a5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -50,10 +50,10 @@ namespace NzbDrone.Core.Organizer private static readonly Regex TitleRegex = new Regex(@"(?\{\{|\}\})|\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[ ,a-z0-9+-]+(?[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", + public static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", + public static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index bcda9c884..e8d39469f 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -75,6 +75,7 @@ namespace NzbDrone.Core.Organizer } return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || + (FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) || FileNameValidation.OriginalTokenRegex.IsMatch(value); } } @@ -91,6 +92,7 @@ namespace NzbDrone.Core.Organizer } return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || + (FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) || FileNameBuilder.AirDateRegex.IsMatch(value) || FileNameValidation.OriginalTokenRegex.IsMatch(value); } @@ -109,6 +111,7 @@ namespace NzbDrone.Core.Organizer } return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || + (FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) || FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value) || FileNameValidation.OriginalTokenRegex.IsMatch(value); } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs index 9367c11d8..8a50137fd 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.IO; using System.Linq; using FluentValidation.Results; using NzbDrone.Core.Parser.Model; @@ -20,7 +21,9 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateStandardFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar) + ? Parser.Parser.ParsePath(sampleResult.FileName) + : Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { @@ -38,7 +41,9 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateDailyFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar) + ? Parser.Parser.ParsePath(sampleResult.FileName) + : Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { @@ -66,7 +71,9 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("AnimeEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar) + ? Parser.Parser.ParsePath(sampleResult.FileName) + : Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 3c15ca812..9104af5ed 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -554,6 +554,8 @@ namespace NzbDrone.Core.Parser private static readonly Regex ArticleWordRegex = new Regex(@"^(a|an|the)\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled); + private static readonly Regex SeasonFolderRegex = new Regex(@"^(?:S|Season|Saison|Series|Stagione)[-_. ]*(?(?(?(?