#pragma warning disable RS0030 // Do not use banned APIs using System; using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; namespace Jellyfin.Server.Implementations.Item; public sealed partial class BaseItemRepository { /// public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter) { return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]); } /// public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter) { return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]); } /// public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter) { return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]); } /// public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter) { return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]); } /// public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter) { return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]); } /// public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter) { return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]); } /// public IReadOnlyList GetStudioNames() { return GetItemValueNames(_getStudiosValueTypes, [], []); } /// public IReadOnlyList GetAllArtistNames() { return GetItemValueNames(_getAllArtistsValueTypes, [], []); } /// public IReadOnlyList GetMusicGenreNames() { return GetItemValueNames( _getGenreValueTypes, _itemTypeLookup.MusicGenreTypes, []); } /// public IReadOnlyList GetGenreNames() { return GetItemValueNames( _getGenreValueTypes, [], _itemTypeLookup.MusicGenreTypes); } private string[] GetItemValueNames(IReadOnlyList itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) { using var context = _dbProvider.CreateDbContext(); var query = context.ItemValuesMap .AsNoTracking() .Where(e => itemValueTypes.Any(w => w == e.ItemValue.Type)); if (withItemTypes.Count > 0) { query = query.Where(e => withItemTypes.Contains(e.Item.Type)); } if (excludeItemTypes.Count > 0) { query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type)); } // query = query.DistinctBy(e => e.CleanValue); return query.Select(e => e.ItemValue) .GroupBy(e => e.CleanValue) .Select(e => e.OrderBy(v => v.Value).First().Value) .ToArray(); } private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList itemValueTypes, string returnType) { ArgumentNullException.ThrowIfNull(filter); if (!filter.Limit.HasValue) { filter.EnableTotalRecordCount = false; } using var context = _dbProvider.CreateDbContext(); var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context, new InternalItemsQuery(filter.User) { ExcludeItemTypes = filter.ExcludeItemTypes, IncludeItemTypes = filter.IncludeItemTypes, MediaTypes = filter.MediaTypes, AncestorIds = filter.AncestorIds, ItemIds = filter.ItemIds, TopParentIds = filter.TopParentIds, ParentId = filter.ParentId, IsAiring = filter.IsAiring, IsMovie = filter.IsMovie, IsSports = filter.IsSports, IsKids = filter.IsKids, IsNews = filter.IsNews, IsSeries = filter.IsSeries }); var itemValuesQuery = context.ItemValuesMap .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type)) .Join( innerQueryFilter, ivm => ivm.ItemId, g => g.Id, (ivm, g) => ivm.ItemValue.CleanValue); var innerQuery = PrepareItemQuery(context, filter) .Where(e => e.Type == returnType) .Where(e => itemValuesQuery.Contains(e.CleanName)); var outerQueryFilter = new InternalItemsQuery(filter.User) { IsPlayed = filter.IsPlayed, IsFavorite = filter.IsFavorite, IsFavoriteOrLiked = filter.IsFavoriteOrLiked, IsLiked = filter.IsLiked, IsLocked = filter.IsLocked, NameLessThan = filter.NameLessThan, NameStartsWith = filter.NameStartsWith, NameStartsWithOrGreater = filter.NameStartsWithOrGreater, Tags = filter.Tags, OfficialRatings = filter.OfficialRatings, StudioIds = filter.StudioIds, GenreIds = filter.GenreIds, Genres = filter.Genres, Years = filter.Years, NameContains = filter.NameContains, SearchTerm = filter.SearchTerm, ExcludeItemIds = filter.ExcludeItemIds }; var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter) .GroupBy(e => e.PresentationUniqueKey) .Select(e => e.OrderBy(x => x.Id).FirstOrDefault()) .Select(e => e!.Id); var query = context.BaseItems .Include(e => e.TrailerTypes) .Include(e => e.Provider) .Include(e => e.LockedFields) .Include(e => e.Images) .Include(e => e.LinkedChildEntities) .AsSingleQuery() .Where(e => masterQuery.Contains(e.Id)); query = ApplyOrder(query, filter, context); var result = new QueryResult<(BaseItemDto, ItemCounts?)>(); if (filter.EnableTotalRecordCount) { result.TotalRecordCount = query.Count(); } if (filter.Limit.HasValue || filter.StartIndex.HasValue) { var offset = filter.StartIndex ?? 0; if (offset > 0) { query = query.Skip(offset); } if (filter.Limit.HasValue) { query = query.Take(filter.Limit.Value); } } if (filter.IncludeItemTypes.Length > 0) { var typeSubQuery = new InternalItemsQuery(filter.User) { ExcludeItemTypes = filter.ExcludeItemTypes, IncludeItemTypes = filter.IncludeItemTypes, MediaTypes = filter.MediaTypes, AncestorIds = filter.AncestorIds, ExcludeItemIds = filter.ExcludeItemIds, ItemIds = filter.ItemIds, TopParentIds = filter.TopParentIds, ParentId = filter.ParentId, IsPlayed = filter.IsPlayed }; var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery) .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type))); var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; // Get the IDs from itemCountQuery to use in the join var itemIds = itemCountQuery.Select(e => e.Id); // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite) // Instead, start from ItemValueMaps and join with BaseItems var countsByCleanName = context.ItemValuesMap .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type)) .Where(ivm => itemIds.Contains(ivm.ItemId)) .Join( context.BaseItems, ivm => ivm.ItemId, e => e.Id, (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type }) .GroupBy(x => new { x.CleanName, x.Type }) .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() }) .GroupBy(x => x.CleanName) .ToDictionary( g => g.Key, g => new ItemCounts { SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count), EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count), MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count), AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count), ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count), SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count), TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count), }); result.StartIndex = filter.StartIndex ?? 0; result.Items = [ .. query .AsEnumerable() .Where(e => e is not null) .Select(e => { var item = DeserializeBaseItem(e, filter.SkipDeserialization); countsByCleanName.TryGetValue(e.CleanName ?? string.Empty, out var itemCount); return (item, itemCount); }) .Where(x => x.item is not null) .Select(x => (x.item!, x.itemCount)) ]; } else { result.StartIndex = filter.StartIndex ?? 0; result.Items = [ .. query .AsEnumerable() .Where(e => e != null) .Select(e => DeserializeBaseItem(e, filter.SkipDeserialization)) .Where(item => item != null) .Select(item => (item!, (ItemCounts?)null)) ]; } return result; } }