using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Extensions; 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 { public static IXmlSerializer XmlSerializer { get; set; } public static IJsonSerializer JsonSerializer { get; set; } public static IServerApplicationHost ApplicationHost { get; set; } public CollectionFolder() { PhysicalLocationsList = Array.Empty(); PhysicalFolderIds = Array.Empty(); } [IgnoreDataMember] public override bool SupportsPlayedStatus => false; [IgnoreDataMember] public override bool SupportsInheritedParentImages => false; public override bool CanDelete() { return false; } public string CollectionType { get; set; } private static readonly Dictionary LibraryOptions = new Dictionary(); 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.DeserializeFromString(JsonSerializer.SerializeToString(options)); 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(); } } /// /// Allow different display preferences for each collection folder /// /// The display prefs id. [IgnoreDataMember] public override Guid DisplayPreferencesId => Id; [IgnoreDataMember] public override string[] PhysicalLocations => PhysicalLocationsList; public override bool IsSaveLocalMetadataEnabled() { return true; } public string[] PhysicalLocationsList { get; set; } public Guid[] PhysicalFolderIds { get; set; } protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { return CreateResolveArgs(directoryService, true).FileSystemChildren; } private bool _requiresRefresh; public override bool RequiresRefresh() { var changed = base.RequiresRefresh() || _requiresRefresh; if (!changed) { var locations = PhysicalLocations; var newLocations = CreateResolveArgs(new DirectoryService(Logger, 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 replaceAllMetdata) { var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _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), Path = 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. /// The cancellation token. /// if set to true [recursive]. /// if set to true [refresh child metadata]. /// The refresh options. /// The directory service. /// Task. protected override Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) { return Task.CompletedTask; } /// /// Our children are actually just references to the ones in the physical root... /// /// The actual children. [IgnoreDataMember] public override IEnumerable Children => GetActualChildren(); 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)).DistinctBy(i => i.Id); } private IEnumerable GetPhysicalParents(string path, List rootChildren) { var result = rootChildren .Where(i => FileSystem.AreEqual(i.Path, path)) .ToList(); if (result.Count == 0) { var folder = LibraryManager.FindByPath(path, true) as Folder; if (folder != null) { result.Add(folder); } } return result; } [IgnoreDataMember] public override bool SupportsPeople => false; } }