aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-08-01 14:25:20 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-08-01 14:25:20 +0200
commitb94ba9d409abd48b9e01bee8c039e85d951505ab (patch)
treec27d4734d351941cb5d01b2145374cd264702b0a /Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
parent5a2809e33725631ed25c0361331060e1821b66de (diff)
parentc55fde25a54e9d2c2ba0e781d4a6f790990f85bd (diff)
Merge remote-tracking branch 'upstream/master' into tmdb-missing-episodes
# Conflicts: # Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
Diffstat (limited to 'Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs')
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs82
1 files changed, 29 insertions, 53 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
index 1b02f2ae41..524a712776 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
@@ -62,18 +62,21 @@ public sealed partial class BaseItemRepository
private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
- // Collapse duplicates sharing a presentation key (e.g. alternate versions) by picking
- // the min Id per group. Keep the grouped ids as an IQueryable sub-select; materializing
+ // Collapse duplicates sharing a presentation key (e.g. alternate versions), preferring the
+ // primary version (PrimaryVersionId is null) so detail pages and actions target it instead
+ // of an arbitrary alternate. Keep the grouped ids as an IQueryable sub-select; materializing
// to a List would inline one bound parameter per id and hit SQLite's variable cap.
var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
{
- var groupedIds = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.Min(x => x.Id));
+ var groupedIds = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey })
+ .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
}
else if (enableGroupByPresentationUniqueKey)
{
- var groupedIds = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.Min(x => x.Id));
+ var groupedIds = dbQuery.GroupBy(e => e.PresentationUniqueKey)
+ .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
}
else if (filter.GroupBySeriesPresentationUniqueKey)
@@ -445,6 +448,7 @@ public sealed partial class BaseItemRepository
if (filter.IncludeInheritedTags.Length > 0)
{
var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
+ var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
var allowedTagItemIds = context.ItemValuesMap
.Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
.Select(f => f.ItemId);
@@ -453,7 +457,10 @@ public sealed partial class BaseItemRepository
allowedTagItemIds.Contains(e.Id)
|| (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
|| e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
- || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value)));
+ || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
+
+ // People don't carry the tags of the media they appear in and would never match
+ || e.Type == personTypeName);
}
// Exclude alternate versions (have PrimaryVersionId set) and owned non-extra items.
@@ -497,62 +504,31 @@ public sealed partial class BaseItemRepository
}
/// <inheritdoc />
- public IQueryable<Guid> GetFullyPlayedFolderIdsQuery(JellyfinDbContext context, IQueryable<Guid> folderIds, User user)
+ public IQueryable<BaseItemEntity> GetAccessFilteredLeafItemsQuery(JellyfinDbContext context, User user, bool includeOwnedItems = false)
{
ArgumentNullException.ThrowIfNull(context);
- ArgumentNullException.ThrowIfNull(folderIds);
ArgumentNullException.ThrowIfNull(user);
- var filter = new InternalItemsQuery(user);
- var userId = user.Id;
-
var leafItems = context.BaseItems
.AsNoTracking()
.Where(DescendantQueryHelper.IsCountableLeaf);
- 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 = 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 = 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 });
+ return ApplyAccessFiltering(context, leafItems, new InternalItemsQuery(user) { IncludeOwnedItems = includeOwnedItems });
+ }
- var linkedFolderLeaves = context.LinkedChildren
- .Where(lc => folderIds.Contains(lc.ParentId))
- .Join(
- context.BaseItems.Where(b => b.IsFolder),
- lc => lc.ChildId,
- b => b.Id,
- (lc, b) => new { lc.ParentId, FolderChildId = b.Id })
- .Join(
- context.AncestorIds,
- x => x.FolderChildId,
- a => a.ParentItemId,
- (x, a) => new { x.ParentId, DescendantId = a.ItemId })
- .Join(
- playedLeafItems,
- x => x.DescendantId,
- b => b.Id,
- (x, b) => new { FolderId = x.ParentId, b.Id, b.Played });
-
- return ancestorLeaves
- .Union(linkedLeaves)
- .Union(linkedFolderLeaves)
- .GroupBy(x => x.FolderId)
- .Where(g => g.Select(x => x.Id).Distinct().Count() == g.Where(x => x.Played).Select(x => x.Id).Distinct().Count())
- .Select(g => g.Key);
+ /// <inheritdoc />
+ public Expression<Func<BaseItemEntity, bool>> BuildHasDescendantFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> descendants)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(descendants);
+
+ // Descendants are reachable through the ancestor chain and - for BoxSets and Playlists - as
+ // linked children, which can themselves be folders contributing their own descendants.
+ // Every step is a correlated index seek, so only the rows the outer query keeps are visited
+ // and a folder is left as soon as its first matching descendant is found.
+ return e => context.AncestorIds.Any(a => a.ParentItemId == e.Id && descendants.Any(d => d.Id == a.ItemId))
+ || context.LinkedChildren.Any(lc => lc.ParentId == e.Id
+ && (descendants.Any(d => d.Id == lc.ChildId)
+ || context.AncestorIds.Any(a => a.ParentItemId == lc.ChildId && descendants.Any(d => d.Id == a.ItemId))));
}
}