< 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: 82
Uncovered lines: 0
Coverable lines: 82
Total lines: 213
Line coverage: 100%
Branch coverage
100%
Covered branches: 50
Total branches: 50
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

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

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using System.Text.RegularExpressions;
 6using Emby.Naming.Common;
 7using Jellyfin.Extensions;
 8using MediaBrowser.Model.IO;
 9
 10namespace Emby.Naming.Video
 11{
 12    /// <summary>
 13    /// Resolves alternative versions and extras from list of video files.
 14    /// </summary>
 15    public static partial class VideoListResolver
 16    {
 17        [GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
 18        private static partial Regex ResolutionRegex();
 19
 20        [GeneratedRegex(@"^\[([^]]*)\]")]
 21        private static partial Regex CheckMultiVersionRegex();
 22
 23        /// <summary>
 24        /// Resolves alternative versions and extras from list of video files.
 25        /// </summary>
 26        /// <param name="videoInfos">List of related video files.</param>
 27        /// <param name="namingOptions">The naming options.</param>
 28        /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
 29        /// <param name="parseName">Whether to parse the name or use the filename.</param>
 30        /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
 31        public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOpti
 32        {
 33            // Filter out all extras, otherwise they could cause stacks to not be resolved
 34            // See the unit test TestStackedWithTrailer
 4035            var nonExtras = videoInfos
 4036                .Where(i => i.ExtraType is null)
 4037                .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
 38
 4039            var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
 40
 4041            var remainingFiles = new List<VideoFileInfo>();
 4042            var standaloneMedia = new List<VideoFileInfo>();
 43
 40044            for (var i = 0; i < videoInfos.Count; i++)
 45            {
 16046                var current = videoInfos[i];
 16047                if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
 48                {
 49                    continue;
 50                }
 51
 14552                if (current.ExtraType is null)
 53                {
 12054                    standaloneMedia.Add(current);
 55                }
 56                else
 57                {
 2558                    remainingFiles.Add(current);
 59                }
 60            }
 61
 4062            var list = new List<VideoInfo>();
 63
 9264            foreach (var stack in stackResult)
 65            {
 666                var info = new VideoInfo(stack.Name)
 667                {
 668                    Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, pars
 669                        .OfType<VideoFileInfo>()
 670                        .ToList()
 671                };
 72
 673                info.Year = info.Files[0].Year;
 674                list.Add(info);
 75            }
 76
 32077            foreach (var media in standaloneMedia)
 78            {
 12079                var info = new VideoInfo(media.Name) { Files = new[] { media } };
 80
 12081                info.Year = info.Files[0].Year;
 12082                list.Add(info);
 83            }
 84
 4085            if (supportMultiVersion)
 86            {
 4087                list = GetVideosGroupedByVersion(list, namingOptions);
 88            }
 89
 90            // Whatever files are left, just add them
 4091            list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
 4092            {
 4093                Files = new[] { i },
 4094                Year = i.Year,
 4095                ExtraType = i.ExtraType
 4096            }));
 97
 4098            return list;
 99        }
 100
 101        private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
 102        {
 40103            if (videos.Count == 0)
 104            {
 1105                return videos;
 106            }
 107
 39108            var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
 109
 39110            if (folderName.Length <= 1 || !HaveSameYear(videos))
 111            {
 17112                return videos;
 113            }
 114
 115            // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the abov
 22116            VideoInfo? primary = null;
 158117            for (var i = 0; i < videos.Count; i++)
 118            {
 67119                var video = videos[i];
 67120                if (video.ExtraType is not null)
 121                {
 122                    continue;
 123                }
 124
 67125                if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
 126                {
 10127                    return videos;
 128                }
 129
 57130                if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal))
 131                {
 7132                    primary = video;
 133                }
 134            }
 135
 12136            if (videos.Count > 1)
 137            {
 11138                var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList(
 11139                videos.Clear();
 54140                foreach (var group in groups)
 141                {
 16142                    if (group.Key)
 143                    {
 7144                        videos.InsertRange(0, group
 7145                            .OrderByDescending(x => ResolutionRegex().Match(x.Files[0].FileNameWithoutExtension.ToString
 7146                            .ThenBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
 147                    }
 148                    else
 149                    {
 9150                        videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new Alphanume
 151                    }
 152                }
 153            }
 154
 12155            primary ??= videos[0];
 12156            videos.Remove(primary);
 157
 12158            var list = new List<VideoInfo>
 12159            {
 12160                primary
 12161            };
 162
 12163            list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
 12164            list[0].Name = folderName.ToString();
 165
 12166            return list;
 167        }
 168
 169        private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
 170        {
 26171            if (videos.Count == 1)
 172            {
 7173                return true;
 174            }
 175
 19176            var firstYear = videos[0].Year ?? -1;
 162177            for (var i = 1; i < videos.Count; i++)
 178            {
 66179                if ((videos[i].Year ?? -1) != firstYear)
 180                {
 4181                    return false;
 182                }
 183            }
 184
 15185            return true;
 186        }
 187
 188        private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, Na
 189        {
 67190            if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
 191            {
 4192                return false;
 193            }
 194
 195            // Remove the folder name before cleaning as we don't care about cleaning that part
 63196            if (folderName.Length <= testFilename.Length)
 197            {
 63198                testFilename = testFilename[folderName.Length..].Trim();
 199            }
 200
 201            // There are no span overloads for regex unfortunately
 63202            if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName)
 203            {
 23204                testFilename = cleanName.AsSpan().Trim();
 205            }
 206
 207            // The CleanStringParser should have removed common keywords etc.
 63208            return testFilename.IsEmpty
 63209                   || testFilename[0] == '-'
 63210                   || CheckMultiVersionRegex().IsMatch(testFilename);
 211        }
 212    }
 213}