Optimize item count calculation for named items

This commit is contained in:
Shadowghost
2026-02-18 20:28:42 +01:00
parent f96c399e62
commit 3a090a5716
6 changed files with 171 additions and 33 deletions

View File

@@ -418,37 +418,7 @@ namespace Emby.Server.Implementations.Dto
return;
}
var query = new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = new DtoOptions(false) { EnableImages = false },
IncludeItemTypes = relatedItemKinds
};
switch (dto.Type)
{
case BaseItemKind.Genre:
case BaseItemKind.MusicGenre:
query.GenreIds = [dto.Id];
break;
case BaseItemKind.MusicArtist:
query.ArtistIds = [dto.Id];
break;
case BaseItemKind.Person:
query.PersonIds = [dto.Id];
break;
case BaseItemKind.Studio:
query.StudioIds = [dto.Id];
break;
case BaseItemKind.Year
when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
query.Years = [year];
break;
default:
return;
}
var counts = _libraryManager.GetItemCounts(query);
var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
dto.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount;

View File

@@ -1561,6 +1561,18 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItemCounts(query);
}
/// <inheritdoc/>
public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? user)
{
var query = new InternalItemsQuery(user);
if (user is not null)
{
AddUserToQuery(query, user);
}
return _itemRepository.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
}
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
{
return _itemRepository.GetChildCountBatch(parentIds, userId);

View File

@@ -647,13 +647,13 @@ public class UserLibraryController : BaseJellyfinApiController
var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
if (!hasMetadata)
if (performFullRefresh)
{
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
ForceSave = performFullRefresh
ForceSave = true
};
await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);

View File

@@ -1343,6 +1343,141 @@ public sealed class BaseItemRepository
return result;
}
/// <inheritdoc />
public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, InternalItemsQuery accessFilter)
{
using var context = _dbProvider.CreateDbContext();
// Look up the item's name/cleanname
var item = context.BaseItems.AsNoTracking()
.Where(e => e.Id == id)
.Select(e => new { e.Name, e.CleanName })
.FirstOrDefault();
if (item is null)
{
return new ItemCounts();
}
// Build the base query starting from the mapping table (flipped join order)
IQueryable<BaseItemEntity> baseQuery;
switch (kind)
{
case BaseItemKind.Person:
baseQuery = context.PeopleBaseItemMap
.AsNoTracking()
.Where(m => m.People.Name == item.Name)
.Select(m => m.Item);
break;
case BaseItemKind.MusicArtist:
baseQuery = context.ItemValuesMap
.AsNoTracking()
.Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
&& (ivm.ItemValue.Type == ItemValueType.Artist || ivm.ItemValue.Type == ItemValueType.AlbumArtist))
.Select(ivm => ivm.Item);
break;
case BaseItemKind.Genre:
case BaseItemKind.MusicGenre:
baseQuery = context.ItemValuesMap
.AsNoTracking()
.Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
&& ivm.ItemValue.Type == ItemValueType.Genre)
.Select(ivm => ivm.Item);
break;
case BaseItemKind.Studio:
baseQuery = context.ItemValuesMap
.AsNoTracking()
.Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
&& ivm.ItemValue.Type == ItemValueType.Studios)
.Select(ivm => ivm.Item);
break;
case BaseItemKind.Year:
if (int.TryParse(item.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
baseQuery = context.BaseItems
.AsNoTracking()
.Where(e => e.ProductionYear == year);
}
else
{
return new ItemCounts();
}
break;
default:
return new ItemCounts();
}
// Apply type filter
var typeNames = relatedItemKinds.Select(k => _itemTypeLookup.BaseItemKindNames[k]).ToArray();
baseQuery = baseQuery.Where(e => typeNames.Contains(e.Type));
// Apply access filtering (parental ratings, blocked/allowed tags, library access)
baseQuery = ApplyAccessFiltering(context, baseQuery, accessFilter);
// Group by type and count
var counts = baseQuery
.GroupBy(x => x.Type)
.Select(x => new { x.Key, Count = x.Count() })
.ToArray();
var lookup = _itemTypeLookup.BaseItemKindNames;
var result = new ItemCounts
{
ItemCount = counts.Sum(c => c.Count)
};
foreach (var count in counts)
{
if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
{
result.AlbumCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
{
result.ArtistCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
{
result.EpisodeCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
{
result.MovieCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
{
result.MusicVideoCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
{
result.ProgramCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
{
result.SeriesCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
{
result.SongCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
{
result.TrailerCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.BoxSet], StringComparison.Ordinal))
{
result.BoxSetCount = count.Count;
}
else if (string.Equals(count.Key, lookup[BaseItemKind.Book], StringComparison.Ordinal))
{
result.BookCount = count.Count;
}
}
return result;
}
#pragma warning disable CA1307 // Specify StringComparison for clarity
/// <summary>
/// Gets the type.

View File

@@ -706,6 +706,16 @@ namespace MediaBrowser.Controller.Library
ItemCounts GetItemCounts(InternalItemsQuery query);
/// <summary>
/// Gets item counts for a "by-name" item using an optimized query path.
/// </summary>
/// <param name="kind">The kind of the name item.</param>
/// <param name="id">The ID of the name item.</param>
/// <param name="relatedItemKinds">The item kinds to count.</param>
/// <param name="user">The user for access filtering.</param>
/// <returns>The item counts grouped by type.</returns>
ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? user);
/// <summary>
/// Batch-fetches child counts for multiple parent folders.
/// Returns the count of immediate children (non-recursive) for each parent.

View File

@@ -112,6 +112,17 @@ public interface IItemRepository
ItemCounts GetItemCounts(InternalItemsQuery filter);
/// <summary>
/// Gets item counts for a "by-name" item (Person, MusicArtist, Genre, MusicGenre, Studio, Year)
/// using an optimized query that starts from the mapping table instead of scanning all BaseItems.
/// </summary>
/// <param name="kind">The kind of the name item.</param>
/// <param name="id">The ID of the name item.</param>
/// <param name="relatedItemKinds">The item kinds to count.</param>
/// <param name="accessFilter">A pre-configured query with user access filtering settings.</param>
/// <returns>The item counts grouped by type.</returns>
ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, InternalItemsQuery accessFilter);
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter);
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter);