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);