< 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: 191
Uncovered lines: 361
Coverable lines: 552
Total lines: 1811
Line coverage: 34.6%
Branch coverage
29%
Covered branches: 108
Total branches: 366
Branch coverage: 29.5%
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%2.15266.66%
get_Children()100%11100%
get_RecursiveChildren()100%210%
get_SupportsShortcutChildren()100%11100%
get_FilterLinkedChildrenPerUser()100%11100%
get_SupportsOwnedItems()100%22100%
get_SupportsUserDataFromChildren()25%84.031635.71%
CanDelete()50%2.15266.66%
RequiresRefresh()50%4.25475%
AddChild(...)0%4260%
IsVisible(...)50%79.761222.22%
LoadChildren()100%11100%
GetRefreshProgress()100%210%
ValidateChildren(...)100%210%
ValidateChildren(...)100%11100%
GetActualChildrenDictionary()37.5%11.08863.63%
IsLibraryFolderAccessible(...)80%10.461083.33%
RefreshMetadataRecursive(...)100%11100%
ValidateSubFolders(...)100%11100%
GetNonCachedChildren(...)100%11100%
GetCachedChildren()100%11100%
GetChildCount(...)0%2040%
GetRecursiveChildCount(...)100%210%
QueryRecursive(...)71.42%29.431457.14%
QueryWithPostFiltering2(...)62.5%8.75877.27%
RequiresPostFiltering2(...)50%6450%
RequiresPostFiltering(...)50%420.063633.33%
SortItemsByRequest(...)100%210%
GetItems(...)0%4260%
GetItemList(...)16.66%12.72642.85%
GetItemsInternal(...)33.33%27.51615.78%
PostFilterAndSort(...)0%110100%
CollapseBoxSetItemsIfNeeded(...)0%620%
CollapseBoxSetItems(...)41.66%109.512447.05%
AllowBoxSetCollapsing(...)0%5256720%
GetChildren(...)100%11100%
GetChildren(...)50%2.02283.33%
GetEligibleChildrenForRecursiveChildren(...)100%11100%
AddChildren(...)71.42%16.411476.92%
AddChildrenFromCollection(...)83.33%14.251275%
GetRecursiveChildren(...)100%210%
GetRecursiveChildren()100%210%
GetRecursiveChildren(...)100%11100%
GetRecursiveChildren(...)100%210%
GetRecursiveChildren(...)100%11100%
AddChildrenToList(...)22.22%142.651827.27%
GetLinkedChildren()25%5.26457.14%
ContainsLinkedChildByItemId(...)0%110100%
GetLinkedChildren(...)9.09%406.31227.4%
GetLinkedChildrenInfos()100%210%
RefreshLinkedChildren(...)66.66%6.07687.8%
MarkPlayed(...)0%110100%
MarkUnplayed(...)0%620%
IsPlayed(...)100%210%
IsUnplayed(...)100%210%
FillUserDataDtoValues(...)8.33%124.13128%
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.IO;
 8using System.Linq;
 9using System.Security;
 10using System.Text.Json.Serialization;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using System.Threading.Tasks.Dataflow;
 14using Jellyfin.Data.Entities;
 15using Jellyfin.Data.Enums;
 16using Jellyfin.Extensions;
 17using MediaBrowser.Controller.Channels;
 18using MediaBrowser.Controller.Collections;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.Dto;
 21using MediaBrowser.Controller.Entities.Audio;
 22using MediaBrowser.Controller.Entities.Movies;
 23using MediaBrowser.Controller.Library;
 24using MediaBrowser.Controller.Providers;
 25using MediaBrowser.Model.Dto;
 26using MediaBrowser.Model.IO;
 27using MediaBrowser.Model.Querying;
 28using Microsoft.Extensions.Logging;
 29using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 30using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 31using Season = MediaBrowser.Controller.Entities.TV.Season;
 32using Series = MediaBrowser.Controller.Entities.TV.Series;
 33
 34namespace MediaBrowser.Controller.Entities
 35{
 36    /// <summary>
 37    /// Class Folder.
 38    /// </summary>
 39    public class Folder : BaseItem
 40    {
 37541        public Folder()
 42        {
 37543            LinkedChildren = Array.Empty<LinkedChild>();
 37544        }
 45
 46        public static IUserViewManager UserViewManager { get; set; }
 47
 48        /// <summary>
 49        /// Gets or sets a value indicating whether this instance is root.
 50        /// </summary>
 51        /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
 52        public bool IsRoot { get; set; }
 53
 54        public LinkedChild[] LinkedChildren { get; set; }
 55
 56        [JsonIgnore]
 57        public DateTime? DateLastMediaAdded { get; set; }
 58
 59        [JsonIgnore]
 060        public override bool SupportsThemeMedia => true;
 61
 62        [JsonIgnore]
 063        public virtual bool IsPreSorted => false;
 64
 65        [JsonIgnore]
 1566        public virtual bool IsPhysicalRoot => false;
 67
 68        [JsonIgnore]
 069        public override bool SupportsInheritedParentImages => true;
 70
 71        [JsonIgnore]
 072        public override bool SupportsPlayedStatus => true;
 73
 74        /// <summary>
 75        /// Gets a value indicating whether this instance is folder.
 76        /// </summary>
 77        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 78        [JsonIgnore]
 69579        public override bool IsFolder => true;
 80
 81        [JsonIgnore]
 082        public override bool IsDisplayedAsFolder => true;
 83
 84        [JsonIgnore]
 8285        public virtual bool SupportsCumulativeRunTimeTicks => false;
 86
 87        [JsonIgnore]
 3688        public virtual bool SupportsDateLastMediaAdded => false;
 89
 90        [JsonIgnore]
 91        public override string FileNameWithoutExtension
 92        {
 93            get
 94            {
 25095                if (IsFileProtocol)
 96                {
 25097                    return System.IO.Path.GetFileName(Path);
 98                }
 99
 0100                return null;
 101            }
 102        }
 103
 104        /// <summary>
 105        /// Gets the actual children.
 106        /// </summary>
 107        /// <value>The actual children.</value>
 108        [JsonIgnore]
 905109        public virtual IEnumerable<BaseItem> Children => LoadChildren();
 110
 111        /// <summary>
 112        /// Gets thread-safe access to all recursive children of this folder - without regard to user.
 113        /// </summary>
 114        /// <value>The recursive children.</value>
 115        [JsonIgnore]
 0116        public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
 117
 118        [JsonIgnore]
 21119        protected virtual bool SupportsShortcutChildren => false;
 120
 15121        protected virtual bool FilterLinkedChildrenPerUser => false;
 122
 123        [JsonIgnore]
 71124        protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
 125
 126        [JsonIgnore]
 127        public virtual bool SupportsUserDataFromChildren
 128        {
 129            get
 130            {
 131                // These are just far too slow.
 14132                if (this is ICollectionFolder)
 133                {
 3134                    return false;
 135                }
 136
 11137                if (this is UserView)
 138                {
 0139                    return false;
 140                }
 141
 11142                if (this is UserRootFolder)
 143                {
 11144                    return false;
 145                }
 146
 0147                if (this is Channel)
 148                {
 0149                    return false;
 150                }
 151
 0152                if (SourceType != SourceType.Library)
 153                {
 0154                    return false;
 155                }
 156
 0157                if (this is IItemByName)
 158                {
 0159                    if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName)
 160                    {
 0161                        return false;
 162                    }
 163                }
 164
 0165                return true;
 166            }
 167        }
 168
 169        public static ICollectionManager CollectionManager { get; set; }
 170
 171        public override bool CanDelete()
 172        {
 11173            if (IsRoot)
 174            {
 11175                return false;
 176            }
 177
 0178            return base.CanDelete();
 179        }
 180
 181        public override bool RequiresRefresh()
 182        {
 46183            var baseResult = base.RequiresRefresh();
 184
 46185            if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue)
 186            {
 0187                baseResult = true;
 188            }
 189
 46190            return baseResult;
 191        }
 192
 193        /// <summary>
 194        /// Adds the child.
 195        /// </summary>
 196        /// <param name="item">The item.</param>
 197        /// <exception cref="InvalidOperationException">Unable to add  + item.Name.</exception>
 198        public void AddChild(BaseItem item)
 199        {
 0200            item.SetParent(this);
 201
 0202            if (item.Id.IsEmpty())
 203            {
 0204                item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
 205            }
 206
 0207            if (item.DateCreated == DateTime.MinValue)
 208            {
 0209                item.DateCreated = DateTime.UtcNow;
 210            }
 211
 0212            if (item.DateModified == DateTime.MinValue)
 213            {
 0214                item.DateModified = DateTime.UtcNow;
 215            }
 216
 0217            LibraryManager.CreateItem(item, this);
 0218        }
 219
 220        public override bool IsVisible(User user)
 221        {
 18222            if (this is ICollectionFolder && this is not BasePluginFolder)
 223            {
 0224                var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 0225                if (blockedMediaFolders.Length > 0)
 226                {
 0227                    if (blockedMediaFolders.Contains(Id))
 228                    {
 0229                        return false;
 230                    }
 231                }
 232                else
 233                {
 0234                    if (!user.HasPermission(PermissionKind.EnableAllFolders)
 0235                        && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id))
 236                    {
 0237                        return false;
 238                    }
 239                }
 240            }
 241
 18242            return base.IsVisible(user);
 243        }
 244
 245        /// <summary>
 246        /// Loads our children.  Validation will occur externally.
 247        /// We want this synchronous.
 248        /// </summary>
 249        /// <returns>Returns children.</returns>
 250        protected virtual List<BaseItem> LoadChildren()
 251        {
 252            // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
 253            // just load our children from the repo - the library will be validated and maintained in other processes
 171254            return GetCachedChildren();
 255        }
 256
 257        public override double? GetRefreshProgress()
 258        {
 0259            return ProviderManager.GetRefreshProgress(Id);
 260        }
 261
 262        public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
 263        {
 0264            return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellation
 265        }
 266
 267        /// <summary>
 268        /// Validates that the children of the folder still exist.
 269        /// </summary>
 270        /// <param name="progress">The progress.</param>
 271        /// <param name="metadataRefreshOptions">The metadata refresh options.</param>
 272        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 273        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 274        /// <param name="cancellationToken">The cancellation token.</param>
 275        /// <returns>Task.</returns>
 276        public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool rec
 277        {
 61278            return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadata
 279        }
 280
 281        private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
 282        {
 61283            var dictionary = new Dictionary<Guid, BaseItem>();
 284
 61285            var childrenList = Children.ToList();
 286
 202287            foreach (var child in childrenList)
 288            {
 40289                var id = child.Id;
 40290                if (dictionary.ContainsKey(id))
 291                {
 0292                    Logger.LogError(
 0293                        "Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}",
 0294                        Path ?? Name,
 0295                        child.Path ?? child.Name);
 296                }
 297                else
 298                {
 40299                    dictionary[id] = child;
 300                }
 301            }
 302
 61303            return dictionary;
 304        }
 305
 306        /// <summary>
 307        /// Validates the children internal.
 308        /// </summary>
 309        /// <param name="progress">The progress.</param>
 310        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 311        /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
 312        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 313        /// <param name="refreshOptions">The refresh options.</param>
 314        /// <param name="directoryService">The directory service.</param>
 315        /// <param name="cancellationToken">The cancellation token.</param>
 316        /// <returns>Task.</returns>
 317        protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshCh
 318        {
 319            if (recursive)
 320            {
 321                ProviderManager.OnRefreshStart(this);
 322            }
 323
 324            try
 325            {
 326                await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptio
 327            }
 328            finally
 329            {
 330                if (recursive)
 331                {
 332                    ProviderManager.OnRefreshComplete(this);
 333                }
 334            }
 335        }
 336
 337        private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item, bool checkColle
 338        {
 103339            if (!checkCollection && (item is BoxSet || string.Equals(item.FileNameWithoutExtension, "collections", Strin
 340            {
 0341                return true;
 342            }
 343
 344            // For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
 103345            if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
 346            {
 40347                Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.Containi
 40348                return false;
 349            }
 350
 63351            return true;
 352        }
 353
 354        private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetada
 355        {
 356            if (!IsLibraryFolderAccessible(directoryService, this, allowRemoveRoot))
 357            {
 358                return;
 359            }
 360
 361            cancellationToken.ThrowIfCancellationRequested();
 362
 363            var validChildren = new List<BaseItem>();
 364            var validChildrenNeedGeneration = false;
 365
 366            if (IsFileProtocol)
 367            {
 368                IEnumerable<BaseItem> nonCachedChildren = [];
 369
 370                try
 371                {
 372                    nonCachedChildren = GetNonCachedChildren(directoryService);
 373                }
 374                catch (IOException ex)
 375                {
 376                    Logger.LogError(ex, "Error retrieving children from file system");
 377                }
 378                catch (SecurityException ex)
 379                {
 380                    Logger.LogError(ex, "Error retrieving children from file system");
 381                }
 382                catch (Exception ex)
 383                {
 384                    Logger.LogError(ex, "Error retrieving children");
 385                    return;
 386                }
 387
 388                progress.Report(ProgressHelpers.RetrievedChildren);
 389
 390                if (recursive)
 391                {
 392                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren);
 393                }
 394
 395                // Build a dictionary of the current children we have now by Id so we can compare quickly and easily
 396                var currentChildren = GetActualChildrenDictionary();
 397
 398                // Create a list for our validated children
 399                var newItems = new List<BaseItem>();
 400
 401                cancellationToken.ThrowIfCancellationRequested();
 402
 403                foreach (var child in nonCachedChildren)
 404                {
 405                    if (!IsLibraryFolderAccessible(directoryService, child, allowRemoveRoot))
 406                    {
 407                        continue;
 408                    }
 409
 410                    if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild))
 411                    {
 412                        validChildren.Add(currentChild);
 413
 414                        if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
 415                        {
 416                            await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken)
 417                        }
 418                        else
 419                        {
 420                            // metadata is up-to-date; make sure DB has correct images dimensions and hash
 421                            await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false);
 422                        }
 423
 424                        continue;
 425                    }
 426
 427                    // Brand new item - needs to be added
 428                    child.SetParent(this);
 429                    newItems.Add(child);
 430                    validChildren.Add(child);
 431                }
 432
 433                // That's all the new and changed ones - now see if any have been removed and need cleanup
 434                var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
 435                var shouldRemove = !IsRoot || allowRemoveRoot;
 436                // If it's an AggregateFolder, don't remove
 437                if (shouldRemove && itemsRemoved.Count > 0)
 438                {
 439                    foreach (var item in itemsRemoved)
 440                    {
 441                        if (item.IsFileProtocol)
 442                        {
 443                            Logger.LogDebug("Removed item: {Path}", item.Path);
 444
 445                            item.SetParent(null);
 446                            LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, fals
 447                        }
 448                    }
 449                }
 450
 451                if (newItems.Count > 0)
 452                {
 453                    LibraryManager.CreateItems(newItems, this, cancellationToken);
 454                }
 455            }
 456            else
 457            {
 458                validChildrenNeedGeneration = true;
 459            }
 460
 461            progress.Report(ProgressHelpers.UpdatedChildItems);
 462
 463            if (recursive)
 464            {
 465                ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems);
 466            }
 467
 468            cancellationToken.ThrowIfCancellationRequested();
 469
 470            if (recursive)
 471            {
 472                var folder = this;
 473                var innerProgress = new Progress<double>(innerPercent =>
 474                {
 475                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.Scanned
 476
 477                    progress.Report(percent);
 478
 479                    ProviderManager.OnRefreshProgress(folder, percent);
 480                });
 481
 482                if (validChildrenNeedGeneration)
 483                {
 484                    validChildren = Children.ToList();
 485                    validChildrenNeedGeneration = false;
 486                }
 487
 488                await ValidateSubFolders(validChildren.OfType<Folder>().ToList(), directoryService, innerProgress, cance
 489            }
 490
 491            if (refreshChildMetadata)
 492            {
 493                progress.Report(ProgressHelpers.ScannedSubfolders);
 494
 495                if (recursive)
 496                {
 497                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders);
 498                }
 499
 500                var container = this as IMetadataContainer;
 501
 502                var folder = this;
 503                var innerProgress = new Progress<double>(innerPercent =>
 504                {
 505                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.Refresh
 506
 507                    progress.Report(percent);
 508
 509                    if (recursive)
 510                    {
 511                        ProviderManager.OnRefreshProgress(folder, percent);
 512                    }
 513                });
 514
 515                if (container is not null)
 516                {
 517                    await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).Co
 518                }
 519                else
 520                {
 521                    if (validChildrenNeedGeneration)
 522                    {
 523                        validChildren = Children.ToList();
 524                    }
 525
 526                    await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellation
 527                }
 528            }
 529        }
 530
 531        private Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recu
 532        {
 61533            return RunTasks(
 61534                (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFold
 61535                children,
 61536                progress,
 61537                cancellationToken);
 538        }
 539
 540        private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOp
 541        {
 542            if (container is Series series)
 543            {
 544                await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 545            }
 546
 547            await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
 548        }
 549
 550        private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, I
 551        {
 552            if (child is IMetadataContainer container)
 553            {
 554                await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAw
 555            }
 556            else
 557            {
 558                if (refreshOptions.RefreshItem(child))
 559                {
 560                    await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 561                }
 562
 563                if (recursive && child is Folder folder)
 564                {
 565                    await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, canc
 566                }
 567            }
 568        }
 569
 570        /// <summary>
 571        /// Refreshes the children.
 572        /// </summary>
 573        /// <param name="children">The children.</param>
 574        /// <param name="directoryService">The directory service.</param>
 575        /// <param name="progress">The progress.</param>
 576        /// <param name="cancellationToken">The cancellation token.</param>
 577        /// <returns>Task.</returns>
 578        private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> pr
 579        {
 19580            return RunTasks(
 19581                (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, dire
 19582                children,
 19583                progress,
 19584                cancellationToken);
 585        }
 586
 587        /// <summary>
 588        /// Runs an action block on a list of children.
 589        /// </summary>
 590        /// <param name="task">The task to run for each child.</param>
 591        /// <param name="children">The list of children.</param>
 592        /// <param name="progress">The progress.</param>
 593        /// <param name="cancellationToken">The cancellation token.</param>
 594        /// <returns>Task.</returns>
 595        private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progr
 596        {
 597            var childrenCount = children.Count;
 598            var childrenProgress = new double[childrenCount];
 599
 600            void UpdateProgress()
 601            {
 602                progress.Report(childrenProgress.Average());
 603            }
 604
 605            var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
 606            var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount;
 607
 608            var actionBlock = new ActionBlock<int>(
 609                async i =>
 610                {
 611                    var innerProgress = new Progress<double>(innerPercent =>
 612                    {
 613                        // round the percent and only update progress if it changed to prevent excessive UpdateProgress 
 614                        var innerPercentRounded = Math.Round(innerPercent);
 615                        if (childrenProgress[i] != innerPercentRounded)
 616                        {
 617                            childrenProgress[i] = innerPercentRounded;
 618                            UpdateProgress();
 619                        }
 620                    });
 621
 622                    await task(children[i], innerProgress).ConfigureAwait(false);
 623
 624                    childrenProgress[i] = 100;
 625
 626                    UpdateProgress();
 627                },
 628                new ExecutionDataflowBlockOptions
 629                {
 630                    MaxDegreeOfParallelism = parallelism,
 631                    CancellationToken = cancellationToken,
 632                });
 633
 634            for (var i = 0; i < childrenCount; i++)
 635            {
 636                await actionBlock.SendAsync(i, cancellationToken).ConfigureAwait(false);
 637            }
 638
 639            actionBlock.Complete();
 640
 641            await actionBlock.Completion.ConfigureAwait(false);
 642        }
 643
 644        /// <summary>
 645        /// Get the children of this folder from the actual file system.
 646        /// </summary>
 647        /// <returns>IEnumerable{BaseItem}.</returns>
 648        /// <param name="directoryService">The directory service to use for operation.</param>
 649        /// <returns>Returns set of base items.</returns>
 650        protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
 651        {
 61652            var collectionType = LibraryManager.GetContentType(this);
 61653            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 654
 61655            return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryO
 656        }
 657
 658        /// <summary>
 659        /// Get our children from the repo - stubbed for now.
 660        /// </summary>
 661        /// <returns>IEnumerable{BaseItem}.</returns>
 662        protected List<BaseItem> GetCachedChildren()
 663        {
 171664            return ItemRepository.GetItemList(new InternalItemsQuery
 171665            {
 171666                Parent = this,
 171667                GroupByPresentationUniqueKey = false,
 171668                DtoOptions = new DtoOptions(true)
 171669            });
 670        }
 671
 672        public virtual int GetChildCount(User user)
 673        {
 0674            if (LinkedChildren.Length > 0)
 675            {
 0676                if (this is not ICollectionFolder)
 677                {
 0678                    return GetChildren(user, true).Count;
 679                }
 680            }
 681
 0682            var result = GetItems(new InternalItemsQuery(user)
 0683            {
 0684                Recursive = false,
 0685                Limit = 0,
 0686                Parent = this,
 0687                DtoOptions = new DtoOptions(false)
 0688                {
 0689                    EnableImages = false
 0690                }
 0691            });
 692
 0693            return result.TotalRecordCount;
 694        }
 695
 696        public virtual int GetRecursiveChildCount(User user)
 697        {
 0698            return GetItems(new InternalItemsQuery(user)
 0699            {
 0700                Recursive = true,
 0701                IsFolder = false,
 0702                IsVirtualItem = false,
 0703                EnableTotalRecordCount = true,
 0704                Limit = 0,
 0705                DtoOptions = new DtoOptions(false)
 0706                {
 0707                    EnableImages = false
 0708                }
 0709            }).TotalRecordCount;
 710        }
 711
 712        public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
 713        {
 4714            var user = query.User;
 715
 4716            if (!query.ForceDirect && RequiresPostFiltering(query))
 717            {
 718                IEnumerable<BaseItem> items;
 0719                Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManage
 720
 0721                if (query.User is null)
 722                {
 0723                    items = GetRecursiveChildren(filter);
 724                }
 725                else
 726                {
 0727                    items = GetRecursiveChildren(user, query);
 728                }
 729
 0730                return PostFilterAndSort(items, query, true);
 731            }
 732
 4733            if (this is not UserRootFolder
 4734                && this is not AggregateFolder
 4735                && query.ParentId.IsEmpty())
 736            {
 4737                query.Parent = this;
 738            }
 739
 4740            if (RequiresPostFiltering2(query))
 741            {
 0742                return QueryWithPostFiltering2(query);
 743            }
 744
 4745            return LibraryManager.GetItemsResult(query);
 746        }
 747
 748        protected QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query)
 749        {
 1750            var startIndex = query.StartIndex;
 1751            var limit = query.Limit;
 752
 1753            query.StartIndex = null;
 1754            query.Limit = null;
 755
 1756            IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query);
 1757            var user = query.User;
 758
 1759            if (user is not null)
 760            {
 761                // needed for boxsets
 1762                itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User));
 763            }
 764
 765            IEnumerable<BaseItem> returnItems;
 1766            int totalCount = 0;
 767
 1768            if (query.EnableTotalRecordCount)
 769            {
 0770                var itemArray = itemsList.ToArray();
 0771                totalCount = itemArray.Length;
 0772                returnItems = itemArray;
 773            }
 774            else
 775            {
 1776                returnItems = itemsList;
 777            }
 778
 1779            if (limit.HasValue)
 780            {
 0781                returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value);
 782            }
 1783            else if (startIndex.HasValue)
 784            {
 0785                returnItems = returnItems.Skip(startIndex.Value);
 786            }
 787
 1788            return new QueryResult<BaseItem>(
 1789                query.StartIndex,
 1790                totalCount,
 1791                returnItems.ToArray());
 792        }
 793
 794        private bool RequiresPostFiltering2(InternalItemsQuery query)
 795        {
 4796            if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet)
 797            {
 0798                Logger.LogDebug("Query requires post-filtering due to BoxSet query");
 0799                return true;
 800            }
 801
 4802            return false;
 803        }
 804
 805        private bool RequiresPostFiltering(InternalItemsQuery query)
 806        {
 4807            if (LinkedChildren.Length > 0)
 808            {
 0809                if (this is not ICollectionFolder)
 810                {
 0811                    Logger.LogDebug("{Type}: Query requires post-filtering due to LinkedChildren.", GetType().Name);
 0812                    return true;
 813                }
 814            }
 815
 816            // Filter by Video3DFormat
 4817            if (query.Is3D.HasValue)
 818            {
 0819                Logger.LogDebug("Query requires post-filtering due to Is3D");
 0820                return true;
 821            }
 822
 4823            if (query.HasOfficialRating.HasValue)
 824            {
 0825                Logger.LogDebug("Query requires post-filtering due to HasOfficialRating");
 0826                return true;
 827            }
 828
 4829            if (query.IsPlaceHolder.HasValue)
 830            {
 0831                Logger.LogDebug("Query requires post-filtering due to IsPlaceHolder");
 0832                return true;
 833            }
 834
 4835            if (query.HasSpecialFeature.HasValue)
 836            {
 0837                Logger.LogDebug("Query requires post-filtering due to HasSpecialFeature");
 0838                return true;
 839            }
 840
 4841            if (query.HasSubtitles.HasValue)
 842            {
 0843                Logger.LogDebug("Query requires post-filtering due to HasSubtitles");
 0844                return true;
 845            }
 846
 4847            if (query.HasTrailer.HasValue)
 848            {
 0849                Logger.LogDebug("Query requires post-filtering due to HasTrailer");
 0850                return true;
 851            }
 852
 4853            if (query.HasThemeSong.HasValue)
 854            {
 0855                Logger.LogDebug("Query requires post-filtering due to HasThemeSong");
 0856                return true;
 857            }
 858
 4859            if (query.HasThemeVideo.HasValue)
 860            {
 0861                Logger.LogDebug("Query requires post-filtering due to HasThemeVideo");
 0862                return true;
 863            }
 864
 865            // Filter by VideoType
 4866            if (query.VideoTypes.Length > 0)
 867            {
 0868                Logger.LogDebug("Query requires post-filtering due to VideoTypes");
 0869                return true;
 870            }
 871
 4872            if (CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
 873            {
 0874                Logger.LogDebug("Query requires post-filtering due to CollapseBoxSetItems");
 0875                return true;
 876            }
 877
 4878            if (!query.AdjacentTo.IsNullOrEmpty())
 879            {
 0880                Logger.LogDebug("Query requires post-filtering due to AdjacentTo");
 0881                return true;
 882            }
 883
 4884            if (query.SeriesStatuses.Length > 0)
 885            {
 0886                Logger.LogDebug("Query requires post-filtering due to SeriesStatuses");
 0887                return true;
 888            }
 889
 4890            if (query.AiredDuringSeason.HasValue)
 891            {
 0892                Logger.LogDebug("Query requires post-filtering due to AiredDuringSeason");
 0893                return true;
 894            }
 895
 4896            if (query.IsPlayed.HasValue)
 897            {
 0898                if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(BaseItemKind.Series))
 899                {
 0900                    Logger.LogDebug("Query requires post-filtering due to IsPlayed");
 0901                    return true;
 902                }
 903            }
 904
 4905            return false;
 906        }
 907
 908        private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
 909        {
 0910            return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
 911        }
 912
 913        public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
 914        {
 0915            if (query.ItemIds.Length > 0)
 916            {
 0917                var result = LibraryManager.GetItemsResult(query);
 918
 0919                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 920                {
 0921                    result.Items = SortItemsByRequest(query, result.Items);
 922                }
 923
 0924                return result;
 925            }
 926
 0927            return GetItemsInternal(query);
 928        }
 929
 930        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 931        {
 5932            query.EnableTotalRecordCount = false;
 933
 5934            if (query.ItemIds.Length > 0)
 935            {
 0936                var result = LibraryManager.GetItemList(query);
 937
 0938                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 939                {
 0940                    return SortItemsByRequest(query, result);
 941                }
 942
 0943                return result;
 944            }
 945
 5946            return GetItemsInternal(query).Items;
 947        }
 948
 949        protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
 950        {
 4951            if (SourceType == SourceType.Channel)
 952            {
 953                try
 954                {
 0955                    query.Parent = this;
 0956                    query.ChannelIds = new[] { ChannelId };
 957
 958                    // Don't blow up here because it could cause parent screens with other content to fail
 0959                    return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None)
 960                }
 0961                catch
 962                {
 963                    // Already logged at lower levels
 0964                    return new QueryResult<BaseItem>();
 965                }
 966            }
 967
 4968            if (query.Recursive)
 969            {
 4970                return QueryRecursive(query);
 971            }
 972
 0973            var user = query.User;
 974
 0975            Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
 976
 977            IEnumerable<BaseItem> items;
 978
 0979            if (query.User is null)
 980            {
 0981                items = Children.Where(filter);
 982            }
 983            else
 984            {
 985                // need to pass this param to the children.
 0986                var childQuery = new InternalItemsQuery
 0987                {
 0988                    DisplayAlbumFolders = query.DisplayAlbumFolders
 0989                };
 990
 0991                items = GetChildren(user, true, childQuery).Where(filter);
 992            }
 993
 0994            return PostFilterAndSort(items, query, true);
 0995        }
 996
 997        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool en
 998        {
 0999            var user = query.User;
 1000
 1001            // Check recursive - don't substitute in plain folder views
 01002            if (user is not null)
 1003            {
 01004                items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
 1005            }
 1006
 1007            #pragma warning disable CA1309
 01008            if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
 1009            {
 01010                items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.Inva
 1011            }
 1012
 01013            if (!string.IsNullOrEmpty(query.NameStartsWith))
 1014            {
 01015                items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIg
 1016            }
 1017
 01018            if (!string.IsNullOrEmpty(query.NameLessThan))
 1019            {
 01020                items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultur
 1021            }
 1022            #pragma warning restore CA1309
 1023
 1024            // This must be the last filter
 01025            if (!query.AdjacentTo.IsNullOrEmpty())
 1026            {
 01027                items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
 1028            }
 1029
 01030            return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
 1031        }
 1032
 1033        private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
 1034            IEnumerable<BaseItem> items,
 1035            InternalItemsQuery query,
 1036            BaseItem queryParent,
 1037            User user,
 1038            IServerConfigurationManager configurationManager,
 1039            ICollectionManager collectionManager)
 1040        {
 01041            ArgumentNullException.ThrowIfNull(items);
 1042
 01043            if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
 1044            {
 01045                items = collectionManager.CollapseItemsWithinBoxSets(items, user);
 1046            }
 1047
 01048            return items;
 1049        }
 1050
 1051        private static bool CollapseBoxSetItems(
 1052            InternalItemsQuery query,
 1053            BaseItem queryParent,
 1054            User user,
 1055            IServerConfigurationManager configurationManager)
 1056        {
 1057            // Could end up stuck in a loop like this
 41058            if (queryParent is BoxSet)
 1059            {
 01060                return false;
 1061            }
 1062
 41063            if (queryParent is Series)
 1064            {
 01065                return false;
 1066            }
 1067
 41068            if (queryParent is Season)
 1069            {
 01070                return false;
 1071            }
 1072
 41073            if (queryParent is MusicAlbum)
 1074            {
 01075                return false;
 1076            }
 1077
 41078            if (queryParent is MusicArtist)
 1079            {
 01080                return false;
 1081            }
 1082
 41083            var param = query.CollapseBoxSetItems;
 1084
 41085            if (!param.HasValue)
 1086            {
 01087                if (user is not null && !configurationManager.Configuration.EnableGroupingIntoCollections)
 1088                {
 01089                    return false;
 1090                }
 1091
 01092                if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie))
 1093                {
 01094                    param = true;
 1095                }
 1096            }
 1097
 41098            return param.HasValue && param.Value && AllowBoxSetCollapsing(query);
 1099        }
 1100
 1101        private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
 1102        {
 01103            if (request.IsFavorite.HasValue)
 1104            {
 01105                return false;
 1106            }
 1107
 01108            if (request.IsFavoriteOrLiked.HasValue)
 1109            {
 01110                return false;
 1111            }
 1112
 01113            if (request.IsLiked.HasValue)
 1114            {
 01115                return false;
 1116            }
 1117
 01118            if (request.IsPlayed.HasValue)
 1119            {
 01120                return false;
 1121            }
 1122
 01123            if (request.IsResumable.HasValue)
 1124            {
 01125                return false;
 1126            }
 1127
 01128            if (request.IsFolder.HasValue)
 1129            {
 01130                return false;
 1131            }
 1132
 01133            if (request.Genres.Count > 0)
 1134            {
 01135                return false;
 1136            }
 1137
 01138            if (request.GenreIds.Count > 0)
 1139            {
 01140                return false;
 1141            }
 1142
 01143            if (request.HasImdbId.HasValue)
 1144            {
 01145                return false;
 1146            }
 1147
 01148            if (request.HasOfficialRating.HasValue)
 1149            {
 01150                return false;
 1151            }
 1152
 01153            if (request.HasOverview.HasValue)
 1154            {
 01155                return false;
 1156            }
 1157
 01158            if (request.HasParentalRating.HasValue)
 1159            {
 01160                return false;
 1161            }
 1162
 01163            if (request.HasSpecialFeature.HasValue)
 1164            {
 01165                return false;
 1166            }
 1167
 01168            if (request.HasSubtitles.HasValue)
 1169            {
 01170                return false;
 1171            }
 1172
 01173            if (request.HasThemeSong.HasValue)
 1174            {
 01175                return false;
 1176            }
 1177
 01178            if (request.HasThemeVideo.HasValue)
 1179            {
 01180                return false;
 1181            }
 1182
 01183            if (request.HasTmdbId.HasValue)
 1184            {
 01185                return false;
 1186            }
 1187
 01188            if (request.HasTrailer.HasValue)
 1189            {
 01190                return false;
 1191            }
 1192
 01193            if (request.ImageTypes.Length > 0)
 1194            {
 01195                return false;
 1196            }
 1197
 01198            if (request.Is3D.HasValue)
 1199            {
 01200                return false;
 1201            }
 1202
 01203            if (request.IsHD.HasValue)
 1204            {
 01205                return false;
 1206            }
 1207
 01208            if (request.IsLocked.HasValue)
 1209            {
 01210                return false;
 1211            }
 1212
 01213            if (request.IsPlaceHolder.HasValue)
 1214            {
 01215                return false;
 1216            }
 1217
 01218            if (request.IsPlayed.HasValue)
 1219            {
 01220                return false;
 1221            }
 1222
 01223            if (!string.IsNullOrWhiteSpace(request.Person))
 1224            {
 01225                return false;
 1226            }
 1227
 01228            if (request.PersonIds.Length > 0)
 1229            {
 01230                return false;
 1231            }
 1232
 01233            if (request.ItemIds.Length > 0)
 1234            {
 01235                return false;
 1236            }
 1237
 01238            if (request.StudioIds.Length > 0)
 1239            {
 01240                return false;
 1241            }
 1242
 01243            if (request.GenreIds.Count > 0)
 1244            {
 01245                return false;
 1246            }
 1247
 01248            if (request.VideoTypes.Length > 0)
 1249            {
 01250                return false;
 1251            }
 1252
 01253            if (request.Years.Length > 0)
 1254            {
 01255                return false;
 1256            }
 1257
 01258            if (request.Tags.Length > 0)
 1259            {
 01260                return false;
 1261            }
 1262
 01263            if (request.OfficialRatings.Length > 0)
 1264            {
 01265                return false;
 1266            }
 1267
 01268            if (request.MinCommunityRating.HasValue)
 1269            {
 01270                return false;
 1271            }
 1272
 01273            if (request.MinCriticRating.HasValue)
 1274            {
 01275                return false;
 1276            }
 1277
 01278            if (request.MinIndexNumber.HasValue)
 1279            {
 01280                return false;
 1281            }
 1282
 01283            return true;
 1284        }
 1285
 1286        public List<BaseItem> GetChildren(User user, bool includeLinkedChildren)
 1287        {
 151288            ArgumentNullException.ThrowIfNull(user);
 1289
 151290            return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user));
 1291        }
 1292
 1293        public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
 1294        {
 151295            ArgumentNullException.ThrowIfNull(user);
 1296
 1297            // the true root should return our users root folder children
 151298            if (IsPhysicalRoot)
 1299            {
 01300                return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren);
 1301            }
 1302
 151303            var result = new Dictionary<Guid, BaseItem>();
 1304
 151305            AddChildren(user, includeLinkedChildren, result, false, query);
 1306
 151307            return result.Values.ToList();
 1308        }
 1309
 1310        protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
 1311        {
 151312            return Children;
 1313        }
 1314
 1315        /// <summary>
 1316        /// Adds the children to list.
 1317        /// </summary>
 1318        private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursiv
 1319        {
 1320            // Prevent infinite recursion of nested folders
 151321            visitedFolders ??= new HashSet<Folder>();
 151322            if (!visitedFolders.Add(this))
 1323            {
 01324                return;
 1325            }
 1326
 1327            // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in mus
 151328            IEnumerable<BaseItem> children = null;
 151329            if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
 1330            {
 01331                children = Children;
 01332                query = null;
 1333            }
 1334
 1335            // If there are not sub-folders, proceed as normal.
 151336            if (children is null)
 1337            {
 151338                children = GetEligibleChildrenForRecursiveChildren(user);
 1339            }
 1340
 151341            AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
 1342
 151343            if (includeLinkedChildren)
 1344            {
 151345                AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query
 1346            }
 151347        }
 1348
 1349        private void AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Di
 1350        {
 901351            foreach (var child in children)
 1352            {
 151353                if (!child.IsVisible(user))
 1354                {
 1355                    continue;
 1356                }
 1357
 151358                if (query is null || UserViewBuilder.FilterItem(child, query))
 1359                {
 151360                    result[child.Id] = child;
 1361                }
 1362
 151363                if (recursive && child.IsFolder)
 1364                {
 01365                    var folder = (Folder)child;
 1366
 01367                    folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
 1368                }
 1369            }
 301370        }
 1371
 1372        public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
 1373        {
 01374            ArgumentNullException.ThrowIfNull(user);
 1375
 01376            var result = new Dictionary<Guid, BaseItem>();
 1377
 01378            AddChildren(user, true, result, true, query);
 1379
 01380            return result.Values;
 1381        }
 1382
 1383        /// <summary>
 1384        /// Gets the recursive children.
 1385        /// </summary>
 1386        /// <returns>IList{BaseItem}.</returns>
 1387        public IList<BaseItem> GetRecursiveChildren()
 1388        {
 01389            return GetRecursiveChildren(true);
 1390        }
 1391
 1392        public IList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
 1393        {
 21394            return GetRecursiveChildren(i => true, includeLinkedChildren);
 1395        }
 1396
 1397        public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
 1398        {
 01399            return GetRecursiveChildren(filter, true);
 1400        }
 1401
 1402        public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
 1403        {
 21404            var result = new Dictionary<Guid, BaseItem>();
 1405
 21406            AddChildrenToList(result, includeLinkedChildren, true, filter);
 1407
 21408            return result.Values.ToList();
 1409        }
 1410
 1411        /// <summary>
 1412        /// Adds the children to list.
 1413        /// </summary>
 1414        private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Fu
 1415        {
 41416            foreach (var child in Children)
 1417            {
 01418                if (filter is null || filter(child))
 1419                {
 01420                    result[child.Id] = child;
 1421                }
 1422
 01423                if (recursive && child.IsFolder)
 1424                {
 01425                    var folder = (Folder)child;
 1426
 1427                    // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loo
 01428                    folder.AddChildrenToList(result, false, true, filter);
 1429                }
 1430            }
 1431
 21432            if (includeLinkedChildren)
 1433            {
 01434                foreach (var child in GetLinkedChildren())
 1435                {
 01436                    if (filter is null || filter(child))
 1437                    {
 01438                        result[child.Id] = child;
 1439                    }
 1440                }
 1441            }
 21442        }
 1443
 1444        /// <summary>
 1445        /// Gets the linked children.
 1446        /// </summary>
 1447        /// <returns>IEnumerable{BaseItem}.</returns>
 1448        public List<BaseItem> GetLinkedChildren()
 1449        {
 151450            var linkedChildren = LinkedChildren;
 151451            var list = new List<BaseItem>(linkedChildren.Length);
 1452
 301453            foreach (var i in linkedChildren)
 1454            {
 01455                var child = GetLinkedChild(i);
 1456
 01457                if (child is not null)
 1458                {
 01459                    list.Add(child);
 1460                }
 1461            }
 1462
 151463            return list;
 1464        }
 1465
 1466        public bool ContainsLinkedChildByItemId(Guid itemId)
 1467        {
 01468            var linkedChildren = LinkedChildren;
 01469            foreach (var i in linkedChildren)
 1470            {
 01471                if (i.ItemId.HasValue)
 1472                {
 01473                    if (i.ItemId.Value.Equals(itemId))
 1474                    {
 01475                        return true;
 1476                    }
 1477
 1478                    continue;
 1479                }
 1480
 01481                var child = GetLinkedChild(i);
 1482
 01483                if (child is not null && child.Id.Equals(itemId))
 1484                {
 01485                    return true;
 1486                }
 1487            }
 1488
 01489            return false;
 1490        }
 1491
 1492        public List<BaseItem> GetLinkedChildren(User user)
 1493        {
 151494            if (!FilterLinkedChildrenPerUser || user is null)
 1495            {
 151496                return GetLinkedChildren();
 1497            }
 1498
 01499            var linkedChildren = LinkedChildren;
 01500            var list = new List<BaseItem>(linkedChildren.Length);
 1501
 01502            if (linkedChildren.Length == 0)
 1503            {
 01504                return list;
 1505            }
 1506
 01507            var allUserRootChildren = LibraryManager.GetUserRootFolder()
 01508                .GetChildren(user, true)
 01509                .OfType<Folder>()
 01510                .ToList();
 1511
 01512            var collectionFolderIds = allUserRootChildren
 01513                .Select(i => i.Id)
 01514                .ToList();
 1515
 01516            foreach (var i in linkedChildren)
 1517            {
 01518                var child = GetLinkedChild(i);
 1519
 01520                if (child is null)
 1521                {
 1522                    continue;
 1523                }
 1524
 01525                var childOwner = child.GetOwner() ?? child;
 1526
 01527                if (child is not IItemByName)
 1528                {
 01529                    var childProtocol = childOwner.PathProtocol;
 01530                    if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File)
 1531                    {
 01532                        if (!childOwner.IsVisibleStandalone(user))
 1533                        {
 01534                            continue;
 1535                        }
 1536                    }
 1537                    else
 1538                    {
 01539                        var itemCollectionFolderIds =
 01540                            LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id);
 1541
 01542                        if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains))
 1543                        {
 1544                            continue;
 1545                        }
 1546                    }
 1547                }
 1548
 01549                list.Add(child);
 1550            }
 1551
 01552            return list;
 1553        }
 1554
 1555        /// <summary>
 1556        /// Gets the linked children.
 1557        /// </summary>
 1558        /// <returns>IEnumerable{BaseItem}.</returns>
 1559        public IEnumerable<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
 1560        {
 01561            return LinkedChildren
 01562                .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i)))
 01563                .Where(i => i.Item2 is not null);
 1564        }
 1565
 1566        protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystem
 1567        {
 1568            var changesFound = false;
 1569
 1570            if (IsFileProtocol)
 1571            {
 1572                if (RefreshLinkedChildren(fileSystemChildren))
 1573                {
 1574                    changesFound = true;
 1575                }
 1576            }
 1577
 1578            var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).Configur
 1579
 1580            return baseHasChanges || changesFound;
 1581        }
 1582
 1583        /// <summary>
 1584        /// Refreshes the linked children.
 1585        /// </summary>
 1586        /// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
 1587        /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
 1588        protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
 1589        {
 211590            if (SupportsShortcutChildren)
 1591            {
 211592                var newShortcutLinks = fileSystemChildren
 211593                    .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName))
 211594                    .Select(i =>
 211595                    {
 211596                        try
 211597                        {
 211598                            Logger.LogDebug("Found shortcut at {0}", i.FullName);
 211599
 211600                            var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShor
 211601
 211602                            if (!string.IsNullOrEmpty(resolvedPath))
 211603                            {
 211604                                return new LinkedChild
 211605                                {
 211606                                    Path = resolvedPath,
 211607                                    Type = LinkedChildType.Shortcut
 211608                                };
 211609                            }
 211610
 211611                            Logger.LogError("Error resolving shortcut {0}", i.FullName);
 211612
 211613                            return null;
 211614                        }
 211615                        catch (IOException ex)
 211616                        {
 211617                            Logger.LogError(ex, "Error resolving shortcut {0}", i.FullName);
 211618                            return null;
 211619                        }
 211620                    })
 211621                    .Where(i => i is not null)
 211622                    .ToList();
 1623
 211624                var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
 1625
 211626                if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem)))
 1627                {
 01628                    Logger.LogInformation("Shortcut links have changed for {0}", Path);
 1629
 01630                    newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual));
 01631                    LinkedChildren = newShortcutLinks.ToArray();
 01632                    return true;
 1633                }
 1634            }
 1635
 421636            foreach (var child in LinkedChildren)
 1637            {
 1638                // Reset the cached value
 01639                child.ItemId = null;
 1640            }
 1641
 211642            return false;
 1643        }
 1644
 1645        /// <summary>
 1646        /// Marks the played.
 1647        /// </summary>
 1648        /// <param name="user">The user.</param>
 1649        /// <param name="datePlayed">The date played.</param>
 1650        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1651        public override void MarkPlayed(
 1652            User user,
 1653            DateTime? datePlayed,
 1654            bool resetPosition)
 1655        {
 01656            var query = new InternalItemsQuery
 01657            {
 01658                User = user,
 01659                Recursive = true,
 01660                IsFolder = false,
 01661                EnableTotalRecordCount = false
 01662            };
 1663
 01664            if (!user.DisplayMissingEpisodes)
 1665            {
 01666                query.IsVirtualItem = false;
 1667            }
 1668
 01669            var itemsResult = GetItemList(query);
 1670
 1671            // Sweep through recursively and update status
 01672            foreach (var item in itemsResult)
 1673            {
 01674                if (item.IsVirtualItem)
 1675                {
 1676                    // The querying doesn't support virtual unaired
 01677                    var episode = item as Episode;
 01678                    if (episode is not null && episode.IsUnaired)
 1679                    {
 1680                        continue;
 1681                    }
 1682                }
 1683
 01684                item.MarkPlayed(user, datePlayed, resetPosition);
 1685            }
 01686        }
 1687
 1688        /// <summary>
 1689        /// Marks the unplayed.
 1690        /// </summary>
 1691        /// <param name="user">The user.</param>
 1692        public override void MarkUnplayed(User user)
 1693        {
 01694            var itemsResult = GetItemList(new InternalItemsQuery
 01695            {
 01696                User = user,
 01697                Recursive = true,
 01698                IsFolder = false,
 01699                EnableTotalRecordCount = false
 01700            });
 1701
 1702            // Sweep through recursively and update status
 01703            foreach (var item in itemsResult)
 1704            {
 01705                item.MarkUnplayed(user);
 1706            }
 01707        }
 1708
 1709        public override bool IsPlayed(User user)
 1710        {
 01711            var itemsResult = GetItemList(new InternalItemsQuery(user)
 01712            {
 01713                Recursive = true,
 01714                IsFolder = false,
 01715                IsVirtualItem = false,
 01716                EnableTotalRecordCount = false
 01717            });
 1718
 01719            return itemsResult
 01720                .All(i => i.IsPlayed(user));
 1721        }
 1722
 1723        public override bool IsUnplayed(User user)
 1724        {
 01725            return !IsPlayed(user);
 1726        }
 1727
 1728        public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User
 1729        {
 141730            if (!SupportsUserDataFromChildren)
 1731            {
 141732                return;
 1733            }
 1734
 01735            if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))
 1736            {
 01737                itemDto.RecursiveItemCount = GetRecursiveChildCount(user);
 1738            }
 1739
 01740            if (SupportsPlayedStatus)
 1741            {
 01742                var unplayedQueryResult = GetItems(new InternalItemsQuery(user)
 01743                {
 01744                    Recursive = true,
 01745                    IsFolder = false,
 01746                    IsVirtualItem = false,
 01747                    EnableTotalRecordCount = true,
 01748                    Limit = 0,
 01749                    IsPlayed = false,
 01750                    DtoOptions = new DtoOptions(false)
 01751                    {
 01752                        EnableImages = false
 01753                    }
 01754                }).TotalRecordCount;
 1755
 01756                dto.UnplayedItemCount = unplayedQueryResult;
 1757
 01758                if (itemDto?.RecursiveItemCount > 0)
 1759                {
 01760                    var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100;
 01761                    dto.PlayedPercentage = 100 - unplayedPercentage;
 01762                    dto.Played = dto.PlayedPercentage.Value >= 100;
 1763                }
 1764                else
 1765                {
 01766                    dto.Played = (dto.UnplayedItemCount ?? 0) == 0;
 1767                }
 1768            }
 01769        }
 1770
 1771        /// <summary>
 1772        /// Contains constants used when reporting scan progress.
 1773        /// </summary>
 1774        private static class ProgressHelpers
 1775        {
 1776            /// <summary>
 1777            /// Reported after the folders immediate children are retrieved.
 1778            /// </summary>
 1779            public const int RetrievedChildren = 5;
 1780
 1781            /// <summary>
 1782            /// Reported after add, updating, or deleting child items from the LibraryManager.
 1783            /// </summary>
 1784            public const int UpdatedChildItems = 10;
 1785
 1786            /// <summary>
 1787            /// Reported once subfolders are scanned.
 1788            /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders].
 1789            /// </summary>
 1790            public const int ScannedSubfolders = 50;
 1791
 1792            /// <summary>
 1793            /// Reported once metadata is refreshed.
 1794            /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed].
 1795            /// </summary>
 1796            public const int RefreshedMetadata = 100;
 1797
 1798            /// <summary>
 1799            /// Gets the current progress given the previous step, next step, and progress in between.
 1800            /// </summary>
 1801            /// <param name="previousProgressStep">The previous progress step.</param>
 1802            /// <param name="nextProgressStep">The next progress step.</param>
 1803            /// <param name="currentProgress">The current progress step.</param>
 1804            /// <returns>The progress.</returns>
 1805            public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress)
 1806            {
 21807                return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100));
 1808            }
 1809        }
 1810    }
 1811}

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()
get_RecursiveChildren()
get_SupportsShortcutChildren()
get_FilterLinkedChildrenPerUser()
get_SupportsOwnedItems()
get_SupportsUserDataFromChildren()
CanDelete()
RequiresRefresh()
AddChild(MediaBrowser.Controller.Entities.BaseItem)
IsVisible(Jellyfin.Data.Entities.User)
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)
RefreshMetadataRecursive(System.Collections.Generic.IList`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Boolean,System.IProgress`1<System.Double>,System.Threading.CancellationToken)
ValidateSubFolders(System.Collections.Generic.IList`1<MediaBrowser.Controller.Entities.Folder>,MediaBrowser.Controller.Providers.IDirectoryService,System.IProgress`1<System.Double>,System.Threading.CancellationToken)
GetNonCachedChildren(MediaBrowser.Controller.Providers.IDirectoryService)
GetCachedChildren()
GetChildCount(Jellyfin.Data.Entities.User)
GetRecursiveChildCount(Jellyfin.Data.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,System.Boolean)
CollapseBoxSetItemsIfNeeded(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Entities.InternalItemsQuery,MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Data.Entities.User,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Collections.ICollectionManager)
CollapseBoxSetItems(MediaBrowser.Controller.Entities.InternalItemsQuery,MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Data.Entities.User,MediaBrowser.Controller.Configuration.IServerConfigurationManager)
AllowBoxSetCollapsing(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetChildren(Jellyfin.Data.Entities.User,System.Boolean)
GetChildren(Jellyfin.Data.Entities.User,System.Boolean,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetEligibleChildrenForRecursiveChildren(Jellyfin.Data.Entities.User)
AddChildren(Jellyfin.Data.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.Data.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.Data.Entities.User,MediaBrowser.Controller.Entities.InternalItemsQuery)
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.Data.Entities.User)
GetLinkedChildrenInfos()
RefreshLinkedChildren(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>)
MarkPlayed(Jellyfin.Data.Entities.User,System.Nullable`1<System.DateTime>,System.Boolean)
MarkUnplayed(Jellyfin.Data.Entities.User)
IsPlayed(Jellyfin.Data.Entities.User)
IsUnplayed(Jellyfin.Data.Entities.User)
FillUserDataDtoValues(MediaBrowser.Model.Dto.UserItemDataDto,MediaBrowser.Controller.Entities.UserItemData,MediaBrowser.Model.Dto.BaseItemDto,Jellyfin.Data.Entities.User,MediaBrowser.Controller.Dto.DtoOptions)
GetProgress(System.Int32,System.Int32,System.Double)