New: Support parsing season number from season folder when importing

Closes #903
This commit is contained in:
Mark McDowall 2024-03-20 16:48:01 -07:00 committed by Mark McDowall
parent 88de927435
commit 40bac23698
5 changed files with 43 additions and 6 deletions

View File

@ -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());

View File

@ -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>[- ._)\]]*)\}",
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);
private static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})",
public static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?<absolute>\{absolute(?:\:0+)?})",

View File

@ -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);
}

View File

@ -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)
{

View File

@ -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)[-_. ]*(?<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);
@ -563,6 +565,28 @@ namespace NzbDrone.Core.Parser
{
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);
if (result == null && int.TryParse(Path.GetFileNameWithoutExtension(fileInfo.Name), out var number))