diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 626f4ed0cd..07938d172f 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -418,37 +418,7 @@ namespace Emby.Server.Implementations.Dto return; } - var query = new InternalItemsQuery(user) - { - Recursive = true, - DtoOptions = new DtoOptions(false) { EnableImages = false }, - IncludeItemTypes = relatedItemKinds - }; - - switch (dto.Type) - { - case BaseItemKind.Genre: - case BaseItemKind.MusicGenre: - query.GenreIds = [dto.Id]; - break; - case BaseItemKind.MusicArtist: - query.ArtistIds = [dto.Id]; - break; - case BaseItemKind.Person: - query.PersonIds = [dto.Id]; - break; - case BaseItemKind.Studio: - query.StudioIds = [dto.Id]; - break; - case BaseItemKind.Year - when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year): - query.Years = [year]; - break; - default: - return; - } - - var counts = _libraryManager.GetItemCounts(query); + var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user); dto.AlbumCount = counts.AlbumCount; dto.ArtistCount = counts.ArtistCount; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 9a5bd41914..60ac15ca97 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1561,6 +1561,18 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItemCounts(query); } + /// + public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? user) + { + var query = new InternalItemsQuery(user); + if (user is not null) + { + AddUserToQuery(query, user); + } + + return _itemRepository.GetItemCountsForNameItem(kind, id, relatedItemKinds, query); + } + public Dictionary GetChildCountBatch(IReadOnlyList parentIds, Guid? userId) { return _itemRepository.GetChildCountBatch(parentIds, userId); diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index c778d3f448..b908f92be6 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -647,13 +647,13 @@ public class UserLibraryController : BaseJellyfinApiController var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3; - if (!hasMetadata) + if (performFullRefresh) { var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { MetadataRefreshMode = MetadataRefreshMode.FullRefresh, ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ForceSave = performFullRefresh + ForceSave = true }; await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index ed5db0f934..e05f4c45dc 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1343,6 +1343,141 @@ public sealed class BaseItemRepository return result; } + /// + public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, InternalItemsQuery accessFilter) + { + using var context = _dbProvider.CreateDbContext(); + + // Look up the item's name/cleanname + var item = context.BaseItems.AsNoTracking() + .Where(e => e.Id == id) + .Select(e => new { e.Name, e.CleanName }) + .FirstOrDefault(); + + if (item is null) + { + return new ItemCounts(); + } + + // Build the base query starting from the mapping table (flipped join order) + IQueryable baseQuery; + switch (kind) + { + case BaseItemKind.Person: + baseQuery = context.PeopleBaseItemMap + .AsNoTracking() + .Where(m => m.People.Name == item.Name) + .Select(m => m.Item); + break; + case BaseItemKind.MusicArtist: + baseQuery = context.ItemValuesMap + .AsNoTracking() + .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName + && (ivm.ItemValue.Type == ItemValueType.Artist || ivm.ItemValue.Type == ItemValueType.AlbumArtist)) + .Select(ivm => ivm.Item); + break; + case BaseItemKind.Genre: + case BaseItemKind.MusicGenre: + baseQuery = context.ItemValuesMap + .AsNoTracking() + .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName + && ivm.ItemValue.Type == ItemValueType.Genre) + .Select(ivm => ivm.Item); + break; + case BaseItemKind.Studio: + baseQuery = context.ItemValuesMap + .AsNoTracking() + .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName + && ivm.ItemValue.Type == ItemValueType.Studios) + .Select(ivm => ivm.Item); + break; + case BaseItemKind.Year: + if (int.TryParse(item.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) + { + baseQuery = context.BaseItems + .AsNoTracking() + .Where(e => e.ProductionYear == year); + } + else + { + return new ItemCounts(); + } + + break; + default: + return new ItemCounts(); + } + + // Apply type filter + var typeNames = relatedItemKinds.Select(k => _itemTypeLookup.BaseItemKindNames[k]).ToArray(); + baseQuery = baseQuery.Where(e => typeNames.Contains(e.Type)); + + // Apply access filtering (parental ratings, blocked/allowed tags, library access) + baseQuery = ApplyAccessFiltering(context, baseQuery, accessFilter); + + // Group by type and count + var counts = baseQuery + .GroupBy(x => x.Type) + .Select(x => new { x.Key, Count = x.Count() }) + .ToArray(); + + var lookup = _itemTypeLookup.BaseItemKindNames; + var result = new ItemCounts + { + ItemCount = counts.Sum(c => c.Count) + }; + + foreach (var count in counts) + { + if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal)) + { + result.AlbumCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal)) + { + result.ArtistCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal)) + { + result.EpisodeCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal)) + { + result.MovieCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal)) + { + result.MusicVideoCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal)) + { + result.ProgramCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal)) + { + result.SeriesCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal)) + { + result.SongCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal)) + { + result.TrailerCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.BoxSet], StringComparison.Ordinal)) + { + result.BoxSetCount = count.Count; + } + else if (string.Equals(count.Key, lookup[BaseItemKind.Book], StringComparison.Ordinal)) + { + result.BookCount = count.Count; + } + } + + return result; + } + #pragma warning disable CA1307 // Specify StringComparison for clarity /// /// Gets the type. diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index e73dede1d4..5cfb940891 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -706,6 +706,16 @@ namespace MediaBrowser.Controller.Library ItemCounts GetItemCounts(InternalItemsQuery query); + /// + /// Gets item counts for a "by-name" item using an optimized query path. + /// + /// The kind of the name item. + /// The ID of the name item. + /// The item kinds to count. + /// The user for access filtering. + /// The item counts grouped by type. + ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? user); + /// /// Batch-fetches child counts for multiple parent folders. /// Returns the count of immediate children (non-recursive) for each parent. diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index f1b27c52db..bcbbcc4785 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -112,6 +112,17 @@ public interface IItemRepository ItemCounts GetItemCounts(InternalItemsQuery filter); + /// + /// Gets item counts for a "by-name" item (Person, MusicArtist, Genre, MusicGenre, Studio, Year) + /// using an optimized query that starts from the mapping table instead of scanning all BaseItems. + /// + /// The kind of the name item. + /// The ID of the name item. + /// The item kinds to count. + /// A pre-configured query with user access filtering settings. + /// The item counts grouped by type. + ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, InternalItemsQuery accessFilter); + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter); QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter);