New: Support parsing season number from season folder when importing

Closes #903
pull/6643/head
Mark McDowall 2 months ago committed by Mark McDowall
parent 88de927435
commit 40bac23698

@ -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 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\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 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 // [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) 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("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) public void should_parse_multi_episode_from_path(string path, string title, int season, int[] episodes)
{ {
var result = Parser.Parser.ParsePath(path.AsOsAgnostic()); var result = Parser.Parser.ParsePath(path.AsOsAgnostic());

@ -50,10 +50,10 @@ namespace NzbDrone.Core.Organizer
private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ ,a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}", private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ ,a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", public static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})", public static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?<absolute>\{absolute(?:\:0+)?})", private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?<absolute>\{absolute(?:\:0+)?})",

@ -75,6 +75,7 @@ namespace NzbDrone.Core.Organizer
} }
return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) ||
(FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) ||
FileNameValidation.OriginalTokenRegex.IsMatch(value); FileNameValidation.OriginalTokenRegex.IsMatch(value);
} }
} }
@ -91,6 +92,7 @@ namespace NzbDrone.Core.Organizer
} }
return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) ||
(FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) ||
FileNameBuilder.AirDateRegex.IsMatch(value) || FileNameBuilder.AirDateRegex.IsMatch(value) ||
FileNameValidation.OriginalTokenRegex.IsMatch(value); FileNameValidation.OriginalTokenRegex.IsMatch(value);
} }
@ -109,6 +111,7 @@ namespace NzbDrone.Core.Organizer
} }
return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) ||
(FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) ||
FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value) || FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value) ||
FileNameValidation.OriginalTokenRegex.IsMatch(value); FileNameValidation.OriginalTokenRegex.IsMatch(value);
} }

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@ -20,7 +21,9 @@ namespace NzbDrone.Core.Organizer
public ValidationFailure ValidateStandardFilename(SampleResult sampleResult) public ValidationFailure ValidateStandardFilename(SampleResult sampleResult)
{ {
var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE); 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) if (parsedEpisodeInfo == null)
{ {
@ -38,7 +41,9 @@ namespace NzbDrone.Core.Organizer
public ValidationFailure ValidateDailyFilename(SampleResult sampleResult) public ValidationFailure ValidateDailyFilename(SampleResult sampleResult)
{ {
var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE); 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) if (parsedEpisodeInfo == null)
{ {
@ -66,7 +71,9 @@ namespace NzbDrone.Core.Organizer
public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult) public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult)
{ {
var validationFailure = new ValidationFailure("AnimeEpisodeFormat", ERROR_MESSAGE); 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) if (parsedEpisodeInfo == null)
{ {

@ -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 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 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 DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled);
private static readonly Regex SeasonFolderRegex = new Regex(@"^(?:S|Season|Saison|Series|Stagione)[-_. ]*(?<season>(?<!\d+)\d{1,4}(?!\d+))(?:[_. ]+(?!\d+)|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleEpisodeNumberRegex = new Regex(@"^[ex]?(?<episode>(?<!\d+)\d{1,3}(?!\d+))(?:[ex-](?<episode>(?<!\d+)\d{1,3}(?!\d+)))?(?:[_. ]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled); private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled);
@ -563,6 +565,28 @@ namespace NzbDrone.Core.Parser
{ {
var fileInfo = new FileInfo(path); var fileInfo = new FileInfo(path);
// Parse using the folder and file separately, but combine if they both parse correctly.
var episodeNumberMatch = SimpleEpisodeNumberRegex.Matches(fileInfo.Name);
if (episodeNumberMatch.Count != 0 && fileInfo.Directory?.Name != null)
{
var parsedFileInfo = ParseMatchCollection(episodeNumberMatch, fileInfo.Name);
if (parsedFileInfo != null)
{
var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name);
if (seasonMatch.Success && seasonMatch.Groups["season"].Success)
{
parsedFileInfo.SeasonNumber = int.Parse(seasonMatch.Groups["season"].Value);
Logger.Debug("Episode parsed from file and folder names. {0}", parsedFileInfo);
return parsedFileInfo;
}
}
}
var result = ParseTitle(fileInfo.Name); var result = ParseTitle(fileInfo.Name);
if (result == null && int.TryParse(Path.GetFileNameWithoutExtension(fileInfo.Name), out var number)) if (result == null && int.TryParse(Path.GetFileNameWithoutExtension(fileInfo.Name), out var number))

Loading…
Cancel
Save