diff options
Diffstat (limited to 'Jellyfin.Server.Implementations')
6 files changed, 74 insertions, 43 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs index 958d11e21e..c2cb644c59 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs @@ -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) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs index 5aa2d7c46b..05ff720ddf 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs @@ -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 }); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs index 5ff6e6da49..c7acf72043 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs @@ -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); diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index 1694e89179..575adc6816 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -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; } } diff --git a/Jellyfin.Server.Implementations/Item/ItemCountService.cs b/Jellyfin.Server.Implementations/Item/ItemCountService.cs index 4aa65769fd..fd683fb57e 100644 --- a/Jellyfin.Server.Implementations/Item/ItemCountService.cs +++ b/Jellyfin.Server.Implementations/Item/ItemCountService.cs @@ -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 diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index 25ad81ec6c..00b10e44a9 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -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, |
