< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.Folder
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/Folder.cs
Line coverage
34%
Covered lines: 194
Uncovered lines: 371
Coverable lines: 565
Total lines: 1801
Line coverage: 34.3%
Branch coverage
29%
Covered branches: 109
Total branches: 364
Branch coverage: 29.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
get_SupportsThemeMedia()100%210%
get_IsPreSorted()100%210%
get_IsPhysicalRoot()100%11100%
get_SupportsInheritedParentImages()100%210%
get_SupportsPlayedStatus()100%210%
get_IsFolder()100%11100%
get_IsDisplayedAsFolder()100%210%
get_SupportsCumulativeRunTimeTicks()100%11100%
get_SupportsDateLastMediaAdded()100%11100%
get_FileNameWithoutExtension()50%2266.66%
get_Children()100%22100%
set_Children(...)100%11100%
get_RecursiveChildren()100%210%
get_SupportsShortcutChildren()100%11100%
get_FilterLinkedChildrenPerUser()100%11100%
get_SupportsOwnedItems()100%22100%
get_SupportsUserDataFromChildren()25%841635.71%
CanDelete()50%2266.66%
RequiresRefresh()50%4475%
AddChild(...)0%4260%
IsVisible(...)50%801222.22%
LoadChildren()100%11100%
GetRefreshProgress()100%210%
ValidateChildren(...)100%210%
ValidateChildren(...)100%11100%
GetActualChildrenDictionary()37.5%10866.66%
IsLibraryFolderAccessible(...)80%101083.33%
GetNonCachedChildren(...)100%11100%
GetCachedChildren()100%11100%
GetChildCount(...)0%2040%
GetRecursiveChildCount(...)100%210%
QueryRecursive(...)71.42%561440%
QueryWithPostFiltering2(...)62.5%9877.27%
RequiresPostFiltering2(...)50%6450%
RequiresPostFiltering(...)50%4203633.33%
SortItemsByRequest(...)100%210%
GetItems(...)0%4260%
GetItemList(...)16.66%13642.85%
GetItemsInternal(...)33.33%31611.11%
PostFilterAndSort(...)0%110100%
CollapseBoxSetItemsIfNeeded(...)0%620%
CollapseBoxSetItems(...)40.9%1332238.88%
AllowBoxSetCollapsing(...)0%4692680%
GetChildren(...)75%4487.5%
GetChildren(...)100%11100%
GetEligibleChildrenForRecursiveChildren(...)100%11100%
AddChildren(...)71.42%171475%
AddChildrenFromCollection(...)66.66%131280.95%
GetRecursiveChildren(...)100%210%
GetRecursiveChildren()100%210%
GetRecursiveChildren(...)100%11100%
GetRecursiveChildren(...)100%210%
GetRecursiveChildren(...)100%11100%
AddChildrenToList(...)22.22%1431827.27%
GetLinkedChildren()25%5457.14%
ContainsLinkedChildByItemId(...)0%110100%
GetLinkedChildren(...)9.09%406227.4%
GetLinkedChildrenInfos()100%210%
RefreshLinkedChildren(...)66.66%6687.8%
MarkPlayed(...)0%110100%
MarkUnplayed(...)0%620%
IsPlayed(...)100%210%
IsUnplayed(...)100%210%
FillUserDataDtoValues(...)8.33%124128%
GetProgress(...)100%11100%

File(s)

/srv/git/jellyfin/MediaBrowser.Controller/Entities/Folder.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CA1002, CA1721, CA1819, CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Collections.Immutable;
 8using System.IO;
 9using System.Linq;
 10using System.Security;
 11using System.Text.Json.Serialization;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using J2N.Collections.Generic.Extensions;
 15using Jellyfin.Data;
 16using Jellyfin.Data.Enums;
 17using Jellyfin.Database.Implementations.Entities;
 18using Jellyfin.Database.Implementations.Enums;
 19using Jellyfin.Extensions;
 20using MediaBrowser.Controller.Channels;
 21using MediaBrowser.Controller.Collections;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Dto;
 24using MediaBrowser.Controller.Entities.Audio;
 25using MediaBrowser.Controller.Entities.Movies;
 26using MediaBrowser.Controller.Library;
 27using MediaBrowser.Controller.LibraryTaskScheduler;
 28using MediaBrowser.Controller.Providers;
 29using MediaBrowser.Model.Dto;
 30using MediaBrowser.Model.IO;
 31using MediaBrowser.Model.Querying;
 32using Microsoft.Extensions.Logging;
 33using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 34using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 35using Season = MediaBrowser.Controller.Entities.TV.Season;
 36using Series = MediaBrowser.Controller.Entities.TV.Series;
 37
 38namespace MediaBrowser.Controller.Entities
 39{
 40    /// <summary>
 41    /// Class Folder.
 42    /// </summary>
 43    public class Folder : BaseItem
 44    {
 45        private IEnumerable<BaseItem> _children;
 46
 36347        public Folder()
 48        {
 36349            LinkedChildren = Array.Empty<LinkedChild>();
 36350        }
 51
 52        public static IUserViewManager UserViewManager { get; set; }
 53
 54        public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get; set; }
 55
 56        /// <summary>
 57        /// Gets or sets a value indicating whether this instance is root.
 58        /// </summary>
 59        /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
 60        public bool IsRoot { get; set; }
 61
 62        public LinkedChild[] LinkedChildren { get; set; }
 63
 64        [JsonIgnore]
 65        public DateTime? DateLastMediaAdded { get; set; }
 66
 67        [JsonIgnore]
 068        public override bool SupportsThemeMedia => true;
 69
 70        [JsonIgnore]
 071        public virtual bool IsPreSorted => false;
 72
 73        [JsonIgnore]
 1074        public virtual bool IsPhysicalRoot => false;
 75
 76        [JsonIgnore]
 077        public override bool SupportsInheritedParentImages => true;
 78
 79        [JsonIgnore]
 080        public override bool SupportsPlayedStatus => true;
 81
 82        /// <summary>
 83        /// Gets a value indicating whether this instance is folder.
 84        /// </summary>
 85        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 86        [JsonIgnore]
 74087        public override bool IsFolder => true;
 88
 89        [JsonIgnore]
 090        public override bool IsDisplayedAsFolder => true;
 91
 92        [JsonIgnore]
 8793        public virtual bool SupportsCumulativeRunTimeTicks => false;
 94
 95        [JsonIgnore]
 3496        public virtual bool SupportsDateLastMediaAdded => false;
 97
 98        [JsonIgnore]
 99        public override string FileNameWithoutExtension
 100        {
 101            get
 102            {
 276103                if (IsFileProtocol)
 104                {
 276105                    return System.IO.Path.GetFileName(Path);
 106                }
 107
 0108                return null;
 109            }
 110        }
 111
 112        /// <summary>
 113        /// Gets or Sets the actual children.
 114        /// </summary>
 115        /// <value>The actual children.</value>
 116        [JsonIgnore]
 117        public virtual IEnumerable<BaseItem> Children
 118        {
 934119            get => _children ??= LoadChildren();
 190120            set => _children = value;
 121        }
 122
 123        /// <summary>
 124        /// Gets thread-safe access to all recursive children of this folder - without regard to user.
 125        /// </summary>
 126        /// <value>The recursive children.</value>
 127        [JsonIgnore]
 0128        public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
 129
 130        [JsonIgnore]
 23131        protected virtual bool SupportsShortcutChildren => false;
 132
 10133        protected virtual bool FilterLinkedChildrenPerUser => false;
 134
 135        [JsonIgnore]
 83136        protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
 137
 138        [JsonIgnore]
 139        public virtual bool SupportsUserDataFromChildren
 140        {
 141            get
 142            {
 143                // These are just far too slow.
 9144                if (this is ICollectionFolder)
 145                {
 3146                    return false;
 147                }
 148
 6149                if (this is UserView)
 150                {
 0151                    return false;
 152                }
 153
 6154                if (this is UserRootFolder)
 155                {
 6156                    return false;
 157                }
 158
 0159                if (this is Channel)
 160                {
 0161                    return false;
 162                }
 163
 0164                if (SourceType != SourceType.Library)
 165                {
 0166                    return false;
 167                }
 168
 0169                if (this is IItemByName)
 170                {
 0171                    if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName)
 172                    {
 0173                        return false;
 174                    }
 175                }
 176
 0177                return true;
 178            }
 179        }
 180
 181        public static ICollectionManager CollectionManager { get; set; }
 182
 183        public override bool CanDelete()
 184        {
 6185            if (IsRoot)
 186            {
 6187                return false;
 188            }
 189
 0190            return base.CanDelete();
 191        }
 192
 193        public override bool RequiresRefresh()
 194        {
 53195            var baseResult = base.RequiresRefresh();
 196
 53197            if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue)
 198            {
 0199                baseResult = true;
 200            }
 201
 53202            return baseResult;
 203        }
 204
 205        /// <summary>
 206        /// Adds the child.
 207        /// </summary>
 208        /// <param name="item">The item.</param>
 209        /// <exception cref="InvalidOperationException">Unable to add  + item.Name.</exception>
 210        public void AddChild(BaseItem item)
 211        {
 0212            item.SetParent(this);
 213
 0214            if (item.Id.IsEmpty())
 215            {
 0216                item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
 217            }
 218
 0219            if (item.DateCreated == DateTime.MinValue)
 220            {
 0221                item.DateCreated = DateTime.UtcNow;
 222            }
 223
 0224            if (item.DateModified == DateTime.MinValue)
 225            {
 0226                item.DateModified = DateTime.UtcNow;
 227            }
 228
 0229            LibraryManager.CreateItem(item, this);
 0230        }
 231
 232        public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 233        {
 13234            if (this is ICollectionFolder && this is not BasePluginFolder)
 235            {
 0236                var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 0237                if (blockedMediaFolders.Length > 0)
 238                {
 0239                    if (blockedMediaFolders.Contains(Id))
 240                    {
 0241                        return false;
 242                    }
 243                }
 244                else
 245                {
 0246                    if (!user.HasPermission(PermissionKind.EnableAllFolders)
 0247                        && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id))
 248                    {
 0249                        return false;
 250                    }
 251                }
 252            }
 253
 13254            return base.IsVisible(user, skipAllowedTagsCheck);
 255        }
 256
 257        /// <summary>
 258        /// Loads our children.  Validation will occur externally.
 259        /// We want this synchronous.
 260        /// </summary>
 261        /// <returns>Returns children.</returns>
 262        protected virtual IReadOnlyList<BaseItem> LoadChildren()
 263        {
 264            // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
 265            // just load our children from the repo - the library will be validated and maintained in other processes
 123266            return GetCachedChildren();
 267        }
 268
 269        public override double? GetRefreshProgress()
 270        {
 0271            return ProviderManager.GetRefreshProgress(Id);
 272        }
 273
 274        public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
 275        {
 0276            return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellation
 277        }
 278
 279        /// <summary>
 280        /// Validates that the children of the folder still exist.
 281        /// </summary>
 282        /// <param name="progress">The progress.</param>
 283        /// <param name="metadataRefreshOptions">The metadata refresh options.</param>
 284        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 285        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 286        /// <param name="cancellationToken">The cancellation token.</param>
 287        /// <returns>Task.</returns>
 288        public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool rec
 289        {
 49290            Children = null; // invalidate cached children.
 49291            return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadata
 292        }
 293
 294        private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
 295        {
 43296            var dictionary = new Dictionary<Guid, BaseItem>();
 297
 43298            Children = null; // invalidate cached children.
 43299            var childrenList = Children.ToList();
 300
 100301            foreach (var child in childrenList)
 302            {
 7303                var id = child.Id;
 7304                if (dictionary.ContainsKey(id))
 305                {
 0306                    Logger.LogError(
 0307                        "Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}",
 0308                        Path ?? Name,
 0309                        child.Path ?? child.Name);
 310                }
 311                else
 312                {
 7313                    dictionary[id] = child;
 314                }
 315            }
 316
 43317            return dictionary;
 318        }
 319
 320        /// <summary>
 321        /// Validates the children internal.
 322        /// </summary>
 323        /// <param name="progress">The progress.</param>
 324        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 325        /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
 326        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 327        /// <param name="refreshOptions">The refresh options.</param>
 328        /// <param name="directoryService">The directory service.</param>
 329        /// <param name="cancellationToken">The cancellation token.</param>
 330        /// <returns>Task.</returns>
 331        protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshCh
 332        {
 333            if (recursive)
 334            {
 335                ProviderManager.OnRefreshStart(this);
 336            }
 337
 338            try
 339            {
 340                await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptio
 341            }
 342            finally
 343            {
 344                if (recursive)
 345                {
 346                    ProviderManager.OnRefreshComplete(this);
 347                }
 348            }
 349        }
 350
 351        private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item, bool checkColle
 352        {
 85353            if (!checkCollection && (item is BoxSet || string.Equals(item.FileNameWithoutExtension, "collections", Strin
 354            {
 0355                return true;
 356            }
 357
 358            // For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
 85359            if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
 360            {
 28361                Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.Containi
 28362                return false;
 363            }
 364
 57365            return true;
 366        }
 367
 368        private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetada
 369        {
 370            if (!IsLibraryFolderAccessible(directoryService, this, allowRemoveRoot))
 371            {
 372                return;
 373            }
 374
 375            cancellationToken.ThrowIfCancellationRequested();
 376
 377            var validChildren = new List<BaseItem>();
 378            var validChildrenNeedGeneration = false;
 379
 380            if (IsFileProtocol)
 381            {
 382                IEnumerable<BaseItem> nonCachedChildren = [];
 383
 384                try
 385                {
 386                    nonCachedChildren = GetNonCachedChildren(directoryService);
 387                }
 388                catch (IOException ex)
 389                {
 390                    Logger.LogError(ex, "Error retrieving children from file system");
 391                }
 392                catch (SecurityException ex)
 393                {
 394                    Logger.LogError(ex, "Error retrieving children from file system");
 395                }
 396                catch (Exception ex)
 397                {
 398                    Logger.LogError(ex, "Error retrieving children");
 399                    return;
 400                }
 401
 402                progress.Report(ProgressHelpers.RetrievedChildren);
 403
 404                if (recursive)
 405                {
 406                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren);
 407                }
 408
 409                // Build a dictionary of the current children we have now by Id so we can compare quickly and easily
 410                var currentChildren = GetActualChildrenDictionary();
 411
 412                // Create a list for our validated children
 413                var newItems = new List<BaseItem>();
 414
 415                cancellationToken.ThrowIfCancellationRequested();
 416
 417                foreach (var child in nonCachedChildren)
 418                {
 419                    if (!IsLibraryFolderAccessible(directoryService, child, allowRemoveRoot))
 420                    {
 421                        continue;
 422                    }
 423
 424                    if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild))
 425                    {
 426                        validChildren.Add(currentChild);
 427
 428                        if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
 429                        {
 430                            await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken)
 431                        }
 432                        else
 433                        {
 434                            // metadata is up-to-date; make sure DB has correct images dimensions and hash
 435                            await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false);
 436                        }
 437
 438                        continue;
 439                    }
 440
 441                    // Brand new item - needs to be added
 442                    child.SetParent(this);
 443                    newItems.Add(child);
 444                    validChildren.Add(child);
 445                }
 446
 447                // That's all the new and changed ones - now see if any have been removed and need cleanup
 448                var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
 449                var shouldRemove = !IsRoot || allowRemoveRoot;
 450                // If it's an AggregateFolder, don't remove
 451                if (shouldRemove && itemsRemoved.Count > 0)
 452                {
 453                    foreach (var item in itemsRemoved)
 454                    {
 455                        if (item.IsFileProtocol)
 456                        {
 457                            Logger.LogDebug("Removed item: {Path}", item.Path);
 458
 459                            item.SetParent(null);
 460                            LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, fals
 461                        }
 462                    }
 463                }
 464
 465                if (newItems.Count > 0)
 466                {
 467                    LibraryManager.CreateItems(newItems, this, cancellationToken);
 468                }
 469            }
 470            else
 471            {
 472                validChildrenNeedGeneration = true;
 473            }
 474
 475            progress.Report(ProgressHelpers.UpdatedChildItems);
 476
 477            if (recursive)
 478            {
 479                ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems);
 480            }
 481
 482            cancellationToken.ThrowIfCancellationRequested();
 483
 484            if (recursive)
 485            {
 486                var folder = this;
 487                var innerProgress = new Progress<double>(innerPercent =>
 488                {
 489                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.Scanned
 490
 491                    progress.Report(percent);
 492
 493                    ProviderManager.OnRefreshProgress(folder, percent);
 494                });
 495
 496                if (validChildrenNeedGeneration)
 497                {
 498                    validChildren = Children.ToList();
 499                    validChildrenNeedGeneration = false;
 500                }
 501
 502                await ValidateSubFolders(validChildren.OfType<Folder>().ToList(), directoryService, innerProgress, cance
 503            }
 504
 505            if (refreshChildMetadata)
 506            {
 507                progress.Report(ProgressHelpers.ScannedSubfolders);
 508
 509                if (recursive)
 510                {
 511                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders);
 512                }
 513
 514                var container = this as IMetadataContainer;
 515
 516                var folder = this;
 517                var innerProgress = new Progress<double>(innerPercent =>
 518                {
 519                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.Refresh
 520
 521                    progress.Report(percent);
 522
 523                    if (recursive)
 524                    {
 525                        ProviderManager.OnRefreshProgress(folder, percent);
 526                    }
 527                });
 528
 529                if (container is not null)
 530                {
 531                    await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).Co
 532                }
 533                else
 534                {
 535                    if (validChildrenNeedGeneration)
 536                    {
 537                        Children = null; // invalidate cached children.
 538                        validChildren = Children.ToList();
 539                    }
 540
 541                    await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellation
 542                }
 543            }
 544        }
 545
 546        private async Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, boo
 547        {
 548            await RunTasks(
 549                (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFold
 550                children,
 551                progress,
 552                cancellationToken).ConfigureAwait(false);
 553        }
 554
 555        private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOp
 556        {
 557            if (container is Series series)
 558            {
 559                await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 560            }
 561
 562            await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
 563        }
 564
 565        private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, I
 566        {
 567            if (child is IMetadataContainer container)
 568            {
 569                await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAw
 570            }
 571            else
 572            {
 573                if (refreshOptions.RefreshItem(child))
 574                {
 575                    await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 576                }
 577
 578                if (recursive && child is Folder folder)
 579                {
 580                    folder.Children = null; // invalidate cached children.
 581                    await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions
 582                }
 583            }
 584        }
 585
 586        /// <summary>
 587        /// Refreshes the children.
 588        /// </summary>
 589        /// <param name="children">The children.</param>
 590        /// <param name="directoryService">The directory service.</param>
 591        /// <param name="progress">The progress.</param>
 592        /// <param name="cancellationToken">The cancellation token.</param>
 593        /// <returns>Task.</returns>
 594        private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<doub
 595        {
 596            await RunTasks(
 597                (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, dire
 598                children,
 599                progress,
 600                cancellationToken).ConfigureAwait(false);
 601        }
 602
 603        /// <summary>
 604        /// Runs an action block on a list of children.
 605        /// </summary>
 606        /// <param name="task">The task to run for each child.</param>
 607        /// <param name="children">The list of children.</param>
 608        /// <param name="progress">The progress.</param>
 609        /// <param name="cancellationToken">The cancellation token.</param>
 610        /// <returns>Task.</returns>
 611        private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progr
 612        {
 613            await LimitedConcurrencyLibraryScheduler
 614                .Enqueue(
 615                    children.ToArray(),
 616                    task,
 617                    progress,
 618                    cancellationToken)
 619                .ConfigureAwait(false);
 620        }
 621
 622        /// <summary>
 623        /// Get the children of this folder from the actual file system.
 624        /// </summary>
 625        /// <returns>IEnumerable{BaseItem}.</returns>
 626        /// <param name="directoryService">The directory service to use for operation.</param>
 627        /// <returns>Returns set of base items.</returns>
 628        protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
 629        {
 43630            var collectionType = LibraryManager.GetContentType(this);
 43631            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 632
 43633            return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryO
 634        }
 635
 636        /// <summary>
 637        /// Get our children from the repo - stubbed for now.
 638        /// </summary>
 639        /// <returns>IEnumerable{BaseItem}.</returns>
 640        protected IReadOnlyList<BaseItem> GetCachedChildren()
 641        {
 123642            return ItemRepository.GetItemList(new InternalItemsQuery
 123643            {
 123644                Parent = this,
 123645                GroupByPresentationUniqueKey = false,
 123646                DtoOptions = new DtoOptions(true)
 123647            });
 648        }
 649
 650        public virtual int GetChildCount(User user)
 651        {
 0652            if (LinkedChildren.Length > 0)
 653            {
 0654                if (this is not ICollectionFolder)
 655                {
 0656                    return GetChildren(user, true).Count;
 657                }
 658            }
 659
 0660            var result = GetItems(new InternalItemsQuery(user)
 0661            {
 0662                Recursive = false,
 0663                Limit = 0,
 0664                Parent = this,
 0665                DtoOptions = new DtoOptions(false)
 0666                {
 0667                    EnableImages = false
 0668                }
 0669            });
 670
 0671            return result.TotalRecordCount;
 672        }
 673
 674        public virtual int GetRecursiveChildCount(User user)
 675        {
 0676            return GetItems(new InternalItemsQuery(user)
 0677            {
 0678                Recursive = true,
 0679                IsFolder = false,
 0680                IsVirtualItem = false,
 0681                EnableTotalRecordCount = true,
 0682                Limit = 0,
 0683                DtoOptions = new DtoOptions(false)
 0684                {
 0685                    EnableImages = false
 0686                }
 0687            }).TotalRecordCount;
 688        }
 689
 690        public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
 691        {
 13692            var user = query.User;
 693
 13694            if (!query.ForceDirect && RequiresPostFiltering(query))
 695            {
 696                IEnumerable<BaseItem> items;
 0697                Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManage
 698
 0699                var totalCount = 0;
 0700                if (query.User is null)
 701                {
 0702                    items = GetRecursiveChildren(filter);
 0703                    totalCount = items.Count();
 704                }
 705                else
 706                {
 0707                    items = GetRecursiveChildren(user, query, out totalCount);
 0708                    query.Limit = null;
 0709                    query.StartIndex = null; // override these here as they have already been applied
 710                }
 711
 0712                var result = PostFilterAndSort(items, query);
 0713                result.TotalRecordCount = totalCount;
 0714                return result;
 715            }
 716
 13717            if (this is not UserRootFolder
 13718                && this is not AggregateFolder
 13719                && query.ParentId.IsEmpty())
 720            {
 13721                query.Parent = this;
 722            }
 723
 13724            if (RequiresPostFiltering2(query))
 725            {
 0726                return QueryWithPostFiltering2(query);
 727            }
 728
 13729            return LibraryManager.GetItemsResult(query);
 730        }
 731
 732        protected QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query)
 733        {
 1734            var startIndex = query.StartIndex;
 1735            var limit = query.Limit;
 736
 1737            query.StartIndex = null;
 1738            query.Limit = null;
 739
 1740            IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query);
 1741            var user = query.User;
 742
 1743            if (user is not null)
 744            {
 745                // needed for boxsets
 1746                itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User));
 747            }
 748
 749            IEnumerable<BaseItem> returnItems;
 1750            int totalCount = 0;
 751
 1752            if (query.EnableTotalRecordCount)
 753            {
 0754                var itemArray = itemsList.ToArray();
 0755                totalCount = itemArray.Length;
 0756                returnItems = itemArray;
 757            }
 758            else
 759            {
 1760                returnItems = itemsList;
 761            }
 762
 1763            if (limit.HasValue)
 764            {
 0765                returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value);
 766            }
 1767            else if (startIndex.HasValue)
 768            {
 0769                returnItems = returnItems.Skip(startIndex.Value);
 770            }
 771
 1772            return new QueryResult<BaseItem>(
 1773                query.StartIndex,
 1774                totalCount,
 1775                returnItems.ToArray());
 776        }
 777
 778        private bool RequiresPostFiltering2(InternalItemsQuery query)
 779        {
 13780            if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet)
 781            {
 0782                Logger.LogDebug("Query requires post-filtering due to BoxSet query");
 0783                return true;
 784            }
 785
 13786            return false;
 787        }
 788
 789        private bool RequiresPostFiltering(InternalItemsQuery query)
 790        {
 13791            if (LinkedChildren.Length > 0)
 792            {
 0793                if (this is not ICollectionFolder)
 794                {
 0795                    Logger.LogDebug("{Type}: Query requires post-filtering due to LinkedChildren.", GetType().Name);
 0796                    return true;
 797                }
 798            }
 799
 800            // Filter by Video3DFormat
 13801            if (query.Is3D.HasValue)
 802            {
 0803                Logger.LogDebug("Query requires post-filtering due to Is3D");
 0804                return true;
 805            }
 806
 13807            if (query.HasOfficialRating.HasValue)
 808            {
 0809                Logger.LogDebug("Query requires post-filtering due to HasOfficialRating");
 0810                return true;
 811            }
 812
 13813            if (query.IsPlaceHolder.HasValue)
 814            {
 0815                Logger.LogDebug("Query requires post-filtering due to IsPlaceHolder");
 0816                return true;
 817            }
 818
 13819            if (query.HasSpecialFeature.HasValue)
 820            {
 0821                Logger.LogDebug("Query requires post-filtering due to HasSpecialFeature");
 0822                return true;
 823            }
 824
 13825            if (query.HasSubtitles.HasValue)
 826            {
 0827                Logger.LogDebug("Query requires post-filtering due to HasSubtitles");
 0828                return true;
 829            }
 830
 13831            if (query.HasTrailer.HasValue)
 832            {
 0833                Logger.LogDebug("Query requires post-filtering due to HasTrailer");
 0834                return true;
 835            }
 836
 13837            if (query.HasThemeSong.HasValue)
 838            {
 0839                Logger.LogDebug("Query requires post-filtering due to HasThemeSong");
 0840                return true;
 841            }
 842
 13843            if (query.HasThemeVideo.HasValue)
 844            {
 0845                Logger.LogDebug("Query requires post-filtering due to HasThemeVideo");
 0846                return true;
 847            }
 848
 849            // Filter by VideoType
 13850            if (query.VideoTypes.Length > 0)
 851            {
 0852                Logger.LogDebug("Query requires post-filtering due to VideoTypes");
 0853                return true;
 854            }
 855
 13856            if (CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
 857            {
 0858                Logger.LogDebug("Query requires post-filtering due to CollapseBoxSetItems");
 0859                return true;
 860            }
 861
 13862            if (!query.AdjacentTo.IsNullOrEmpty())
 863            {
 0864                Logger.LogDebug("Query requires post-filtering due to AdjacentTo");
 0865                return true;
 866            }
 867
 13868            if (query.SeriesStatuses.Length > 0)
 869            {
 0870                Logger.LogDebug("Query requires post-filtering due to SeriesStatuses");
 0871                return true;
 872            }
 873
 13874            if (query.AiredDuringSeason.HasValue)
 875            {
 0876                Logger.LogDebug("Query requires post-filtering due to AiredDuringSeason");
 0877                return true;
 878            }
 879
 13880            if (query.IsPlayed.HasValue)
 881            {
 0882                if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(BaseItemKind.Series))
 883                {
 0884                    Logger.LogDebug("Query requires post-filtering due to IsPlayed");
 0885                    return true;
 886                }
 887            }
 888
 13889            return false;
 890        }
 891
 892        private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
 893        {
 0894            return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
 895        }
 896
 897        public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
 898        {
 0899            if (query.ItemIds.Length > 0)
 900            {
 0901                var result = LibraryManager.GetItemsResult(query);
 902
 0903                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 904                {
 0905                    result.Items = SortItemsByRequest(query, result.Items);
 906                }
 907
 0908                return result;
 909            }
 910
 0911            return GetItemsInternal(query);
 912        }
 913
 914        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 915        {
 14916            query.EnableTotalRecordCount = false;
 917
 14918            if (query.ItemIds.Length > 0)
 919            {
 0920                var result = LibraryManager.GetItemList(query);
 921
 0922                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 923                {
 0924                    return SortItemsByRequest(query, result);
 925                }
 926
 0927                return result;
 928            }
 929
 14930            return GetItemsInternal(query).Items;
 931        }
 932
 933        protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
 934        {
 13935            if (SourceType == SourceType.Channel)
 936            {
 937                try
 938                {
 0939                    query.Parent = this;
 0940                    query.ChannelIds = new[] { ChannelId };
 941
 942                    // Don't blow up here because it could cause parent screens with other content to fail
 0943                    return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None)
 944                }
 0945                catch
 946                {
 947                    // Already logged at lower levels
 0948                    return new QueryResult<BaseItem>();
 949                }
 950            }
 951
 13952            if (query.Recursive)
 953            {
 13954                return QueryRecursive(query);
 955            }
 956
 0957            var user = query.User;
 958
 0959            Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
 960
 961            IEnumerable<BaseItem> items;
 962
 0963            int totalItemCount = 0;
 0964            if (query.User is null)
 965            {
 0966                items = Children.Where(filter);
 0967                totalItemCount = items.Count();
 968            }
 969            else
 970            {
 971                // need to pass this param to the children.
 0972                var childQuery = new InternalItemsQuery
 0973                {
 0974                    DisplayAlbumFolders = query.DisplayAlbumFolders,
 0975                    Limit = query.Limit,
 0976                    StartIndex = query.StartIndex
 0977                };
 978
 0979                items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
 980
 0981                query.Limit = null;
 0982                query.StartIndex = null;
 983            }
 984
 0985            var result = PostFilterAndSort(items, query);
 0986            result.TotalRecordCount = totalItemCount;
 0987            return result;
 0988        }
 989
 990        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
 991        {
 0992            var user = query.User;
 993
 994            // Check recursive - don't substitute in plain folder views
 0995            if (user is not null)
 996            {
 0997                items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
 998            }
 999
 1000#pragma warning disable CA1309
 01001            if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
 1002            {
 01003                items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.Inva
 1004            }
 1005
 01006            if (!string.IsNullOrEmpty(query.NameStartsWith))
 1007            {
 01008                items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIg
 1009            }
 1010
 01011            if (!string.IsNullOrEmpty(query.NameLessThan))
 1012            {
 01013                items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultur
 1014            }
 1015#pragma warning restore CA1309
 1016
 1017            // This must be the last filter
 01018            if (!query.AdjacentTo.IsNullOrEmpty())
 1019            {
 01020                items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
 1021            }
 1022
 01023            return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
 1024        }
 1025
 1026        private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
 1027            IEnumerable<BaseItem> items,
 1028            InternalItemsQuery query,
 1029            BaseItem queryParent,
 1030            User user,
 1031            IServerConfigurationManager configurationManager,
 1032            ICollectionManager collectionManager)
 1033        {
 01034            ArgumentNullException.ThrowIfNull(items);
 1035
 01036            if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
 1037            {
 01038                items = collectionManager.CollapseItemsWithinBoxSets(items, user);
 1039            }
 1040
 01041            return items;
 1042        }
 1043
 1044        private static bool CollapseBoxSetItems(
 1045            InternalItemsQuery query,
 1046            BaseItem queryParent,
 1047            User user,
 1048            IServerConfigurationManager configurationManager)
 1049        {
 1050            // Could end up stuck in a loop like this
 131051            if (queryParent is BoxSet)
 1052            {
 01053                return false;
 1054            }
 1055
 131056            if (queryParent is Season)
 1057            {
 01058                return false;
 1059            }
 1060
 131061            if (queryParent is MusicAlbum)
 1062            {
 01063                return false;
 1064            }
 1065
 131066            if (queryParent is MusicArtist)
 1067            {
 01068                return false;
 1069            }
 1070
 131071            var param = query.CollapseBoxSetItems;
 1072
 131073            if (!param.HasValue)
 1074            {
 01075                if (user is not null && query.IncludeItemTypes.Any(type =>
 01076                    (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollectio
 01077                    (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollectio
 1078                {
 01079                    return false;
 1080                }
 1081
 01082                if (query.IncludeItemTypes.Length == 0
 01083                    || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series))
 1084                {
 01085                    param = true;
 1086                }
 1087            }
 1088
 131089            return param.HasValue && param.Value && AllowBoxSetCollapsing(query);
 1090        }
 1091
 1092        private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
 1093        {
 01094            if (request.IsFavorite.HasValue)
 1095            {
 01096                return false;
 1097            }
 1098
 01099            if (request.IsFavoriteOrLiked.HasValue)
 1100            {
 01101                return false;
 1102            }
 1103
 01104            if (request.IsLiked.HasValue)
 1105            {
 01106                return false;
 1107            }
 1108
 01109            if (request.IsPlayed.HasValue)
 1110            {
 01111                return false;
 1112            }
 1113
 01114            if (request.IsResumable.HasValue)
 1115            {
 01116                return false;
 1117            }
 1118
 01119            if (request.IsFolder.HasValue)
 1120            {
 01121                return false;
 1122            }
 1123
 01124            if (request.Genres.Count > 0)
 1125            {
 01126                return false;
 1127            }
 1128
 01129            if (request.GenreIds.Count > 0)
 1130            {
 01131                return false;
 1132            }
 1133
 01134            if (request.HasImdbId.HasValue)
 1135            {
 01136                return false;
 1137            }
 1138
 01139            if (request.HasOfficialRating.HasValue)
 1140            {
 01141                return false;
 1142            }
 1143
 01144            if (request.HasOverview.HasValue)
 1145            {
 01146                return false;
 1147            }
 1148
 01149            if (request.HasParentalRating.HasValue)
 1150            {
 01151                return false;
 1152            }
 1153
 01154            if (request.HasSpecialFeature.HasValue)
 1155            {
 01156                return false;
 1157            }
 1158
 01159            if (request.HasSubtitles.HasValue)
 1160            {
 01161                return false;
 1162            }
 1163
 01164            if (request.HasThemeSong.HasValue)
 1165            {
 01166                return false;
 1167            }
 1168
 01169            if (request.HasThemeVideo.HasValue)
 1170            {
 01171                return false;
 1172            }
 1173
 01174            if (request.HasTmdbId.HasValue)
 1175            {
 01176                return false;
 1177            }
 1178
 01179            if (request.HasTrailer.HasValue)
 1180            {
 01181                return false;
 1182            }
 1183
 01184            if (request.ImageTypes.Length > 0)
 1185            {
 01186                return false;
 1187            }
 1188
 01189            if (request.Is3D.HasValue)
 1190            {
 01191                return false;
 1192            }
 1193
 01194            if (request.Is4K.HasValue)
 1195            {
 01196                return false;
 1197            }
 1198
 01199            if (request.IsHD.HasValue)
 1200            {
 01201                return false;
 1202            }
 1203
 01204            if (request.IsLocked.HasValue)
 1205            {
 01206                return false;
 1207            }
 1208
 01209            if (request.IsPlaceHolder.HasValue)
 1210            {
 01211                return false;
 1212            }
 1213
 01214            if (!string.IsNullOrWhiteSpace(request.Person))
 1215            {
 01216                return false;
 1217            }
 1218
 01219            if (request.PersonIds.Length > 0)
 1220            {
 01221                return false;
 1222            }
 1223
 01224            if (request.ItemIds.Length > 0)
 1225            {
 01226                return false;
 1227            }
 1228
 01229            if (request.StudioIds.Length > 0)
 1230            {
 01231                return false;
 1232            }
 1233
 01234            if (request.VideoTypes.Length > 0)
 1235            {
 01236                return false;
 1237            }
 1238
 01239            if (request.Years.Length > 0)
 1240            {
 01241                return false;
 1242            }
 1243
 01244            if (request.Tags.Length > 0)
 1245            {
 01246                return false;
 1247            }
 1248
 01249            if (request.OfficialRatings.Length > 0)
 1250            {
 01251                return false;
 1252            }
 1253
 01254            if (request.MinIndexNumber.HasValue)
 1255            {
 01256                return false;
 1257            }
 1258
 01259            if (request.OrderBy.Any(o =>
 01260                o.OrderBy == ItemSortBy.CommunityRating ||
 01261                o.OrderBy == ItemSortBy.CriticRating ||
 01262                o.OrderBy == ItemSortBy.Runtime))
 1263            {
 01264                return false;
 1265            }
 1266
 01267            return true;
 1268        }
 1269
 1270        public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount
 1271        {
 101272            ArgumentNullException.ThrowIfNull(user);
 101273            query ??= new InternalItemsQuery();
 101274            query.User = user;
 1275
 1276            // the true root should return our users root folder children
 101277            if (IsPhysicalRoot)
 1278            {
 01279                return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren, out totalItemCount);
 1280            }
 1281
 101282            var result = new Dictionary<Guid, BaseItem>();
 1283
 101284            totalItemCount = AddChildren(user, includeLinkedChildren, result, false, query);
 1285
 101286            return result.Values.ToArray();
 1287        }
 1288
 1289        public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery que
 1290        {
 101291            return GetChildren(user, includeLinkedChildren, out _, query);
 1292        }
 1293
 1294        protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
 1295        {
 101296            return Children;
 1297        }
 1298
 1299        /// <summary>
 1300        /// Adds the children to list.
 1301        /// </summary>
 1302        private int AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive
 1303        {
 1304            // Prevent infinite recursion of nested folders
 101305            visitedFolders ??= new HashSet<Folder>();
 101306            if (!visitedFolders.Add(this))
 1307            {
 01308                return 0;
 1309            }
 1310
 1311            // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in mus
 101312            IEnumerable<BaseItem> children = null;
 101313            if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
 1314            {
 01315                children = Children;
 01316                query = null;
 1317            }
 1318
 1319            // If there are not sub-folders, proceed as normal.
 101320            if (children is null)
 1321            {
 101322                children = GetEligibleChildrenForRecursiveChildren(user);
 1323            }
 1324
 101325            if (includeLinkedChildren)
 1326            {
 101327                children = children.Concat(GetLinkedChildren(user)).ToArray();
 1328            }
 1329
 101330            return AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFol
 1331        }
 1332
 1333        private int AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dic
 1334        {
 101335            query ??= new InternalItemsQuery();
 101336            var limit = query.Limit;
 101337            query.Limit = 100; // this is a bit of a dirty hack thats in favor of specifically the webUI as it does not 
 1338
 101339            var visibileChildren = children
 101340                .Where(e => e.IsVisible(user))
 101341                .ToArray();
 1342
 101343            var realChildren = visibileChildren
 101344                .Where(e => query is null || UserViewBuilder.FilterItem(e, query))
 101345                .ToArray();
 101346            var childCount = realChildren.Count();
 101347            if (result.Count < query.Limit)
 1348            {
 401349                foreach (var child in realChildren
 101350                    .Skip(query.StartIndex ?? 0)
 101351                    .TakeWhile(e => query.Limit >= result.Count))
 1352                {
 101353                    result[child.Id] = child;
 1354                }
 1355            }
 1356
 101357            if (recursive)
 1358            {
 01359                foreach (var child in visibileChildren
 01360                    .Where(e => e.IsFolder)
 01361                    .OfType<Folder>())
 1362                {
 01363                    childCount += child.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
 1364                }
 1365            }
 1366
 101367            return childCount;
 1368        }
 1369
 1370        public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCo
 1371        {
 01372            ArgumentNullException.ThrowIfNull(user);
 1373
 01374            var result = new Dictionary<Guid, BaseItem>();
 1375
 01376            totalCount = AddChildren(user, true, result, true, query);
 1377
 01378            return result.Values.ToArray();
 1379        }
 1380
 1381        /// <summary>
 1382        /// Gets the recursive children.
 1383        /// </summary>
 1384        /// <returns>IList{BaseItem}.</returns>
 1385        public IReadOnlyList<BaseItem> GetRecursiveChildren()
 1386        {
 01387            return GetRecursiveChildren(true);
 1388        }
 1389
 1390        public IReadOnlyList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
 1391        {
 11392            return GetRecursiveChildren(i => true, includeLinkedChildren);
 1393        }
 1394
 1395        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
 1396        {
 01397            return GetRecursiveChildren(filter, true);
 1398        }
 1399
 1400        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
 1401        {
 11402            var result = new Dictionary<Guid, BaseItem>();
 1403
 11404            AddChildrenToList(result, includeLinkedChildren, true, filter);
 1405
 11406            return result.Values.ToArray();
 1407        }
 1408
 1409        /// <summary>
 1410        /// Adds the children to list.
 1411        /// </summary>
 1412        private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Fu
 1413        {
 21414            foreach (var child in Children)
 1415            {
 01416                if (filter is null || filter(child))
 1417                {
 01418                    result[child.Id] = child;
 1419                }
 1420
 01421                if (recursive && child.IsFolder)
 1422                {
 01423                    var folder = (Folder)child;
 1424
 1425                    // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loo
 01426                    folder.AddChildrenToList(result, false, true, filter);
 1427                }
 1428            }
 1429
 11430            if (includeLinkedChildren)
 1431            {
 01432                foreach (var child in GetLinkedChildren())
 1433                {
 01434                    if (filter is null || filter(child))
 1435                    {
 01436                        result[child.Id] = child;
 1437                    }
 1438                }
 1439            }
 11440        }
 1441
 1442        /// <summary>
 1443        /// Gets the linked children.
 1444        /// </summary>
 1445        /// <returns>IEnumerable{BaseItem}.</returns>
 1446        public List<BaseItem> GetLinkedChildren()
 1447        {
 101448            var linkedChildren = LinkedChildren;
 101449            var list = new List<BaseItem>(linkedChildren.Length);
 1450
 201451            foreach (var i in linkedChildren)
 1452            {
 01453                var child = GetLinkedChild(i);
 1454
 01455                if (child is not null)
 1456                {
 01457                    list.Add(child);
 1458                }
 1459            }
 1460
 101461            return list;
 1462        }
 1463
 1464        public bool ContainsLinkedChildByItemId(Guid itemId)
 1465        {
 01466            var linkedChildren = LinkedChildren;
 01467            foreach (var i in linkedChildren)
 1468            {
 01469                if (i.ItemId.HasValue)
 1470                {
 01471                    if (i.ItemId.Value.Equals(itemId))
 1472                    {
 01473                        return true;
 1474                    }
 1475
 1476                    continue;
 1477                }
 1478
 01479                var child = GetLinkedChild(i);
 1480
 01481                if (child is not null && child.Id.Equals(itemId))
 1482                {
 01483                    return true;
 1484                }
 1485            }
 1486
 01487            return false;
 1488        }
 1489
 1490        public List<BaseItem> GetLinkedChildren(User user)
 1491        {
 101492            if (!FilterLinkedChildrenPerUser || user is null)
 1493            {
 101494                return GetLinkedChildren();
 1495            }
 1496
 01497            var linkedChildren = LinkedChildren;
 01498            var list = new List<BaseItem>(linkedChildren.Length);
 1499
 01500            if (linkedChildren.Length == 0)
 1501            {
 01502                return list;
 1503            }
 1504
 01505            var allUserRootChildren = LibraryManager.GetUserRootFolder()
 01506                .GetChildren(user, true)
 01507                .OfType<Folder>()
 01508                .ToList();
 1509
 01510            var collectionFolderIds = allUserRootChildren
 01511                .Select(i => i.Id)
 01512                .ToList();
 1513
 01514            foreach (var i in linkedChildren)
 1515            {
 01516                var child = GetLinkedChild(i);
 1517
 01518                if (child is null)
 1519                {
 1520                    continue;
 1521                }
 1522
 01523                var childOwner = child.GetOwner() ?? child;
 1524
 01525                if (child is not IItemByName)
 1526                {
 01527                    var childProtocol = childOwner.PathProtocol;
 01528                    if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File)
 1529                    {
 01530                        if (!childOwner.IsVisibleStandalone(user))
 1531                        {
 01532                            continue;
 1533                        }
 1534                    }
 1535                    else
 1536                    {
 01537                        var itemCollectionFolderIds =
 01538                            LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id);
 1539
 01540                        if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains))
 1541                        {
 1542                            continue;
 1543                        }
 1544                    }
 1545                }
 1546
 01547                list.Add(child);
 1548            }
 1549
 01550            return list;
 1551        }
 1552
 1553        /// <summary>
 1554        /// Gets the linked children.
 1555        /// </summary>
 1556        /// <returns>IEnumerable{BaseItem}.</returns>
 1557        public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
 1558        {
 01559            return LinkedChildren
 01560                .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i)))
 01561                .Where(i => i.Item2 is not null)
 01562                .ToArray();
 1563        }
 1564
 1565        protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystem
 1566        {
 1567            var changesFound = false;
 1568
 1569            if (IsFileProtocol)
 1570            {
 1571                if (RefreshLinkedChildren(fileSystemChildren))
 1572                {
 1573                    changesFound = true;
 1574                }
 1575            }
 1576
 1577            var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).Configur
 1578
 1579            return baseHasChanges || changesFound;
 1580        }
 1581
 1582        /// <summary>
 1583        /// Refreshes the linked children.
 1584        /// </summary>
 1585        /// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
 1586        /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
 1587        protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
 1588        {
 171589            if (SupportsShortcutChildren)
 1590            {
 171591                var newShortcutLinks = fileSystemChildren
 171592                    .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName))
 171593                    .Select(i =>
 171594                    {
 171595                        try
 171596                        {
 171597                            Logger.LogDebug("Found shortcut at {0}", i.FullName);
 171598
 171599                            var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShor
 171600
 171601                            if (!string.IsNullOrEmpty(resolvedPath))
 171602                            {
 171603                                return new LinkedChild
 171604                                {
 171605                                    Path = resolvedPath,
 171606                                    Type = LinkedChildType.Shortcut
 171607                                };
 171608                            }
 171609
 171610                            Logger.LogError("Error resolving shortcut {0}", i.FullName);
 171611
 171612                            return null;
 171613                        }
 171614                        catch (IOException ex)
 171615                        {
 171616                            Logger.LogError(ex, "Error resolving shortcut {0}", i.FullName);
 171617                            return null;
 171618                        }
 171619                    })
 171620                    .Where(i => i is not null)
 171621                    .ToList();
 1622
 171623                var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
 1624
 171625                if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem)))
 1626                {
 01627                    Logger.LogInformation("Shortcut links have changed for {0}", Path);
 1628
 01629                    newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual));
 01630                    LinkedChildren = newShortcutLinks.ToArray();
 01631                    return true;
 1632                }
 1633            }
 1634
 341635            foreach (var child in LinkedChildren)
 1636            {
 1637                // Reset the cached value
 01638                child.ItemId = null;
 1639            }
 1640
 171641            return false;
 1642        }
 1643
 1644        /// <summary>
 1645        /// Marks the played.
 1646        /// </summary>
 1647        /// <param name="user">The user.</param>
 1648        /// <param name="datePlayed">The date played.</param>
 1649        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1650        public override void MarkPlayed(
 1651            User user,
 1652            DateTime? datePlayed,
 1653            bool resetPosition)
 1654        {
 01655            var query = new InternalItemsQuery
 01656            {
 01657                User = user,
 01658                Recursive = true,
 01659                IsFolder = false,
 01660                EnableTotalRecordCount = false
 01661            };
 1662
 01663            if (!user.DisplayMissingEpisodes)
 1664            {
 01665                query.IsVirtualItem = false;
 1666            }
 1667
 01668            var itemsResult = GetItemList(query);
 1669
 1670            // Sweep through recursively and update status
 01671            foreach (var item in itemsResult)
 1672            {
 01673                if (item.IsVirtualItem)
 1674                {
 1675                    // The querying doesn't support virtual unaired
 01676                    var episode = item as Episode;
 01677                    if (episode is not null && episode.IsUnaired)
 1678                    {
 1679                        continue;
 1680                    }
 1681                }
 1682
 01683                item.MarkPlayed(user, datePlayed, resetPosition);
 1684            }
 01685        }
 1686
 1687        /// <summary>
 1688        /// Marks the unplayed.
 1689        /// </summary>
 1690        /// <param name="user">The user.</param>
 1691        public override void MarkUnplayed(User user)
 1692        {
 01693            var itemsResult = GetItemList(new InternalItemsQuery
 01694            {
 01695                User = user,
 01696                Recursive = true,
 01697                IsFolder = false,
 01698                EnableTotalRecordCount = false
 01699            });
 1700
 1701            // Sweep through recursively and update status
 01702            foreach (var item in itemsResult)
 1703            {
 01704                item.MarkUnplayed(user);
 1705            }
 01706        }
 1707
 1708        public override bool IsPlayed(User user, UserItemData userItemData)
 1709        {
 01710            return ItemRepository.GetIsPlayed(user, Id, true);
 1711        }
 1712
 1713        public override bool IsUnplayed(User user, UserItemData userItemData)
 1714        {
 01715            return !IsPlayed(user, userItemData);
 1716        }
 1717
 1718        public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User
 1719        {
 91720            if (!SupportsUserDataFromChildren)
 1721            {
 91722                return;
 1723            }
 1724
 01725            if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))
 1726            {
 01727                itemDto.RecursiveItemCount = GetRecursiveChildCount(user);
 1728            }
 1729
 01730            if (SupportsPlayedStatus)
 1731            {
 01732                var unplayedQueryResult = GetItems(new InternalItemsQuery(user)
 01733                {
 01734                    Recursive = true,
 01735                    IsFolder = false,
 01736                    IsVirtualItem = false,
 01737                    EnableTotalRecordCount = true,
 01738                    Limit = 0,
 01739                    IsPlayed = false,
 01740                    DtoOptions = new DtoOptions(false)
 01741                    {
 01742                        EnableImages = false
 01743                    }
 01744                }).TotalRecordCount;
 1745
 01746                dto.UnplayedItemCount = unplayedQueryResult;
 1747
 01748                if (itemDto?.RecursiveItemCount > 0)
 1749                {
 01750                    var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100;
 01751                    dto.PlayedPercentage = 100 - unplayedPercentage;
 01752                    dto.Played = dto.PlayedPercentage.Value >= 100;
 1753                }
 1754                else
 1755                {
 01756                    dto.Played = (dto.UnplayedItemCount ?? 0) == 0;
 1757                }
 1758            }
 01759        }
 1760
 1761        /// <summary>
 1762        /// Contains constants used when reporting scan progress.
 1763        /// </summary>
 1764        private static class ProgressHelpers
 1765        {
 1766            /// <summary>
 1767            /// Reported after the folders immediate children are retrieved.
 1768            /// </summary>
 1769            public const int RetrievedChildren = 5;
 1770
 1771            /// <summary>
 1772            /// Reported after add, updating, or deleting child items from the LibraryManager.
 1773            /// </summary>
 1774            public const int UpdatedChildItems = 10;
 1775
 1776            /// <summary>
 1777            /// Reported once subfolders are scanned.
 1778            /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders].
 1779            /// </summary>
 1780            public const int ScannedSubfolders = 50;
 1781
 1782            /// <summary>
 1783            /// Reported once metadata is refreshed.
 1784            /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed].
 1785            /// </summary>
 1786            public const int RefreshedMetadata = 100;
 1787
 1788            /// <summary>
 1789            /// Gets the current progress given the previous step, next step, and progress in between.
 1790            /// </summary>
 1791            /// <param name="previousProgressStep">The previous progress step.</param>
 1792            /// <param name="nextProgressStep">The next progress step.</param>
 1793            /// <param name="currentProgress">The current progress step.</param>
 1794            /// <returns>The progress.</returns>
 1795            public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress)
 1796            {
 561797                return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100));
 1798            }
 1799        }
 1800    }
 1801}

Methods/Properties

.ctor()
get_SupportsThemeMedia()
get_IsPreSorted()
get_IsPhysicalRoot()
get_SupportsInheritedParentImages()
get_SupportsPlayedStatus()
get_IsFolder()
get_IsDisplayedAsFolder()
get_SupportsCumulativeRunTimeTicks()
get_SupportsDateLastMediaAdded()
get_FileNameWithoutExtension()
get_Children()
set_Children(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>)
get_RecursiveChildren()
get_SupportsShortcutChildren()
get_FilterLinkedChildrenPerUser()
get_SupportsOwnedItems()
get_SupportsUserDataFromChildren()
CanDelete()
RequiresRefresh()
AddChild(MediaBrowser.Controller.Entities.BaseItem)
IsVisible(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
LoadChildren()
GetRefreshProgress()
ValidateChildren(System.IProgress`1<System.Double>,System.Threading.CancellationToken)
ValidateChildren(System.IProgress`1<System.Double>,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Boolean,System.Boolean,System.Threading.CancellationToken)
GetActualChildrenDictionary()
IsLibraryFolderAccessible(MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
GetNonCachedChildren(MediaBrowser.Controller.Providers.IDirectoryService)
GetCachedChildren()
GetChildCount(Jellyfin.Database.Implementations.Entities.User)
GetRecursiveChildCount(Jellyfin.Database.Implementations.Entities.User)
QueryRecursive(MediaBrowser.Controller.Entities.InternalItemsQuery)
QueryWithPostFiltering2(MediaBrowser.Controller.Entities.InternalItemsQuery)
RequiresPostFiltering2(MediaBrowser.Controller.Entities.InternalItemsQuery)
RequiresPostFiltering(MediaBrowser.Controller.Entities.InternalItemsQuery)
SortItemsByRequest(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
GetItems(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemsInternal(MediaBrowser.Controller.Entities.InternalItemsQuery)
PostFilterAndSort(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Entities.InternalItemsQuery)
CollapseBoxSetItemsIfNeeded(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Entities.InternalItemsQuery,MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Collections.ICollectionManager)
CollapseBoxSetItems(MediaBrowser.Controller.Entities.InternalItemsQuery,MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Configuration.IServerConfigurationManager)
AllowBoxSetCollapsing(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetChildren(Jellyfin.Database.Implementations.Entities.User,System.Boolean,System.Int32&,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetChildren(Jellyfin.Database.Implementations.Entities.User,System.Boolean,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetEligibleChildrenForRecursiveChildren(Jellyfin.Database.Implementations.Entities.User)
AddChildren(Jellyfin.Database.Implementations.Entities.User,System.Boolean,System.Collections.Generic.Dictionary`2<System.Guid,MediaBrowser.Controller.Entities.BaseItem>,System.Boolean,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.HashSet`1<MediaBrowser.Controller.Entities.Folder>)
AddChildrenFromCollection(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User,System.Boolean,System.Collections.Generic.Dictionary`2<System.Guid,MediaBrowser.Controller.Entities.BaseItem>,System.Boolean,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.HashSet`1<MediaBrowser.Controller.Entities.Folder>)
GetRecursiveChildren(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Int32&)
GetRecursiveChildren()
GetRecursiveChildren(System.Boolean)
GetRecursiveChildren(System.Func`2<MediaBrowser.Controller.Entities.BaseItem,System.Boolean>)
GetRecursiveChildren(System.Func`2<MediaBrowser.Controller.Entities.BaseItem,System.Boolean>,System.Boolean)
AddChildrenToList(System.Collections.Generic.Dictionary`2<System.Guid,MediaBrowser.Controller.Entities.BaseItem>,System.Boolean,System.Boolean,System.Func`2<MediaBrowser.Controller.Entities.BaseItem,System.Boolean>)
GetLinkedChildren()
ContainsLinkedChildByItemId(System.Guid)
GetLinkedChildren(Jellyfin.Database.Implementations.Entities.User)
GetLinkedChildrenInfos()
RefreshLinkedChildren(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>)
MarkPlayed(Jellyfin.Database.Implementations.Entities.User,System.Nullable`1<System.DateTime>,System.Boolean)
MarkUnplayed(Jellyfin.Database.Implementations.Entities.User)
IsPlayed(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.UserItemData)
IsUnplayed(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.UserItemData)
FillUserDataDtoValues(MediaBrowser.Model.Dto.UserItemDataDto,MediaBrowser.Controller.Entities.UserItemData,MediaBrowser.Model.Dto.BaseItemDto,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Dto.DtoOptions)
GetProgress(System.Int32,System.Int32,System.Double)