< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Collections.CollectionManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Collections/CollectionManager.cs
Line coverage
5%
Covered lines: 9
Uncovered lines: 146
Coverable lines: 155
Total lines: 392
Line coverage: 5.8%
Branch coverage
0%
Covered branches: 0
Total branches: 62
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/6/2026 - 12:14:09 AM Line coverage: 37.5% (15/40) Branch coverage: 0% (0/20) Total lines: 3694/19/2026 - 12:14:27 AM Line coverage: 13.6% (20/147) Branch coverage: 3.3% (2/60) Total lines: 3695/4/2026 - 12:15:16 AM Line coverage: 5.4% (8/147) Branch coverage: 0% (0/60) Total lines: 3695/30/2026 - 12:15:32 AM Line coverage: 5.8% (9/155) Branch coverage: 0% (0/62) Total lines: 392 3/6/2026 - 12:14:09 AM Line coverage: 37.5% (15/40) Branch coverage: 0% (0/20) Total lines: 3694/19/2026 - 12:14:27 AM Line coverage: 13.6% (20/147) Branch coverage: 3.3% (2/60) Total lines: 3695/4/2026 - 12:15:16 AM Line coverage: 5.4% (8/147) Branch coverage: 0% (0/60) Total lines: 3695/30/2026 - 12:15:32 AM Line coverage: 5.8% (9/155) Branch coverage: 0% (0/62) Total lines: 392

Coverage delta

Coverage delta 24 -24

Metrics

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Collections/CollectionManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using System.Threading;
 6using System.Threading.Tasks;
 7using Jellyfin.Data.Enums;
 8using Jellyfin.Database.Implementations.Entities;
 9using Jellyfin.Extensions;
 10using MediaBrowser.Common.Configuration;
 11using MediaBrowser.Controller.Collections;
 12using MediaBrowser.Controller.Entities;
 13using MediaBrowser.Controller.Entities.Movies;
 14using MediaBrowser.Controller.Library;
 15using MediaBrowser.Controller.Persistence;
 16using MediaBrowser.Controller.Providers;
 17using MediaBrowser.Model.Configuration;
 18using MediaBrowser.Model.Entities;
 19using MediaBrowser.Model.Globalization;
 20using MediaBrowser.Model.IO;
 21using Microsoft.Extensions.Logging;
 22
 23namespace Emby.Server.Implementations.Collections
 24{
 25    /// <summary>
 26    /// The collection manager.
 27    /// </summary>
 28    public class CollectionManager : ICollectionManager
 29    {
 30        private readonly ILibraryManager _libraryManager;
 31        private readonly IFileSystem _fileSystem;
 32        private readonly ILibraryMonitor _iLibraryMonitor;
 33        private readonly ILogger<CollectionManager> _logger;
 34        private readonly IProviderManager _providerManager;
 35        private readonly ILinkedChildrenService _linkedChildrenService;
 36        private readonly ILocalizationManager _localizationManager;
 37        private readonly IApplicationPaths _appPaths;
 38
 39        /// <summary>
 40        /// Initializes a new instance of the <see cref="CollectionManager"/> class.
 41        /// </summary>
 42        /// <param name="libraryManager">The library manager.</param>
 43        /// <param name="appPaths">The application paths.</param>
 44        /// <param name="localizationManager">The localization manager.</param>
 45        /// <param name="fileSystem">The filesystem.</param>
 46        /// <param name="iLibraryMonitor">The library monitor.</param>
 47        /// <param name="loggerFactory">The logger factory.</param>
 48        /// <param name="providerManager">The provider manager.</param>
 49        /// <param name="linkedChildrenService">The linked children service.</param>
 50        public CollectionManager(
 51            ILibraryManager libraryManager,
 52            IApplicationPaths appPaths,
 53            ILocalizationManager localizationManager,
 54            IFileSystem fileSystem,
 55            ILibraryMonitor iLibraryMonitor,
 56            ILoggerFactory loggerFactory,
 57            IProviderManager providerManager,
 58            ILinkedChildrenService linkedChildrenService)
 59        {
 2160            _libraryManager = libraryManager;
 2161            _fileSystem = fileSystem;
 2162            _iLibraryMonitor = iLibraryMonitor;
 2163            _logger = loggerFactory.CreateLogger<CollectionManager>();
 2164            _providerManager = providerManager;
 2165            _linkedChildrenService = linkedChildrenService;
 2166            _localizationManager = localizationManager;
 2167            _appPaths = appPaths;
 2168        }
 69
 70        /// <inheritdoc />
 71        public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
 72
 73        /// <inheritdoc />
 74        public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
 75
 76        /// <inheritdoc />
 77        public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
 78
 79        private IEnumerable<Folder> FindFolders(string path)
 80        {
 081            return _libraryManager
 082                .RootFolder
 083                .Children
 084                .OfType<Folder>()
 085                .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
 86        }
 87
 88        internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
 89        {
 090            var existingFolder = FindFolders(path).FirstOrDefault();
 091            if (existingFolder is not null)
 92            {
 093                return existingFolder;
 94            }
 95
 096            if (!createIfNeeded)
 97            {
 098                return null;
 99            }
 100
 0101            Directory.CreateDirectory(path);
 102
 0103            var libraryOptions = new LibraryOptions
 0104            {
 0105                PathInfos = [new MediaPathInfo(path)],
 0106                EnableRealtimeMonitor = false,
 0107                SaveLocalMetadata = true
 0108            };
 109
 0110            var name = _localizationManager.GetLocalizedString("Collections");
 111
 0112            await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureA
 113
 0114            _libraryManager.RootFolder.Children = null;
 115
 0116            return FindFolders(path).First();
 0117        }
 118
 119        internal string GetCollectionsFolderPath()
 120        {
 0121            return Path.Combine(_appPaths.DataPath, "collections");
 122        }
 123
 124        /// <inheritdoc />
 125        public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
 126        {
 0127            return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
 128        }
 129
 130        /// <inheritdoc />
 131        public IEnumerable<BoxSet> GetCollectionsContainingItem(User user, Guid itemId)
 132        {
 0133            ArgumentNullException.ThrowIfNull(user);
 134
 0135            if (itemId.IsEmpty())
 136            {
 0137                return Enumerable.Empty<BoxSet>();
 138            }
 139
 0140            return _linkedChildrenService
 0141                .GetManualLinkedParentIds(itemId, BaseItemKind.BoxSet)
 0142                .Select(parentId => _libraryManager.GetItemById<BoxSet>(parentId, user))
 0143                .OfType<BoxSet>();
 144        }
 145
 146        private IEnumerable<BoxSet> GetCollections(User user)
 147        {
 0148            var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
 149
 0150            return folder is null
 0151                ? Enumerable.Empty<BoxSet>()
 0152                : folder.GetChildren(user, true).OfType<BoxSet>();
 153        }
 154
 155        /// <inheritdoc />
 156        public async Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options)
 157        {
 0158            var name = options.Name;
 159
 160            // Need to use the [boxset] suffix
 161            // If internet metadata is not found, or if xml saving is off there will be no collection.xml
 162            // This could cause it to get re-resolved as a plain folder
 0163            var folderName = _fileSystem.GetValidFilename(name) + " [boxset]";
 164
 0165            var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false);
 166
 0167            if (parentFolder is null)
 168            {
 0169                throw new ArgumentException(nameof(parentFolder));
 170            }
 171
 0172            var path = Path.Combine(parentFolder.Path, folderName);
 173
 0174            _iLibraryMonitor.ReportFileSystemChangeBeginning(path);
 175
 176            try
 177            {
 0178                var info = Directory.CreateDirectory(path);
 0179                var collection = new BoxSet
 0180                {
 0181                    Name = name,
 0182                    Path = path,
 0183                    IsLocked = options.IsLocked,
 0184                    ProviderIds = options.ProviderIds,
 0185                    DateCreated = info.CreationTimeUtc,
 0186                    DateModified = info.LastWriteTimeUtc
 0187                };
 188
 0189                parentFolder.AddChild(collection);
 190
 0191                if (options.ItemIdList.Count > 0)
 192                {
 0193                    await AddToCollectionAsync(
 0194                        collection.Id,
 0195                        options.ItemIdList.Select(x => new Guid(x)),
 0196                        false,
 0197                        new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 0198                        {
 0199                            // The initial adding of items is going to create a local metadata file
 0200                            // This will cause internet metadata to be skipped as a result
 0201                            MetadataRefreshMode = MetadataRefreshMode.FullRefresh
 0202                        }).ConfigureAwait(false);
 203                }
 204                else
 205                {
 0206                    _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSy
 207                }
 208
 0209                CollectionCreated?.Invoke(this, new CollectionCreatedEventArgs
 0210                {
 0211                    Collection = collection,
 0212                    Options = options
 0213                });
 214
 0215                return collection;
 216            }
 217            finally
 218            {
 219                // Refresh handled internally
 0220                _iLibraryMonitor.ReportFileSystemChangeComplete(path, false);
 221            }
 0222        }
 223
 224        /// <inheritdoc />
 225        public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
 0226            => AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSy
 227
 228        private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefres
 229        {
 0230            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
 231            {
 0232                throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId);
 233            }
 234
 0235            List<BaseItem>? itemList = null;
 236
 0237            var linkedChildrenList = collection.GetLinkedChildren();
 0238            var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
 239
 0240            foreach (var id in ids)
 241            {
 0242                var item = _libraryManager.GetItemById(id);
 243
 0244                if (item is null)
 245                {
 0246                    throw new ArgumentException("No item exists with the supplied Id " + id);
 247                }
 248
 0249                if (!currentLinkedChildrenIds.Contains(id))
 250                {
 0251                    (itemList ??= new()).Add(item);
 252
 0253                    linkedChildrenList.Add(item);
 254                }
 255            }
 256
 0257            if (itemList is not null)
 258            {
 0259                var originalLen = collection.LinkedChildren.Length;
 0260                var newItemCount = itemList.Count;
 0261                LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount];
 0262                collection.LinkedChildren.CopyTo(newChildren, 0);
 0263                for (int i = 0; i < newItemCount; i++)
 264                {
 0265                    newChildren[originalLen + i] = LinkedChild.Create(itemList[i]);
 266                }
 267
 0268                collection.LinkedChildren = newChildren;
 0269                collection.UpdateRatingToItems(linkedChildrenList);
 270
 0271                await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureA
 272
 0273                refreshOptions.ForceSave = true;
 0274                _providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High);
 275
 0276                if (fireEvent)
 277                {
 0278                    ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
 279                }
 280            }
 0281        }
 282
 283        /// <inheritdoc />
 284        public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
 285        {
 0286            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
 287            {
 0288                throw new ArgumentException("No collection exists with the supplied Id");
 289            }
 290
 0291            var list = new List<LinkedChild>();
 0292            var itemList = new List<BaseItem>();
 293
 0294            foreach (var guidId in itemIds)
 295            {
 0296                var childItem = _libraryManager.GetItemById(guidId);
 297
 0298                var child = collection.LinkedChildren.FirstOrDefault(i => i.ItemId.HasValue && i.ItemId.Value.Equals(gui
 299
 0300                if (child is null)
 301                {
 0302                    _logger.LogWarning("No collection title exists with the supplied Id");
 0303                    continue;
 304                }
 305
 0306                list.Add(child);
 307
 0308                if (childItem is not null)
 309                {
 0310                    itemList.Add(childItem);
 311                }
 312            }
 313
 0314            if (list.Count > 0)
 315            {
 0316                collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray();
 317            }
 318
 0319            await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait
 0320            _providerManager.QueueRefresh(
 0321                collection.Id,
 0322                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 0323                {
 0324                    ForceSave = true
 0325                },
 0326                RefreshPriority.High);
 327
 0328            ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
 0329        }
 330
 331        /// <inheritdoc />
 332        public IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user)
 333        {
 0334            var results = new Dictionary<Guid, BaseItem>();
 335
 0336            var allBoxSets = GetCollections(user).ToList();
 337
 0338            foreach (var item in items)
 339            {
 0340                if (item is ISupportsBoxSetGrouping)
 341                {
 0342                    var itemId = item.Id;
 343
 0344                    var itemIsInBoxSet = false;
 0345                    foreach (var boxSet in allBoxSets)
 346                    {
 0347                        if (!boxSet.ContainsLinkedChildByItemId(itemId))
 348                        {
 349                            continue;
 350                        }
 351
 0352                        itemIsInBoxSet = true;
 353
 0354                        results.TryAdd(boxSet.Id, boxSet);
 355                    }
 356
 357                    // skip any item that is in a box set
 0358                    if (itemIsInBoxSet)
 359                    {
 360                        continue;
 361                    }
 362
 0363                    var alreadyInResults = false;
 364
 365                    // this is kind of a performance hack because only Video has alternate versions that should be in a 
 0366                    if (item is Video video)
 367                    {
 0368                        foreach (var childId in _libraryManager.GetLocalAlternateVersionIds(video))
 369                        {
 0370                            if (!results.ContainsKey(childId))
 371                            {
 372                                continue;
 373                            }
 374
 0375                            alreadyInResults = true;
 0376                            break;
 377                        }
 378                    }
 379
 0380                    if (alreadyInResults)
 381                    {
 382                        continue;
 383                    }
 384                }
 385
 0386                results[item.Id] = item;
 387            }
 388
 0389            return results.Values;
 390        }
 391    }
 392}