aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNiels van Velzen <nielsvanvelzen@users.noreply.github.com>2026-05-05 15:49:02 +0200
committerGitHub <noreply@github.com>2026-05-05 15:49:02 +0200
commit064fd8c5c0946ccecc686606528faa8f3b47dc96 (patch)
treec76128e120cf4357a794a1538a2943832940a73b
parent7be1350205ec79ab818d2d047a200d26d3a473e0 (diff)
parentd4f91ab5cac87e087657b825def6bb30841d2963 (diff)
Merge pull request #16756 from Shadowghost/artist-speedup
Speed-up LatestItems for Music
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs50
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs73
2 files changed, 84 insertions, 39 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index cc57d183b6..94e2468719 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -203,6 +203,39 @@ namespace Emby.Server.Implementations.Dto
}
}
+ // Batch-fetch MusicArtist lookups across all items to avoid N+1 queries.
+ IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null;
+ var artistNames = new HashSet<string>(StringComparer.Ordinal);
+ foreach (var item in accessibleItems)
+ {
+ if (item is IHasArtist hasArtist)
+ {
+ foreach (var name in hasArtist.Artists)
+ {
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ artistNames.Add(name);
+ }
+ }
+ }
+
+ if (item is IHasAlbumArtist hasAlbumArtist)
+ {
+ foreach (var name in hasAlbumArtist.AlbumArtists)
+ {
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ artistNames.Add(name);
+ }
+ }
+ }
+ }
+
+ if (artistNames.Count > 0)
+ {
+ artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
+ }
+
for (int index = 0; index < accessibleItems.Count; index++)
{
var item = accessibleItems[index];
@@ -214,7 +247,8 @@ namespace Emby.Server.Implementations.Dto
userDataBatch?.GetValueOrDefault(item.Id),
allCollectionFolders,
childCountBatch,
- playedCountBatch);
+ playedCountBatch,
+ artistsBatch);
if (item is LiveTvChannel tvChannel)
{
@@ -274,7 +308,8 @@ namespace Emby.Server.Implementations.Dto
UserItemData? userData = null,
List<Folder>? allCollectionFolders = null,
Dictionary<Guid, int>? childCountBatch = null,
- Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
+ Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
+ IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
{
var dto = new BaseItemDto
{
@@ -334,7 +369,7 @@ namespace Emby.Server.Implementations.Dto
AttachStudios(dto, item);
}
- AttachBasicFields(dto, item, owner, options);
+ AttachBasicFields(dto, item, owner, options, artistsBatch);
if (options.ContainsField(ItemFields.CanDelete))
{
@@ -907,7 +942,8 @@ namespace Emby.Server.Implementations.Dto
/// <param name="item">The item.</param>
/// <param name="owner">The owner.</param>
/// <param name="options">The options.</param>
- private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options)
+ /// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
+ private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
{
if (options.ContainsField(ItemFields.DateCreated))
{
@@ -1152,7 +1188,8 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
- var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
+ var artistsLookup = artistsBatch
+ ?? _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
dto.ArtistItems = hasArtist.Artists
.Where(name => !string.IsNullOrWhiteSpace(name))
@@ -1186,7 +1223,8 @@ namespace Emby.Server.Implementations.Dto
// })
// .ToList();
- var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
+ var albumArtistsLookup = artistsBatch
+ ?? _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
.Where(name => !string.IsNullOrWhiteSpace(name))
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs
index d516bc0d13..dc16c3b1b3 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,45 +124,53 @@ 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;
-
if (collectionType is CollectionType.movies)
{
- groupKeyFilter = e => e.PresentationUniqueKey != null;
- groupKeySelector = e => e.PresentationUniqueKey;
- }
- else
- {
- groupKeyFilter = e => e.Album != null;
- groupKeySelector = e => e.Album;
+ // Group by PresentationUniqueKey, pick the newest item per group.
+ 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 firstIdsQuery = filter.Limit.HasValue
+ ? topGroupItems.Take(filter.Limit.Value).Select(g => g.FirstId)
+ : topGroupItems.Select(g => g.FirstId);
+
+ return LoadLatestByIds(context, firstIdsQuery, filter);
}
- // 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);
+ // Albums whose Id is the parent of any track matching the user's filter.
+ var albumIdsWithMatchingTrack = context.AncestorIds
+ .Join(baseQuery, ai => ai.ItemId, t => t.Id, (ai, _) => ai.ParentItemId);
- var firstIdsQuery = filter.Limit.HasValue
- ? topGroupItems.Take(filter.Limit.Value).Select(g => g.FirstId)
- : topGroupItems.Select(g => g.FirstId);
+ var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]!;
+ var topAlbumsQuery = context.BaseItems.AsNoTracking()
+ .Where(album => album.Type == musicAlbumTypeName)
+ .Where(album => albumIdsWithMatchingTrack.Contains(album.Id))
+ .OrderByDescending(album => album.DateCreated)
+ .ThenByDescending(album => album.Id);
- var firstIds = firstIdsQuery.ToList();
+ var albumIdsQuery = filter.Limit.HasValue
+ ? topAlbumsQuery.Take(filter.Limit.Value).Select(a => a.Id)
+ : topAlbumsQuery.Select(a => a.Id);
- // 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 LoadLatestByIds(context, albumIdsQuery, filter);
+ }
+
+ // Keeping idsQuery deferred lets EF emit `WHERE Id IN (<subquery>)`.
+ private IReadOnlyList<BaseItemDto> LoadLatestByIds(
+ JellyfinDbContext context,
+ IQueryable<Guid> idsQuery,
+ InternalItemsQuery filter)
+ {
+ var itemsQuery = ApplyNavigations(
+ context.BaseItems.AsNoTracking().Where(e => idsQuery.Contains(e.Id)),
+ filter);
return itemsQuery
.OrderByDescending(e => e.DateCreated)