using System; using System.Globalization; using System.IO; namespace Emby.Naming.TV { /// /// Class to parse season paths. /// public static 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" }; /// /// Attempts to parse season number from path. /// /// Path to season. /// Support special aliases when parsing. /// Support numeric season folders when parsing. /// Returns object. public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) { var result = new SeasonPathParserResult(); var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders); result.SeasonNumber = seasonNumber; if (result.SeasonNumber.HasValue) { result.Success = true; result.IsSeasonFolder = isSeasonFolder; } return result; } /// /// Gets the season number from path. /// /// The path. /// 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, bool supportSpecialAliases, bool supportNumericSeasonFolders) { string filename = Path.GetFileName(path); if (supportSpecialAliases) { if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase)) { return (0, true); } if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase)) { return (0, true); } } if (supportNumericSeasonFolders) { if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { return (val, true); } } if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase)) { var testFilename = filename.AsSpan().Slice(1); if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { return (val, 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(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries); foreach (var part in parts) { if (TryGetSeasonNumberFromPart(part, out int 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; } /// /// Extracts the season number from the second half of the Season folder name (everything after "Season", or "Staffel"). /// /// The path. /// System.Nullable{System.Int32}. private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan path) { var numericStart = -1; var length = 0; var hasOpenParenthesis = false; var isSeasonFolder = true; // Find out where the numbers start, and then keep going until they end for (var i = 0; i < path.Length; i++) { if (char.IsNumber(path[i])) { if (!hasOpenParenthesis) { if (numericStart == -1) { numericStart = i; } length++; } } else if (numericStart != -1) { // There's other stuff after the season number, e.g. episode number isSeasonFolder = false; break; } var currentChar = path[i]; if (currentChar == '(') { hasOpenParenthesis = true; } else if (currentChar == ')') { hasOpenParenthesis = false; } } if (numericStart == -1) { return (null, isSeasonFolder); } return (int.Parse(path.Slice(numericStart, length), provider: CultureInfo.InvariantCulture), isSeasonFolder); } } }