Merge remote-tracking branch 'upstream/master' into fix-byname-queries

This commit is contained in:
Shadowghost
2026-08-02 22:12:57 +02:00
36 changed files with 2850 additions and 170 deletions
@@ -35,11 +35,40 @@ public sealed partial class BaseItemRepository
{
dbQuery = TranslateQuery(dbQuery, context, filter);
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery;
}
/// <summary>
/// Trims an ordered query down to the AdjacentTo item and its immediate neighbours.
/// </summary>
private IQueryable<BaseItemEntity> ApplyAdjacencyFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
if (filter.AdjacentTo.IsNullOrEmpty())
{
return dbQuery;
}
// Adjacency is relative to the result set and the order the query asked for, so the ids have
// to be read back in that order.
var orderedIds = dbQuery.Select(e => e.Id).ToList();
var index = orderedIds.IndexOf(filter.AdjacentTo.Value);
if (index < 0)
{
// The item isn't part of this result set, so it has no neighbours in it either.
return dbQuery.Take(0);
}
var start = Math.Max(index - 1, 0);
var adjacentIds = orderedIds.GetRange(start, Math.Min(index + 2, orderedIds.Count) - start);
var adjacentQuery = context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => adjacentIds.Contains(e.Id));
return ApplyOrder(adjacentQuery, filter, context);
}
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
@@ -244,8 +273,8 @@ public sealed partial class BaseItemRepository
dbQuery = dbQuery.Include(e => e.Images);
}
// Include LinkedChildEntities for container types and videos that use them
// (BoxSet, Playlist, CollectionFolder for manual linking; Video, Movie for alternate versions).
// Include LinkedChildEntities for container types and videos that use them (BoxSet, Playlist,
// CollectionFolder for manual linking; every video type for alternate versions).
// When IncludeItemTypes is empty (any type may be returned), always include them to ensure
// LinkedChildren are loaded before items are saved back, preventing accidental deletion.
var linkedChildTypes = new[]
@@ -254,7 +283,10 @@ public sealed partial class BaseItemRepository
BaseItemKind.Playlist,
BaseItemKind.CollectionFolder,
BaseItemKind.Video,
BaseItemKind.Movie
BaseItemKind.Movie,
BaseItemKind.Episode,
BaseItemKind.MusicVideo,
BaseItemKind.Trailer
};
if (filter.IncludeItemTypes.Length == 0 || filter.IncludeItemTypes.Any(linkedChildTypes.Contains))
{
@@ -390,7 +422,8 @@ public sealed partial class BaseItemRepository
var baseQuery = context.BaseItems
.AsNoTracking()
.Where(b => allDescendantIds.Contains(b.Id) && !b.IsFolder && !b.IsVirtualItem);
.Where(b => allDescendantIds.Contains(b.Id))
.Where(DescendantQueryHelper.IsCountableLeaf);
return ApplyAccessFiltering(context, baseQuery, filter);
}
@@ -628,7 +661,7 @@ public sealed partial class BaseItemRepository
var leafItems = context.BaseItems
.AsNoTracking()
.Where(e => !e.IsFolder && !e.IsVirtualItem);
.Where(DescendantQueryHelper.IsCountableLeaf);
return ApplyAccessFiltering(context, leafItems, new InternalItemsQuery(user) { IncludeOwnedItems = includeOwnedItems });
}