From 5996c4afce11249804d24f1caa3a99b390543c4d Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 17 Jan 2026 17:10:07 +0100 Subject: Complete LinkedChildren integration and batch DTO optimizations This commit integrates remaining performance changes: - Add batch user data fetching in DtoService to reduce N+1 queries - Add GetNextUpEpisodesBatch in TVSeriesManager for efficient batch retrieval - Update Video/Movie/BoxSet to use LibraryManager for alternate versions - Transition LinkedChild to use ItemId instead of Path (obsolete Path/LibraryItemId) - Update providers and controllers for LinkedChildren-based references - Add NextUpEpisodeBatchResult for batched episode queries - Integrate IDescendantQueryProvider in SqliteDatabaseProvider --- MediaBrowser.Controller/Dto/IDtoService.cs | 3 +- MediaBrowser.Controller/Entities/BaseItem.cs | 19 +- .../Entities/CollectionFolder.cs | 7 + MediaBrowser.Controller/Entities/Folder.cs | 201 ++++++----------- .../Entities/InternalItemsQuery.cs | 4 + MediaBrowser.Controller/Entities/LinkedChild.cs | 20 +- .../Entities/LinkedChildComparer.cs | 23 +- .../Entities/LinkedChildType.cs | 12 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 4 +- MediaBrowser.Controller/Entities/Movies/Movie.cs | 36 ++- MediaBrowser.Controller/Entities/TV/Episode.cs | 4 +- MediaBrowser.Controller/Entities/TV/Series.cs | 4 +- .../Entities/UserViewBuilder.cs | 243 +++------------------ MediaBrowser.Controller/Entities/Video.cs | 69 ++++-- MediaBrowser.Controller/Library/ILibraryManager.cs | 47 ++++ .../Persistence/IItemRepository.cs | 72 ++++++ .../Persistence/NextUpEpisodeBatchResult.cs | 38 ++++ 17 files changed, 397 insertions(+), 409 deletions(-) create mode 100644 MediaBrowser.Controller/Persistence/NextUpEpisodeBatchResult.cs (limited to 'MediaBrowser.Controller') diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index f1d507fcbd..8dc6f0aa68 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -36,8 +36,9 @@ namespace MediaBrowser.Controller.Dto /// The options. /// The user. /// The owner. + /// Skip redundant visibility check if items are already filtered. /// The of . - IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User? user = null, BaseItem? owner = null); + IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User? user = null, BaseItem? owner = null, bool skipVisibilityCheck = false); /// /// Gets the item by name dto. diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index eb7daeb532..13af7a6178 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1806,10 +1806,23 @@ namespace MediaBrowser.Controller.Entities return item; } +#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy LinkedChild data private BaseItem FindLinkedChild(LinkedChild info) { - var path = info.Path; + // First try to find by ItemId (new preferred method) + if (info.ItemId.HasValue && !info.ItemId.Value.Equals(Guid.Empty)) + { + var item = LibraryManager.GetItemById(info.ItemId.Value); + if (item is not null) + { + return item; + } + Logger.LogWarning("Unable to find linked item by ItemId {0}", info.ItemId); + } + + // Fall back to Path (legacy method) + var path = info.Path; if (!string.IsNullOrEmpty(path)) { path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path); @@ -1824,13 +1837,14 @@ namespace MediaBrowser.Controller.Entities return itemByPath; } + // Fall back to LibraryItemId (legacy method) if (!string.IsNullOrEmpty(info.LibraryItemId)) { var item = LibraryManager.GetItemById(info.LibraryItemId); if (item is null) { - Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item by LibraryItemId {0}", info.LibraryItemId); } return item; @@ -1838,6 +1852,7 @@ namespace MediaBrowser.Controller.Entities return null; } +#pragma warning restore CS0618 /// /// Adds a studio to the item. diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index ca79e62454..cf615788ee 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -44,6 +44,11 @@ namespace MediaBrowser.Controller.Entities PhysicalFolderIds = Array.Empty(); } + /// + /// Event raised when library options are updated for any collection folder. + /// + public static event EventHandler LibraryOptionsUpdated; + /// /// Gets the display preferences id. /// @@ -168,6 +173,8 @@ namespace MediaBrowser.Controller.Entities } XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); + + LibraryOptionsUpdated?.Invoke(null, new LibraryOptionsUpdatedEventArgs(path, options)); } public static void OnCollectionFolderChange() diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index d2a3290c47..6338e54292 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -59,6 +59,10 @@ namespace MediaBrowser.Controller.Entities /// true if this instance is root; otherwise, false. public bool IsRoot { get; set; } + /// + /// Gets or sets the linked children. + /// + [JsonIgnore] public LinkedChild[] LinkedChildren { get; set; } [JsonIgnore] @@ -455,6 +459,14 @@ namespace MediaBrowser.Controller.Entities // If it's an AggregateFolder, don't remove if (shouldRemove && itemsRemoved.Count > 0) { + // Build a set of paths that are alternate versions of valid children + // These items should not be deleted - they're managed by their primary video + var alternateVersionPaths = validChildren + .OfType