aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Dto
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/Dto')
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs285
1 files changed, 232 insertions, 53 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 3cd72a8ac1..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;
@@ -71,6 +72,8 @@ namespace Emby.Server.Implementations.Dto
{
BaseItemKind.Person, [
BaseItemKind.Audio,
+ BaseItemKind.AudioBook,
+ BaseItemKind.Book,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
@@ -167,9 +170,13 @@ namespace Emby.Server.Implementations.Dto
// Batch-fetch user data for all items
Dictionary<Guid, UserItemData>? userDataBatch = null;
+ IReadOnlyDictionary<Guid, VersionResumeData>? resumeDataBatch = null;
if (user is not null && options.EnableUserData)
{
userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
+
+ // For items with alternate versions, the most recently played version drives resume.
+ resumeDataBatch = _userDataRepository.GetResumeUserDataBatch(accessibleItems, user);
}
// Pre-compute collection folders once to avoid N+1 queries in CanDelete
@@ -179,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
@@ -236,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];
@@ -248,7 +281,10 @@ namespace Emby.Server.Implementations.Dto
allCollectionFolders,
childCountBatch,
playedCountBatch,
- artistsBatch);
+ artistsBatch,
+ resumeDataBatch?.GetValueOrDefault(item.Id),
+ peopleBatch,
+ alternateVersionItemIds);
if (item is LiveTvChannel tvChannel)
{
@@ -261,7 +297,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ItemCounts))
{
- SetItemByNameInfo(dto, user);
+ SetItemByNameInfo(dto, user, itemCountsBatch);
}
returnItems[index] = dto;
@@ -309,7 +345,10 @@ namespace Emby.Server.Implementations.Dto
List<Folder>? allCollectionFolders = null,
Dictionary<Guid, int>? childCountBatch = null,
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
- IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
+ IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null,
+ VersionResumeData? resumeData = null,
+ IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null,
+ IReadOnlySet<Guid>? alternateVersionItemIds = null)
{
var dto = new BaseItemDto
{
@@ -323,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))
@@ -353,7 +400,8 @@ namespace Emby.Server.Implementations.Dto
options,
userData,
childCountBatch,
- playedCountBatch);
+ playedCountBatch,
+ resumeData);
}
if (item is IHasMediaSources
@@ -369,7 +417,7 @@ namespace Emby.Server.Implementations.Dto
AttachStudios(dto, item);
}
- AttachBasicFields(dto, item, owner, options, artistsBatch);
+ AttachBasicFields(dto, item, owner, options, artistsBatch, user, alternateVersionItemIds);
if (options.ContainsField(ItemFields.CanDelete))
{
@@ -474,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;
@@ -538,7 +608,8 @@ namespace Emby.Server.Implementations.Dto
DtoOptions options,
UserItemData? userData = null,
Dictionary<Guid, int>? childCountBatch = null,
- Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
+ Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
+ VersionResumeData? resumeData = null)
{
if (item.IsFolder)
{
@@ -566,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)
@@ -600,6 +675,9 @@ namespace Emby.Server.Implementations.Dto
// Use pre-fetched user data
dto.UserData = GetUserItemDataDto(userData, item.Id);
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
+
+ // For items with alternate versions, the most recently played version drives resume.
+ resumeData?.ApplyTo(dto.UserData);
}
else
{
@@ -633,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)
+ {
+ Dictionary<Guid, IReadOnlyList<Guid>>? sources = null;
+ foreach (var folder in items.OfType<Folder>())
+ {
+ 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)
{
- // 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)
+ 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 Random.Shared.Next(1, 10);
+ 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)
@@ -729,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))
@@ -943,7 +1106,9 @@ namespace Emby.Server.Implementations.Dto
/// <param name="owner">The owner.</param>
/// <param name="options">The options.</param>
/// <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)
+ /// <param name="user">The user, for per-user values such as the accessible media source count.</param>
+ /// <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))
{
@@ -1074,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))
{
@@ -1257,10 +1422,27 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.MediaSourceCount))
{
- var mediaSourceCount = video.MediaSourceCount;
- 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;
+ }
}
}
@@ -1366,38 +1548,22 @@ namespace Emby.Server.Implementations.Dto
}
}
- if (options.PreferEpisodeParentPoster)
+ if (options.GetImageLimit(ImageType.Primary) > 0)
{
var episodeSeason = episode.Season;
var seasonPrimaryTag = episodeSeason is not null
? GetTagAndFillBlurhash(dto, episodeSeason, ImageType.Primary)
: null;
- BaseItem? posterParent = null;
if (seasonPrimaryTag is not null)
{
dto.ParentPrimaryImageItemId = episodeSeason!.Id;
dto.ParentPrimaryImageTag = seasonPrimaryTag;
- posterParent = episodeSeason;
}
else if (episodeSeries is not null && dto.SeriesPrimaryImageTag is not null)
{
dto.ParentPrimaryImageItemId = episodeSeries.Id;
dto.ParentPrimaryImageTag = dto.SeriesPrimaryImageTag;
- posterParent = episodeSeries;
- }
-
- if (posterParent is not null)
- {
- if (dto.ImageTags is not null && dto.ImageTags.Remove(ImageType.Primary, out var ownPrimaryTag))
- {
- // Only drop the episode's own primary blurhash; keep the poster parent's.
- dto.ImageBlurHashes?.GetValueOrDefault(ImageType.Primary)?.Remove(ownPrimaryTag);
- }
-
- dto.SeriesPrimaryImageTag = null;
- dto.PrimaryImageAspectRatio = null;
- AttachPrimaryImageAspectRatio(dto, posterParent);
}
}
@@ -1516,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;
@@ -1537,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
@@ -1582,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;
@@ -1641,7 +1820,7 @@ namespace Emby.Server.Implementations.Dto
break;
}
- parent = GetImageDisplayParent(parent, item);
+ parent = GetImageDisplayParent(parent, item, artistsBatch);
}
}