aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-05-04 01:56:21 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-05-04 02:00:38 +0200
commit88cad2ad1acc158c48de950e0b0adb03d2d84b89 (patch)
treed3a11e2c882b89619bf464ce681e75e22e29ffc9 /Jellyfin.Server.Implementations
parent622947e37425f3620432995cde5d4a0809d91694 (diff)
Speed-up LatestItems for Music
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs89
1 files changed, 52 insertions, 37 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs
index d516bc0d13..5a96205b04 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Linq.Expressions;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
@@ -125,53 +124,69 @@ public sealed partial class BaseItemRepository
return GetLatestTvShowItems(context, baseQuery, filter, limit);
}
- // Find the top N group keys ordered by most recent DateCreated.
- // Movies group by PresentationUniqueKey (alternate versions like 4K/1080p share a key).
- // Music groups by Album.
- Expression<Func<BaseItemEntity, bool>> groupKeyFilter;
- Expression<Func<BaseItemEntity, string?>> groupKeySelector;
-
+ // Resolve the top N result item ids in a single SQL statement, ordered by the
+ // group's most recent DateCreated. Movies and music differ in what an "item"
+ // is, so the grouping shape is per-branch.
+ List<Guid> firstIds;
if (collectionType is CollectionType.movies)
{
- groupKeyFilter = e => e.PresentationUniqueKey != null;
- groupKeySelector = e => e.PresentationUniqueKey;
+ // Movies group by PresentationUniqueKey. Alternate versions (4K/1080p of the
+ // same movie) share that key, but they're already filtered out upstream by
+ // PrimaryVersionId IS NULL.
+ var topGroupItems = baseQuery
+ .Where(e => e.PresentationUniqueKey != null)
+ .GroupBy(e => e.PresentationUniqueKey)
+ .Select(g => new
+ {
+ MaxDate = g.Max(e => e.DateCreated),
+ FirstId = g.OrderByDescending(e => e.DateCreated).ThenByDescending(e => e.Id).Select(e => e.Id).First()
+ })
+ .OrderByDescending(g => g.MaxDate);
+
+ var idsQuery = filter.Limit.HasValue
+ ? topGroupItems.Take(filter.Limit.Value).Select(g => g.FirstId)
+ : topGroupItems.Select(g => g.FirstId);
+
+ firstIds = idsQuery.ToList();
}
else
{
- groupKeyFilter = e => e.Album != null;
- groupKeySelector = e => e.Album;
+ // Music returns MusicAlbum entities, ordered by their latest track's
+ // DateCreated. Group by the MusicAlbum ancestor of each track via
+ // AncestorIds.
+ var musicAlbumType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]!;
+
+ var topGroupItems =
+ from ancestor in context.AncestorIds
+ join track in baseQuery on ancestor.ItemId equals track.Id
+ join album in context.BaseItems on ancestor.ParentItemId equals album.Id
+ where album.Type == musicAlbumType
+ group track.DateCreated by album.Id into g
+ orderby g.Max() descending
+ select new { AlbumId = g.Key, MaxDate = g.Max() };
+
+ var idsQuery = filter.Limit.HasValue
+ ? topGroupItems.Take(filter.Limit.Value).Select(g => g.AlbumId)
+ : topGroupItems.Select(g => g.AlbumId);
+
+ firstIds = idsQuery.ToList();
}
- // Group by GroupKey, pick the latest item per group (correlated subquery: ORDER BY DateCreated DESC, Id DESC LIMIT 1),
- // order groups by group max date, take the top N — all in a single SQL statement.
- // ThenByDescending(Id) is the tiebreaker for deterministic ordering when items share a DateCreated.
- var topGroupItems = baseQuery
- .Where(groupKeyFilter)
- .GroupBy(groupKeySelector)
- .Select(g => new
- {
- MaxDate = g.Max(e => e.DateCreated),
- FirstId = g.OrderByDescending(e => e.DateCreated).ThenByDescending(e => e.Id).Select(e => e.Id).First()
- })
- .OrderByDescending(g => g.MaxDate);
-
- var firstIdsQuery = filter.Limit.HasValue
- ? topGroupItems.Take(filter.Limit.Value).Select(g => g.FirstId)
- : topGroupItems.Select(g => g.FirstId);
+ // Load the result items by id. The order from firstIds is the group ordering
+ // we want; we re-apply it via dictionary lookup because for music the loaded
+ // album's own DateCreated may not match the album's latest-track date, so a
+ // SQL ORDER BY DateCreated wouldn't preserve it.
+ var itemsQuery = ApplyNavigations(
+ context.BaseItems.AsNoTracking().WhereOneOrMany(firstIds, e => e.Id),
+ filter);
- var firstIds = firstIdsQuery.ToList();
-
- // Single bound JSON / array parameter via WhereOneOrMany — keeps SQL small regardless of N.
- var itemsQuery = context.BaseItems.AsNoTracking().WhereOneOrMany(firstIds, e => e.Id);
- itemsQuery = ApplyNavigations(itemsQuery, filter);
-
- return itemsQuery
- .OrderByDescending(e => e.DateCreated)
- .ThenByDescending(e => e.Id)
+ var itemsById = itemsQuery
.AsEnumerable()
.Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
.Where(dto => dto != null)
- .ToArray()!;
+ .ToDictionary(i => i!.Id);
+
+ return firstIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!;
}
/// <summary>