aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
authorbrandon <brandon@clinger.dev>2026-08-07 11:59:17 -0400
committerbrandon <brandon@clinger.dev>2026-08-07 11:59:17 -0400
commitd6da6906a4b3426f05c5bf4ffe8bab4b2bc78ce8 (patch)
tree78683665d1792916de8683b6ac5bc13e17904d87 /Emby.Server.Implementations
parent6c37a6ef8b4ce027e7ac2aaa827244711cf5f39c (diff)
Batch people lookups when building item DTOs
GetBaseItemDtos already batch fetches user data, child counts, played counts and artists before its per item loop, but AttachPeople still ran one GetPeople query per item. Rendering a page of items (for example a large playlist) fired one extra query per row. Add GetPeopleByItems to IPeopleRepository, which reads every requested item in a single query over the people mapping table and returns full PersonInfo (role, type and sort order) grouped by item id. GetBaseItemDtos prefetches this once when the People field is requested and passes it into AttachPeople, which reads from the batch instead of querying per item. The single item GetBaseItemDto path keeps its existing per item behaviour when no batch is supplied. Adds a DtoService test asserting people resolve from the batch and the per item GetPeople is never called.
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs37
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs6
2 files changed, 38 insertions, 5 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 71c3b24907..da0c52df5b 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -242,6 +242,17 @@ namespace Emby.Server.Implementations.Dto
artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
}
+ // Batch-fetch people across all items to avoid one GetPeople query per item.
+ IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null;
+ if (options.ContainsField(ItemFields.People))
+ {
+ var peopleItemIds = accessibleItems.Where(i => i.SupportsPeople).Select(i => i.Id).ToList();
+ if (peopleItemIds.Count > 0)
+ {
+ peopleBatch = _libraryManager.GetPeopleByItems(peopleItemIds);
+ }
+ }
+
for (int index = 0; index < accessibleItems.Count; index++)
{
var item = accessibleItems[index];
@@ -255,7 +266,8 @@ namespace Emby.Server.Implementations.Dto
childCountBatch,
playedCountBatch,
artistsBatch,
- resumeDataBatch?.GetValueOrDefault(item.Id));
+ resumeDataBatch?.GetValueOrDefault(item.Id),
+ peopleBatch);
if (item is LiveTvChannel tvChannel)
{
@@ -317,7 +329,8 @@ namespace Emby.Server.Implementations.Dto
Dictionary<Guid, int>? childCountBatch = null,
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null,
- VersionResumeData? resumeData = null)
+ VersionResumeData? resumeData = null,
+ IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null)
{
var dto = new BaseItemDto
{
@@ -331,7 +344,15 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.People))
{
- AttachPeople(dto, item, user);
+ IReadOnlyList<PersonInfo>? prefetchedPeople = null;
+ if (peopleBatch is not null)
+ {
+ // The batch omits items with no people, so a miss means "no people",
+ // not "not fetched". Use an empty list to skip the per-item query.
+ prefetchedPeople = peopleBatch.GetValueOrDefault(item.Id) ?? [];
+ }
+
+ AttachPeople(dto, item, user, prefetchedPeople);
}
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
@@ -742,12 +763,18 @@ namespace Emby.Server.Implementations.Dto
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <param name="user">The requesting user.</param>
- private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null)
+ /// <param name="prefetchedPeople">People fetched in batch by the caller; when null the people are queried per item.</param>
+ private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null, IReadOnlyList<PersonInfo>? prefetchedPeople = null)
{
+ // When rendering a page of items the caller batch-fetches people for every item up
+ // front and passes them in, avoiding one GetPeople query per item. Fall back to the
+ // per-item query for the single item path where no batch is available.
+ var source = prefetchedPeople ?? _libraryManager.GetPeople(item);
+
// Ordering by person type to ensure actors and artists are at the front.
// This is taking advantage of the fact that they both begin with A
// This should be improved in the future
- var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
+ var people = source.OrderBy(i => i.SortOrder ?? int.MaxValue)
.ThenBy(i =>
{
if (i.IsType(PersonKind.Actor))
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 5db3b80386..19371f68d7 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -3537,6 +3537,12 @@ namespace Emby.Server.Implementations.Library
return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes);
}
+ /// <inheritdoc/>
+ public IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds)
+ {
+ return _peopleRepository.GetPeopleByItems(itemIds);
+ }
+
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
{
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();