< Summary - Jellyfin

Information
Class: Emby.Naming.Video.VideoListResolver
Assembly: Emby.Naming
File(s): /srv/git/jellyfin/Emby.Naming/Video/VideoListResolver.cs
Line coverage
100%
Covered lines: 90
Uncovered lines: 0
Coverable lines: 90
Total lines: 225
Line coverage: 100%
Branch coverage
100%
Covered branches: 54
Total branches: 54
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/27/2025 - 12:11:51 AM Line coverage: 100% (82/82) Branch coverage: 100% (50/50) Total lines: 2141/19/2026 - 12:13:54 AM Line coverage: 100% (88/88) Branch coverage: 100% (50/50) Total lines: 2233/30/2026 - 12:14:34 AM Line coverage: 100% (90/90) Branch coverage: 100% (54/54) Total lines: 225 12/27/2025 - 12:11:51 AM Line coverage: 100% (82/82) Branch coverage: 100% (50/50) Total lines: 2141/19/2026 - 12:13:54 AM Line coverage: 100% (88/88) Branch coverage: 100% (50/50) Total lines: 2233/30/2026 - 12:14:34 AM Line coverage: 100% (90/90) Branch coverage: 100% (54/54) Total lines: 225

Coverage delta

Coverage delta 1 -1

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Resolve(...)100%1212100%
GetVideosGroupedByVersion(...)100%2222100%
HaveSameYear(...)100%66100%
IsEligibleForMultiVersion(...)100%1414100%

File(s)

/srv/git/jellyfin/Emby.Naming/Video/VideoListResolver.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using System.Text.RegularExpressions;
 7using Emby.Naming.Common;
 8using Jellyfin.Extensions;
 9using MediaBrowser.Model.IO;
 10
 11namespace Emby.Naming.Video
 12{
 13    /// <summary>
 14    /// Resolves alternative versions and extras from list of video files.
 15    /// </summary>
 16    public static partial class VideoListResolver
 17    {
 18        [GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
 19        private static partial Regex ResolutionRegex();
 20
 21        [GeneratedRegex(@"^\[([^]]*)\]")]
 22        private static partial Regex CheckMultiVersionRegex();
 23
 24        /// <summary>
 25        /// Resolves alternative versions and extras from list of video files.
 26        /// </summary>
 27        /// <param name="videoInfos">List of related video files.</param>
 28        /// <param name="namingOptions">The naming options.</param>
 29        /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
 30        /// <param name="parseName">Whether to parse the name or use the filename.</param>
 31        /// <param name="libraryRoot">Top-level folder for the containing library.</param>
 32        /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
 33        public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOpti
 34        {
 35            // Filter out all extras, otherwise they could cause stacks to not be resolved
 36            // See the unit test TestStackedWithTrailer
 4237            var nonExtras = videoInfos
 4238                .Where(i => i.ExtraType is null)
 4239                .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
 40
 4241            var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
 42
 4243            var remainingFiles = new List<VideoFileInfo>();
 4244            var standaloneMedia = new List<VideoFileInfo>();
 45
 41246            for (var i = 0; i < videoInfos.Count; i++)
 47            {
 16448                var current = videoInfos[i];
 16449                if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
 50                {
 51                    continue;
 52                }
 53
 14954                if (current.ExtraType is null)
 55                {
 12456                    standaloneMedia.Add(current);
 57                }
 58                else
 59                {
 2560                    remainingFiles.Add(current);
 61                }
 62            }
 63
 4264            var list = new List<VideoInfo>();
 65
 9666            foreach (var stack in stackResult)
 67            {
 668                var info = new VideoInfo(stack.Name)
 669                {
 670                    Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, pars
 671                        .OfType<VideoFileInfo>()
 672                        .ToList()
 673                };
 74
 675                info.Year = info.Files[0].Year;
 676                list.Add(info);
 77            }
 78
 33279            foreach (var media in standaloneMedia)
 80            {
 12481                var info = new VideoInfo(media.Name) { Files = new[] { media } };
 82
 12483                info.Year = info.Files[0].Year;
 12484                list.Add(info);
 85            }
 86
 4287            if (supportMultiVersion)
 88            {
 4289                list = GetVideosGroupedByVersion(list, namingOptions);
 90            }
 91
 92            // Whatever files are left, just add them
 4293            list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
 4294            {
 4295                Files = new[] { i },
 4296                Year = i.Year,
 4297                ExtraType = i.ExtraType
 4298            }));
 99
 42100            return list;
 101        }
 102
 103        private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
 104        {
 42105            if (videos.Count == 0)
 106            {
 1107                return videos;
 108            }
 109
 41110            var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
 111
 41112            if (folderName.Length <= 1 || !HaveSameYear(videos))
 113            {
 17114                return videos;
 115            }
 116
 117            // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the abov
 24118            VideoInfo? primary = null;
 182119            for (var i = 0; i < videos.Count; i++)
 120            {
 76121                var video = videos[i];
 76122                if (video.ExtraType is not null)
 123                {
 124                    continue;
 125                }
 126
 76127                if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
 128                {
 9129                    return videos;
 130                }
 131
 67132                if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal))
 133                {
 7134                    primary = video;
 135                }
 136            }
 137
 15138            if (videos.Count > 1)
 139            {
 14140                var groups = videos
 14141                    .Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
 14142                    .Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
 14143                    .GroupBy(x => x.resolutionMatch.Success)
 14144                    .ToList();
 145
 14146                videos.Clear();
 147
 14148                StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrde
 72149                foreach (var group in groups)
 150                {
 22151                    if (group.Key)
 152                    {
 10153                        videos.InsertRange(0, group
 10154                            .OrderByDescending(x => x.resolutionMatch.Value, comparer)
 10155                            .ThenBy(x => x.filename, comparer)
 10156                            .Select(x => x.value));
 157                    }
 158                    else
 159                    {
 12160                        videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value));
 161                    }
 162                }
 163            }
 164
 15165            primary ??= videos[0];
 15166            videos.Remove(primary);
 167
 15168            var list = new List<VideoInfo>
 15169            {
 15170                primary
 15171            };
 172
 15173            list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
 15174            list[0].Name = folderName.ToString();
 175
 15176            return list;
 177        }
 178
 179        private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
 180        {
 28181            if (videos.Count == 1)
 182            {
 7183                return true;
 184            }
 185
 21186            var firstYear = videos[0].Year ?? -1;
 170187            for (var i = 1; i < videos.Count; i++)
 188            {
 68189                if ((videos[i].Year ?? -1) != firstYear)
 190                {
 4191                    return false;
 192                }
 193            }
 194
 17195            return true;
 196        }
 197
 198        private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, Na
 199        {
 76200            if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
 201            {
 4202                return false;
 203            }
 204
 205            // Remove the folder name before cleaning as we don't care about cleaning that part
 72206            if (folderName.Length <= testFilename.Length)
 207            {
 72208                testFilename = testFilename[folderName.Length..].Trim();
 209            }
 210
 211            // There are no span overloads for regex unfortunately
 72212            if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName)
 213            {
 25214                testFilename = cleanName.AsSpan().Trim();
 215            }
 216
 217            // The CleanStringParser should have removed common keywords etc.
 72218            return testFilename.IsEmpty
 72219                   || testFilename[0] == '-'
 72220                   || testFilename[0] == '_'
 72221                   || testFilename[0] == '.'
 72222                   || CheckMultiVersionRegex().IsMatch(testFilename);
 223        }
 224    }
 225}