#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// /// Specialized Folder class that points to a subset of the physical folders in the system. /// It is created from the user-specific folders within the system root. /// public class CollectionFolder : Folder, ICollectionFolder { private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private static readonly Dictionary _libraryOptions = new Dictionary(); private bool _requiresRefresh; /// /// Initializes a new instance of the class. /// public CollectionFolder() { PhysicalLocationsList = Array.Empty(); PhysicalFolderIds = Array.Empty(); } public static IXmlSerializer XmlSerializer { get; set; } public static IServerApplicationHost ApplicationHost { get; set; } [JsonIgnore] public override bool SupportsPlayedStatus => false; [JsonIgnore] public override bool SupportsInheritedParentImages => false; public string CollectionType { get; set; } /// /// Gets the item's children. /// /// /// Our children are actually just references to the ones in the physical root... /// /// The actual children. [JsonIgnore] public override IEnumerable Children => GetActualChildren(); public override bool CanDelete() { return false; } public LibraryOptions GetLibraryOptions() { return GetLibraryOptions(Path); } private static LibraryOptions LoadLibraryOptions(string path) { try { var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions; if (result == null) { return new LibraryOptions(); } foreach (var mediaPath in result.PathInfos) { if (!string.IsNullOrEmpty(mediaPath.Path)) { mediaPath.Path = ApplicationHost.ExpandVirtualPath(mediaPath.Path); } } return result; } catch (FileNotFoundException) { return new LibraryOptions(); } catch (IOException) { return new LibraryOptions(); } catch (Exception ex) { Logger.LogError(ex, "Error loading library options"); return new LibraryOptions(); } } private static string GetLibraryOptionsPath(string path) { return System.IO.Path.Combine(path, "options.xml"); } public void UpdateLibraryOptions(LibraryOptions options) { SaveLibraryOptions(Path, options); } public static LibraryOptions GetLibraryOptions(string path) { lock (_libraryOptions) { if (!_libraryOptions.TryGetValue(path, out var options)) { options = LoadLibraryOptions(path); _libraryOptions[path] = options; } return options; } } public static void SaveLibraryOptions(string path, LibraryOptions options) { lock (_libraryOptions) { _libraryOptions[path] = options; var clone = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); foreach (var mediaPath in clone.PathInfos) { if (!string.IsNullOrEmpty(mediaPath.Path)) { mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); } } XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); } } public static void OnCollectionFolderChange() { lock (_libraryOptions) { _libraryOptions.Clear(); } } /// /// Gets the display preferences id. /// /// /// Allow different display preferences for each collection folder. /// /// The display prefs id. [JsonIgnore] public override Guid DisplayPreferencesId => Id; [JsonIgnore] public override string[] PhysicalLocations => PhysicalLocationsList; public string[] PhysicalLocationsList { get; set; } public Guid[] PhysicalFolderIds { get; set; } public override bool IsSaveLocalMetadataEnabled() { return true; } protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { return CreateResolveArgs(directoryService, true).FileSystemChildren; } public override bool RequiresRefresh() { var changed = base.RequiresRefresh() || _requiresRefresh; if (!changed) { var locations = PhysicalLocations; var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations; if (!locations.SequenceEqual(newLocations)) { changed = true; } } if (!changed) { var folderIds = PhysicalFolderIds; var newFolderIds = GetPhysicalFolders(false).Select(i => i.Id).ToList(); if (!folderIds.SequenceEqual(newFolderIds)) { changed = true; } } return changed; } public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh; _requiresRefresh = false; return changed; } public override double? GetRefreshProgress() { var folders = GetPhysicalFolders(true).ToList(); double totalProgresses = 0; var foldersWithProgress = 0; foreach (var folder in folders) { var progress = ProviderManager.GetRefreshProgress(folder.Id); if (progress.HasValue) { totalProgresses += progress.Value; foldersWithProgress++; } } if (foldersWithProgress == 0) { return null; } return totalProgresses / foldersWithProgress; } protected override bool RefreshLinkedChildren(IEnumerable fileSystemChildren) { return RefreshLinkedChildrenInternal(true); } private bool RefreshLinkedChildrenInternal(bool setFolders) { var physicalFolders = GetPhysicalFolders(false) .ToList(); var linkedChildren = physicalFolders .SelectMany(c => c.LinkedChildren) .ToList(); var changed = !linkedChildren.SequenceEqual(LinkedChildren, new LinkedChildComparer(FileSystem)); LinkedChildren = linkedChildren.ToArray(); var folderIds = PhysicalFolderIds; var newFolderIds = physicalFolders.Select(i => i.Id).ToArray(); if (!folderIds.SequenceEqual(newFolderIds)) { changed = true; if (setFolders) { PhysicalFolderIds = newFolderIds; } } return changed; } private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations) { var path = ContainingFolderPath; var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { FileInfo = FileSystem.GetDirectoryInfo(path), Parent = GetParent() as Folder, CollectionType = CollectionType }; // Gather child folder and files if (args.IsDirectory) { var flattenFolderDepth = 0; var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true); args.FileSystemChildren = files; } _requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations); if (setPhysicalLocations) { PhysicalLocationsList = args.PhysicalLocations; } return args; } /// /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes /// ***Currently does not contain logic to maintain items that are unavailable in the file system***. /// /// The progress. /// if set to true [recursive]. /// if set to true [refresh child metadata]. /// The refresh options. /// The directory service. /// The cancellation token. /// Task. protected override Task ValidateChildrenInternal(IProgress progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { return Task.CompletedTask; } public IEnumerable GetActualChildren() { return GetPhysicalFolders(true).SelectMany(c => c.Children); } public IEnumerable GetPhysicalFolders() { return GetPhysicalFolders(true); } private IEnumerable GetPhysicalFolders(bool enableCache) { if (enableCache) { return PhysicalFolderIds.Select(i => LibraryManager.GetItemById(i)).OfType(); } var rootChildren = LibraryManager.RootFolder.Children .OfType() .ToList(); return PhysicalLocations .Where(i => !FileSystem.AreEqual(i, Path)) .SelectMany(i => GetPhysicalParents(i, rootChildren)) .GroupBy(x => x.Id) .Select(x => x.First()); } private IEnumerable GetPhysicalParents(string path, List rootChildren) { var result = rootChildren .Where(i => FileSystem.AreEqual(i.Path, path)) .ToList(); if (result.Count == 0) { if (LibraryManager.FindByPath(path, true) is Folder folder) { result.Add(folder); } } return result; } [JsonIgnore] public override bool SupportsPeople => false; } }