diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index eafb09a6a34..72adfb2d96a 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -10,12 +10,17 @@ namespace Emby.Naming.TV /// public static partial class SeasonPathParser { + private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled); + [GeneratedRegex(@"^\s*((?(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?.*)$", RegexOptions.IgnoreCase)] private static partial Regex ProcessPre(); - [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?(?>\d+)(?!\s*[Ee]\d+))(?.*)$", RegexOptions.IgnoreCase)] + [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?.*)$", RegexOptions.IgnoreCase)] private static partial Regex ProcessPost(); + [GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)] + private static partial Regex SeasonPrefix(); + /// /// Attempts to parse season number from path. /// @@ -56,44 +61,34 @@ namespace Emby.Naming.TV bool supportSpecialAliases, bool supportNumericSeasonFolders) { - string filename = Path.GetFileName(path); - filename = Regex.Replace(filename, "[ ._-]", string.Empty); + var fileName = Path.GetFileName(path); + + var seasonPrefixMatch = SeasonPrefix().Match(fileName); + if (seasonPrefixMatch.Success && + int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) + { + return (val, true); + } + + string filename = CleanNameRegex.Replace(fileName, string.Empty); if (parentFolderName is not null) { - parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty); - filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase); + var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty); + filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase); } - if (supportSpecialAliases) + if (supportSpecialAliases && + (filename.Equals("specials", StringComparison.OrdinalIgnoreCase) || + filename.Equals("extras", StringComparison.OrdinalIgnoreCase))) { - if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase)) - { - return (0, true); - } - - if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase)) - { - return (0, true); - } + return (0, true); } - if (supportNumericSeasonFolders) + if (supportNumericSeasonFolders && + int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) { - if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return (val, true); - } - } - - if (filename.Length > 0 && (filename[0] == 'S' || filename[0] == 's')) - { - var testFilename = filename.AsSpan()[1..]; - - if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return (val, true); - } + return (val, true); } var preMatch = ProcessPre().Match(filename); diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs index 0c3671f4fb7..4dbe769bf49 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs @@ -69,6 +69,12 @@ public class SeasonPathParserTests [InlineData("/media/YouTube/Devyn Johnston/2024-01-24 4070 Ti SUPER in under 7 minutes", "/media/YouTube/Devyn Johnston", null, false)] [InlineData("/media/YouTube/Devyn Johnston/2025-01-28 5090 vs 2 SFF Cases", "/media/YouTube/Devyn Johnston", null, false)] [InlineData("/Drive/202401244070", "/Drive", null, false)] + [InlineData("/Drive/Drive.S01.2160p.WEB-DL.DDP5.1.H.265-XXXX", "/Drive", 1, true)] + [InlineData("The Wonder Years/The.Wonder.Years.S04.1080p.PDTV.x264-JCH", "/The Wonder Years", 4, true)] + [InlineData("The Wonder Years/[The.Wonder.Years.S04.1080p.PDTV.x264-JCH]", "/The Wonder Years", 4, true)] + [InlineData("The Wonder Years/The.Wonder.Years [S04][1080p.PDTV.x264-JCH]", "/The Wonder Years", 4, true)] + [InlineData("The Wonder Years/The Wonder Years Season 01 1080p", "/The Wonder Years", 1, true)] + public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory) { var result = SeasonPathParser.Parse(path, parentPath, true, true);