mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-19 22:20:33 +01:00
Optimize Collection Grouping, NextUp and Latest queries
This commit is contained in:
@@ -1693,7 +1693,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
|
||||
protected bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
|
||||
{
|
||||
var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
|
||||
var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
|
||||
@@ -2487,7 +2487,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return path;
|
||||
}
|
||||
|
||||
public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
|
||||
public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields, (int Played, int Total)? precomputedCounts = null)
|
||||
{
|
||||
if (RunTimeTicks.HasValue)
|
||||
{
|
||||
|
||||
@@ -730,36 +730,9 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
|
||||
{
|
||||
var user = query.User;
|
||||
|
||||
if (!query.ForceDirect && RequiresPostFiltering(query))
|
||||
{
|
||||
IEnumerable<BaseItem> items;
|
||||
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
|
||||
|
||||
var totalCount = 0;
|
||||
if (query.User is null)
|
||||
{
|
||||
items = GetRecursiveChildren(filter);
|
||||
totalCount = items.Count();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Save pagination params before clearing them to prevent pagination from happening
|
||||
// before sorting. PostFilterAndSort will apply pagination after sorting.
|
||||
var limit = query.Limit;
|
||||
var startIndex = query.StartIndex;
|
||||
query.Limit = null;
|
||||
query.StartIndex = null;
|
||||
|
||||
items = GetRecursiveChildren(user, query, out totalCount);
|
||||
|
||||
// Restore pagination params so PostFilterAndSort can apply them after sorting
|
||||
query.Limit = limit;
|
||||
query.StartIndex = startIndex;
|
||||
}
|
||||
|
||||
return PostFilterAndSort(items, query);
|
||||
query.CollapseBoxSetItems = true;
|
||||
}
|
||||
|
||||
if (this is not UserRootFolder
|
||||
@@ -1690,7 +1663,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return !IsPlayed(user, userItemData);
|
||||
}
|
||||
|
||||
public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
|
||||
public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields, (int Played, int Total)? precomputedCounts = null)
|
||||
{
|
||||
if (!SupportsUserDataFromChildren)
|
||||
{
|
||||
@@ -1699,22 +1672,28 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
if (SupportsPlayedStatus || (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount)))
|
||||
{
|
||||
// Create a minimal query with just the user - skip ConfigureUserAccess to avoid
|
||||
// expensive GetUserViews calls. Since we're counting descendants of a specific
|
||||
// item (this folder) that the user already has access to, TopParentIds filtering
|
||||
// is redundant. The parental rating filter is applied via query.User.
|
||||
var query = new InternalItemsQuery(user);
|
||||
|
||||
int playedCount;
|
||||
int totalCount;
|
||||
|
||||
if (LinkedChildren.Length > 0)
|
||||
if (precomputedCounts.HasValue && LinkedChildren.Length == 0)
|
||||
{
|
||||
(playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCountFromLinkedChildren(query, Id);
|
||||
// Use batch-fetched counts (avoids N+1 queries)
|
||||
(playedCount, totalCount) = precomputedCounts.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
(playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCount(query, Id);
|
||||
// Fall back to per-item query for LinkedChildren items (BoxSets, Playlists)
|
||||
// or when no batch data is available
|
||||
var query = new InternalItemsQuery(user);
|
||||
|
||||
if (LinkedChildren.Length > 0)
|
||||
{
|
||||
(playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCountFromLinkedChildren(query, Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
(playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCount(query, Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))
|
||||
|
||||
@@ -158,25 +158,25 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||
}
|
||||
|
||||
if (base.IsVisible(user, skipAllowedTagsCheck))
|
||||
if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
|
||||
{
|
||||
if (LinkedChildren.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var userLibraryFolderIds = GetLibraryFolderIds(user);
|
||||
var libraryFolderIds = LibraryFolderIds ?? GetLibraryFolderIds();
|
||||
|
||||
if (libraryFolderIds.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i));
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (LinkedChildren.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var userLibraryFolderIds = GetLibraryFolderIds(user);
|
||||
var libraryFolderIds = LibraryFolderIds ?? GetLibraryFolderIds();
|
||||
|
||||
if (libraryFolderIds.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i));
|
||||
}
|
||||
|
||||
public override bool IsVisibleStandalone(User user)
|
||||
|
||||
@@ -697,6 +697,15 @@ namespace MediaBrowser.Controller.Library
|
||||
/// <returns>Dictionary mapping parent ID to child count.</returns>
|
||||
Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId);
|
||||
|
||||
/// <summary>
|
||||
/// Batch-fetches played and total counts for multiple folder items.
|
||||
/// Avoids N+1 queries when building DTOs for lists of folder items.
|
||||
/// </summary>
|
||||
/// <param name="folderIds">The list of folder item IDs.</param>
|
||||
/// <param name="user">The user for access filtering and played status.</param>
|
||||
/// <returns>Dictionary mapping folder ID to (Played count, Total count).</returns>
|
||||
Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user);
|
||||
|
||||
/// <summary>
|
||||
/// Configures the query with user access settings including TopParentIds for library access.
|
||||
/// Call this before passing a query to methods that need user access filtering.
|
||||
|
||||
@@ -188,6 +188,16 @@ public interface IItemRepository
|
||||
/// <returns>A tuple containing (Played count, Total count).</returns>
|
||||
(int Played, int Total) GetPlayedAndTotalCountFromLinkedChildren(InternalItemsQuery filter, Guid parentId);
|
||||
|
||||
/// <summary>
|
||||
/// Batch-fetches played and total counts for multiple folder items using the AncestorIds table.
|
||||
/// This avoids N+1 queries when building DTOs for lists of folder items (Series, Seasons, etc.).
|
||||
/// Applies user access filtering (parental controls, tags).
|
||||
/// </summary>
|
||||
/// <param name="folderIds">The list of folder item IDs to get counts for.</param>
|
||||
/// <param name="user">The user for access filtering and played status.</param>
|
||||
/// <returns>Dictionary mapping folder ID to (Played count, Total count).</returns>
|
||||
Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IDs of linked children for the specified parent.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user