Merge remote-tracking branch 'upstream/master' into fix-byname-queries
This commit is contained in:
@@ -134,6 +134,21 @@ public static class BaseItemMapper
|
||||
if (dto is Video video)
|
||||
{
|
||||
video.PrimaryVersionId = entity.PrimaryVersionId;
|
||||
|
||||
// The LinkedChildren table is the source of truth for version links
|
||||
if (entity.LinkedChildEntities is not null)
|
||||
{
|
||||
video.LinkedAlternateVersions = entity.LinkedChildEntities
|
||||
// LocalAlternateVersion links belong to Video.LocalAlternateVersions, not here
|
||||
.Where(e => e.ChildType == Database.Implementations.Entities.LinkedChildType.LinkedAlternateVersion)
|
||||
.OrderBy(e => e.SortOrder)
|
||||
.Select(e => new LinkedChild
|
||||
{
|
||||
ItemId = e.ChildId,
|
||||
Type = (MediaBrowser.Controller.Entities.LinkedChildType)e.ChildType
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
if (dto is IHasSeries hasSeriesName)
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ public sealed partial class BaseItemRepository
|
||||
|
||||
dbQuery = TranslateQuery(dbQuery, context, filter);
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
|
||||
|
||||
if (filter.EnableTotalRecordCount)
|
||||
{
|
||||
@@ -75,6 +76,7 @@ public sealed partial class BaseItemRepository
|
||||
dbQuery = TranslateQuery(dbQuery, context, filter);
|
||||
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
|
||||
var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
|
||||
|
||||
@@ -1099,16 +1099,23 @@ public sealed partial class BaseItemRepository
|
||||
: baseQuery.WhereNeitherItemNorDescendantMatches(context, isPlaceHolder);
|
||||
}
|
||||
|
||||
// An extra is owned by the single version of an item it is named after, so an extra on any
|
||||
// version counts for the item itself
|
||||
IQueryable<Guid> WithPrimaryVersions(IQueryable<Guid> ownerIds)
|
||||
=> ownerIds.Concat(context.BaseItems
|
||||
.Where(version => version.PrimaryVersionId != null && ownerIds.Contains(version.Id))
|
||||
.Select(version => version.PrimaryVersionId!.Value));
|
||||
|
||||
if (filter.HasSpecialFeature.HasValue)
|
||||
{
|
||||
var itemsWithExtras = context.BaseItems
|
||||
var itemsWithExtras = WithPrimaryVersions(context.BaseItems
|
||||
.Where(extra => extra.OwnerId != null
|
||||
&& extra.ExtraType != null
|
||||
&& extra.ExtraType != BaseItemExtraType.Unknown
|
||||
&& extra.ExtraType != BaseItemExtraType.Trailer
|
||||
&& extra.ExtraType != BaseItemExtraType.ThemeSong
|
||||
&& extra.ExtraType != BaseItemExtraType.ThemeVideo)
|
||||
.Select(extra => extra.OwnerId!.Value)
|
||||
.Select(extra => extra.OwnerId!.Value))
|
||||
.Distinct();
|
||||
|
||||
Expression<Func<BaseItemEntity, bool>> hasExtras = e => itemsWithExtras.Contains(e.Id);
|
||||
@@ -1120,9 +1127,9 @@ public sealed partial class BaseItemRepository
|
||||
|
||||
if (filter.HasTrailer.HasValue)
|
||||
{
|
||||
var trailerOwnerIds = context.BaseItems
|
||||
var trailerOwnerIds = WithPrimaryVersions(context.BaseItems
|
||||
.Where(extra => extra.ExtraType == BaseItemExtraType.Trailer && extra.OwnerId != null)
|
||||
.Select(extra => extra.OwnerId!.Value);
|
||||
.Select(extra => extra.OwnerId!.Value));
|
||||
|
||||
Expression<Func<BaseItemEntity, bool>> hasTrailer = e => trailerOwnerIds.Contains(e.Id);
|
||||
|
||||
@@ -1133,9 +1140,9 @@ public sealed partial class BaseItemRepository
|
||||
|
||||
if (filter.HasThemeSong.HasValue)
|
||||
{
|
||||
var themeSongOwnerIds = context.BaseItems
|
||||
var themeSongOwnerIds = WithPrimaryVersions(context.BaseItems
|
||||
.Where(extra => extra.ExtraType == BaseItemExtraType.ThemeSong && extra.OwnerId != null)
|
||||
.Select(extra => extra.OwnerId!.Value);
|
||||
.Select(extra => extra.OwnerId!.Value));
|
||||
|
||||
Expression<Func<BaseItemEntity, bool>> hasThemeSong = e => themeSongOwnerIds.Contains(e.Id);
|
||||
|
||||
@@ -1146,9 +1153,9 @@ public sealed partial class BaseItemRepository
|
||||
|
||||
if (filter.HasThemeVideo.HasValue)
|
||||
{
|
||||
var themeVideoOwnerIds = context.BaseItems
|
||||
var themeVideoOwnerIds = WithPrimaryVersions(context.BaseItems
|
||||
.Where(extra => extra.ExtraType == BaseItemExtraType.ThemeVideo && extra.OwnerId != null)
|
||||
.Select(extra => extra.OwnerId!.Value);
|
||||
.Select(extra => extra.OwnerId!.Value));
|
||||
|
||||
Expression<Func<BaseItemEntity, bool>> hasThemeVideo = e => themeVideoOwnerIds.Contains(e.Id);
|
||||
|
||||
@@ -1175,33 +1182,6 @@ public sealed partial class BaseItemRepository
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.AdjacentTo.HasValue && !filter.AdjacentTo.Value.IsEmpty())
|
||||
{
|
||||
var adjacentToId = filter.AdjacentTo.Value;
|
||||
var targetItem = context.BaseItems.Where(e => e.Id == adjacentToId).Select(e => new { e.SortName, e.Id }).FirstOrDefault();
|
||||
if (targetItem is not null)
|
||||
{
|
||||
var targetSortName = targetItem.SortName ?? string.Empty;
|
||||
|
||||
// Fetch both prev and next adjacent items in a single query using Concat (UNION ALL).
|
||||
var adjacentIds = context.BaseItems
|
||||
.Where(e => string.Compare(e.SortName, targetSortName) < 0)
|
||||
.OrderByDescending(e => e.SortName)
|
||||
.Select(e => e.Id)
|
||||
.Take(1)
|
||||
.Concat(
|
||||
context.BaseItems
|
||||
.Where(e => string.Compare(e.SortName, targetSortName) > 0)
|
||||
.OrderBy(e => e.SortName)
|
||||
.Select(e => e.Id)
|
||||
.Take(1))
|
||||
.ToList();
|
||||
|
||||
adjacentIds.Add(adjacentToId);
|
||||
baseQuery = baseQuery.Where(e => adjacentIds.Contains(e.Id));
|
||||
}
|
||||
}
|
||||
|
||||
return baseQuery;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,8 @@ public class ItemCountService : IItemCountService
|
||||
|
||||
var allDescendantIds = DescendantQueryHelper.GetAllDescendantIds(dbContext, parentId);
|
||||
var baseQuery = dbContext.BaseItems
|
||||
.Where(b => allDescendantIds.Contains(b.Id) && !b.IsFolder && !b.IsVirtualItem);
|
||||
.Where(b => allDescendantIds.Contains(b.Id))
|
||||
.Where(DescendantQueryHelper.IsCountableLeaf);
|
||||
baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, filter);
|
||||
|
||||
return GetPlayedAndTotalCountFromQuery(baseQuery, filter.User.Id);
|
||||
@@ -357,7 +358,7 @@ public class ItemCountService : IItemCountService
|
||||
var userId = user.Id;
|
||||
|
||||
var leafItems = dbContext.BaseItems
|
||||
.Where(b => !b.IsFolder && !b.IsVirtualItem);
|
||||
.Where(DescendantQueryHelper.IsCountableLeaf);
|
||||
leafItems = _queryHelpers.ApplyAccessFiltering(dbContext, leafItems, filter);
|
||||
|
||||
var playedLeafItems = leafItems
|
||||
|
||||
@@ -68,7 +68,7 @@ public static class OrderMapper
|
||||
(ItemSortBy.DateCreated, _) => e => e.DateCreated,
|
||||
(ItemSortBy.PremiereDate, _) => e => e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null),
|
||||
(ItemSortBy.StartDate, _) => e => e.StartDate,
|
||||
(ItemSortBy.Name, _) => e => e.SortName,
|
||||
(ItemSortBy.Name, _) => e => e.CleanName,
|
||||
(ItemSortBy.CommunityRating, _) => e => e.CommunityRating,
|
||||
(ItemSortBy.ProductionYear, _) => e => e.ProductionYear,
|
||||
(ItemSortBy.CriticRating, _) => e => e.CriticRating,
|
||||
|
||||
Reference in New Issue
Block a user