#nullable disable #pragma warning disable CA1002, CA1721, CA1819, CS1591 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; using Season = MediaBrowser.Controller.Entities.TV.Season; using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Controller.Entities { /// /// Class Folder. /// public class Folder : BaseItem { public Folder() { LinkedChildren = Array.Empty(); } public static IUserViewManager UserViewManager { get; set; } /// /// Gets or sets a value indicating whether this instance is root. /// /// true if this instance is root; otherwise, false. public bool IsRoot { get; set; } public LinkedChild[] LinkedChildren { get; set; } [JsonIgnore] public DateTime? DateLastMediaAdded { get; set; } [JsonIgnore] public override bool SupportsThemeMedia => true; [JsonIgnore] public virtual bool IsPreSorted => false; [JsonIgnore] public virtual bool IsPhysicalRoot => false; [JsonIgnore] public override bool SupportsInheritedParentImages => true; [JsonIgnore] public override bool SupportsPlayedStatus => true; /// /// Gets a value indicating whether this instance is folder. /// /// true if this instance is folder; otherwise, false. [JsonIgnore] public override bool IsFolder => true; [JsonIgnore] public override bool IsDisplayedAsFolder => true; [JsonIgnore] public virtual bool SupportsCumulativeRunTimeTicks => false; [JsonIgnore] public virtual bool SupportsDateLastMediaAdded => false; [JsonIgnore] public override string FileNameWithoutExtension { get { if (IsFileProtocol) { return System.IO.Path.GetFileName(Path); } return null; } } /// /// Gets the actual children. /// /// The actual children. [JsonIgnore] public virtual IEnumerable Children => LoadChildren(); /// /// Gets thread-safe access to all recursive children of this folder - without regard to user. /// /// The recursive children. [JsonIgnore] public IEnumerable RecursiveChildren => GetRecursiveChildren(); [JsonIgnore] protected virtual bool SupportsShortcutChildren => false; protected virtual bool FilterLinkedChildrenPerUser => false; [JsonIgnore] protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren; [JsonIgnore] public virtual bool SupportsUserDataFromChildren { get { // These are just far too slow. if (this is ICollectionFolder) { return false; } if (this is UserView) { return false; } if (this is UserRootFolder) { return false; } if (this is Channel) { return false; } if (SourceType != SourceType.Library) { return false; } if (this is IItemByName) { if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName) { return false; } } return true; } } public static ICollectionManager CollectionManager { get; set; } public override bool CanDelete() { if (IsRoot) { return false; } return base.CanDelete(); } public override bool RequiresRefresh() { var baseResult = base.RequiresRefresh(); if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue) { baseResult = true; } return baseResult; } /// /// Adds the child. /// /// The item. /// Unable to add + item.Name. public void AddChild(BaseItem item) { item.SetParent(this); if (item.Id.IsEmpty()) { item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType()); } if (item.DateCreated == DateTime.MinValue) { item.DateCreated = DateTime.UtcNow; } if (item.DateModified == DateTime.MinValue) { item.DateModified = DateTime.UtcNow; } LibraryManager.CreateItem(item, this); } public override bool IsVisible(User user) { if (this is ICollectionFolder && this is not BasePluginFolder) { var blockedMediaFolders = user.GetPreferenceValues(PreferenceKind.BlockedMediaFolders); if (blockedMediaFolders.Length > 0) { if (blockedMediaFolders.Contains(Id)) { return false; } } else { if (!user.HasPermission(PermissionKind.EnableAllFolders) && !user.GetPreferenceValues(PreferenceKind.EnabledFolders).Contains(Id)) { return false; } } } return base.IsVisible(user); } /// /// Loads our children. Validation will occur externally. /// We want this synchronous. /// /// Returns children. protected virtual List LoadChildren() { // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path); // just load our children from the repo - the library will be validated and maintained in other processes return GetCachedChildren(); } public override double? GetRefreshProgress() { return ProviderManager.GetRefreshProgress(Id); } public Task ValidateChildren(IProgress progress, CancellationToken cancellationToken) { return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken: cancellationToken); } /// /// Validates that the children of the folder still exist. /// /// The progress. /// The metadata refresh options. /// if set to true [recursive]. /// remove item even this folder is root. /// The cancellation token. /// Task. public Task ValidateChildren(IProgress progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, bool allowRemoveRoot = false, CancellationToken cancellationToken = default) { return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken); } private Dictionary GetActualChildrenDictionary() { var dictionary = new Dictionary(); var childrenList = Children.ToList(); foreach (var child in childrenList) { var id = child.Id; if (dictionary.ContainsKey(id)) { Logger.LogError( "Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}", Path ?? Name, child.Path ?? child.Name); } else { dictionary[id] = child; } } return dictionary; } /// /// Validates the children internal. /// /// The progress. /// if set to true [recursive]. /// if set to true [refresh child metadata]. /// remove item even this folder is root. /// The refresh options. /// The directory service. /// The cancellation token. /// Task. protected virtual async Task ValidateChildrenInternal(IProgress progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (recursive) { ProviderManager.OnRefreshStart(this); } try { await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false); } finally { if (recursive) { ProviderManager.OnRefreshComplete(this); } } } private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item, bool checkCollection) { if (!checkCollection && (item is BoxSet || string.Equals(item.FileNameWithoutExtension, "collections", StringComparison.OrdinalIgnoreCase))) { return true; } // For top parents i.e. Library folders, skip the validation if it's empty or inaccessible if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath)) { Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.ContainingFolderPath); return false; } return true; } private async Task ValidateChildrenInternal2(IProgress progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (!IsLibraryFolderAccessible(directoryService, this, allowRemoveRoot)) { return; } cancellationToken.ThrowIfCancellationRequested(); var validChildren = new List(); var validChildrenNeedGeneration = false; if (IsFileProtocol) { IEnumerable nonCachedChildren = []; try { nonCachedChildren = GetNonCachedChildren(directoryService); } catch (IOException ex) { Logger.LogError(ex, "Error retrieving children from file system"); } catch (SecurityException ex) { Logger.LogError(ex, "Error retrieving children from file system"); } catch (Exception ex) { Logger.LogError(ex, "Error retrieving children"); return; } progress.Report(ProgressHelpers.RetrievedChildren); if (recursive) { ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren); } // Build a dictionary of the current children we have now by Id so we can compare quickly and easily var currentChildren = GetActualChildrenDictionary(); // Create a list for our validated children var newItems = new List(); cancellationToken.ThrowIfCancellationRequested(); foreach (var child in nonCachedChildren) { if (!IsLibraryFolderAccessible(directoryService, child, allowRemoveRoot)) { continue; } if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild)) { validChildren.Add(currentChild); if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None) { await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } else { // metadata is up-to-date; make sure DB has correct images dimensions and hash await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false); } continue; } // Brand new item - needs to be added child.SetParent(this); newItems.Add(child); validChildren.Add(child); } // That's all the new and changed ones - now see if any have been removed and need cleanup var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); var shouldRemove = !IsRoot || allowRemoveRoot; // If it's an AggregateFolder, don't remove if (shouldRemove && itemsRemoved.Count > 0) { foreach (var item in itemsRemoved) { if (item.IsFileProtocol) { Logger.LogDebug("Removed item: {Path}", item.Path); item.SetParent(null); LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false); } } } if (newItems.Count > 0) { LibraryManager.CreateItems(newItems, this, cancellationToken); } } else { validChildrenNeedGeneration = true; } progress.Report(ProgressHelpers.UpdatedChildItems); if (recursive) { ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems); } cancellationToken.ThrowIfCancellationRequested(); if (recursive) { var folder = this; var innerProgress = new Progress(innerPercent => { var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.ScannedSubfolders, innerPercent); progress.Report(percent); ProviderManager.OnRefreshProgress(folder, percent); }); if (validChildrenNeedGeneration) { validChildren = Children.ToList(); validChildrenNeedGeneration = false; } await ValidateSubFolders(validChildren.OfType().ToList(), directoryService, innerProgress, cancellationToken).ConfigureAwait(false); } if (refreshChildMetadata) { progress.Report(ProgressHelpers.ScannedSubfolders); if (recursive) { ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders); } var container = this as IMetadataContainer; var folder = this; var innerProgress = new Progress(innerPercent => { var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.RefreshedMetadata, innerPercent); progress.Report(percent); if (recursive) { ProviderManager.OnRefreshProgress(folder, percent); } }); if (container is not null) { await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false); } else { if (validChildrenNeedGeneration) { validChildren = Children.ToList(); } await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken).ConfigureAwait(false); } } } private Task RefreshMetadataRecursive(IList children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) { return RunTasks( (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFolder, innerProgress, cancellationToken), children, progress, cancellationToken); } private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress progress, CancellationToken cancellationToken) { if (container is Series series) { await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); } await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); } private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) { if (child is IMetadataContainer container) { await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAwait(false); } else { if (refreshOptions.RefreshItem(child)) { await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); } if (recursive && child is Folder folder) { await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false); } } } /// /// Refreshes the children. /// /// The children. /// The directory service. /// The progress. /// The cancellation token. /// Task. private Task ValidateSubFolders(IList children, IDirectoryService directoryService, IProgress progress, CancellationToken cancellationToken) { return RunTasks( (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, directoryService, cancellationToken), children, progress, cancellationToken); } /// /// Runs an action block on a list of children. /// /// The task to run for each child. /// The list of children. /// The progress. /// The cancellation token. /// Task. private async Task RunTasks(Func, Task> task, IList children, IProgress progress, CancellationToken cancellationToken) { var childrenCount = children.Count; var childrenProgress = new double[childrenCount]; void UpdateProgress() { progress.Report(childrenProgress.Average()); } var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount; var actionBlock = new ActionBlock( async i => { var innerProgress = new Progress(innerPercent => { // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls var innerPercentRounded = Math.Round(innerPercent); if (childrenProgress[i] != innerPercentRounded) { childrenProgress[i] = innerPercentRounded; UpdateProgress(); } }); await task(children[i], innerProgress).ConfigureAwait(false); childrenProgress[i] = 100; UpdateProgress(); }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = parallelism, CancellationToken = cancellationToken, }); for (var i = 0; i < childrenCount; i++) { await actionBlock.SendAsync(i, cancellationToken).ConfigureAwait(false); } actionBlock.Complete(); await actionBlock.Completion.ConfigureAwait(false); } /// /// Get the children of this folder from the actual file system. /// /// IEnumerable{BaseItem}. /// The directory service to use for operation. /// Returns set of base items. protected virtual IEnumerable GetNonCachedChildren(IDirectoryService directoryService) { var collectionType = LibraryManager.GetContentType(this); var libraryOptions = LibraryManager.GetLibraryOptions(this); return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryOptions, collectionType); } /// /// Get our children from the repo - stubbed for now. /// /// IEnumerable{BaseItem}. protected List GetCachedChildren() { return ItemRepository.GetItemList(new InternalItemsQuery { Parent = this, GroupByPresentationUniqueKey = false, DtoOptions = new DtoOptions(true) }); } public virtual int GetChildCount(User user) { if (LinkedChildren.Length > 0) { if (this is not ICollectionFolder) { return GetChildren(user, true).Count; } } var result = GetItems(new InternalItemsQuery(user) { Recursive = false, Limit = 0, Parent = this, DtoOptions = new DtoOptions(false) { EnableImages = false } }); return result.TotalRecordCount; } public virtual int GetRecursiveChildCount(User user) { return GetItems(new InternalItemsQuery(user) { Recursive = true, IsFolder = false, IsVirtualItem = false, EnableTotalRecordCount = true, Limit = 0, DtoOptions = new DtoOptions(false) { EnableImages = false } }).TotalRecordCount; } public QueryResult QueryRecursive(InternalItemsQuery query) { var user = query.User; if (!query.ForceDirect && RequiresPostFiltering(query)) { IEnumerable items; Func filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); if (query.User is null) { items = GetRecursiveChildren(filter); } else { items = GetRecursiveChildren(user, query); } return PostFilterAndSort(items, query, true); } if (this is not UserRootFolder && this is not AggregateFolder && query.ParentId.IsEmpty()) { query.Parent = this; } if (RequiresPostFiltering2(query)) { return QueryWithPostFiltering2(query); } return LibraryManager.GetItemsResult(query); } protected QueryResult QueryWithPostFiltering2(InternalItemsQuery query) { var startIndex = query.StartIndex; var limit = query.Limit; query.StartIndex = null; query.Limit = null; IEnumerable itemsList = LibraryManager.GetItemList(query); var user = query.User; if (user is not null) { // needed for boxsets itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)); } IEnumerable returnItems; int totalCount = 0; if (query.EnableTotalRecordCount) { var itemArray = itemsList.ToArray(); totalCount = itemArray.Length; returnItems = itemArray; } else { returnItems = itemsList; } if (limit.HasValue) { returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value); } else if (startIndex.HasValue) { returnItems = returnItems.Skip(startIndex.Value); } return new QueryResult( query.StartIndex, totalCount, returnItems.ToArray()); } private bool RequiresPostFiltering2(InternalItemsQuery query) { if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet) { Logger.LogDebug("Query requires post-filtering due to BoxSet query"); return true; } return false; } private bool RequiresPostFiltering(InternalItemsQuery query) { if (LinkedChildren.Length > 0) { if (this is not ICollectionFolder) { Logger.LogDebug("{Type}: Query requires post-filtering due to LinkedChildren.", GetType().Name); return true; } } // Filter by Video3DFormat if (query.Is3D.HasValue) { Logger.LogDebug("Query requires post-filtering due to Is3D"); return true; } if (query.HasOfficialRating.HasValue) { Logger.LogDebug("Query requires post-filtering due to HasOfficialRating"); return true; } if (query.IsPlaceHolder.HasValue) { Logger.LogDebug("Query requires post-filtering due to IsPlaceHolder"); return true; } if (query.HasSpecialFeature.HasValue) { Logger.LogDebug("Query requires post-filtering due to HasSpecialFeature"); return true; } if (query.HasSubtitles.HasValue) { Logger.LogDebug("Query requires post-filtering due to HasSubtitles"); return true; } if (query.HasTrailer.HasValue) { Logger.LogDebug("Query requires post-filtering due to HasTrailer"); return true; } if (query.HasThemeSong.HasValue) { Logger.LogDebug("Query requires post-filtering due to HasThemeSong"); return true; } if (query.HasThemeVideo.HasValue) { Logger.LogDebug("Query requires post-filtering due to HasThemeVideo"); return true; } // Filter by VideoType if (query.VideoTypes.Length > 0) { Logger.LogDebug("Query requires post-filtering due to VideoTypes"); return true; } if (CollapseBoxSetItems(query, this, query.User, ConfigurationManager)) { Logger.LogDebug("Query requires post-filtering due to CollapseBoxSetItems"); return true; } if (!query.AdjacentTo.IsNullOrEmpty()) { Logger.LogDebug("Query requires post-filtering due to AdjacentTo"); return true; } if (query.SeriesStatuses.Length > 0) { Logger.LogDebug("Query requires post-filtering due to SeriesStatuses"); return true; } if (query.AiredDuringSeason.HasValue) { Logger.LogDebug("Query requires post-filtering due to AiredDuringSeason"); return true; } if (query.IsPlayed.HasValue) { if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(BaseItemKind.Series)) { Logger.LogDebug("Query requires post-filtering due to IsPlayed"); return true; } } return false; } private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList items) { return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray(); } public QueryResult GetItems(InternalItemsQuery query) { if (query.ItemIds.Length > 0) { var result = LibraryManager.GetItemsResult(query); if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) { result.Items = SortItemsByRequest(query, result.Items); } return result; } return GetItemsInternal(query); } public IReadOnlyList GetItemList(InternalItemsQuery query) { query.EnableTotalRecordCount = false; if (query.ItemIds.Length > 0) { var result = LibraryManager.GetItemList(query); if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) { return SortItemsByRequest(query, result); } return result; } return GetItemsInternal(query).Items; } protected virtual QueryResult GetItemsInternal(InternalItemsQuery query) { if (SourceType == SourceType.Channel) { try { query.Parent = this; query.ChannelIds = new[] { ChannelId }; // Don't blow up here because it could cause parent screens with other content to fail return ChannelManager.GetChannelItemsInternal(query, new Progress(), CancellationToken.None).GetAwaiter().GetResult(); } catch { // Already logged at lower levels return new QueryResult(); } } if (query.Recursive) { return QueryRecursive(query); } var user = query.User; Func filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager); IEnumerable items; if (query.User is null) { items = Children.Where(filter); } else { // need to pass this param to the children. var childQuery = new InternalItemsQuery { DisplayAlbumFolders = query.DisplayAlbumFolders }; items = GetChildren(user, true, childQuery).Where(filter); } return PostFilterAndSort(items, query, true); } protected QueryResult PostFilterAndSort(IEnumerable items, InternalItemsQuery query, bool enableSorting) { var user = query.User; // Check recursive - don't substitute in plain folder views if (user is not null) { items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager); } #pragma warning disable CA1309 if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater)) { items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1); } if (!string.IsNullOrEmpty(query.NameStartsWith)) { items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase)); } if (!string.IsNullOrEmpty(query.NameLessThan)) { items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1); } #pragma warning restore CA1309 // This must be the last filter if (!query.AdjacentTo.IsNullOrEmpty()) { items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting); } private static IEnumerable CollapseBoxSetItemsIfNeeded( IEnumerable items, InternalItemsQuery query, BaseItem queryParent, User user, IServerConfigurationManager configurationManager, ICollectionManager collectionManager) { ArgumentNullException.ThrowIfNull(items); if (CollapseBoxSetItems(query, queryParent, user, configurationManager)) { items = collectionManager.CollapseItemsWithinBoxSets(items, user); } return items; } private static bool CollapseBoxSetItems( InternalItemsQuery query, BaseItem queryParent, User user, IServerConfigurationManager configurationManager) { // Could end up stuck in a loop like this if (queryParent is BoxSet) { return false; } if (queryParent is Series) { return false; } if (queryParent is Season) { return false; } if (queryParent is MusicAlbum) { return false; } if (queryParent is MusicArtist) { return false; } var param = query.CollapseBoxSetItems; if (!param.HasValue) { if (user is not null && !configurationManager.Configuration.EnableGroupingIntoCollections) { return false; } if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie)) { param = true; } } return param.HasValue && param.Value && AllowBoxSetCollapsing(query); } private static bool AllowBoxSetCollapsing(InternalItemsQuery request) { if (request.IsFavorite.HasValue) { return false; } if (request.IsFavoriteOrLiked.HasValue) { return false; } if (request.IsLiked.HasValue) { return false; } if (request.IsPlayed.HasValue) { return false; } if (request.IsResumable.HasValue) { return false; } if (request.IsFolder.HasValue) { return false; } if (request.Genres.Count > 0) { return false; } if (request.GenreIds.Count > 0) { return false; } if (request.HasImdbId.HasValue) { return false; } if (request.HasOfficialRating.HasValue) { return false; } if (request.HasOverview.HasValue) { return false; } if (request.HasParentalRating.HasValue) { return false; } if (request.HasSpecialFeature.HasValue) { return false; } if (request.HasSubtitles.HasValue) { return false; } if (request.HasThemeSong.HasValue) { return false; } if (request.HasThemeVideo.HasValue) { return false; } if (request.HasTmdbId.HasValue) { return false; } if (request.HasTrailer.HasValue) { return false; } if (request.ImageTypes.Length > 0) { return false; } if (request.Is3D.HasValue) { return false; } if (request.IsHD.HasValue) { return false; } if (request.IsLocked.HasValue) { return false; } if (request.IsPlaceHolder.HasValue) { return false; } if (request.IsPlayed.HasValue) { return false; } if (!string.IsNullOrWhiteSpace(request.Person)) { return false; } if (request.PersonIds.Length > 0) { return false; } if (request.ItemIds.Length > 0) { return false; } if (request.StudioIds.Length > 0) { return false; } if (request.GenreIds.Count > 0) { return false; } if (request.VideoTypes.Length > 0) { return false; } if (request.Years.Length > 0) { return false; } if (request.Tags.Length > 0) { return false; } if (request.OfficialRatings.Length > 0) { return false; } if (request.MinCommunityRating.HasValue) { return false; } if (request.MinCriticRating.HasValue) { return false; } if (request.MinIndexNumber.HasValue) { return false; } return true; } public List GetChildren(User user, bool includeLinkedChildren) { ArgumentNullException.ThrowIfNull(user); return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user)); } public virtual List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(user); // the true root should return our users root folder children if (IsPhysicalRoot) { return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); } var result = new Dictionary(); AddChildren(user, includeLinkedChildren, result, false, query); return result.Values.ToList(); } protected virtual IEnumerable GetEligibleChildrenForRecursiveChildren(User user) { return Children; } /// /// Adds the children to list. /// private void AddChildren(User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query, HashSet visitedFolders = null) { // Prevent infinite recursion of nested folders visitedFolders ??= new HashSet(); if (!visitedFolders.Add(this)) { return; } // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums. IEnumerable children = null; if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum)) { children = Children; query = null; } // If there are not sub-folders, proceed as normal. if (children is null) { children = GetEligibleChildrenForRecursiveChildren(user); } AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders); if (includeLinkedChildren) { AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query, visitedFolders); } } private void AddChildrenFromCollection(IEnumerable children, User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query, HashSet visitedFolders) { foreach (var child in children) { if (!child.IsVisible(user)) { continue; } if (query is null || UserViewBuilder.FilterItem(child, query)) { result[child.Id] = child; } if (recursive && child.IsFolder) { var folder = (Folder)child; folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders); } } } public virtual IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(user); var result = new Dictionary(); AddChildren(user, true, result, true, query); return result.Values; } /// /// Gets the recursive children. /// /// IList{BaseItem}. public IList GetRecursiveChildren() { return GetRecursiveChildren(true); } public IList GetRecursiveChildren(bool includeLinkedChildren) { return GetRecursiveChildren(i => true, includeLinkedChildren); } public IList GetRecursiveChildren(Func filter) { return GetRecursiveChildren(filter, true); } public IList GetRecursiveChildren(Func filter, bool includeLinkedChildren) { var result = new Dictionary(); AddChildrenToList(result, includeLinkedChildren, true, filter); return result.Values.ToList(); } /// /// Adds the children to list. /// private void AddChildrenToList(Dictionary result, bool includeLinkedChildren, bool recursive, Func filter) { foreach (var child in Children) { if (filter is null || filter(child)) { result[child.Id] = child; } if (recursive && child.IsFolder) { var folder = (Folder)child; // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loop of linked items folder.AddChildrenToList(result, false, true, filter); } } if (includeLinkedChildren) { foreach (var child in GetLinkedChildren()) { if (filter is null || filter(child)) { result[child.Id] = child; } } } } /// /// Gets the linked children. /// /// IEnumerable{BaseItem}. public List GetLinkedChildren() { var linkedChildren = LinkedChildren; var list = new List(linkedChildren.Length); foreach (var i in linkedChildren) { var child = GetLinkedChild(i); if (child is not null) { list.Add(child); } } return list; } public bool ContainsLinkedChildByItemId(Guid itemId) { var linkedChildren = LinkedChildren; foreach (var i in linkedChildren) { if (i.ItemId.HasValue) { if (i.ItemId.Value.Equals(itemId)) { return true; } continue; } var child = GetLinkedChild(i); if (child is not null && child.Id.Equals(itemId)) { return true; } } return false; } public List GetLinkedChildren(User user) { if (!FilterLinkedChildrenPerUser || user is null) { return GetLinkedChildren(); } var linkedChildren = LinkedChildren; var list = new List(linkedChildren.Length); if (linkedChildren.Length == 0) { return list; } var allUserRootChildren = LibraryManager.GetUserRootFolder() .GetChildren(user, true) .OfType() .ToList(); var collectionFolderIds = allUserRootChildren .Select(i => i.Id) .ToList(); foreach (var i in linkedChildren) { var child = GetLinkedChild(i); if (child is null) { continue; } var childOwner = child.GetOwner() ?? child; if (child is not IItemByName) { var childProtocol = childOwner.PathProtocol; if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File) { if (!childOwner.IsVisibleStandalone(user)) { continue; } } else { var itemCollectionFolderIds = LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id); if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains)) { continue; } } } list.Add(child); } return list; } /// /// Gets the linked children. /// /// IEnumerable{BaseItem}. public IEnumerable> GetLinkedChildrenInfos() { return LinkedChildren .Select(i => new Tuple(i, GetLinkedChild(i))) .Where(i => i.Item2 is not null); } protected override async Task RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) { var changesFound = false; if (IsFileProtocol) { if (RefreshLinkedChildren(fileSystemChildren)) { changesFound = true; } } var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); return baseHasChanges || changesFound; } /// /// Refreshes the linked children. /// /// The enumerable of file system metadata. /// true if the linked children were updated, false otherwise. protected virtual bool RefreshLinkedChildren(IEnumerable fileSystemChildren) { if (SupportsShortcutChildren) { var newShortcutLinks = fileSystemChildren .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName)) .Select(i => { try { Logger.LogDebug("Found shortcut at {0}", i.FullName); var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShortcut(i.FullName)); if (!string.IsNullOrEmpty(resolvedPath)) { return new LinkedChild { Path = resolvedPath, Type = LinkedChildType.Shortcut }; } Logger.LogError("Error resolving shortcut {0}", i.FullName); return null; } catch (IOException ex) { Logger.LogError(ex, "Error resolving shortcut {0}", i.FullName); return null; } }) .Where(i => i is not null) .ToList(); var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList(); if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem))) { Logger.LogInformation("Shortcut links have changed for {0}", Path); newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual)); LinkedChildren = newShortcutLinks.ToArray(); return true; } } foreach (var child in LinkedChildren) { // Reset the cached value child.ItemId = null; } return false; } /// /// Marks the played. /// /// The user. /// The date played. /// if set to true [reset position]. public override void MarkPlayed( User user, DateTime? datePlayed, bool resetPosition) { var query = new InternalItemsQuery { User = user, Recursive = true, IsFolder = false, EnableTotalRecordCount = false }; if (!user.DisplayMissingEpisodes) { query.IsVirtualItem = false; } var itemsResult = GetItemList(query); // Sweep through recursively and update status foreach (var item in itemsResult) { if (item.IsVirtualItem) { // The querying doesn't support virtual unaired var episode = item as Episode; if (episode is not null && episode.IsUnaired) { continue; } } item.MarkPlayed(user, datePlayed, resetPosition); } } /// /// Marks the unplayed. /// /// The user. public override void MarkUnplayed(User user) { var itemsResult = GetItemList(new InternalItemsQuery { User = user, Recursive = true, IsFolder = false, EnableTotalRecordCount = false }); // Sweep through recursively and update status foreach (var item in itemsResult) { item.MarkUnplayed(user); } } public override bool IsPlayed(User user) { var itemsResult = GetItemList(new InternalItemsQuery(user) { Recursive = true, IsFolder = false, IsVirtualItem = false, EnableTotalRecordCount = false }); return itemsResult .All(i => i.IsPlayed(user)); } public override bool IsUnplayed(User user) { return !IsPlayed(user); } public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) { if (!SupportsUserDataFromChildren) { return; } if (itemDto is not null) { if (fields.ContainsField(ItemFields.RecursiveItemCount)) { itemDto.RecursiveItemCount = GetRecursiveChildCount(user); } } if (SupportsPlayedStatus) { var unplayedQueryResult = GetItems(new InternalItemsQuery(user) { Recursive = true, IsFolder = false, IsVirtualItem = false, EnableTotalRecordCount = true, Limit = 0, IsPlayed = false, DtoOptions = new DtoOptions(false) { EnableImages = false } }).TotalRecordCount; dto.UnplayedItemCount = unplayedQueryResult; if (itemDto?.RecursiveItemCount > 0) { var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100; dto.PlayedPercentage = 100 - unplayedPercentage; dto.Played = dto.PlayedPercentage.Value >= 100; } else { dto.Played = (dto.UnplayedItemCount ?? 0) == 0; } } } /// /// Contains constants used when reporting scan progress. /// private static class ProgressHelpers { /// /// Reported after the folders immediate children are retrieved. /// public const int RetrievedChildren = 5; /// /// Reported after add, updating, or deleting child items from the LibraryManager. /// public const int UpdatedChildItems = 10; /// /// Reported once subfolders are scanned. /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders]. /// public const int ScannedSubfolders = 50; /// /// Reported once metadata is refreshed. /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed]. /// public const int RefreshedMetadata = 100; /// /// Gets the current progress given the previous step, next step, and progress in between. /// /// The previous progress step. /// The next progress step. /// The current progress step. /// The progress. public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress) { return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100)); } } } }