diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index 45b91971bf..98ee1e4b8f 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -1,43 +1,35 @@
using System;
using System.Globalization;
using System.IO;
+using System.Text.RegularExpressions;
namespace Emby.Naming.TV
{
///
/// Class to parse season paths.
///
- public static class SeasonPathParser
+ public static partial class SeasonPathParser
{
- ///
- /// A season folder must contain one of these somewhere in the name.
- ///
- private static readonly string[] _seasonFolderNames =
- {
- "season",
- "sæson",
- "temporada",
- "saison",
- "staffel",
- "series",
- "сезон",
- "stagione"
- };
+ [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*(?.*)$")]
+ private static partial Regex ProcessPre();
- private static readonly char[] _splitChars = ['.', '_', ' ', '-'];
+ [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+))(?.*)$")]
+ private static partial Regex ProcessPost();
///
/// Attempts to parse season number from path.
///
/// Path to season.
+ /// Folder name of the parent.
/// Support special aliases when parsing.
/// Support numeric season folders when parsing.
/// Returns object.
- public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
+ public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{
var result = new SeasonPathParserResult();
+ var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
- var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
+ var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders);
result.SeasonNumber = seasonNumber;
@@ -54,15 +46,24 @@ namespace Emby.Naming.TV
/// Gets the season number from path.
///
/// The path.
+ /// The parent folder name.
/// if set to true [support special aliases].
/// if set to true [support numeric season folders].
/// System.Nullable{System.Int32}.
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
string path,
+ string? parentFolderName,
bool supportSpecialAliases,
bool supportNumericSeasonFolders)
{
string filename = Path.GetFileName(path);
+ filename = Regex.Replace(filename, "[ ._-]", string.Empty);
+
+ if (parentFolderName is not null)
+ {
+ parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty);
+ filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
if (supportSpecialAliases)
{
@@ -85,53 +86,38 @@ namespace Emby.Naming.TV
}
}
- if (TryGetSeasonNumberFromPart(filename, out int seasonNumber))
+ if (filename.StartsWith('s'))
{
+ var testFilename = filename.AsSpan()[1..];
+
+ if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
+ {
+ return (val, true);
+ }
+ }
+
+ var preMatch = ProcessPre().Match(filename);
+ if (preMatch.Success)
+ {
+ return CheckMatch(preMatch);
+ }
+ else
+ {
+ var postMatch = ProcessPost().Match(filename);
+ return CheckMatch(postMatch);
+ }
+ }
+
+ private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
+ {
+ var numberString = match.Groups["seasonnumber"];
+ if (numberString.Success)
+ {
+ var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture);
return (seasonNumber, true);
}
- // Look for one of the season folder names
- foreach (var name in _seasonFolderNames)
- {
- if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
- {
- var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
- if (result.SeasonNumber.HasValue)
- {
- return result;
- }
-
- break;
- }
- }
-
- var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries);
- foreach (var part in parts)
- {
- if (TryGetSeasonNumberFromPart(part, out seasonNumber))
- {
- return (seasonNumber, true);
- }
- }
-
- return (null, true);
- }
-
- private static bool TryGetSeasonNumberFromPart(ReadOnlySpan part, out int seasonNumber)
- {
- seasonNumber = 0;
- if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- seasonNumber = value;
- return true;
- }
-
- return false;
+ return (null, false);
}
///
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 3432aa3222..b810ad4de1 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2512,8 +2512,11 @@ namespace Emby.Server.Implementations.Library
}
///
- public int? GetSeasonNumberFromPath(string path)
- => SeasonPathParser.Parse(path, true, true).SeasonNumber;
+ public int? GetSeasonNumberFromPath(string path, Guid? parentId)
+ {
+ var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
+ return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
+ }
///
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index abf2d01159..6cb63a28a2 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path;
- var seasonParserResult = SeasonPathParser.Parse(path, true, true);
+ var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
var season = new Season
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index fb48d7bf17..c81a0adb89 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (child.IsDirectory)
{
- if (IsSeasonFolder(child.FullName, isTvContentType))
+ if (IsSeasonFolder(child.FullName, path, isTvContentType))
{
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
@@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// Determines whether [is season folder] [the specified path].
///
/// The path.
+ /// The parentpath.
/// if set to true [is tv content type].
/// true if [is season folder] [the specified path]; otherwise, false.
- private static bool IsSeasonFolder(string path, bool isTvContentType)
+ private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
{
- var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
+ var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue;
}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index e3fbe8e4d6..9dbac1e920 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -257,7 +257,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
{
- IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path);
+ IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId);
// If a change was made record it
if (IndexNumber.HasValue)
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 03a28fd8c0..e4490bca3b 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -426,8 +426,9 @@ namespace MediaBrowser.Controller.Library
/// Gets the season number from path.
///
/// The path.
+ /// The parent id.
/// System.Nullable<System.Int32>.
- int? GetSeasonNumberFromPath(string path);
+ int? GetSeasonNumberFromPath(string path, Guid? parentId);
///
/// Fills the missing episode numbers from path.
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
index 3a042df683..4c8ba58d04 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
@@ -6,32 +6,54 @@ namespace Jellyfin.Naming.Tests.TV;
public class SeasonPathParserTests
{
[Theory]
- [InlineData("/Drive/Season 1", 1, true)]
- [InlineData("/Drive/s1", 1, true)]
- [InlineData("/Drive/S1", 1, true)]
- [InlineData("/Drive/Season 2", 2, true)]
- [InlineData("/Drive/Season 02", 2, true)]
- [InlineData("/Drive/Seinfeld/S02", 2, true)]
- [InlineData("/Drive/Seinfeld/2", 2, true)]
- [InlineData("/Drive/Seinfeld - S02", 2, true)]
- [InlineData("/Drive/Season 2009", 2009, true)]
- [InlineData("/Drive/Season1", 1, true)]
- [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
- [InlineData("/Drive/Season 7 (2016)", 7, false)]
- [InlineData("/Drive/Staffel 7 (2016)", 7, false)]
- [InlineData("/Drive/Stagione 7 (2016)", 7, false)]
- [InlineData("/Drive/Season (8)", null, false)]
- [InlineData("/Drive/3.Staffel", 3, false)]
- [InlineData("/Drive/s06e05", null, false)]
- [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
- [InlineData("/Drive/extras", 0, true)]
- [InlineData("/Drive/specials", 0, true)]
- public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
+ [InlineData("/Drive/Season 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Staffel 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Stagione 1", "/Drive", 1, true)]
+ [InlineData("/Drive/sæson 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Temporada 1", "/Drive", 1, true)]
+ [InlineData("/Drive/series 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Kausi 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Säsong 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Seizoen 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Seasong 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Sezon 1", "/Drive", 1, true)]
+ [InlineData("/Drive/sezona 1", "/Drive", 1, true)]
+ [InlineData("/Drive/sezóna 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Sezonul 1", "/Drive", 1, true)]
+ [InlineData("/Drive/시즌 1", "/Drive", 1, true)]
+ [InlineData("/Drive/シーズン 1", "/Drive", 1, true)]
+ [InlineData("/Drive/сезон 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Сезон 1", "/Drive", 1, true)]
+ [InlineData("/Drive/Season 10", "/Drive", 10, true)]
+ [InlineData("/Drive/Season 100", "/Drive", 100, true)]
+ [InlineData("/Drive/s1", "/Drive", 1, true)]
+ [InlineData("/Drive/S1", "/Drive", 1, true)]
+ [InlineData("/Drive/Season 2", "/Drive", 2, true)]
+ [InlineData("/Drive/Season 02", "/Drive", 2, true)]
+ [InlineData("/Drive/Seinfeld/S02", "/Seinfeld", 2, true)]
+ [InlineData("/Drive/Seinfeld/2", "/Seinfeld", 2, true)]
+ [InlineData("/Drive/Seinfeld Season 2", "/Drive", null, false)]
+ [InlineData("/Drive/Season 2009", "/Drive", 2009, true)]
+ [InlineData("/Drive/Season1", "/Drive", 1, true)]
+ [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", "/The Wonder Years", 4, true)]
+ [InlineData("/Drive/Season 7 (2016)", "/Drive", 7, true)]
+ [InlineData("/Drive/Staffel 7 (2016)", "/Drive", 7, true)]
+ [InlineData("/Drive/Stagione 7 (2016)", "/Drive", 7, true)]
+ [InlineData("/Drive/Stargate SG-1/Season 1", "/Drive/Stargate SG-1", 1, true)]
+ [InlineData("/Drive/Stargate SG-1/Stargate SG-1 Season 1", "/Drive/Stargate SG-1", 1, true)]
+ [InlineData("/Drive/Season (8)", "/Drive", null, false)]
+ [InlineData("/Drive/3.Staffel", "/Drive", 3, true)]
+ [InlineData("/Drive/s06e05", "/Drive", null, false)]
+ [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", "/Drive", null, false)]
+ [InlineData("/Drive/extras", "/Drive", 0, true)]
+ [InlineData("/Drive/specials", "/Drive", 0, true)]
+ [InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)]
+ public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory)
{
- var result = SeasonPathParser.Parse(path, true, true);
+ var result = SeasonPathParser.Parse(path, parentPath, true, true);
Assert.Equal(result.SeasonNumber is not null, result.Success);
- Assert.Equal(result.SeasonNumber, seasonNumber);
+ Assert.Equal(seasonNumber, result.SeasonNumber);
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
}
}