From fa65a392b0e754848caf94f08724ba19ec8bdd9f Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 4 May 2026 10:22:13 +0200 Subject: Fix Playlist and Boxset query and count perf --- MediaBrowser.Controller/Entities/Folder.cs | 82 +++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 13 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 3b20dc85fe..5fa1213db3 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1593,17 +1593,11 @@ namespace MediaBrowser.Controller.Entities /// IEnumerable{BaseItem}. public List GetLinkedChildren() { - var linkedChildren = LinkedChildren; - var list = new List(linkedChildren.Length); - - foreach (var i in linkedChildren) + var resolved = ResolveLinkedChildren(LinkedChildren); + var list = new List(resolved.Count); + foreach (var (_, item) in resolved) { - var child = GetLinkedChild(i); - - if (child is not null) - { - list.Add(child); - } + list.Add(item); } return list; @@ -1704,12 +1698,74 @@ namespace MediaBrowser.Controller.Entities /// IEnumerable{BaseItem}. public IReadOnlyList> GetLinkedChildrenInfos() { - return LinkedChildren - .Select(i => new Tuple(i, GetLinkedChild(i))) - .Where(i => i.Item2 is not null) + return ResolveLinkedChildren(LinkedChildren) + .Select(t => new Tuple(t.Info, t.Item)) .ToArray(); } + /// + /// Resolves a list of entries to their targets, + /// batching the database lookup across all entries with a known ItemId. + /// Entries without a usable ItemId fall back to the per-entry + /// path (legacy path-based resolution). + /// + /// Linked children to resolve. + /// Each input entry paired with its resolved item; entries that fail to resolve are dropped. + private List<(LinkedChild Info, BaseItem Item)> ResolveLinkedChildren(IReadOnlyList linkedChildren) + { + var resolved = new List<(LinkedChild Info, BaseItem Item)>(linkedChildren.Count); + if (linkedChildren.Count == 0) + { + return resolved; + } + + var idsToBatch = new HashSet(); + foreach (var info in linkedChildren) + { + if (info.ItemId.HasValue && !info.ItemId.Value.IsEmpty()) + { + idsToBatch.Add(info.ItemId.Value); + } + } + + Dictionary byId = null; + if (idsToBatch.Count > 0) + { + var batched = LibraryManager.GetItemList(new InternalItemsQuery + { + ItemIds = [.. idsToBatch] + }); + byId = new Dictionary(batched.Count); + foreach (var item in batched) + { + byId[item.Id] = item; + } + } + + foreach (var info in linkedChildren) + { + BaseItem item = null; + if (byId is not null && info.ItemId.HasValue && byId.TryGetValue(info.ItemId.Value, out var batchedItem)) + { + item = batchedItem; + } + else + { + // ItemId is missing/empty or the batched query couldn't return the item + // (e.g. it has been removed). Fall back to per-entry resolution, which also + // handles legacy path-based linked children. + item = GetLinkedChild(info); + } + + if (item is not null) + { + resolved.Add((info, item)); + } + } + + return resolved; + } + protected override async Task RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) { var changesFound = false; -- cgit v1.2.3