< Summary - Jellyfin

Information
Class: Emby.Naming.TV.SeasonPathParser
Assembly: Emby.Naming
File(s): /srv/git/jellyfin/Emby.Naming/TV/SeasonPathParser.cs
Line coverage
62%
Covered lines: 38
Uncovered lines: 23
Coverable lines: 61
Total lines: 193
Line coverage: 62.2%
Branch coverage
59%
Covered branches: 31
Total branches: 52
Branch coverage: 59.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/2/2026 - 12:14:15 AM Line coverage: 62.5% (35/56) Branch coverage: 59.5% (25/42) Total lines: 1745/20/2026 - 12:15:44 AM Line coverage: 62.5% (35/56) Branch coverage: 57.1% (24/42) Total lines: 1746/1/2026 - 12:16:05 AM Line coverage: 62.2% (38/61) Branch coverage: 59.6% (31/52) Total lines: 193 3/2/2026 - 12:14:15 AM Line coverage: 62.5% (35/56) Branch coverage: 59.5% (25/42) Total lines: 1745/20/2026 - 12:15:44 AM Line coverage: 62.5% (35/56) Branch coverage: 57.1% (24/42) Total lines: 1746/1/2026 - 12:16:05 AM Line coverage: 62.2% (38/61) Branch coverage: 59.6% (31/52) Total lines: 193

Coverage delta

Coverage delta 3 -3

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Parse(...)75%44100%
GetSeasonNumberFromPath(...)85.71%282892.3%
CheckMatch(...)100%44100%
GetSeasonNumberFromPathSubstring(...)0%272160%

File(s)

/srv/git/jellyfin/Emby.Naming/TV/SeasonPathParser.cs

#LineLine coverage
 1using System;
 2using System.Globalization;
 3using System.IO;
 4using System.Text.RegularExpressions;
 5
 6namespace Emby.Naming.TV
 7{
 8    /// <summary>
 9    /// Class to parse season paths.
 10    /// </summary>
 11    public static partial class SeasonPathParser
 12    {
 13        private const string SeasonKeywordPattern =
 14            @"시즌|シーズン|сезон" +
 15            @"|season|sæson|saison|staffel|series|stagione|säsong|seizoen|seasong" +
 16            @"|sezon|sezona|sezóna|sezonul|série|séria|serie|seria|temporada|kausi";
 17
 218        private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
 19
 20        [GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:" + SeasonKeywordPattern 
 21        private static partial Regex ProcessPre();
 22
 23        [GeneratedRegex(@"^\s*(?:" + SeasonKeywordPattern + @")\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)
 24        private static partial Regex ProcessPost();
 25
 26        [GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
 27        private static partial Regex SeasonPrefix();
 28
 29        [GeneratedRegex(SeasonKeywordPattern, RegexOptions.IgnoreCase)]
 30        private static partial Regex SeasonKeyword();
 31
 32        /// <summary>
 33        /// Attempts to parse season number from path.
 34        /// </summary>
 35        /// <param name="path">Path to season.</param>
 36        /// <param name="parentPath">Folder name of the parent.</param>
 37        /// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
 38        /// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
 39        /// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
 40        public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool sup
 41        {
 8942            var result = new SeasonPathParserResult();
 8943            var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
 44
 8945            var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, 
 46
 8947            result.SeasonNumber = seasonNumber;
 48
 8949            if (result.SeasonNumber.HasValue)
 50            {
 7351                result.Success = true;
 7352                result.IsSeasonFolder = isSeasonFolder;
 53            }
 54
 8955            return result;
 56        }
 57
 58        /// <summary>
 59        /// Gets the season number from path.
 60        /// </summary>
 61        /// <param name="path">The path.</param>
 62        /// <param name="parentFolderName">The parent folder name.</param>
 63        /// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
 64        /// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
 65        /// <returns>System.Nullable{System.Int32}.</returns>
 66        private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
 67            string path,
 68            string? parentFolderName,
 69            bool supportSpecialAliases,
 70            bool supportNumericSeasonFolders)
 71        {
 8972            var fileName = Path.GetFileName(path);
 73
 8974            var seasonPrefixMatch = SeasonPrefix().Match(fileName);
 8975            if (seasonPrefixMatch.Success &&
 8976                int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out 
 77            {
 1078                return (val, true);
 79            }
 80
 7981            string filename = CleanNameRegex.Replace(fileName, string.Empty);
 82
 7983            if (parentFolderName is not null)
 84            {
 7985                var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty);
 7986                filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase);
 87            }
 88
 7989            if (supportSpecialAliases &&
 7990                (filename.Equals("specials", StringComparison.OrdinalIgnoreCase) ||
 7991                 filename.Equals("extras", StringComparison.OrdinalIgnoreCase)))
 92            {
 493                return (0, true);
 94            }
 95
 7596            if (supportNumericSeasonFolders &&
 7597                int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val))
 98            {
 199                return (val, true);
 100            }
 101
 74102            bool isMixedLibrary = !supportNumericSeasonFolders && !supportSpecialAliases;
 74103            var preMatch = ProcessPre().Match(filename);
 74104            if (preMatch.Success)
 105            {
 1106                if (isMixedLibrary && !SeasonKeyword().IsMatch(fileName))
 107                {
 0108                    return (null, false);
 109                }
 110
 1111                return CheckMatch(preMatch);
 112            }
 113            else
 114            {
 73115                var postMatch = ProcessPost().Match(filename);
 73116                if (postMatch.Success && isMixedLibrary && !SeasonKeyword().IsMatch(fileName))
 117                {
 0118                    return (null, false);
 119                }
 120
 73121                return CheckMatch(postMatch);
 122            }
 123        }
 124
 125        private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
 126        {
 74127            var numberString = match.Groups["seasonnumber"];
 74128            if (numberString.Success)
 129            {
 58130                if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonN
 131                {
 58132                    return (seasonNumber, true);
 133                }
 134            }
 135
 16136            return (null, false);
 137        }
 138
 139        /// <summary>
 140        /// Extracts the season number from the second half of the Season folder name (everything after "Season", or "St
 141        /// </summary>
 142        /// <param name="path">The path.</param>
 143        /// <returns>System.Nullable{System.Int32}.</returns>
 144        private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path
 145        {
 0146            var numericStart = -1;
 0147            var length = 0;
 148
 0149            var hasOpenParenthesis = false;
 0150            var isSeasonFolder = true;
 151
 152            // Find out where the numbers start, and then keep going until they end
 0153            for (var i = 0; i < path.Length; i++)
 154            {
 0155                if (char.IsNumber(path[i]))
 156                {
 0157                    if (!hasOpenParenthesis)
 158                    {
 0159                        if (numericStart == -1)
 160                        {
 0161                            numericStart = i;
 162                        }
 163
 0164                        length++;
 165                    }
 166                }
 0167                else if (numericStart != -1)
 168                {
 169                    // There's other stuff after the season number, e.g. episode number
 0170                    isSeasonFolder = false;
 0171                    break;
 172                }
 173
 0174                var currentChar = path[i];
 0175                if (currentChar == '(')
 176                {
 0177                    hasOpenParenthesis = true;
 178                }
 0179                else if (currentChar == ')')
 180                {
 0181                    hasOpenParenthesis = false;
 182                }
 183            }
 184
 0185            if (numericStart == -1)
 186            {
 0187                return (null, isSeasonFolder);
 188            }
 189
 0190            return (int.Parse(path.Slice(numericStart, length), provider: CultureInfo.InvariantCulture), isSeasonFolder)
 191        }
 192    }
 193}