< 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: 394
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 5/16/2026 - 12:15:55 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: 39210/29/2026 - 12:17:11 AM Line coverage: 5.8% (9/155) Branch coverage: 0% (0/62) Total lines: 394 5/16/2026 - 12:15:55 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: 39210/29/2026 - 12:17:11 AM Line coverage: 5.8% (9/155) Branch coverage: 0% (0/62) Total lines: 394

Coverage delta

Coverage delta 1 -1

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.Dto;
 13using MediaBrowser.Controller.Entities;
 14using MediaBrowser.Controller.Entities.Movies;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.Persistence;
 17using MediaBrowser.Controller.Providers;
 18using MediaBrowser.Model.Configuration;
 19using MediaBrowser.Model.Entities;
 20using MediaBrowser.Model.Globalization;
 21using MediaBrowser.Model.IO;
 22using Microsoft.Extensions.Logging;
 23
 24namespace Emby.Server.Implementations.Collections
 25{
 26    /// <summary>
 27    /// The collection manager.
 28    /// </summary>
 29    public class CollectionManager : ICollectionManager
 30    {
 31        private readonly ILibraryManager _libraryManager;
 32        private readonly IFileSystem _fileSystem;
 33        private readonly ILibraryMonitor _iLibraryMonitor;
 34        private readonly ILogger<CollectionManager> _logger;
 35        private readonly IProviderManager _providerManager;
 36        private readonly ILinkedChildrenService _linkedChildrenService;
 37        private readonly ILocalizationManager _localizationManager;
 38        private readonly IApplicationPaths _appPaths;
 39
 40        /// <summary>
 41        /// Initializes a new instance of the <see cref="CollectionManager"/> class.
 42        /// </summary>
 43        /// <param name="libraryManager">The library manager.</param>
 44        /// <param name="appPaths">The application paths.</param>
 45        /// <param name="localizationManager">The localization manager.</param>
 46        /// <param name="fileSystem">The filesystem.</param>
 47        /// <param name="iLibraryMonitor">The library monitor.</param>
 48        /// <param name="loggerFactory">The logger factory.</param>
 49        /// <param name="providerManager">The provider manager.</param>
 50        /// <param name="linkedChildrenService">The linked children service.</param>
 51        public CollectionManager(
 52            ILibraryManager libraryManager,
 53            IApplicationPaths appPaths,
 54            ILocalizationManager localizationManager,
 55            IFileSystem fileSystem,
 56            ILibraryMonitor iLibraryMonitor,
 57            ILoggerFactory loggerFactory,
 58            IProviderManager providerManager,
 59            ILinkedChildrenService linkedChildrenService)
 60        {
 2761            _libraryManager = libraryManager;
 2762            _fileSystem = fileSystem;
 2763            _iLibraryMonitor = iLibraryMonitor;
 2764            _logger = loggerFactory.CreateLogger<CollectionManager>();
 2765            _providerManager = providerManager;
 2766            _linkedChildrenService = linkedChildrenService;
 2767            _localizationManager = localizationManager;
 2768            _appPaths = appPaths;
 2769        }
 70
 71        /// <inheritdoc />
 72        public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
 73
 74        /// <inheritdoc />
 75        public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
 76
 77        /// <inheritdoc />
 78        public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
 79
 80        private IEnumerable<Folder> FindFolders(string path)
 81        {
 082            return _libraryManager
 083                .RootFolder
 084                .Children
 085                .OfType<Folder>()
 086                .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
 87        }
 88
 89        internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
 90        {
 091            var existingFolder = FindFolders(path).FirstOrDefault();
 092            if (existingFolder is not null)
 93            {
 094                return existingFolder;
 95            }
 96
 097            if (!createIfNeeded)
 98            {
 099                return null;
 100            }
 101
 0102            Directory.CreateDirectory(path);
 103
 0104            var libraryOptions = new LibraryOptions
 0105            {
 0106                PathInfos = [new MediaPathInfo(path)],
 0107                EnableRealtimeMonitor = false,
 0108                SaveLocalMetadata = true
 0109            };
 110
 111            // This names a library for the whole server, so ignore the requesting client's language.
 0112            var name = _localizationManager.GetServerLocalizedString("Collections");
 113
 0114            await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureA
 115
 0116            _libraryManager.RootFolder.Children = null;
 117
 0118            return FindFolders(path).FirstOrDefault();
 0119        }
 120
 121        internal string GetCollectionsFolderPath()
 122        {
 0123            return Path.Combine(_appPaths.DataPath, "collections");
 124        }
 125
 126        /// <inheritdoc />
 127        public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
 128        {
 0129            return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
 130        }
 131
 132        /// <inheritdoc />
 133        public IEnumerable<BoxSet> GetCollectionsContainingItem(User user, Guid itemId)
 134        {
 0135            ArgumentNullException.ThrowIfNull(user);
 136
 0137            if (itemId.IsEmpty())
 138            {
 0139                return Enumerable.Empty<BoxSet>();
 140            }
 141
 0142            return _linkedChildrenService
 0143                .GetManualLinkedParentIds(itemId, BaseItemKind.BoxSet)
 0144                .Select(parentId => _libraryManager.GetItemById<BoxSet>(parentId, user))
 0145                .OfType<BoxSet>();
 146        }
 147
 148        private IEnumerable<BoxSet> GetCollections(User user)
 149        {
 0150            var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
 151
 0152            return folder is null
 0153                ? Enumerable.Empty<BoxSet>()
 0154                : folder.GetChildren(user, true).OfType<BoxSet>();
 155        }
 156
 157        /// <inheritdoc />
 158        public async Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options)
 159        {
 0160            var name = options.Name;
 161
 162            // Need to use the [boxset] suffix
 163            // If internet metadata is not found, or if xml saving is off there will be no collection.xml
 164            // This could cause it to get re-resolved as a plain folder
 0165            var folderName = _fileSystem.GetValidFilename(name) + " [boxset]";
 166
 0167            var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false);
 168
 0169            if (parentFolder is null)
 170            {
 0171                throw new InvalidOperationException("Unable to resolve the collections library folder, so the collection
 172            }
 173
 0174            var path = Path.Combine(parentFolder.Path, folderName);
 175
 0176            _iLibraryMonitor.ReportFileSystemChangeBeginning(path);
 177
 178            try
 179            {
 0180                var info = Directory.CreateDirectory(path);
 0181                var collection = new BoxSet
 0182                {
 0183                    Name = name,
 0184                    Path = path,
 0185                    IsLocked = options.IsLocked,
 0186                    ProviderIds = options.ProviderIds,
 0187                    DateCreated = info.CreationTimeUtc,
 0188                    DateModified = info.LastWriteTimeUtc
 0189                };
 190
 0191                parentFolder.AddChild(collection);
 192
 0193                if (options.ItemIdList.Count > 0)
 194                {
 0195                    await AddToCollectionAsync(
 0196                        collection.Id,
 0197                        options.ItemIdList.Select(x => new Guid(x)),
 0198                        false,
 0199                        new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 0200                        {
 0201                            // The initial adding of items is going to create a local metadata file
 0202                            // This will cause internet metadata to be skipped as a result
 0203                            MetadataRefreshMode = MetadataRefreshMode.FullRefresh
 0204                        }).ConfigureAwait(false);
 205                }
 206                else
 207                {
 0208                    _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSy
 209                }
 210
 0211                CollectionCreated?.Invoke(this, new CollectionCreatedEventArgs
 0212                {
 0213                    Collection = collection,
 0214                    Options = options
 0215                });
 216
 0217                return collection;
 218            }
 219            finally
 220            {
 221                // Refresh handled internally
 0222                _iLibraryMonitor.ReportFileSystemChangeComplete(path, false);
 223            }
 0224        }
 225
 226        /// <inheritdoc />
 227        public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
 0228            => AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSy
 229
 230        private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefres
 231        {
 0232            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
 233            {
 0234                throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId);
 235            }
 236
 0237            List<BaseItem>? itemList = null;
 238
 0239            var linkedChildrenList = collection.GetLinkedChildren(DtoOptions.StoredColumnsOnly);
 0240            var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
 241
 0242            foreach (var id in ids)
 243            {
 0244                var item = _libraryManager.GetItemById(id);
 245
 0246                if (item is null)
 247                {
 0248                    throw new ArgumentException("No item exists with the supplied Id " + id);
 249                }
 250
 0251                if (!currentLinkedChildrenIds.Contains(id))
 252                {
 0253                    (itemList ??= new()).Add(item);
 254
 0255                    linkedChildrenList.Add(item);
 256                }
 257            }
 258
 0259            if (itemList is not null)
 260            {
 0261                var originalLen = collection.LinkedChildren.Length;
 0262                var newItemCount = itemList.Count;
 0263                LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount];
 0264                collection.LinkedChildren.CopyTo(newChildren, 0);
 0265                for (int i = 0; i < newItemCount; i++)
 266                {
 0267                    newChildren[originalLen + i] = LinkedChild.Create(itemList[i]);
 268                }
 269
 0270                collection.LinkedChildren = newChildren;
 0271                collection.UpdateRatingToItems(linkedChildrenList);
 272
 0273                await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureA
 274
 0275                refreshOptions.ForceSave = true;
 0276                _providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High);
 277
 0278                if (fireEvent)
 279                {
 0280                    ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
 281                }
 282            }
 0283        }
 284
 285        /// <inheritdoc />
 286        public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
 287        {
 0288            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
 289            {
 0290                throw new ArgumentException("No collection exists with the supplied Id");
 291            }
 292
 0293            var list = new List<LinkedChild>();
 0294            var itemList = new List<BaseItem>();
 295
 0296            foreach (var guidId in itemIds)
 297            {
 0298                var childItem = _libraryManager.GetItemById(guidId);
 299
 0300                var child = collection.LinkedChildren.FirstOrDefault(i => i.ItemId.HasValue && i.ItemId.Value.Equals(gui
 301
 0302                if (child is null)
 303                {
 0304                    _logger.LogWarning("No collection title exists with the supplied Id");
 0305                    continue;
 306                }
 307
 0308                list.Add(child);
 309
 0310                if (childItem is not null)
 311                {
 0312                    itemList.Add(childItem);
 313                }
 314            }
 315
 0316            if (list.Count > 0)
 317            {
 0318                collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray();
 319            }
 320
 0321            await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait
 0322            _providerManager.QueueRefresh(
 0323                collection.Id,
 0324                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 0325                {
 0326                    ForceSave = true
 0327                },
 0328                RefreshPriority.High);
 329
 0330            ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
 0331        }
 332
 333        /// <inheritdoc />
 334        public IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user)
 335        {
 0336            var results = new Dictionary<Guid, BaseItem>();
 337
 0338            var allBoxSets = GetCollections(user).ToList();
 339
 0340            foreach (var item in items)
 341            {
 0342                if (item is ISupportsBoxSetGrouping)
 343                {
 0344                    var itemId = item.Id;
 345
 0346                    var itemIsInBoxSet = false;
 0347                    foreach (var boxSet in allBoxSets)
 348                    {
 0349                        if (!boxSet.ContainsLinkedChildByItemId(itemId))
 350                        {
 351                            continue;
 352                        }
 353
 0354                        itemIsInBoxSet = true;
 355
 0356                        results.TryAdd(boxSet.Id, boxSet);
 357                    }
 358
 359                    // skip any item that is in a box set
 0360                    if (itemIsInBoxSet)
 361                    {
 362                        continue;
 363                    }
 364
 0365                    var alreadyInResults = false;
 366
 367                    // this is kind of a performance hack because only Video has alternate versions that should be in a 
 0368                    if (item is Video video)
 369                    {
 0370                        foreach (var childId in _libraryManager.GetLocalAlternateVersionIds(video))
 371                        {
 0372                            if (!results.ContainsKey(childId))
 373                            {
 374                                continue;
 375                            }
 376
 0377                            alreadyInResults = true;
 0378                            break;
 379                        }
 380                    }
 381
 0382                    if (alreadyInResults)
 383                    {
 384                        continue;
 385                    }
 386                }
 387
 0388                results[item.Id] = item;
 389            }
 390
 0391            return results.Values;
 392        }
 393    }
 394}