aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Dto/DtoService.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/Dto/DtoService.cs')
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs254
1 files changed, 215 insertions, 39 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 8cbf42585d..e539508644 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
@@ -185,15 +186,18 @@ namespace Emby.Server.Implementations.Dto
allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
}
- // Batch-fetch child counts for all folders to avoid N+1 queries
+ // Batch-fetch by-name item counts to avoid N+1 queries
+ Dictionary<Guid, ItemCounts>? itemCountsBatch = null;
+ if (options.ContainsField(ItemFields.ItemCounts))
+ {
+ itemCountsBatch = GetItemCountsBatch(accessibleItems, user);
+ }
+
+ // Batch-fetch child counts for all folders to avoid N+1 queries.
Dictionary<Guid, int>? childCountBatch = null;
- if (options.ContainsField(ItemFields.ChildCount))
+ if (user is not null && options.ContainsField(ItemFields.ChildCount))
{
- var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
- if (folderIds.Count > 0)
- {
- childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
- }
+ childCountBatch = GetChildCountBatch(accessibleItems, user);
}
// Batch-fetch played/total counts for all folders to avoid N+1 queries
@@ -242,6 +246,29 @@ 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);
+ }
+ }
+
+ // Batch-detect which videos own alternate versions to avoid the per-item alternate-version
+ // queries in MediaSourceCount. Videos absent from this set have a single media source.
+ IReadOnlySet<Guid>? alternateVersionItemIds = null;
+ if (options.ContainsField(ItemFields.MediaSourceCount))
+ {
+ var versionItemIds = accessibleItems.OfType<Video>().Select(i => i.Id).ToList();
+ if (versionItemIds.Count > 0)
+ {
+ alternateVersionItemIds = _libraryManager.GetItemIdsWithAlternateVersions(versionItemIds);
+ }
+ }
+
for (int index = 0; index < accessibleItems.Count; index++)
{
var item = accessibleItems[index];
@@ -255,7 +282,9 @@ namespace Emby.Server.Implementations.Dto
childCountBatch,
playedCountBatch,
artistsBatch,
- resumeDataBatch?.GetValueOrDefault(item.Id));
+ resumeDataBatch?.GetValueOrDefault(item.Id),
+ peopleBatch,
+ alternateVersionItemIds);
if (item is LiveTvChannel tvChannel)
{
@@ -268,7 +297,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ItemCounts))
{
- SetItemByNameInfo(dto, user);
+ SetItemByNameInfo(dto, user, itemCountsBatch);
}
returnItems[index] = dto;
@@ -317,7 +346,9 @@ 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,
+ IReadOnlySet<Guid>? alternateVersionItemIds = null)
{
var dto = new BaseItemDto
{
@@ -331,7 +362,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))
@@ -378,7 +417,7 @@ namespace Emby.Server.Implementations.Dto
AttachStudios(dto, item);
}
- AttachBasicFields(dto, item, owner, options, artistsBatch, user);
+ AttachBasicFields(dto, item, owner, options, artistsBatch, user, alternateVersionItemIds);
if (options.ContainsField(ItemFields.CanDelete))
{
@@ -483,14 +522,36 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
- private void SetItemByNameInfo(BaseItemDto dto, User? user)
+ private Dictionary<Guid, ItemCounts> GetItemCountsBatch(IReadOnlyList<BaseItem> items, User? user)
+ {
+ var result = new Dictionary<Guid, ItemCounts>();
+
+ foreach (var group in items.GroupBy(item => item.GetBaseItemKind()))
+ {
+ if (!_relatedItemKinds.TryGetValue(group.Key, out var relatedItemKinds))
+ {
+ continue;
+ }
+
+ var ids = group.Select(item => item.Id).ToArray();
+ foreach (var (id, counts) in _libraryManager.GetItemCountsForNameItems(group.Key, ids, relatedItemKinds, user))
+ {
+ result[id] = counts;
+ }
+ }
+
+ return result;
+ }
+
+ private void SetItemByNameInfo(BaseItemDto dto, User? user, IReadOnlyDictionary<Guid, ItemCounts>? prefetchedCounts = null)
{
if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
{
return;
}
- var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
+ var counts = prefetchedCounts?.GetValueOrDefault(dto.Id)
+ ?? _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
dto.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount;
@@ -576,7 +637,11 @@ namespace Emby.Server.Implementations.Dto
// For these types we can try to optimize and assume these values will be equal
if (item is MusicAlbum || item is Season || item is Playlist)
{
- dto.ChildCount = dto.RecursiveItemCount;
+ if (dto.RecursiveItemCount > 0)
+ {
+ dto.ChildCount = dto.RecursiveItemCount;
+ }
+
var folderChildCount = folder.LinkedChildren.Length;
// The default is an empty array, so we can't reliably use the count when it's empty
if (folderChildCount > 0)
@@ -646,23 +711,102 @@ namespace Emby.Server.Implementations.Dto
};
}
- private static int GetChildCount(Folder folder, User user, Dictionary<Guid, int>? childCountBatch)
+ private Dictionary<Guid, int>? GetChildCountBatch(IReadOnlyList<BaseItem> items, User user)
{
- // Right now this is too slow to calculate for top level folders on a per-user basis
- // Just return something so that apps that are expecting a value won't think the folders are empty
- if (folder is ICollectionFolder || folder is UserView)
+ Dictionary<Guid, IReadOnlyList<Guid>>? sources = null;
+ foreach (var folder in items.OfType<Folder>())
{
- return Random.Shared.Next(1, 10);
+ var sourceIds = GetChildCountSourceIds(folder, user);
+ if (sourceIds.Count > 0)
+ {
+ (sources ??= new Dictionary<Guid, IReadOnlyList<Guid>>())[folder.Id] = sourceIds;
+ }
}
+ if (sources is null)
+ {
+ return null;
+ }
+
+ var counts = _libraryManager.GetChildCountBatch(
+ sources.Values.SelectMany(ids => ids).Distinct().ToList(),
+ user);
+
+ var result = new Dictionary<Guid, int>(sources.Count);
+ foreach (var (folderId, sourceIds) in sources)
+ {
+ var total = 0;
+ foreach (var sourceId in sourceIds)
+ {
+ total += counts.GetValueOrDefault(sourceId);
+ }
+
+ result[folderId] = total;
+ }
+
+ return result;
+ }
+
+ private IReadOnlyList<Guid> GetChildCountSourceIds(Folder folder, User user)
+ {
+ if (folder is CollectionFolder collectionFolder)
+ {
+ return collectionFolder.PhysicalFolderIds;
+ }
+
+ if (folder is not UserView view)
+ {
+ return [folder.Id];
+ }
+
+ // Only a view that stands for a library proxies it. The sub-views a movie or show view
+ // is built from hang off the same library but hold a query, not the library's children.
+ if (!UserView.EnableOriginalFolder(view.ViewType)
+ && view.ViewType is not (CollectionType.movies or CollectionType.tvshows))
+ {
+ return [];
+ }
+
+ // A view over a single library proxies that library, whatever the view type.
+ var parentId = view.DisplayParentId.IsEmpty() ? view.ParentId : view.DisplayParentId;
+ if (!parentId.IsEmpty()
+ && !parentId.Equals(view.Id)
+ && _libraryManager.GetItemById(parentId) is Folder parent
+ && parent is not UserView)
+ {
+ return GetChildCountSourceIds(parent, user);
+ }
+
+ // A grouped view has no single parent: it stands for every library the user grouped
+ // into it, the same set UserViewManager builds the view from.
+ if (view.ViewType is CollectionType.movies or CollectionType.tvshows)
+ {
+ return _libraryManager.GetUserRootFolder()
+ .GetChildren(user, true)
+ .OfType<CollectionFolder>()
+ .Where(f => user.IsFolderGrouped(f.Id)
+ && (f.CollectionType == view.ViewType || f.CollectionType is null))
+ .SelectMany(f => f.PhysicalFolderIds)
+ .Distinct()
+ .ToList();
+ }
+
+ return [];
+ }
+
+ private int GetChildCount(Folder folder, User user, Dictionary<Guid, int>? childCountBatch)
+ {
// Use pre-fetched batch data if available
if (childCountBatch is not null && childCountBatch.TryGetValue(folder.Id, out var count))
{
return count;
}
- // Fall back to individual query for special cases (Series, Season, etc.)
- return folder.GetChildCount(user);
+ // No batch covered this folder.
+ var single = GetChildCountBatch([folder], user);
+ return single is not null && single.TryGetValue(folder.Id, out var singleCount)
+ ? singleCount
+ : folder.GetChildCount(user);
}
private static void SetBookProperties(BaseItemDto dto, Book item)
@@ -742,12 +886,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))
@@ -957,7 +1107,8 @@ namespace Emby.Server.Implementations.Dto
/// <param name="options">The options.</param>
/// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
/// <param name="user">The user, for per-user values such as the accessible media source count.</param>
- private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null, User? user = null)
+ /// <param name="alternateVersionItemIds">Optional pre-fetched set of item IDs that own alternate versions, shared across a batch of items.</param>
+ private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null, User? user = null, IReadOnlySet<Guid>? alternateVersionItemIds = null)
{
if (options.ContainsField(ItemFields.DateCreated))
{
@@ -1088,7 +1239,7 @@ namespace Emby.Server.Implementations.Dto
dto.ParentId = item.DisplayParentId;
}
- AddInheritedImages(dto, item, options, owner);
+ AddInheritedImages(dto, item, options, owner, artistsBatch);
if (options.ContainsField(ItemFields.Path))
{
@@ -1271,15 +1422,27 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.MediaSourceCount))
{
- // Match the per-user filtering of the media sources: versions the user cannot
- // access are not selectable, so they must not count towards the badge either.
- var mediaSourceCount = user is null
- || (!video.PrimaryVersionId.HasValue && video.LinkedAlternateVersions.Length == 0 && !video.HasLocalAlternateVersions)
- ? video.MediaSourceCount
- : video.GetAllVersions().Count(v => v.Id.Equals(video.Id) || v.IsVisibleStandalone(user));
- if (mediaSourceCount != 1)
+ // A video with no primary version and no alternate versions always has a single
+ // media source. Only compute the count for videos that might have more: a primary
+ // version, or membership in the batch's set of items that own alternate versions.
+ // Without the batch we can't rule it out, so fall back to computing (the single-item
+ // path). Everything else is the common case and keeps the default count of one.
+ var mayHaveAlternateVersions = alternateVersionItemIds is null
+ || video.PrimaryVersionId.HasValue
+ || alternateVersionItemIds.Contains(video.Id);
+
+ if (mayHaveAlternateVersions)
{
- dto.MediaSourceCount = mediaSourceCount;
+ // Match the per-user filtering of the media sources: versions the user cannot
+ // access are not selectable, so they must not count towards the badge either.
+ var mediaSourceCount = user is null
+ || (!video.PrimaryVersionId.HasValue && video.LinkedAlternateVersions.Length == 0 && !video.HasLocalAlternateVersions)
+ ? video.MediaSourceCount
+ : video.GetAllVersions().Count(v => v.Id.Equals(video.Id) || v.IsVisibleStandalone(user));
+ if (mediaSourceCount != 1)
+ {
+ dto.MediaSourceCount = mediaSourceCount;
+ }
}
}
@@ -1519,11 +1682,11 @@ namespace Emby.Server.Implementations.Dto
}
}
- private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
+ private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch)
{
if (currentItem is MusicAlbum musicAlbum)
{
- var artist = musicAlbum.GetMusicArtist(new DtoOptions(false));
+ var artist = GetBatchedAlbumArtist(musicAlbum, artistsBatch) ?? musicAlbum.GetMusicArtist(new DtoOptions(false));
if (artist is not null)
{
return artist;
@@ -1540,7 +1703,20 @@ namespace Emby.Server.Implementations.Dto
return parent;
}
- private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner)
+ private static MusicArtist? GetBatchedAlbumArtist(MusicAlbum album, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch)
+ {
+ if (artistsBatch is null)
+ {
+ return null;
+ }
+
+ var name = album.AlbumArtists.Count > 0 ? album.AlbumArtists[0] : null;
+ return !string.IsNullOrEmpty(name) && artistsBatch.TryGetValue(name, out var artists) && artists.Length > 0
+ ? artists[0]
+ : null;
+ }
+
+ private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch)
{
if (item is UserView { ViewType: CollectionType.playlists } playlistsView
&& options.GetImageLimit(ImageType.Primary) > 0
@@ -1585,7 +1761,7 @@ namespace Emby.Server.Implementations.Dto
|| (!(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
|| parent is Series)
{
- parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
+ parent ??= isFirst ? GetImageDisplayParent(item, item, artistsBatch) ?? owner : parent;
if (parent is null)
{
break;
@@ -1644,7 +1820,7 @@ namespace Emby.Server.Implementations.Dto
break;
}
- parent = GetImageDisplayParent(parent, item);
+ parent = GetImageDisplayParent(parent, item, artistsBatch);
}
}