< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.Entities.Folder
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/Entities/Folder.cs
Line coverage
38%
Covered lines: 304
Uncovered lines: 496
Coverable lines: 800
Total lines: 2019
Line coverage: 38%
Branch coverage
28%
Covered branches: 146
Total branches: 508
Branch coverage: 28.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 32% (188/587) Branch coverage: 26% (102/392) Total lines: 18652/15/2026 - 12:13:43 AM Line coverage: 32% (188/587) Branch coverage: 26% (102/392) Total lines: 18814/19/2026 - 12:14:27 AM Line coverage: 40.7% (299/734) Branch coverage: 32.3% (148/458) Total lines: 18815/4/2026 - 12:15:16 AM Line coverage: 38.6% (301/779) Branch coverage: 29.7% (145/488) Total lines: 19635/5/2026 - 12:15:44 AM Line coverage: 38% (304/800) Branch coverage: 28.7% (146/508) Total lines: 2019 1/23/2026 - 12:11:06 AM Line coverage: 32% (188/587) Branch coverage: 26% (102/392) Total lines: 18652/15/2026 - 12:13:43 AM Line coverage: 32% (188/587) Branch coverage: 26% (102/392) Total lines: 18814/19/2026 - 12:14:27 AM Line coverage: 40.7% (299/734) Branch coverage: 32.3% (148/458) Total lines: 18815/4/2026 - 12:15:16 AM Line coverage: 38.6% (301/779) Branch coverage: 29.7% (145/488) Total lines: 19635/5/2026 - 12:15:44 AM Line coverage: 38% (304/800) Branch coverage: 28.7% (146/508) Total lines: 2019

Coverage delta

Coverage delta 9 -9

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
get_SupportsThemeMedia()100%210%
get_IsPreSorted()100%210%
get_IsPhysicalRoot()100%11100%
get_SupportsInheritedParentImages()100%210%
get_SupportsPlayedStatus()100%210%
get_IsFolder()100%11100%
get_IsDisplayedAsFolder()100%210%
get_SupportsCumulativeRunTimeTicks()100%11100%
get_SupportsDateLastMediaAdded()100%11100%
get_FileNameWithoutExtension()50%2266.66%
get_Children()100%22100%
set_Children(...)100%11100%
get_RecursiveChildren()100%210%
get_SupportsShortcutChildren()100%11100%
get_FilterLinkedChildrenPerUser()100%11100%
get_SupportsOwnedItems()100%22100%
get_SupportsUserDataFromChildren()25%841635.71%
CanDelete()50%2266.66%
RequiresRefresh()50%4475%
AddChild(...)0%4260%
IsVisible(...)50%801222.22%
LoadChildren()100%11100%
GetRefreshProgress()100%210%
ValidateChildren(...)100%210%
ValidateChildren(...)100%11100%
GetActualChildrenDictionary()37.5%10866.66%
ValidateChildrenInternal()83.33%6687.5%
IsLibraryFolderAccessible(...)80%101083.33%
ValidateChildrenInternal2()47.22%182210847.23%
RefreshMetadataRecursive()100%11100%
RefreshAllMetadataForContainer()0%620%
RefreshChildMetadata()62.5%11862.5%
ValidateSubFolders()100%11100%
RunTasks()100%11100%
GetNonCachedChildren(...)100%11100%
GetCachedChildren()100%11100%
GetChildCount(...)0%2040%
GetRecursiveChildCount(...)100%210%
QueryRecursive(...)78.57%191470%
QueryWithPostFiltering(...)62.5%9877.27%
SortItemsByRequest(...)100%210%
GetItems(...)0%4260%
GetItemList(...)16.66%13642.85%
GetItemsInternal(...)33.33%32610.71%
PostFilterAndSort(...)0%4260%
ApplyNameFilter(...)0%4260%
CollapseBoxSetItemsIfNeeded(...)0%506220%
CollapseBoxSetItems(...)25%1822435%
SetCollapseBoxSetItemTypes(...)0%4260%
AllowBoxSetCollapsing(...)0%4692680%
GetChildren(...)75%4487.5%
GetChildren(...)100%11100%
GetEligibleChildrenForRecursiveChildren(...)100%11100%
AddChildren(...)71.42%171475%
AddChildrenFromCollection(...)57.14%151480.95%
GetRecursiveChildren(...)100%210%
GetRecursiveChildren()100%210%
GetRecursiveChildren(...)100%210%
GetRecursiveChildren(...)100%210%
GetRecursiveChildren(...)100%210%
AddChildrenToList(...)0%342180%
GetLinkedChildren()50%2280%
ContainsLinkedChildByItemId(...)0%110100%
GetLinkedChildren(...)9.09%406227.4%
GetLinkedChildrenInfos()100%210%
ResolveLinkedChildren(...)4.54%3462212.5%
RefreshedOwnedItems()75%4485.71%
RefreshLinkedChildren(...)75%4490.24%
MarkPlayed(...)0%110100%
MarkUnplayed(...)0%620%
IsPlayed(...)100%210%
IsUnplayed(...)100%210%
FillUserDataDtoValues(...)5%3072010.52%
GetProgress(...)100%11100%

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CA1002, CA1721, CA1819, CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Collections.Immutable;
 8using System.IO;
 9using System.Linq;
 10using System.Security;
 11using System.Text.Json.Serialization;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using J2N.Collections.Generic.Extensions;
 15using Jellyfin.Data;
 16using Jellyfin.Data.Enums;
 17using Jellyfin.Database.Implementations.Entities;
 18using Jellyfin.Database.Implementations.Enums;
 19using Jellyfin.Extensions;
 20using MediaBrowser.Controller.Channels;
 21using MediaBrowser.Controller.Collections;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Dto;
 24using MediaBrowser.Controller.Entities.Audio;
 25using MediaBrowser.Controller.Entities.Movies;
 26using MediaBrowser.Controller.Library;
 27using MediaBrowser.Controller.LibraryTaskScheduler;
 28using MediaBrowser.Controller.Providers;
 29using MediaBrowser.Model.Dto;
 30using MediaBrowser.Model.IO;
 31using MediaBrowser.Model.Querying;
 32using Microsoft.Extensions.Logging;
 33using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 34using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 35using Season = MediaBrowser.Controller.Entities.TV.Season;
 36using Series = MediaBrowser.Controller.Entities.TV.Series;
 37
 38namespace MediaBrowser.Controller.Entities
 39{
 40    /// <summary>
 41    /// Class Folder.
 42    /// </summary>
 43    public class Folder : BaseItem
 44    {
 45        private IEnumerable<BaseItem> _children;
 46
 36547        public Folder()
 48        {
 36549            LinkedChildren = Array.Empty<LinkedChild>();
 36550        }
 51
 52        public static IUserViewManager UserViewManager { get; set; }
 53
 54        public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get; set; }
 55
 56        /// <summary>
 57        /// Gets or sets a value indicating whether this instance is root.
 58        /// </summary>
 59        /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
 60        public bool IsRoot { get; set; }
 61
 62        /// <summary>
 63        /// Gets or sets the linked children.
 64        /// </summary>
 65        [JsonIgnore]
 66        public LinkedChild[] LinkedChildren { get; set; }
 67
 68        [JsonIgnore]
 69        public DateTime? DateLastMediaAdded { get; set; }
 70
 71        [JsonIgnore]
 072        public override bool SupportsThemeMedia => true;
 73
 74        [JsonIgnore]
 075        public virtual bool IsPreSorted => false;
 76
 77        [JsonIgnore]
 1078        public virtual bool IsPhysicalRoot => false;
 79
 80        [JsonIgnore]
 081        public override bool SupportsInheritedParentImages => true;
 82
 83        [JsonIgnore]
 084        public override bool SupportsPlayedStatus => true;
 85
 86        /// <summary>
 87        /// Gets a value indicating whether this instance is folder.
 88        /// </summary>
 89        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 90        [JsonIgnore]
 91491        public override bool IsFolder => true;
 92
 93        [JsonIgnore]
 094        public override bool IsDisplayedAsFolder => true;
 95
 96        [JsonIgnore]
 9197        public virtual bool SupportsCumulativeRunTimeTicks => false;
 98
 99        [JsonIgnore]
 57100        public virtual bool SupportsDateLastMediaAdded => false;
 101
 102        [JsonIgnore]
 103        public override string FileNameWithoutExtension
 104        {
 105            get
 106            {
 313107                if (IsFileProtocol)
 108                {
 313109                    return System.IO.Path.GetFileName(Path);
 110                }
 111
 0112                return null;
 113            }
 114        }
 115
 116        /// <summary>
 117        /// Gets or Sets the actual children.
 118        /// </summary>
 119        /// <value>The actual children.</value>
 120        [JsonIgnore]
 121        public virtual IEnumerable<BaseItem> Children
 122        {
 1056123            get => _children ??= LoadChildren();
 229124            set => _children = value;
 125        }
 126
 127        /// <summary>
 128        /// Gets thread-safe access to all recursive children of this folder - without regard to user.
 129        /// </summary>
 130        /// <value>The recursive children.</value>
 131        [JsonIgnore]
 0132        public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
 133
 134        [JsonIgnore]
 22135        protected virtual bool SupportsShortcutChildren => false;
 136
 10137        protected virtual bool FilterLinkedChildrenPerUser => false;
 138
 139        [JsonIgnore]
 92140        protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
 141
 142        [JsonIgnore]
 143        public virtual bool SupportsUserDataFromChildren
 144        {
 145            get
 146            {
 147                // These are just far too slow.
 12148                if (this is ICollectionFolder)
 149                {
 6150                    return false;
 151                }
 152
 6153                if (this is UserView)
 154                {
 0155                    return false;
 156                }
 157
 6158                if (this is UserRootFolder)
 159                {
 6160                    return false;
 161                }
 162
 0163                if (this is Channel)
 164                {
 0165                    return false;
 166                }
 167
 0168                if (SourceType != SourceType.Library)
 169                {
 0170                    return false;
 171                }
 172
 0173                if (this is IItemByName)
 174                {
 0175                    if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName)
 176                    {
 0177                        return false;
 178                    }
 179                }
 180
 0181                return true;
 182            }
 183        }
 184
 185        public static ICollectionManager CollectionManager { get; set; }
 186
 187        public override bool CanDelete()
 188        {
 6189            if (IsRoot)
 190            {
 6191                return false;
 192            }
 193
 0194            return base.CanDelete();
 195        }
 196
 197        public override bool RequiresRefresh()
 198        {
 57199            var baseResult = base.RequiresRefresh();
 200
 57201            if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue)
 202            {
 0203                baseResult = true;
 204            }
 205
 57206            return baseResult;
 207        }
 208
 209        /// <summary>
 210        /// Adds the child.
 211        /// </summary>
 212        /// <param name="item">The item.</param>
 213        /// <exception cref="InvalidOperationException">Unable to add  + item.Name.</exception>
 214        public void AddChild(BaseItem item)
 215        {
 0216            item.SetParent(this);
 217
 0218            if (item.Id.IsEmpty())
 219            {
 0220                item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
 221            }
 222
 0223            if (item.DateCreated == DateTime.MinValue)
 224            {
 0225                item.DateCreated = DateTime.UtcNow;
 226            }
 227
 0228            if (item.DateModified == DateTime.MinValue)
 229            {
 0230                item.DateModified = DateTime.UtcNow;
 231            }
 232
 0233            LibraryManager.CreateItem(item, this);
 0234        }
 235
 236        public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 237        {
 10238            if (this is ICollectionFolder && this is not BasePluginFolder)
 239            {
 0240                var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 0241                if (blockedMediaFolders.Length > 0)
 242                {
 0243                    if (blockedMediaFolders.Contains(Id))
 244                    {
 0245                        return false;
 246                    }
 247                }
 248                else
 249                {
 0250                    if (!user.HasPermission(PermissionKind.EnableAllFolders)
 0251                        && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id))
 252                    {
 0253                        return false;
 254                    }
 255                }
 256            }
 257
 10258            return base.IsVisible(user, skipAllowedTagsCheck);
 259        }
 260
 261        /// <summary>
 262        /// Loads our children.  Validation will occur externally.
 263        /// We want this synchronous.
 264        /// </summary>
 265        /// <returns>Returns children.</returns>
 266        protected virtual IReadOnlyList<BaseItem> LoadChildren()
 267        {
 268            // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
 269            // just load our children from the repo - the library will be validated and maintained in other processes
 144270            return GetCachedChildren();
 271        }
 272
 273        public override double? GetRefreshProgress()
 274        {
 0275            return ProviderManager.GetRefreshProgress(Id);
 276        }
 277
 278        public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
 279        {
 0280            return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellation
 281        }
 282
 283        /// <summary>
 284        /// Validates that the children of the folder still exist.
 285        /// </summary>
 286        /// <param name="progress">The progress.</param>
 287        /// <param name="metadataRefreshOptions">The metadata refresh options.</param>
 288        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 289        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 290        /// <param name="cancellationToken">The cancellation token.</param>
 291        /// <returns>Task.</returns>
 292        public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool rec
 293        {
 59294            Children = null; // invalidate cached children.
 59295            return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadata
 296        }
 297
 298        private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
 299        {
 58300            var dictionary = new Dictionary<Guid, BaseItem>();
 301
 58302            Children = null; // invalidate cached children.
 58303            var childrenList = Children.ToList();
 304
 204305            foreach (var child in childrenList)
 306            {
 44307                var id = child.Id;
 44308                if (dictionary.ContainsKey(id))
 309                {
 0310                    Logger.LogError(
 0311                        "Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}",
 0312                        Path ?? Name,
 0313                        child.Path ?? child.Name);
 314                }
 315                else
 316                {
 44317                    dictionary[id] = child;
 318                }
 319            }
 320
 58321            return dictionary;
 322        }
 323
 324        /// <summary>
 325        /// Validates the children internal.
 326        /// </summary>
 327        /// <param name="progress">The progress.</param>
 328        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 329        /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
 330        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 331        /// <param name="refreshOptions">The refresh options.</param>
 332        /// <param name="directoryService">The directory service.</param>
 333        /// <param name="cancellationToken">The cancellation token.</param>
 334        /// <returns>Task.</returns>
 335        protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshCh
 336        {
 59337            if (recursive)
 338            {
 17339                ProviderManager.OnRefreshStart(this);
 340            }
 341
 342            try
 343            {
 344                if (GetParents().Any(f => f.Id.Equals(Id)))
 345                {
 0346                    throw new InvalidOperationException("Recursive datastructure detected abort processing this item.");
 347                }
 348
 59349                await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptio
 57350            }
 351            finally
 352            {
 59353                if (recursive)
 354                {
 17355                    ProviderManager.OnRefreshComplete(this);
 356                }
 357            }
 57358        }
 359
 360        private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item, bool checkColle
 361        {
 104362            if (!checkCollection && (item is BoxSet || string.Equals(item.FileNameWithoutExtension, "collections", Strin
 363            {
 0364                return true;
 365            }
 366
 367            // For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
 104368            if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
 369            {
 38370                Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.Containi
 38371                return false;
 372            }
 373
 66374            return true;
 375        }
 376
 377        private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetada
 378        {
 59379            if (!IsLibraryFolderAccessible(directoryService, this, allowRemoveRoot))
 380            {
 0381                return;
 382            }
 383
 59384            cancellationToken.ThrowIfCancellationRequested();
 385
 58386            var validChildren = new List<BaseItem>();
 58387            var validChildrenNeedGeneration = false;
 388
 58389            if (IsFileProtocol)
 390            {
 58391                IEnumerable<BaseItem> nonCachedChildren = [];
 392
 393                try
 394                {
 58395                    nonCachedChildren = GetNonCachedChildren(directoryService);
 58396                }
 0397                catch (IOException ex)
 398                {
 0399                    Logger.LogError(ex, "Error retrieving children from file system");
 0400                }
 0401                catch (SecurityException ex)
 402                {
 0403                    Logger.LogError(ex, "Error retrieving children from file system");
 0404                }
 0405                catch (Exception ex)
 406                {
 0407                    Logger.LogError(ex, "Error retrieving children");
 0408                    return;
 409                }
 410
 58411                progress.Report(ProgressHelpers.RetrievedChildren);
 412
 58413                if (recursive)
 414                {
 17415                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren);
 416                }
 417
 418                // Build a dictionary of the current children we have now by Id so we can compare quickly and easily
 58419                var currentChildren = GetActualChildrenDictionary();
 420
 421                // Create a list for our validated children
 58422                var newItems = new List<BaseItem>();
 58423                var actuallyRemoved = new List<BaseItem>();
 424
 425                // Build a reverse path→item lookup for detecting type changes
 58426                var currentChildrenByPath = new Dictionary<string, BaseItem>(StringComparer.OrdinalIgnoreCase);
 204427                foreach (var kvp in currentChildren)
 428                {
 44429                    if (!string.IsNullOrEmpty(kvp.Value.Path))
 430                    {
 44431                        currentChildrenByPath.TryAdd(kvp.Value.Path, kvp.Value);
 432                    }
 433                }
 434
 58435                cancellationToken.ThrowIfCancellationRequested();
 436
 206437                foreach (var child in nonCachedChildren)
 438                {
 45439                    if (!IsLibraryFolderAccessible(directoryService, child, allowRemoveRoot))
 440                    {
 441                        continue;
 442                    }
 443
 7444                    if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild))
 445                    {
 5446                        validChildren.Add(currentChild);
 447
 5448                        if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
 449                        {
 0450                            await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken)
 451                        }
 452                        else
 453                        {
 454                            // metadata is up-to-date; make sure DB has correct images dimensions and hash
 5455                            await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false);
 456                        }
 457
 5458                        continue;
 459                    }
 460
 461                    // Check if an existing item occupies the same path with different type/ID
 2462                    if (!string.IsNullOrEmpty(child.Path)
 2463                        && currentChildrenByPath.TryGetValue(child.Path, out var staleItem)
 2464                        && !staleItem.Id.Equals(child.Id))
 465                    {
 0466                        Logger.LogInformation(
 0467                            "Item type changed at {Path}: {OldType} -> {NewType}, removing stale entry",
 0468                            child.Path,
 0469                            staleItem.GetType().Name,
 0470                            child.GetType().Name);
 471
 0472                        currentChildren.Remove(staleItem.Id);
 0473                        currentChildrenByPath.Remove(child.Path);
 0474                        staleItem.SetParent(null);
 0475                        LibraryManager.DeleteItem(staleItem, new DeleteOptions { DeleteFileLocation = false }, this, fal
 0476                        actuallyRemoved.Add(staleItem);
 477                    }
 478
 479                    // Brand new item - needs to be added
 2480                    child.SetParent(this);
 2481                    newItems.Add(child);
 2482                    validChildren.Add(child);
 483                }
 484
 485                // That's all the new and changed ones - now see if any have been removed and need cleanup
 58486                var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
 58487                var shouldRemove = !IsRoot || allowRemoveRoot;
 488                // If it's an AggregateFolder, don't remove
 489                // Collect replaced primaries for deferred deletion (after CreateItems)
 58490                var replacedPrimaries = new List<(Video OldPrimary, Video NewPrimary)>();
 491
 492                // Build a set of paths that are alternate versions of valid children
 493                // These items should not be deleted - they're managed by their primary video
 58494                var alternateVersionPaths = validChildren
 58495                    .OfType<Video>()
 58496                    .SelectMany(v => v.LocalAlternateVersions ?? [])
 58497                    .Where(p => !string.IsNullOrEmpty(p))
 58498                    .ToHashSet(StringComparer.OrdinalIgnoreCase);
 499
 58500                if (shouldRemove && itemsRemoved.Count > 0)
 501                {
 8502                    foreach (var item in itemsRemoved)
 503                    {
 2504                        if (!item.CanDelete())
 505                        {
 2506                            Logger.LogDebug("Item marked as non-removable, skipping: {Path}", item.Path ?? item.Name);
 2507                            continue;
 508                        }
 509
 510                        // Skip items that are alternate versions of another video
 0511                        if (item is Video video)
 512                        {
 513                            // Check if path is in LocalAlternateVersions of any valid child
 0514                            if (!string.IsNullOrEmpty(item.Path) && alternateVersionPaths.Contains(item.Path))
 515                            {
 0516                                Logger.LogDebug("Item path matches an alternate version, skipping deletion: {Path}", ite
 0517                                continue;
 518                            }
 519                        }
 520
 521                        // Defer deletion if this primary video is being replaced by a new primary
 522                        // that takes over its alternates. Deleting now would trigger premature
 523                        // promotion inside DeleteItem and write stale paths to collection NFOs.
 0524                        if (item is Video primaryVideo
 0525                            && !primaryVideo.PrimaryVersionId.HasValue
 0526                            && primaryVideo.OwnerId.IsEmpty()
 0527                            && (primaryVideo.LocalAlternateVersions ?? []).Any(p => alternateVersionPaths.Contains(p)))
 528                        {
 0529                            var newPrimary = newItems
 0530                                .OfType<Video>()
 0531                                .FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
 0532                                    .Any(p => (primaryVideo.LocalAlternateVersions ?? [])
 0533                                        .Any(op => string.Equals(op, p, StringComparison.OrdinalIgnoreCase))));
 0534                            if (newPrimary is not null)
 535                            {
 0536                                Logger.LogDebug("Deferring deletion of replaced primary: {Path}", item.Path);
 0537                                replacedPrimaries.Add((primaryVideo, newPrimary));
 0538                                actuallyRemoved.Add(item);
 0539                                item.SetParent(null);
 0540                                continue;
 541                            }
 542                        }
 543
 0544                        if (item.IsFileProtocol)
 545                        {
 0546                            Logger.LogDebug("Removed item: {Path}", item.Path);
 547
 0548                            actuallyRemoved.Add(item);
 0549                            item.SetParent(null);
 0550                            LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, fals
 551                        }
 552                    }
 553                }
 554
 58555                if (newItems.Count > 0)
 556                {
 2557                    LibraryManager.CreateItems(newItems, this, cancellationToken);
 558                }
 559
 560                // Process deferred replaced-primary deletions now that new primaries exist in DB/cache.
 561                // This avoids the premature promotion that would occur if DeleteItem ran before CreateItems.
 116562                foreach (var (oldPrimary, newPrimary) in replacedPrimaries)
 563                {
 0564                    Logger.LogInformation(
 0565                        "Processing deferred deletion of replaced primary {OldName} ({OldId}), new primary {NewName} ({N
 0566                        oldPrimary.Name,
 0567                        oldPrimary.Id,
 0568                        newPrimary.Name,
 0569                        newPrimary.Id);
 570
 571                    // Reroute collection/playlist references from old primary to new primary
 0572                    await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(
 573
 574                    // Transfer alternates from old primary to new primary
 0575                    var localAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary).ToHashSet();
 0576                    var allAlternateIds = localAlternateIds
 0577                        .Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
 0578                        .Distinct()
 0579                        .ToList();
 580
 0581                    foreach (var altId in allAlternateIds)
 582                    {
 0583                        if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
 584                        {
 0585                            altVideo.SetPrimaryVersionId(newPrimary.Id);
 0586                            altVideo.OwnerId = localAlternateIds.Contains(altVideo.Id) ? newPrimary.Id : Guid.Empty;
 0587                            await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).Confi
 588                        }
 589                    }
 590
 591                    // Clear alternate arrays so DeleteItem won't trigger promotion
 0592                    oldPrimary.LocalAlternateVersions = [];
 0593                    oldPrimary.LinkedAlternateVersions = [];
 594
 595                    // Safe to delete now — no promotion will happen
 0596                    LibraryManager.DeleteItem(oldPrimary, new DeleteOptions { DeleteFileLocation = false }, this, false)
 0597                }
 598
 599                // Demote old primaries that are now alternate versions of newly created primaries.
 600                // This handles the case where a new file is added that becomes the new primary
 601                // (e.g. movie-2 added, movie-3 was primary → movie-3 needs demotion).
 602                // Items in replacedPrimaries are excluded (already in actuallyRemoved).
 58603                var oldPrimariesToDemote = new List<(Video OldPrimary, Video NewPrimary)>();
 194604                foreach (var item in itemsRemoved.Except(actuallyRemoved))
 605                {
 39606                    if (item is Video video
 39607                        && video.OwnerId.IsEmpty()
 39608                        && !string.IsNullOrEmpty(item.Path)
 39609                        && alternateVersionPaths.Contains(item.Path))
 610                    {
 0611                        var newPrimary = newItems
 0612                            .OfType<Video>()
 0613                            .FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
 0614                                .Any(p => string.Equals(p, item.Path, StringComparison.OrdinalIgnoreCase)));
 0615                        if (newPrimary is not null)
 616                        {
 0617                            oldPrimariesToDemote.Add((video, newPrimary));
 618                        }
 619                    }
 620                }
 621
 116622                foreach (var (oldPrimary, newPrimary) in oldPrimariesToDemote)
 623                {
 0624                    Logger.LogInformation(
 0625                        "Demoting old primary {OldName} ({OldId}) to alternate of new primary {NewName} ({NewId})",
 0626                        oldPrimary.Name,
 0627                        oldPrimary.Id,
 0628                        newPrimary.Name,
 0629                        newPrimary.Id);
 630
 631                    // First: update old primary's alternate items to point to new primary.
 632                    // Order matters — update alternates FIRST so they don't get orphan-deleted
 633                    // when old primary's arrays are cleared.
 0634                    var oldAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary)
 0635                        .Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
 0636                        .Distinct()
 0637                        .ToList();
 638
 0639                    foreach (var altId in oldAlternateIds)
 640                    {
 0641                        if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
 642                        {
 0643                            altVideo.SetPrimaryVersionId(newPrimary.Id);
 0644                            altVideo.OwnerId = newPrimary.Id;
 0645                            await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).Confi
 646                        }
 647                    }
 648
 649                    // Then: demote old primary — clear its arrays and set it as alternate of new primary
 0650                    oldPrimary.LocalAlternateVersions = [];
 0651                    oldPrimary.LinkedAlternateVersions = [];
 0652                    oldPrimary.SetPrimaryVersionId(newPrimary.Id);
 0653                    oldPrimary.OwnerId = newPrimary.Id;
 0654                    await oldPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAw
 655
 656                    // Re-route playlist/collection references from old primary to new primary
 0657                    await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(
 0658                }
 659
 660                // After removing items, reattach any detached user data to remaining children
 661                // that share the same user data keys (eg. same episode replaced with a new file).
 58662                if (actuallyRemoved.Count > 0)
 663                {
 0664                    var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet();
 0665                    foreach (var child in validChildren)
 666                    {
 0667                        if (child.GetUserDataKeys().Any(removedKeys.Contains))
 668                        {
 0669                            await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
 670                        }
 671                    }
 0672                }
 58673            }
 674            else
 675            {
 0676                validChildrenNeedGeneration = true;
 677            }
 678
 58679            progress.Report(ProgressHelpers.UpdatedChildItems);
 680
 58681            if (recursive)
 682            {
 17683                ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems);
 684            }
 685
 58686            cancellationToken.ThrowIfCancellationRequested();
 687
 57688            if (recursive)
 689            {
 16690                var folder = this;
 16691                var innerProgress = new Progress<double>(innerPercent =>
 16692                {
 16693                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.Scanned
 16694
 16695                    progress.Report(percent);
 16696
 16697                    ProviderManager.OnRefreshProgress(folder, percent);
 16698                });
 699
 16700                if (validChildrenNeedGeneration)
 701                {
 0702                    validChildren = Children.ToList();
 0703                    validChildrenNeedGeneration = false;
 704                }
 705
 16706                await ValidateSubFolders(validChildren.OfType<Folder>().ToList(), directoryService, innerProgress, cance
 707            }
 708
 57709            if (refreshChildMetadata)
 710            {
 57711                progress.Report(ProgressHelpers.ScannedSubfolders);
 712
 57713                if (recursive)
 714                {
 16715                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders);
 716                }
 717
 57718                var container = this as IMetadataContainer;
 719
 57720                var folder = this;
 57721                var innerProgress = new Progress<double>(innerPercent =>
 57722                {
 57723                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.Refresh
 57724
 57725                    progress.Report(percent);
 57726
 57727                    if (recursive)
 57728                    {
 57729                        ProviderManager.OnRefreshProgress(folder, percent);
 57730                    }
 57731                });
 732
 57733                if (container is not null)
 734                {
 0735                    await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).Co
 736                }
 737                else
 738                {
 57739                    if (validChildrenNeedGeneration)
 740                    {
 0741                        Children = null; // invalidate cached children.
 0742                        validChildren = Children.ToList();
 743                    }
 744
 57745                    await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellation
 746                }
 747            }
 57748        }
 749
 750        private async Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, boo
 751        {
 57752            await RunTasks(
 57753                (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFold
 57754                children,
 57755                progress,
 57756                cancellationToken).ConfigureAwait(false);
 57757        }
 758
 759        private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOp
 760        {
 0761            if (container is Series series)
 762            {
 0763                await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 764            }
 765
 0766            await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
 0767        }
 768
 769        private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, I
 770        {
 7771            if (child is IMetadataContainer container)
 772            {
 0773                await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAw
 774            }
 775            else
 776            {
 7777                if (refreshOptions.RefreshItem(child))
 778                {
 7779                    await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 780                }
 781
 7782                if (recursive && child is Folder folder)
 783                {
 0784                    folder.Children = null; // invalidate cached children.
 0785                    await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions
 786                }
 787            }
 7788        }
 789
 790        /// <summary>
 791        /// Refreshes the children.
 792        /// </summary>
 793        /// <param name="children">The children.</param>
 794        /// <param name="directoryService">The directory service.</param>
 795        /// <param name="progress">The progress.</param>
 796        /// <param name="cancellationToken">The cancellation token.</param>
 797        /// <returns>Task.</returns>
 798        private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<doub
 799        {
 16800            await RunTasks(
 16801                (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, dire
 16802                children,
 16803                progress,
 16804                cancellationToken).ConfigureAwait(false);
 16805        }
 806
 807        /// <summary>
 808        /// Runs an action block on a list of children.
 809        /// </summary>
 810        /// <param name="task">The task to run for each child.</param>
 811        /// <param name="children">The list of children.</param>
 812        /// <param name="progress">The progress.</param>
 813        /// <param name="cancellationToken">The cancellation token.</param>
 814        /// <returns>Task.</returns>
 815        private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progr
 816        {
 73817            await LimitedConcurrencyLibraryScheduler
 73818                .Enqueue(
 73819                    children.ToArray(),
 73820                    task,
 73821                    progress,
 73822                    cancellationToken)
 73823                .ConfigureAwait(false);
 73824        }
 825
 826        /// <summary>
 827        /// Get the children of this folder from the actual file system.
 828        /// </summary>
 829        /// <returns>IEnumerable{BaseItem}.</returns>
 830        /// <param name="directoryService">The directory service to use for operation.</param>
 831        /// <returns>Returns set of base items.</returns>
 832        protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
 833        {
 58834            var collectionType = LibraryManager.GetContentType(this);
 58835            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 836
 58837            return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryO
 838        }
 839
 840        /// <summary>
 841        /// Get our children from the repo - stubbed for now.
 842        /// </summary>
 843        /// <returns>IEnumerable{BaseItem}.</returns>
 844        protected IReadOnlyList<BaseItem> GetCachedChildren()
 845        {
 144846            return ItemRepository.GetItemList(new InternalItemsQuery
 144847            {
 144848                Parent = this,
 144849                GroupByPresentationUniqueKey = false,
 144850                DtoOptions = new DtoOptions(true)
 144851            });
 852        }
 853
 854        public virtual int GetChildCount(User user)
 855        {
 0856            if (LinkedChildren.Length > 0)
 857            {
 0858                if (this is not ICollectionFolder)
 859                {
 0860                    return GetChildren(user, true).Count;
 861                }
 862            }
 863
 0864            var result = GetItems(new InternalItemsQuery(user)
 0865            {
 0866                Recursive = false,
 0867                Limit = 0,
 0868                Parent = this,
 0869                DtoOptions = new DtoOptions(false)
 0870                {
 0871                    EnableImages = false
 0872                }
 0873            });
 874
 0875            return result.TotalRecordCount;
 876        }
 877
 878        public virtual int GetRecursiveChildCount(User user)
 879        {
 0880            return GetItems(new InternalItemsQuery(user)
 0881            {
 0882                Recursive = true,
 0883                IsFolder = false,
 0884                IsVirtualItem = false,
 0885                EnableTotalRecordCount = true,
 0886                Limit = 0,
 0887                DtoOptions = new DtoOptions(false)
 0888                {
 0889                    EnableImages = false
 0890                }
 0891            }).TotalRecordCount;
 892        }
 893
 894        public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
 895        {
 14896            if (!query.ForceDirect && CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
 897            {
 0898                query.CollapseBoxSetItems = true;
 0899                SetCollapseBoxSetItemTypes(query);
 900            }
 901
 14902            if (this is not UserRootFolder
 14903                && this is not AggregateFolder
 14904                && query.ParentId.IsEmpty())
 905            {
 14906                query.Parent = this;
 907            }
 908
 14909            if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet)
 910            {
 0911                return QueryWithPostFiltering(query);
 912            }
 913
 14914            return LibraryManager.GetItemsResult(query);
 915        }
 916
 917        protected QueryResult<BaseItem> QueryWithPostFiltering(InternalItemsQuery query)
 918        {
 1919            var startIndex = query.StartIndex;
 1920            var limit = query.Limit;
 921
 1922            query.StartIndex = null;
 1923            query.Limit = null;
 924
 1925            IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query);
 1926            var user = query.User;
 927
 1928            if (user is not null)
 929            {
 930                // needed for boxsets
 1931                itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User));
 932            }
 933
 934            IEnumerable<BaseItem> returnItems;
 1935            int totalCount = 0;
 936
 1937            if (query.EnableTotalRecordCount)
 938            {
 0939                var itemArray = itemsList.ToArray();
 0940                totalCount = itemArray.Length;
 0941                returnItems = itemArray;
 942            }
 943            else
 944            {
 1945                returnItems = itemsList;
 946            }
 947
 1948            if (limit.HasValue)
 949            {
 0950                returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value);
 951            }
 1952            else if (startIndex.HasValue)
 953            {
 0954                returnItems = returnItems.Skip(startIndex.Value);
 955            }
 956
 1957            return new QueryResult<BaseItem>(
 1958                query.StartIndex,
 1959                totalCount,
 1960                returnItems.ToArray());
 961        }
 962
 963        private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
 964        {
 0965            return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
 966        }
 967
 968        public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
 969        {
 0970            if (query.ItemIds.Length > 0)
 971            {
 0972                var result = LibraryManager.GetItemsResult(query);
 973
 0974                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 975                {
 0976                    result.Items = SortItemsByRequest(query, result.Items);
 977                }
 978
 0979                return result;
 980            }
 981
 0982            return GetItemsInternal(query);
 983        }
 984
 985        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 986        {
 15987            query.EnableTotalRecordCount = false;
 988
 15989            if (query.ItemIds.Length > 0)
 990            {
 0991                var result = LibraryManager.GetItemList(query);
 992
 0993                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 994                {
 0995                    return SortItemsByRequest(query, result);
 996                }
 997
 0998                return result;
 999            }
 1000
 151001            return GetItemsInternal(query).Items;
 1002        }
 1003
 1004        protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
 1005        {
 141006            if (SourceType == SourceType.Channel)
 1007            {
 1008                try
 1009                {
 01010                    query.Parent = this;
 01011                    query.ChannelIds = new[] { ChannelId };
 1012
 1013                    // Don't blow up here because it could cause parent screens with other content to fail
 01014                    return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None)
 1015                }
 01016                catch
 1017                {
 1018                    // Already logged at lower levels
 01019                    return new QueryResult<BaseItem>();
 1020                }
 1021            }
 1022
 141023            if (query.Recursive)
 1024            {
 141025                return QueryRecursive(query);
 1026            }
 1027
 01028            var user = query.User;
 1029
 1030            IEnumerable<BaseItem> items;
 1031
 01032            int totalItemCount = 0;
 01033            if (query.User is null)
 1034            {
 01035                items = UserViewBuilder.Filter(Children, user, query, UserDataManager, LibraryManager);
 01036                totalItemCount = items.Count();
 1037            }
 1038            else
 1039            {
 1040                // need to pass this param to the children.
 1041                // Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
 01042                var childQuery = new InternalItemsQuery
 01043                {
 01044                    DisplayAlbumFolders = query.DisplayAlbumFolders,
 01045                    NameStartsWith = query.NameStartsWith,
 01046                    NameStartsWithOrGreater = query.NameStartsWithOrGreater,
 01047                    NameLessThan = query.NameLessThan
 01048                };
 1049
 01050                items = UserViewBuilder.Filter(
 01051                    GetChildren(user, true, out totalItemCount, childQuery),
 01052                    user,
 01053                    query,
 01054                    UserDataManager,
 01055                    LibraryManager);
 1056            }
 1057
 01058            return PostFilterAndSort(items, query);
 01059        }
 1060
 1061        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
 1062        {
 01063            var user = query.User;
 1064
 1065            // Check recursive - don't substitute in plain folder views
 01066            if (user is not null)
 1067            {
 01068                items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
 1069
 1070                // After collapse, BoxSets may have replaced items whose names matched the filter
 1071                // but the BoxSet's own name may not match. Re-apply name filtering so BoxSets
 1072                // appear under the correct letter (e.g. "Jump Street" under J, not under #).
 01073                items = ApplyNameFilter(items, query);
 1074            }
 1075
 01076            var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList();
 01077            var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager);
 1078
 01079            if (query.EnableTotalRecordCount)
 1080            {
 01081                result.TotalRecordCount = filteredItems.Count;
 1082            }
 1083
 01084            return result;
 1085        }
 1086
 1087        private static IEnumerable<BaseItem> ApplyNameFilter(IEnumerable<BaseItem> items, InternalItemsQuery query)
 1088        {
 01089            if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
 1090            {
 01091                items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)
 1092            }
 1093
 01094            if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
 1095            {
 01096                items = items.Where(i => string.Compare(i.SortName, query.NameStartsWithOrGreater, StringComparison.Ordi
 1097            }
 1098
 01099            if (!string.IsNullOrWhiteSpace(query.NameLessThan))
 1100            {
 01101                items = items.Where(i => string.Compare(i.SortName, query.NameLessThan, StringComparison.OrdinalIgnoreCa
 1102            }
 1103
 01104            return items;
 1105        }
 1106
 1107        private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
 1108            IEnumerable<BaseItem> items,
 1109            InternalItemsQuery query,
 1110            BaseItem queryParent,
 1111            User user,
 1112            IServerConfigurationManager configurationManager,
 1113            ICollectionManager collectionManager)
 1114        {
 01115            ArgumentNullException.ThrowIfNull(items);
 1116
 01117            if (!CollapseBoxSetItems(query, queryParent, user, configurationManager))
 1118            {
 01119                return items;
 1120            }
 1121
 01122            var config = configurationManager.Configuration;
 1123
 01124            bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
 01125            bool collapseSeries = config.EnableGroupingShowsIntoCollections;
 1126
 01127            if (user is null || (collapseMovies && collapseSeries))
 1128            {
 01129                return collectionManager.CollapseItemsWithinBoxSets(items, user);
 1130            }
 1131
 01132            if (!collapseMovies && !collapseSeries)
 1133            {
 01134                return items;
 1135            }
 1136
 01137            var collapsibleItems = new List<BaseItem>();
 01138            var remainingItems = new List<BaseItem>();
 1139
 01140            foreach (var item in items)
 1141            {
 01142                if ((collapseMovies && item is Movie) || (collapseSeries && item is Series))
 1143                {
 01144                    collapsibleItems.Add(item);
 1145                }
 1146                else
 1147                {
 01148                    remainingItems.Add(item);
 1149                }
 1150            }
 1151
 01152            if (collapsibleItems.Count == 0)
 1153            {
 01154                return remainingItems;
 1155            }
 1156
 01157            var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user);
 1158
 01159            return collapsedItems.Concat(remainingItems);
 1160        }
 1161
 1162        private static bool CollapseBoxSetItems(
 1163            InternalItemsQuery query,
 1164            BaseItem queryParent,
 1165            User user,
 1166            IServerConfigurationManager configurationManager)
 1167        {
 1168            // Could end up stuck in a loop like this
 141169            if (queryParent is BoxSet)
 1170            {
 01171                return false;
 1172            }
 1173
 141174            if (queryParent is Season)
 1175            {
 01176                return false;
 1177            }
 1178
 141179            if (queryParent is MusicAlbum)
 1180            {
 01181                return false;
 1182            }
 1183
 141184            if (queryParent is MusicArtist)
 1185            {
 01186                return false;
 1187            }
 1188
 141189            var param = query.CollapseBoxSetItems;
 141190            if (param.HasValue)
 1191            {
 141192                return param.Value && AllowBoxSetCollapsing(query);
 1193            }
 1194
 01195            var config = configurationManager.Configuration;
 1196
 01197            bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Mov
 01198            bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Ser
 1199
 01200            bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
 01201            bool collapseSeries = config.EnableGroupingShowsIntoCollections;
 1202
 01203            if (user is not null)
 1204            {
 01205                bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries);
 01206                return canCollapse && AllowBoxSetCollapsing(query);
 1207            }
 1208
 01209            return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query);
 1210        }
 1211
 1212        private void SetCollapseBoxSetItemTypes(InternalItemsQuery query)
 1213        {
 01214            var config = ConfigurationManager.Configuration;
 01215            bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
 01216            bool collapseSeries = config.EnableGroupingShowsIntoCollections;
 1217
 01218            if (collapseMovies && collapseSeries)
 1219            {
 1220                // Empty means collapse all types
 01221                query.CollapseBoxSetItemTypes = [];
 01222                return;
 1223            }
 1224
 01225            var types = new List<BaseItemKind>();
 01226            if (collapseMovies)
 1227            {
 01228                types.Add(BaseItemKind.Movie);
 1229            }
 1230
 01231            if (collapseSeries)
 1232            {
 01233                types.Add(BaseItemKind.Series);
 1234            }
 1235
 01236            query.CollapseBoxSetItemTypes = types.ToArray();
 01237        }
 1238
 1239        private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
 1240        {
 01241            if (request.IsFavorite.HasValue)
 1242            {
 01243                return false;
 1244            }
 1245
 01246            if (request.IsFavoriteOrLiked.HasValue)
 1247            {
 01248                return false;
 1249            }
 1250
 01251            if (request.IsLiked.HasValue)
 1252            {
 01253                return false;
 1254            }
 1255
 01256            if (request.IsPlayed.HasValue)
 1257            {
 01258                return false;
 1259            }
 1260
 01261            if (request.IsResumable.HasValue)
 1262            {
 01263                return false;
 1264            }
 1265
 01266            if (request.IsFolder.HasValue)
 1267            {
 01268                return false;
 1269            }
 1270
 01271            if (request.Genres.Count > 0)
 1272            {
 01273                return false;
 1274            }
 1275
 01276            if (request.GenreIds.Count > 0)
 1277            {
 01278                return false;
 1279            }
 1280
 01281            if (request.HasImdbId.HasValue)
 1282            {
 01283                return false;
 1284            }
 1285
 01286            if (request.HasOfficialRating.HasValue)
 1287            {
 01288                return false;
 1289            }
 1290
 01291            if (request.HasOverview.HasValue)
 1292            {
 01293                return false;
 1294            }
 1295
 01296            if (request.HasParentalRating.HasValue)
 1297            {
 01298                return false;
 1299            }
 1300
 01301            if (request.HasSpecialFeature.HasValue)
 1302            {
 01303                return false;
 1304            }
 1305
 01306            if (request.HasSubtitles.HasValue)
 1307            {
 01308                return false;
 1309            }
 1310
 01311            if (request.HasThemeSong.HasValue)
 1312            {
 01313                return false;
 1314            }
 1315
 01316            if (request.HasThemeVideo.HasValue)
 1317            {
 01318                return false;
 1319            }
 1320
 01321            if (request.HasTmdbId.HasValue)
 1322            {
 01323                return false;
 1324            }
 1325
 01326            if (request.HasTrailer.HasValue)
 1327            {
 01328                return false;
 1329            }
 1330
 01331            if (request.ImageTypes.Length > 0)
 1332            {
 01333                return false;
 1334            }
 1335
 01336            if (request.Is3D.HasValue)
 1337            {
 01338                return false;
 1339            }
 1340
 01341            if (request.Is4K.HasValue)
 1342            {
 01343                return false;
 1344            }
 1345
 01346            if (request.IsHD.HasValue)
 1347            {
 01348                return false;
 1349            }
 1350
 01351            if (request.IsLocked.HasValue)
 1352            {
 01353                return false;
 1354            }
 1355
 01356            if (request.IsPlaceHolder.HasValue)
 1357            {
 01358                return false;
 1359            }
 1360
 01361            if (!string.IsNullOrWhiteSpace(request.Person))
 1362            {
 01363                return false;
 1364            }
 1365
 01366            if (request.PersonIds.Length > 0)
 1367            {
 01368                return false;
 1369            }
 1370
 01371            if (request.ItemIds.Length > 0)
 1372            {
 01373                return false;
 1374            }
 1375
 01376            if (request.StudioIds.Length > 0)
 1377            {
 01378                return false;
 1379            }
 1380
 01381            if (request.VideoTypes.Length > 0)
 1382            {
 01383                return false;
 1384            }
 1385
 01386            if (request.Years.Length > 0)
 1387            {
 01388                return false;
 1389            }
 1390
 01391            if (request.Tags.Length > 0)
 1392            {
 01393                return false;
 1394            }
 1395
 01396            if (request.OfficialRatings.Length > 0)
 1397            {
 01398                return false;
 1399            }
 1400
 01401            if (request.MinIndexNumber.HasValue)
 1402            {
 01403                return false;
 1404            }
 1405
 01406            if (request.OrderBy.Any(o =>
 01407                o.OrderBy == ItemSortBy.CommunityRating ||
 01408                o.OrderBy == ItemSortBy.CriticRating ||
 01409                o.OrderBy == ItemSortBy.Runtime))
 1410            {
 01411                return false;
 1412            }
 1413
 01414            return true;
 1415        }
 1416
 1417        public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount
 1418        {
 101419            ArgumentNullException.ThrowIfNull(user);
 101420            query ??= new InternalItemsQuery();
 101421            query.User = user;
 1422
 1423            // the true root should return our users root folder children
 101424            if (IsPhysicalRoot)
 1425            {
 01426                return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren, out totalItemCount);
 1427            }
 1428
 101429            var result = new Dictionary<Guid, BaseItem>();
 1430
 101431            totalItemCount = AddChildren(user, includeLinkedChildren, result, false, query);
 1432
 101433            return result.Values.ToArray();
 1434        }
 1435
 1436        public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery que
 1437        {
 101438            return GetChildren(user, includeLinkedChildren, out _, query);
 1439        }
 1440
 1441        protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
 1442        {
 101443            return Children;
 1444        }
 1445
 1446        /// <summary>
 1447        /// Adds the children to list.
 1448        /// </summary>
 1449        private int AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive
 1450        {
 1451            // Prevent infinite recursion of nested folders
 101452            visitedFolders ??= new HashSet<Folder>();
 101453            if (!visitedFolders.Add(this))
 1454            {
 01455                return 0;
 1456            }
 1457
 1458            // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in mus
 101459            IEnumerable<BaseItem> children = null;
 101460            if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
 1461            {
 01462                children = Children;
 01463                query = null;
 1464            }
 1465
 1466            // If there are not sub-folders, proceed as normal.
 101467            if (children is null)
 1468            {
 101469                children = GetEligibleChildrenForRecursiveChildren(user);
 1470            }
 1471
 101472            if (includeLinkedChildren)
 1473            {
 101474                children = children.Concat(GetLinkedChildren(user)).ToArray();
 1475            }
 1476
 101477            return AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFol
 1478        }
 1479
 1480        private int AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dic
 1481        {
 101482            query ??= new InternalItemsQuery();
 101483            var limit = query.Limit > 0 ? query.Limit : int.MaxValue;
 101484            query.Limit = 0;
 1485
 101486            var visibleChildren = children
 101487                .Where(e => e.IsVisible(user))
 101488                .ToArray();
 1489
 101490            var realChildren = UserViewBuilder.Filter(visibleChildren, query.User, query, UserDataManager, LibraryManage
 101491                .ToArray();
 1492
 101493            var childCount = realChildren.Length;
 101494            if (result.Count < limit)
 1495            {
 101496                var remainingCount = (int)(limit - result.Count);
 401497                foreach (var child in realChildren
 101498                    .Skip(query.StartIndex ?? 0)
 101499                    .Take(remainingCount))
 1500                {
 101501                    result[child.Id] = child;
 1502                }
 1503            }
 1504
 101505            if (recursive)
 1506            {
 01507                foreach (var child in visibleChildren
 01508                    .Where(e => e.IsFolder)
 01509                    .OfType<Folder>())
 1510                {
 01511                    childCount += child.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
 1512                }
 1513            }
 1514
 101515            return childCount;
 1516        }
 1517
 1518        public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCo
 1519        {
 01520            ArgumentNullException.ThrowIfNull(user);
 1521
 01522            var result = new Dictionary<Guid, BaseItem>();
 1523
 01524            totalCount = AddChildren(user, true, result, true, query);
 1525
 01526            return result.Values.ToArray();
 1527        }
 1528
 1529        /// <summary>
 1530        /// Gets the recursive children.
 1531        /// </summary>
 1532        /// <returns>IList{BaseItem}.</returns>
 1533        public IReadOnlyList<BaseItem> GetRecursiveChildren()
 1534        {
 01535            return GetRecursiveChildren(true);
 1536        }
 1537
 1538        public IReadOnlyList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
 1539        {
 01540            return GetRecursiveChildren(i => true, includeLinkedChildren);
 1541        }
 1542
 1543        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
 1544        {
 01545            return GetRecursiveChildren(filter, true);
 1546        }
 1547
 1548        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
 1549        {
 01550            var result = new Dictionary<Guid, BaseItem>();
 1551
 01552            AddChildrenToList(result, includeLinkedChildren, true, filter);
 1553
 01554            return result.Values.ToArray();
 1555        }
 1556
 1557        /// <summary>
 1558        /// Adds the children to list.
 1559        /// </summary>
 1560        private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Fu
 1561        {
 01562            foreach (var child in Children)
 1563            {
 01564                if (filter is null || filter(child))
 1565                {
 01566                    result[child.Id] = child;
 1567                }
 1568
 01569                if (recursive && child.IsFolder)
 1570                {
 01571                    var folder = (Folder)child;
 1572
 1573                    // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loo
 01574                    folder.AddChildrenToList(result, false, true, filter);
 1575                }
 1576            }
 1577
 01578            if (includeLinkedChildren)
 1579            {
 01580                foreach (var child in GetLinkedChildren())
 1581                {
 01582                    if (filter is null || filter(child))
 1583                    {
 01584                        result[child.Id] = child;
 1585                    }
 1586                }
 1587            }
 01588        }
 1589
 1590        /// <summary>
 1591        /// Gets the linked children.
 1592        /// </summary>
 1593        /// <returns>IEnumerable{BaseItem}.</returns>
 1594        public List<BaseItem> GetLinkedChildren()
 1595        {
 101596            var resolved = ResolveLinkedChildren(LinkedChildren);
 101597            var list = new List<BaseItem>(resolved.Count);
 201598            foreach (var (_, item) in resolved)
 1599            {
 01600                list.Add(item);
 1601            }
 1602
 101603            return list;
 1604        }
 1605
 1606        public bool ContainsLinkedChildByItemId(Guid itemId)
 1607        {
 01608            var linkedChildren = LinkedChildren;
 01609            foreach (var i in linkedChildren)
 1610            {
 01611                if (i.ItemId.HasValue)
 1612                {
 01613                    if (i.ItemId.Value.Equals(itemId))
 1614                    {
 01615                        return true;
 1616                    }
 1617
 1618                    continue;
 1619                }
 1620
 01621                var child = GetLinkedChild(i);
 1622
 01623                if (child is not null && child.Id.Equals(itemId))
 1624                {
 01625                    return true;
 1626                }
 1627            }
 1628
 01629            return false;
 1630        }
 1631
 1632        public List<BaseItem> GetLinkedChildren(User user)
 1633        {
 101634            if (!FilterLinkedChildrenPerUser || user is null)
 1635            {
 101636                return GetLinkedChildren();
 1637            }
 1638
 01639            var linkedChildren = LinkedChildren;
 01640            var list = new List<BaseItem>(linkedChildren.Length);
 1641
 01642            if (linkedChildren.Length == 0)
 1643            {
 01644                return list;
 1645            }
 1646
 01647            var allUserRootChildren = LibraryManager.GetUserRootFolder()
 01648                .GetChildren(user, true)
 01649                .OfType<Folder>()
 01650                .ToList();
 1651
 01652            var collectionFolderIds = allUserRootChildren
 01653                .Select(i => i.Id)
 01654                .ToList();
 1655
 01656            foreach (var i in linkedChildren)
 1657            {
 01658                var child = GetLinkedChild(i);
 1659
 01660                if (child is null)
 1661                {
 1662                    continue;
 1663                }
 1664
 01665                var childOwner = child.GetOwner() ?? child;
 1666
 01667                if (child is not IItemByName)
 1668                {
 01669                    var childProtocol = childOwner.PathProtocol;
 01670                    if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File)
 1671                    {
 01672                        if (!childOwner.IsVisibleStandalone(user))
 1673                        {
 01674                            continue;
 1675                        }
 1676                    }
 1677                    else
 1678                    {
 01679                        var itemCollectionFolderIds =
 01680                            LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id);
 1681
 01682                        if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains))
 1683                        {
 1684                            continue;
 1685                        }
 1686                    }
 1687                }
 1688
 01689                list.Add(child);
 1690            }
 1691
 01692            return list;
 1693        }
 1694
 1695        /// <summary>
 1696        /// Gets the linked children.
 1697        /// </summary>
 1698        /// <returns>IEnumerable{BaseItem}.</returns>
 1699        public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
 1700        {
 01701            return ResolveLinkedChildren(LinkedChildren)
 01702                .Select(t => new Tuple<LinkedChild, BaseItem>(t.Info, t.Item))
 01703                .ToArray();
 1704        }
 1705
 1706        /// <summary>
 1707        /// Resolves a list of <see cref="LinkedChild"/> entries to their <see cref="BaseItem"/> targets,
 1708        /// batching the database lookup across all entries with a known ItemId.
 1709        /// Entries without a usable ItemId fall back to the per-entry <see cref="BaseItem.GetLinkedChild"/>
 1710        /// path (legacy path-based resolution).
 1711        /// </summary>
 1712        /// <param name="linkedChildren">Linked children to resolve.</param>
 1713        /// <returns>Each input entry paired with its resolved item; entries that fail to resolve are dropped.</returns>
 1714        private List<(LinkedChild Info, BaseItem Item)> ResolveLinkedChildren(IReadOnlyList<LinkedChild> linkedChildren)
 1715        {
 101716            var resolved = new List<(LinkedChild Info, BaseItem Item)>(linkedChildren.Count);
 101717            if (linkedChildren.Count == 0)
 1718            {
 101719                return resolved;
 1720            }
 1721
 01722            var idsToBatch = new HashSet<Guid>();
 01723            foreach (var info in linkedChildren)
 1724            {
 01725                if (info.ItemId.HasValue && !info.ItemId.Value.IsEmpty())
 1726                {
 01727                    idsToBatch.Add(info.ItemId.Value);
 1728                }
 1729            }
 1730
 01731            Dictionary<Guid, BaseItem> byId = null;
 01732            if (idsToBatch.Count > 0)
 1733            {
 01734                var batched = LibraryManager.GetItemList(new InternalItemsQuery
 01735                {
 01736                    ItemIds = [.. idsToBatch]
 01737                });
 01738                byId = new Dictionary<Guid, BaseItem>(batched.Count);
 01739                foreach (var item in batched)
 1740                {
 01741                    byId[item.Id] = item;
 1742                }
 1743            }
 1744
 01745            foreach (var info in linkedChildren)
 1746            {
 01747                BaseItem item = null;
 01748                if (byId is not null && info.ItemId.HasValue && byId.TryGetValue(info.ItemId.Value, out var batchedItem)
 1749                {
 01750                    item = batchedItem;
 1751                }
 1752                else
 1753                {
 1754                    // ItemId is missing/empty or the batched query couldn't return the item
 1755                    // (e.g. it has been removed). Fall back to per-entry resolution, which also
 1756                    // handles legacy path-based linked children.
 01757                    item = GetLinkedChild(info);
 1758                }
 1759
 01760                if (item is not null)
 1761                {
 01762                    resolved.Add((info, item));
 1763                }
 1764            }
 1765
 01766            return resolved;
 1767        }
 1768
 1769        protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystem
 1770        {
 351771            var changesFound = false;
 1772
 351773            if (IsFileProtocol)
 1774            {
 351775                if (RefreshLinkedChildren(fileSystemChildren))
 1776                {
 01777                    changesFound = true;
 1778                }
 1779            }
 1780
 351781            var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).Configur
 1782
 351783            return baseHasChanges || changesFound;
 351784        }
 1785
 1786        /// <summary>
 1787        /// Refreshes the linked children.
 1788        /// </summary>
 1789        /// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
 1790        /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
 1791        protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
 1792        {
 211793            if (SupportsShortcutChildren)
 1794            {
 211795                var newShortcutLinks = fileSystemChildren
 211796                    .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName))
 211797                    .Select(i =>
 211798                    {
 211799                        try
 211800                        {
 211801                            Logger.LogDebug("Found shortcut at {0}", i.FullName);
 211802
 211803                            var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShor
 211804
 211805                            if (!string.IsNullOrEmpty(resolvedPath))
 211806                            {
 211807#pragma warning disable CS0618 // Type or member is obsolete - shortcuts require Path for lazy ItemId resolution
 211808                                return new LinkedChild
 211809                                {
 211810                                    Path = resolvedPath,
 211811                                    Type = LinkedChildType.Shortcut
 211812                                };
 211813#pragma warning restore CS0618
 211814                            }
 211815
 211816                            Logger.LogError("Error resolving shortcut {0}", i.FullName);
 211817
 211818                            return null;
 211819                        }
 211820                        catch (IOException ex)
 211821                        {
 211822                            Logger.LogError(ex, "Error resolving shortcut {0}", i.FullName);
 211823                            return null;
 211824                        }
 211825                    })
 211826                    .Where(i => i is not null)
 211827                    .ToList();
 1828
 211829                var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
 1830
 211831                if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem)))
 1832                {
 01833                    Logger.LogInformation("Shortcut links have changed for {0}", Path);
 1834
 01835                    newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual));
 01836                    LinkedChildren = newShortcutLinks.ToArray();
 01837                    return true;
 1838                }
 1839            }
 1840
 211841            return false;
 1842        }
 1843
 1844        /// <summary>
 1845        /// Marks the played.
 1846        /// </summary>
 1847        /// <param name="user">The user.</param>
 1848        /// <param name="datePlayed">The date played.</param>
 1849        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1850        public override void MarkPlayed(
 1851            User user,
 1852            DateTime? datePlayed,
 1853            bool resetPosition)
 1854        {
 01855            var query = new InternalItemsQuery
 01856            {
 01857                User = user,
 01858                Recursive = true,
 01859                IsFolder = false,
 01860                EnableTotalRecordCount = false
 01861            };
 1862
 01863            if (!user.DisplayMissingEpisodes)
 1864            {
 01865                query.IsVirtualItem = false;
 1866            }
 1867
 01868            var itemsResult = GetItemList(query);
 1869
 1870            // Sweep through recursively and update status
 01871            foreach (var item in itemsResult)
 1872            {
 01873                if (item.IsVirtualItem)
 1874                {
 1875                    // The querying doesn't support virtual unaired
 01876                    var episode = item as Episode;
 01877                    if (episode is not null && episode.IsUnaired)
 1878                    {
 1879                        continue;
 1880                    }
 1881                }
 1882
 01883                item.MarkPlayed(user, datePlayed, resetPosition);
 1884            }
 01885        }
 1886
 1887        /// <summary>
 1888        /// Marks the unplayed.
 1889        /// </summary>
 1890        /// <param name="user">The user.</param>
 1891        public override void MarkUnplayed(User user)
 1892        {
 01893            var itemsResult = GetItemList(new InternalItemsQuery
 01894            {
 01895                User = user,
 01896                Recursive = true,
 01897                IsFolder = false,
 01898                EnableTotalRecordCount = false
 01899            });
 1900
 1901            // Sweep through recursively and update status
 01902            foreach (var item in itemsResult)
 1903            {
 01904                item.MarkUnplayed(user);
 1905            }
 01906        }
 1907
 1908        public override bool IsPlayed(User user, UserItemData userItemData)
 1909        {
 01910            return ItemRepository.GetIsPlayed(user, Id, true);
 1911        }
 1912
 1913        public override bool IsUnplayed(User user, UserItemData userItemData)
 1914        {
 01915            return !IsPlayed(user, userItemData);
 1916        }
 1917
 1918        public override void FillUserDataDtoValues(
 1919            UserItemDataDto dto,
 1920            UserItemData userData,
 1921            BaseItemDto itemDto,
 1922            User user,
 1923            DtoOptions fields,
 1924            (int Played, int Total)? precomputedCounts = null)
 1925        {
 91926            if (!SupportsUserDataFromChildren)
 1927            {
 91928                return;
 1929            }
 1930
 01931            if (SupportsPlayedStatus || (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount)))
 1932            {
 1933                int playedCount;
 1934                int totalCount;
 1935
 01936                if (precomputedCounts.HasValue)
 1937                {
 1938                    // Use batch-fetched counts (avoids N+1 queries)
 01939                    (playedCount, totalCount) = precomputedCounts.Value;
 1940                }
 1941                else
 1942                {
 1943                    // Fall back to per-item query when no batch data is available
 01944                    var query = new InternalItemsQuery(user);
 1945
 01946                    if (LinkedChildren.Length > 0)
 1947                    {
 01948                        (playedCount, totalCount) = ItemCountService.GetPlayedAndTotalCountFromLinkedChildren(query, Id)
 1949                    }
 1950                    else
 1951                    {
 01952                        (playedCount, totalCount) = ItemCountService.GetPlayedAndTotalCount(query, Id);
 1953                    }
 1954                }
 1955
 01956                if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))
 1957                {
 01958                    itemDto.RecursiveItemCount = totalCount;
 1959                }
 1960
 01961                if (SupportsPlayedStatus)
 1962                {
 01963                    var unplayedCount = totalCount - playedCount;
 01964                    dto.UnplayedItemCount = unplayedCount;
 1965
 01966                    if (totalCount > 0)
 1967                    {
 01968                        dto.PlayedPercentage = playedCount / (double)totalCount * 100;
 01969                        dto.Played = playedCount >= totalCount;
 1970                    }
 1971                    else
 1972                    {
 01973                        dto.Played = true;
 1974                    }
 1975                }
 1976            }
 01977        }
 1978
 1979        /// <summary>
 1980        /// Contains constants used when reporting scan progress.
 1981        /// </summary>
 1982        private static class ProgressHelpers
 1983        {
 1984            /// <summary>
 1985            /// Reported after the folders immediate children are retrieved.
 1986            /// </summary>
 1987            public const int RetrievedChildren = 5;
 1988
 1989            /// <summary>
 1990            /// Reported after add, updating, or deleting child items from the LibraryManager.
 1991            /// </summary>
 1992            public const int UpdatedChildItems = 10;
 1993
 1994            /// <summary>
 1995            /// Reported once subfolders are scanned.
 1996            /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders].
 1997            /// </summary>
 1998            public const int ScannedSubfolders = 50;
 1999
 2000            /// <summary>
 2001            /// Reported once metadata is refreshed.
 2002            /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed].
 2003            /// </summary>
 2004            public const int RefreshedMetadata = 100;
 2005
 2006            /// <summary>
 2007            /// Gets the current progress given the previous step, next step, and progress in between.
 2008            /// </summary>
 2009            /// <param name="previousProgressStep">The previous progress step.</param>
 2010            /// <param name="nextProgressStep">The next progress step.</param>
 2011            /// <param name="currentProgress">The current progress step.</param>
 2012            /// <returns>The progress.</returns>
 2013            public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress)
 2014            {
 742015                return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100));
 2016            }
 2017        }
 2018    }
 2019}

Methods/Properties

.ctor()
get_SupportsThemeMedia()
get_IsPreSorted()
get_IsPhysicalRoot()
get_SupportsInheritedParentImages()
get_SupportsPlayedStatus()
get_IsFolder()
get_IsDisplayedAsFolder()
get_SupportsCumulativeRunTimeTicks()
get_SupportsDateLastMediaAdded()
get_FileNameWithoutExtension()
get_Children()
set_Children(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>)
get_RecursiveChildren()
get_SupportsShortcutChildren()
get_FilterLinkedChildrenPerUser()
get_SupportsOwnedItems()
get_SupportsUserDataFromChildren()
CanDelete()
RequiresRefresh()
AddChild(MediaBrowser.Controller.Entities.BaseItem)
IsVisible(Jellyfin.Database.Implementations.Entities.User,System.Boolean)
LoadChildren()
GetRefreshProgress()
ValidateChildren(System.IProgress`1<System.Double>,System.Threading.CancellationToken)
ValidateChildren(System.IProgress`1<System.Double>,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Boolean,System.Boolean,System.Threading.CancellationToken)
GetActualChildrenDictionary()
ValidateChildrenInternal()
IsLibraryFolderAccessible(MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
ValidateChildrenInternal2()
RefreshMetadataRecursive()
RefreshAllMetadataForContainer()
RefreshChildMetadata()
ValidateSubFolders()
RunTasks()
GetNonCachedChildren(MediaBrowser.Controller.Providers.IDirectoryService)
GetCachedChildren()
GetChildCount(Jellyfin.Database.Implementations.Entities.User)
GetRecursiveChildCount(Jellyfin.Database.Implementations.Entities.User)
QueryRecursive(MediaBrowser.Controller.Entities.InternalItemsQuery)
QueryWithPostFiltering(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)
ApplyNameFilter(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Entities.InternalItemsQuery)
CollapseBoxSetItemsIfNeeded(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Entities.InternalItemsQuery,MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Collections.ICollectionManager)
CollapseBoxSetItems(MediaBrowser.Controller.Entities.InternalItemsQuery,MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Configuration.IServerConfigurationManager)
SetCollapseBoxSetItemTypes(MediaBrowser.Controller.Entities.InternalItemsQuery)
AllowBoxSetCollapsing(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetChildren(Jellyfin.Database.Implementations.Entities.User,System.Boolean,System.Int32&,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetChildren(Jellyfin.Database.Implementations.Entities.User,System.Boolean,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetEligibleChildrenForRecursiveChildren(Jellyfin.Database.Implementations.Entities.User)
AddChildren(Jellyfin.Database.Implementations.Entities.User,System.Boolean,System.Collections.Generic.Dictionary`2<System.Guid,MediaBrowser.Controller.Entities.BaseItem>,System.Boolean,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.HashSet`1<MediaBrowser.Controller.Entities.Folder>)
AddChildrenFromCollection(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User,System.Boolean,System.Collections.Generic.Dictionary`2<System.Guid,MediaBrowser.Controller.Entities.BaseItem>,System.Boolean,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.HashSet`1<MediaBrowser.Controller.Entities.Folder>)
GetRecursiveChildren(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Int32&)
GetRecursiveChildren()
GetRecursiveChildren(System.Boolean)
GetRecursiveChildren(System.Func`2<MediaBrowser.Controller.Entities.BaseItem,System.Boolean>)
GetRecursiveChildren(System.Func`2<MediaBrowser.Controller.Entities.BaseItem,System.Boolean>,System.Boolean)
AddChildrenToList(System.Collections.Generic.Dictionary`2<System.Guid,MediaBrowser.Controller.Entities.BaseItem>,System.Boolean,System.Boolean,System.Func`2<MediaBrowser.Controller.Entities.BaseItem,System.Boolean>)
GetLinkedChildren()
ContainsLinkedChildByItemId(System.Guid)
GetLinkedChildren(Jellyfin.Database.Implementations.Entities.User)
GetLinkedChildrenInfos()
ResolveLinkedChildren(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.LinkedChild>)
RefreshedOwnedItems()
RefreshLinkedChildren(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>)
MarkPlayed(Jellyfin.Database.Implementations.Entities.User,System.Nullable`1<System.DateTime>,System.Boolean)
MarkUnplayed(Jellyfin.Database.Implementations.Entities.User)
IsPlayed(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.UserItemData)
IsUnplayed(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.UserItemData)
FillUserDataDtoValues(MediaBrowser.Model.Dto.UserItemDataDto,MediaBrowser.Controller.Entities.UserItemData,MediaBrowser.Model.Dto.BaseItemDto,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Dto.DtoOptions,System.Nullable`1<System.ValueTuple`2<System.Int32,System.Int32>>)
GetProgress(System.Int32,System.Int32,System.Double)