using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Collections { /// /// The collection manager. /// public class CollectionManager : ICollectionManager { private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _iLibraryMonitor; private readonly ILogger _logger; private readonly IProviderManager _providerManager; private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; /// /// Initializes a new instance of the class. /// /// The library manager. /// The application paths. /// The localization manager. /// The filesystem. /// The library monitor. /// The logger factory. /// The provider manager. public CollectionManager( ILibraryManager libraryManager, IApplicationPaths appPaths, ILocalizationManager localizationManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, ILoggerFactory loggerFactory, IProviderManager providerManager) { _libraryManager = libraryManager; _fileSystem = fileSystem; _iLibraryMonitor = iLibraryMonitor; _logger = loggerFactory.CreateLogger(); _providerManager = providerManager; _localizationManager = localizationManager; _appPaths = appPaths; } /// public event EventHandler? CollectionCreated; /// public event EventHandler? ItemsAddedToCollection; /// public event EventHandler? ItemsRemovedFromCollection; private IEnumerable FindFolders(string path) { return _libraryManager .RootFolder .Children .OfType() .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path)); } internal async Task EnsureLibraryFolder(string path, bool createIfNeeded) { var existingFolder = FindFolders(path).FirstOrDefault(); if (existingFolder != null) { return existingFolder; } if (!createIfNeeded) { return null; } Directory.CreateDirectory(path); var libraryOptions = new LibraryOptions { PathInfos = new[] { new MediaPathInfo(path) }, EnableRealtimeMonitor = false, SaveLocalMetadata = true }; var name = _localizationManager.GetLocalizedString("Collections"); await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false); return FindFolders(path).First(); } internal string GetCollectionsFolderPath() { return Path.Combine(_appPaths.DataPath, "collections"); } private Task GetCollectionsFolder(bool createIfNeeded) { return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded); } private IEnumerable GetCollections(User user) { var folder = GetCollectionsFolder(false).GetAwaiter().GetResult(); return folder == null ? Enumerable.Empty() : folder.GetChildren(user, true).OfType(); } /// public async Task CreateCollectionAsync(CollectionCreationOptions options) { var name = options.Name; // Need to use the [boxset] suffix // If internet metadata is not found, or if xml saving is off there will be no collection.xml // This could cause it to get re-resolved as a plain folder var folderName = _fileSystem.GetValidFilename(name) + " [boxset]"; var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false); if (parentFolder == null) { throw new ArgumentException(nameof(parentFolder)); } var path = Path.Combine(parentFolder.Path, folderName); _iLibraryMonitor.ReportFileSystemChangeBeginning(path); try { Directory.CreateDirectory(path); var collection = new BoxSet { Name = name, Path = path, IsLocked = options.IsLocked, ProviderIds = options.ProviderIds, DateCreated = DateTime.UtcNow }; parentFolder.AddChild(collection); if (options.ItemIdList.Count > 0) { await AddToCollectionAsync( collection.Id, options.ItemIdList.Select(x => new Guid(x)), false, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // The initial adding of items is going to create a local metadata file // This will cause internet metadata to be skipped as a result MetadataRefreshMode = MetadataRefreshMode.FullRefresh }).ConfigureAwait(false); } else { _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); } CollectionCreated?.Invoke(this, new CollectionCreatedEventArgs { Collection = collection, Options = options }); return collection; } finally { // Refresh handled internally _iLibraryMonitor.ReportFileSystemChangeComplete(path, false); } } /// public Task AddToCollectionAsync(Guid collectionId, IEnumerable itemIds) => AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); private async Task AddToCollectionAsync(Guid collectionId, IEnumerable ids, bool fireEvent, MetadataRefreshOptions refreshOptions) { if (_libraryManager.GetItemById(collectionId) is not BoxSet collection) { throw new ArgumentException("No collection exists with the supplied Id"); } var list = new List(); var itemList = new List(); var linkedChildrenList = collection.GetLinkedChildren(); var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList(); foreach (var id in ids) { var item = _libraryManager.GetItemById(id); if (item == null) { throw new ArgumentException("No item exists with the supplied Id"); } if (!currentLinkedChildrenIds.Contains(id)) { itemList.Add(item); list.Add(LinkedChild.Create(item)); linkedChildrenList.Add(item); } } if (list.Count > 0) { var newList = collection.LinkedChildren.ToList(); newList.AddRange(list); collection.LinkedChildren = newList.ToArray(); collection.UpdateRatingToItems(linkedChildrenList); await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); refreshOptions.ForceSave = true; _providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High); if (fireEvent) { ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList)); } } } /// public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable itemIds) { if (_libraryManager.GetItemById(collectionId) is not BoxSet collection) { throw new ArgumentException("No collection exists with the supplied Id"); } var list = new List(); var itemList = new List(); foreach (var guidId in itemIds) { var childItem = _libraryManager.GetItemById(guidId); var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase))); if (child == null) { _logger.LogWarning("No collection title exists with the supplied Id"); continue; } list.Add(child); if (childItem != null) { itemList.Add(childItem); } } if (list.Count > 0) { collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray(); } await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); _providerManager.QueueRefresh( collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, RefreshPriority.High); ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList)); } /// public IEnumerable CollapseItemsWithinBoxSets(IEnumerable items, User user) { var results = new Dictionary(); var allBoxSets = GetCollections(user).ToList(); foreach (var item in items) { if (item is ISupportsBoxSetGrouping) { var itemId = item.Id; var itemIsInBoxSet = false; foreach (var boxSet in allBoxSets) { if (!boxSet.ContainsLinkedChildByItemId(itemId)) { continue; } itemIsInBoxSet = true; results.TryAdd(boxSet.Id, boxSet); } // skip any item that is in a box set if (itemIsInBoxSet) { continue; } var alreadyInResults = false; // this is kind of a performance hack because only Video has alternate versions that should be in a box set? if (item is Video video) { foreach (var childId in video.GetLocalAlternateVersionIds()) { if (!results.ContainsKey(childId)) { continue; } alreadyInResults = true; break; } } if (alreadyInResults) { continue; } } results[item.Id] = item; } return results.Values; } } }