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 cf886ae826..adeb889b72 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 58841e5b78..cde4cd6b3e 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 65669e6804..9538e0cfaa 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 6ec78a270e..20fab93b2d 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 4928bda7a2..3e79e3f00a 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 5cc4d322f7..d4158655bf 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 9103b09a95..a44b32d85b 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 37820296cc..a1ff9f4d4b 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 3e79e3f00a..0cacadb672 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 0000000000..0e70429886 --- /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 adeb889b72..0db1606ea5 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 df71868b6a..3aaaaba5a0 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 d59eba6903..c5fdcd834a 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 cde4cd6b3e..58841e5b78 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 9538e0cfaa..65669e6804 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 20fab93b2d..6ec78a270e 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 0cacadb672..4928bda7a2 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 d4158655bf..5cc4d322f7 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 a44b32d85b..9103b09a95 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 0e70429886..0000000000 --- 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 a1ff9f4d4b..37820296cc 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 98ed15eb6c..b72d1d0b4c 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 f4ac0ece44..a0dabbac62 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 95f4a3d772..a15a0c82a8 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; + } } }