< 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: 35
Uncovered lines: 21
Coverable lines: 56
Total lines: 174
Line coverage: 62.5%
Branch coverage
59%
Covered branches: 25
Total branches: 42
Branch coverage: 59.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/14/2025 - 12:11:05 AM Line coverage: 62.5% (35/56) Branch coverage: 57.5% (23/40) Total lines: 17710/28/2025 - 12:11:27 AM Line coverage: 62.5% (35/56) Branch coverage: 61.3% (27/44) Total lines: 17711/4/2025 - 12:11:59 AM Line coverage: 62.5% (35/56) Branch coverage: 63% (29/46) Total lines: 17911/18/2025 - 12:11:25 AM Line coverage: 62.5% (35/56) Branch coverage: 59.5% (25/42) Total lines: 174 8/14/2025 - 12:11:05 AM Line coverage: 62.5% (35/56) Branch coverage: 57.5% (23/40) Total lines: 17710/28/2025 - 12:11:27 AM Line coverage: 62.5% (35/56) Branch coverage: 61.3% (27/44) Total lines: 17711/4/2025 - 12:11:59 AM Line coverage: 62.5% (35/56) Branch coverage: 63% (29/46) Total lines: 17911/18/2025 - 12:11:25 AM Line coverage: 62.5% (35/56) Branch coverage: 59.5% (25/42) Total lines: 174

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Parse(...)75%44100%
GetSeasonNumberFromPath(...)100%1818100%
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    {
 113        private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
 14
 15        [GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eas
 16        private static partial Regex ProcessPre();
 17
 18        [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ez
 19        private static partial Regex ProcessPost();
 20
 21        [GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
 22        private static partial Regex SeasonPrefix();
 23
 24        /// <summary>
 25        /// Attempts to parse season number from path.
 26        /// </summary>
 27        /// <param name="path">Path to season.</param>
 28        /// <param name="parentPath">Folder name of the parent.</param>
 29        /// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
 30        /// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
 31        /// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
 32        public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool sup
 33        {
 6834            var result = new SeasonPathParserResult();
 6835            var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
 36
 6837            var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, 
 38
 6839            result.SeasonNumber = seasonNumber;
 40
 6841            if (result.SeasonNumber.HasValue)
 42            {
 5943                result.Success = true;
 5944                result.IsSeasonFolder = isSeasonFolder;
 45            }
 46
 6847            return result;
 48        }
 49
 50        /// <summary>
 51        /// Gets the season number from path.
 52        /// </summary>
 53        /// <param name="path">The path.</param>
 54        /// <param name="parentFolderName">The parent folder name.</param>
 55        /// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
 56        /// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
 57        /// <returns>System.Nullable{System.Int32}.</returns>
 58        private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
 59            string path,
 60            string? parentFolderName,
 61            bool supportSpecialAliases,
 62            bool supportNumericSeasonFolders)
 63        {
 6864            var fileName = Path.GetFileName(path);
 65
 6866            var seasonPrefixMatch = SeasonPrefix().Match(fileName);
 6867            if (seasonPrefixMatch.Success &&
 6868                int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out 
 69            {
 870                return (val, true);
 71            }
 72
 6073            string filename = CleanNameRegex.Replace(fileName, string.Empty);
 74
 6075            if (parentFolderName is not null)
 76            {
 6077                var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty);
 6078                filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase);
 79            }
 80
 6081            if (supportSpecialAliases &&
 6082                (filename.Equals("specials", StringComparison.OrdinalIgnoreCase) ||
 6083                 filename.Equals("extras", StringComparison.OrdinalIgnoreCase)))
 84            {
 485                return (0, true);
 86            }
 87
 5688            if (supportNumericSeasonFolders &&
 5689                int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val))
 90            {
 191                return (val, true);
 92            }
 93
 5594            var preMatch = ProcessPre().Match(filename);
 5595            if (preMatch.Success)
 96            {
 497                return CheckMatch(preMatch);
 98            }
 99            else
 100            {
 51101                var postMatch = ProcessPost().Match(filename);
 51102                return CheckMatch(postMatch);
 103            }
 104        }
 105
 106        private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
 107        {
 55108            var numberString = match.Groups["seasonnumber"];
 55109            if (numberString.Success)
 110            {
 49111                if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonN
 112                {
 46113                    return (seasonNumber, true);
 114                }
 115            }
 116
 9117            return (null, false);
 118        }
 119
 120        /// <summary>
 121        /// Extracts the season number from the second half of the Season folder name (everything after "Season", or "St
 122        /// </summary>
 123        /// <param name="path">The path.</param>
 124        /// <returns>System.Nullable{System.Int32}.</returns>
 125        private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path
 126        {
 0127            var numericStart = -1;
 0128            var length = 0;
 129
 0130            var hasOpenParenthesis = false;
 0131            var isSeasonFolder = true;
 132
 133            // Find out where the numbers start, and then keep going until they end
 0134            for (var i = 0; i < path.Length; i++)
 135            {
 0136                if (char.IsNumber(path[i]))
 137                {
 0138                    if (!hasOpenParenthesis)
 139                    {
 0140                        if (numericStart == -1)
 141                        {
 0142                            numericStart = i;
 143                        }
 144
 0145                        length++;
 146                    }
 147                }
 0148                else if (numericStart != -1)
 149                {
 150                    // There's other stuff after the season number, e.g. episode number
 0151                    isSeasonFolder = false;
 0152                    break;
 153                }
 154
 0155                var currentChar = path[i];
 0156                if (currentChar == '(')
 157                {
 0158                    hasOpenParenthesis = true;
 159                }
 0160                else if (currentChar == ')')
 161                {
 0162                    hasOpenParenthesis = false;
 163                }
 164            }
 165
 0166            if (numericStart == -1)
 167            {
 0168                return (null, isSeasonFolder);
 169            }
 170
 0171            return (int.Parse(path.Slice(numericStart, length), provider: CultureInfo.InvariantCulture), isSeasonFolder)
 172        }
 173    }
 174}