New: Support parsing season number from season folder when importing
Closes #903
This commit is contained in:
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…
Reference in New Issue