Fix GetBaseItemDto to return related item counts via SQL count

For API call /Items/{item id} GetBaseItemDto will return the counts of related items e.g. artists, albums, songs.  GetBaseItemDto currently does this by calling GetTaggedItems which retrieves the objects into memory to count them.  Replace with SQL count.

Fixes:
This should be an improvement for any large libraries, but especially large music libraries.  Example:

Request Library -> Genres -> any very popular genre in your large library, e.g. Classical
Number of albums = 1552, songs = 23515, ...

- Before change: Try to retrieve 1552 albums, 23515 songs, ... in memory, API never returns, database on fire
- After change: API returns in 367ms and Genre view opens with 200 albums in 2 seconds

I verified the numbers returned are correct but note that there is a bug somewhere else in Jellyfin that is setting TopParentId to NULL for a large portion of my MusicArtists, which causes them to not be counted by the existing GetCount().  This is not related to this change, also happens with the existing code, and does not seem to affect the Web UI.

Includes Cory's changes in:
- https://github.com/jellyfin/jellyfin/pull/14610#issuecomment-3172211468
- https://github.com/jellyfin/jellyfin/pull/14610#issuecomment-3172239154
This commit is contained in:
Evan 2025-08-08 23:21:40 +08:00
parent 2b94b3b5f6
commit 0a4ff3f3c0
8 changed files with 247 additions and 38 deletions

View File

@ -102,21 +102,9 @@ namespace Emby.Server.Implementations.Dto
(programTuples ??= []).Add((item, dto)); (programTuples ??= []).Add((item, dto));
} }
if (item is IItemByName byName) if (item is IItemByName itemByName && options.ContainsField(ItemFields.ItemCounts))
{ {
if (options.ContainsField(ItemFields.ItemCounts)) SetItemByNameInfo(itemByName, dto, user);
{
var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = new DtoOptions(false)
{
EnableImages = false
}
});
SetItemByNameInfo(item, dto, libraryItems);
}
} }
returnItems[index] = dto; returnItems[index] = dto;
@ -147,34 +135,14 @@ namespace Emby.Server.Implementations.Dto
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult(); LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
} }
if (item is IItemByName itemByName if (item is IItemByName itemByName && options.ContainsField(ItemFields.ItemCounts))
&& options.ContainsField(ItemFields.ItemCounts))
{ {
SetItemByNameInfo( SetItemByNameInfo(itemByName, dto, user);
item,
dto,
GetTaggedItems(
itemByName,
user,
new DtoOptions(false)
{
EnableImages = false
}));
} }
return dto; return dto;
} }
private static IReadOnlyList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
{
return byName.GetTaggedItems(
new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = options
});
}
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null) private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
{ {
var dto = new BaseItemDto var dto = new BaseItemDto
@ -319,14 +287,43 @@ namespace Emby.Server.Implementations.Dto
{ {
var dto = GetBaseItemDtoInternal(item, options, user); var dto = GetBaseItemDtoInternal(item, options, user);
if (taggedItems is not null && options.ContainsField(ItemFields.ItemCounts)) if (options.ContainsField(ItemFields.ItemCounts))
{ {
SetItemByNameInfo(item, dto, taggedItems); if (taggedItems is not null)
{
SetItemByNameInfo(item, dto, taggedItems!);
}
else if (item is IItemByName itemByName)
{
SetItemByNameInfo(itemByName, dto, user);
}
} }
return dto; return dto;
} }
private static void SetItemByNameInfo(IItemByName item, BaseItemDto dto, User? user)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = new DtoOptions(false) { EnableImages = false }
};
var counts = item.GetTaggedItemCounts(query);
dto.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount;
dto.EpisodeCount = counts.EpisodeCount;
dto.MovieCount = counts.MovieCount;
dto.MusicVideoCount = counts.MusicVideoCount;
dto.ProgramCount = counts.ProgramCount;
dto.SeriesCount = counts.SeriesCount;
dto.SongCount = counts.SongCount;
dto.TrailerCount = counts.TrailerCount;
dto.ChildCount = counts.ChildCount;
}
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems) private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
{ {
if (item is MusicArtist) if (item is MusicArtist)

View File

@ -98,6 +98,24 @@ namespace MediaBrowser.Controller.Entities.Audio
return LibraryManager.GetItemList(query); return LibraryManager.GetItemList(query);
} }
public TaggedItemCounts GetTaggedItemCounts(InternalItemsQuery query)
{
query.ArtistIds = [Id];
var counts = new TaggedItemCounts();
query.IncludeItemTypes = [BaseItemKind.MusicAlbum];
counts.AlbumCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.MusicVideo];
counts.MusicVideoCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Audio];
counts.SongCount = LibraryManager.GetCount(query);
return counts;
}
public override int GetChildCount(User user) public override int GetChildCount(User user)
{ {
return IsAccessedByName ? 0 : base.GetChildCount(user); return IsAccessedByName ? 0 : base.GetChildCount(user);

View File

@ -73,6 +73,27 @@ namespace MediaBrowser.Controller.Entities.Audio
return LibraryManager.GetItemList(query); return LibraryManager.GetItemList(query);
} }
public TaggedItemCounts GetTaggedItemCounts(InternalItemsQuery query)
{
query.GenreIds = [Id];
var counts = new TaggedItemCounts();
query.IncludeItemTypes = [BaseItemKind.MusicAlbum];
counts.AlbumCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.MusicArtist];
counts.ArtistCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.MusicVideo];
counts.MusicVideoCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Audio];
counts.SongCount = LibraryManager.GetCount(query);
return counts;
}
public static string GetPath(string name) public static string GetPath(string name)
{ {
return GetPath(name, true); return GetPath(name, true);

View File

@ -76,6 +76,37 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemList(query); return LibraryManager.GetItemList(query);
} }
public TaggedItemCounts GetTaggedItemCounts(InternalItemsQuery query)
{
query.GenreIds = [Id];
query.ExcludeItemTypes =
[
BaseItemKind.MusicVideo,
BaseItemKind.Audio,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist
];
var counts = new TaggedItemCounts();
query.IncludeItemTypes = [BaseItemKind.Episode];
counts.EpisodeCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Movie];
counts.MovieCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.LiveTvProgram];
counts.ProgramCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Series];
counts.SeriesCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Trailer];
counts.TrailerCount = LibraryManager.GetCount(query);
return counts;
}
public static string GetPath(string name) public static string GetPath(string name)
{ {
return GetPath(name, true); return GetPath(name, true);

View File

@ -10,10 +10,35 @@ namespace MediaBrowser.Controller.Entities
public interface IItemByName public interface IItemByName
{ {
IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query); IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query);
TaggedItemCounts GetTaggedItemCounts(InternalItemsQuery query);
} }
public interface IHasDualAccess : IItemByName public interface IHasDualAccess : IItemByName
{ {
bool IsAccessedByName { get; } bool IsAccessedByName { get; }
} }
public class TaggedItemCounts
{
public int? AlbumCount { get; set; }
public int? ArtistCount { get; set; }
public int? EpisodeCount { get; set; }
public int? MovieCount { get; set; }
public int? MusicVideoCount { get; set; }
public int? ProgramCount { get; set; }
public int? SeriesCount { get; set; }
public int? SongCount { get; set; }
public int? TrailerCount { get; set; }
public int ChildCount => (AlbumCount ?? 0) + (ArtistCount ?? 0) + (EpisodeCount ?? 0) + (MovieCount ?? 0) + (MusicVideoCount ?? 0) + (ProgramCount ?? 0) + (SeriesCount ?? 0) + (SongCount ?? 0) + (TrailerCount ?? 0);
}
} }

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -70,6 +71,43 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemList(query); return LibraryManager.GetItemList(query);
} }
public TaggedItemCounts GetTaggedItemCounts(InternalItemsQuery query)
{
query.PersonIds = [Id];
var counts = new TaggedItemCounts();
// TODO: Remove MusicAlbum and MusicArtist when the relationship between Persons and Music is removed
query.IncludeItemTypes = [BaseItemKind.MusicAlbum];
counts.AlbumCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.MusicArtist];
counts.ArtistCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Episode];
counts.EpisodeCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Movie];
counts.MovieCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.MusicVideo];
counts.MusicVideoCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.LiveTvProgram];
counts.ProgramCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Series];
counts.SeriesCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Audio];
counts.SongCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Trailer];
counts.TrailerCount = LibraryManager.GetCount(query);
return counts;
}
public override bool CanDelete() public override bool CanDelete()
{ {
return false; return false;

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -71,6 +72,42 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemList(query); return LibraryManager.GetItemList(query);
} }
public TaggedItemCounts GetTaggedItemCounts(InternalItemsQuery query)
{
query.StudioIds = [Id];
var counts = new TaggedItemCounts();
query.IncludeItemTypes = [BaseItemKind.MusicAlbum];
counts.AlbumCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.MusicArtist];
counts.ArtistCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Episode];
counts.EpisodeCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Movie];
counts.MovieCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.MusicVideo];
counts.MusicVideoCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.LiveTvProgram];
counts.ProgramCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Series];
counts.SeriesCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Audio];
counts.SongCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Trailer];
counts.TrailerCount = LibraryManager.GetCount(query);
return counts;
}
public static string GetPath(string name) public static string GetPath(string name)
{ {
return GetPath(name, true); return GetPath(name, true);

View File

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities namespace MediaBrowser.Controller.Entities
@ -68,6 +69,47 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemList(query); return LibraryManager.GetItemList(query);
} }
public TaggedItemCounts GetTaggedItemCounts(InternalItemsQuery query)
{
if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
return new TaggedItemCounts();
}
query.Years = [year];
var counts = new TaggedItemCounts();
query.IncludeItemTypes = [BaseItemKind.MusicAlbum];
counts.AlbumCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.MusicArtist];
counts.ArtistCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Episode];
counts.EpisodeCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Movie];
counts.MovieCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.MusicVideo];
counts.MusicVideoCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.LiveTvProgram];
counts.ProgramCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Series];
counts.SeriesCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Audio];
counts.SongCount = LibraryManager.GetCount(query);
query.IncludeItemTypes = [BaseItemKind.Trailer];
counts.TrailerCount = LibraryManager.GetCount(query);
return counts;
}
public int? GetYearValue() public int? GetYearValue()
{ {
if (int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) if (int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))