< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.Folder
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/Folder.cs
Line coverage
33%
Covered lines: 180
Uncovered lines: 363
Coverable lines: 543
Total lines: 1771
Line coverage: 33.1%
Branch coverage
29%
Covered branches: 107
Total branches: 360
Branch coverage: 29.7%
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%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%11863.63%
IsLibraryFolderAccessible(...)80%101083.33%
GetNonCachedChildren(...)100%11100%
GetCachedChildren()100%11100%
GetChildCount(...)0%2040%
GetRecursiveChildCount(...)100%210%
QueryRecursive(...)71.42%291457.14%
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%28615.78%
PostFilterAndSort(...)0%110100%
CollapseBoxSetItemsIfNeeded(...)0%620%
CollapseBoxSetItems(...)40.9%1332238.88%
AllowBoxSetCollapsing(...)0%4692680%
GetChildren(...)100%11100%
GetChildren(...)50%2283.33%
GetEligibleChildrenForRecursiveChildren(...)100%11100%
AddChildren(...)71.42%161476.92%
AddChildrenFromCollection(...)83.33%141275%
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    {
 39945        public Folder()
 46        {
 39947            LinkedChildren = Array.Empty<LinkedChild>();
 39948        }
 49
 50        public static IUserViewManager UserViewManager { get; set; }
 51
 52        public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get; set; }
 53
 54        /// <summary>
 55        /// Gets or sets a value indicating whether this instance is root.
 56        /// </summary>
 57        /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
 58        public bool IsRoot { get; set; }
 59
 60        public LinkedChild[] LinkedChildren { get; set; }
 61
 62        [JsonIgnore]
 63        public DateTime? DateLastMediaAdded { get; set; }
 64
 65        [JsonIgnore]
 066        public override bool SupportsThemeMedia => true;
 67
 68        [JsonIgnore]
 069        public virtual bool IsPreSorted => false;
 70
 71        [JsonIgnore]
 1072        public virtual bool IsPhysicalRoot => false;
 73
 74        [JsonIgnore]
 075        public override bool SupportsInheritedParentImages => true;
 76
 77        [JsonIgnore]
 078        public override bool SupportsPlayedStatus => true;
 79
 80        /// <summary>
 81        /// Gets a value indicating whether this instance is folder.
 82        /// </summary>
 83        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 84        [JsonIgnore]
 77985        public override bool IsFolder => true;
 86
 87        [JsonIgnore]
 088        public override bool IsDisplayedAsFolder => true;
 89
 90        [JsonIgnore]
 9191        public virtual bool SupportsCumulativeRunTimeTicks => false;
 92
 93        [JsonIgnore]
 3494        public virtual bool SupportsDateLastMediaAdded => false;
 95
 96        [JsonIgnore]
 97        public override string FileNameWithoutExtension
 98        {
 99            get
 100            {
 313101                if (IsFileProtocol)
 102                {
 313103                    return System.IO.Path.GetFileName(Path);
 104                }
 105
 0106                return null;
 107            }
 108        }
 109
 110        /// <summary>
 111        /// Gets the actual children.
 112        /// </summary>
 113        /// <value>The actual children.</value>
 114        [JsonIgnore]
 953115        public virtual IEnumerable<BaseItem> Children => LoadChildren();
 116
 117        /// <summary>
 118        /// Gets thread-safe access to all recursive children of this folder - without regard to user.
 119        /// </summary>
 120        /// <value>The recursive children.</value>
 121        [JsonIgnore]
 0122        public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
 123
 124        [JsonIgnore]
 22125        protected virtual bool SupportsShortcutChildren => false;
 126
 10127        protected virtual bool FilterLinkedChildrenPerUser => false;
 128
 129        [JsonIgnore]
 92130        protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
 131
 132        [JsonIgnore]
 133        public virtual bool SupportsUserDataFromChildren
 134        {
 135            get
 136            {
 137                // These are just far too slow.
 9138                if (this is ICollectionFolder)
 139                {
 3140                    return false;
 141                }
 142
 6143                if (this is UserView)
 144                {
 0145                    return false;
 146                }
 147
 6148                if (this is UserRootFolder)
 149                {
 6150                    return false;
 151                }
 152
 0153                if (this is Channel)
 154                {
 0155                    return false;
 156                }
 157
 0158                if (SourceType != SourceType.Library)
 159                {
 0160                    return false;
 161                }
 162
 0163                if (this is IItemByName)
 164                {
 0165                    if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName)
 166                    {
 0167                        return false;
 168                    }
 169                }
 170
 0171                return true;
 172            }
 173        }
 174
 175        public static ICollectionManager CollectionManager { get; set; }
 176
 177        public override bool CanDelete()
 178        {
 6179            if (IsRoot)
 180            {
 6181                return false;
 182            }
 183
 0184            return base.CanDelete();
 185        }
 186
 187        public override bool RequiresRefresh()
 188        {
 57189            var baseResult = base.RequiresRefresh();
 190
 57191            if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue)
 192            {
 0193                baseResult = true;
 194            }
 195
 57196            return baseResult;
 197        }
 198
 199        /// <summary>
 200        /// Adds the child.
 201        /// </summary>
 202        /// <param name="item">The item.</param>
 203        /// <exception cref="InvalidOperationException">Unable to add  + item.Name.</exception>
 204        public void AddChild(BaseItem item)
 205        {
 0206            item.SetParent(this);
 207
 0208            if (item.Id.IsEmpty())
 209            {
 0210                item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
 211            }
 212
 0213            if (item.DateCreated == DateTime.MinValue)
 214            {
 0215                item.DateCreated = DateTime.UtcNow;
 216            }
 217
 0218            if (item.DateModified == DateTime.MinValue)
 219            {
 0220                item.DateModified = DateTime.UtcNow;
 221            }
 222
 0223            LibraryManager.CreateItem(item, this);
 0224        }
 225
 226        public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 227        {
 13228            if (this is ICollectionFolder && this is not BasePluginFolder)
 229            {
 0230                var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 0231                if (blockedMediaFolders.Length > 0)
 232                {
 0233                    if (blockedMediaFolders.Contains(Id))
 234                    {
 0235                        return false;
 236                    }
 237                }
 238                else
 239                {
 0240                    if (!user.HasPermission(PermissionKind.EnableAllFolders)
 0241                        && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id))
 242                    {
 0243                        return false;
 244                    }
 245                }
 246            }
 247
 13248            return base.IsVisible(user, skipAllowedTagsCheck);
 249        }
 250
 251        /// <summary>
 252        /// Loads our children.  Validation will occur externally.
 253        /// We want this synchronous.
 254        /// </summary>
 255        /// <returns>Returns children.</returns>
 256        protected virtual IReadOnlyList<BaseItem> LoadChildren()
 257        {
 258            // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
 259            // just load our children from the repo - the library will be validated and maintained in other processes
 164260            return GetCachedChildren();
 261        }
 262
 263        public override double? GetRefreshProgress()
 264        {
 0265            return ProviderManager.GetRefreshProgress(Id);
 266        }
 267
 268        public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
 269        {
 0270            return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellation
 271        }
 272
 273        /// <summary>
 274        /// Validates that the children of the folder still exist.
 275        /// </summary>
 276        /// <param name="progress">The progress.</param>
 277        /// <param name="metadataRefreshOptions">The metadata refresh options.</param>
 278        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 279        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 280        /// <param name="cancellationToken">The cancellation token.</param>
 281        /// <returns>Task.</returns>
 282        public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool rec
 283        {
 52284            return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadata
 285        }
 286
 287        private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
 288        {
 48289            var dictionary = new Dictionary<Guid, BaseItem>();
 290
 48291            var childrenList = Children.ToList();
 292
 168293            foreach (var child in childrenList)
 294            {
 36295                var id = child.Id;
 36296                if (dictionary.ContainsKey(id))
 297                {
 0298                    Logger.LogError(
 0299                        "Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}",
 0300                        Path ?? Name,
 0301                        child.Path ?? child.Name);
 302                }
 303                else
 304                {
 36305                    dictionary[id] = child;
 306                }
 307            }
 308
 48309            return dictionary;
 310        }
 311
 312        /// <summary>
 313        /// Validates the children internal.
 314        /// </summary>
 315        /// <param name="progress">The progress.</param>
 316        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 317        /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
 318        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 319        /// <param name="refreshOptions">The refresh options.</param>
 320        /// <param name="directoryService">The directory service.</param>
 321        /// <param name="cancellationToken">The cancellation token.</param>
 322        /// <returns>Task.</returns>
 323        protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshCh
 324        {
 325            if (recursive)
 326            {
 327                ProviderManager.OnRefreshStart(this);
 328            }
 329
 330            try
 331            {
 332                await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptio
 333            }
 334            finally
 335            {
 336                if (recursive)
 337                {
 338                    ProviderManager.OnRefreshComplete(this);
 339                }
 340            }
 341        }
 342
 343        private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item, bool checkColle
 344        {
 92345            if (!checkCollection && (item is BoxSet || string.Equals(item.FileNameWithoutExtension, "collections", Strin
 346            {
 0347                return true;
 348            }
 349
 350            // For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
 92351            if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
 352            {
 31353                Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.Containi
 31354                return false;
 355            }
 356
 61357            return true;
 358        }
 359
 360        private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetada
 361        {
 362            if (!IsLibraryFolderAccessible(directoryService, this, allowRemoveRoot))
 363            {
 364                return;
 365            }
 366
 367            cancellationToken.ThrowIfCancellationRequested();
 368
 369            var validChildren = new List<BaseItem>();
 370            var validChildrenNeedGeneration = false;
 371
 372            if (IsFileProtocol)
 373            {
 374                IEnumerable<BaseItem> nonCachedChildren = [];
 375
 376                try
 377                {
 378                    nonCachedChildren = GetNonCachedChildren(directoryService);
 379                }
 380                catch (IOException ex)
 381                {
 382                    Logger.LogError(ex, "Error retrieving children from file system");
 383                }
 384                catch (SecurityException ex)
 385                {
 386                    Logger.LogError(ex, "Error retrieving children from file system");
 387                }
 388                catch (Exception ex)
 389                {
 390                    Logger.LogError(ex, "Error retrieving children");
 391                    return;
 392                }
 393
 394                progress.Report(ProgressHelpers.RetrievedChildren);
 395
 396                if (recursive)
 397                {
 398                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren);
 399                }
 400
 401                // Build a dictionary of the current children we have now by Id so we can compare quickly and easily
 402                var currentChildren = GetActualChildrenDictionary();
 403
 404                // Create a list for our validated children
 405                var newItems = new List<BaseItem>();
 406
 407                cancellationToken.ThrowIfCancellationRequested();
 408
 409                foreach (var child in nonCachedChildren)
 410                {
 411                    if (!IsLibraryFolderAccessible(directoryService, child, allowRemoveRoot))
 412                    {
 413                        continue;
 414                    }
 415
 416                    if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild))
 417                    {
 418                        validChildren.Add(currentChild);
 419
 420                        if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
 421                        {
 422                            await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken)
 423                        }
 424                        else
 425                        {
 426                            // metadata is up-to-date; make sure DB has correct images dimensions and hash
 427                            await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false);
 428                        }
 429
 430                        continue;
 431                    }
 432
 433                    // Brand new item - needs to be added
 434                    child.SetParent(this);
 435                    newItems.Add(child);
 436                    validChildren.Add(child);
 437                }
 438
 439                // That's all the new and changed ones - now see if any have been removed and need cleanup
 440                var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
 441                var shouldRemove = !IsRoot || allowRemoveRoot;
 442                // If it's an AggregateFolder, don't remove
 443                if (shouldRemove && itemsRemoved.Count > 0)
 444                {
 445                    foreach (var item in itemsRemoved)
 446                    {
 447                        if (item.IsFileProtocol)
 448                        {
 449                            Logger.LogDebug("Removed item: {Path}", item.Path);
 450
 451                            item.SetParent(null);
 452                            LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, fals
 453                        }
 454                    }
 455                }
 456
 457                if (newItems.Count > 0)
 458                {
 459                    LibraryManager.CreateItems(newItems, this, cancellationToken);
 460                }
 461            }
 462            else
 463            {
 464                validChildrenNeedGeneration = true;
 465            }
 466
 467            progress.Report(ProgressHelpers.UpdatedChildItems);
 468
 469            if (recursive)
 470            {
 471                ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems);
 472            }
 473
 474            cancellationToken.ThrowIfCancellationRequested();
 475
 476            if (recursive)
 477            {
 478                var folder = this;
 479                var innerProgress = new Progress<double>(innerPercent =>
 480                {
 481                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.Scanned
 482
 483                    progress.Report(percent);
 484
 485                    ProviderManager.OnRefreshProgress(folder, percent);
 486                });
 487
 488                if (validChildrenNeedGeneration)
 489                {
 490                    validChildren = Children.ToList();
 491                    validChildrenNeedGeneration = false;
 492                }
 493
 494                await ValidateSubFolders(validChildren.OfType<Folder>().ToList(), directoryService, innerProgress, cance
 495            }
 496
 497            if (refreshChildMetadata)
 498            {
 499                progress.Report(ProgressHelpers.ScannedSubfolders);
 500
 501                if (recursive)
 502                {
 503                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders);
 504                }
 505
 506                var container = this as IMetadataContainer;
 507
 508                var folder = this;
 509                var innerProgress = new Progress<double>(innerPercent =>
 510                {
 511                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.Refresh
 512
 513                    progress.Report(percent);
 514
 515                    if (recursive)
 516                    {
 517                        ProviderManager.OnRefreshProgress(folder, percent);
 518                    }
 519                });
 520
 521                if (container is not null)
 522                {
 523                    await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).Co
 524                }
 525                else
 526                {
 527                    if (validChildrenNeedGeneration)
 528                    {
 529                        validChildren = Children.ToList();
 530                    }
 531
 532                    await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellation
 533                }
 534            }
 535        }
 536
 537        private async Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, boo
 538        {
 539            await RunTasks(
 540                (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFold
 541                children,
 542                progress,
 543                cancellationToken).ConfigureAwait(false);
 544        }
 545
 546        private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOp
 547        {
 548            if (container is Series series)
 549            {
 550                await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 551            }
 552
 553            await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
 554        }
 555
 556        private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, I
 557        {
 558            if (child is IMetadataContainer container)
 559            {
 560                await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAw
 561            }
 562            else
 563            {
 564                if (refreshOptions.RefreshItem(child))
 565                {
 566                    await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 567                }
 568
 569                if (recursive && child is Folder folder)
 570                {
 571                    await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, canc
 572                }
 573            }
 574        }
 575
 576        /// <summary>
 577        /// Refreshes the children.
 578        /// </summary>
 579        /// <param name="children">The children.</param>
 580        /// <param name="directoryService">The directory service.</param>
 581        /// <param name="progress">The progress.</param>
 582        /// <param name="cancellationToken">The cancellation token.</param>
 583        /// <returns>Task.</returns>
 584        private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<doub
 585        {
 586            await RunTasks(
 587                (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, dire
 588                children,
 589                progress,
 590                cancellationToken).ConfigureAwait(false);
 591        }
 592
 593        /// <summary>
 594        /// Runs an action block on a list of children.
 595        /// </summary>
 596        /// <param name="task">The task to run for each child.</param>
 597        /// <param name="children">The list of children.</param>
 598        /// <param name="progress">The progress.</param>
 599        /// <param name="cancellationToken">The cancellation token.</param>
 600        /// <returns>Task.</returns>
 601        private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progr
 602        {
 603            await LimitedConcurrencyLibraryScheduler
 604                .Enqueue(
 605                    children.ToArray(),
 606                    task,
 607                    progress,
 608                    cancellationToken)
 609                .ConfigureAwait(false);
 610        }
 611
 612        /// <summary>
 613        /// Get the children of this folder from the actual file system.
 614        /// </summary>
 615        /// <returns>IEnumerable{BaseItem}.</returns>
 616        /// <param name="directoryService">The directory service to use for operation.</param>
 617        /// <returns>Returns set of base items.</returns>
 618        protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
 619        {
 48620            var collectionType = LibraryManager.GetContentType(this);
 48621            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 622
 48623            return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryO
 624        }
 625
 626        /// <summary>
 627        /// Get our children from the repo - stubbed for now.
 628        /// </summary>
 629        /// <returns>IEnumerable{BaseItem}.</returns>
 630        protected IReadOnlyList<BaseItem> GetCachedChildren()
 631        {
 164632            return ItemRepository.GetItemList(new InternalItemsQuery
 164633            {
 164634                Parent = this,
 164635                GroupByPresentationUniqueKey = false,
 164636                DtoOptions = new DtoOptions(true)
 164637            });
 638        }
 639
 640        public virtual int GetChildCount(User user)
 641        {
 0642            if (LinkedChildren.Length > 0)
 643            {
 0644                if (this is not ICollectionFolder)
 645                {
 0646                    return GetChildren(user, true).Count;
 647                }
 648            }
 649
 0650            var result = GetItems(new InternalItemsQuery(user)
 0651            {
 0652                Recursive = false,
 0653                Limit = 0,
 0654                Parent = this,
 0655                DtoOptions = new DtoOptions(false)
 0656                {
 0657                    EnableImages = false
 0658                }
 0659            });
 660
 0661            return result.TotalRecordCount;
 662        }
 663
 664        public virtual int GetRecursiveChildCount(User user)
 665        {
 0666            return GetItems(new InternalItemsQuery(user)
 0667            {
 0668                Recursive = true,
 0669                IsFolder = false,
 0670                IsVirtualItem = false,
 0671                EnableTotalRecordCount = true,
 0672                Limit = 0,
 0673                DtoOptions = new DtoOptions(false)
 0674                {
 0675                    EnableImages = false
 0676                }
 0677            }).TotalRecordCount;
 678        }
 679
 680        public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
 681        {
 18682            var user = query.User;
 683
 18684            if (!query.ForceDirect && RequiresPostFiltering(query))
 685            {
 686                IEnumerable<BaseItem> items;
 0687                Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManage
 688
 0689                if (query.User is null)
 690                {
 0691                    items = GetRecursiveChildren(filter);
 692                }
 693                else
 694                {
 0695                    items = GetRecursiveChildren(user, query);
 696                }
 697
 0698                return PostFilterAndSort(items, query, true);
 699            }
 700
 18701            if (this is not UserRootFolder
 18702                && this is not AggregateFolder
 18703                && query.ParentId.IsEmpty())
 704            {
 18705                query.Parent = this;
 706            }
 707
 18708            if (RequiresPostFiltering2(query))
 709            {
 0710                return QueryWithPostFiltering2(query);
 711            }
 712
 18713            return LibraryManager.GetItemsResult(query);
 714        }
 715
 716        protected QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query)
 717        {
 1718            var startIndex = query.StartIndex;
 1719            var limit = query.Limit;
 720
 1721            query.StartIndex = null;
 1722            query.Limit = null;
 723
 1724            IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query);
 1725            var user = query.User;
 726
 1727            if (user is not null)
 728            {
 729                // needed for boxsets
 1730                itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User));
 731            }
 732
 733            IEnumerable<BaseItem> returnItems;
 1734            int totalCount = 0;
 735
 1736            if (query.EnableTotalRecordCount)
 737            {
 0738                var itemArray = itemsList.ToArray();
 0739                totalCount = itemArray.Length;
 0740                returnItems = itemArray;
 741            }
 742            else
 743            {
 1744                returnItems = itemsList;
 745            }
 746
 1747            if (limit.HasValue)
 748            {
 0749                returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value);
 750            }
 1751            else if (startIndex.HasValue)
 752            {
 0753                returnItems = returnItems.Skip(startIndex.Value);
 754            }
 755
 1756            return new QueryResult<BaseItem>(
 1757                query.StartIndex,
 1758                totalCount,
 1759                returnItems.ToArray());
 760        }
 761
 762        private bool RequiresPostFiltering2(InternalItemsQuery query)
 763        {
 18764            if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet)
 765            {
 0766                Logger.LogDebug("Query requires post-filtering due to BoxSet query");
 0767                return true;
 768            }
 769
 18770            return false;
 771        }
 772
 773        private bool RequiresPostFiltering(InternalItemsQuery query)
 774        {
 18775            if (LinkedChildren.Length > 0)
 776            {
 0777                if (this is not ICollectionFolder)
 778                {
 0779                    Logger.LogDebug("{Type}: Query requires post-filtering due to LinkedChildren.", GetType().Name);
 0780                    return true;
 781                }
 782            }
 783
 784            // Filter by Video3DFormat
 18785            if (query.Is3D.HasValue)
 786            {
 0787                Logger.LogDebug("Query requires post-filtering due to Is3D");
 0788                return true;
 789            }
 790
 18791            if (query.HasOfficialRating.HasValue)
 792            {
 0793                Logger.LogDebug("Query requires post-filtering due to HasOfficialRating");
 0794                return true;
 795            }
 796
 18797            if (query.IsPlaceHolder.HasValue)
 798            {
 0799                Logger.LogDebug("Query requires post-filtering due to IsPlaceHolder");
 0800                return true;
 801            }
 802
 18803            if (query.HasSpecialFeature.HasValue)
 804            {
 0805                Logger.LogDebug("Query requires post-filtering due to HasSpecialFeature");
 0806                return true;
 807            }
 808
 18809            if (query.HasSubtitles.HasValue)
 810            {
 0811                Logger.LogDebug("Query requires post-filtering due to HasSubtitles");
 0812                return true;
 813            }
 814
 18815            if (query.HasTrailer.HasValue)
 816            {
 0817                Logger.LogDebug("Query requires post-filtering due to HasTrailer");
 0818                return true;
 819            }
 820
 18821            if (query.HasThemeSong.HasValue)
 822            {
 0823                Logger.LogDebug("Query requires post-filtering due to HasThemeSong");
 0824                return true;
 825            }
 826
 18827            if (query.HasThemeVideo.HasValue)
 828            {
 0829                Logger.LogDebug("Query requires post-filtering due to HasThemeVideo");
 0830                return true;
 831            }
 832
 833            // Filter by VideoType
 18834            if (query.VideoTypes.Length > 0)
 835            {
 0836                Logger.LogDebug("Query requires post-filtering due to VideoTypes");
 0837                return true;
 838            }
 839
 18840            if (CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
 841            {
 0842                Logger.LogDebug("Query requires post-filtering due to CollapseBoxSetItems");
 0843                return true;
 844            }
 845
 18846            if (!query.AdjacentTo.IsNullOrEmpty())
 847            {
 0848                Logger.LogDebug("Query requires post-filtering due to AdjacentTo");
 0849                return true;
 850            }
 851
 18852            if (query.SeriesStatuses.Length > 0)
 853            {
 0854                Logger.LogDebug("Query requires post-filtering due to SeriesStatuses");
 0855                return true;
 856            }
 857
 18858            if (query.AiredDuringSeason.HasValue)
 859            {
 0860                Logger.LogDebug("Query requires post-filtering due to AiredDuringSeason");
 0861                return true;
 862            }
 863
 18864            if (query.IsPlayed.HasValue)
 865            {
 0866                if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(BaseItemKind.Series))
 867                {
 0868                    Logger.LogDebug("Query requires post-filtering due to IsPlayed");
 0869                    return true;
 870                }
 871            }
 872
 18873            return false;
 874        }
 875
 876        private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
 877        {
 0878            return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
 879        }
 880
 881        public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
 882        {
 0883            if (query.ItemIds.Length > 0)
 884            {
 0885                var result = LibraryManager.GetItemsResult(query);
 886
 0887                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 888                {
 0889                    result.Items = SortItemsByRequest(query, result.Items);
 890                }
 891
 0892                return result;
 893            }
 894
 0895            return GetItemsInternal(query);
 896        }
 897
 898        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 899        {
 19900            query.EnableTotalRecordCount = false;
 901
 19902            if (query.ItemIds.Length > 0)
 903            {
 0904                var result = LibraryManager.GetItemList(query);
 905
 0906                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 907                {
 0908                    return SortItemsByRequest(query, result);
 909                }
 910
 0911                return result;
 912            }
 913
 19914            return GetItemsInternal(query).Items;
 915        }
 916
 917        protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
 918        {
 18919            if (SourceType == SourceType.Channel)
 920            {
 921                try
 922                {
 0923                    query.Parent = this;
 0924                    query.ChannelIds = new[] { ChannelId };
 925
 926                    // Don't blow up here because it could cause parent screens with other content to fail
 0927                    return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None)
 928                }
 0929                catch
 930                {
 931                    // Already logged at lower levels
 0932                    return new QueryResult<BaseItem>();
 933                }
 934            }
 935
 18936            if (query.Recursive)
 937            {
 18938                return QueryRecursive(query);
 939            }
 940
 0941            var user = query.User;
 942
 0943            Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
 944
 945            IEnumerable<BaseItem> items;
 946
 0947            if (query.User is null)
 948            {
 0949                items = Children.Where(filter);
 950            }
 951            else
 952            {
 953                // need to pass this param to the children.
 0954                var childQuery = new InternalItemsQuery
 0955                {
 0956                    DisplayAlbumFolders = query.DisplayAlbumFolders
 0957                };
 958
 0959                items = GetChildren(user, true, childQuery).Where(filter);
 960            }
 961
 0962            return PostFilterAndSort(items, query, true);
 0963        }
 964
 965        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool en
 966        {
 0967            var user = query.User;
 968
 969            // Check recursive - don't substitute in plain folder views
 0970            if (user is not null)
 971            {
 0972                items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
 973            }
 974
 975#pragma warning disable CA1309
 0976            if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
 977            {
 0978                items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.Inva
 979            }
 980
 0981            if (!string.IsNullOrEmpty(query.NameStartsWith))
 982            {
 0983                items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIg
 984            }
 985
 0986            if (!string.IsNullOrEmpty(query.NameLessThan))
 987            {
 0988                items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultur
 989            }
 990#pragma warning restore CA1309
 991
 992            // This must be the last filter
 0993            if (!query.AdjacentTo.IsNullOrEmpty())
 994            {
 0995                items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
 996            }
 997
 0998            return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
 999        }
 1000
 1001        private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
 1002            IEnumerable<BaseItem> items,
 1003            InternalItemsQuery query,
 1004            BaseItem queryParent,
 1005            User user,
 1006            IServerConfigurationManager configurationManager,
 1007            ICollectionManager collectionManager)
 1008        {
 01009            ArgumentNullException.ThrowIfNull(items);
 1010
 01011            if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
 1012            {
 01013                items = collectionManager.CollapseItemsWithinBoxSets(items, user);
 1014            }
 1015
 01016            return items;
 1017        }
 1018
 1019        private static bool CollapseBoxSetItems(
 1020            InternalItemsQuery query,
 1021            BaseItem queryParent,
 1022            User user,
 1023            IServerConfigurationManager configurationManager)
 1024        {
 1025            // Could end up stuck in a loop like this
 181026            if (queryParent is BoxSet)
 1027            {
 01028                return false;
 1029            }
 1030
 181031            if (queryParent is Season)
 1032            {
 01033                return false;
 1034            }
 1035
 181036            if (queryParent is MusicAlbum)
 1037            {
 01038                return false;
 1039            }
 1040
 181041            if (queryParent is MusicArtist)
 1042            {
 01043                return false;
 1044            }
 1045
 181046            var param = query.CollapseBoxSetItems;
 1047
 181048            if (!param.HasValue)
 1049            {
 01050                if (user is not null && query.IncludeItemTypes.Any(type =>
 01051                    (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollectio
 01052                    (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollectio
 1053                {
 01054                    return false;
 1055                }
 1056
 01057                if (query.IncludeItemTypes.Length == 0
 01058                    || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series))
 1059                {
 01060                    param = true;
 1061                }
 1062            }
 1063
 181064            return param.HasValue && param.Value && AllowBoxSetCollapsing(query);
 1065        }
 1066
 1067        private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
 1068        {
 01069            if (request.IsFavorite.HasValue)
 1070            {
 01071                return false;
 1072            }
 1073
 01074            if (request.IsFavoriteOrLiked.HasValue)
 1075            {
 01076                return false;
 1077            }
 1078
 01079            if (request.IsLiked.HasValue)
 1080            {
 01081                return false;
 1082            }
 1083
 01084            if (request.IsPlayed.HasValue)
 1085            {
 01086                return false;
 1087            }
 1088
 01089            if (request.IsResumable.HasValue)
 1090            {
 01091                return false;
 1092            }
 1093
 01094            if (request.IsFolder.HasValue)
 1095            {
 01096                return false;
 1097            }
 1098
 01099            if (request.Genres.Count > 0)
 1100            {
 01101                return false;
 1102            }
 1103
 01104            if (request.GenreIds.Count > 0)
 1105            {
 01106                return false;
 1107            }
 1108
 01109            if (request.HasImdbId.HasValue)
 1110            {
 01111                return false;
 1112            }
 1113
 01114            if (request.HasOfficialRating.HasValue)
 1115            {
 01116                return false;
 1117            }
 1118
 01119            if (request.HasOverview.HasValue)
 1120            {
 01121                return false;
 1122            }
 1123
 01124            if (request.HasParentalRating.HasValue)
 1125            {
 01126                return false;
 1127            }
 1128
 01129            if (request.HasSpecialFeature.HasValue)
 1130            {
 01131                return false;
 1132            }
 1133
 01134            if (request.HasSubtitles.HasValue)
 1135            {
 01136                return false;
 1137            }
 1138
 01139            if (request.HasThemeSong.HasValue)
 1140            {
 01141                return false;
 1142            }
 1143
 01144            if (request.HasThemeVideo.HasValue)
 1145            {
 01146                return false;
 1147            }
 1148
 01149            if (request.HasTmdbId.HasValue)
 1150            {
 01151                return false;
 1152            }
 1153
 01154            if (request.HasTrailer.HasValue)
 1155            {
 01156                return false;
 1157            }
 1158
 01159            if (request.ImageTypes.Length > 0)
 1160            {
 01161                return false;
 1162            }
 1163
 01164            if (request.Is3D.HasValue)
 1165            {
 01166                return false;
 1167            }
 1168
 01169            if (request.Is4K.HasValue)
 1170            {
 01171                return false;
 1172            }
 1173
 01174            if (request.IsHD.HasValue)
 1175            {
 01176                return false;
 1177            }
 1178
 01179            if (request.IsLocked.HasValue)
 1180            {
 01181                return false;
 1182            }
 1183
 01184            if (request.IsPlaceHolder.HasValue)
 1185            {
 01186                return false;
 1187            }
 1188
 01189            if (!string.IsNullOrWhiteSpace(request.Person))
 1190            {
 01191                return false;
 1192            }
 1193
 01194            if (request.PersonIds.Length > 0)
 1195            {
 01196                return false;
 1197            }
 1198
 01199            if (request.ItemIds.Length > 0)
 1200            {
 01201                return false;
 1202            }
 1203
 01204            if (request.StudioIds.Length > 0)
 1205            {
 01206                return false;
 1207            }
 1208
 01209            if (request.VideoTypes.Length > 0)
 1210            {
 01211                return false;
 1212            }
 1213
 01214            if (request.Years.Length > 0)
 1215            {
 01216                return false;
 1217            }
 1218
 01219            if (request.Tags.Length > 0)
 1220            {
 01221                return false;
 1222            }
 1223
 01224            if (request.OfficialRatings.Length > 0)
 1225            {
 01226                return false;
 1227            }
 1228
 01229            if (request.MinIndexNumber.HasValue)
 1230            {
 01231                return false;
 1232            }
 1233
 01234            if (request.OrderBy.Any(o =>
 01235                o.OrderBy == ItemSortBy.CommunityRating ||
 01236                o.OrderBy == ItemSortBy.CriticRating ||
 01237                o.OrderBy == ItemSortBy.Runtime))
 1238            {
 01239                return false;
 1240            }
 1241
 01242            return true;
 1243        }
 1244
 1245        public IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren)
 1246        {
 101247            ArgumentNullException.ThrowIfNull(user);
 1248
 101249            return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user));
 1250        }
 1251
 1252        public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery que
 1253        {
 101254            ArgumentNullException.ThrowIfNull(user);
 1255
 1256            // the true root should return our users root folder children
 101257            if (IsPhysicalRoot)
 1258            {
 01259                return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren);
 1260            }
 1261
 101262            var result = new Dictionary<Guid, BaseItem>();
 1263
 101264            AddChildren(user, includeLinkedChildren, result, false, query);
 1265
 101266            return result.Values.ToArray();
 1267        }
 1268
 1269        protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
 1270        {
 101271            return Children;
 1272        }
 1273
 1274        /// <summary>
 1275        /// Adds the children to list.
 1276        /// </summary>
 1277        private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursiv
 1278        {
 1279            // Prevent infinite recursion of nested folders
 101280            visitedFolders ??= new HashSet<Folder>();
 101281            if (!visitedFolders.Add(this))
 1282            {
 01283                return;
 1284            }
 1285
 1286            // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in mus
 101287            IEnumerable<BaseItem> children = null;
 101288            if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
 1289            {
 01290                children = Children;
 01291                query = null;
 1292            }
 1293
 1294            // If there are not sub-folders, proceed as normal.
 101295            if (children is null)
 1296            {
 101297                children = GetEligibleChildrenForRecursiveChildren(user);
 1298            }
 1299
 101300            AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
 1301
 101302            if (includeLinkedChildren)
 1303            {
 101304                AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query
 1305            }
 101306        }
 1307
 1308        private void AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Di
 1309        {
 601310            foreach (var child in children)
 1311            {
 101312                if (!child.IsVisible(user))
 1313                {
 1314                    continue;
 1315                }
 1316
 101317                if (query is null || UserViewBuilder.FilterItem(child, query))
 1318                {
 101319                    result[child.Id] = child;
 1320                }
 1321
 101322                if (recursive && child.IsFolder)
 1323                {
 01324                    var folder = (Folder)child;
 1325
 01326                    folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
 1327                }
 1328            }
 201329        }
 1330
 1331        public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
 1332        {
 01333            ArgumentNullException.ThrowIfNull(user);
 1334
 01335            var result = new Dictionary<Guid, BaseItem>();
 1336
 01337            AddChildren(user, true, result, true, query);
 1338
 01339            return result.Values.ToArray();
 1340        }
 1341
 1342        /// <summary>
 1343        /// Gets the recursive children.
 1344        /// </summary>
 1345        /// <returns>IList{BaseItem}.</returns>
 1346        public IReadOnlyList<BaseItem> GetRecursiveChildren()
 1347        {
 01348            return GetRecursiveChildren(true);
 1349        }
 1350
 1351        public IReadOnlyList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
 1352        {
 21353            return GetRecursiveChildren(i => true, includeLinkedChildren);
 1354        }
 1355
 1356        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
 1357        {
 01358            return GetRecursiveChildren(filter, true);
 1359        }
 1360
 1361        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
 1362        {
 21363            var result = new Dictionary<Guid, BaseItem>();
 1364
 21365            AddChildrenToList(result, includeLinkedChildren, true, filter);
 1366
 21367            return result.Values.ToArray();
 1368        }
 1369
 1370        /// <summary>
 1371        /// Adds the children to list.
 1372        /// </summary>
 1373        private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Fu
 1374        {
 41375            foreach (var child in Children)
 1376            {
 01377                if (filter is null || filter(child))
 1378                {
 01379                    result[child.Id] = child;
 1380                }
 1381
 01382                if (recursive && child.IsFolder)
 1383                {
 01384                    var folder = (Folder)child;
 1385
 1386                    // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loo
 01387                    folder.AddChildrenToList(result, false, true, filter);
 1388                }
 1389            }
 1390
 21391            if (includeLinkedChildren)
 1392            {
 01393                foreach (var child in GetLinkedChildren())
 1394                {
 01395                    if (filter is null || filter(child))
 1396                    {
 01397                        result[child.Id] = child;
 1398                    }
 1399                }
 1400            }
 21401        }
 1402
 1403        /// <summary>
 1404        /// Gets the linked children.
 1405        /// </summary>
 1406        /// <returns>IEnumerable{BaseItem}.</returns>
 1407        public List<BaseItem> GetLinkedChildren()
 1408        {
 101409            var linkedChildren = LinkedChildren;
 101410            var list = new List<BaseItem>(linkedChildren.Length);
 1411
 201412            foreach (var i in linkedChildren)
 1413            {
 01414                var child = GetLinkedChild(i);
 1415
 01416                if (child is not null)
 1417                {
 01418                    list.Add(child);
 1419                }
 1420            }
 1421
 101422            return list;
 1423        }
 1424
 1425        public bool ContainsLinkedChildByItemId(Guid itemId)
 1426        {
 01427            var linkedChildren = LinkedChildren;
 01428            foreach (var i in linkedChildren)
 1429            {
 01430                if (i.ItemId.HasValue)
 1431                {
 01432                    if (i.ItemId.Value.Equals(itemId))
 1433                    {
 01434                        return true;
 1435                    }
 1436
 1437                    continue;
 1438                }
 1439
 01440                var child = GetLinkedChild(i);
 1441
 01442                if (child is not null && child.Id.Equals(itemId))
 1443                {
 01444                    return true;
 1445                }
 1446            }
 1447
 01448            return false;
 1449        }
 1450
 1451        public List<BaseItem> GetLinkedChildren(User user)
 1452        {
 101453            if (!FilterLinkedChildrenPerUser || user is null)
 1454            {
 101455                return GetLinkedChildren();
 1456            }
 1457
 01458            var linkedChildren = LinkedChildren;
 01459            var list = new List<BaseItem>(linkedChildren.Length);
 1460
 01461            if (linkedChildren.Length == 0)
 1462            {
 01463                return list;
 1464            }
 1465
 01466            var allUserRootChildren = LibraryManager.GetUserRootFolder()
 01467                .GetChildren(user, true)
 01468                .OfType<Folder>()
 01469                .ToList();
 1470
 01471            var collectionFolderIds = allUserRootChildren
 01472                .Select(i => i.Id)
 01473                .ToList();
 1474
 01475            foreach (var i in linkedChildren)
 1476            {
 01477                var child = GetLinkedChild(i);
 1478
 01479                if (child is null)
 1480                {
 1481                    continue;
 1482                }
 1483
 01484                var childOwner = child.GetOwner() ?? child;
 1485
 01486                if (child is not IItemByName)
 1487                {
 01488                    var childProtocol = childOwner.PathProtocol;
 01489                    if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File)
 1490                    {
 01491                        if (!childOwner.IsVisibleStandalone(user))
 1492                        {
 01493                            continue;
 1494                        }
 1495                    }
 1496                    else
 1497                    {
 01498                        var itemCollectionFolderIds =
 01499                            LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id);
 1500
 01501                        if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains))
 1502                        {
 1503                            continue;
 1504                        }
 1505                    }
 1506                }
 1507
 01508                list.Add(child);
 1509            }
 1510
 01511            return list;
 1512        }
 1513
 1514        /// <summary>
 1515        /// Gets the linked children.
 1516        /// </summary>
 1517        /// <returns>IEnumerable{BaseItem}.</returns>
 1518        public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
 1519        {
 01520            return LinkedChildren
 01521                .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i)))
 01522                .Where(i => i.Item2 is not null)
 01523                .ToArray();
 1524        }
 1525
 1526        protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystem
 1527        {
 1528            var changesFound = false;
 1529
 1530            if (IsFileProtocol)
 1531            {
 1532                if (RefreshLinkedChildren(fileSystemChildren))
 1533                {
 1534                    changesFound = true;
 1535                }
 1536            }
 1537
 1538            var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).Configur
 1539
 1540            return baseHasChanges || changesFound;
 1541        }
 1542
 1543        /// <summary>
 1544        /// Refreshes the linked children.
 1545        /// </summary>
 1546        /// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
 1547        /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
 1548        protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
 1549        {
 171550            if (SupportsShortcutChildren)
 1551            {
 171552                var newShortcutLinks = fileSystemChildren
 171553                    .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName))
 171554                    .Select(i =>
 171555                    {
 171556                        try
 171557                        {
 171558                            Logger.LogDebug("Found shortcut at {0}", i.FullName);
 171559
 171560                            var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShor
 171561
 171562                            if (!string.IsNullOrEmpty(resolvedPath))
 171563                            {
 171564                                return new LinkedChild
 171565                                {
 171566                                    Path = resolvedPath,
 171567                                    Type = LinkedChildType.Shortcut
 171568                                };
 171569                            }
 171570
 171571                            Logger.LogError("Error resolving shortcut {0}", i.FullName);
 171572
 171573                            return null;
 171574                        }
 171575                        catch (IOException ex)
 171576                        {
 171577                            Logger.LogError(ex, "Error resolving shortcut {0}", i.FullName);
 171578                            return null;
 171579                        }
 171580                    })
 171581                    .Where(i => i is not null)
 171582                    .ToList();
 1583
 171584                var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
 1585
 171586                if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem)))
 1587                {
 01588                    Logger.LogInformation("Shortcut links have changed for {0}", Path);
 1589
 01590                    newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual));
 01591                    LinkedChildren = newShortcutLinks.ToArray();
 01592                    return true;
 1593                }
 1594            }
 1595
 341596            foreach (var child in LinkedChildren)
 1597            {
 1598                // Reset the cached value
 01599                child.ItemId = null;
 1600            }
 1601
 171602            return false;
 1603        }
 1604
 1605        /// <summary>
 1606        /// Marks the played.
 1607        /// </summary>
 1608        /// <param name="user">The user.</param>
 1609        /// <param name="datePlayed">The date played.</param>
 1610        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1611        public override void MarkPlayed(
 1612            User user,
 1613            DateTime? datePlayed,
 1614            bool resetPosition)
 1615        {
 01616            var query = new InternalItemsQuery
 01617            {
 01618                User = user,
 01619                Recursive = true,
 01620                IsFolder = false,
 01621                EnableTotalRecordCount = false
 01622            };
 1623
 01624            if (!user.DisplayMissingEpisodes)
 1625            {
 01626                query.IsVirtualItem = false;
 1627            }
 1628
 01629            var itemsResult = GetItemList(query);
 1630
 1631            // Sweep through recursively and update status
 01632            foreach (var item in itemsResult)
 1633            {
 01634                if (item.IsVirtualItem)
 1635                {
 1636                    // The querying doesn't support virtual unaired
 01637                    var episode = item as Episode;
 01638                    if (episode is not null && episode.IsUnaired)
 1639                    {
 1640                        continue;
 1641                    }
 1642                }
 1643
 01644                item.MarkPlayed(user, datePlayed, resetPosition);
 1645            }
 01646        }
 1647
 1648        /// <summary>
 1649        /// Marks the unplayed.
 1650        /// </summary>
 1651        /// <param name="user">The user.</param>
 1652        public override void MarkUnplayed(User user)
 1653        {
 01654            var itemsResult = GetItemList(new InternalItemsQuery
 01655            {
 01656                User = user,
 01657                Recursive = true,
 01658                IsFolder = false,
 01659                EnableTotalRecordCount = false
 01660            });
 1661
 1662            // Sweep through recursively and update status
 01663            foreach (var item in itemsResult)
 1664            {
 01665                item.MarkUnplayed(user);
 1666            }
 01667        }
 1668
 1669        public override bool IsPlayed(User user)
 1670        {
 01671            var itemsResult = GetItemList(new InternalItemsQuery(user)
 01672            {
 01673                Recursive = true,
 01674                IsFolder = false,
 01675                IsVirtualItem = false,
 01676                EnableTotalRecordCount = false
 01677            });
 1678
 01679            return itemsResult
 01680                .All(i => i.IsPlayed(user));
 1681        }
 1682
 1683        public override bool IsUnplayed(User user)
 1684        {
 01685            return !IsPlayed(user);
 1686        }
 1687
 1688        public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User
 1689        {
 91690            if (!SupportsUserDataFromChildren)
 1691            {
 91692                return;
 1693            }
 1694
 01695            if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))
 1696            {
 01697                itemDto.RecursiveItemCount = GetRecursiveChildCount(user);
 1698            }
 1699
 01700            if (SupportsPlayedStatus)
 1701            {
 01702                var unplayedQueryResult = GetItems(new InternalItemsQuery(user)
 01703                {
 01704                    Recursive = true,
 01705                    IsFolder = false,
 01706                    IsVirtualItem = false,
 01707                    EnableTotalRecordCount = true,
 01708                    Limit = 0,
 01709                    IsPlayed = false,
 01710                    DtoOptions = new DtoOptions(false)
 01711                    {
 01712                        EnableImages = false
 01713                    }
 01714                }).TotalRecordCount;
 1715
 01716                dto.UnplayedItemCount = unplayedQueryResult;
 1717
 01718                if (itemDto?.RecursiveItemCount > 0)
 1719                {
 01720                    var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100;
 01721                    dto.PlayedPercentage = 100 - unplayedPercentage;
 01722                    dto.Played = dto.PlayedPercentage.Value >= 100;
 1723                }
 1724                else
 1725                {
 01726                    dto.Played = (dto.UnplayedItemCount ?? 0) == 0;
 1727                }
 1728            }
 01729        }
 1730
 1731        /// <summary>
 1732        /// Contains constants used when reporting scan progress.
 1733        /// </summary>
 1734        private static class ProgressHelpers
 1735        {
 1736            /// <summary>
 1737            /// Reported after the folders immediate children are retrieved.
 1738            /// </summary>
 1739            public const int RetrievedChildren = 5;
 1740
 1741            /// <summary>
 1742            /// Reported after add, updating, or deleting child items from the LibraryManager.
 1743            /// </summary>
 1744            public const int UpdatedChildItems = 10;
 1745
 1746            /// <summary>
 1747            /// Reported once subfolders are scanned.
 1748            /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders].
 1749            /// </summary>
 1750            public const int ScannedSubfolders = 50;
 1751
 1752            /// <summary>
 1753            /// Reported once metadata is refreshed.
 1754            /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed].
 1755            /// </summary>
 1756            public const int RefreshedMetadata = 100;
 1757
 1758            /// <summary>
 1759            /// Gets the current progress given the previous step, next step, and progress in between.
 1760            /// </summary>
 1761            /// <param name="previousProgressStep">The previous progress step.</param>
 1762            /// <param name="nextProgressStep">The next progress step.</param>
 1763            /// <param name="currentProgress">The current progress step.</param>
 1764            /// <returns>The progress.</returns>
 1765            public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress)
 1766            {
 641767                return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100));
 1768            }
 1769        }
 1770    }
 1771}

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.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,System.Boolean)
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)
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)
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)
IsUnplayed(Jellyfin.Database.Implementations.Entities.User)
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)