< 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: 310
Uncovered lines: 496
Coverable lines: 806
Total lines: 2031
Line coverage: 38.4%
Branch coverage
26%
Covered branches: 136
Total branches: 508
Branch coverage: 26.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 4/15/2026 - 12:14:34 AM Line coverage: 32% (188/587) Branch coverage: 26% (102/392) Total lines: 18814/19/2026 - 12:14:27 AM Line coverage: 40.7% (299/734) Branch coverage: 32.3% (148/458) Total lines: 18815/4/2026 - 12:15:16 AM Line coverage: 38.6% (301/779) Branch coverage: 29.7% (145/488) Total lines: 19635/5/2026 - 12:15:44 AM Line coverage: 38% (304/800) Branch coverage: 28.7% (146/508) Total lines: 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: 2031 4/15/2026 - 12:14:34 AM Line coverage: 32% (188/587) Branch coverage: 26% (102/392) Total lines: 18814/19/2026 - 12:14:27 AM Line coverage: 40.7% (299/734) Branch coverage: 32.3% (148/458) Total lines: 18815/4/2026 - 12:15:16 AM Line coverage: 38.6% (301/779) Branch coverage: 29.7% (145/488) Total lines: 19635/5/2026 - 12:15:44 AM Line coverage: 38% (304/800) Branch coverage: 28.7% (146/508) Total lines: 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: 2031

Coverage delta

Coverage delta 9 -9

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
get_SupportsThemeMedia()100%210%
get_IsPreSorted()100%210%
get_IsPhysicalRoot()100%11100%
get_SupportsInheritedParentImages()100%210%
get_SupportsPlayedStatus()100%210%
get_IsFolder()100%11100%
get_IsDisplayedAsFolder()100%210%
get_SupportsCumulativeRunTimeTicks()100%11100%
get_SupportsDateLastMediaAdded()100%11100%
get_FileNameWithoutExtension()50%2266.66%
get_Children()100%22100%
set_Children(...)100%11100%
get_RecursiveChildren()100%210%
get_SupportsShortcutChildren()100%11100%
get_FilterLinkedChildrenPerUser()100%11100%
get_SupportsOwnedItems()100%22100%
get_SupportsUserDataFromChildren()25%841635.71%
CanDelete()50%2266.66%
RequiresRefresh()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%4260%
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;
 46
 47747        public Folder()
 48        {
 47749            LinkedChildren = Array.Empty<LinkedChild>();
 47750        }
 51
 52        public static IUserViewManager UserViewManager { get; set; }
 53
 54        public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get; set; }
 55
 56        /// <summary>
 57        /// Gets or sets a value indicating whether this instance is root.
 58        /// </summary>
 59        /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
 60        public bool IsRoot { get; set; }
 61
 62        /// <summary>
 63        /// Gets or sets the linked children.
 64        /// </summary>
 65        [JsonIgnore]
 66        public LinkedChild[] LinkedChildren { get; set; }
 67
 68        [JsonIgnore]
 69        public DateTime? DateLastMediaAdded { get; set; }
 70
 71        [JsonIgnore]
 072        public override bool SupportsThemeMedia => true;
 73
 74        [JsonIgnore]
 075        public virtual bool IsPreSorted => false;
 76
 77        [JsonIgnore]
 1078        public virtual bool IsPhysicalRoot => false;
 79
 80        [JsonIgnore]
 081        public override bool SupportsInheritedParentImages => true;
 82
 83        [JsonIgnore]
 084        public override bool SupportsPlayedStatus => true;
 85
 86        /// <summary>
 87        /// Gets a value indicating whether this instance is folder.
 88        /// </summary>
 89        /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
 90        [JsonIgnore]
 96791        public override bool IsFolder => true;
 92
 93        [JsonIgnore]
 094        public override bool IsDisplayedAsFolder => true;
 95
 96        [JsonIgnore]
 9497        public virtual bool SupportsCumulativeRunTimeTicks => false;
 98
 99        [JsonIgnore]
 58100        public virtual bool SupportsDateLastMediaAdded => false;
 101
 102        [JsonIgnore]
 103        public override string FileNameWithoutExtension
 104        {
 105            get
 106            {
 319107                if (IsFileProtocol)
 108                {
 319109                    return System.IO.Path.GetFileName(Path);
 110                }
 111
 0112                return null;
 113            }
 114        }
 115
 116        /// <summary>
 117        /// Gets or Sets the actual children.
 118        /// </summary>
 119        /// <value>The actual children.</value>
 120        [JsonIgnore]
 121        public virtual IEnumerable<BaseItem> Children
 122        {
 1104123            get => _children ??= LoadChildren();
 237124            set => _children = value;
 125        }
 126
 127        /// <summary>
 128        /// Gets thread-safe access to all recursive children of this folder - without regard to user.
 129        /// </summary>
 130        /// <value>The recursive children.</value>
 131        [JsonIgnore]
 0132        public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
 133
 134        [JsonIgnore]
 23135        protected virtual bool SupportsShortcutChildren => false;
 136
 10137        protected virtual bool FilterLinkedChildrenPerUser => false;
 138
 139        [JsonIgnore]
 93140        protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
 141
 142        [JsonIgnore]
 143        public virtual bool SupportsUserDataFromChildren
 144        {
 145            get
 146            {
 147                // These are just far too slow.
 12148                if (this is ICollectionFolder)
 149                {
 6150                    return false;
 151                }
 152
 6153                if (this is UserView)
 154                {
 0155                    return false;
 156                }
 157
 6158                if (this is UserRootFolder)
 159                {
 6160                    return false;
 161                }
 162
 0163                if (this is Channel)
 164                {
 0165                    return false;
 166                }
 167
 0168                if (SourceType != SourceType.Library)
 169                {
 0170                    return false;
 171                }
 172
 0173                if (this is IItemByName)
 174                {
 0175                    if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName)
 176                    {
 0177                        return false;
 178                    }
 179                }
 180
 0181                return true;
 182            }
 183        }
 184
 185        public static ICollectionManager CollectionManager { get; set; }
 186
 187        public override bool CanDelete()
 188        {
 6189            if (IsRoot)
 190            {
 6191                return false;
 192            }
 193
 0194            return base.CanDelete();
 195        }
 196
 197        public override bool RequiresRefresh()
 198        {
 58199            var baseResult = base.RequiresRefresh();
 200
 58201            if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue)
 202            {
 0203                baseResult = true;
 204            }
 205
 58206            return baseResult;
 207        }
 208
 209        /// <summary>
 210        /// Adds the child.
 211        /// </summary>
 212        /// <param name="item">The item.</param>
 213        /// <exception cref="InvalidOperationException">Unable to add  + item.Name.</exception>
 214        public void AddChild(BaseItem item)
 215        {
 0216            item.SetParent(this);
 217
 0218            if (item.Id.IsEmpty())
 219            {
 0220                item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
 221            }
 222
 0223            if (item.DateCreated == DateTime.MinValue)
 224            {
 0225                item.DateCreated = DateTime.UtcNow;
 226            }
 227
 0228            if (item.DateModified == DateTime.MinValue)
 229            {
 0230                item.DateModified = DateTime.UtcNow;
 231            }
 232
 0233            LibraryManager.CreateItem(item, this);
 0234        }
 235
 236        public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
 237        {
 10238            if (this is ICollectionFolder && this is not BasePluginFolder)
 239            {
 0240                var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
 0241                if (blockedMediaFolders.Length > 0)
 242                {
 0243                    if (blockedMediaFolders.Contains(Id))
 244                    {
 0245                        return false;
 246                    }
 247                }
 248                else
 249                {
 0250                    if (!user.HasPermission(PermissionKind.EnableAllFolders)
 0251                        && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id))
 252                    {
 0253                        return false;
 254                    }
 255                }
 256            }
 257
 10258            return base.IsVisible(user, skipAllowedTagsCheck);
 259        }
 260
 261        /// <summary>
 262        /// Loads our children.  Validation will occur externally.
 263        /// We want this synchronous.
 264        /// </summary>
 265        /// <returns>Returns children.</returns>
 266        protected virtual IReadOnlyList<BaseItem> LoadChildren()
 267        {
 268            // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
 269            // just load our children from the repo - the library will be validated and maintained in other processes
 149270            return GetCachedChildren();
 271        }
 272
 273        public override double? GetRefreshProgress()
 274        {
 0275            return ProviderManager.GetRefreshProgress(Id);
 276        }
 277
 278        public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
 279        {
 0280            return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellation
 281        }
 282
 283        /// <summary>
 284        /// Validates that the children of the folder still exist.
 285        /// </summary>
 286        /// <param name="progress">The progress.</param>
 287        /// <param name="metadataRefreshOptions">The metadata refresh options.</param>
 288        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 289        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 290        /// <param name="cancellationToken">The cancellation token.</param>
 291        /// <returns>Task.</returns>
 292        public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool rec
 293        {
 61294            Children = null; // invalidate cached children.
 61295            return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadata
 296        }
 297
 298        private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
 299        {
 60300            var dictionary = new Dictionary<Guid, BaseItem>();
 301
 60302            Children = null; // invalidate cached children.
 60303            var childrenList = Children.ToList();
 304
 210305            foreach (var child in childrenList)
 306            {
 45307                var id = child.Id;
 45308                if (dictionary.ContainsKey(id))
 309                {
 0310                    Logger.LogError(
 0311                        "Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}",
 0312                        Path ?? Name,
 0313                        child.Path ?? child.Name);
 314                }
 315                else
 316                {
 45317                    dictionary[id] = child;
 318                }
 319            }
 320
 60321            return dictionary;
 322        }
 323
 324        /// <summary>
 325        /// Validates the children internal.
 326        /// </summary>
 327        /// <param name="progress">The progress.</param>
 328        /// <param name="recursive">if set to <c>true</c> [recursive].</param>
 329        /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
 330        /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
 331        /// <param name="refreshOptions">The refresh options.</param>
 332        /// <param name="directoryService">The directory service.</param>
 333        /// <param name="cancellationToken">The cancellation token.</param>
 334        /// <returns>Task.</returns>
 335        protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshCh
 336        {
 61337            if (recursive)
 338            {
 18339                ProviderManager.OnRefreshStart(this);
 340            }
 341
 342            try
 343            {
 344                if (GetParents().Any(f => f.Id.Equals(Id)))
 345                {
 0346                    throw new InvalidOperationException("Recursive datastructure detected abort processing this item.");
 347                }
 348
 61349                await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptio
 60350            }
 351            finally
 352            {
 61353                if (recursive)
 354                {
 18355                    ProviderManager.OnRefreshComplete(this);
 356                }
 357            }
 60358        }
 359
 360        private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item, bool checkColle
 361        {
 107362            if (!checkCollection && (item is BoxSet || string.Equals(item.FileNameWithoutExtension, "collections", Strin
 363            {
 0364                return true;
 365            }
 366
 367            // For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
 107368            if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
 369            {
 39370                Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.Containi
 39371                return false;
 372            }
 373
 68374            return true;
 375        }
 376
 377        private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetada
 378        {
 61379            if (!IsLibraryFolderAccessible(directoryService, this, allowRemoveRoot))
 380            {
 0381                return;
 382            }
 383
 61384            cancellationToken.ThrowIfCancellationRequested();
 385
 60386            var validChildren = new List<BaseItem>();
 60387            var accessibleChildren = new List<BaseItem>();
 60388            var validChildrenNeedGeneration = false;
 389
 60390            if (IsFileProtocol)
 391            {
 60392                IEnumerable<BaseItem> nonCachedChildren = [];
 393
 394                try
 395                {
 60396                    nonCachedChildren = GetNonCachedChildren(directoryService);
 60397                }
 0398                catch (IOException ex)
 399                {
 0400                    Logger.LogError(ex, "Error retrieving children from file system");
 0401                }
 0402                catch (SecurityException ex)
 403                {
 0404                    Logger.LogError(ex, "Error retrieving children from file system");
 0405                }
 0406                catch (Exception ex)
 407                {
 0408                    Logger.LogError(ex, "Error retrieving children");
 0409                    return;
 410                }
 411
 60412                progress.Report(ProgressHelpers.RetrievedChildren);
 413
 60414                if (recursive)
 415                {
 18416                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren);
 417                }
 418
 419                // Build a dictionary of the current children we have now by Id so we can compare quickly and easily
 60420                var currentChildren = GetActualChildrenDictionary();
 421
 422                // Create a list for our validated children
 60423                var newItems = new List<BaseItem>();
 60424                var actuallyRemoved = new List<BaseItem>();
 425
 426                // Build a reverse path→item lookup for detecting type changes
 60427                var currentChildrenByPath = new Dictionary<string, BaseItem>(StringComparer.OrdinalIgnoreCase);
 210428                foreach (var kvp in currentChildren)
 429                {
 45430                    if (!string.IsNullOrEmpty(kvp.Value.Path))
 431                    {
 45432                        currentChildrenByPath.TryAdd(kvp.Value.Path, kvp.Value);
 433                    }
 434                }
 435
 60436                cancellationToken.ThrowIfCancellationRequested();
 437
 212438                foreach (var child in nonCachedChildren)
 439                {
 46440                    if (!IsLibraryFolderAccessible(directoryService, child, allowRemoveRoot))
 441                    {
 442                        // Preserve inaccessible items so they aren't treated as removed.
 39443                        if (currentChildren.TryGetValue(child.Id, out var childrenToKeep))
 444                        {
 39445                            validChildren.Add(childrenToKeep);
 446                        }
 447
 39448                        continue;
 449                    }
 450
 7451                    if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild))
 452                    {
 5453                        validChildren.Add(currentChild);
 5454                        accessibleChildren.Add(currentChild);
 455
 5456                        if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
 457                        {
 0458                            await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken)
 459                        }
 460                        else
 461                        {
 462                            // metadata is up-to-date; make sure DB has correct images dimensions and hash
 5463                            await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false);
 464                        }
 465
 5466                        continue;
 467                    }
 468
 469                    // Check if an existing item occupies the same path with different type/ID
 2470                    if (!string.IsNullOrEmpty(child.Path)
 2471                        && currentChildrenByPath.TryGetValue(child.Path, out var staleItem)
 2472                        && !staleItem.Id.Equals(child.Id))
 473                    {
 0474                        Logger.LogInformation(
 0475                            "Item type changed at {Path}: {OldType} -> {NewType}, removing stale entry",
 0476                            child.Path,
 0477                            staleItem.GetType().Name,
 0478                            child.GetType().Name);
 479
 0480                        currentChildren.Remove(staleItem.Id);
 0481                        currentChildrenByPath.Remove(child.Path);
 0482                        staleItem.SetParent(null);
 0483                        LibraryManager.DeleteItem(staleItem, new DeleteOptions { DeleteFileLocation = false }, this, fal
 0484                        actuallyRemoved.Add(staleItem);
 485                    }
 486
 487                    // Brand new item - needs to be added
 2488                    child.SetParent(this);
 2489                    newItems.Add(child);
 2490                    validChildren.Add(child);
 2491                    accessibleChildren.Add(child);
 492                }
 493
 494                // That's all the new and changed ones - now see if any have been removed and need cleanup
 60495                var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
 496
 497                // If it's an AggregateFolder, don't remove
 498                // Collect replaced primaries for deferred deletion (after CreateItems)
 60499                var replacedPrimaries = new List<(Video OldPrimary, Video NewPrimary)>();
 500
 501                // Build a set of paths that are alternate versions of valid children
 502                // These items should not be deleted - they're managed by their primary video
 60503                var alternateVersionPaths = validChildren
 60504                    .OfType<Video>()
 60505                    .SelectMany(v => v.LocalAlternateVersions ?? [])
 60506                    .Where(p => !string.IsNullOrEmpty(p))
 60507                    .ToHashSet(StringComparer.OrdinalIgnoreCase);
 508
 60509                if (itemsRemoved.Count > 0)
 510                {
 4511                    foreach (var item in itemsRemoved)
 512                    {
 1513                        if (!item.CanDelete())
 514                        {
 1515                            Logger.LogDebug("Item marked as non-removable, skipping: {Path}", item.Path ?? item.Name);
 1516                            continue;
 517                        }
 518
 519                        // Skip items that are alternate versions of another video
 0520                        if (item is Video video)
 521                        {
 522                            // Check if path is in LocalAlternateVersions of any valid child
 0523                            if (!string.IsNullOrEmpty(item.Path) && alternateVersionPaths.Contains(item.Path))
 524                            {
 0525                                Logger.LogDebug("Item path matches an alternate version, skipping deletion: {Path}", ite
 0526                                continue;
 527                            }
 528                        }
 529
 530                        // Defer deletion if this primary video is being replaced by a new primary
 531                        // that takes over its alternates. Deleting now would trigger premature
 532                        // promotion inside DeleteItem and write stale paths to collection NFOs.
 0533                        if (item is Video primaryVideo
 0534                            && !primaryVideo.PrimaryVersionId.HasValue
 0535                            && primaryVideo.OwnerId.IsEmpty()
 0536                            && (primaryVideo.LocalAlternateVersions ?? []).Any(p => alternateVersionPaths.Contains(p)))
 537                        {
 0538                            var newPrimary = newItems
 0539                                .OfType<Video>()
 0540                                .FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
 0541                                    .Any(p => (primaryVideo.LocalAlternateVersions ?? [])
 0542                                        .Any(op => string.Equals(op, p, StringComparison.OrdinalIgnoreCase))));
 0543                            if (newPrimary is not null)
 544                            {
 0545                                Logger.LogDebug("Deferring deletion of replaced primary: {Path}", item.Path);
 0546                                replacedPrimaries.Add((primaryVideo, newPrimary));
 0547                                actuallyRemoved.Add(item);
 0548                                item.SetParent(null);
 0549                                continue;
 550                            }
 551                        }
 552
 0553                        if (item.IsFileProtocol)
 554                        {
 0555                            Logger.LogDebug("Removed item: {Path}", item.Path);
 556
 0557                            actuallyRemoved.Add(item);
 0558                            item.SetParent(null);
 0559                            LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, fals
 560                        }
 561                    }
 562                }
 563
 60564                if (newItems.Count > 0)
 565                {
 2566                    LibraryManager.CreateItems(newItems, this, cancellationToken);
 567                }
 568
 569                // Process deferred replaced-primary deletions now that new primaries exist in DB/cache.
 570                // This avoids the premature promotion that would occur if DeleteItem ran before CreateItems.
 120571                foreach (var (oldPrimary, newPrimary) in replacedPrimaries)
 572                {
 0573                    Logger.LogInformation(
 0574                        "Processing deferred deletion of replaced primary {OldName} ({OldId}), new primary {NewName} ({N
 0575                        oldPrimary.Name,
 0576                        oldPrimary.Id,
 0577                        newPrimary.Name,
 0578                        newPrimary.Id);
 579
 580                    // Reroute collection/playlist references from old primary to new primary
 0581                    await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(
 582
 583                    // Transfer alternates from old primary to new primary
 0584                    var localAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary).ToHashSet();
 0585                    var allAlternateIds = localAlternateIds
 0586                        .Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
 0587                        .Distinct()
 0588                        .ToList();
 589
 0590                    foreach (var altId in allAlternateIds)
 591                    {
 0592                        if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
 593                        {
 0594                            altVideo.SetPrimaryVersionId(newPrimary.Id);
 0595                            altVideo.OwnerId = localAlternateIds.Contains(altVideo.Id) ? newPrimary.Id : Guid.Empty;
 0596                            await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).Confi
 597                        }
 598                    }
 599
 600                    // Clear alternate arrays so DeleteItem won't trigger promotion
 0601                    oldPrimary.LocalAlternateVersions = [];
 0602                    oldPrimary.LinkedAlternateVersions = [];
 603
 604                    // Safe to delete now — no promotion will happen
 0605                    LibraryManager.DeleteItem(oldPrimary, new DeleteOptions { DeleteFileLocation = false }, this, false)
 0606                }
 607
 608                // Demote old primaries that are now alternate versions of newly created primaries.
 609                // This handles the case where a new file is added that becomes the new primary
 610                // (e.g. movie-2 added, movie-3 was primary → movie-3 needs demotion).
 611                // Items in replacedPrimaries are excluded (already in actuallyRemoved).
 60612                var oldPrimariesToDemote = new List<(Video OldPrimary, Video NewPrimary)>();
 122613                foreach (var item in itemsRemoved.Except(actuallyRemoved))
 614                {
 1615                    if (item is Video video
 1616                        && video.OwnerId.IsEmpty()
 1617                        && !string.IsNullOrEmpty(item.Path)
 1618                        && alternateVersionPaths.Contains(item.Path))
 619                    {
 0620                        var newPrimary = newItems
 0621                            .OfType<Video>()
 0622                            .FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
 0623                                .Any(p => string.Equals(p, item.Path, StringComparison.OrdinalIgnoreCase)));
 0624                        if (newPrimary is not null)
 625                        {
 0626                            oldPrimariesToDemote.Add((video, newPrimary));
 627                        }
 628                    }
 629                }
 630
 120631                foreach (var (oldPrimary, newPrimary) in oldPrimariesToDemote)
 632                {
 0633                    Logger.LogInformation(
 0634                        "Demoting old primary {OldName} ({OldId}) to alternate of new primary {NewName} ({NewId})",
 0635                        oldPrimary.Name,
 0636                        oldPrimary.Id,
 0637                        newPrimary.Name,
 0638                        newPrimary.Id);
 639
 640                    // First: update old primary's alternate items to point to new primary.
 641                    // Order matters — update alternates FIRST so they don't get orphan-deleted
 642                    // when old primary's arrays are cleared.
 0643                    var oldAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary)
 0644                        .Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
 0645                        .Distinct()
 0646                        .ToList();
 647
 0648                    foreach (var altId in oldAlternateIds)
 649                    {
 0650                        if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
 651                        {
 0652                            altVideo.SetPrimaryVersionId(newPrimary.Id);
 0653                            altVideo.OwnerId = newPrimary.Id;
 0654                            await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).Confi
 655                        }
 656                    }
 657
 658                    // Then: demote old primary — clear its arrays and set it as alternate of new primary
 0659                    oldPrimary.LocalAlternateVersions = [];
 0660                    oldPrimary.LinkedAlternateVersions = [];
 0661                    oldPrimary.SetPrimaryVersionId(newPrimary.Id);
 0662                    oldPrimary.OwnerId = newPrimary.Id;
 0663                    await oldPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAw
 664
 665                    // Re-route playlist/collection references from old primary to new primary
 0666                    await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(
 0667                }
 668
 669                // After removing items, reattach any detached user data to remaining children
 670                // that share the same user data keys (eg. same episode replaced with a new file).
 60671                if (actuallyRemoved.Count > 0)
 672                {
 0673                    var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet();
 0674                    foreach (var child in validChildren)
 675                    {
 0676                        if (child.GetUserDataKeys().Any(removedKeys.Contains))
 677                        {
 0678                            await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
 679                        }
 680                    }
 0681                }
 60682            }
 683            else
 684            {
 0685                validChildrenNeedGeneration = true;
 686            }
 687
 60688            progress.Report(ProgressHelpers.UpdatedChildItems);
 689
 60690            if (recursive)
 691            {
 18692                ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems);
 693            }
 694
 60695            cancellationToken.ThrowIfCancellationRequested();
 696
 60697            if (recursive)
 698            {
 18699                var folder = this;
 18700                var innerProgress = new Progress<double>(innerPercent =>
 18701                {
 18702                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.Scanned
 18703
 18704                    progress.Report(percent);
 18705
 18706                    ProviderManager.OnRefreshProgress(folder, percent);
 18707                });
 708
 18709                if (validChildrenNeedGeneration)
 710                {
 0711                    validChildren = Children.ToList();
 0712                    validChildrenNeedGeneration = false;
 713                }
 714
 18715                await ValidateSubFolders(accessibleChildren.OfType<Folder>().ToList(), directoryService, innerProgress, 
 716            }
 717
 60718            if (refreshChildMetadata)
 719            {
 60720                progress.Report(ProgressHelpers.ScannedSubfolders);
 721
 60722                if (recursive)
 723                {
 18724                    ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders);
 725                }
 726
 60727                var container = this as IMetadataContainer;
 728
 60729                var folder = this;
 60730                var innerProgress = new Progress<double>(innerPercent =>
 60731                {
 60732                    var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.Refresh
 60733
 60734                    progress.Report(percent);
 60735
 60736                    if (recursive)
 60737                    {
 60738                        ProviderManager.OnRefreshProgress(folder, percent);
 60739                    }
 60740                });
 741
 60742                if (container is not null)
 743                {
 0744                    await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).Co
 745                }
 746                else
 747                {
 60748                    if (validChildrenNeedGeneration)
 749                    {
 0750                        Children = null; // invalidate cached children.
 0751                        validChildren = Children.ToList();
 752                    }
 753
 60754                    await RefreshMetadataRecursive(accessibleChildren, refreshOptions, recursive, innerProgress, cancell
 755                }
 756            }
 60757        }
 758
 759        private async Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, boo
 760        {
 60761            await RunTasks(
 60762                (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFold
 60763                children,
 60764                progress,
 60765                cancellationToken).ConfigureAwait(false);
 60766        }
 767
 768        private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOp
 769        {
 0770            if (container is Series series)
 771            {
 0772                await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 773            }
 774
 0775            await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
 0776        }
 777
 778        private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, I
 779        {
 7780            if (child is IMetadataContainer container)
 781            {
 0782                await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAw
 783            }
 784            else
 785            {
 7786                if (refreshOptions.RefreshItem(child))
 787                {
 7788                    await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 789                }
 790
 7791                if (recursive && child is Folder folder)
 792                {
 0793                    folder.Children = null; // invalidate cached children.
 0794                    await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions
 795                }
 796            }
 7797        }
 798
 799        /// <summary>
 800        /// Refreshes the children.
 801        /// </summary>
 802        /// <param name="children">The children.</param>
 803        /// <param name="directoryService">The directory service.</param>
 804        /// <param name="progress">The progress.</param>
 805        /// <param name="cancellationToken">The cancellation token.</param>
 806        /// <returns>Task.</returns>
 807        private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<doub
 808        {
 18809            await RunTasks(
 18810                (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, dire
 18811                children,
 18812                progress,
 18813                cancellationToken).ConfigureAwait(false);
 18814        }
 815
 816        /// <summary>
 817        /// Runs an action block on a list of children.
 818        /// </summary>
 819        /// <param name="task">The task to run for each child.</param>
 820        /// <param name="children">The list of children.</param>
 821        /// <param name="progress">The progress.</param>
 822        /// <param name="cancellationToken">The cancellation token.</param>
 823        /// <returns>Task.</returns>
 824        private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progr
 825        {
 78826            await LimitedConcurrencyLibraryScheduler
 78827                .Enqueue(
 78828                    children.ToArray(),
 78829                    task,
 78830                    progress,
 78831                    cancellationToken)
 78832                .ConfigureAwait(false);
 78833        }
 834
 835        /// <summary>
 836        /// Get the children of this folder from the actual file system.
 837        /// </summary>
 838        /// <returns>IEnumerable{BaseItem}.</returns>
 839        /// <param name="directoryService">The directory service to use for operation.</param>
 840        /// <returns>Returns set of base items.</returns>
 841        protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
 842        {
 60843            var collectionType = LibraryManager.GetContentType(this);
 60844            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 845
 60846            return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryO
 847        }
 848
 849        /// <summary>
 850        /// Get our children from the repo - stubbed for now.
 851        /// </summary>
 852        /// <returns>IEnumerable{BaseItem}.</returns>
 853        protected IReadOnlyList<BaseItem> GetCachedChildren()
 854        {
 149855            return ItemRepository.GetItemList(new InternalItemsQuery
 149856            {
 149857                Parent = this,
 149858                GroupByPresentationUniqueKey = false,
 149859                DtoOptions = new DtoOptions(true)
 149860            });
 861        }
 862
 863        public virtual int GetChildCount(User user)
 864        {
 0865            if (LinkedChildren.Length > 0)
 866            {
 0867                if (this is not ICollectionFolder)
 868                {
 0869                    return GetChildren(user, true).Count;
 870                }
 871            }
 872
 0873            var result = GetItems(new InternalItemsQuery(user)
 0874            {
 0875                Recursive = false,
 0876                Limit = 0,
 0877                Parent = this,
 0878                DtoOptions = new DtoOptions(false)
 0879                {
 0880                    EnableImages = false
 0881                }
 0882            });
 883
 0884            return result.TotalRecordCount;
 885        }
 886
 887        public virtual int GetRecursiveChildCount(User user)
 888        {
 0889            return GetItems(new InternalItemsQuery(user)
 0890            {
 0891                Recursive = true,
 0892                IsFolder = false,
 0893                IsVirtualItem = false,
 0894                EnableTotalRecordCount = true,
 0895                Limit = 0,
 0896                DtoOptions = new DtoOptions(false)
 0897                {
 0898                    EnableImages = false
 0899                }
 0900            }).TotalRecordCount;
 901        }
 902
 903        public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
 904        {
 14905            if (!query.ForceDirect && CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
 906            {
 0907                query.CollapseBoxSetItems = true;
 0908                SetCollapseBoxSetItemTypes(query);
 909            }
 910
 14911            if (this is not UserRootFolder
 14912                && this is not AggregateFolder
 14913                && query.ParentId.IsEmpty())
 914            {
 14915                query.Parent = this;
 916            }
 917
 918            // BoxSets and Playlists can have per-user visibility (shares/open access) that is stored in the
 919            // serialized item data and cannot be evaluated by the database query, so filter them in memory.
 14920            if (query.IncludeItemTypes.Length > 0
 14921                && query.IncludeItemTypes.All(t => t == BaseItemKind.BoxSet || t == BaseItemKind.Playlist))
 922            {
 0923                return QueryWithPostFiltering(query);
 924            }
 925
 14926            return LibraryManager.GetItemsResult(query);
 927        }
 928
 929        protected QueryResult<BaseItem> QueryWithPostFiltering(InternalItemsQuery query)
 930        {
 1931            var startIndex = query.StartIndex;
 1932            var limit = query.Limit;
 933
 1934            query.StartIndex = null;
 1935            query.Limit = null;
 936
 1937            IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query);
 1938            var user = query.User;
 939
 1940            if (user is not null)
 941            {
 942                // needed for boxsets and playlists
 1943                itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User));
 944            }
 945
 946            IEnumerable<BaseItem> returnItems;
 1947            int totalCount = 0;
 948
 1949            if (query.EnableTotalRecordCount)
 950            {
 0951                var itemArray = itemsList.ToArray();
 0952                totalCount = itemArray.Length;
 0953                returnItems = itemArray;
 954            }
 955            else
 956            {
 1957                returnItems = itemsList;
 958            }
 959
 1960            if (limit.HasValue)
 961            {
 0962                returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value);
 963            }
 1964            else if (startIndex.HasValue)
 965            {
 0966                returnItems = returnItems.Skip(startIndex.Value);
 967            }
 968
 1969            return new QueryResult<BaseItem>(
 1970                query.StartIndex,
 1971                totalCount,
 1972                returnItems.ToArray());
 973        }
 974
 975        private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
 976        {
 0977            return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
 978        }
 979
 980        public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
 981        {
 0982            if (query.ItemIds.Length > 0)
 983            {
 0984                var result = LibraryManager.GetItemsResult(query);
 985
 0986                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 987                {
 0988                    result.Items = SortItemsByRequest(query, result.Items);
 989                }
 990
 0991                return result;
 992            }
 993
 0994            return GetItemsInternal(query);
 995        }
 996
 997        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 998        {
 15999            query.EnableTotalRecordCount = false;
 1000
 151001            if (query.ItemIds.Length > 0)
 1002            {
 01003                var result = LibraryManager.GetItemList(query);
 1004
 01005                if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
 1006                {
 01007                    return SortItemsByRequest(query, result);
 1008                }
 1009
 01010                return result;
 1011            }
 1012
 151013            return GetItemsInternal(query).Items;
 1014        }
 1015
 1016        protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
 1017        {
 141018            if (SourceType == SourceType.Channel)
 1019            {
 1020                try
 1021                {
 01022                    query.Parent = this;
 01023                    query.ChannelIds = new[] { ChannelId };
 1024
 1025                    // Don't blow up here because it could cause parent screens with other content to fail
 01026                    return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None)
 1027                }
 01028                catch
 1029                {
 1030                    // Already logged at lower levels
 01031                    return new QueryResult<BaseItem>();
 1032                }
 1033            }
 1034
 141035            if (query.Recursive)
 1036            {
 141037                return QueryRecursive(query);
 1038            }
 1039
 01040            var user = query.User;
 1041
 1042            IEnumerable<BaseItem> items;
 1043
 01044            int totalItemCount = 0;
 01045            if (query.User is null)
 1046            {
 01047                items = UserViewBuilder.Filter(Children, user, query, UserDataManager, LibraryManager);
 01048                totalItemCount = items.Count();
 1049            }
 1050            else
 1051            {
 1052                // need to pass this param to the children.
 1053                // Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
 01054                var childQuery = new InternalItemsQuery
 01055                {
 01056                    DisplayAlbumFolders = query.DisplayAlbumFolders,
 01057                    NameStartsWith = query.NameStartsWith,
 01058                    NameStartsWithOrGreater = query.NameStartsWithOrGreater,
 01059                    NameLessThan = query.NameLessThan
 01060                };
 1061
 01062                items = UserViewBuilder.Filter(
 01063                    GetChildren(user, true, out totalItemCount, childQuery),
 01064                    user,
 01065                    query,
 01066                    UserDataManager,
 01067                    LibraryManager);
 1068            }
 1069
 01070            return PostFilterAndSort(items, query);
 01071        }
 1072
 1073        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
 1074        {
 01075            var user = query.User;
 1076
 1077            // Check recursive - don't substitute in plain folder views
 01078            if (user is not null)
 1079            {
 01080                items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
 1081
 1082                // After collapse, BoxSets may have replaced items whose names matched the filter
 1083                // but the BoxSet's own name may not match. Re-apply name filtering so BoxSets
 1084                // appear under the correct letter (e.g. "Jump Street" under J, not under #).
 01085                items = ApplyNameFilter(items, query);
 1086            }
 1087
 01088            var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList();
 01089            var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager);
 1090
 01091            if (query.EnableTotalRecordCount)
 1092            {
 01093                result.TotalRecordCount = filteredItems.Count;
 1094            }
 1095
 01096            return result;
 1097        }
 1098
 1099        private static IEnumerable<BaseItem> ApplyNameFilter(IEnumerable<BaseItem> items, InternalItemsQuery query)
 1100        {
 01101            if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
 1102            {
 01103                items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)
 1104            }
 1105
 01106            if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
 1107            {
 01108                items = items.Where(i => string.Compare(i.SortName, query.NameStartsWithOrGreater, StringComparison.Ordi
 1109            }
 1110
 01111            if (!string.IsNullOrWhiteSpace(query.NameLessThan))
 1112            {
 01113                items = items.Where(i => string.Compare(i.SortName, query.NameLessThan, StringComparison.OrdinalIgnoreCa
 1114            }
 1115
 01116            return items;
 1117        }
 1118
 1119        private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
 1120            IEnumerable<BaseItem> items,
 1121            InternalItemsQuery query,
 1122            BaseItem queryParent,
 1123            User user,
 1124            IServerConfigurationManager configurationManager,
 1125            ICollectionManager collectionManager)
 1126        {
 01127            ArgumentNullException.ThrowIfNull(items);
 1128
 01129            if (!CollapseBoxSetItems(query, queryParent, user, configurationManager))
 1130            {
 01131                return items;
 1132            }
 1133
 01134            var config = configurationManager.Configuration;
 1135
 01136            bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
 01137            bool collapseSeries = config.EnableGroupingShowsIntoCollections;
 1138
 01139            if (user is null || (collapseMovies && collapseSeries))
 1140            {
 01141                return collectionManager.CollapseItemsWithinBoxSets(items, user);
 1142            }
 1143
 01144            if (!collapseMovies && !collapseSeries)
 1145            {
 01146                return items;
 1147            }
 1148
 01149            var collapsibleItems = new List<BaseItem>();
 01150            var remainingItems = new List<BaseItem>();
 1151
 01152            foreach (var item in items)
 1153            {
 01154                if ((collapseMovies && item is Movie) || (collapseSeries && item is Series))
 1155                {
 01156                    collapsibleItems.Add(item);
 1157                }
 1158                else
 1159                {
 01160                    remainingItems.Add(item);
 1161                }
 1162            }
 1163
 01164            if (collapsibleItems.Count == 0)
 1165            {
 01166                return remainingItems;
 1167            }
 1168
 01169            var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user);
 1170
 01171            return collapsedItems.Concat(remainingItems);
 1172        }
 1173
 1174        private static bool CollapseBoxSetItems(
 1175            InternalItemsQuery query,
 1176            BaseItem queryParent,
 1177            User user,
 1178            IServerConfigurationManager configurationManager)
 1179        {
 1180            // Could end up stuck in a loop like this
 141181            if (queryParent is BoxSet)
 1182            {
 01183                return false;
 1184            }
 1185
 141186            if (queryParent is Season)
 1187            {
 01188                return false;
 1189            }
 1190
 141191            if (queryParent is MusicAlbum)
 1192            {
 01193                return false;
 1194            }
 1195
 141196            if (queryParent is MusicArtist)
 1197            {
 01198                return false;
 1199            }
 1200
 141201            var param = query.CollapseBoxSetItems;
 141202            if (param.HasValue)
 1203            {
 141204                return param.Value && AllowBoxSetCollapsing(query);
 1205            }
 1206
 01207            var config = configurationManager.Configuration;
 1208
 01209            bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Mov
 01210            bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Ser
 1211
 01212            bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
 01213            bool collapseSeries = config.EnableGroupingShowsIntoCollections;
 1214
 01215            if (user is not null)
 1216            {
 01217                bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries);
 01218                return canCollapse && AllowBoxSetCollapsing(query);
 1219            }
 1220
 01221            return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query);
 1222        }
 1223
 1224        private void SetCollapseBoxSetItemTypes(InternalItemsQuery query)
 1225        {
 01226            var config = ConfigurationManager.Configuration;
 01227            bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
 01228            bool collapseSeries = config.EnableGroupingShowsIntoCollections;
 1229
 01230            if (collapseMovies && collapseSeries)
 1231            {
 1232                // Empty means collapse all types
 01233                query.CollapseBoxSetItemTypes = [];
 01234                return;
 1235            }
 1236
 01237            var types = new List<BaseItemKind>();
 01238            if (collapseMovies)
 1239            {
 01240                types.Add(BaseItemKind.Movie);
 1241            }
 1242
 01243            if (collapseSeries)
 1244            {
 01245                types.Add(BaseItemKind.Series);
 1246            }
 1247
 01248            query.CollapseBoxSetItemTypes = types.ToArray();
 01249        }
 1250
 1251        private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
 1252        {
 01253            if (request.IsFavorite.HasValue)
 1254            {
 01255                return false;
 1256            }
 1257
 01258            if (request.IsFavoriteOrLiked.HasValue)
 1259            {
 01260                return false;
 1261            }
 1262
 01263            if (request.IsLiked.HasValue)
 1264            {
 01265                return false;
 1266            }
 1267
 01268            if (request.IsPlayed.HasValue)
 1269            {
 01270                return false;
 1271            }
 1272
 01273            if (request.IsResumable.HasValue)
 1274            {
 01275                return false;
 1276            }
 1277
 01278            if (request.IsFolder.HasValue)
 1279            {
 01280                return false;
 1281            }
 1282
 01283            if (request.Genres.Count > 0)
 1284            {
 01285                return false;
 1286            }
 1287
 01288            if (request.GenreIds.Count > 0)
 1289            {
 01290                return false;
 1291            }
 1292
 01293            if (request.HasImdbId.HasValue)
 1294            {
 01295                return false;
 1296            }
 1297
 01298            if (request.HasOfficialRating.HasValue)
 1299            {
 01300                return false;
 1301            }
 1302
 01303            if (request.HasOverview.HasValue)
 1304            {
 01305                return false;
 1306            }
 1307
 01308            if (request.HasParentalRating.HasValue)
 1309            {
 01310                return false;
 1311            }
 1312
 01313            if (request.HasSpecialFeature.HasValue)
 1314            {
 01315                return false;
 1316            }
 1317
 01318            if (request.HasSubtitles.HasValue)
 1319            {
 01320                return false;
 1321            }
 1322
 01323            if (request.HasThemeSong.HasValue)
 1324            {
 01325                return false;
 1326            }
 1327
 01328            if (request.HasThemeVideo.HasValue)
 1329            {
 01330                return false;
 1331            }
 1332
 01333            if (request.HasTmdbId.HasValue)
 1334            {
 01335                return false;
 1336            }
 1337
 01338            if (request.HasTrailer.HasValue)
 1339            {
 01340                return false;
 1341            }
 1342
 01343            if (request.ImageTypes.Length > 0)
 1344            {
 01345                return false;
 1346            }
 1347
 01348            if (request.Is3D.HasValue)
 1349            {
 01350                return false;
 1351            }
 1352
 01353            if (request.Is4K.HasValue)
 1354            {
 01355                return false;
 1356            }
 1357
 01358            if (request.IsHD.HasValue)
 1359            {
 01360                return false;
 1361            }
 1362
 01363            if (request.IsLocked.HasValue)
 1364            {
 01365                return false;
 1366            }
 1367
 01368            if (request.IsPlaceHolder.HasValue)
 1369            {
 01370                return false;
 1371            }
 1372
 01373            if (!string.IsNullOrWhiteSpace(request.Person))
 1374            {
 01375                return false;
 1376            }
 1377
 01378            if (request.PersonIds.Length > 0)
 1379            {
 01380                return false;
 1381            }
 1382
 01383            if (request.ItemIds.Length > 0)
 1384            {
 01385                return false;
 1386            }
 1387
 01388            if (request.StudioIds.Length > 0)
 1389            {
 01390                return false;
 1391            }
 1392
 01393            if (request.VideoTypes.Length > 0)
 1394            {
 01395                return false;
 1396            }
 1397
 01398            if (request.Years.Length > 0)
 1399            {
 01400                return false;
 1401            }
 1402
 01403            if (request.Tags.Length > 0)
 1404            {
 01405                return false;
 1406            }
 1407
 01408            if (request.OfficialRatings.Length > 0)
 1409            {
 01410                return false;
 1411            }
 1412
 01413            if (request.MinIndexNumber.HasValue)
 1414            {
 01415                return false;
 1416            }
 1417
 01418            if (request.OrderBy.Any(o =>
 01419                o.OrderBy == ItemSortBy.CommunityRating ||
 01420                o.OrderBy == ItemSortBy.CriticRating ||
 01421                o.OrderBy == ItemSortBy.Runtime))
 1422            {
 01423                return false;
 1424            }
 1425
 01426            return true;
 1427        }
 1428
 1429        public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount
 1430        {
 101431            ArgumentNullException.ThrowIfNull(user);
 101432            query ??= new InternalItemsQuery();
 101433            query.User = user;
 1434
 1435            // the true root should return our users root folder children
 101436            if (IsPhysicalRoot)
 1437            {
 01438                return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren, out totalItemCount);
 1439            }
 1440
 101441            var result = new Dictionary<Guid, BaseItem>();
 1442
 101443            totalItemCount = AddChildren(user, includeLinkedChildren, result, false, query);
 1444
 101445            return result.Values.ToArray();
 1446        }
 1447
 1448        public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery que
 1449        {
 101450            return GetChildren(user, includeLinkedChildren, out _, query);
 1451        }
 1452
 1453        protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
 1454        {
 101455            return Children;
 1456        }
 1457
 1458        /// <summary>
 1459        /// Adds the children to list.
 1460        /// </summary>
 1461        private int AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive
 1462        {
 1463            // Prevent infinite recursion of nested folders
 101464            visitedFolders ??= new HashSet<Folder>();
 101465            if (!visitedFolders.Add(this))
 1466            {
 01467                return 0;
 1468            }
 1469
 1470            // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in mus
 101471            IEnumerable<BaseItem> children = null;
 101472            if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
 1473            {
 01474                children = Children;
 01475                query = null;
 1476            }
 1477
 1478            // If there are not sub-folders, proceed as normal.
 101479            if (children is null)
 1480            {
 101481                children = GetEligibleChildrenForRecursiveChildren(user);
 1482            }
 1483
 101484            if (includeLinkedChildren)
 1485            {
 101486                children = children.Concat(GetLinkedChildren(user)).ToArray();
 1487            }
 1488
 101489            return AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFol
 1490        }
 1491
 1492        private int AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dic
 1493        {
 101494            query ??= new InternalItemsQuery();
 101495            var limit = query.Limit > 0 ? query.Limit : int.MaxValue;
 101496            query.Limit = 0;
 1497
 101498            var visibleChildren = children
 101499                .Where(e => e.IsVisible(user))
 101500                .ToArray();
 1501
 101502            var realChildren = UserViewBuilder.Filter(visibleChildren, query.User, query, UserDataManager, LibraryManage
 101503                .ToArray();
 1504
 101505            var childCount = realChildren.Length;
 101506            if (result.Count < limit)
 1507            {
 101508                var remainingCount = (int)(limit - result.Count);
 401509                foreach (var child in realChildren
 101510                    .Skip(query.StartIndex ?? 0)
 101511                    .Take(remainingCount))
 1512                {
 101513                    result[child.Id] = child;
 1514                }
 1515            }
 1516
 101517            if (recursive)
 1518            {
 01519                foreach (var child in visibleChildren
 01520                    .Where(e => e.IsFolder)
 01521                    .OfType<Folder>())
 1522                {
 01523                    childCount += child.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
 1524                }
 1525            }
 1526
 101527            return childCount;
 1528        }
 1529
 1530        public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCo
 1531        {
 01532            ArgumentNullException.ThrowIfNull(user);
 1533
 01534            var result = new Dictionary<Guid, BaseItem>();
 1535
 01536            totalCount = AddChildren(user, true, result, true, query);
 1537
 01538            return result.Values.ToArray();
 1539        }
 1540
 1541        /// <summary>
 1542        /// Gets the recursive children.
 1543        /// </summary>
 1544        /// <returns>IList{BaseItem}.</returns>
 1545        public IReadOnlyList<BaseItem> GetRecursiveChildren()
 1546        {
 01547            return GetRecursiveChildren(true);
 1548        }
 1549
 1550        public IReadOnlyList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
 1551        {
 01552            return GetRecursiveChildren(i => true, includeLinkedChildren);
 1553        }
 1554
 1555        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
 1556        {
 01557            return GetRecursiveChildren(filter, true);
 1558        }
 1559
 1560        public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
 1561        {
 01562            var result = new Dictionary<Guid, BaseItem>();
 1563
 01564            AddChildrenToList(result, includeLinkedChildren, true, filter);
 1565
 01566            return result.Values.ToArray();
 1567        }
 1568
 1569        /// <summary>
 1570        /// Adds the children to list.
 1571        /// </summary>
 1572        private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Fu
 1573        {
 01574            foreach (var child in Children)
 1575            {
 01576                if (filter is null || filter(child))
 1577                {
 01578                    result[child.Id] = child;
 1579                }
 1580
 01581                if (recursive && child.IsFolder)
 1582                {
 01583                    var folder = (Folder)child;
 1584
 1585                    // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loo
 01586                    folder.AddChildrenToList(result, false, true, filter);
 1587                }
 1588            }
 1589
 01590            if (includeLinkedChildren)
 1591            {
 01592                foreach (var child in GetLinkedChildren())
 1593                {
 01594                    if (filter is null || filter(child))
 1595                    {
 01596                        result[child.Id] = child;
 1597                    }
 1598                }
 1599            }
 01600        }
 1601
 1602        /// <summary>
 1603        /// Gets the linked children.
 1604        /// </summary>
 1605        /// <returns>IEnumerable{BaseItem}.</returns>
 1606        public List<BaseItem> GetLinkedChildren()
 1607        {
 101608            var resolved = ResolveLinkedChildren(LinkedChildren);
 101609            var list = new List<BaseItem>(resolved.Count);
 201610            foreach (var (_, item) in resolved)
 1611            {
 01612                list.Add(item);
 1613            }
 1614
 101615            return list;
 1616        }
 1617
 1618        public bool ContainsLinkedChildByItemId(Guid itemId)
 1619        {
 01620            var linkedChildren = LinkedChildren;
 01621            foreach (var i in linkedChildren)
 1622            {
 01623                if (i.ItemId.HasValue)
 1624                {
 01625                    if (i.ItemId.Value.Equals(itemId))
 1626                    {
 01627                        return true;
 1628                    }
 1629
 1630                    continue;
 1631                }
 1632
 01633                var child = GetLinkedChild(i);
 1634
 01635                if (child is not null && child.Id.Equals(itemId))
 1636                {
 01637                    return true;
 1638                }
 1639            }
 1640
 01641            return false;
 1642        }
 1643
 1644        public List<BaseItem> GetLinkedChildren(User user)
 1645        {
 101646            if (!FilterLinkedChildrenPerUser || user is null)
 1647            {
 101648                return GetLinkedChildren();
 1649            }
 1650
 01651            var linkedChildren = LinkedChildren;
 01652            var list = new List<BaseItem>(linkedChildren.Length);
 1653
 01654            if (linkedChildren.Length == 0)
 1655            {
 01656                return list;
 1657            }
 1658
 01659            var allUserRootChildren = LibraryManager.GetUserRootFolder()
 01660                .GetChildren(user, true)
 01661                .OfType<Folder>()
 01662                .ToList();
 1663
 01664            var collectionFolderIds = allUserRootChildren
 01665                .Select(i => i.Id)
 01666                .ToList();
 1667
 01668            foreach (var i in linkedChildren)
 1669            {
 01670                var child = GetLinkedChild(i);
 1671
 01672                if (child is null)
 1673                {
 1674                    continue;
 1675                }
 1676
 01677                var childOwner = child.GetOwner() ?? child;
 1678
 01679                if (child is not IItemByName)
 1680                {
 01681                    var childProtocol = childOwner.PathProtocol;
 01682                    if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File)
 1683                    {
 01684                        if (!childOwner.IsVisibleStandalone(user))
 1685                        {
 01686                            continue;
 1687                        }
 1688                    }
 1689                    else
 1690                    {
 01691                        var itemCollectionFolderIds =
 01692                            LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id);
 1693
 01694                        if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains))
 1695                        {
 1696                            continue;
 1697                        }
 1698                    }
 1699                }
 1700
 01701                list.Add(child);
 1702            }
 1703
 01704            return list;
 1705        }
 1706
 1707        /// <summary>
 1708        /// Gets the linked children.
 1709        /// </summary>
 1710        /// <returns>IEnumerable{BaseItem}.</returns>
 1711        public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
 1712        {
 01713            return ResolveLinkedChildren(LinkedChildren)
 01714                .Select(t => new Tuple<LinkedChild, BaseItem>(t.Info, t.Item))
 01715                .ToArray();
 1716        }
 1717
 1718        /// <summary>
 1719        /// Resolves a list of <see cref="LinkedChild"/> entries to their <see cref="BaseItem"/> targets,
 1720        /// batching the database lookup across all entries with a known ItemId.
 1721        /// Entries without a usable ItemId fall back to the per-entry <see cref="BaseItem.GetLinkedChild"/>
 1722        /// path (legacy path-based resolution).
 1723        /// </summary>
 1724        /// <param name="linkedChildren">Linked children to resolve.</param>
 1725        /// <returns>Each input entry paired with its resolved item; entries that fail to resolve are dropped.</returns>
 1726        private List<(LinkedChild Info, BaseItem Item)> ResolveLinkedChildren(IReadOnlyList<LinkedChild> linkedChildren)
 1727        {
 101728            var resolved = new List<(LinkedChild Info, BaseItem Item)>(linkedChildren.Count);
 101729            if (linkedChildren.Count == 0)
 1730            {
 101731                return resolved;
 1732            }
 1733
 01734            var idsToBatch = new HashSet<Guid>();
 01735            foreach (var info in linkedChildren)
 1736            {
 01737                if (info.ItemId.HasValue && !info.ItemId.Value.IsEmpty())
 1738                {
 01739                    idsToBatch.Add(info.ItemId.Value);
 1740                }
 1741            }
 1742
 01743            Dictionary<Guid, BaseItem> byId = null;
 01744            if (idsToBatch.Count > 0)
 1745            {
 01746                var batched = LibraryManager.GetItemList(new InternalItemsQuery
 01747                {
 01748                    ItemIds = [.. idsToBatch]
 01749                });
 01750                byId = new Dictionary<Guid, BaseItem>(batched.Count);
 01751                foreach (var item in batched)
 1752                {
 01753                    byId[item.Id] = item;
 1754                }
 1755            }
 1756
 01757            foreach (var info in linkedChildren)
 1758            {
 01759                BaseItem item = null;
 01760                if (byId is not null && info.ItemId.HasValue && byId.TryGetValue(info.ItemId.Value, out var batchedItem)
 1761                {
 01762                    item = batchedItem;
 1763                }
 1764                else
 1765                {
 1766                    // ItemId is missing/empty or the batched query couldn't return the item
 1767                    // (e.g. it has been removed). Fall back to per-entry resolution, which also
 1768                    // handles legacy path-based linked children.
 01769                    item = GetLinkedChild(info);
 1770                }
 1771
 01772                if (item is not null)
 1773                {
 01774                    resolved.Add((info, item));
 1775                }
 1776            }
 1777
 01778            return resolved;
 1779        }
 1780
 1781        protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystem
 1782        {
 351783            var changesFound = false;
 1784
 351785            if (IsFileProtocol)
 1786            {
 351787                if (RefreshLinkedChildren(fileSystemChildren))
 1788                {
 01789                    changesFound = true;
 1790                }
 1791            }
 1792
 351793            var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).Configur
 1794
 351795            return baseHasChanges || changesFound;
 351796        }
 1797
 1798        /// <summary>
 1799        /// Refreshes the linked children.
 1800        /// </summary>
 1801        /// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
 1802        /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
 1803        protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
 1804        {
 211805            if (SupportsShortcutChildren)
 1806            {
 211807                var newShortcutLinks = fileSystemChildren
 211808                    .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName))
 211809                    .Select(i =>
 211810                    {
 211811                        try
 211812                        {
 211813                            Logger.LogDebug("Found shortcut at {0}", i.FullName);
 211814
 211815                            var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShor
 211816
 211817                            if (!string.IsNullOrEmpty(resolvedPath))
 211818                            {
 211819#pragma warning disable CS0618 // Type or member is obsolete - shortcuts require Path for lazy ItemId resolution
 211820                                return new LinkedChild
 211821                                {
 211822                                    Path = resolvedPath,
 211823                                    Type = LinkedChildType.Shortcut
 211824                                };
 211825#pragma warning restore CS0618
 211826                            }
 211827
 211828                            Logger.LogError("Error resolving shortcut {0}", i.FullName);
 211829
 211830                            return null;
 211831                        }
 211832                        catch (IOException ex)
 211833                        {
 211834                            Logger.LogError(ex, "Error resolving shortcut {0}", i.FullName);
 211835                            return null;
 211836                        }
 211837                    })
 211838                    .Where(i => i is not null)
 211839                    .ToList();
 1840
 211841                var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
 1842
 211843                if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem)))
 1844                {
 01845                    Logger.LogInformation("Shortcut links have changed for {0}", Path);
 1846
 01847                    newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual));
 01848                    LinkedChildren = newShortcutLinks.ToArray();
 01849                    return true;
 1850                }
 1851            }
 1852
 211853            return false;
 1854        }
 1855
 1856        /// <summary>
 1857        /// Marks the played.
 1858        /// </summary>
 1859        /// <param name="user">The user.</param>
 1860        /// <param name="datePlayed">The date played.</param>
 1861        /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
 1862        public override void MarkPlayed(
 1863            User user,
 1864            DateTime? datePlayed,
 1865            bool resetPosition)
 1866        {
 01867            var query = new InternalItemsQuery
 01868            {
 01869                User = user,
 01870                Recursive = true,
 01871                IsFolder = false,
 01872                EnableTotalRecordCount = false
 01873            };
 1874
 01875            if (!user.DisplayMissingEpisodes)
 1876            {
 01877                query.IsVirtualItem = false;
 1878            }
 1879
 01880            var itemsResult = GetItemList(query);
 1881
 1882            // Sweep through recursively and update status
 01883            foreach (var item in itemsResult)
 1884            {
 01885                if (item.IsVirtualItem)
 1886                {
 1887                    // The querying doesn't support virtual unaired
 01888                    var episode = item as Episode;
 01889                    if (episode is not null && episode.IsUnaired)
 1890                    {
 1891                        continue;
 1892                    }
 1893                }
 1894
 01895                item.MarkPlayed(user, datePlayed, resetPosition);
 1896            }
 01897        }
 1898
 1899        /// <summary>
 1900        /// Marks the unplayed.
 1901        /// </summary>
 1902        /// <param name="user">The user.</param>
 1903        public override void MarkUnplayed(User user)
 1904        {
 01905            var itemsResult = GetItemList(new InternalItemsQuery
 01906            {
 01907                User = user,
 01908                Recursive = true,
 01909                IsFolder = false,
 01910                EnableTotalRecordCount = false
 01911            });
 1912
 1913            // Sweep through recursively and update status
 01914            foreach (var item in itemsResult)
 1915            {
 01916                item.MarkUnplayed(user);
 1917            }
 01918        }
 1919
 1920        public override bool IsPlayed(User user, UserItemData userItemData)
 1921        {
 01922            return ItemRepository.GetIsPlayed(user, Id, true);
 1923        }
 1924
 1925        public override bool IsUnplayed(User user, UserItemData userItemData)
 1926        {
 01927            return !IsPlayed(user, userItemData);
 1928        }
 1929
 1930        public override void FillUserDataDtoValues(
 1931            UserItemDataDto dto,
 1932            UserItemData userData,
 1933            BaseItemDto itemDto,
 1934            User user,
 1935            DtoOptions fields,
 1936            (int Played, int Total)? precomputedCounts = null)
 1937        {
 91938            if (!SupportsUserDataFromChildren)
 1939            {
 91940                return;
 1941            }
 1942
 01943            if (SupportsPlayedStatus || (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount)))
 1944            {
 1945                int playedCount;
 1946                int totalCount;
 1947
 01948                if (precomputedCounts.HasValue)
 1949                {
 1950                    // Use batch-fetched counts (avoids N+1 queries)
 01951                    (playedCount, totalCount) = precomputedCounts.Value;
 1952                }
 1953                else
 1954                {
 1955                    // Fall back to per-item query when no batch data is available
 01956                    var query = new InternalItemsQuery(user);
 1957
 01958                    if (LinkedChildren.Length > 0)
 1959                    {
 01960                        (playedCount, totalCount) = ItemCountService.GetPlayedAndTotalCountFromLinkedChildren(query, Id)
 1961                    }
 1962                    else
 1963                    {
 01964                        (playedCount, totalCount) = ItemCountService.GetPlayedAndTotalCount(query, Id);
 1965                    }
 1966                }
 1967
 01968                if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))
 1969                {
 01970                    itemDto.RecursiveItemCount = totalCount;
 1971                }
 1972
 01973                if (SupportsPlayedStatus)
 1974                {
 01975                    var unplayedCount = totalCount - playedCount;
 01976                    dto.UnplayedItemCount = unplayedCount;
 1977
 01978                    if (totalCount > 0)
 1979                    {
 01980                        dto.PlayedPercentage = playedCount / (double)totalCount * 100;
 01981                        dto.Played = playedCount >= totalCount;
 1982                    }
 1983                    else
 1984                    {
 01985                        dto.Played = true;
 1986                    }
 1987                }
 1988            }
 01989        }
 1990
 1991        /// <summary>
 1992        /// Contains constants used when reporting scan progress.
 1993        /// </summary>
 1994        private static class ProgressHelpers
 1995        {
 1996            /// <summary>
 1997            /// Reported after the folders immediate children are retrieved.
 1998            /// </summary>
 1999            public const int RetrievedChildren = 5;
 2000
 2001            /// <summary>
 2002            /// Reported after add, updating, or deleting child items from the LibraryManager.
 2003            /// </summary>
 2004            public const int UpdatedChildItems = 10;
 2005
 2006            /// <summary>
 2007            /// Reported once subfolders are scanned.
 2008            /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders].
 2009            /// </summary>
 2010            public const int ScannedSubfolders = 50;
 2011
 2012            /// <summary>
 2013            /// Reported once metadata is refreshed.
 2014            /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed].
 2015            /// </summary>
 2016            public const int RefreshedMetadata = 100;
 2017
 2018            /// <summary>
 2019            /// Gets the current progress given the previous step, next step, and progress in between.
 2020            /// </summary>
 2021            /// <param name="previousProgressStep">The previous progress step.</param>
 2022            /// <param name="nextProgressStep">The next progress step.</param>
 2023            /// <param name="currentProgress">The current progress step.</param>
 2024            /// <returns>The progress.</returns>
 2025            public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress)
 2026            {
 802027                return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100));
 2028            }
 2029        }
 2030    }
 2031}

Methods/Properties

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