< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Library.Resolvers.Movies.MovieResolver
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
Line coverage
7%
Covered lines: 18
Uncovered lines: 232
Coverable lines: 250
Total lines: 586
Line coverage: 7.2%
Branch coverage
4%
Covered branches: 7
Total branches: 172
Branch coverage: 4%
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: 7.2% (18/248) Branch coverage: 4.1% (7/168) Total lines: 5789/8/2025 - 12:11:25 AM Line coverage: 7.2% (18/249) Branch coverage: 4.1% (7/170) Total lines: 58311/18/2025 - 12:11:25 AM Line coverage: 7.2% (18/250) Branch coverage: 4% (7/172) Total lines: 586 8/14/2025 - 12:11:05 AM Line coverage: 7.2% (18/248) Branch coverage: 4.1% (7/168) Total lines: 5789/8/2025 - 12:11:25 AM Line coverage: 7.2% (18/249) Branch coverage: 4.1% (7/170) Total lines: 58311/18/2025 - 12:11:25 AM Line coverage: 7.2% (18/250) Branch coverage: 4% (7/172) Total lines: 586

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%11100%
get_Priority()100%11100%
ResolveMultiple(...)25%5460%
Resolve(...)7.14%12114212.82%
ResolveMultipleInternal(...)4.54%3552211.76%
ResolveVideos(...)0%930300%
ContainsFile(...)0%110100%
ContainsFile(...)100%210%
SetInitialItemValues(...)100%210%
SetProviderIdsFromPath(...)0%156120%
FindMovie(...)0%1482380%
GetMultiDiscMovie(...)0%7280%
IsInvalid(...)33.33%11650%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Generic;
 5using System.IO;
 6using System.Linq;
 7using System.Text.RegularExpressions;
 8using Emby.Naming.Common;
 9using Emby.Naming.Video;
 10using Jellyfin.Data.Enums;
 11using Jellyfin.Extensions;
 12using MediaBrowser.Controller.Drawing;
 13using MediaBrowser.Controller.Entities;
 14using MediaBrowser.Controller.Entities.Movies;
 15using MediaBrowser.Controller.Entities.TV;
 16using MediaBrowser.Controller.Library;
 17using MediaBrowser.Controller.Providers;
 18using MediaBrowser.Controller.Resolvers;
 19using MediaBrowser.Model.Entities;
 20using MediaBrowser.Model.IO;
 21using Microsoft.Extensions.Logging;
 22
 23namespace Emby.Server.Implementations.Library.Resolvers.Movies
 24{
 25    /// <summary>
 26    /// Class MovieResolver.
 27    /// </summary>
 28    public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
 29    {
 30        private readonly IImageProcessor _imageProcessor;
 31
 032        private static readonly CollectionType[] _validCollectionTypes = new[]
 033        {
 034            CollectionType.movies,
 035            CollectionType.homevideos,
 036            CollectionType.musicvideos,
 037            CollectionType.tvshows,
 038            CollectionType.photos
 039        };
 40
 41        /// <summary>
 42        /// Initializes a new instance of the <see cref="MovieResolver"/> class.
 43        /// </summary>
 44        /// <param name="imageProcessor">The image processor.</param>
 45        /// <param name="logger">The logger.</param>
 46        /// <param name="namingOptions">The naming options.</param>
 47        /// <param name="directoryService">The directory service.</param>
 48        public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions,
 2249            : base(logger, namingOptions, directoryService)
 50        {
 2251            _imageProcessor = imageProcessor;
 2252        }
 53
 54        /// <summary>
 55        /// Gets the priority.
 56        /// </summary>
 57        /// <value>The priority.</value>
 2158        public override ResolverPriority Priority => ResolverPriority.Fourth;
 59
 60        [GeneratedRegex(@"\bsample\b", RegexOptions.IgnoreCase)]
 61        private static partial Regex IsIgnoredRegex();
 62
 63        /// <inheritdoc />
 64        public MultiItemResolverResult ResolveMultiple(
 65            Folder parent,
 66            List<FileSystemMetadata> files,
 67            CollectionType? collectionType,
 68            IDirectoryService directoryService)
 69        {
 5070            var result = ResolveMultipleInternal(parent, files, collectionType);
 71
 5072            if (result is not null)
 73            {
 074                foreach (var item in result.Items)
 75                {
 076                    SetInitialItemValues((Video)item, null);
 77                }
 78            }
 79
 5080            return result;
 81        }
 82
 83        /// <summary>
 84        /// Resolves the specified args.
 85        /// </summary>
 86        /// <param name="args">The args.</param>
 87        /// <returns>Video.</returns>
 88        protected override Video Resolve(ItemResolveArgs args)
 89        {
 1190            var collectionType = args.GetCollectionType();
 91
 92            // Find movies with their own folders
 1193            if (args.IsDirectory)
 94            {
 095                if (IsInvalid(args.Parent, collectionType))
 96                {
 097                    return null;
 98                }
 99
 0100                Video movie = null;
 0101                var files = args.GetActualFileSystemChildren().ToList();
 102
 0103                if (collectionType == CollectionType.musicvideos)
 104                {
 0105                    movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType,
 106                }
 107
 0108                if (collectionType == CollectionType.homevideos)
 109                {
 0110                    movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, fals
 111                }
 112
 0113                if (collectionType is null)
 114                {
 115                    // Owned items will be caught by the video extra resolver
 0116                    if (args.Parent is null)
 117                    {
 0118                        return null;
 119                    }
 120
 0121                    if (args.HasParent<Series>())
 122                    {
 0123                        return null;
 124                    }
 125
 0126                    movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true
 127                }
 128
 0129                if (collectionType == CollectionType.movies)
 130                {
 0131                    movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true
 132                }
 133
 134                // ignore extras
 0135                return movie?.ExtraType is null ? movie : null;
 136            }
 137
 11138            if (args.Parent is null)
 139            {
 1140                return base.Resolve(args);
 141            }
 142
 10143            if (IsInvalid(args.Parent, collectionType))
 144            {
 10145                return null;
 146            }
 147
 0148            Video item = null;
 149
 0150            if (collectionType == CollectionType.musicvideos)
 151            {
 0152                item = ResolveVideo<MusicVideo>(args, false);
 153            }
 154
 155            // To find a movie file, the collection type must be movies or boxsets
 0156            else if (collectionType == CollectionType.movies)
 157            {
 0158                item = ResolveVideo<Movie>(args, true);
 159            }
 0160            else if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos)
 161            {
 0162                item = ResolveVideo<Video>(args, false);
 163            }
 0164            else if (collectionType is null)
 165            {
 0166                if (args.HasParent<Series>())
 167                {
 0168                    return null;
 169                }
 170
 0171                item = ResolveVideo<Video>(args, false);
 172            }
 173
 174            // Ignore extras
 0175            if (item?.ExtraType is not null)
 176            {
 0177                return null;
 178            }
 179
 0180            if (item is not null)
 181            {
 0182                item.IsInMixedFolder = true;
 183            }
 184
 0185            return item;
 186        }
 187
 188        private MultiItemResolverResult ResolveMultipleInternal(
 189            Folder parent,
 190            List<FileSystemMetadata> files,
 191            CollectionType? collectionType)
 192        {
 50193            if (IsInvalid(parent, collectionType))
 194            {
 50195                return null;
 196            }
 197
 0198            if (collectionType is CollectionType.musicvideos)
 199            {
 0200                return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
 201            }
 202
 0203            if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos)
 204            {
 0205                return ResolveVideos<Video>(parent, files, false, collectionType, false);
 206            }
 207
 0208            if (collectionType is null)
 209            {
 210                // Owned items should just use the plain video type
 0211                if (parent is null)
 212                {
 0213                    return ResolveVideos<Video>(parent, files, false, collectionType, false);
 214                }
 215
 0216                if (parent is Series || parent.GetParents().OfType<Series>().Any())
 217                {
 0218                    return null;
 219                }
 220
 0221                return ResolveVideos<Movie>(parent, files, false, collectionType, true);
 222            }
 223
 0224            if (collectionType == CollectionType.movies)
 225            {
 0226                return ResolveVideos<Movie>(parent, files, true, collectionType, true);
 227            }
 228
 0229            if (collectionType == CollectionType.tvshows)
 230            {
 0231                return ResolveVideos<Episode>(parent, files, false, collectionType, true);
 232            }
 233
 0234            return null;
 235        }
 236
 237        private MultiItemResolverResult ResolveVideos<T>(
 238            Folder parent,
 239            IEnumerable<FileSystemMetadata> fileSystemEntries,
 240            bool supportMultiEditions,
 241            CollectionType? collectionType,
 242            bool parseName)
 243            where T : Video, new()
 244        {
 0245            var files = new List<FileSystemMetadata>();
 0246            var leftOver = new List<FileSystemMetadata>();
 0247            var hasCollectionType = collectionType is not null;
 248
 249            // Loop through each child file/folder and see if we find a video
 0250            foreach (var child in fileSystemEntries)
 251            {
 252                // This is a hack but currently no better way to resolve a sometimes ambiguous situation
 0253                if (!hasCollectionType)
 254                {
 0255                    if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
 0256                        || string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
 257                    {
 0258                        return null;
 259                    }
 260                }
 261
 0262                if (child.IsDirectory)
 263                {
 0264                    leftOver.Add(child);
 265                }
 0266                else if (!IsIgnoredRegex().IsMatch(child.Name))
 267                {
 0268                    files.Add(child);
 269                }
 270            }
 271
 0272            var videoInfos = files
 0273                .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.Containin
 0274                .Where(f => f is not null)
 0275                .ToList();
 276
 0277            var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, p
 278
 0279            var result = new MultiItemResolverResult
 0280            {
 0281                ExtraFiles = leftOver
 0282            };
 283
 0284            var isInMixedFolder = resolverResult.Count > 1 || parent?.IsTopParent == true;
 285
 0286            foreach (var video in resolverResult)
 287            {
 0288                var firstVideo = video.Files[0];
 0289                var path = firstVideo.Path;
 0290                if (video.ExtraType is not null)
 291                {
 0292                    result.ExtraFiles.Add(files.Find(f => string.Equals(f.FullName, path, StringComparison.OrdinalIgnore
 0293                    continue;
 294                }
 295
 0296                var additionalParts = video.Files.Count > 1 ? video.Files.Skip(1).Select(i => i.Path).ToArray() : Array.
 297
 0298                var videoItem = new T
 0299                {
 0300                    Path = path,
 0301                    IsInMixedFolder = isInMixedFolder,
 0302                    ProductionYear = video.Year,
 0303                    Name = parseName ? video.Name : firstVideo.Name,
 0304                    AdditionalParts = additionalParts,
 0305                    LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
 0306                };
 307
 0308                SetVideoType(videoItem, firstVideo);
 0309                Set3DFormat(videoItem, firstVideo);
 310
 0311                result.Items.Add(videoItem);
 312            }
 313
 0314            result.ExtraFiles.AddRange(files.Where(i => !ContainsFile(resolverResult, i)));
 315
 0316            return result;
 0317        }
 318
 319        private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
 320        {
 0321            for (var i = 0; i < result.Count; i++)
 322            {
 0323                var current = result[i];
 0324                for (var j = 0; j < current.Files.Count; j++)
 325                {
 0326                    if (ContainsFile(current.Files[j], file))
 327                    {
 0328                        return true;
 329                    }
 330                }
 331
 0332                for (var j = 0; j < current.AlternateVersions.Count; j++)
 333                {
 0334                    if (ContainsFile(current.AlternateVersions[j], file))
 335                    {
 0336                        return true;
 337                    }
 338                }
 339            }
 340
 0341            return false;
 342        }
 343
 344        private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
 345        {
 0346            return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
 347        }
 348
 349        /// <summary>
 350        /// Sets the initial item values.
 351        /// </summary>
 352        /// <param name="item">The item.</param>
 353        /// <param name="args">The args.</param>
 354        protected override void SetInitialItemValues(Video item, ItemResolveArgs args)
 355        {
 0356            base.SetInitialItemValues(item, args);
 357
 0358            SetProviderIdsFromPath(item);
 0359        }
 360
 361        /// <summary>
 362        /// Sets the provider id from path.
 363        /// </summary>
 364        /// <param name="item">The item.</param>
 365        private static void SetProviderIdsFromPath(Video item)
 366        {
 0367            if (item is Movie || item is MusicVideo)
 368            {
 369                // We need to only look at the name of this actual item (not parents)
 0370                var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.Conta
 371
 0372                var tmdbid = justName.GetAttributeValue("tmdbid");
 373
 374                // If not in a mixed folder and ID not found in folder path, check filename
 0375                if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
 376                {
 0377                    tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid");
 378                }
 379
 0380                item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
 381
 0382                if (!string.IsNullOrEmpty(item.Path))
 383                {
 384                    // Check for IMDb id - we use full media path, as we can assume that this will match in any use case
 0385                    var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
 0386                    item.TrySetProviderId(MetadataProvider.Imdb, imdbid);
 387                }
 388            }
 0389        }
 390
 391        /// <summary>
 392        /// Finds a movie based on a child file system entries.
 393        /// </summary>
 394        /// <returns>Movie.</returns>
 395        private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntr
 396            where T : Video, new()
 397        {
 0398            var multiDiscFolders = new List<FileSystemMetadata>();
 399
 0400            var libraryOptions = args.LibraryOptions;
 0401            var supportPhotos = collectionType == CollectionType.homevideos && libraryOptions.EnablePhotos;
 0402            var photos = new List<FileSystemMetadata>();
 403
 404            // Search for a folder rip
 0405            foreach (var child in fileSystemEntries)
 406            {
 0407                var filename = child.Name;
 408
 0409                if (child.IsDirectory)
 410                {
 0411                    if (NamingOptions.AllExtrasTypesFolderNames.ContainsKey(filename))
 412                    {
 413                        continue;
 414                    }
 415
 0416                    if (IsDvdDirectory(child.FullName, filename, directoryService))
 417                    {
 0418                        var movie = new T
 0419                        {
 0420                            Path = path,
 0421                            VideoType = VideoType.Dvd
 0422                        };
 0423                        Set3DFormat(movie);
 0424                        return movie;
 425                    }
 426
 0427                    if (IsBluRayDirectory(filename))
 428                    {
 0429                        var movie = new T
 0430                        {
 0431                            Path = path,
 0432                            VideoType = VideoType.BluRay
 0433                        };
 0434                        Set3DFormat(movie);
 0435                        return movie;
 436                    }
 437
 0438                    multiDiscFolders.Add(child);
 439                }
 0440                else if (IsDvdFile(filename))
 441                {
 0442                    var movie = new T
 0443                    {
 0444                        Path = path,
 0445                        VideoType = VideoType.Dvd
 0446                    };
 0447                    Set3DFormat(movie);
 0448                    return movie;
 449                }
 0450                else if (supportPhotos && PhotoResolver.IsImageFile(child.FullName, _imageProcessor))
 451                {
 0452                    photos.Add(child);
 453                }
 454            }
 455
 456            // TODO: Allow GetMultiDiscMovie in here
 457            const bool SupportsMultiVersion = true;
 458
 0459            var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
 0460                new MultiItemResolverResult();
 461
 0462            var isPhotosCollection = collectionType == CollectionType.homevideos || collectionType == CollectionType.pho
 0463            if (!isPhotosCollection && result.Items.Count == 1)
 464            {
 0465                var videoPath = result.Items[0].Path;
 0466                var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
 0467                var hasOtherSubfolders = multiDiscFolders.Count > 0;
 468
 0469                if (!hasPhotos && !hasOtherSubfolders)
 470                {
 0471                    var movie = (T)result.Items[0];
 0472                    movie.IsInMixedFolder = false;
 0473                    if (collectionType == CollectionType.movies || collectionType is null)
 474                    {
 0475                        movie.Name = Path.GetFileName(movie.ContainingFolderPath);
 476                    }
 477
 0478                    return movie;
 479                }
 480            }
 0481            else if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
 482            {
 0483                return GetMultiDiscMovie<T>(multiDiscFolders, directoryService);
 484            }
 485
 0486            return null;
 0487        }
 488
 489        /// <summary>
 490        /// Gets the multi disc movie.
 491        /// </summary>
 492        /// <param name="multiDiscFolders">The folders.</param>
 493        /// <param name="directoryService">The directory service.</param>
 494        /// <returns>``0.</returns>
 495        private T GetMultiDiscMovie<T>(List<FileSystemMetadata> multiDiscFolders, IDirectoryService directoryService)
 496               where T : Video, new()
 497        {
 0498            var videoTypes = new List<VideoType>();
 499
 0500            var folderPaths = multiDiscFolders.Select(i => i.FullName).Where(i =>
 0501            {
 0502                var subFileEntries = directoryService.GetFileSystemEntries(i);
 0503
 0504                var subfolders = subFileEntries
 0505                    .Where(e => e.IsDirectory)
 0506                    .ToList();
 0507
 0508                if (subfolders.Any(s => IsDvdDirectory(s.FullName, s.Name, directoryService)))
 0509                {
 0510                    videoTypes.Add(VideoType.Dvd);
 0511                    return true;
 0512                }
 0513
 0514                if (subfolders.Any(s => IsBluRayDirectory(s.Name)))
 0515                {
 0516                    videoTypes.Add(VideoType.BluRay);
 0517                    return true;
 0518                }
 0519
 0520                var subFiles = subFileEntries
 0521                 .Where(e => !e.IsDirectory)
 0522                 .Select(d => d.Name);
 0523
 0524                if (subFiles.Any(IsDvdFile))
 0525                {
 0526                    videoTypes.Add(VideoType.Dvd);
 0527                    return true;
 0528                }
 0529
 0530                return false;
 0531            }).Order().ToList();
 532
 533            // If different video types were found, don't allow this
 0534            if (videoTypes.Distinct().Count() > 1)
 535            {
 0536                return null;
 537            }
 538
 0539            if (folderPaths.Count == 0)
 540            {
 0541                return null;
 542            }
 543
 0544            var result = StackResolver.ResolveDirectories(folderPaths, NamingOptions).ToList();
 545
 0546            if (result.Count != 1)
 547            {
 0548                return null;
 549            }
 550
 0551            int additionalPartsLen = folderPaths.Count - 1;
 0552            var additionalParts = new string[additionalPartsLen];
 0553            folderPaths.CopyTo(1, additionalParts, 0, additionalPartsLen);
 554
 0555            var returnVideo = new T
 0556            {
 0557                Path = folderPaths[0],
 0558                AdditionalParts = additionalParts,
 0559                VideoType = videoTypes[0],
 0560                Name = result[0].Name
 0561            };
 562
 0563            SetIsoType(returnVideo);
 564
 0565            return returnVideo;
 566        }
 567
 568        private bool IsInvalid(Folder parent, CollectionType? collectionType)
 569        {
 60570            if (parent is not null)
 571            {
 60572                if (parent.IsRoot)
 573                {
 60574                    return true;
 575                }
 576            }
 577
 0578            if (collectionType is null)
 579            {
 0580                return false;
 581            }
 582
 0583            return !_validCollectionTypes.Contains(collectionType.Value);
 584        }
 585    }
 586}

Methods/Properties

.cctor()
.ctor(MediaBrowser.Controller.Drawing.IImageProcessor,Microsoft.Extensions.Logging.ILogger`1<Emby.Server.Implementations.Library.Resolvers.Movies.MovieResolver>,Emby.Naming.Common.NamingOptions,MediaBrowser.Controller.Providers.IDirectoryService)
get_Priority()
ResolveMultiple(MediaBrowser.Controller.Entities.Folder,System.Collections.Generic.List`1<MediaBrowser.Model.IO.FileSystemMetadata>,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,MediaBrowser.Controller.Providers.IDirectoryService)
Resolve(MediaBrowser.Controller.Library.ItemResolveArgs)
ResolveMultipleInternal(MediaBrowser.Controller.Entities.Folder,System.Collections.Generic.List`1<MediaBrowser.Model.IO.FileSystemMetadata>,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)
ResolveVideos(MediaBrowser.Controller.Entities.Folder,System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>,System.Boolean,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.Boolean)
ContainsFile(System.Collections.Generic.IReadOnlyList`1<Emby.Naming.Video.VideoInfo>,MediaBrowser.Model.IO.FileSystemMetadata)
ContainsFile(Emby.Naming.Video.VideoFileInfo,MediaBrowser.Model.IO.FileSystemMetadata)
SetInitialItemValues(MediaBrowser.Controller.Entities.Video,MediaBrowser.Controller.Library.ItemResolveArgs)
SetProviderIdsFromPath(MediaBrowser.Controller.Entities.Video)
FindMovie(MediaBrowser.Controller.Library.ItemResolveArgs,System.String,MediaBrowser.Controller.Entities.Folder,System.Collections.Generic.List`1<MediaBrowser.Model.IO.FileSystemMetadata>,MediaBrowser.Controller.Providers.IDirectoryService,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.Boolean)
GetMultiDiscMovie(System.Collections.Generic.List`1<MediaBrowser.Model.IO.FileSystemMetadata>,MediaBrowser.Controller.Providers.IDirectoryService)
IsInvalid(MediaBrowser.Controller.Entities.Folder,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)