diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 9567a29a98..cd93f76d26 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -9,6 +9,7 @@ using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; @@ -235,7 +236,7 @@ namespace Emby.Server.Implementations.Collections List? itemList = null; - var linkedChildrenList = collection.GetLinkedChildren(); + var linkedChildrenList = collection.GetLinkedChildren(DtoOptions.StoredColumnsOnly); var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList(); foreach (var id in ids) diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index db911cd24a..36af2e4caf 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -60,20 +60,15 @@ namespace Emby.Server.Implementations.Library var folderViewType = collectionFolder?.CollectionType; // Playlist and BoxSet libraries require special handling because the folder only references linked items - if (folderViewType == CollectionType.boxsets) - { - // Only the existence of one visible box set matters here, so probe the children - // lazily and stop at the first hit. - if (!folder.Children.Any(item => item.IsVisible(user))) - { - continue; - } - } - else if (folderViewType == CollectionType.playlists) + if (folderViewType == CollectionType.playlists || folderViewType == CollectionType.boxsets) { + // Only visibility is asked of these, which reads stored columns. Folder.Children + // would answer it from cache once warm, but it populates that cache with a query + // for every field, which is the one this has to avoid on a cold start. var items = folder.GetItemList(new InternalItemsQuery(user) { - ParentId = folder.ParentId + ParentId = folder.ParentId, + DtoOptions = DtoOptions.StoredColumnsOnly }); if (!items.Any(item => item.IsVisible(user))) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 308faed8cc..8208c85222 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -371,7 +371,7 @@ namespace Emby.Server.Implementations.Playlists if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new WplPlaylist(); - foreach (var child in item.GetLinkedChildren()) + foreach (var child in item.GetLinkedChildren(DtoOptions.StoredColumnsOnly)) { var entry = new WplPlaylistEntry() { @@ -404,7 +404,7 @@ namespace Emby.Server.Implementations.Playlists else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new ZplPlaylist(); - foreach (var child in item.GetLinkedChildren()) + foreach (var child in item.GetLinkedChildren(DtoOptions.StoredColumnsOnly)) { var entry = new ZplPlaylistEntry() { @@ -440,7 +440,7 @@ namespace Emby.Server.Implementations.Playlists { IsExtended = true }; - foreach (var child in item.GetLinkedChildren()) + foreach (var child in item.GetLinkedChildren(DtoOptions.StoredColumnsOnly)) { var entry = new M3uPlaylistEntry() { @@ -472,7 +472,7 @@ namespace Emby.Server.Implementations.Playlists IsExtended = true }; - foreach (var child in item.GetLinkedChildren()) + foreach (var child in item.GetLinkedChildren(DtoOptions.StoredColumnsOnly)) { var entry = new M3uPlaylistEntry() { @@ -500,7 +500,7 @@ namespace Emby.Server.Implementations.Playlists else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase)) { var playlist = new PlsPlaylist(); - foreach (var child in item.GetLinkedChildren()) + foreach (var child in item.GetLinkedChildren(DtoOptions.StoredColumnsOnly)) { var entry = new PlsPlaylistEntry() { diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index 052626355f..4259b7b65c 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -46,6 +46,20 @@ namespace MediaBrowser.Controller.Dto ImageTypes = AllImageTypes; } + /// + /// Gets options that populate nothing beyond the item's own stored columns. + /// + /// + /// Each enabled field group is a collection the item query left-joins, so the rows returned are + /// the product of the item's provider, image and user data counts. Never use this for items that + /// get saved back: saving rewrites the owned rows from what the instance holds. + /// + public static DtoOptions StoredColumnsOnly => new(false) + { + EnableImages = false, + EnableUserData = false + }; + /// /// Gets or sets the fields to populate on the DTO. /// diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index c02c98f889..10597d514c 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1656,7 +1656,17 @@ namespace MediaBrowser.Controller.Entities /// IEnumerable{BaseItem}. public List GetLinkedChildren() { - var resolved = ResolveLinkedChildren(LinkedChildren); + return GetLinkedChildren(new DtoOptions()); + } + + /// + /// Gets the linked children, populating only what asks for. + /// + /// Fields to populate on the resolved children. + /// The resolved children. + public List GetLinkedChildren(DtoOptions options) + { + var resolved = ResolveLinkedChildren(LinkedChildren, options); var list = new List(resolved.Count); foreach (var (_, item) in resolved) { @@ -1773,8 +1783,9 @@ namespace MediaBrowser.Controller.Entities /// path (legacy path-based resolution). /// /// Linked children to resolve. + /// Fields to populate on the resolved items; all fields when null. /// Each input entry paired with its resolved item; entries that fail to resolve are dropped. - private List<(LinkedChild Info, BaseItem Item)> ResolveLinkedChildren(IReadOnlyList linkedChildren) + private List<(LinkedChild Info, BaseItem Item)> ResolveLinkedChildren(IReadOnlyList linkedChildren, DtoOptions options = null) { var resolved = new List<(LinkedChild Info, BaseItem Item)>(linkedChildren.Count); if (linkedChildren.Count == 0) @@ -1796,7 +1807,8 @@ namespace MediaBrowser.Controller.Entities { var batched = LibraryManager.GetItemList(new InternalItemsQuery { - ItemIds = [.. idsToBatch] + ItemIds = [.. idsToBatch], + DtoOptions = options ?? new DtoOptions() }); byId = new Dictionary(batched.Count); foreach (var item in batched) diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index d7f9102be9..16d8bfe391 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -11,6 +11,7 @@ using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Querying; @@ -89,7 +90,7 @@ namespace MediaBrowser.Controller.Entities.Movies return base.GetNonCachedChildren(directoryService); } - return Enumerable.Empty(); + return []; } protected override IReadOnlyList LoadChildren() @@ -172,7 +173,7 @@ namespace MediaBrowser.Controller.Entities.Movies var libraryFolderIds = LibraryFolderIds; if (libraryFolderIds is null) { - linkedItems = GetLinkedChildren(); + linkedItems = GetLinkedChildren(DtoOptions.StoredColumnsOnly); libraryFolderIds = GetLibraryFolderIds(linkedItems); } @@ -191,7 +192,7 @@ namespace MediaBrowser.Controller.Entities.Movies // If user has parental controls, hide the BoxSet when all children are restricted if (user.MaxParentalRatingScore.HasValue) { - linkedItems ??= GetLinkedChildren(); + linkedItems ??= GetLinkedChildren(DtoOptions.StoredColumnsOnly); if (linkedItems.Count > 0 && linkedItems.All(child => !child.IsParentalAllowed(user, true))) { return false; @@ -248,7 +249,7 @@ namespace MediaBrowser.Controller.Entities.Movies public Guid[] GetLibraryFolderIds() { - return GetLibraryFolderIds(GetLinkedChildren()); + return GetLibraryFolderIds(GetLinkedChildren(DtoOptions.StoredColumnsOnly)); } private Guid[] GetLibraryFolderIds(IEnumerable linkedChildren) @@ -280,13 +281,13 @@ namespace MediaBrowser.Controller.Entities.Movies { expandedFolders.Add(item.Id); - return FlattenItems(boxset.GetLinkedChildren(), expandedFolders); + return FlattenItems(boxset.GetLinkedChildren(DtoOptions.StoredColumnsOnly), expandedFolders); } - return Array.Empty(); + return []; } - return new[] { item }; + return [item]; } } } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index bc184d82fb..d5e233d4f8 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using System.Xml; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; @@ -500,7 +501,8 @@ namespace MediaBrowser.LocalMetadata.Savers { var batched = LibraryManager.GetItemList(new InternalItemsQuery { - ItemIds = [.. idsToResolve] + ItemIds = [.. idsToResolve], + DtoOptions = DtoOptions.StoredColumnsOnly }); pathById = new Dictionary(batched.Count); foreach (var batchedItem in batched)