From 077fa89717957f871b172ca4b2dc4a178efd3bc5 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 7 Mar 2026 20:12:42 +0100 Subject: Split BaseItemRepository and IItemRepository --- .../Persistence/IItemQueryHelpers.cs | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs (limited to 'MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs') diff --git a/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs b/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs new file mode 100644 index 0000000000..45fa92c90b --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs @@ -0,0 +1,94 @@ +using System; +using System.Linq; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Persistence; + +/// +/// Provides shared query-building methods used by extracted item services. +/// Implemented by BaseItemRepository. +/// +public interface IItemQueryHelpers +{ + /// + /// Translates an into EF Core filter expressions. + /// + /// The base queryable to filter. + /// The database context. + /// The query filter. + /// The filtered queryable. + IQueryable TranslateQuery( + IQueryable baseQuery, + JellyfinDbContext context, + InternalItemsQuery filter); + + /// + /// Prepares a base query for items from the context. + /// + /// The database context. + /// The query filter. + /// The prepared queryable. + IQueryable PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter); + + /// + /// Applies user access filtering (library access, parental controls, tags) to a query. + /// + /// The database context. + /// The base queryable to filter. + /// The query filter containing access settings. + /// The access-filtered queryable. + IQueryable ApplyAccessFiltering( + JellyfinDbContext context, + IQueryable baseQuery, + InternalItemsQuery filter); + + /// + /// Applies navigation property includes to a query based on filter options. + /// + /// The queryable to apply navigations to. + /// The query filter. + /// The queryable with navigation includes. + IQueryable ApplyNavigations( + IQueryable dbQuery, + InternalItemsQuery filter); + + /// + /// Applies ordering to a query based on filter options. + /// + /// The queryable to order. + /// The query filter. + /// The database context. + /// The ordered queryable. + IQueryable ApplyOrder( + IQueryable query, + InternalItemsQuery filter, + JellyfinDbContext context); + + /// + /// Builds a query for descendants of an ancestor with user access filtering applied. + /// + /// The database context. + /// The query filter. + /// The ancestor item ID. + /// The filtered descendant queryable. + IQueryable BuildAccessFilteredDescendantsQuery( + JellyfinDbContext context, + InternalItemsQuery filter, + Guid ancestorId); + + /// + /// Deserializes a into a . + /// + /// The database entity. + /// Whether to skip JSON deserialization. + /// The deserialized item, or null. + BaseItem? DeserializeBaseItem(BaseItemEntity entity, bool skipDeserialization = false); + + /// + /// Prepares a filter query by adjusting limits and virtual item settings. + /// + /// The query to prepare. + void PrepareFilterQuery(InternalItemsQuery query); +} -- cgit v1.2.3 From 00b08c0b32b3c8fa36330d72e4a25c7b157de4e3 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 3 May 2026 13:26:30 +0200 Subject: Omit BoxSet related materialization --- .../Item/BaseItemRepository.QueryBuilding.cs | 45 ++++++++-------------- .../Item/BaseItemRepository.TranslateQuery.cs | 17 ++++---- .../Persistence/IItemQueryHelpers.cs | 13 +++++++ 3 files changed, 37 insertions(+), 38 deletions(-) (limited to 'MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs') diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs index 7570421e78..d6ddf8f5c8 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs @@ -495,53 +495,49 @@ public sealed partial class BaseItemRepository && (lc.Child.InheritedParentalRatingSubValue ?? 0) <= maxSubScore))))); } - private Dictionary GetPlayedAndTotalCountBatch(IReadOnlyList folderIds, User user) + /// + public IQueryable GetFullyPlayedFolderIdsQuery(JellyfinDbContext context, IQueryable folderIds, User user) { + ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(folderIds); ArgumentNullException.ThrowIfNull(user); - if (folderIds.Count == 0) - { - return new Dictionary(); - } - - using var dbContext = _dbProvider.CreateDbContext(); - var folderIdsArray = folderIds.ToArray(); var filter = new InternalItemsQuery(user); var userId = user.Id; - var leafItems = dbContext.BaseItems + var leafItems = context.BaseItems + .AsNoTracking() .Where(b => !b.IsFolder && !b.IsVirtualItem); - leafItems = ApplyAccessFiltering(dbContext, leafItems, filter); + leafItems = ApplyAccessFiltering(context, leafItems, filter); var playedLeafItems = leafItems .Select(b => new { b.Id, Played = b.UserData!.Any(ud => ud.UserId == userId && ud.Played) }); - var ancestorLeaves = dbContext.AncestorIds - .WhereOneOrMany(folderIdsArray, a => a.ParentItemId) + var ancestorLeaves = context.AncestorIds + .Where(a => folderIds.Contains(a.ParentItemId)) .Join( playedLeafItems, a => a.ItemId, b => b.Id, (a, b) => new { FolderId = a.ParentItemId, b.Id, b.Played }); - var linkedLeaves = dbContext.LinkedChildren - .WhereOneOrMany(folderIdsArray, lc => lc.ParentId) + var linkedLeaves = context.LinkedChildren + .Where(lc => folderIds.Contains(lc.ParentId)) .Join( playedLeafItems, lc => lc.ChildId, b => b.Id, (lc, b) => new { FolderId = lc.ParentId, b.Id, b.Played }); - var linkedFolderLeaves = dbContext.LinkedChildren - .WhereOneOrMany(folderIdsArray, lc => lc.ParentId) + var linkedFolderLeaves = context.LinkedChildren + .Where(lc => folderIds.Contains(lc.ParentId)) .Join( - dbContext.BaseItems.Where(b => b.IsFolder), + context.BaseItems.Where(b => b.IsFolder), lc => lc.ChildId, b => b.Id, (lc, b) => new { lc.ParentId, FolderChildId = b.Id }) .Join( - dbContext.AncestorIds, + context.AncestorIds, x => x.FolderChildId, a => a.ParentItemId, (x, a) => new { x.ParentId, DescendantId = a.ItemId }) @@ -551,18 +547,11 @@ public sealed partial class BaseItemRepository b => b.Id, (x, b) => new { FolderId = x.ParentId, b.Id, b.Played }); - var results = ancestorLeaves + return ancestorLeaves .Union(linkedLeaves) .Union(linkedFolderLeaves) .GroupBy(x => x.FolderId) - .Select(g => new - { - FolderId = g.Key, - Total = g.Select(x => x.Id).Distinct().Count(), - Played = g.Where(x => x.Played).Select(x => x.Id).Distinct().Count() - }) - .ToDictionary(x => x.FolderId, x => (x.Played, x.Total)); - - return results; + .Where(g => g.Select(x => x.Id).Distinct().Count() == g.Where(x => x.Played).Select(x => x.Id).Distinct().Count()) + .Select(g => g.Key); } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index 9a57691fbd..0abe981af8 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -471,16 +471,13 @@ public sealed partial class BaseItemRepository .Select(g => g.Key) : Enumerable.Empty().AsQueryable(); - // BoxSet: played = all children played - IEnumerable playedBoxSetIds = []; - if (hasBoxSet) - { - var boxSetIds = baseQuery.Where(e => e.Type == boxSetTypeName).Select(e => e.Id).ToList(); - var playedCounts = GetPlayedAndTotalCountBatch(boxSetIds, filter.User!); - playedBoxSetIds = playedCounts - .Where(kvp => kvp.Value.Total > 0 && kvp.Value.Played == kvp.Value.Total) - .Select(kvp => kvp.Key); - } + // BoxSet: played = all children played. + IQueryable playedBoxSetIds = hasBoxSet + ? GetFullyPlayedFolderIdsQuery( + context, + baseQuery.Where(e => e.Type == boxSetTypeName).Select(e => e.Id), + filter.User!) + : Enumerable.Empty().AsQueryable(); // Non-folder items: check UserData directly var playedItemIds = context.UserData diff --git a/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs b/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs index 45fa92c90b..2e29cbdbba 100644 --- a/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs +++ b/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs @@ -78,6 +78,19 @@ public interface IItemQueryHelpers InternalItemsQuery filter, Guid ancestorId); + /// + /// Builds an of folder IDs whose descendants are all played + /// for the given user. Composable into outer queries to avoid an extra DB roundtrip. + /// + /// The database context the resulting query is bound to. + /// A query yielding candidate folder IDs. + /// The user for access filtering and played status. + /// An of fully-played folder IDs. + IQueryable GetFullyPlayedFolderIdsQuery( + JellyfinDbContext context, + IQueryable folderIds, + User user); + /// /// Deserializes a into a . /// -- cgit v1.2.3