< Summary - Jellyfin

Information
Class: Emby.Naming.TV.EpisodePathParser
Assembly: Emby.Naming
File(s): /srv/git/jellyfin/Emby.Naming/TV/EpisodePathParser.cs
Line coverage
100%
Covered lines: 86
Uncovered lines: 0
Coverable lines: 86
Total lines: 244
Line coverage: 100%
Branch coverage
93%
Covered branches: 77
Total branches: 82
Branch coverage: 93.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 AM Line coverage: 100% (85/85) Branch coverage: 100% (80/80) Total lines: 2425/20/2026 - 12:15:44 AM Line coverage: 100% (85/85) Branch coverage: 93.7% (75/80) Total lines: 24210/29/2026 - 12:17:11 AM Line coverage: 100% (86/86) Branch coverage: 93.9% (77/82) Total lines: 244 5/16/2026 - 12:15:55 AM Line coverage: 100% (85/85) Branch coverage: 100% (80/80) Total lines: 2425/20/2026 - 12:15:44 AM Line coverage: 100% (85/85) Branch coverage: 93.7% (75/80) Total lines: 24210/29/2026 - 12:17:11 AM Line coverage: 100% (86/86) Branch coverage: 93.9% (77/82) Total lines: 244

Coverage delta

Coverage delta 7 -7

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Parse(...)100%2424100%
Parse(...)87.5%4040100%
FillAdditional(...)100%22100%
FillAdditional(...)100%1616100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Linq;
 5using Emby.Naming.Common;
 6
 7namespace Emby.Naming.TV
 8{
 9    /// <summary>
 10    /// Used to parse information about episode from path.
 11    /// </summary>
 12    public class EpisodePathParser
 13    {
 14        private readonly NamingOptions _options;
 15
 16        /// <summary>
 17        /// Initializes a new instance of the <see cref="EpisodePathParser"/> class.
 18        /// </summary>
 19        /// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeEx
 20        public EpisodePathParser(NamingOptions options)
 21        {
 34722            _options = options;
 34723        }
 24
 25        /// <summary>
 26        /// Parses information about episode from path.
 27        /// </summary>
 28        /// <param name="path">Path.</param>
 29        /// <param name="isDirectory">Is path for a directory or file.</param>
 30        /// <param name="isNamed">Do we want to use IsNamed expressions.</param>
 31        /// <param name="isOptimistic">Do we want to use Optimistic expressions.</param>
 32        /// <param name="supportsAbsoluteNumbers">Do we want to use expressions supporting absolute episode numbers.</pa
 33        /// <param name="fillExtendedInfo">Should we attempt to retrieve extended information.</param>
 34        /// <returns>Returns <see cref="EpisodePathParserResult"/> object.</returns>
 35        public EpisodePathParserResult Parse(
 36            string path,
 37            bool isDirectory,
 38            bool? isNamed = null,
 39            bool? isOptimistic = null,
 40            bool? supportsAbsoluteNumbers = null,
 41            bool fillExtendedInfo = true)
 42        {
 43            // Added to be able to use regex patterns which require a file extension.
 44            // There were no failed tests without this block, but to be safe, we can keep it until
 45            // the regex which require file extensions are modified so that they don't need them.
 31846            if (isDirectory)
 47            {
 848                path += ".mp4";
 49            }
 50
 31851            EpisodePathParserResult? result = null;
 52
 480353            foreach (var expression in _options.EpisodeExpressions)
 54            {
 224055                if (supportsAbsoluteNumbers.HasValue
 224056                    && expression.SupportsAbsoluteEpisodeNumbers != supportsAbsoluteNumbers.Value)
 57                {
 58                    continue;
 59                }
 60
 223961                if (isNamed.HasValue && expression.IsNamed != isNamed.Value)
 62                {
 63                    continue;
 64                }
 65
 223366                if (isOptimistic.HasValue && expression.IsOptimistic != isOptimistic.Value)
 67                {
 68                    continue;
 69                }
 70
 220571                var currentResult = Parse(path, expression);
 220572                if (currentResult.Success)
 73                {
 31374                    result = currentResult;
 31375                    break;
 76                }
 77            }
 78
 31879            if (result is not null && fillExtendedInfo)
 80            {
 24281                FillAdditional(path, result);
 82
 24283                if (!string.IsNullOrEmpty(result.SeriesName))
 84                {
 14285                    result.SeriesName = result.SeriesName
 14286                        .Trim()
 14287                        .Trim('_', '.', '-')
 14288                        .Trim();
 89                }
 90            }
 91
 31892            return result ?? new EpisodePathParserResult();
 93        }
 94
 95        private static EpisodePathParserResult Parse(string name, EpisodeExpression expression)
 96        {
 729897            var result = new EpisodePathParserResult();
 98
 99            // This is a hack to handle wmc naming
 7298100            if (expression.IsByDate)
 101            {
 360102                name = name.Replace('_', '-');
 103            }
 104
 7298105            var match = expression.Regex.Match(name);
 106
 107            // (Full)(Season)(Episode)(Extension)
 7298108            if (match.Success && match.Groups.Count >= 3)
 109            {
 808110                if (expression.IsByDate)
 111                {
 112                    DateTime date;
 6113                    if (expression.DateTimeFormats.Length > 0)
 114                    {
 5115                        if (DateTime.TryParseExact(
 5116                            match.Groups[0].ValueSpan,
 5117                            expression.DateTimeFormats,
 5118                            CultureInfo.InvariantCulture,
 5119                            DateTimeStyles.None,
 5120                            out date))
 121                        {
 5122                            result.Year = date.Year;
 5123                            result.Month = date.Month;
 5124                            result.Day = date.Day;
 5125                            result.Success = true;
 126                        }
 127                    }
 1128                    else if (DateTime.TryParse(match.Groups[0].ValueSpan, CultureInfo.InvariantCulture, out date))
 129                    {
 1130                        result.Year = date.Year;
 1131                        result.Month = date.Month;
 1132                        result.Day = date.Day;
 1133                        result.Success = true;
 134                    }
 135
 136                    // TODO: Only consider success if date successfully parsed?
 6137                    result.Success = true;
 138                }
 802139                else if (expression.IsNamed)
 140                {
 708141                    if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.Invariant
 142                    {
 574143                        result.SeasonNumber = num;
 144                    }
 145
 708146                    if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCult
 147                    {
 675148                        result.EpisodeNumber = num;
 149                    }
 150
 708151                    var endingNumberGroup = match.Groups["endingepnumber"];
 708152                    if (endingNumberGroup.Success)
 153                    {
 154                        // Will only set EndingEpisodeNumber if the captured number is not followed by additional number
 155                        // or a 'p' or 'i' as what you would get with a pixel resolution specification.
 156                        // It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode fr
 132157                        int nextIndex = endingNumberGroup.Index + endingNumberGroup.Length;
 132158                        if (nextIndex >= name.Length
 132159                            || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
 160                        {
 161                            // A range cannot end before it starts, so a lower number belongs to the episode title rathe
 129162                            if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCul
 129163                                && num >= result.EpisodeNumber)
 164                            {
 127165                                result.EndingEpisodeNumber = num;
 166                            }
 167                        }
 168                    }
 169
 708170                    result.SeriesName = match.Groups["seriesname"].Value;
 708171                    result.Success = result.EpisodeNumber.HasValue;
 172                }
 173                else
 174                {
 94175                    if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out 
 176                    {
 92177                        result.SeasonNumber = num;
 178                    }
 179
 94180                    if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out 
 181                    {
 94182                        result.EpisodeNumber = num;
 183                    }
 184
 94185                    result.Success = result.EpisodeNumber.HasValue;
 186                }
 187
 188                // Invalidate match when the season is 200 through 1927 or above 2500
 189                // because it is an error unless the TV show is intentionally using false season numbers.
 190                // It avoids erroneous parsing of something like "Series Special (1920x1080).mkv" as being season 1920 e
 808191                if ((result.SeasonNumber >= 200 && result.SeasonNumber < 1928)
 808192                    || result.SeasonNumber > 2500)
 193                {
 2194                    result.Success = false;
 195                }
 196
 808197                result.IsByDate = expression.IsByDate;
 198            }
 199
 7298200            return result;
 201        }
 202
 203        private void FillAdditional(string path, EpisodePathParserResult info)
 204        {
 242205            var expressions = _options.MultipleEpisodeExpressions.Where(i => i.IsNamed).ToList();
 206
 242207            if (string.IsNullOrEmpty(info.SeriesName))
 208            {
 154209                expressions.InsertRange(0, _options.EpisodeExpressions.Where(i => i.IsNamed));
 210            }
 211
 242212            FillAdditional(path, info, expressions);
 242213        }
 214
 215        private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expression
 216        {
 10623217            foreach (var i in expressions)
 218            {
 5093219                var result = Parse(path, i);
 220
 5093221                if (!result.Success)
 222                {
 223                    continue;
 224                }
 225
 460226                if (string.IsNullOrEmpty(info.SeriesName))
 227                {
 378228                    info.SeriesName = result.SeriesName;
 229                }
 230
 460231                if (!info.EndingEpisodeNumber.HasValue && result.EndingEpisodeNumber >= info.EpisodeNumber)
 232                {
 79233                    info.EndingEpisodeNumber = result.EndingEpisodeNumber;
 234                }
 235
 460236                if (!string.IsNullOrEmpty(info.SeriesName)
 460237                    && (!info.EpisodeNumber.HasValue || info.EndingEpisodeNumber.HasValue))
 238                {
 42239                    break;
 240                }
 241            }
 242242        }
 243    }
 244}