From 0a4ff3f3c0592cb32b7fb98bfc9f423386ecb84c Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 8 Aug 2025 23:21:40 +0800 Subject: [PATCH 1/3] 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 --- Emby.Server.Implementations/Dto/DtoService.cs | 73 +++++++++---------- .../Entities/Audio/MusicArtist.cs | 18 +++++ .../Entities/Audio/MusicGenre.cs | 21 ++++++ MediaBrowser.Controller/Entities/Genre.cs | 31 ++++++++ .../Entities/IItemByName.cs | 25 +++++++ MediaBrowser.Controller/Entities/Person.cs | 38 ++++++++++ MediaBrowser.Controller/Entities/Studio.cs | 37 ++++++++++ MediaBrowser.Controller/Entities/Year.cs | 42 +++++++++++ 8 files changed, 247 insertions(+), 38 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index cf886ae82..adeb889b7 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -102,21 +102,9 @@ namespace Emby.Server.Implementations.Dto (programTuples ??= []).Add((item, dto)); } - if (item is IItemByName byName) + if (item is IItemByName itemByName && options.ContainsField(ItemFields.ItemCounts)) { - if (options.ContainsField(ItemFields.ItemCounts)) - { - var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user) - { - Recursive = true, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } - }); - - SetItemByNameInfo(item, dto, libraryItems); - } + SetItemByNameInfo(itemByName, dto, user); } returnItems[index] = dto; @@ -147,34 +135,14 @@ namespace Emby.Server.Implementations.Dto LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult(); } - if (item is IItemByName itemByName - && options.ContainsField(ItemFields.ItemCounts)) + if (item is IItemByName itemByName && options.ContainsField(ItemFields.ItemCounts)) { - SetItemByNameInfo( - item, - dto, - GetTaggedItems( - itemByName, - user, - new DtoOptions(false) - { - EnableImages = false - })); + SetItemByNameInfo(itemByName, dto, user); } return dto; } - private static IReadOnlyList 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) { var dto = new BaseItemDto @@ -319,14 +287,43 @@ namespace Emby.Server.Implementations.Dto { 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; } + 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 taggedItems) { if (item is MusicArtist) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 58841e5b7..cde4cd6b3 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -98,6 +98,24 @@ namespace MediaBrowser.Controller.Entities.Audio 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) { return IsAccessedByName ? 0 : base.GetChildCount(user); diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 65669e680..9538e0cfa 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -73,6 +73,27 @@ namespace MediaBrowser.Controller.Entities.Audio 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) { return GetPath(name, true); diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 6ec78a270..20fab93b2 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -76,6 +76,37 @@ namespace MediaBrowser.Controller.Entities 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) { return GetPath(name, true); diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index 4928bda7a..3e79e3f00 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -10,10 +10,35 @@ namespace MediaBrowser.Controller.Entities public interface IItemByName { IReadOnlyList GetTaggedItems(InternalItemsQuery query); + + TaggedItemCounts GetTaggedItemCounts(InternalItemsQuery query); } public interface IHasDualAccess : IItemByName { 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); + } } diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 5cc4d322f..d4158655b 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; @@ -70,6 +71,43 @@ namespace MediaBrowser.Controller.Entities 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() { return false; diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index 9103b09a9..a44b32d85 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using Microsoft.Extensions.Logging; @@ -71,6 +72,42 @@ namespace MediaBrowser.Controller.Entities 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) { return GetPath(name, true); diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index 37820296c..a1ff9f4d4 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -68,6 +69,47 @@ namespace MediaBrowser.Controller.Entities 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() { if (int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) From 5eef85f027194491796ceb693057239858ac017c Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 11 Aug 2025 20:49:06 +0800 Subject: [PATCH 2/3] move new TaggedItemCounts to its own file --- .../Entities/IItemByName.cs | 23 ---------------- .../Entities/TaggedItemCounts.cs | 27 +++++++++++++++++++ 2 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 MediaBrowser.Controller/Entities/TaggedItemCounts.cs diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index 3e79e3f00..0cacadb67 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -18,27 +18,4 @@ namespace MediaBrowser.Controller.Entities { 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); - } } diff --git a/MediaBrowser.Controller/Entities/TaggedItemCounts.cs b/MediaBrowser.Controller/Entities/TaggedItemCounts.cs new file mode 100644 index 000000000..0e7042988 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TaggedItemCounts.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Controller.Entities +{ + 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); + } +} From beca405ad4c771f9cfdd5b410b4a4ba9f1c2d6c8 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 11 Aug 2025 21:03:55 -0600 Subject: [PATCH 3/3] Refactor to pull item counts in a single query --- Emby.Server.Implementations/Dto/DtoService.cs | 132 +++++++++++++++--- .../Library/LibraryManager.cs | 19 +++ .../Item/BaseItemRepository.cs | 60 ++++++++ .../Entities/Audio/MusicArtist.cs | 18 --- .../Entities/Audio/MusicGenre.cs | 21 --- MediaBrowser.Controller/Entities/Genre.cs | 31 ---- .../Entities/IItemByName.cs | 2 - MediaBrowser.Controller/Entities/Person.cs | 38 ----- MediaBrowser.Controller/Entities/Studio.cs | 37 ----- .../Entities/TaggedItemCounts.cs | 27 ---- MediaBrowser.Controller/Entities/Year.cs | 42 ------ .../Library/ILibraryManager.cs | 2 + .../Persistence/IItemRepository.cs | 2 + MediaBrowser.Model/Dto/ItemCounts.cs | 9 ++ 14 files changed, 207 insertions(+), 233 deletions(-) delete mode 100644 MediaBrowser.Controller/Entities/TaggedItemCounts.cs diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index adeb889b7..0db1606ea 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -37,6 +38,77 @@ namespace Emby.Server.Implementations.Dto { public class DtoService : IDtoService { + private static readonly FrozenDictionary _relatedItemKinds = new Dictionary + { + { + BaseItemKind.Genre, [ + BaseItemKind.Audio, + BaseItemKind.Episode, + BaseItemKind.Movie, + BaseItemKind.LiveTvProgram, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.MusicVideo, + BaseItemKind.Series, + BaseItemKind.Trailer + ] + }, + { + BaseItemKind.MusicArtist, [ + BaseItemKind.Audio, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicVideo + ] + }, + { + BaseItemKind.MusicGenre, [ + BaseItemKind.Audio, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.MusicVideo + ] + }, + { + BaseItemKind.Person, [ + BaseItemKind.Audio, + BaseItemKind.Episode, + BaseItemKind.Movie, + BaseItemKind.LiveTvProgram, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.MusicVideo, + BaseItemKind.Series, + BaseItemKind.Trailer + ] + }, + { + BaseItemKind.Studio, [ + BaseItemKind.Audio, + BaseItemKind.Episode, + BaseItemKind.Movie, + BaseItemKind.LiveTvProgram, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.MusicVideo, + BaseItemKind.Series, + BaseItemKind.Trailer + ] + }, + { + BaseItemKind.Year, [ + BaseItemKind.Audio, + BaseItemKind.Episode, + BaseItemKind.Movie, + BaseItemKind.LiveTvProgram, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.MusicVideo, + BaseItemKind.Series, + BaseItemKind.Trailer + ] + } + }.ToFrozenDictionary(); + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataRepository; @@ -102,9 +174,9 @@ namespace Emby.Server.Implementations.Dto (programTuples ??= []).Add((item, dto)); } - if (item is IItemByName itemByName && options.ContainsField(ItemFields.ItemCounts)) + if (options.ContainsField(ItemFields.ItemCounts)) { - SetItemByNameInfo(itemByName, dto, user); + SetItemByNameInfo(dto, user); } returnItems[index] = dto; @@ -135,9 +207,9 @@ namespace Emby.Server.Implementations.Dto LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult(); } - if (item is IItemByName itemByName && options.ContainsField(ItemFields.ItemCounts)) + if (options.ContainsField(ItemFields.ItemCounts)) { - SetItemByNameInfo(itemByName, dto, user); + SetItemByNameInfo(dto, user); } return dto; @@ -283,34 +355,60 @@ namespace Emby.Server.Implementations.Dto } /// + /// TODO refactor this to use the new SetItemByNameInfo. + /// Some callers already have the counts extracted so no reason to retrieve them again. public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List? taggedItems, User? user = null) { var dto = GetBaseItemDtoInternal(item, options, user); - if (options.ContainsField(ItemFields.ItemCounts)) + if (options.ContainsField(ItemFields.ItemCounts) + && taggedItems is not null + && taggedItems.Count != 0) { - if (taggedItems is not null) - { - SetItemByNameInfo(item, dto, taggedItems!); - } - else if (item is IItemByName itemByName) - { - SetItemByNameInfo(itemByName, dto, user); - } + SetItemByNameInfo(item, dto, taggedItems); } return dto; } - private static void SetItemByNameInfo(IItemByName item, BaseItemDto dto, User? user) + private void SetItemByNameInfo(BaseItemDto dto, User? user) { + if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds)) + { + return; + } + var query = new InternalItemsQuery(user) { Recursive = true, - DtoOptions = new DtoOptions(false) { EnableImages = false } + DtoOptions = new DtoOptions(false) { EnableImages = false }, + IncludeItemTypes = relatedItemKinds }; - var counts = item.GetTaggedItemCounts(query); + 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); dto.AlbumCount = counts.AlbumCount; dto.ArtistCount = counts.ArtistCount; @@ -321,7 +419,7 @@ namespace Emby.Server.Implementations.Dto dto.SeriesCount = counts.SeriesCount; dto.SongCount = counts.SongCount; dto.TrailerCount = counts.TrailerCount; - dto.ChildCount = counts.ChildCount; + dto.ChildCount = counts.TotalItemCount(); } private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList taggedItems) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index df71868b6..3aaaaba5a 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1389,6 +1389,25 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetCount(query); } + public ItemCounts GetItemCounts(InternalItemsQuery query) + { + if (query.Recursive && !query.ParentId.IsEmpty()) + { + var parent = GetItemById(query.ParentId); + if (parent is not null) + { + SetTopParentIdsOrAncestors(query, [parent]); + } + } + + if (query.User is not null) + { + AddUserToQuery(query, query.User); + } + + return _itemRepository.GetItemCounts(query); + } + public IReadOnlyList GetItemList(InternalItemsQuery query, List parents) { SetTopParentIdsOrAncestors(query, parents); diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index d59eba690..c5fdcd834 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -457,6 +457,66 @@ public sealed class BaseItemRepository return dbQuery.Count(); } + /// + public ItemCounts GetItemCounts(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + // Hack for right now since we currently don't support filtering out these duplicates within a query + PrepareFilterQuery(filter); + + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter); + + var counts = dbQuery + .GroupBy(x => x.Type) + .Select(x => new { x.Key, Count = x.Count() }) + .AsEnumerable(); + + var lookup = _itemTypeLookup.BaseItemKindNames; + var result = new ItemCounts(); + 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; + } + } + + return result; + } + #pragma warning disable CA1307 // Specify StringComparison for clarity /// /// Gets the type. diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index cde4cd6b3..58841e5b7 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -98,24 +98,6 @@ namespace MediaBrowser.Controller.Entities.Audio 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) { return IsAccessedByName ? 0 : base.GetChildCount(user); diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 9538e0cfa..65669e680 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -73,27 +73,6 @@ namespace MediaBrowser.Controller.Entities.Audio 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) { return GetPath(name, true); diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 20fab93b2..6ec78a270 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -76,37 +76,6 @@ namespace MediaBrowser.Controller.Entities 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) { return GetPath(name, true); diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index 0cacadb67..4928bda7a 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -10,8 +10,6 @@ namespace MediaBrowser.Controller.Entities public interface IItemByName { IReadOnlyList GetTaggedItems(InternalItemsQuery query); - - TaggedItemCounts GetTaggedItemCounts(InternalItemsQuery query); } public interface IHasDualAccess : IItemByName diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index d4158655b..5cc4d322f 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; @@ -71,43 +70,6 @@ namespace MediaBrowser.Controller.Entities 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() { return false; diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index a44b32d85..9103b09a9 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -using Jellyfin.Data.Enums; using Jellyfin.Extensions; using Microsoft.Extensions.Logging; @@ -72,42 +71,6 @@ namespace MediaBrowser.Controller.Entities 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) { return GetPath(name, true); diff --git a/MediaBrowser.Controller/Entities/TaggedItemCounts.cs b/MediaBrowser.Controller/Entities/TaggedItemCounts.cs deleted file mode 100644 index 0e7042988..000000000 --- a/MediaBrowser.Controller/Entities/TaggedItemCounts.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Controller.Entities -{ - 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); - } -} diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index a1ff9f4d4..37820296c 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json.Serialization; -using Jellyfin.Data.Enums; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -69,47 +68,6 @@ namespace MediaBrowser.Controller.Entities 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() { if (int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 98ed15eb6..b72d1d0b4 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -630,6 +630,8 @@ namespace MediaBrowser.Controller.Library int GetCount(InternalItemsQuery query); + ItemCounts GetItemCounts(InternalItemsQuery query); + Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason); BaseItem GetParentItem(Guid? parentId, Guid? userId); diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index f4ac0ece4..a0dabbac6 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -84,6 +84,8 @@ public interface IItemRepository int GetCount(InternalItemsQuery filter); + ItemCounts GetItemCounts(InternalItemsQuery filter); + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter); QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter); diff --git a/MediaBrowser.Model/Dto/ItemCounts.cs b/MediaBrowser.Model/Dto/ItemCounts.cs index 95f4a3d77..a15a0c82a 100644 --- a/MediaBrowser.Model/Dto/ItemCounts.cs +++ b/MediaBrowser.Model/Dto/ItemCounts.cs @@ -76,5 +76,14 @@ namespace MediaBrowser.Model.Dto /// /// The item count. public int ItemCount { get; set; } + + /// + /// Adds all counts. + /// + /// The total of the counts. + public int TotalItemCount() + { + return MovieCount + SeriesCount + EpisodeCount + ArtistCount + ProgramCount + TrailerCount + SongCount + AlbumCount + MusicVideoCount + BoxSetCount + BookCount; + } } }