< 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: 312
Uncovered lines: 492
Coverable lines: 804
Total lines: 2039
Line coverage: 38.8%
Branch coverage
26%
Covered branches: 136
Total branches: 504
Branch coverage: 26.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/8/2026 - 12:15:13 AM Line coverage: 38% (304/800) Branch coverage: 28.7% (146/508) Total lines: 20195/20/2026 - 12:15:44 AM Line coverage: 38% (304/800) Branch coverage: 26.5% (135/508) Total lines: 20196/7/2026 - 12:16:09 AM Line coverage: 38% (305/801) Branch coverage: 26.7% (136/508) Total lines: 20227/6/2026 - 12:16:28 AM Line coverage: 38.4% (310/806) Branch coverage: 26.7% (136/508) Total lines: 20318/2/2026 - 12:17:28 AM Line coverage: 38.6% (312/808) Branch coverage: 26.7% (136/508) Total lines: 20478/3/2026 - 12:16:46 AM Line coverage: 38.8% (312/804) Branch coverage: 26.9% (136/504) Total lines: 2039 5/8/2026 - 12:15:13 AM Line coverage: 38% (304/800) Branch coverage: 28.7% (146/508) Total lines: 20195/20/2026 - 12:15:44 AM Line coverage: 38% (304/800) Branch coverage: 26.5% (135/508) Total lines: 20196/7/2026 - 12:16:09 AM Line coverage: 38% (305/801) Branch coverage: 26.7% (136/508) Total lines: 20227/6/2026 - 12:16:28 AM Line coverage: 38.4% (310/806) Branch coverage: 26.7% (136/508) Total lines: 20318/2/2026 - 12:17:28 AM Line coverage: 38.6% (312/808) Branch coverage: 26.7% (136/508) Total lines: 20478/3/2026 - 12:16:46 AM Line coverage: 38.8% (312/804) Branch coverage: 26.9% (136/504) Total lines: 2039

Coverage delta

Coverage delta 3 -3

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
get_LinkedChildren()100%11100%
set_LinkedChildren(...)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()25%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%169910848.52%
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(...)57.14%181472.72%
QueryWithPostFiltering(...)50%9877.27%
SortItemsByRequest(...)100%210%
GetItems(...)0%4260%
GetItemList(...)16.66%13642.85%
GetItemsInternal(...)33.33%32610.71%
PostFilterAndSort(...)0%620%
ApplyNameFilter(...)0%4260%
CollapseBoxSetItemsIfNeeded(...)0%506220%
CollapseBoxSetItems(...)25%1822435%
SetCollapseBoxSetItemTypes(...)0%4260%
AllowBoxSetCollapsing(...)0%4692680%
GetChildren(...)50%4487.5%
GetChildren(...)100%11100%
GetEligibleChildrenForRecursiveChildren(...)100%11100%
AddChildren(...)42.85%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;
 50446        private LinkedChild[] _linkedChildren = [];
 47
 48        public static IUserViewManager UserViewManager { get; set; }
 49
 50        public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get; set; }
 51
 52        /// <summary>
 53        /// Gets or sets a value indicating whether this instance is root.
 54        /// </summary>
 55        /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
 56        public bool IsRoot { get; set; }
 57
 58        /// <summary>
 59        /// Gets or sets the linked children.
 60        /// </summary>
 61        [JsonIgnore]
 62        public LinkedChild[] LinkedChildren
 63        {
 27764            get => _linkedChildren;
 65            set
 66            {
 14667                _linkedChildren = value;
 68
 69                // Assigning the collection means the caller knows the complete set of links.
 14670                LinkedChildrenLoaded = true;
 14671            }
 72        }
 73
 74        /// <summary>
 75        /// Gets a value indicating whether <see cref="LinkedChildren"/> holds the stored set of links.
 76        /// </summary>
 77        /// <remarks>
 78        /// An unloaded instance carries an empty array that means "unknown", not "no children" —
 79        /// persisting it would delete every link the item has.
 80        /// </remarks>
 81        [JsonIgnore]
 82        public bool LinkedChildrenLoaded { get; private set; }
 83
 84        [JsonIgnore]
 85        public DateTime? DateLastMediaAdded { get; set; }
 86
 87        [JsonIgnore]
 088        public override bool SupportsThemeMedia => true;
 89
 90        [JsonIgnore]
 091        public virtual bool IsPreSorted => false;
 92
 93        [JsonIgnore]
 1094        public virtual bool IsPhysicalRoot => false;
 95
 96        [JsonIgnore]
 097        public override bool SupportsInheritedParentImages => true;
 98
 99        [JsonIgnore]
 0100        public override bool SupportsPlayedStatus => true;
 101
 102        /// <summary>
 103        /// Gets a value indicating whether this instance is folder.
 104        /// </summary>
 105        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 106        [JsonIgnore]
 993107        public override bool IsFolder => true;
 108
 109        [JsonIgnore]
 0110        public override bool IsDisplayedAsFolder => true;
 111
 112        [JsonIgnore]
 98113        public virtual bool SupportsCumulativeRunTimeTicks => false;
 114
 115        [JsonIgnore]
 62116        public virtual bool SupportsDateLastMediaAdded => false;
 117
 118        [JsonIgnore]
 119        public override string FileNameWithoutExtension
 120        {
 121            get
 122            {
 342123                if (IsFileProtocol)
 124                {
 342125                    return System.IO.Path.GetFileName(Path);
 126                }
 127
 0128                return null;
 129            }
 130        }
 131
 132        /// <summary>
 133        /// Gets or Sets the actual children.
 134        /// </summary>
 135        /// <value>The actual children.</value>
 136        [JsonIgnore]
 137        public virtual IEnumerable<BaseItem> Children
 138        {
 1133139            get => _children ??= LoadChildren();
 240140            set => _children = value;
 141        }
 142
 143        /// <summary>
 144        /// Gets thread-safe access to all recursive children of this folder - without regard to user.
 145        /// </summary>
 146        /// <value>The recursive children.</value>
 147        [JsonIgnore]
 0148        public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
 149
 150        [JsonIgnore]
 23151        protected virtual bool SupportsShortcutChildren => false;
 152
 10153        protected virtual bool FilterLinkedChildrenPerUser => false;
 154
 155        [JsonIgnore]
 103156        protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
 157
 158        [JsonIgnore]
 159        public virtual bool SupportsUserDataFromChildren
 160        {
 161            get
 162            {
 163                // These are just far too slow.
 12164                if (this is ICollectionFolder)
 165                {
 6166                    return false;
 167                }
 168
 6169                if (this is UserView)
 170                {
 0171                    return false;
 172                }
 173
 6174                if (this is UserRootFolder)
 175                {
 6176                    return false;
 177                }
 178
 0179                if (this is Channel)
 180                {
 0181                    return false;
 182                }
 183
 0184                if (SourceType != SourceType.Library)
 185                {
 0186                    return false;
 187                }
 188
 0189                if (this is IItemByName)
 190                {
 0191                    if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName)
 192                    {
 0193                        return false;
 194                    }
 195                }
 196
 0197                return true;
 198            }
 199        }
 200
 201        public static ICollectionManager CollectionManager { get; set; }
 202
 203        public override bool CanDelete()
 204        {
 6205            if (IsRoot)
 206            {
 6207                return false;
 208            }
 209
 0210            return base.CanDelete();
 211        }
 212
 213        public override bool RequiresRefresh()
 214        {
 62215            var baseResult = base.RequiresRefresh();
 216
 62217            if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue)
 218            {
 0219                baseResult = true;
 220            }
 221
 62222            return baseResult;
 223        }
 224
 225        /// <summary>
 226        /// Adds the child.
 227        /// </summary>
 228        /// <param name="item">The item.</param>
 229        /// <exception cref="InvalidOperationException">Unable to add  + item.Name.</exception>
 230        public void AddChild(BaseItem item)
 231        {
 0232            item.SetParent(this);
 233
 0234            if (item.Id.IsEmpty())
 235            {
 0236                item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
 237            }
 238
 0239            if (item.DateCreated == DateTime.MinValue)
 240            {
 0241                item.DateCreated = DateTime.UtcNow;
 242            }
 243
 0244            if (item.DateModified == DateTime.MinValue)
 245            {
 0246                item.DateModified = DateTime.UtcNow;
 247            }
 248
 0249            LibraryManager.CreateItem(item, this);
 0250        }
 251
 252        public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 253        {
 10254            if (this is ICollectionFolder && this is not BasePluginFolder)
 255            {
 0256                var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 0257                if (blockedMediaFolders.Length > 0)
 258                {
 0259                    if (blockedMediaFolders.Contains(Id))
 260                    {
 0261                        return false;
 262                    }
 263                }
 264                else
 265                {
 0266                    if (!user.HasPermission(PermissionKind.EnableAllFolders)
 0267                        && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id))
 268                    {
 0269                        return false;
 270                    }
 271                }
 272            }
 273
 10274            return base.IsVisible(user, skipAllowedTagsCheck);
 275        }
 276
 277        /// <summary>
 278        /// Loads our children.  Validation will occur externally.
 279        /// We want this synchronous.
 280        /// </summary>
 281        /// <returns>Returns children.</returns>
 282        protected virtual IReadOnlyList<BaseItem> LoadChildren()
 283        {
 284            // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
 285            // just load our children from the repo - the library will be validated and maintained in other processes
 150286            return GetCachedChildren();
 287        }
 288
 289        public override double? GetRefreshProgress()
 290        {
 0291            return ProviderManager.GetRefreshProgress(Id);
 292        }
 293
 294        public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
 295        {
 0296            return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellation
 297        }
 298
 299        /// <summary>
 300        /// Validates that the children of the folder still exist.
 301        /// </summary>
 302        /// <param name="progress">The progress.</param>
 303        /// <param name="metadataRefreshOptions">The metadata refresh options.</param>
 304        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 305        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 306        /// <param name="cancellationToken">The cancellation token.</param>
 307        /// <returns>Task.</returns>
 308        public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool rec
 309        {
 62310            Children = null; // invalidate cached children.
 62311            return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadata
 312        }
 313
 314        private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
 315        {
 61316            var dictionary = new Dictionary<Guid, BaseItem>();
 317
 61318            Children = null; // invalidate cached children.
 61319            var childrenList = Children.ToList();
 320
 214321            foreach (var child in childrenList)
 322            {
 46323                var id = child.Id;
 46324                if (dictionary.ContainsKey(id))
 325                {
 0326                    Logger.LogError(
 0327                        "Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}",
 0328                        Path ?? Name,
 0329                        child.Path ?? child.Name);
 330                }
 331                else
 332                {
 46333                    dictionary[id] = child;
 334                }
 335            }
 336
 61337            return dictionary;
 338        }
 339
 340        /// <summary>
 341        /// Validates the children internal.
 342        /// </summary>
 343        /// <param name="progress">The progress.</param>
 344        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 345        /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
 346        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 347        /// <param name="refreshOptions">The refresh options.</param>
 348        /// <param name="directoryService">The directory service.</param>
 349        /// <param name="cancellationToken">The cancellation token.</param>
 350        /// <returns>Task.</returns>
 351        protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshCh
 352        {
 62353            if (recursive)
 354            {
 18355                ProviderManager.OnRefreshStart(this);
 356            }
 357
 358            try
 359            {
 360                if (GetParents().Any(f => f.Id.Equals(Id)))
 361                {
 0362                    throw new InvalidOperationException("Recursive datastructure detected abort processing this item.");
 363                }
 364
 62365                await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptio
 61366            }
 367            finally
 368            {
 62369                if (recursive)
 370                {
 18371                    ProviderManager.OnRefreshComplete(this);
 372                }
 373            }
 61374        }
 375
 376        private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item, bool checkColle
 377        {
 109378            if (!checkCollection && (item is BoxSet || string.Equals(item.FileNameWithoutExtension, "collections", Strin
 379            {
 0380                return true;
 381            }
 382
 383            // For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
 109384            if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
 385            {
 40386                Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.Containi
 40387                return false;
 388            }
 389
 69390            return true;
 391        }
 392
 393        private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetada
 394        {
 62395            if (!IsLibraryFolderAccessible(directoryService, this, allowRemoveRoot))
 396            {
 0397                return;
 398            }
 399
 62400            cancellationToken.ThrowIfCancellationRequested();
 401
 61402            var validChildren = new List<BaseItem>();
 61403            var accessibleChildren = new List<BaseItem>();
 61404            var validChildrenNeedGeneration = false;
 405
 61406            if (IsFileProtocol)
 407            {
 61408                IEnumerable<BaseItem> nonCachedChildren = [];
 409
 410                try
 411                {
 61412                    nonCachedChildren = GetNonCachedChildren(directoryService);
 61413                }
 0414                catch (IOException ex)
 415                {
 0416                    Logger.LogError(ex, "Error retrieving children from file system");
 0417                }
 0418                catch (SecurityException ex)
 419                {
 0420                    Logger.LogError(ex, "Error retrieving children from file system");
 0421                }
 0422                catch (Exception ex)
 423                {
 0424                    Logger.LogError(ex, "Error retrieving children");
 0425                    return;
 426                }
 427
 61428                progress.Report(ProgressHelpers.RetrievedChildren);
 429
 61430                if (recursive)
 431                {
 18432                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren);
 433                }
 434
 435                // Build a dictionary of the current children we have now by Id so we can compare quickly and easily
 61436                var currentChildren = GetActualChildrenDictionary();
 437
 438                // Create a list for our validated children
 61439                var newItems = new List<BaseItem>();
 61440                var actuallyRemoved = new List<BaseItem>();
 441
 442                // Build a reverse path→item lookup for detecting type changes
 61443                var currentChildrenByPath = new Dictionary<string, BaseItem>(StringComparer.OrdinalIgnoreCase);
 214444                foreach (var kvp in currentChildren)
 445                {
 46446                    if (!string.IsNullOrEmpty(kvp.Value.Path))
 447                    {
 46448                        currentChildrenByPath.TryAdd(kvp.Value.Path, kvp.Value);
 449                    }
 450                }
 451
 61452                cancellationToken.ThrowIfCancellationRequested();
 453
 216454                foreach (var child in nonCachedChildren)
 455                {
 47456                    if (!IsLibraryFolderAccessible(directoryService, child, allowRemoveRoot))
 457                    {
 458                        // Preserve inaccessible items so they aren't treated as removed.
 40459                        if (currentChildren.TryGetValue(child.Id, out var childrenToKeep))
 460                        {
 40461                            validChildren.Add(childrenToKeep);
 462                        }
 463
 40464                        continue;
 465                    }
 466
 7467                    if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild))
 468                    {
 5469                        validChildren.Add(currentChild);
 5470                        accessibleChildren.Add(currentChild);
 471
 5472                        if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
 473                        {
 0474                            await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken)
 475                        }
 476                        else
 477                        {
 478                            // metadata is up-to-date; make sure DB has correct images dimensions and hash
 5479                            await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false);
 480                        }
 481
 5482                        continue;
 483                    }
 484
 485                    // Check if an existing item occupies the same path with different type/ID
 2486                    if (!string.IsNullOrEmpty(child.Path)
 2487                        && currentChildrenByPath.TryGetValue(child.Path, out var staleItem)
 2488                        && !staleItem.Id.Equals(child.Id))
 489                    {
 0490                        Logger.LogInformation(
 0491                            "Item type changed at {Path}: {OldType} -> {NewType}, removing stale entry",
 0492                            child.Path,
 0493                            staleItem.GetType().Name,
 0494                            child.GetType().Name);
 495
 0496                        currentChildren.Remove(staleItem.Id);
 0497                        currentChildrenByPath.Remove(child.Path);
 0498                        staleItem.SetParent(null);
 0499                        LibraryManager.DeleteItem(staleItem, new DeleteOptions { DeleteFileLocation = false }, this, fal
 0500                        actuallyRemoved.Add(staleItem);
 501                    }
 502
 503                    // Brand new item - needs to be added
 2504                    child.SetParent(this);
 2505                    newItems.Add(child);
 2506                    validChildren.Add(child);
 2507                    accessibleChildren.Add(child);
 508                }
 509
 510                // That's all the new and changed ones - now see if any have been removed and need cleanup
 61511                var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
 512
 513                // If it's an AggregateFolder, don't remove
 514                // Collect replaced primaries for deferred deletion (after CreateItems)
 61515                var replacedPrimaries = new List<(Video OldPrimary, Video NewPrimary)>();
 516
 517                // Build a set of paths that are alternate versions of valid children
 518                // These items should not be deleted - they're managed by their primary video
 61519                var alternateVersionPaths = validChildren
 61520                    .OfType<Video>()
 61521                    .SelectMany(v => v.LocalAlternateVersions ?? [])
 61522                    .Where(p => !string.IsNullOrEmpty(p))
 61523                    .ToHashSet(StringComparer.OrdinalIgnoreCase);
 524
 61525                if (itemsRemoved.Count > 0)
 526                {
 4527                    foreach (var item in itemsRemoved)
 528                    {
 1529                        if (!item.CanDelete())
 530                        {
 1531                            Logger.LogDebug("Item marked as non-removable, skipping: {Path}", item.Path ?? item.Name);
 1532                            continue;
 533                        }
 534
 535                        // Skip items that are alternate versions of another video
 0536                        if (item is Video video)
 537                        {
 538                            // Check if path is in LocalAlternateVersions of any valid child
 0539                            if (!string.IsNullOrEmpty(item.Path) && alternateVersionPaths.Contains(item.Path))
 540                            {
 0541                                Logger.LogDebug("Item path matches an alternate version, skipping deletion: {Path}", ite
 0542                                continue;
 543                            }
 544                        }
 545
 546                        // Defer deletion if this primary video is being replaced by a new primary
 547                        // that takes over its alternates. Deleting now would trigger premature
 548                        // promotion inside DeleteItem and write stale paths to collection NFOs.
 0549                        if (item is Video primaryVideo
 0550                            && !primaryVideo.PrimaryVersionId.HasValue
 0551                            && primaryVideo.OwnerId.IsEmpty()
 0552                            && (primaryVideo.LocalAlternateVersions ?? []).Any(p => alternateVersionPaths.Contains(p)))
 553                        {
 0554                            var newPrimary = newItems
 0555                                .OfType<Video>()
 0556                                .FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
 0557                                    .Any(p => (primaryVideo.LocalAlternateVersions ?? [])
 0558                                        .Any(op => string.Equals(op, p, StringComparison.OrdinalIgnoreCase))));
 0559                            if (newPrimary is not null)
 560                            {
 0561                                Logger.LogDebug("Deferring deletion of replaced primary: {Path}", item.Path);
 0562                                replacedPrimaries.Add((primaryVideo, newPrimary));
 0563                                actuallyRemoved.Add(item);
 0564                                item.SetParent(null);
 0565                                continue;
 566                            }
 567                        }
 568
 0569                        if (item.IsFileProtocol)
 570                        {
 0571                            Logger.LogDebug("Removed item: {Path}", item.Path);
 572
 0573                            actuallyRemoved.Add(item);
 0574                            item.SetParent(null);
 0575                            LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, fals
 576                        }
 577                    }
 578                }
 579
 61580                if (newItems.Count > 0)
 581                {
 2582                    LibraryManager.CreateItems(newItems, this, cancellationToken);
 583                }
 584
 585                // Process deferred replaced-primary deletions now that new primaries exist in DB/cache.
 586                // This avoids the premature promotion that would occur if DeleteItem ran before CreateItems.
 122587                foreach (var (oldPrimary, newPrimary) in replacedPrimaries)
 588                {
 0589                    Logger.LogInformation(
 0590                        "Processing deferred deletion of replaced primary {OldName} ({OldId}), new primary {NewName} ({N
 0591                        oldPrimary.Name,
 0592                        oldPrimary.Id,
 0593                        newPrimary.Name,
 0594                        newPrimary.Id);
 595
 596                    // Reroute collection/playlist references from old primary to new primary
 0597                    await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(
 598
 599                    // Transfer alternates from old primary to new primary
 0600                    var localAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary).ToHashSet();
 0601                    var allAlternateIds = localAlternateIds
 0602                        .Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
 0603                        .Distinct()
 0604                        .ToList();
 605
 0606                    foreach (var altId in allAlternateIds)
 607                    {
 0608                        if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
 609                        {
 0610                            altVideo.SetPrimaryVersionId(newPrimary.Id);
 0611                            altVideo.OwnerId = localAlternateIds.Contains(altVideo.Id) ? newPrimary.Id : Guid.Empty;
 0612                            await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).Confi
 613                        }
 614                    }
 615
 616                    // Clear alternate arrays so DeleteItem won't trigger promotion
 0617                    oldPrimary.LocalAlternateVersions = [];
 0618                    oldPrimary.LinkedAlternateVersions = [];
 619
 620                    // Safe to delete now — no promotion will happen
 0621                    LibraryManager.DeleteItem(oldPrimary, new DeleteOptions { DeleteFileLocation = false }, this, false)
 0622                }
 623
 624                // Demote old primaries that are now alternate versions of newly created primaries.
 625                // This handles the case where a new file is added that becomes the new primary
 626                // (e.g. movie-2 added, movie-3 was primary → movie-3 needs demotion).
 627                // Items in replacedPrimaries are excluded (already in actuallyRemoved).
 61628                var oldPrimariesToDemote = new List<(Video OldPrimary, Video NewPrimary)>();
 124629                foreach (var item in itemsRemoved.Except(actuallyRemoved))
 630                {
 1631                    if (item is Video video
 1632                        && video.OwnerId.IsEmpty()
 1633                        && !string.IsNullOrEmpty(item.Path)
 1634                        && alternateVersionPaths.Contains(item.Path))
 635                    {
 0636                        var newPrimary = newItems
 0637                            .OfType<Video>()
 0638                            .FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
 0639                                .Any(p => string.Equals(p, item.Path, StringComparison.OrdinalIgnoreCase)));
 0640                        if (newPrimary is not null)
 641                        {
 0642                            oldPrimariesToDemote.Add((video, newPrimary));
 643                        }
 644                    }
 645                }
 646
 122647                foreach (var (oldPrimary, newPrimary) in oldPrimariesToDemote)
 648                {
 0649                    Logger.LogInformation(
 0650                        "Demoting old primary {OldName} ({OldId}) to alternate of new primary {NewName} ({NewId})",
 0651                        oldPrimary.Name,
 0652                        oldPrimary.Id,
 0653                        newPrimary.Name,
 0654                        newPrimary.Id);
 655
 656                    // First: update old primary's alternate items to point to new primary.
 657                    // Order matters — update alternates FIRST so they don't get orphan-deleted
 658                    // when old primary's arrays are cleared.
 0659                    var oldAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary)
 0660                        .Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
 0661                        .Distinct()
 0662                        .ToList();
 663
 0664                    foreach (var altId in oldAlternateIds)
 665                    {
 0666                        if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
 667                        {
 0668                            altVideo.SetPrimaryVersionId(newPrimary.Id);
 0669                            altVideo.OwnerId = newPrimary.Id;
 0670                            await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).Confi
 671                        }
 672                    }
 673
 674                    // Then: demote old primary — clear its arrays and set it as alternate of new primary
 0675                    oldPrimary.LocalAlternateVersions = [];
 0676                    oldPrimary.LinkedAlternateVersions = [];
 0677                    oldPrimary.SetPrimaryVersionId(newPrimary.Id);
 0678                    oldPrimary.OwnerId = newPrimary.Id;
 0679                    await oldPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAw
 680
 681                    // Re-route playlist/collection references from old primary to new primary
 0682                    await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(
 0683                }
 684
 685                // After removing items, reattach any detached user data to remaining children
 686                // that share the same user data keys (eg. same episode replaced with a new file).
 61687                if (actuallyRemoved.Count > 0)
 688                {
 0689                    var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet();
 0690                    foreach (var child in validChildren)
 691                    {
 0692                        if (child.GetUserDataKeys().Any(removedKeys.Contains))
 693                        {
 0694                            await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
 695                        }
 696                    }
 0697                }
 61698            }
 699            else
 700            {
 0701                validChildrenNeedGeneration = true;
 702            }
 703
 61704            progress.Report(ProgressHelpers.UpdatedChildItems);
 705
 61706            if (recursive)
 707            {
 18708                ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems);
 709            }
 710
 61711            cancellationToken.ThrowIfCancellationRequested();
 712
 61713            if (recursive)
 714            {
 18715                var folder = this;
 18716                var innerProgress = new Progress<double>(innerPercent =>
 18717                {
 18718                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.Scanned
 18719
 18720                    progress.Report(percent);
 18721
 18722                    ProviderManager.OnRefreshProgress(folder, percent);
 18723                });
 724
 18725                if (validChildrenNeedGeneration)
 726                {
 0727                    validChildren = Children.ToList();
 0728                    validChildrenNeedGeneration = false;
 729                }
 730
 18731                await ValidateSubFolders(accessibleChildren.OfType<Folder>().ToList(), directoryService, innerProgress, 
 732            }
 733
 61734            if (refreshChildMetadata)
 735            {
 61736                progress.Report(ProgressHelpers.ScannedSubfolders);
 737
 61738                if (recursive)
 739                {
 18740                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders);
 741                }
 742
 61743                var container = this as IMetadataContainer;
 744
 61745                var folder = this;
 61746                var innerProgress = new Progress<double>(innerPercent =>
 61747                {
 61748                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.Refresh
 61749
 61750                    progress.Report(percent);
 61751
 61752                    if (recursive)
 61753                    {
 61754                        ProviderManager.OnRefreshProgress(folder, percent);
 61755                    }
 61756                });
 757
 61758                if (container is not null)
 759                {
 0760                    await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).Co
 761                }
 762                else
 763                {
 61764                    if (validChildrenNeedGeneration)
 765                    {
 0766                        Children = null; // invalidate cached children.
 0767                        validChildren = Children.ToList();
 768                    }
 769
 61770                    await RefreshMetadataRecursive(accessibleChildren, refreshOptions, recursive, innerProgress, cancell
 771                }
 772            }
 61773        }
 774
 775        private async Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, boo
 776        {
 61777            await RunTasks(
 61778                (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFold
 61779                children,
 61780                progress,
 61781                cancellationToken).ConfigureAwait(false);
 61782        }
 783
 784        private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOp
 785        {
 0786            if (container is Series series)
 787            {
 0788                await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 789            }
 790
 0791            await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
 0792        }
 793
 794        private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, I
 795        {
 7796            if (child is IMetadataContainer container)
 797            {
 0798                await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAw
 799            }
 800            else
 801            {
 7802                if (refreshOptions.RefreshItem(child))
 803                {
 7804                    await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 805                }
 806
 7807                if (recursive && child is Folder folder)
 808                {
 0809                    folder.Children = null; // invalidate cached children.
 0810                    await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions
 811                }
 812            }
 7813        }
 814
 815        /// <summary>
 816        /// Refreshes the children.
 817        /// </summary>
 818        /// <param name="children">The children.</param>
 819        /// <param name="directoryService">The directory service.</param>
 820        /// <param name="progress">The progress.</param>
 821        /// <param name="cancellationToken">The cancellation token.</param>
 822        /// <returns>Task.</returns>
 823        private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<doub
 824        {
 18825            await RunTasks(
 18826                (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, dire
 18827                children,
 18828                progress,
 18829                cancellationToken).ConfigureAwait(false);
 18830        }
 831
 832        /// <summary>
 833        /// Runs an action block on a list of children.
 834        /// </summary>
 835        /// <param name="task">The task to run for each child.</param>
 836        /// <param name="children">The list of children.</param>
 837        /// <param name="progress">The progress.</param>
 838        /// <param name="cancellationToken">The cancellation token.</param>
 839        /// <returns>Task.</returns>
 840        private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progr
 841        {
 79842            await LimitedConcurrencyLibraryScheduler
 79843                .Enqueue(
 79844                    children.ToArray(),
 79845                    task,
 79846                    progress,
 79847                    cancellationToken)
 79848                .ConfigureAwait(false);
 79849        }
 850
 851        /// <summary>
 852        /// Get the children of this folder from the actual file system.
 853        /// </summary>
 854        /// <returns>IEnumerable{BaseItem}.</returns>
 855        /// <param name="directoryService">The directory service to use for operation.</param>
 856        /// <returns>Returns set of base items.</returns>
 857        protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
 858        {
 61859            var collectionType = LibraryManager.GetContentType(this);
 61860            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 861
 61862            return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryO
 863        }
 864
 865        /// <summary>
 866        /// Get our children from the repo - stubbed for now.
 867        /// </summary>
 868        /// <returns>IEnumerable{BaseItem}.</returns>
 869        protected IReadOnlyList<BaseItem> GetCachedChildren()
 870        {
 150871            return ItemRepository.GetItemList(new InternalItemsQuery
 150872            {
 150873                Parent = this,
 150874                GroupByPresentationUniqueKey = false,
 150875                DtoOptions = new DtoOptions(true)
 150876            });
 877        }
 878
 879        public virtual int GetChildCount(User user)
 880        {
 0881            if (LinkedChildren.Length > 0)
 882            {
 0883                if (this is not ICollectionFolder)
 884                {
 0885                    return GetChildren(user, true).Count;
 886                }
 887            }
 888
 0889            var result = GetItems(new InternalItemsQuery(user)
 0890            {
 0891                Recursive = false,
 0892                Limit = 0,
 0893                Parent = this,
 0894                DtoOptions = new DtoOptions(false)
 0895                {
 0896                    EnableImages = false
 0897                }
 0898            });
 899
 0900            return result.TotalRecordCount;
 901        }
 902
 903        public virtual int GetRecursiveChildCount(User user)
 904        {
 0905            return GetItems(new InternalItemsQuery(user)
 0906            {
 0907                Recursive = true,
 0908                IsFolder = false,
 0909                IsVirtualItem = false,
 0910                EnableTotalRecordCount = true,
 0911                Limit = 0,
 0912                DtoOptions = new DtoOptions(false)
 0913                {
 0914                    EnableImages = false
 0915                }
 0916            }).TotalRecordCount;
 917        }
 918
 919        public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
 920        {
 17921            if (!query.ForceDirect && CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
 922            {
 0923                query.CollapseBoxSetItems = true;
 0924                SetCollapseBoxSetItemTypes(query);
 925            }
 926
 17927            if (this is not UserRootFolder
 17928                && this is not AggregateFolder
 17929                && query.ParentId.IsEmpty())
 930            {
 17931                query.Parent = this;
 932            }
 933
 934            // BoxSets and Playlists can have per-user visibility (shares/open access) that is stored in the
 935            // serialized item data and cannot be evaluated by the database query, so filter them in memory.
 17936            if (query.IncludeItemTypes.Length > 0
 17937                && query.IncludeItemTypes.All(t => t == BaseItemKind.BoxSet || t == BaseItemKind.Playlist))
 938            {
 0939                return QueryWithPostFiltering(query);
 940            }
 941
 17942            return LibraryManager.GetItemsResult(query);
 943        }
 944
 945        protected QueryResult<BaseItem> QueryWithPostFiltering(InternalItemsQuery query)
 946        {
 1947            var startIndex = query.StartIndex;
 1948            var limit = query.Limit;
 949
 1950            query.StartIndex = null;
 1951            query.Limit = null;
 952
 1953            IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query);
 1954            var user = query.User;
 955
 1956            if (user is not null)
 957            {
 958                // needed for boxsets and playlists
 1959                itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User));
 960            }
 961
 962            IEnumerable<BaseItem> returnItems;
 1963            int totalCount = 0;
 964
 1965            if (query.EnableTotalRecordCount)
 966            {
 0967                var itemArray = itemsList.ToArray();
 0968                totalCount = itemArray.Length;
 0969                returnItems = itemArray;
 970            }
 971            else
 972            {
 1973                returnItems = itemsList;
 974            }
 975
 1976            if (limit.HasValue)
 977            {
 0978                returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value);
 979            }
 1980            else if (startIndex.HasValue)
 981            {
 0982                returnItems = returnItems.Skip(startIndex.Value);
 983            }
 984
 1985            return new QueryResult<BaseItem>(
 1986                query.StartIndex,
 1987                totalCount,
 1988                returnItems.ToArray());
 989        }
 990
 991        private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
 992        {
 0993            return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
 994        }
 995
 996        public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
 997        {
 0998            if (query.ItemIds.Length > 0)
 999            {
 01000                var result = LibraryManager.GetItemsResult(query);
 1001
 01002                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 1003                {
 01004                    result.Items = SortItemsByRequest(query, result.Items);
 1005                }
 1006
 01007                return result;
 1008            }
 1009
 01010            return GetItemsInternal(query);
 1011        }
 1012
 1013        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1014        {
 181015            query.EnableTotalRecordCount = false;
 1016
 181017            if (query.ItemIds.Length > 0)
 1018            {
 01019                var result = LibraryManager.GetItemList(query);
 1020
 01021                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 1022                {
 01023                    return SortItemsByRequest(query, result);
 1024                }
 1025
 01026                return result;
 1027            }
 1028
 181029            return GetItemsInternal(query).Items;
 1030        }
 1031
 1032        protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
 1033        {
 171034            if (SourceType == SourceType.Channel)
 1035            {
 1036                try
 1037                {
 01038                    query.Parent = this;
 01039                    query.ChannelIds = new[] { ChannelId };
 1040
 1041                    // Don't blow up here because it could cause parent screens with other content to fail
 01042                    return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None)
 1043                }
 01044                catch
 1045                {
 1046                    // Already logged at lower levels
 01047                    return new QueryResult<BaseItem>();
 1048                }
 1049            }
 1050
 171051            if (query.Recursive)
 1052            {
 171053                return QueryRecursive(query);
 1054            }
 1055
 01056            var user = query.User;
 1057
 1058            IEnumerable<BaseItem> items;
 1059
 01060            int totalItemCount = 0;
 01061            if (query.User is null)
 1062            {
 01063                items = UserViewBuilder.Filter(Children, user, query, UserDataManager, LibraryManager);
 01064                totalItemCount = items.Count();
 1065            }
 1066            else
 1067            {
 1068                // need to pass this param to the children.
 1069                // Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
 01070                var childQuery = new InternalItemsQuery
 01071                {
 01072                    DisplayAlbumFolders = query.DisplayAlbumFolders,
 01073                    NameStartsWith = query.NameStartsWith,
 01074                    NameStartsWithOrGreater = query.NameStartsWithOrGreater,
 01075                    NameLessThan = query.NameLessThan
 01076                };
 1077
 01078                items = UserViewBuilder.Filter(
 01079                    GetChildren(user, true, out totalItemCount, childQuery),
 01080                    user,
 01081                    query,
 01082                    UserDataManager,
 01083                    LibraryManager);
 1084            }
 1085
 01086            return PostFilterAndSort(items, query);
 01087        }
 1088
 1089        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
 1090        {
 01091            var user = query.User;
 1092
 1093            // Check recursive - don't substitute in plain folder views
 01094            if (user is not null)
 1095            {
 01096                items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
 1097
 1098                // After collapse, BoxSets may have replaced items whose names matched the filter
 1099                // but the BoxSet's own name may not match. Re-apply name filtering so BoxSets
 1100                // appear under the correct letter (e.g. "Jump Street" under J, not under #).
 01101                items = ApplyNameFilter(items, query);
 1102            }
 1103
 01104            return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
 1105        }
 1106
 1107        private static IEnumerable<BaseItem> ApplyNameFilter(IEnumerable<BaseItem> items, InternalItemsQuery query)
 1108        {
 01109            if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
 1110            {
 01111                items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)
 1112            }
 1113
 01114            if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
 1115            {
 01116                items = items.Where(i => string.Compare(i.SortName, query.NameStartsWithOrGreater, StringComparison.Ordi
 1117            }
 1118
 01119            if (!string.IsNullOrWhiteSpace(query.NameLessThan))
 1120            {
 01121                items = items.Where(i => string.Compare(i.SortName, query.NameLessThan, StringComparison.OrdinalIgnoreCa
 1122            }
 1123
 01124            return items;
 1125        }
 1126
 1127        private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
 1128            IEnumerable<BaseItem> items,
 1129            InternalItemsQuery query,
 1130            BaseItem queryParent,
 1131            User user,
 1132            IServerConfigurationManager configurationManager,
 1133            ICollectionManager collectionManager)
 1134        {
 01135            ArgumentNullException.ThrowIfNull(items);
 1136
 01137            if (!CollapseBoxSetItems(query, queryParent, user, configurationManager))
 1138            {
 01139                return items;
 1140            }
 1141
 01142            var config = configurationManager.Configuration;
 1143
 01144            bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
 01145            bool collapseSeries = config.EnableGroupingShowsIntoCollections;
 1146
 01147            if (user is null || (collapseMovies && collapseSeries))
 1148            {
 01149                return collectionManager.CollapseItemsWithinBoxSets(items, user);
 1150            }
 1151
 01152            if (!collapseMovies && !collapseSeries)
 1153            {
 01154                return items;
 1155            }
 1156
 01157            var collapsibleItems = new List<BaseItem>();
 01158            var remainingItems = new List<BaseItem>();
 1159
 01160            foreach (var item in items)
 1161            {
 01162                if ((collapseMovies && item is Movie) || (collapseSeries && item is Series))
 1163                {
 01164                    collapsibleItems.Add(item);
 1165                }
 1166                else
 1167                {
 01168                    remainingItems.Add(item);
 1169                }
 1170            }
 1171
 01172            if (collapsibleItems.Count == 0)
 1173            {
 01174                return remainingItems;
 1175            }
 1176
 01177            var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user);
 1178
 01179            return collapsedItems.Concat(remainingItems);
 1180        }
 1181
 1182        private static bool CollapseBoxSetItems(
 1183            InternalItemsQuery query,
 1184            BaseItem queryParent,
 1185            User user,
 1186            IServerConfigurationManager configurationManager)
 1187        {
 1188            // Could end up stuck in a loop like this
 171189            if (queryParent is BoxSet)
 1190            {
 01191                return false;
 1192            }
 1193
 171194            if (queryParent is Season)
 1195            {
 01196                return false;
 1197            }
 1198
 171199            if (queryParent is MusicAlbum)
 1200            {
 01201                return false;
 1202            }
 1203
 171204            if (queryParent is MusicArtist)
 1205            {
 01206                return false;
 1207            }
 1208
 171209            var param = query.CollapseBoxSetItems;
 171210            if (param.HasValue)
 1211            {
 171212                return param.Value && AllowBoxSetCollapsing(query);
 1213            }
 1214
 01215            var config = configurationManager.Configuration;
 1216
 01217            bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Mov
 01218            bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Ser
 1219
 01220            bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
 01221            bool collapseSeries = config.EnableGroupingShowsIntoCollections;
 1222
 01223            if (user is not null)
 1224            {
 01225                bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries);
 01226                return canCollapse && AllowBoxSetCollapsing(query);
 1227            }
 1228
 01229            return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query);
 1230        }
 1231
 1232        private void SetCollapseBoxSetItemTypes(InternalItemsQuery query)
 1233        {
 01234            var config = ConfigurationManager.Configuration;
 01235            bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
 01236            bool collapseSeries = config.EnableGroupingShowsIntoCollections;
 1237
 01238            if (collapseMovies && collapseSeries)
 1239            {
 1240                // Empty means collapse all types
 01241                query.CollapseBoxSetItemTypes = [];
 01242                return;
 1243            }
 1244
 01245            var types = new List<BaseItemKind>();
 01246            if (collapseMovies)
 1247            {
 01248                types.Add(BaseItemKind.Movie);
 1249            }
 1250
 01251            if (collapseSeries)
 1252            {
 01253                types.Add(BaseItemKind.Series);
 1254            }
 1255
 01256            query.CollapseBoxSetItemTypes = types.ToArray();
 01257        }
 1258
 1259        private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
 1260        {
 01261            if (request.IsFavorite.HasValue)
 1262            {
 01263                return false;
 1264            }
 1265
 01266            if (request.IsFavoriteOrLiked.HasValue)
 1267            {
 01268                return false;
 1269            }
 1270
 01271            if (request.IsLiked.HasValue)
 1272            {
 01273                return false;
 1274            }
 1275
 01276            if (request.IsPlayed.HasValue)
 1277            {
 01278                return false;
 1279            }
 1280
 01281            if (request.IsResumable.HasValue)
 1282            {
 01283                return false;
 1284            }
 1285
 01286            if (request.IsFolder.HasValue)
 1287            {
 01288                return false;
 1289            }
 1290
 01291            if (request.Genres.Count > 0)
 1292            {
 01293                return false;
 1294            }
 1295
 01296            if (request.GenreIds.Count > 0)
 1297            {
 01298                return false;
 1299            }
 1300
 01301            if (request.HasImdbId.HasValue)
 1302            {
 01303                return false;
 1304            }
 1305
 01306            if (request.HasOfficialRating.HasValue)
 1307            {
 01308                return false;
 1309            }
 1310
 01311            if (request.HasOverview.HasValue)
 1312            {
 01313                return false;
 1314            }
 1315
 01316            if (request.HasParentalRating.HasValue)
 1317            {
 01318                return false;
 1319            }
 1320
 01321            if (request.HasSpecialFeature.HasValue)
 1322            {
 01323                return false;
 1324            }
 1325
 01326            if (request.HasSubtitles.HasValue)
 1327            {
 01328                return false;
 1329            }
 1330
 01331            if (request.HasThemeSong.HasValue)
 1332            {
 01333                return false;
 1334            }
 1335
 01336            if (request.HasThemeVideo.HasValue)
 1337            {
 01338                return false;
 1339            }
 1340
 01341            if (request.HasTmdbId.HasValue)
 1342            {
 01343                return false;
 1344            }
 1345
 01346            if (request.HasTrailer.HasValue)
 1347            {
 01348                return false;
 1349            }
 1350
 01351            if (request.ImageTypes.Length > 0)
 1352            {
 01353                return false;
 1354            }
 1355
 01356            if (request.Is3D.HasValue)
 1357            {
 01358                return false;
 1359            }
 1360
 01361            if (request.Is4K.HasValue)
 1362            {
 01363                return false;
 1364            }
 1365
 01366            if (request.IsHD.HasValue)
 1367            {
 01368                return false;
 1369            }
 1370
 01371            if (request.IsLocked.HasValue)
 1372            {
 01373                return false;
 1374            }
 1375
 01376            if (request.IsPlaceHolder.HasValue)
 1377            {
 01378                return false;
 1379            }
 1380
 01381            if (!string.IsNullOrWhiteSpace(request.Person))
 1382            {
 01383                return false;
 1384            }
 1385
 01386            if (request.PersonIds.Length > 0)
 1387            {
 01388                return false;
 1389            }
 1390
 01391            if (request.ItemIds.Length > 0)
 1392            {
 01393                return false;
 1394            }
 1395
 01396            if (request.StudioIds.Length > 0)
 1397            {
 01398                return false;
 1399            }
 1400
 01401            if (request.VideoTypes.Length > 0)
 1402            {
 01403                return false;
 1404            }
 1405
 01406            if (request.Years.Length > 0)
 1407            {
 01408                return false;
 1409            }
 1410
 01411            if (request.Tags.Length > 0)
 1412            {
 01413                return false;
 1414            }
 1415
 01416            if (request.OfficialRatings.Length > 0)
 1417            {
 01418                return false;
 1419            }
 1420
 01421            if (request.MinIndexNumber.HasValue)
 1422            {
 01423                return false;
 1424            }
 1425
 01426            if (request.OrderBy.Any(o =>
 01427                o.OrderBy == ItemSortBy.CommunityRating ||
 01428                o.OrderBy == ItemSortBy.CriticRating ||
 01429                o.OrderBy == ItemSortBy.Runtime))
 1430            {
 01431                return false;
 1432            }
 1433
 01434            return true;
 1435        }
 1436
 1437        public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount
 1438        {
 101439            ArgumentNullException.ThrowIfNull(user);
 101440            query ??= new InternalItemsQuery();
 101441            query.User = user;
 1442
 1443            // the true root should return our users root folder children
 101444            if (IsPhysicalRoot)
 1445            {
 01446                return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren, out totalItemCount);
 1447            }
 1448
 101449            var result = new Dictionary<Guid, BaseItem>();
 1450
 101451            totalItemCount = AddChildren(user, includeLinkedChildren, result, false, query);
 1452
 101453            return result.Values.ToArray();
 1454        }
 1455
 1456        public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery que
 1457        {
 101458            return GetChildren(user, includeLinkedChildren, out _, query);
 1459        }
 1460
 1461        protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
 1462        {
 101463            return Children;
 1464        }
 1465
 1466        /// <summary>
 1467        /// Adds the children to list.
 1468        /// </summary>
 1469        private int AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive
 1470        {
 1471            // Prevent infinite recursion of nested folders
 101472            visitedFolders ??= new HashSet<Folder>();
 101473            if (!visitedFolders.Add(this))
 1474            {
 01475                return 0;
 1476            }
 1477
 1478            // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in mus
 101479            IEnumerable<BaseItem> children = null;
 101480            if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
 1481            {
 01482                children = Children;
 01483                query = null;
 1484            }
 1485
 1486            // If there are not sub-folders, proceed as normal.
 101487            if (children is null)
 1488            {
 101489                children = GetEligibleChildrenForRecursiveChildren(user);
 1490            }
 1491
 101492            if (includeLinkedChildren)
 1493            {
 101494                children = children.Concat(GetLinkedChildren(user)).ToArray();
 1495            }
 1496
 101497            return AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFol
 1498        }
 1499
 1500        private int AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dic
 1501        {
 101502            query ??= new InternalItemsQuery();
 101503            var limit = query.Limit > 0 ? query.Limit : int.MaxValue;
 101504            query.Limit = 0;
 1505
 101506            var visibleChildren = children
 101507                .Where(e => e.IsVisible(user))
 101508                .ToArray();
 1509
 101510            var realChildren = UserViewBuilder.Filter(visibleChildren, query.User, query, UserDataManager, LibraryManage
 101511                .ToArray();
 1512
 101513            var childCount = realChildren.Length;
 101514            if (result.Count < limit)
 1515            {
 101516                var remainingCount = (int)(limit - result.Count);
 401517                foreach (var child in realChildren
 101518                    .Skip(query.StartIndex ?? 0)
 101519                    .Take(remainingCount))
 1520                {
 101521                    result[child.Id] = child;
 1522                }
 1523            }
 1524
 101525            if (recursive)
 1526            {
 01527                foreach (var child in visibleChildren
 01528                    .Where(e => e.IsFolder)
 01529                    .OfType<Folder>())
 1530                {
 01531                    childCount += child.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
 1532                }
 1533            }
 1534
 101535            return childCount;
 1536        }
 1537
 1538        public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCo
 1539        {
 01540            ArgumentNullException.ThrowIfNull(user);
 1541
 01542            var result = new Dictionary<Guid, BaseItem>();
 1543
 01544            totalCount = AddChildren(user, true, result, true, query);
 1545
 01546            return result.Values.ToArray();
 1547        }
 1548
 1549        /// <summary>
 1550        /// Gets the recursive children.
 1551        /// </summary>
 1552        /// <returns>IList{BaseItem}.</returns>
 1553        public IReadOnlyList<BaseItem> GetRecursiveChildren()
 1554        {
 01555            return GetRecursiveChildren(true);
 1556        }
 1557
 1558        public IReadOnlyList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
 1559        {
 01560            return GetRecursiveChildren(i => true, includeLinkedChildren);
 1561        }
 1562
 1563        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
 1564        {
 01565            return GetRecursiveChildren(filter, true);
 1566        }
 1567
 1568        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
 1569        {
 01570            var result = new Dictionary<Guid, BaseItem>();
 1571
 01572            AddChildrenToList(result, includeLinkedChildren, true, filter);
 1573
 01574            return result.Values.ToArray();
 1575        }
 1576
 1577        /// <summary>
 1578        /// Adds the children to list.
 1579        /// </summary>
 1580        private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Fu
 1581        {
 01582            foreach (var child in Children)
 1583            {
 01584                if (filter is null || filter(child))
 1585                {
 01586                    result[child.Id] = child;
 1587                }
 1588
 01589                if (recursive && child.IsFolder)
 1590                {
 01591                    var folder = (Folder)child;
 1592
 1593                    // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loo
 01594                    folder.AddChildrenToList(result, false, true, filter);
 1595                }
 1596            }
 1597
 01598            if (includeLinkedChildren)
 1599            {
 01600                foreach (var child in GetLinkedChildren())
 1601                {
 01602                    if (filter is null || filter(child))
 1603                    {
 01604                        result[child.Id] = child;
 1605                    }
 1606                }
 1607            }
 01608        }
 1609
 1610        /// <summary>
 1611        /// Gets the linked children.
 1612        /// </summary>
 1613        /// <returns>IEnumerable{BaseItem}.</returns>
 1614        public List<BaseItem> GetLinkedChildren()
 1615        {
 101616            var resolved = ResolveLinkedChildren(LinkedChildren);
 101617            var list = new List<BaseItem>(resolved.Count);
 201618            foreach (var (_, item) in resolved)
 1619            {
 01620                list.Add(item);
 1621            }
 1622
 101623            return list;
 1624        }
 1625
 1626        public bool ContainsLinkedChildByItemId(Guid itemId)
 1627        {
 01628            var linkedChildren = LinkedChildren;
 01629            foreach (var i in linkedChildren)
 1630            {
 01631                if (i.ItemId.HasValue)
 1632                {
 01633                    if (i.ItemId.Value.Equals(itemId))
 1634                    {
 01635                        return true;
 1636                    }
 1637
 1638                    continue;
 1639                }
 1640
 01641                var child = GetLinkedChild(i);
 1642
 01643                if (child is not null && child.Id.Equals(itemId))
 1644                {
 01645                    return true;
 1646                }
 1647            }
 1648
 01649            return false;
 1650        }
 1651
 1652        public List<BaseItem> GetLinkedChildren(User user)
 1653        {
 101654            if (!FilterLinkedChildrenPerUser || user is null)
 1655            {
 101656                return GetLinkedChildren();
 1657            }
 1658
 01659            var linkedChildren = LinkedChildren;
 01660            var list = new List<BaseItem>(linkedChildren.Length);
 1661
 01662            if (linkedChildren.Length == 0)
 1663            {
 01664                return list;
 1665            }
 1666
 01667            var allUserRootChildren = LibraryManager.GetUserRootFolder()
 01668                .GetChildren(user, true)
 01669                .OfType<Folder>()
 01670                .ToList();
 1671
 01672            var collectionFolderIds = allUserRootChildren
 01673                .Select(i => i.Id)
 01674                .ToList();
 1675
 01676            foreach (var i in linkedChildren)
 1677            {
 01678                var child = GetLinkedChild(i);
 1679
 01680                if (child is null)
 1681                {
 1682                    continue;
 1683                }
 1684
 01685                var childOwner = child.GetOwner() ?? child;
 1686
 01687                if (child is not IItemByName)
 1688                {
 01689                    var childProtocol = childOwner.PathProtocol;
 01690                    if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File)
 1691                    {
 01692                        if (!childOwner.IsVisibleStandalone(user))
 1693                        {
 01694                            continue;
 1695                        }
 1696                    }
 1697                    else
 1698                    {
 01699                        var itemCollectionFolderIds =
 01700                            LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id);
 1701
 01702                        if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains))
 1703                        {
 1704                            continue;
 1705                        }
 1706                    }
 1707                }
 1708
 01709                list.Add(child);
 1710            }
 1711
 01712            return list;
 1713        }
 1714
 1715        /// <summary>
 1716        /// Gets the linked children.
 1717        /// </summary>
 1718        /// <returns>IEnumerable{BaseItem}.</returns>
 1719        public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
 1720        {
 01721            return ResolveLinkedChildren(LinkedChildren)
 01722                .Select(t => new Tuple<LinkedChild, BaseItem>(t.Info, t.Item))
 01723                .ToArray();
 1724        }
 1725
 1726        /// <summary>
 1727        /// Resolves a list of <see cref="LinkedChild"/> entries to their <see cref="BaseItem"/> targets,
 1728        /// batching the database lookup across all entries with a known ItemId.
 1729        /// Entries without a usable ItemId fall back to the per-entry <see cref="BaseItem.GetLinkedChild"/>
 1730        /// path (legacy path-based resolution).
 1731        /// </summary>
 1732        /// <param name="linkedChildren">Linked children to resolve.</param>
 1733        /// <returns>Each input entry paired with its resolved item; entries that fail to resolve are dropped.</returns>
 1734        private List<(LinkedChild Info, BaseItem Item)> ResolveLinkedChildren(IReadOnlyList<LinkedChild> linkedChildren)
 1735        {
 101736            var resolved = new List<(LinkedChild Info, BaseItem Item)>(linkedChildren.Count);
 101737            if (linkedChildren.Count == 0)
 1738            {
 101739                return resolved;
 1740            }
 1741
 01742            var idsToBatch = new HashSet<Guid>();
 01743            foreach (var info in linkedChildren)
 1744            {
 01745                if (info.ItemId.HasValue && !info.ItemId.Value.IsEmpty())
 1746                {
 01747                    idsToBatch.Add(info.ItemId.Value);
 1748                }
 1749            }
 1750
 01751            Dictionary<Guid, BaseItem> byId = null;
 01752            if (idsToBatch.Count > 0)
 1753            {
 01754                var batched = LibraryManager.GetItemList(new InternalItemsQuery
 01755                {
 01756                    ItemIds = [.. idsToBatch]
 01757                });
 01758                byId = new Dictionary<Guid, BaseItem>(batched.Count);
 01759                foreach (var item in batched)
 1760                {
 01761                    byId[item.Id] = item;
 1762                }
 1763            }
 1764
 01765            foreach (var info in linkedChildren)
 1766            {
 01767                BaseItem item = null;
 01768                if (byId is not null && info.ItemId.HasValue && byId.TryGetValue(info.ItemId.Value, out var batchedItem)
 1769                {
 01770                    item = batchedItem;
 1771                }
 1772                else
 1773                {
 1774                    // ItemId is missing/empty or the batched query couldn't return the item
 1775                    // (e.g. it has been removed). Fall back to per-entry resolution, which also
 1776                    // handles legacy path-based linked children.
 01777                    item = GetLinkedChild(info);
 1778                }
 1779
 01780                if (item is not null)
 1781                {
 01782                    resolved.Add((info, item));
 1783                }
 1784            }
 1785
 01786            return resolved;
 1787        }
 1788
 1789        protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystem
 1790        {
 391791            var changesFound = false;
 1792
 391793            if (IsFileProtocol)
 1794            {
 391795                if (RefreshLinkedChildren(fileSystemChildren))
 1796                {
 01797                    changesFound = true;
 1798                }
 1799            }
 1800
 391801            var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).Configur
 1802
 391803            return baseHasChanges || changesFound;
 391804        }
 1805
 1806        /// <summary>
 1807        /// Refreshes the linked children.
 1808        /// </summary>
 1809        /// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
 1810        /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
 1811        protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
 1812        {
 221813            if (SupportsShortcutChildren)
 1814            {
 221815                var newShortcutLinks = fileSystemChildren
 221816                    .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName))
 221817                    .Select(i =>
 221818                    {
 221819                        try
 221820                        {
 221821                            Logger.LogDebug("Found shortcut at {0}", i.FullName);
 221822
 221823                            var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShor
 221824
 221825                            if (!string.IsNullOrEmpty(resolvedPath))
 221826                            {
 221827#pragma warning disable CS0618 // Type or member is obsolete - shortcuts require Path for lazy ItemId resolution
 221828                                return new LinkedChild
 221829                                {
 221830                                    Path = resolvedPath,
 221831                                    Type = LinkedChildType.Shortcut
 221832                                };
 221833#pragma warning restore CS0618
 221834                            }
 221835
 221836                            Logger.LogError("Error resolving shortcut {0}", i.FullName);
 221837
 221838                            return null;
 221839                        }
 221840                        catch (IOException ex)
 221841                        {
 221842                            Logger.LogError(ex, "Error resolving shortcut {0}", i.FullName);
 221843                            return null;
 221844                        }
 221845                    })
 221846                    .Where(i => i is not null)
 221847                    .ToList();
 1848
 221849                var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
 1850
 221851                if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem)))
 1852                {
 01853                    Logger.LogInformation("Shortcut links have changed for {0}", Path);
 1854
 01855                    newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual));
 01856                    LinkedChildren = newShortcutLinks.ToArray();
 01857                    return true;
 1858                }
 1859            }
 1860
 221861            return false;
 1862        }
 1863
 1864        /// <summary>
 1865        /// Marks the played.
 1866        /// </summary>
 1867        /// <param name="user">The user.</param>
 1868        /// <param name="datePlayed">The date played.</param>
 1869        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1870        public override void MarkPlayed(
 1871            User user,
 1872            DateTime? datePlayed,
 1873            bool resetPosition)
 1874        {
 01875            var query = new InternalItemsQuery
 01876            {
 01877                User = user,
 01878                Recursive = true,
 01879                IsFolder = false,
 01880                EnableTotalRecordCount = false
 01881            };
 1882
 01883            if (!user.DisplayMissingEpisodes)
 1884            {
 01885                query.IsVirtualItem = false;
 1886            }
 1887
 01888            var itemsResult = GetItemList(query);
 1889
 1890            // Sweep through recursively and update status
 01891            foreach (var item in itemsResult)
 1892            {
 01893                if (item.IsVirtualItem)
 1894                {
 1895                    // The querying doesn't support virtual unaired
 01896                    var episode = item as Episode;
 01897                    if (episode is not null && episode.IsUnaired)
 1898                    {
 1899                        continue;
 1900                    }
 1901                }
 1902
 01903                item.MarkPlayed(user, datePlayed, resetPosition);
 1904            }
 01905        }
 1906
 1907        /// <summary>
 1908        /// Marks the unplayed.
 1909        /// </summary>
 1910        /// <param name="user">The user.</param>
 1911        public override void MarkUnplayed(User user)
 1912        {
 01913            var itemsResult = GetItemList(new InternalItemsQuery
 01914            {
 01915                User = user,
 01916                Recursive = true,
 01917                IsFolder = false,
 01918                EnableTotalRecordCount = false
 01919            });
 1920
 1921            // Sweep through recursively and update status
 01922            foreach (var item in itemsResult)
 1923            {
 01924                item.MarkUnplayed(user);
 1925            }
 01926        }
 1927
 1928        public override bool IsPlayed(User user, UserItemData userItemData)
 1929        {
 01930            return ItemRepository.GetIsPlayed(user, Id, true);
 1931        }
 1932
 1933        public override bool IsUnplayed(User user, UserItemData userItemData)
 1934        {
 01935            return !IsPlayed(user, userItemData);
 1936        }
 1937
 1938        public override void FillUserDataDtoValues(
 1939            UserItemDataDto dto,
 1940            UserItemData userData,
 1941            BaseItemDto itemDto,
 1942            User user,
 1943            DtoOptions fields,
 1944            (int Played, int Total)? precomputedCounts = null)
 1945        {
 91946            if (!SupportsUserDataFromChildren)
 1947            {
 91948                return;
 1949            }
 1950
 01951            if (SupportsPlayedStatus || (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount)))
 1952            {
 1953                int playedCount;
 1954                int totalCount;
 1955
 01956                if (precomputedCounts.HasValue)
 1957                {
 1958                    // Use batch-fetched counts (avoids N+1 queries)
 01959                    (playedCount, totalCount) = precomputedCounts.Value;
 1960                }
 1961                else
 1962                {
 1963                    // Fall back to per-item query when no batch data is available
 01964                    var query = new InternalItemsQuery(user);
 1965
 01966                    if (LinkedChildren.Length > 0)
 1967                    {
 01968                        (playedCount, totalCount) = ItemCountService.GetPlayedAndTotalCountFromLinkedChildren(query, Id)
 1969                    }
 1970                    else
 1971                    {
 01972                        (playedCount, totalCount) = ItemCountService.GetPlayedAndTotalCount(query, Id);
 1973                    }
 1974                }
 1975
 01976                if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))
 1977                {
 01978                    itemDto.RecursiveItemCount = totalCount;
 1979                }
 1980
 01981                if (SupportsPlayedStatus)
 1982                {
 01983                    var unplayedCount = totalCount - playedCount;
 01984                    dto.UnplayedItemCount = unplayedCount;
 1985
 01986                    if (totalCount > 0)
 1987                    {
 01988                        dto.PlayedPercentage = playedCount / (double)totalCount * 100;
 01989                        dto.Played = playedCount >= totalCount;
 1990                    }
 1991                    else
 1992                    {
 01993                        dto.Played = true;
 1994                    }
 1995                }
 1996            }
 01997        }
 1998
 1999        /// <summary>
 2000        /// Contains constants used when reporting scan progress.
 2001        /// </summary>
 2002        private static class ProgressHelpers
 2003        {
 2004            /// <summary>
 2005            /// Reported after the folders immediate children are retrieved.
 2006            /// </summary>
 2007            public const int RetrievedChildren = 5;
 2008
 2009            /// <summary>
 2010            /// Reported after add, updating, or deleting child items from the LibraryManager.
 2011            /// </summary>
 2012            public const int UpdatedChildItems = 10;
 2013
 2014            /// <summary>
 2015            /// Reported once subfolders are scanned.
 2016            /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders].
 2017            /// </summary>
 2018            public const int ScannedSubfolders = 50;
 2019
 2020            /// <summary>
 2021            /// Reported once metadata is refreshed.
 2022            /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed].
 2023            /// </summary>
 2024            public const int RefreshedMetadata = 100;
 2025
 2026            /// <summary>
 2027            /// Gets the current progress given the previous step, next step, and progress in between.
 2028            /// </summary>
 2029            /// <param name="previousProgressStep">The previous progress step.</param>
 2030            /// <param name="nextProgressStep">The next progress step.</param>
 2031            /// <param name="currentProgress">The current progress step.</param>
 2032            /// <returns>The progress.</returns>
 2033            public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress)
 2034            {
 812035                return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100));
 2036            }
 2037        }
 2038    }
 2039}

Methods/Properties

.ctor()
get_LinkedChildren()
set_LinkedChildren(MediaBrowser.Controller.Entities.LinkedChild[])
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)