< Summary - Jellyfin

Line coverage
28%
Covered lines: 401
Uncovered lines: 1028
Coverable lines: 1429
Total lines: 2862
Line coverage: 28%
Branch coverage
39%
Covered branches: 257
Total branches: 648
Branch coverage: 39.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/13/2026 - 12:11:21 AM Line coverage: 51.4% (689/1338) Branch coverage: 50.5% (386/764) Total lines: 27353/11/2026 - 12:13:45 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/12/2026 - 12:13:54 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.9% (389/764) Total lines: 27364/13/2026 - 12:14:08 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/15/2026 - 12:14:34 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.6% (387/764) Total lines: 27364/16/2026 - 12:15:18 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/19/2026 - 12:14:27 AM Line coverage: 51.8% (717/1384) Branch coverage: 50.3% (386/766) Total lines: 27365/4/2026 - 12:15:16 AM Line coverage: 28.4% (397/1394) Branch coverage: 43.6% (277/634) Total lines: 28075/6/2026 - 12:15:23 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 28145/11/2026 - 12:15:59 AM Line coverage: 28.2% (397/1404) Branch coverage: 43.5% (277/636) Total lines: 28155/12/2026 - 12:15:57 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 28185/16/2026 - 12:15:55 AM Line coverage: 28% (400/1424) Branch coverage: 43.6% (282/646) Total lines: 28535/20/2026 - 12:15:44 AM Line coverage: 28% (400/1424) Branch coverage: 39.6% (256/646) Total lines: 28535/22/2026 - 12:15:17 AM Line coverage: 28% (401/1429) Branch coverage: 39.6% (257/648) Total lines: 2862 2/13/2026 - 12:11:21 AM Line coverage: 51.4% (689/1338) Branch coverage: 50.5% (386/764) Total lines: 27353/11/2026 - 12:13:45 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/12/2026 - 12:13:54 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.9% (389/764) Total lines: 27364/13/2026 - 12:14:08 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/15/2026 - 12:14:34 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.6% (387/764) Total lines: 27364/16/2026 - 12:15:18 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/19/2026 - 12:14:27 AM Line coverage: 51.8% (717/1384) Branch coverage: 50.3% (386/766) Total lines: 27365/4/2026 - 12:15:16 AM Line coverage: 28.4% (397/1394) Branch coverage: 43.6% (277/634) Total lines: 28075/6/2026 - 12:15:23 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 28145/11/2026 - 12:15:59 AM Line coverage: 28.2% (397/1404) Branch coverage: 43.5% (277/636) Total lines: 28155/12/2026 - 12:15:57 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 28185/16/2026 - 12:15:55 AM Line coverage: 28% (400/1424) Branch coverage: 43.6% (282/646) Total lines: 28535/20/2026 - 12:15:44 AM Line coverage: 28% (400/1424) Branch coverage: 39.6% (256/646) Total lines: 28535/22/2026 - 12:15:17 AM Line coverage: 28% (401/1429) Branch coverage: 39.6% (257/648) Total lines: 2862

Coverage delta

Coverage delta 24 -24

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: GetAllArtists(...)100%210%
File 1: GetArtists(...)100%210%
File 1: GetAlbumArtists(...)100%210%
File 1: GetStudios(...)100%210%
File 1: GetGenres(...)100%210%
File 1: GetMusicGenres(...)100%210%
File 1: GetStudioNames()100%11100%
File 1: GetAllArtistNames()100%11100%
File 1: GetMusicGenreNames()100%11100%
File 1: GetGenreNames()100%11100%
File 1: GetItemValueNames(...)100%44100%
File 1: GetItemValues(...)0%156120%
File 1: BuildItemCountsByCleanName(...)100%210%
File 2: .cctor()100%11100%
File 2: .ctor(...)100%11100%
File 2: Map(...)100%210%
File 2: Map(...)100%11100%
File 2: DeserializeBaseItem(...)100%11100%
File 2: PrepareFilterQuery(...)83.33%6680%
File 2: GetItemByNameTypesInQuery(...)100%1010100%
File 2: IsTypeInQuery(...)75%5466.66%
File 2: EnableGroupByPresentationUniqueKey(...)65%212087.5%
File 2: Map(...)100%210%
File 2: Map(...)100%210%
File 2: GetPathToSave(...)0%620%
File 2: DeserializeBaseItem(...)50%101088.88%
File 3: PrepareItemQuery(...)100%11100%
File 3: ApplyQueryFilter(...)100%11100%
File 3: ApplyQueryPaging(...)75%8885.71%
File 3: ApplyGroupingFilter(...)70%151062.5%
File 3: ApplyBoxSetCollapsing(...)0%620%
File 3: ApplyBoxSetCollapsingAll(...)100%210%
File 3: ApplyNameFilters(...)50%14640%
File 3: ApplyNavigations(...)72.22%181891.3%
File 3: ApplyOrder(...)70.58%423481.25%
File 3: ApplySeriesDatePlayedOrder(...)0%2040%
File 3: BuildAccessFilteredDescendantsQuery(...)100%210%
File 3: ApplyAccessFiltering(...)0%156120%
File 3: BuildMaxParentalRatingFilter(...)100%11100%
File 3: GetFullyPlayedFolderIdsQuery(...)100%210%
File 4: GetItemIdsList(...)100%11100%
File 4: GetItems(...)37.5%27833.33%
File 4: GetItemList(...)75%5468.18%
File 4: GetLatestItemList(...)0%210140%
File 4: LoadLatestByIds(...)100%210%
File 4: GetLatestTvShowItems(...)0%4692680%
File 4: ItemExistsAsync()100%11100%
File 4: RetrieveItem(...)50%4490.9%
File 4: GetIsPlayed(...)0%620%
File 4: GetQueryFiltersLegacy(...)100%210%
File 5: .cctor()100%11100%
File 5: TranslateQuery(...)40.41%5632338627.86%

File(s)

/srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Linq;
 6using Jellyfin.Data.Enums;
 7using Jellyfin.Database.Implementations.Entities;
 8using MediaBrowser.Controller.Entities;
 9using MediaBrowser.Model.Dto;
 10using MediaBrowser.Model.Querying;
 11using Microsoft.EntityFrameworkCore;
 12using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 13
 14namespace Jellyfin.Server.Implementations.Item;
 15
 16public sealed partial class BaseItemRepository
 17{
 18    /// <inheritdoc />
 19    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 20    {
 021        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 22    }
 23
 24    /// <inheritdoc />
 25    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 26    {
 027        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 28    }
 29
 30    /// <inheritdoc />
 31    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 32    {
 033        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 34    }
 35
 36    /// <inheritdoc />
 37    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 38    {
 039        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 40    }
 41
 42    /// <inheritdoc />
 43    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 44    {
 045        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 46    }
 47
 48    /// <inheritdoc />
 49    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 50    {
 051        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 52    }
 53
 54    /// <inheritdoc />
 55    public IReadOnlyList<string> GetStudioNames()
 56    {
 1657        return GetItemValueNames(_getStudiosValueTypes, [], []);
 58    }
 59
 60    /// <inheritdoc />
 61    public IReadOnlyList<string> GetAllArtistNames()
 62    {
 1663        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 64    }
 65
 66    /// <inheritdoc />
 67    public IReadOnlyList<string> GetMusicGenreNames()
 68    {
 1669        return GetItemValueNames(
 1670            _getGenreValueTypes,
 1671            _itemTypeLookup.MusicGenreTypes,
 1672            []);
 73    }
 74
 75    /// <inheritdoc />
 76    public IReadOnlyList<string> GetGenreNames()
 77    {
 1678        return GetItemValueNames(
 1679            _getGenreValueTypes,
 1680            [],
 1681            _itemTypeLookup.MusicGenreTypes);
 82    }
 83
 84    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 85    {
 6486        using var context = _dbProvider.CreateDbContext();
 87
 6488        var query = context.ItemValuesMap
 6489            .AsNoTracking()
 6490            .Where(e => itemValueTypes.Any(w => w == e.ItemValue.Type));
 6491        if (withItemTypes.Count > 0)
 92        {
 1693            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 94        }
 95
 6496        if (excludeItemTypes.Count > 0)
 97        {
 1698            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 99        }
 100
 64101        return query.Select(e => e.ItemValue)
 64102            .GroupBy(e => e.CleanValue)
 64103            .Select(g => g.Min(v => v.Value)!)
 64104            .ToArray();
 64105    }
 106
 107    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 108    {
 0109        ArgumentNullException.ThrowIfNull(filter);
 110
 0111        if (!filter.Limit.HasValue)
 112        {
 0113            filter.EnableTotalRecordCount = false;
 114        }
 115
 0116        using var context = _dbProvider.CreateDbContext();
 117
 0118        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 0119        {
 0120            ExcludeItemTypes = filter.ExcludeItemTypes,
 0121            IncludeItemTypes = filter.IncludeItemTypes,
 0122            MediaTypes = filter.MediaTypes,
 0123            AncestorIds = filter.AncestorIds,
 0124            ItemIds = filter.ItemIds,
 0125            TopParentIds = filter.TopParentIds,
 0126            ParentId = filter.ParentId,
 0127            IsAiring = filter.IsAiring,
 0128            IsMovie = filter.IsMovie,
 0129            IsSports = filter.IsSports,
 0130            IsKids = filter.IsKids,
 0131            IsNews = filter.IsNews,
 0132            IsSeries = filter.IsSeries
 0133        });
 134
 135        // Keep this as an IQueryable sub-select. Materializing to a list would inline one
 136        // bound parameter per CleanValue and hit SQLite's variable cap on libraries with
 137        // high-cardinality value types (e.g. tens of thousands of artists).
 0138        var matchingCleanValues = context.ItemValuesMap
 0139            .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
 0140            .Join(
 0141                innerQueryFilter,
 0142                ivm => ivm.ItemId,
 0143                g => g.Id,
 0144                (ivm, g) => ivm.ItemValue.CleanValue)
 0145            .Distinct();
 146
 0147        var innerQuery = PrepareItemQuery(context, filter)
 0148            .Where(e => e.Type == returnType)
 0149            .Where(e => matchingCleanValues.Contains(e.CleanName!));
 150
 0151        var outerQueryFilter = new InternalItemsQuery(filter.User)
 0152        {
 0153            IsPlayed = filter.IsPlayed,
 0154            IsFavorite = filter.IsFavorite,
 0155            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 0156            IsLiked = filter.IsLiked,
 0157            IsLocked = filter.IsLocked,
 0158            NameLessThan = filter.NameLessThan,
 0159            NameStartsWith = filter.NameStartsWith,
 0160            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 0161            Tags = filter.Tags,
 0162            OfficialRatings = filter.OfficialRatings,
 0163            StudioIds = filter.StudioIds,
 0164            GenreIds = filter.GenreIds,
 0165            Genres = filter.Genres,
 0166            Years = filter.Years,
 0167            NameContains = filter.NameContains,
 0168            SearchTerm = filter.SearchTerm,
 0169            ExcludeItemIds = filter.ExcludeItemIds
 0170        };
 171
 172        // Collapse rows that share a PresentationUniqueKey (e.g. alternate versions) by picking
 173        // the lowest Id per group. Keep as an IQueryable sub-select so paging is applied AFTER
 174        // ApplyOrder runs the caller's actual sort.
 0175        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter);
 0176        var representativeIds = masterQuery
 0177            .GroupBy(e => e.PresentationUniqueKey)
 0178            .Select(g => g.Min(e => e.Id));
 179
 0180        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 0181        if (filter.EnableTotalRecordCount)
 182        {
 0183            result.TotalRecordCount = representativeIds.Count();
 184        }
 185
 0186        var query = ApplyNavigations(
 0187                context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => representativeIds.Contains(e.Id)),
 0188                filter);
 189
 0190        query = ApplyOrder(query, filter, context);
 191
 0192        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 193        {
 0194            query = query.Skip(filter.StartIndex.Value);
 195        }
 196
 0197        if (filter.Limit.HasValue)
 198        {
 0199            query = query.Take(filter.Limit.Value);
 200        }
 201
 0202        result.StartIndex = filter.StartIndex ?? 0;
 0203        if (filter.IncludeItemTypes.Length > 0)
 204        {
 0205            var countsByCleanName = BuildItemCountsByCleanName(context, filter, itemValueTypes);
 0206            result.Items =
 0207            [
 0208                .. query
 0209                    .AsEnumerable()
 0210                    .Where(e => e is not null)
 0211                    .Select(e =>
 0212                    {
 0213                        var item = DeserializeBaseItem(e, filter.SkipDeserialization);
 0214                        countsByCleanName.TryGetValue(e.CleanName ?? string.Empty, out var itemCount);
 0215                        return (item, itemCount);
 0216                    })
 0217                    .Where(x => x.item is not null)
 0218                    .Select(x => (x.item!, x.itemCount))
 0219            ];
 220        }
 221        else
 222        {
 0223            result.Items =
 0224            [
 0225                .. query
 0226                    .AsEnumerable()
 0227                    .Where(e => e != null)
 0228                    .Select(e => DeserializeBaseItem(e, filter.SkipDeserialization))
 0229                    .Where(item => item != null)
 0230                    .Select(item => (item!, (ItemCounts?)null))
 0231            ];
 232        }
 233
 0234        return result;
 0235    }
 236
 237    private Dictionary<string, ItemCounts> BuildItemCountsByCleanName(
 238        Database.Implementations.JellyfinDbContext context,
 239        InternalItemsQuery filter,
 240        IReadOnlyList<ItemValueType> itemValueTypes)
 241    {
 0242        var typeSubQuery = new InternalItemsQuery(filter.User)
 0243        {
 0244            ExcludeItemTypes = filter.ExcludeItemTypes,
 0245            IncludeItemTypes = filter.IncludeItemTypes,
 0246            MediaTypes = filter.MediaTypes,
 0247            AncestorIds = filter.AncestorIds,
 0248            ExcludeItemIds = filter.ExcludeItemIds,
 0249            ItemIds = filter.ItemIds,
 0250            TopParentIds = filter.TopParentIds,
 0251            ParentId = filter.ParentId,
 0252            IsPlayed = filter.IsPlayed
 0253        };
 254
 0255        var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 0256            .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 257
 0258        var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 0259        var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 0260        var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 0261        var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 0262        var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 0263        var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 0264        var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 0265        var itemIds = itemCountQuery.Select(e => e.Id);
 266
 267        // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite
 268        // Instead, start from ItemValueMaps and join with BaseItems
 0269        return context.ItemValuesMap
 0270            .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
 0271            .Where(ivm => itemIds.Contains(ivm.ItemId))
 0272            .Join(
 0273                context.BaseItems,
 0274                ivm => ivm.ItemId,
 0275                e => e.Id,
 0276                (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type })
 0277            .GroupBy(x => new { x.CleanName, x.Type })
 0278            .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() })
 0279            .GroupBy(x => x.CleanName)
 0280            .ToDictionary(
 0281                g => g.Key,
 0282                g => new ItemCounts
 0283                {
 0284                    SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count),
 0285                    EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count),
 0286                    MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count),
 0287                    AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count),
 0288                    ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count),
 0289                    SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count),
 0290                    TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count),
 0291                });
 292    }
 293}

/srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using Jellyfin.Data.Enums;
 5using Jellyfin.Database.Implementations;
 6using Jellyfin.Database.Implementations.Entities;
 7using Jellyfin.Extensions;
 8using MediaBrowser.Controller;
 9using MediaBrowser.Controller.Channels;
 10using MediaBrowser.Controller.Configuration;
 11using MediaBrowser.Controller.Entities;
 12using MediaBrowser.Controller.Persistence;
 13using Microsoft.EntityFrameworkCore;
 14using Microsoft.Extensions.Logging;
 15using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 16using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 17
 18namespace Jellyfin.Server.Implementations.Item;
 19
 20/*
 21    All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null
 22    This is done as the code isn't actually executed client side, but only the expressions are interpret and the compile
 23    This is your only warning/message regarding this topic.
 24*/
 25
 26/// <summary>
 27/// Handles all storage logic for BaseItems.
 28/// </summary>
 29public sealed partial class BaseItemRepository
 30    : IItemRepository, IItemQueryHelpers
 31{
 32    /// <summary>
 33    /// Gets the placeholder id for UserData detached items.
 34    /// </summary>
 135    public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
 36
 37    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 38    private readonly IServerApplicationHost _appHost;
 39    private readonly IItemTypeLookup _itemTypeLookup;
 40    private readonly IServerConfigurationManager _serverConfigurationManager;
 41    private readonly ILogger<BaseItemRepository> _logger;
 42
 143    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 144    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 145    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 146    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 147    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 48
 49    /// <summary>
 50    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
 51    /// </summary>
 52    /// <param name="dbProvider">The db factory.</param>
 53    /// <param name="appHost">The Application host.</param>
 54    /// <param name="itemTypeLookup">The static type lookup.</param>
 55    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
 56    /// <param name="logger">System logger.</param>
 57    public BaseItemRepository(
 58        IDbContextFactory<JellyfinDbContext> dbProvider,
 59        IServerApplicationHost appHost,
 60        IItemTypeLookup itemTypeLookup,
 61        IServerConfigurationManager serverConfigurationManager,
 62        ILogger<BaseItemRepository> logger)
 63    {
 3164        _dbProvider = dbProvider;
 3165        _appHost = appHost;
 3166        _itemTypeLookup = itemTypeLookup;
 3167        _serverConfigurationManager = serverConfigurationManager;
 3168        _logger = logger;
 3169    }
 70
 71    /// <summary>
 72    /// Maps a Entity to the DTO. Delegates to <see cref="BaseItemMapper"/>.
 73    /// </summary>
 74    /// <param name="entity">The database entity.</param>
 75    /// <param name="dto">The target DTO.</param>
 76    /// <param name="appHost">The application host.</param>
 77    /// <param name="logger">The logger.</param>
 78    /// <returns>The mapped DTO.</returns>
 79    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 80    {
 081        return BaseItemMapper.Map(entity, dto, appHost);
 82    }
 83
 84    /// <summary>
 85    /// Maps a DTO to a database entity. Delegates to <see cref="BaseItemMapper"/>.
 86    /// </summary>
 87    /// <param name="dto">The DTO to map.</param>
 88    /// <returns>The mapped database entity.</returns>
 89    public BaseItemEntity Map(BaseItemDto dto)
 90    {
 1091        return BaseItemMapper.Map(dto, _appHost);
 92    }
 93
 94    /// <summary>
 95    /// Deserializes a BaseItemEntity and sets all properties.
 96    /// </summary>
 97    /// <param name="baseItemEntity">The entity to deserialize.</param>
 98    /// <param name="logger">The logger.</param>
 99    /// <param name="appHost">The application host.</param>
 100    /// <param name="skipDeserialization">Whether to skip JSON deserialization.</param>
 101    /// <returns>The deserialized item, or null.</returns>
 102    public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost
 103    {
 3104        return BaseItemMapper.DeserializeBaseItem(baseItemEntity, logger, appHost, skipDeserialization);
 105    }
 106
 107    /// <inheritdoc />
 108    public void PrepareFilterQuery(InternalItemsQuery query)
 109    {
 454110        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 111        {
 0112            query.Limit = query.Limit.Value + 4;
 113        }
 114
 454115        if (query.IsResumable ?? false)
 116        {
 1117            query.IsVirtualItem = false;
 118        }
 454119    }
 120
 121    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 122    {
 15123        var list = new List<string>();
 124
 15125        if (IsTypeInQuery(BaseItemKind.Person, query))
 126        {
 1127            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 128        }
 129
 15130        if (IsTypeInQuery(BaseItemKind.Genre, query))
 131        {
 1132            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 133        }
 134
 15135        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 136        {
 1137            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 138        }
 139
 15140        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 141        {
 1142            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 143        }
 144
 15145        if (IsTypeInQuery(BaseItemKind.Studio, query))
 146        {
 1147            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 148        }
 149
 15150        return list;
 151    }
 152
 153    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 154    {
 75155        if (query.ExcludeItemTypes.Contains(type))
 156        {
 0157            return false;
 158        }
 159
 75160        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 161    }
 162
 163    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 164    {
 454165        if (!query.GroupByPresentationUniqueKey)
 166        {
 141167            return false;
 168        }
 169
 313170        if (query.GroupBySeriesPresentationUniqueKey)
 171        {
 0172            return false;
 173        }
 174
 313175        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 176        {
 0177            return false;
 178        }
 179
 313180        if (query.User is null)
 181        {
 311182            return false;
 183        }
 184
 2185        if (query.IncludeItemTypes.Length == 0)
 186        {
 1187            return true;
 188        }
 189
 1190        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 1191            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 1192            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 1193            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 1194            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 1195            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 196    }
 197
 198    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 199    {
 0200        return BaseItemMapper.MapImageToEntity(baseItemId, e);
 201    }
 202
 203    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 204    {
 0205        return BaseItemMapper.MapImageFromEntity(e, appHost);
 206    }
 207
 208    private string? GetPathToSave(string path)
 209    {
 0210        if (path is null)
 211        {
 0212            return null;
 213        }
 214
 0215        return _appHost.ReverseVirtualPath(path);
 216    }
 217
 218    /// <inheritdoc />
 219    public BaseItemDto? DeserializeBaseItem(BaseItemEntity entity, bool skipDeserialization = false)
 220    {
 72221        ArgumentNullException.ThrowIfNull(entity, nameof(entity));
 72222        if (_serverConfigurationManager?.Configuration is null)
 223        {
 0224            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 225        }
 226
 72227        var typeToSerialise = BaseItemMapper.GetType(entity.Type);
 72228        return BaseItemMapper.DeserializeBaseItem(
 72229            entity,
 72230            _logger,
 72231            _appHost,
 72232            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 233    }
 234}

/srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2#pragma warning disable CA1304 // Specify CultureInfo
 3#pragma warning disable CA1311 // Specify a culture or use an invariant version
 4#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 5
 6using System;
 7using System.Collections.Generic;
 8using System.Linq;
 9using System.Linq.Expressions;
 10using Jellyfin.Data.Enums;
 11using Jellyfin.Database.Implementations;
 12using Jellyfin.Database.Implementations.Entities;
 13using Jellyfin.Database.Implementations.Enums;
 14using Jellyfin.Extensions;
 15using MediaBrowser.Controller.Entities;
 16using MediaBrowser.Model.Entities;
 17using MediaBrowser.Model.Querying;
 18using Microsoft.EntityFrameworkCore;
 19using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 20
 21namespace Jellyfin.Server.Implementations.Item;
 22
 23public sealed partial class BaseItemRepository
 24{
 25    /// <inheritdoc />
 26    public IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 27    {
 46028        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 46029        dbQuery = dbQuery.AsSingleQuery();
 30
 46031        return dbQuery;
 32    }
 33
 34    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 35    {
 8036        dbQuery = TranslateQuery(dbQuery, context, filter);
 8037        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 8038        dbQuery = ApplyQueryPaging(dbQuery, filter);
 8039        dbQuery = ApplyNavigations(dbQuery, filter);
 8040        return dbQuery;
 41    }
 42
 43    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 44    {
 45445        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 46        {
 10547            var offset = filter.StartIndex ?? 0;
 48
 10549            if (offset > 0)
 50            {
 051                dbQuery = dbQuery.Skip(offset);
 52            }
 53
 10554            if (filter.Limit.HasValue)
 55            {
 10556                dbQuery = dbQuery.Take(filter.Limit.Value);
 57            }
 58        }
 59
 45460        return dbQuery;
 61    }
 62
 63    private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery
 64    {
 65        // Collapse duplicates sharing a presentation key (e.g. alternate versions) by picking
 66        // the min Id per group. Keep the grouped ids as an IQueryable sub-select; materializing
 67        // to a List would inline one bound parameter per id and hit SQLite's variable cap.
 45468        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 45469        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 70        {
 071            var groupedIds = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select
 072            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 73        }
 45474        else if (enableGroupByPresentationUniqueKey)
 75        {
 176            var groupedIds = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.Min(x => x.Id));
 177            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 78        }
 45379        else if (filter.GroupBySeriesPresentationUniqueKey)
 80        {
 081            var groupedIds = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.Min(x => x.Id));
 082            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 83        }
 84        else
 85        {
 45386            dbQuery = dbQuery.Distinct();
 87        }
 88
 45489        if (filter.CollapseBoxSetItems == true)
 90        {
 091            dbQuery = ApplyBoxSetCollapsing(context, dbQuery, filter.CollapseBoxSetItemTypes);
 92
 93            // Name filters run after collapse so BoxSets match by their own name, not a child's.
 094            dbQuery = ApplyNameFilters(dbQuery, filter);
 95        }
 96
 45497        dbQuery = ApplyOrder(dbQuery, filter, context);
 98
 45499        return dbQuery;
 100    }
 101
 102    private IQueryable<BaseItemEntity> ApplyBoxSetCollapsing(
 103        JellyfinDbContext context,
 104        IQueryable<BaseItemEntity> dbQuery,
 105        BaseItemKind[] collapsibleTypes)
 106    {
 0107        var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
 108
 0109        var currentIds = dbQuery.Select(e => e.Id);
 110
 0111        if (collapsibleTypes.Length == 0)
 112        {
 113            // Collapse all item types into box sets
 0114            return ApplyBoxSetCollapsingAll(context, currentIds, boxSetTypeName);
 115        }
 116
 117        // Only collapse specific item types, keep others untouched
 0118        var collapsibleTypeNames = collapsibleTypes.Select(t => _itemTypeLookup.BaseItemKindNames[t]).ToList();
 119
 120        // Categorize items in currentIds in a single pass to avoid multiple correlated EXISTS over BaseItems.
 0121        var categorized = context.BaseItems
 0122            .AsNoTracking()
 0123            .Where(bi => currentIds.Contains(bi.Id))
 0124            .Select(bi => new
 0125            {
 0126                bi.Id,
 0127                IsCollapsible = collapsibleTypeNames.Contains(bi.Type),
 0128                IsBoxSet = bi.Type == boxSetTypeName
 0129            });
 130
 0131        var collapsibleChildIds = categorized.Where(c => c.IsCollapsible).Select(c => c.Id);
 132
 133        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0134        var manualBoxSetLinks = context.LinkedChildren
 0135            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0136                && currentIds.Contains(lc.ChildId))
 0137            .Join(
 0138                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0139                lc => lc.ParentId,
 0140                bs => bs.Id,
 0141                (lc, bs) => new { lc.ChildId, lc.ParentId });
 142
 0143        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 144
 145        // Items whose type is NOT collapsible (always kept in results)
 0146        var nonCollapsibleIds = categorized.Where(c => !c.IsCollapsible).Select(c => c.Id);
 147
 148        // Collapsible items that are not a BoxSet themselves and not a manual child of any BoxSet
 0149        var collapsibleNotInBoxSet = categorized
 0150            .Where(c => c.IsCollapsible && !c.IsBoxSet)
 0151            .Select(c => c.Id)
 0152            .Where(id => !childrenInBoxSet.Contains(id));
 153
 154        // BoxSet IDs containing at least one collapsible child item from currentIds
 0155        var boxSetIds = manualBoxSetLinks
 0156            .Where(x => collapsibleChildIds.Contains(x.ChildId))
 0157            .Select(x => x.ParentId)
 0158            .Distinct();
 159
 0160        var collapsedIds = nonCollapsibleIds.Union(collapsibleNotInBoxSet).Union(boxSetIds);
 0161        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 162    }
 163
 164    private static IQueryable<BaseItemEntity> ApplyBoxSetCollapsingAll(
 165        JellyfinDbContext context,
 166        IQueryable<Guid> currentIds,
 167        string boxSetTypeName)
 168    {
 169        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0170        var manualBoxSetLinks = context.LinkedChildren
 0171            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0172                && currentIds.Contains(lc.ChildId))
 0173            .Join(
 0174                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0175                lc => lc.ParentId,
 0176                bs => bs.Id,
 0177                (lc, bs) => new { lc.ChildId, lc.ParentId });
 178
 0179        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 0180        var boxSetIds = manualBoxSetLinks.Select(x => x.ParentId).Distinct();
 181
 182        // Items in currentIds that are not BoxSets themselves and not a manual child of any BoxSet
 0183        var notInBoxSet = context.BaseItems
 0184            .AsNoTracking()
 0185            .Where(e => currentIds.Contains(e.Id) && e.Type != boxSetTypeName)
 0186            .Select(e => e.Id)
 0187            .Where(id => !childrenInBoxSet.Contains(id));
 188
 0189        var collapsedIds = notInBoxSet.Union(boxSetIds);
 0190        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 191    }
 192
 193    private static IQueryable<BaseItemEntity> ApplyNameFilters(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 194    {
 454195        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 196        {
 0197            var nameStartsWithLower = filter.NameStartsWith.ToLowerInvariant();
 0198            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().StartsWith(nameStartsWithLower));
 199        }
 200
 454201        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 202        {
 0203            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 0204            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(startsOrGreaterLower) >= 0);
 205        }
 206
 454207        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 208        {
 0209            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 0210            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(lessThanLower) < 0);
 211        }
 212
 454213        return dbQuery;
 214    }
 215
 216    /// <inheritdoc />
 217    public IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 218    {
 392219        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 220        {
 0221            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 222        }
 223
 392224        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 225        {
 391226            dbQuery = dbQuery.Include(e => e.Provider);
 227        }
 228
 392229        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 230        {
 391231            dbQuery = dbQuery.Include(e => e.LockedFields);
 232        }
 233
 392234        if (filter.DtoOptions.EnableUserData)
 235        {
 392236            dbQuery = dbQuery.Include(e => e.UserData);
 237        }
 238
 392239        if (filter.DtoOptions.EnableImages)
 240        {
 392241            dbQuery = dbQuery.Include(e => e.Images);
 242        }
 243
 244        // Include LinkedChildEntities for container types and videos that use them
 245        // (BoxSet, Playlist, CollectionFolder for manual linking; Video, Movie for alternate versions).
 246        // When IncludeItemTypes is empty (any type may be returned), always include them to ensure
 247        // LinkedChildren are loaded before items are saved back, preventing accidental deletion.
 392248        var linkedChildTypes = new[]
 392249        {
 392250            BaseItemKind.BoxSet,
 392251            BaseItemKind.Playlist,
 392252            BaseItemKind.CollectionFolder,
 392253            BaseItemKind.Video,
 392254            BaseItemKind.Movie
 392255        };
 392256        if (filter.IncludeItemTypes.Length == 0 || filter.IncludeItemTypes.Any(linkedChildTypes.Contains))
 257        {
 245258            dbQuery = dbQuery.Include(e => e.LinkedChildEntities);
 259        }
 260
 392261        if (filter.IncludeExtras)
 262        {
 0263            dbQuery = dbQuery.Include(e => e.Extras);
 264        }
 265
 392266        return dbQuery;
 267    }
 268
 269    /// <inheritdoc />
 270    public IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDb
 271    {
 454272        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 454273        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 274
 275        // SeriesDatePlayed requires special handling to avoid correlated subqueries.
 276        // Instead of running a MAX() subquery per-row in ORDER BY, we pre-aggregate
 277        // max played dates per series in one query and left-join it.
 454278        if (!hasSearch && orderBy.Any(o => o.OrderBy == ItemSortBy.SeriesDatePlayed))
 279        {
 0280            return ApplySeriesDatePlayedOrder(query, filter, context, orderBy);
 281        }
 282
 454283        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 284
 454285        if (hasSearch)
 286        {
 0287            var relevanceExpression = OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!);
 0288            orderedQuery = query.OrderBy(relevanceExpression);
 289        }
 290
 454291        if (orderBy.Length > 0)
 292        {
 112293            var firstOrdering = orderBy[0];
 112294            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 295
 112296            if (orderedQuery is null)
 297            {
 112298                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 112299                    ? query.OrderBy(expression)
 112300                    : query.OrderByDescending(expression);
 301            }
 302            else
 303            {
 0304                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 0305                    ? orderedQuery.ThenBy(expression)
 0306                    : orderedQuery.ThenByDescending(expression);
 307            }
 308
 112309            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 310            {
 6311                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 6312                    ? orderedQuery.ThenBy(e => e.Name)
 6313                    : orderedQuery.ThenByDescending(e => e.Name);
 314            }
 315
 310316            foreach (var item in orderBy.Skip(1))
 317            {
 43318                expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 43319                orderedQuery = item.SortOrder == SortOrder.Ascending
 43320                    ? orderedQuery.ThenBy(expression)
 43321                    : orderedQuery.ThenByDescending(expression);
 322            }
 323        }
 324
 454325        if (orderedQuery is null)
 326        {
 342327            return query.OrderBy(e => e.SortName);
 328        }
 329
 330        // Add SortName as final tiebreaker
 112331        if (!hasSearch && (orderBy.Length == 0 || orderBy.All(o => o.OrderBy is not ItemSortBy.SortName and not ItemSort
 332        {
 63333            orderedQuery = orderedQuery.ThenBy(e => e.SortName);
 334        }
 335
 112336        return orderedQuery;
 337    }
 338
 339    private IQueryable<BaseItemEntity> ApplySeriesDatePlayedOrder(
 340        IQueryable<BaseItemEntity> query,
 341        InternalItemsQuery filter,
 342        JellyfinDbContext context,
 343        (ItemSortBy OrderBy, SortOrder SortOrder)[] orderBy)
 344    {
 345        // Pre-aggregate max played date per series key in ONE query.
 346        // This generates a single: SELECT SeriesPresentationUniqueKey, MAX(LastPlayedDate) ... GROUP BY
 347        // instead of a correlated subquery per outer row.
 0348        IQueryable<UserData> userDataQuery = filter.User is not null
 0349            ? context.UserData.Where(ud => ud.UserId == filter.User.Id && ud.Played)
 0350            : context.UserData.Where(ud => ud.Played);
 351
 0352        var seriesMaxDates = userDataQuery
 0353            .Join(
 0354                context.BaseItems,
 0355                ud => ud.ItemId,
 0356                bi => bi.Id,
 0357                (ud, bi) => new { bi.SeriesPresentationUniqueKey, ud.LastPlayedDate })
 0358            .Where(x => x.SeriesPresentationUniqueKey != null)
 0359            .GroupBy(x => x.SeriesPresentationUniqueKey)
 0360            .Select(g => new { SeriesKey = g.Key!, MaxDate = g.Max(x => x.LastPlayedDate) });
 361
 0362        var joined = query.LeftJoin(
 0363            seriesMaxDates,
 0364            e => e.PresentationUniqueKey,
 0365            s => s.SeriesKey,
 0366            (e, s) => new { Item = e, MaxDate = s != null ? s.MaxDate : (DateTime?)null });
 367
 0368        var seriesSort = orderBy.First(o => o.OrderBy == ItemSortBy.SeriesDatePlayed);
 369
 0370        return seriesSort.SortOrder == SortOrder.Ascending
 0371            ? joined.OrderBy(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item)
 0372            : joined.OrderByDescending(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item);
 373    }
 374
 375    /// <summary>
 376    /// Builds a query for descendants of an ancestor with user access filtering applied.
 377    /// Uses recursive CTE to traverse both hierarchical (AncestorIds) and linked (LinkedChildren) relationships.
 378    /// </summary>
 379    /// <inheritdoc />
 380    public IQueryable<BaseItemEntity> BuildAccessFilteredDescendantsQuery(
 381        JellyfinDbContext context,
 382        InternalItemsQuery filter,
 383        Guid ancestorId)
 384    {
 385        // Use recursive CTE to get all descendants (hierarchical and linked)
 0386        var allDescendantIds = DescendantQueryHelper.GetAllDescendantIds(context, ancestorId);
 387
 0388        var baseQuery = context.BaseItems
 0389            .AsNoTracking()
 0390            .Where(b => allDescendantIds.Contains(b.Id) && !b.IsFolder && !b.IsVirtualItem);
 391
 0392        return ApplyAccessFiltering(context, baseQuery, filter);
 393    }
 394
 395    /// <summary>
 396    /// Applies user access filtering to a query.
 397    /// Includes TopParentIds, parental rating, and tag filtering.
 398    /// </summary>
 399    /// <inheritdoc />
 400    public IQueryable<BaseItemEntity> ApplyAccessFiltering(
 401        JellyfinDbContext context,
 402        IQueryable<BaseItemEntity> baseQuery,
 403        InternalItemsQuery filter)
 404    {
 405        // Apply TopParentIds filtering (library folder access)
 0406        if (filter.TopParentIds.Length > 0)
 407        {
 0408            var topParentIds = filter.TopParentIds;
 0409            baseQuery = baseQuery.Where(e => topParentIds.Contains(e.TopParentId!.Value));
 410        }
 411
 412        // Apply parental rating filtering
 0413        if (filter.MaxParentalRating is not null)
 414        {
 0415            baseQuery = baseQuery.Where(BuildMaxParentalRatingFilter(context, filter.MaxParentalRating));
 416        }
 417
 418        // Apply block unrated items filtering
 0419        if (filter.BlockUnratedItems.Length > 0)
 420        {
 0421            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0422            baseQuery = baseQuery.Where(e =>
 0423                e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType));
 424        }
 425
 426        // Apply excluded tags filtering (blocked tags).
 427        // Pre-build the blocked-item-id set as a sub-select; then four index-seek Contains checks
 428        // instead of one EXISTS over a 4-way OR predicate that defeats index seeks.
 0429        if (filter.ExcludeInheritedTags.Length > 0)
 430        {
 0431            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0432            var blockedTagItemIds = context.ItemValuesMap
 0433                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 0434                .Select(f => f.ItemId);
 435
 0436            baseQuery = baseQuery.Where(e =>
 0437                !blockedTagItemIds.Contains(e.Id)
 0438                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 0439                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 0440                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 441        }
 442
 443        // Apply included tags filtering (allowed tags - item must have at least one).
 0444        if (filter.IncludeInheritedTags.Length > 0)
 445        {
 0446            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0447            var allowedTagItemIds = context.ItemValuesMap
 0448                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 0449                .Select(f => f.ItemId);
 450
 0451            baseQuery = baseQuery.Where(e =>
 0452                allowedTagItemIds.Contains(e.Id)
 0453                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 0454                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 0455                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value)));
 456        }
 457
 458        // Exclude alternate versions (have PrimaryVersionId set) and owned non-extra items.
 459        // Extras (trailers, etc.) have OwnerId set but also have ExtraType set — keep those.
 0460        if (!filter.IncludeOwnedItems)
 461        {
 0462            baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 463        }
 464
 0465        return baseQuery;
 466    }
 467
 468    /// <summary>
 469    /// Builds a filter expression for max parental rating that handles both rated items
 470    /// and unrated BoxSets/Playlists (which check linked children's ratings).
 471    /// </summary>
 472    private static Expression<Func<BaseItemEntity, bool>> BuildMaxParentalRatingFilter(
 473        JellyfinDbContext context,
 474        ParentalRatingScore maxRating)
 475    {
 48476        var maxScore = maxRating.Score;
 48477        var maxSubScore = maxRating.SubScore ?? 0;
 48478        var linkedChildren = context.LinkedChildren;
 479
 48480        return e =>
 48481            // Item has a rating: check against limit
 48482            (e.InheritedParentalRatingValue != null
 48483                && (e.InheritedParentalRatingValue < maxScore
 48484                    || (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSub
 48485            // Item has no rating
 48486            || (e.InheritedParentalRatingValue == null
 48487                && (
 48488                    // No linked children (not a BoxSet/Playlist): pass as unrated
 48489                    !linkedChildren.Any(lc => lc.ParentId == e.Id)
 48490                    // Has linked children: at least one child must be within limits
 48491                    || linkedChildren.Any(lc => lc.ParentId == e.Id
 48492                        && (lc.Child!.InheritedParentalRatingValue == null
 48493                            || lc.Child.InheritedParentalRatingValue < maxScore
 48494                            || (lc.Child.InheritedParentalRatingValue == maxScore
 48495                                && (lc.Child.InheritedParentalRatingSubValue ?? 0) <= maxSubScore)))));
 496    }
 497
 498    /// <inheritdoc />
 499    public IQueryable<Guid> GetFullyPlayedFolderIdsQuery(JellyfinDbContext context, IQueryable<Guid> folderIds, User use
 500    {
 0501        ArgumentNullException.ThrowIfNull(context);
 0502        ArgumentNullException.ThrowIfNull(folderIds);
 0503        ArgumentNullException.ThrowIfNull(user);
 504
 0505        var filter = new InternalItemsQuery(user);
 0506        var userId = user.Id;
 507
 0508        var leafItems = context.BaseItems
 0509            .AsNoTracking()
 0510            .Where(b => !b.IsFolder && !b.IsVirtualItem);
 0511        leafItems = ApplyAccessFiltering(context, leafItems, filter);
 512
 0513        var playedLeafItems = leafItems
 0514            .Select(b => new { b.Id, Played = b.UserData!.Any(ud => ud.UserId == userId && ud.Played) });
 515
 0516        var ancestorLeaves = context.AncestorIds
 0517            .Where(a => folderIds.Contains(a.ParentItemId))
 0518            .Join(
 0519                playedLeafItems,
 0520                a => a.ItemId,
 0521                b => b.Id,
 0522                (a, b) => new { FolderId = a.ParentItemId, b.Id, b.Played });
 523
 0524        var linkedLeaves = context.LinkedChildren
 0525            .Where(lc => folderIds.Contains(lc.ParentId))
 0526            .Join(
 0527                playedLeafItems,
 0528                lc => lc.ChildId,
 0529                b => b.Id,
 0530                (lc, b) => new { FolderId = lc.ParentId, b.Id, b.Played });
 531
 0532        var linkedFolderLeaves = context.LinkedChildren
 0533            .Where(lc => folderIds.Contains(lc.ParentId))
 0534            .Join(
 0535                context.BaseItems.Where(b => b.IsFolder),
 0536                lc => lc.ChildId,
 0537                b => b.Id,
 0538                (lc, b) => new { lc.ParentId, FolderChildId = b.Id })
 0539            .Join(
 0540                context.AncestorIds,
 0541                x => x.FolderChildId,
 0542                a => a.ParentItemId,
 0543                (x, a) => new { x.ParentId, DescendantId = a.ItemId })
 0544            .Join(
 0545                playedLeafItems,
 0546                x => x.DescendantId,
 0547                b => b.Id,
 0548                (x, b) => new { FolderId = x.ParentId, b.Id, b.Played });
 549
 0550        return ancestorLeaves
 0551            .Union(linkedLeaves)
 0552            .Union(linkedFolderLeaves)
 0553            .GroupBy(x => x.FolderId)
 0554            .Where(g => g.Select(x => x.Id).Distinct().Count() == g.Where(x => x.Played).Select(x => x.Id).Distinct().Co
 0555            .Select(g => g.Key);
 556    }
 557}

/srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Linq;
 6using System.Threading.Tasks;
 7using Jellyfin.Data.Enums;
 8using Jellyfin.Database.Implementations;
 9using Jellyfin.Database.Implementations.Entities;
 10using Jellyfin.Extensions;
 11using MediaBrowser.Controller.Entities;
 12using MediaBrowser.Model.Querying;
 13using Microsoft.EntityFrameworkCore;
 14using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 15
 16namespace Jellyfin.Server.Implementations.Item;
 17
 18public sealed partial class BaseItemRepository
 19{
 20    /// <inheritdoc />
 21    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 22    {
 8023        ArgumentNullException.ThrowIfNull(filter);
 8024        PrepareFilterQuery(filter);
 25
 8026        using var context = _dbProvider.CreateDbContext();
 8027        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 8028    }
 29
 30    /// <inheritdoc />
 31    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
 32    {
 133        ArgumentNullException.ThrowIfNull(filter);
 134        if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
 35        {
 136            var returnList = GetItemList(filter);
 137            return new QueryResult<BaseItemDto>(
 138                filter.StartIndex,
 139                returnList.Count,
 140                returnList);
 41        }
 42
 043        PrepareFilterQuery(filter);
 044        var result = new QueryResult<BaseItemDto>();
 45
 046        using var context = _dbProvider.CreateDbContext();
 47
 048        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 49
 050        dbQuery = TranslateQuery(dbQuery, context, filter);
 051        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 52
 053        if (filter.EnableTotalRecordCount)
 54        {
 055            result.TotalRecordCount = dbQuery.Count();
 56        }
 57
 058        dbQuery = ApplyQueryPaging(dbQuery, filter);
 059        dbQuery = ApplyNavigations(dbQuery, filter);
 60
 061        result.Items = dbQuery.AsEnumerable().Where(e => e != null).Select(w => DeserializeBaseItem(w, filter.SkipDeseri
 062        result.StartIndex = filter.StartIndex ?? 0;
 063        return result;
 064    }
 65
 66    /// <inheritdoc />
 67    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
 68    {
 37469        ArgumentNullException.ThrowIfNull(filter);
 37470        PrepareFilterQuery(filter);
 71
 37472        using var context = _dbProvider.CreateDbContext();
 37473        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 74
 37475        dbQuery = TranslateQuery(dbQuery, context, filter);
 76
 37477        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 37478        dbQuery = ApplyQueryPaging(dbQuery, filter);
 79
 37480        var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
 37481        if (hasRandomSort)
 82        {
 6283            var orderedIds = dbQuery.AsNoTracking().Select(e => e.Id).ToList();
 6284            if (orderedIds.Count == 0)
 85            {
 6286                return Array.Empty<BaseItemDto>();
 87            }
 88
 089            var itemsById = ApplyNavigations(context.BaseItems.AsNoTracking().WhereOneOrMany(orderedIds, e => e.Id), fil
 090                .AsSplitQuery()
 091                .AsEnumerable()
 092                .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 093                .Where(dto => dto != null)
 094                .ToDictionary(i => i!.Id);
 95
 096            return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!;
 97        }
 98
 31299        dbQuery = ApplyNavigations(dbQuery, filter);
 100
 312101        return dbQuery.AsEnumerable().Where(e => e != null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializatio
 374102    }
 103
 104    /// <inheritdoc/>
 105    public IReadOnlyList<BaseItemDto> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 106    {
 0107        ArgumentNullException.ThrowIfNull(filter);
 0108        PrepareFilterQuery(filter);
 109
 110        // Early exit if collection type is not supported
 0111        if (collectionType is not CollectionType.movies and not CollectionType.tvshows and not CollectionType.music)
 112        {
 0113            return [];
 114        }
 115
 0116        var limit = filter.Limit;
 0117        using var context = _dbProvider.CreateDbContext();
 118
 0119        var baseQuery = PrepareItemQuery(context, filter);
 0120        baseQuery = TranslateQuery(baseQuery, context, filter);
 121
 0122        if (collectionType == CollectionType.tvshows)
 123        {
 0124            return GetLatestTvShowItems(context, baseQuery, filter, limit);
 125        }
 126
 0127        if (collectionType is CollectionType.movies)
 128        {
 129            // Group by PresentationUniqueKey, pick the newest item per group.
 0130            var topGroupItems = baseQuery
 0131                .Where(e => e.PresentationUniqueKey != null)
 0132                .GroupBy(e => e.PresentationUniqueKey)
 0133                .Select(g => new
 0134                {
 0135                    MaxDate = g.Max(e => e.DateCreated),
 0136                    FirstId = g.OrderByDescending(e => e.DateCreated).ThenByDescending(e => e.Id).Select(e => e.Id).Firs
 0137                })
 0138                .OrderByDescending(g => g.MaxDate);
 139
 0140            var firstIdsQuery = filter.Limit.HasValue
 0141                ? topGroupItems.Take(filter.Limit.Value).Select(g => g.FirstId)
 0142                : topGroupItems.Select(g => g.FirstId);
 143
 0144            return LoadLatestByIds(context, firstIdsQuery, filter);
 145        }
 146
 147        // Albums whose Id is the parent of any track matching the user's filter.
 0148        var albumIdsWithMatchingTrack = context.AncestorIds
 0149            .Join(baseQuery, ai => ai.ItemId, t => t.Id, (ai, _) => ai.ParentItemId);
 150
 0151        var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]!;
 0152        var topAlbumsQuery = context.BaseItems.AsNoTracking()
 0153            .Where(album => album.Type == musicAlbumTypeName)
 0154            .Where(album => albumIdsWithMatchingTrack.Contains(album.Id))
 0155            .OrderByDescending(album => album.DateCreated)
 0156            .ThenByDescending(album => album.Id);
 157
 0158        var albumIdsQuery = filter.Limit.HasValue
 0159            ? topAlbumsQuery.Take(filter.Limit.Value).Select(a => a.Id)
 0160            : topAlbumsQuery.Select(a => a.Id);
 161
 0162        return LoadLatestByIds(context, albumIdsQuery, filter);
 0163    }
 164
 165    // Keeping idsQuery deferred lets EF emit `WHERE Id IN (<subquery>)`.
 166    private IReadOnlyList<BaseItemDto> LoadLatestByIds(
 167        JellyfinDbContext context,
 168        IQueryable<Guid> idsQuery,
 169        InternalItemsQuery filter)
 170    {
 0171        var itemsQuery = ApplyNavigations(
 0172            context.BaseItems.AsNoTracking().Where(e => idsQuery.Contains(e.Id)),
 0173            filter);
 174
 0175        return itemsQuery
 0176            .OrderByDescending(e => e.DateCreated)
 0177            .ThenByDescending(e => e.Id)
 0178            .AsEnumerable()
 0179            .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 0180            .Where(dto => dto != null)
 0181            .ToArray()!;
 182    }
 183
 184    /// <summary>
 185    /// Gets the latest TV show items with smart Season/Series container selection.
 186    /// </summary>
 187    /// <remarks>
 188    /// <para>
 189    /// This method implements intelligent container selection for TV shows in the "Latest" section.
 190    /// Instead of always showing individual episodes, it analyzes recent additions and may return
 191    /// a Season or Series container when multiple related episodes were recently added.
 192    /// </para>
 193    /// <para>
 194    /// The selection logic is:
 195    /// <list type="bullet">
 196    ///     <item>If recent episodes span multiple seasons → return the Series</item>
 197    ///     <item>If multiple recent episodes are from one season AND the series has multiple seasons → return the Seaso
 198    ///     <item>If multiple recent episodes are from one season AND the series has only one season → return the Series
 199    ///     <item>Otherwise → return the most recent Episode</item>
 200    /// </list>
 201    /// </para>
 202    /// </remarks>
 203    /// <param name="context">The database context.</param>
 204    /// <param name="baseQuery">The base query with filters already applied.</param>
 205    /// <param name="filter">The query filter options.</param>
 206    /// <param name="limit">Maximum number of items to return.</param>
 207    /// <returns>A list of BaseItemDto representing the latest TV content.</returns>
 208    private IReadOnlyList<BaseItemDto> GetLatestTvShowItems(JellyfinDbContext context, IQueryable<BaseItemEntity> baseQu
 209    {
 210        // Episodes added within this window are considered "recently added together"
 211        const double RecentAdditionWindowHours = 24.0;
 212
 213        // Step 1: Find the top N series with recently added content, ordered by most recent addition
 0214        var topSeriesWithDates = baseQuery
 0215            .Where(e => e.SeriesName != null)
 0216            .GroupBy(e => e.SeriesName)
 0217            .Select(g => new { SeriesName = g.Key!, MaxDate = g.Max(e => e.DateCreated) })
 0218            .OrderByDescending(g => g.MaxDate);
 219
 0220        if (limit.HasValue)
 221        {
 0222            topSeriesWithDates = topSeriesWithDates.Take(limit.Value).OrderByDescending(g => g.MaxDate);
 223        }
 224
 225        // Materialize series names and cutoff to avoid embedding the GroupBy+OrderBy
 226        // expression tree as a subquery inside the episode query.
 0227        var topSeriesData = topSeriesWithDates
 0228            .Select(g => new { g.SeriesName, g.MaxDate })
 0229            .ToList();
 0230        var topSeriesNames = topSeriesData.Select(g => g.SeriesName).ToList();
 231
 232        // Compute a global date cutoff: the oldest series' max date minus the window.
 233        // Episodes before this cutoff cannot be in any series' "recent additions" window,
 234        // so we can safely exclude them to avoid loading ancient episodes.
 0235        var globalCutoff = topSeriesData.Count > 0
 0236            ? topSeriesData.Min(g => g.MaxDate)?.AddHours(-RecentAdditionWindowHours)
 0237            : null;
 238
 239        // Restrict to episodes of the top series, optionally bounded by the global cutoff.
 0240        var episodeQuery = baseQuery.Where(e => e.SeriesName != null && topSeriesNames.Contains(e.SeriesName));
 0241        if (globalCutoff is not null)
 242        {
 0243            episodeQuery = episodeQuery.Where(e => e.DateCreated >= globalCutoff);
 244        }
 245
 246        // Lightweight projection: only the columns needed for the in-memory analysis below.
 0247        var allEpisodes = episodeQuery
 0248            .OrderByDescending(e => e.DateCreated)
 0249            .ThenByDescending(e => e.Id)
 0250            .Select(e => new { e.Id, e.SeriesName, e.DateCreated, e.SeasonId, e.SeriesId })
 0251            .AsEnumerable();
 252
 253        // Collect all season/series IDs we'll need to look up for count information
 0254        var allSeasonIds = new HashSet<Guid>();
 0255        var allSeriesIds = new HashSet<Guid>();
 256
 257        // Analysis data for each series: recent episode count, season IDs, and the most recent episode ID
 0258        var analysisData = new List<(
 0259            int RecentEpisodeCount,
 0260            List<Guid> SeasonIds,
 0261            Guid? FirstRecentSeriesId,
 0262            DateTime MaxDate,
 0263            Guid MostRecentEpisodeId)>();
 264
 265        // Step 3: Analyze each series to identify recent additions within the time window
 0266        foreach (var group in allEpisodes.GroupBy(e => e.SeriesName))
 267        {
 0268            var episodes = group.ToList();
 0269            var mostRecentDate = episodes[0].DateCreated ?? DateTime.MinValue;
 0270            var recentCutoff = mostRecentDate.AddHours(-RecentAdditionWindowHours);
 271
 272            // Find episodes added within the recent window
 0273            var recentEpisodeCount = 0;
 0274            var seasonIdSet = new HashSet<Guid>();
 0275            Guid? firstRecentSeriesId = null;
 276
 0277            foreach (var ep in episodes)
 278            {
 0279                if (ep.DateCreated >= recentCutoff)
 280                {
 0281                    recentEpisodeCount++;
 0282                    if (ep.SeasonId.HasValue)
 283                    {
 0284                        seasonIdSet.Add(ep.SeasonId.Value);
 285                    }
 286
 0287                    firstRecentSeriesId ??= ep.SeriesId;
 288                }
 289            }
 290
 0291            var seasonIds = seasonIdSet.ToList();
 0292            analysisData.Add((recentEpisodeCount, seasonIds, firstRecentSeriesId, mostRecentDate, episodes[0].Id));
 293
 294            // Track all unique season/series IDs for batch lookups
 0295            foreach (var sid in seasonIds)
 296            {
 0297                allSeasonIds.Add(sid);
 298            }
 299
 0300            if (firstRecentSeriesId.HasValue)
 301            {
 0302                allSeriesIds.Add(firstRecentSeriesId.Value);
 303            }
 304        }
 305
 306        // Step 4: Batch fetch counts - episodes per season and seasons per series
 307        // These counts help determine whether to show Season or Series as the container
 0308        var episodeType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 0309        var seasonType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Season];
 0310        var seasonEpisodeCounts = allSeasonIds.Count > 0
 0311            ? context.BaseItems
 0312                .AsNoTracking()
 0313                .Where(e => e.SeasonId.HasValue && allSeasonIds.Contains(e.SeasonId.Value) && e.Type == episodeType)
 0314                .GroupBy(e => e.SeasonId!.Value)
 0315                .Select(g => new { SeasonId = g.Key, Count = g.Count() })
 0316                .ToDictionary(x => x.SeasonId, x => x.Count)
 0317            : [];
 318
 0319        var seriesSeasonCounts = allSeriesIds.Count > 0
 0320            ? context.BaseItems
 0321                .AsNoTracking()
 0322                .Where(e => e.SeriesId.HasValue && allSeriesIds.Contains(e.SeriesId.Value) && e.Type == seasonType)
 0323                .GroupBy(e => e.SeriesId!.Value)
 0324                .Select(g => new { SeriesId = g.Key, Count = g.Count() })
 0325                .ToDictionary(x => x.SeriesId, x => x.Count)
 0326            : [];
 327
 328        // Step 5: Apply the container selection logic for each series.
 329        // For each series, decide which entity best represents the recent additions:
 330        //   - 1 episode added → show the Episode itself
 331        //   - Multiple episodes in 1 season (multi-season series) → show the Season
 332        //   - Multiple episodes in 1 season (single-season series) → show the Series
 333        //   - Episodes across multiple seasons → show the Series
 0334        var entitiesToFetch = new HashSet<Guid>();
 0335        var seriesResults = new List<(Guid? SeasonId, Guid? SeriesId, DateTime MaxDate, Guid MostRecentEpisodeId)>(analy
 336
 0337        foreach (var (recentEpisodeCount, seasonIds, firstRecentSeriesId, maxDate, mostRecentEpisodeId) in analysisData)
 338        {
 0339            Guid? seasonId = null;
 0340            Guid? seriesId = null;
 341
 0342            if (seasonIds.Count == 1)
 343            {
 344                // All recent episodes are from a single season
 0345                var sid = seasonIds[0];
 0346                var totalEpisodes = seasonEpisodeCounts.GetValueOrDefault(sid, 0);
 0347                var totalSeasonsInSeries = firstRecentSeriesId.HasValue
 0348                    ? seriesSeasonCounts.GetValueOrDefault(firstRecentSeriesId.Value, 1)
 0349                    : 1;
 350
 351                // Check if multiple episodes were added, or if all episodes in the season were added
 0352                var hasMultipleOrAllEpisodes = recentEpisodeCount > 1 || recentEpisodeCount == totalEpisodes;
 353
 0354                if (totalSeasonsInSeries > 1 && hasMultipleOrAllEpisodes)
 355                {
 356                    // Multi-season series with bulk additions: show the Season
 0357                    seasonId = sid;
 0358                    entitiesToFetch.Add(sid);
 359                }
 0360                else if (hasMultipleOrAllEpisodes && firstRecentSeriesId.HasValue)
 361                {
 362                    // Single-season series with bulk additions: show the Series
 0363                    seriesId = firstRecentSeriesId;
 0364                    entitiesToFetch.Add(firstRecentSeriesId.Value);
 365                }
 366
 367                // Otherwise: single episode, will fall through to show the Episode
 368            }
 0369            else if (seasonIds.Count > 1 && firstRecentSeriesId.HasValue)
 370            {
 371                // Recent episodes span multiple seasons: show the Series
 0372                seriesId = firstRecentSeriesId;
 0373                entitiesToFetch.Add(seriesId!.Value);
 374            }
 375
 0376            if (seasonId is null && seriesId is null)
 377            {
 0378                entitiesToFetch.Add(mostRecentEpisodeId);
 379            }
 380
 0381            seriesResults.Add((seasonId, seriesId, maxDate, mostRecentEpisodeId));
 382        }
 383
 384        // Step 6: Fetch the Season/Series entities we decided to return
 0385        var entities = entitiesToFetch.Count > 0
 0386            ? ApplyNavigations(
 0387                    context.BaseItems.AsNoTracking().Where(e => entitiesToFetch.Contains(e.Id)),
 0388                    filter)
 0389                .AsSingleQuery()
 0390                .ToDictionary(e => e.Id)
 0391            : [];
 392
 393        // Step 7: Build final results, preferring Season > Series > Episode.
 394        // All needed entities are already fetched in step 6 with navigation properties.
 0395        var results = new List<(BaseItemEntity Entity, DateTime MaxDate)>(seriesResults.Count);
 0396        foreach (var (seasonId, seriesId, maxDate, mostRecentEpisodeId) in seriesResults)
 397        {
 0398            if (seasonId.HasValue && entities.TryGetValue(seasonId.Value, out var seasonEntity))
 399            {
 0400                results.Add((seasonEntity, maxDate));
 0401                continue;
 402            }
 403
 0404            if (seriesId.HasValue && entities.TryGetValue(seriesId.Value, out var seriesEntity))
 405            {
 0406                results.Add((seriesEntity, maxDate));
 0407                continue;
 408            }
 409
 0410            if (entities.TryGetValue(mostRecentEpisodeId, out var episodeEntity))
 411            {
 0412                results.Add((episodeEntity, maxDate));
 413            }
 414        }
 415
 0416        var finalResults = results
 0417            .OrderByDescending(r => r.MaxDate)
 0418            .ThenByDescending(r => r.Entity.Id);
 419
 0420        if (limit.HasValue)
 421        {
 0422            finalResults = finalResults
 0423            .Take(limit.Value)
 0424            .OrderByDescending(r => r.MaxDate)
 0425            .ThenByDescending(r => r.Entity.Id);
 426        }
 427
 0428        return finalResults
 0429            .Select(r => DeserializeBaseItem(r.Entity, filter.SkipDeserialization))
 0430            .Where(dto => dto is not null)
 0431            .ToArray()!;
 432    }
 433
 434    /// <inheritdoc/>
 435    public async Task<bool> ItemExistsAsync(Guid id)
 436    {
 24437        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 24438        await using (dbContext.ConfigureAwait(false))
 439        {
 24440            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 441        }
 24442    }
 443
 444    /// <inheritdoc  />
 445    public BaseItemDto? RetrieveItem(Guid id)
 446    {
 86447        if (id.IsEmpty())
 448        {
 0449            throw new ArgumentException("Guid can't be empty", nameof(id));
 450        }
 451
 86452        using var context = _dbProvider.CreateDbContext();
 86453        var dbQuery = PrepareItemQuery(context, new()
 86454        {
 86455            DtoOptions = new()
 86456            {
 86457                EnableImages = true
 86458            }
 86459        });
 86460        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 86461            .Include(e => e.Provider)
 86462            .Include(e => e.LockedFields)
 86463            .Include(e => e.UserData)
 86464            .Include(e => e.Images)
 86465            .Include(e => e.LinkedChildEntities)
 86466            .AsSingleQuery();
 467
 86468        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 86469        if (item is null)
 470        {
 86471            return null;
 472        }
 473
 0474        return DeserializeBaseItem(item);
 86475    }
 476
 477    /// <inheritdoc />
 478    public bool GetIsPlayed(User user, Guid id, bool recursive)
 479    {
 0480        using var dbContext = _dbProvider.CreateDbContext();
 481
 0482        if (recursive)
 483        {
 0484            var descendantIds = DescendantQueryHelper.GetAllDescendantIds(dbContext, id);
 485
 0486            return dbContext.BaseItems
 0487                    .Where(e => descendantIds.Contains(e.Id) && !e.IsFolder && !e.IsVirtualItem)
 0488                    .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
 489        }
 490
 0491        return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.P
 0492    }
 493
 494    /// <inheritdoc />
 495    public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery filter)
 496    {
 0497        ArgumentNullException.ThrowIfNull(filter);
 0498        PrepareFilterQuery(filter);
 499
 0500        using var context = _dbProvider.CreateDbContext();
 0501        var baseQuery = PrepareItemQuery(context, filter);
 0502        baseQuery = TranslateQuery(baseQuery, context, filter);
 503
 0504        var matchingItemIds = baseQuery.Select(e => e.Id);
 505
 0506        var years = baseQuery
 0507            .Where(e => e.ProductionYear != null && e.ProductionYear > 0)
 0508            .Select(e => e.ProductionYear!.Value)
 0509            .Distinct()
 0510            .OrderBy(y => y)
 0511            .ToArray();
 512
 0513        var officialRatings = baseQuery
 0514            .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty)
 0515            .Select(e => e.OfficialRating!)
 0516            .Distinct()
 0517            .OrderBy(r => r)
 0518            .ToArray();
 519
 0520        var tags = context.ItemValuesMap
 0521            .Where(ivm => ivm.ItemValue.Type == ItemValueType.Tags)
 0522            .Where(ivm => matchingItemIds.Contains(ivm.ItemId))
 0523            .Select(ivm => ivm.ItemValue)
 0524            .GroupBy(iv => iv.CleanValue)
 0525            .Select(g => g.Min(iv => iv.Value))
 0526            .OrderBy(t => t)
 0527            .ToArray();
 528
 0529        var genres = context.ItemValuesMap
 0530            .Where(ivm => ivm.ItemValue.Type == ItemValueType.Genre)
 0531            .Where(ivm => matchingItemIds.Contains(ivm.ItemId))
 0532            .Select(ivm => ivm.ItemValue)
 0533            .GroupBy(iv => iv.CleanValue)
 0534            .Select(g => g.Min(iv => iv.Value))
 0535            .OrderBy(g => g)
 0536            .ToArray();
 537
 0538        return new QueryFiltersLegacy
 0539        {
 0540            Years = years,
 0541            OfficialRatings = officialRatings,
 0542            Tags = tags,
 0543            Genres = genres
 0544        };
 0545    }
 546}

/srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2#pragma warning disable CA1304 // Specify CultureInfo
 3#pragma warning disable CA1309 // Use ordinal string comparison
 4#pragma warning disable CA1311 // Specify a culture or use an invariant version
 5#pragma warning disable CA1307 // Specify StringComparison for clarity
 6#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 7
 8using System;
 9using System.Collections.Generic;
 10using System.Globalization;
 11using System.Linq;
 12using System.Linq.Expressions;
 13using Jellyfin.Data.Enums;
 14using Jellyfin.Database.Implementations;
 15using Jellyfin.Database.Implementations.Entities;
 16using Jellyfin.Database.Implementations.MatchCriteria;
 17using Jellyfin.Extensions;
 18using Jellyfin.Server.Implementations.Extensions;
 19using MediaBrowser.Controller.Entities;
 20using MediaBrowser.Model.Entities;
 21using Microsoft.EntityFrameworkCore;
 22using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 23
 24namespace Jellyfin.Server.Implementations.Item;
 25
 26public sealed partial class BaseItemRepository
 27{
 128    private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
 29
 130    private static readonly string ImdbProviderName = MetadataProvider.Imdb.ToString().ToLowerInvariant();
 131    private static readonly string TmdbProviderName = MetadataProvider.Tmdb.ToString().ToLowerInvariant();
 132    private static readonly string TvdbProviderName = MetadataProvider.Tvdb.ToString().ToLowerInvariant();
 33
 34    /// <inheritdoc />
 35    public IQueryable<BaseItemEntity> TranslateQuery(
 36        IQueryable<BaseItemEntity> baseQuery,
 37        JellyfinDbContext context,
 38        InternalItemsQuery filter)
 39    {
 40        const int HDWidth = 1200;
 41        const int UHDWidth = 3800;
 42        const int UHDHeight = 2100;
 43
 45444        var minWidth = filter.MinWidth;
 45445        var maxWidth = filter.MaxWidth;
 45446        var now = DateTime.UtcNow;
 47
 45448        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 49        {
 050            bool includeSD = false;
 051            bool includeHD = false;
 052            bool include4K = false;
 53
 054            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 55            {
 056                includeSD = true;
 57            }
 58
 059            if (filter.IsHD.HasValue && filter.IsHD.Value)
 60            {
 061                includeHD = true;
 62            }
 63
 064            if (filter.Is4K.HasValue && filter.Is4K.Value)
 65            {
 066                include4K = true;
 67            }
 68
 69            // Non-folders: check own resolution directly (no subquery).
 70            // Folders (Series, BoxSets): EXISTS check on descendants/linked children.
 71            // Using navigation properties (a.Item, lc.Child) produces efficient
 72            // EXISTS + JOIN instead of nested IN (SELECT ...) subqueries.
 073            baseQuery = baseQuery.Where(e =>
 074                (!e.IsFolder && e.Width > 0
 075                    && ((includeSD && e.Width < HDWidth)
 076                        || (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight))
 077                        || (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight))))
 078                || (e.IsFolder
 079                    && (e.Children!.Any(a =>
 080                            a.Item.Width > 0
 081                            && ((includeSD && a.Item.Width < HDWidth)
 082                                || (includeHD && a.Item.Width >= HDWidth && !(a.Item.Width >= UHDWidth || a.Item.Height 
 083                                || (include4K && (a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight))))
 084                        || context.LinkedChildren.Any(lc =>
 085                            lc.ParentId == e.Id
 086                            && lc.Child!.Width > 0
 087                            && ((includeSD && lc.Child.Width < HDWidth)
 088                                || (includeHD && lc.Child.Width >= HDWidth && !(lc.Child.Width >= UHDWidth || lc.Child.H
 089                                || (include4K && (lc.Child.Width >= UHDWidth || lc.Child.Height >= UHDHeight)))))));
 90        }
 91
 45492        if (minWidth.HasValue)
 93        {
 094            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 95        }
 96
 45497        if (filter.MinHeight.HasValue)
 98        {
 099            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 100        }
 101
 454102        if (maxWidth.HasValue)
 103        {
 0104            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 105        }
 106
 454107        if (filter.MaxHeight.HasValue)
 108        {
 0109            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 110        }
 111
 454112        if (filter.IsLocked.HasValue)
 113        {
 48114            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 115        }
 116
 454117        var tags = filter.Tags.ToList();
 454118        var excludeTags = filter.ExcludeTags.ToList();
 119
 454120        if (filter.IsMovie.HasValue)
 121        {
 0122            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 0123                && (filter.IncludeItemTypes.Length == 0
 0124                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 0125                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 126
 0127            if (!shouldIncludeAllMovieTypes)
 128            {
 0129                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 130            }
 131        }
 132
 454133        if (filter.IsSeries.HasValue)
 134        {
 0135            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 136        }
 137
 454138        if (filter.IsSports.HasValue)
 139        {
 0140            if (filter.IsSports.Value)
 141            {
 0142                tags.Add("Sports");
 143            }
 144            else
 145            {
 0146                excludeTags.Add("Sports");
 147            }
 148        }
 149
 454150        if (filter.IsNews.HasValue)
 151        {
 0152            if (filter.IsNews.Value)
 153            {
 0154                tags.Add("News");
 155            }
 156            else
 157            {
 0158                excludeTags.Add("News");
 159            }
 160        }
 161
 454162        if (filter.IsKids.HasValue)
 163        {
 0164            if (filter.IsKids.Value)
 165            {
 0166                tags.Add("Kids");
 167            }
 168            else
 169            {
 0170                excludeTags.Add("Kids");
 171            }
 172        }
 173
 454174        if (!string.IsNullOrEmpty(filter.SearchTerm))
 175        {
 0176            var cleanedSearchTerm = filter.SearchTerm.GetCleanValue();
 0177            var originalSearchTerm = filter.SearchTerm;
 0178            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 179            {
 0180                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 0181                var likeSearchTerm = $"%{originalSearchTerm.Trim('%')}%";
 0182                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 183            }
 184            else
 185            {
 0186                var likeSearchTerm = $"%{originalSearchTerm}%";
 0187                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 188            }
 189        }
 190
 454191        if (filter.IsFolder.HasValue)
 192        {
 21193            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 194        }
 195
 454196        var includeTypes = filter.IncludeItemTypes;
 197
 198        // Only specify excluded types if no included types are specified
 454199        if (filter.IncludeItemTypes.Length == 0)
 200        {
 228201            var excludeTypes = filter.ExcludeItemTypes;
 228202            if (excludeTypes.Length == 1)
 203            {
 0204                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 205                {
 0206                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 207                }
 208            }
 228209            else if (excludeTypes.Length > 1)
 210            {
 0211                var excludeTypeName = new List<string>();
 0212                foreach (var excludeType in excludeTypes)
 213                {
 0214                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 215                    {
 0216                        excludeTypeName.Add(baseItemKindName!);
 217                    }
 218                }
 219
 0220                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 221            }
 222        }
 223        else
 224        {
 226225            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 226226            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 227        }
 228
 454229        if (filter.ChannelIds.Count > 0)
 230        {
 0231            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 232        }
 233
 454234        if (!filter.ParentId.IsEmpty())
 235        {
 141236            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 237        }
 238
 454239        if (!string.IsNullOrWhiteSpace(filter.Path))
 240        {
 0241            var pathToQuery = GetPathToSave(filter.Path);
 0242            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 243        }
 244
 454245        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 246        {
 0247            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 248        }
 249
 454250        if (filter.MinCommunityRating.HasValue)
 251        {
 0252            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 253        }
 254
 454255        if (filter.MinIndexNumber.HasValue)
 256        {
 0257            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 258        }
 259
 454260        if (filter.MinParentAndIndexNumber.HasValue)
 261        {
 0262            baseQuery = baseQuery
 0263                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 264        }
 265
 454266        if (filter.MinDateCreated.HasValue)
 267        {
 0268            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 269        }
 270
 454271        if (filter.MinDateLastSaved.HasValue)
 272        {
 0273            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 274        }
 275
 454276        if (filter.MinDateLastSavedForUser.HasValue)
 277        {
 0278            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 279        }
 280
 454281        if (filter.IndexNumber.HasValue)
 282        {
 0283            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 284        }
 285
 454286        if (filter.ParentIndexNumber.HasValue)
 287        {
 0288            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 289        }
 290
 454291        if (filter.ParentIndexNumberNotEquals.HasValue)
 292        {
 0293            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 294        }
 295
 454296        var minEndDate = filter.MinEndDate;
 454297        var maxEndDate = filter.MaxEndDate;
 298
 454299        if (filter.HasAired.HasValue)
 300        {
 0301            if (filter.HasAired.Value)
 302            {
 0303                maxEndDate = DateTime.UtcNow;
 304            }
 305            else
 306            {
 0307                minEndDate = DateTime.UtcNow;
 308            }
 309        }
 310
 454311        if (minEndDate.HasValue)
 312        {
 0313            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 314        }
 315
 454316        if (maxEndDate.HasValue)
 317        {
 0318            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 319        }
 320
 454321        if (filter.MinStartDate.HasValue)
 322        {
 0323            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 324        }
 325
 454326        if (filter.MaxStartDate.HasValue)
 327        {
 0328            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 329        }
 330
 454331        if (filter.MinPremiereDate.HasValue)
 332        {
 0333            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 334        }
 335
 454336        if (filter.MaxPremiereDate.HasValue)
 337        {
 0338            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 339        }
 340
 454341        if (filter.TrailerTypes.Length > 0)
 342        {
 0343            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 0344            baseQuery = baseQuery.Where(e => e.TrailerTypes!.Any(w => trailerTypes.Contains(w.Id)));
 345        }
 346
 454347        if (filter.IsAiring.HasValue)
 348        {
 0349            if (filter.IsAiring.Value)
 350            {
 0351                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 352            }
 353            else
 354            {
 0355                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 356            }
 357        }
 358
 454359        if (filter.PersonIds.Length > 0)
 360        {
 0361            var peopleEntityIds = context.BaseItems
 0362                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 0363                .Join(
 0364                    context.Peoples,
 0365                    b => b.Name,
 0366                    p => p.Name,
 0367                    (b, p) => p.Id);
 368
 0369            baseQuery = baseQuery
 0370                .Where(e => context.PeopleBaseItemMap
 0371                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 372        }
 373
 454374        if (!string.IsNullOrWhiteSpace(filter.Person))
 375        {
 0376            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 377        }
 378
 454379        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 380        {
 0381            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 382        }
 383
 454384        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 385        {
 0386            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 387        }
 388
 454389        if (!string.IsNullOrWhiteSpace(filter.Name))
 390        {
 3391            if (filter.UseRawName == true)
 392            {
 0393                var nameLower = filter.Name.ToLowerInvariant();
 0394                baseQuery = baseQuery.Where(e => e.Name!.ToLower() == nameLower);
 395            }
 396            else
 397            {
 3398                var cleanName = filter.Name.GetCleanValue();
 3399                baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 400            }
 401        }
 402
 454403        var nameContains = filter.NameContains;
 454404        if (!string.IsNullOrWhiteSpace(nameContains))
 405        {
 0406            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 407            {
 0408                nameContains = $"%{nameContains.Trim('%')}%";
 0409                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 410            }
 411            else
 412            {
 0413                var likeNameContains = $"%{nameContains}%";
 0414                baseQuery = baseQuery.Where(e =>
 0415                                    e.CleanName!.Contains(nameContains)
 0416                                    || EF.Functions.Like(e.OriginalTitle, likeNameContains));
 417            }
 418        }
 419
 420        // When box set collapsing is active, defer name-range filters to after the collapse.
 421        // Otherwise, items are filtered by their own name but then collapsed into a BoxSet
 422        // whose name may fall in a different range (e.g. "21 Jump Street" is under "#"
 423        // but its BoxSet "Jump Street Collection" should appear under "J").
 454424        if (filter.CollapseBoxSetItems != true)
 425        {
 454426            baseQuery = ApplyNameFilters(baseQuery, filter);
 427        }
 428
 454429        if (filter.ImageTypes.Length > 0)
 430        {
 105431            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 105432            baseQuery = baseQuery.Where(e => e.Images!.Any(w => imgTypes.Contains(w.ImageType)));
 433        }
 434
 454435        if (filter.IsLiked.HasValue)
 436        {
 0437            var isLiked = filter.IsLiked.Value;
 0438            baseQuery = baseQuery.Where(e => e.UserData!.Any(ud => ud.UserId == filter.User!.Id && ud.Rating >= UserItem
 439        }
 440
 454441        if (filter.IsFavoriteOrLiked.HasValue)
 442        {
 0443            var isFavoriteOrLiked = filter.IsFavoriteOrLiked.Value;
 0444            baseQuery = baseQuery.Where(e => e.UserData!.Any(ud => ud.UserId == filter.User!.Id && ud.IsFavorite) == isF
 445        }
 446
 454447        if (filter.IsFavorite.HasValue)
 448        {
 0449            var isFavorite = filter.IsFavorite.Value;
 0450            baseQuery = baseQuery.Where(e => e.UserData!.Any(ud => ud.UserId == filter.User!.Id && ud.IsFavorite) == isF
 451        }
 452
 454453        if (filter.IsPlayed.HasValue)
 454        {
 0455            var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series);
 0456            var hasBoxSet = filter.IncludeItemTypes.Contains(BaseItemKind.BoxSet);
 457
 0458            if (hasSeries || hasBoxSet)
 459            {
 0460                var userId = filter.User!.Id;
 0461                var isPlayed = filter.IsPlayed.Value;
 0462                var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 0463                var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
 464
 465                // Series: played = at least one episode AND all episodes played; unplayed = otherwise.
 0466                IQueryable<Guid> playedSeriesIds = hasSeries
 0467                    ? context.BaseItems
 0468                        .AsNoTracking()
 0469                        .Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
 0470                        .GroupBy(e => e.SeriesId!.Value)
 0471                        .Where(g => !g.Any(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played)))
 0472                        .Select(g => g.Key)
 0473                    : Enumerable.Empty<Guid>().AsQueryable();
 474
 475                // BoxSet: played = all children played.
 0476                IQueryable<Guid> playedBoxSetIds = hasBoxSet
 0477                    ? GetFullyPlayedFolderIdsQuery(
 0478                        context,
 0479                        baseQuery.Where(e => e.Type == boxSetTypeName).Select(e => e.Id),
 0480                        filter.User!)
 0481                    : Enumerable.Empty<Guid>().AsQueryable();
 482
 483                // Non-folder items: check UserData directly
 0484                var playedItemIds = context.UserData
 0485                    .Where(ud => ud.UserId == userId && ud.Played)
 0486                    .Select(ud => ud.ItemId);
 487
 0488                if (isPlayed)
 489                {
 0490                    baseQuery = baseQuery.Where(e =>
 0491                        (e.Type == seriesTypeName && playedSeriesIds.Contains(e.Id))
 0492                        || (e.Type == boxSetTypeName && playedBoxSetIds.Contains(e.Id))
 0493                        || (e.Type != seriesTypeName && e.Type != boxSetTypeName && playedItemIds.Contains(e.Id)));
 494                }
 495                else
 496                {
 0497                    baseQuery = baseQuery.Where(e =>
 0498                        (e.Type == seriesTypeName && !playedSeriesIds.Contains(e.Id))
 0499                        || (e.Type == boxSetTypeName && !playedBoxSetIds.Contains(e.Id))
 0500                        || (e.Type != seriesTypeName && e.Type != boxSetTypeName && !playedItemIds.Contains(e.Id)));
 501                }
 502            }
 503            else
 504            {
 0505                var playedItemIds = context.UserData
 0506                    .Where(ud => ud.UserId == filter.User!.Id && ud.Played)
 0507                    .Select(ud => ud.ItemId);
 0508                var isPlayedItem = filter.IsPlayed.Value;
 0509                baseQuery = baseQuery.Where(e => playedItemIds.Contains(e.Id) == isPlayedItem);
 510            }
 511        }
 512
 454513        if (filter.IsResumable.HasValue)
 514        {
 1515            var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series);
 516
 1517            if (hasSeries)
 518            {
 0519                var userId = filter.User!.Id;
 0520                var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 0521                var isResumable = filter.IsResumable.Value;
 522
 523                // Aggregate per series in a single GROUP BY pass, instead of three full scans.
 0524                var seriesEpisodeStats = context.BaseItems
 0525                    .AsNoTracking()
 0526                    .Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
 0527                    .GroupBy(e => e.SeriesId!.Value)
 0528                    .Select(g => new
 0529                    {
 0530                        SeriesId = g.Key,
 0531                        HasInProgress = g.Any(e => e.UserData!.Any(ud => ud.UserId == userId && ud.PlaybackPositionTicks
 0532                        HasPlayed = g.Any(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played)),
 0533                        HasUnplayed = g.Any(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
 0534                    });
 535
 536                // A series is resumable if it has an in-progress episode,
 537                // or if it has both played and unplayed episodes (partially watched).
 0538                var resumableSeriesIds = seriesEpisodeStats
 0539                    .Where(s => s.HasInProgress || (s.HasPlayed && s.HasUnplayed))
 0540                    .Select(s => s.SeriesId);
 541
 542                // Non-series items: resumable if PlaybackPositionTicks > 0
 0543                var resumableItemIds = context.UserData
 0544                    .Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0)
 0545                    .Select(ud => ud.ItemId);
 546
 0547                baseQuery = baseQuery.Where(e =>
 0548                    (e.Type == seriesTypeName && resumableSeriesIds.Contains(e.Id) == isResumable)
 0549                    || (e.Type != seriesTypeName && resumableItemIds.Contains(e.Id) == isResumable));
 550            }
 551            else
 552            {
 1553                var resumableItemIds = context.UserData
 1554                    .Where(ud => ud.UserId == filter.User!.Id && ud.PlaybackPositionTicks > 0)
 1555                    .Select(ud => ud.ItemId);
 1556                var isResumable = filter.IsResumable.Value;
 1557                baseQuery = baseQuery.Where(e => resumableItemIds.Contains(e.Id) == isResumable);
 558            }
 559        }
 560
 454561        if (filter.ArtistIds.Length > 0)
 562        {
 0563            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 564        }
 565
 454566        if (filter.AlbumArtistIds.Length > 0)
 567        {
 0568            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 569        }
 570
 454571        if (filter.ContributingArtistIds.Length > 0)
 572        {
 0573            var contributingNames = context.BaseItems
 0574                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 0575                .Select(b => b.CleanName);
 576
 0577            baseQuery = baseQuery.Where(e =>
 0578                e.ItemValues!.Any(ivm =>
 0579                    ivm.ItemValue.Type == ItemValueType.Artist &&
 0580                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 0581                &&
 0582                !e.ItemValues!.Any(ivm =>
 0583                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 0584                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 585        }
 586
 454587        if (filter.AlbumIds.Length > 0)
 588        {
 0589            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 0590            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 591        }
 592
 454593        if (filter.ExcludeArtistIds.Length > 0)
 594        {
 0595            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 596        }
 597
 454598        if (filter.GenreIds.Count > 0)
 599        {
 0600            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 601        }
 602
 454603        if (filter.Genres.Count > 0)
 604        {
 0605            var cleanGenres = filter.Genres.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValu
 0606            baseQuery = baseQuery
 0607                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 608        }
 609
 454610        if (tags.Count > 0)
 611        {
 0612            var cleanValues = tags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, str
 0613            baseQuery = baseQuery
 0614                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 615        }
 616
 454617        if (excludeTags.Count > 0)
 618        {
 0619            var cleanValues = excludeTags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueM
 0620            baseQuery = baseQuery
 0621                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 622        }
 623
 454624        if (filter.StudioIds.Length > 0)
 625        {
 0626            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 627        }
 628
 454629        if (filter.OfficialRatings.Length > 0)
 630        {
 0631            var ratings = filter.OfficialRatings;
 0632            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, e => ratings.Contains(e.OfficialRating));
 633        }
 634
 454635        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 454636        if (filter.MinParentalRating != null)
 637        {
 0638            var min = filter.MinParentalRating;
 0639            var minScore = min.Score;
 0640            var minSubScore = min.SubScore ?? 0;
 641
 0642            minParentalRatingFilter = e =>
 0643                e.InheritedParentalRatingValue == null ||
 0644                e.InheritedParentalRatingValue > minScore ||
 0645                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 646        }
 647
 454648        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 454649        if (filter.MaxParentalRating != null)
 650        {
 48651            maxParentalRatingFilter = BuildMaxParentalRatingFilter(context, filter.MaxParentalRating);
 652        }
 653
 454654        if (filter.HasParentalRating ?? false)
 655        {
 0656            if (minParentalRatingFilter != null)
 657            {
 0658                baseQuery = baseQuery.Where(minParentalRatingFilter);
 659            }
 660
 0661            if (maxParentalRatingFilter != null)
 662            {
 0663                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 664            }
 665        }
 454666        else if (filter.BlockUnratedItems.Length > 0)
 667        {
 0668            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0669            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 670
 0671            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 672            {
 0673                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 674            }
 0675            else if (minParentalRatingFilter != null)
 676            {
 0677                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 678            }
 0679            else if (maxParentalRatingFilter != null)
 680            {
 0681                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 682            }
 683            else
 684            {
 0685                baseQuery = baseQuery.Where(unratedItemFilter);
 686            }
 687        }
 454688        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 689        {
 48690            if (minParentalRatingFilter != null)
 691            {
 0692                baseQuery = baseQuery.Where(minParentalRatingFilter);
 693            }
 694
 48695            if (maxParentalRatingFilter != null)
 696            {
 48697                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 698            }
 699        }
 406700        else if (!filter.HasParentalRating ?? false)
 701        {
 0702            baseQuery = baseQuery
 0703                .Where(e => e.InheritedParentalRatingValue == null);
 704        }
 705
 454706        if (filter.HasOfficialRating.HasValue)
 707        {
 0708            Expression<Func<BaseItemEntity, bool>> hasRating =
 0709                e => e.OfficialRating != null && e.OfficialRating != string.Empty;
 710
 0711            baseQuery = filter.HasOfficialRating.Value
 0712                ? baseQuery.WhereItemOrDescendantMatches(context, hasRating)
 0713                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasRating);
 714        }
 715
 454716        if (filter.HasOverview.HasValue)
 717        {
 0718            if (filter.HasOverview.Value)
 719            {
 0720                baseQuery = baseQuery
 0721                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 722            }
 723            else
 724            {
 0725                baseQuery = baseQuery
 0726                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 727            }
 728        }
 729
 454730        if (filter.HasOwnerId.HasValue)
 731        {
 0732            if (filter.HasOwnerId.Value)
 733            {
 0734                baseQuery = baseQuery
 0735                    .Where(e => e.OwnerId != null);
 736            }
 737            else
 738            {
 0739                baseQuery = baseQuery
 0740                    .Where(e => e.OwnerId == null);
 741            }
 742        }
 454743        else if (filter.OwnerIds.Length == 0 && filter.ExtraTypes.Length == 0 && !filter.IncludeOwnedItems)
 744        {
 745            // Exclude alternate versions and owned non-extra items from general queries.
 746            // Alternate versions have PrimaryVersionId set (pointing to their primary).
 747            // Extras (trailers, etc.) have OwnerId set but also have ExtraType set - keep those.
 427748            baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 749        }
 750
 454751        if (filter.OwnerIds.Length > 0)
 752        {
 6753            baseQuery = baseQuery.Where(e => e.OwnerId != null && filter.OwnerIds.Contains(e.OwnerId.Value));
 754        }
 755
 454756        if (filter.ExtraTypes.Length > 0)
 757        {
 758            // Convert ExtraType enum to BaseItemExtraType enum via int cast (same underlying values)
 0759            var extraTypeValues = filter.ExtraTypes.Select(e => (BaseItemExtraType?)(int)e).ToArray();
 0760            baseQuery = baseQuery.Where(e => e.ExtraType != null && extraTypeValues.Contains(e.ExtraType));
 761        }
 762
 454763        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 764        {
 0765            var lang = filter.HasNoAudioTrackWithLanguage;
 0766            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 767
 0768            baseQuery = baseQuery
 0769                .Where(e =>
 0770                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Audio && ms.Langua
 0771                    || (e.IsFolder && !foldersWithAudio.Contains(e.Id)));
 772        }
 773
 454774        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 775        {
 0776            var lang = filter.HasNoInternalSubtitleTrackWithLanguage;
 0777            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 778
 0779            baseQuery = baseQuery
 0780                .Where(e =>
 0781                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && !ms.Is
 0782                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 783        }
 784
 454785        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 786        {
 0787            var lang = filter.HasNoExternalSubtitleTrackWithLanguage;
 0788            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 789
 0790            baseQuery = baseQuery
 0791                .Where(e =>
 0792                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.IsE
 0793                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 794        }
 795
 454796        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 797        {
 0798            var lang = filter.HasNoSubtitleTrackWithLanguage;
 0799            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 800
 0801            baseQuery = baseQuery
 0802                .Where(e =>
 0803                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.Lan
 0804                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 805        }
 806
 454807        if (filter.HasSubtitles.HasValue)
 808        {
 0809            var hasSubtitles = filter.HasSubtitles.Value;
 0810            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasSubtitles());
 0811            if (hasSubtitles)
 812            {
 0813                baseQuery = baseQuery
 0814                    .Where(e =>
 0815                        (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0816                        || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 817            }
 818            else
 819            {
 0820                baseQuery = baseQuery
 0821                    .Where(e =>
 0822                        (!e.IsFolder && !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0823                        || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 824            }
 825        }
 826
 454827        if (filter.SubtitleLanguages.Count > 0)
 828        {
 0829            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 0830            baseQuery = baseQuery
 0831                .Where(e =>
 0832                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle
 0833                     && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && s
 0834                    || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 835        }
 836
 454837        if (filter.AudioLanguages.Count > 0)
 838        {
 0839            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 0840            baseQuery = baseQuery
 0841                .Where(e =>
 0842                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio
 0843                     && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.
 0844                    || (e.IsFolder && foldersWithAudio.Contains(e.Id)));
 845        }
 846
 454847        if (filter.HasChapterImages.HasValue)
 848        {
 0849            var hasChapterImages = filter.HasChapterImages.Value;
 0850            var foldersWithChapterImages = DescendantQueryHelper.GetFolderIdsMatching(context, new HasChapterImages());
 0851            if (hasChapterImages)
 852            {
 0853                baseQuery = baseQuery
 0854                    .Where(e =>
 0855                        (!e.IsFolder && e.Chapters!.Any(f => f.ImagePath != null))
 0856                        || (e.IsFolder && foldersWithChapterImages.Contains(e.Id)));
 857            }
 858            else
 859            {
 0860                baseQuery = baseQuery
 0861                    .Where(e =>
 0862                        (!e.IsFolder && !e.Chapters!.Any(f => f.ImagePath != null))
 0863                        || (e.IsFolder && !foldersWithChapterImages.Contains(e.Id)));
 864            }
 865        }
 866
 454867        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 868        {
 16869            baseQuery = baseQuery
 16870                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 871        }
 872
 454873        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 874        {
 16875            baseQuery = baseQuery
 16876                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 877        }
 878
 454879        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 880        {
 16881            baseQuery = baseQuery
 16882                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 883        }
 884
 454885        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 886        {
 16887            baseQuery = baseQuery
 16888                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 889        }
 890
 454891        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 892        {
 0893            baseQuery = baseQuery
 0894                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 895        }
 896
 454897        if (filter.Years.Length > 0)
 898        {
 0899            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 900        }
 901
 454902        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 454903        if (isVirtualItem.HasValue)
 904        {
 22905            baseQuery = baseQuery
 22906                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 907        }
 908
 454909        if (filter.IsSpecialSeason.HasValue)
 910        {
 0911            if (filter.IsSpecialSeason.Value)
 912            {
 0913                baseQuery = baseQuery
 0914                    .Where(e => e.IndexNumber == 0);
 915            }
 916            else
 917            {
 0918                baseQuery = baseQuery
 0919                    .Where(e => e.IndexNumber != 0);
 920            }
 921        }
 922
 454923        if (filter.IsUnaired.HasValue)
 924        {
 0925            if (filter.IsUnaired.Value)
 926            {
 0927                baseQuery = baseQuery
 0928                    .Where(e => e.PremiereDate >= now);
 929            }
 930            else
 931            {
 0932                baseQuery = baseQuery
 0933                    .Where(e => e.PremiereDate < now);
 934            }
 935        }
 936
 454937        if (filter.MediaTypes.Length > 0)
 938        {
 21939            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 21940            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 941        }
 942
 454943        if (filter.ItemIds.Length > 0)
 944        {
 0945            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 946        }
 947
 454948        if (filter.ExcludeItemIds.Length > 0)
 949        {
 0950            baseQuery = baseQuery
 0951                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 952        }
 953
 454954        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 955        {
 0956            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 0957            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 958        }
 959
 454960        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 961        {
 962            // Allow setting a null or empty value to get all items that have the specified provider set.
 0963            var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArra
 0964            if (includeAny.Length > 0)
 965            {
 0966                baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
 967            }
 968
 0969            var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Ke
 0970            if (includeSelected.Length > 0)
 971            {
 0972                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 973            }
 974        }
 975
 454976        if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
 977        {
 0978            var includeAny = filter.HasAnyProviderIds
 0979                .SelectMany(kvp => kvp.Value.Select(v => $"{kvp.Key}:{v}"))
 0980                .ToArray();
 0981            if (includeAny.Length > 0)
 982            {
 0983                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 984            }
 985        }
 986
 454987        if (filter.HasImdbId.HasValue)
 988        {
 0989            baseQuery = filter.HasImdbId.Value
 0990                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == ImdbProviderName))
 0991                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != ImdbProviderName));
 992        }
 993
 454994        if (filter.HasTmdbId.HasValue)
 995        {
 0996            baseQuery = filter.HasTmdbId.Value
 0997                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TmdbProviderName))
 0998                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TmdbProviderName));
 999        }
 1000
 4541001        if (filter.HasTvdbId.HasValue)
 1002        {
 01003            baseQuery = filter.HasTvdbId.Value
 01004                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TvdbProviderName))
 01005                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TvdbProviderName));
 1006        }
 1007
 4541008        var queryTopParentIds = filter.TopParentIds;
 1009
 4541010        if (queryTopParentIds.Length > 0)
 1011        {
 151012            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 151013            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 151014            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 1015            {
 01016                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 1017            }
 1018            else
 1019            {
 151020                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 1021            }
 1022        }
 1023
 4541024        if (filter.AncestorIds.Length > 0)
 1025        {
 441026            var ancestorFilter = filter.AncestorIds.OneOrManyExpressionBuilder<AncestorId, Guid>(f => f.ParentItemId);
 441027            baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
 1028        }
 1029
 4541030        if (filter.LinkedChildAncestorIds.Length > 0)
 1031        {
 1032            // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ances
 01033            var linkedChildAncestorIds = filter.LinkedChildAncestorIds;
 01034            baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc =>
 01035                lc.ParentId == e.Id
 01036                && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId))));
 1037        }
 1038
 4541039        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 1040        {
 01041            baseQuery = baseQuery
 01042                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 1043        }
 1044
 4541045        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 1046        {
 01047            baseQuery = baseQuery
 01048                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 1049        }
 1050
 1051        // Pre-build the blocked-item-id set as a sub-select
 4541052        if (filter.ExcludeInheritedTags.Length > 0)
 1053        {
 01054            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01055            var blockedTagItemIds = context.ItemValuesMap
 01056                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 01057                .Select(f => f.ItemId);
 1058
 01059            baseQuery = baseQuery.Where(e =>
 01060                !blockedTagItemIds.Contains(e.Id)
 01061                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 01062                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 01063                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 1064        }
 1065
 4541066        if (filter.IncludeInheritedTags.Length > 0)
 1067        {
 01068            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01069            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 01070            var allowedTagItemIds = context.ItemValuesMap
 01071                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 01072                .Select(f => f.ItemId);
 1073
 01074            baseQuery = baseQuery.Where(e =>
 01075                allowedTagItemIds.Contains(e.Id)
 01076                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 01077                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 01078                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 01079
 01080                // A playlist should be accessible to its owner regardless of allowed tags
 01081                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 1082        }
 1083
 4541084        if (filter.SeriesStatuses.Length > 0)
 1085        {
 01086            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 01087            baseQuery = baseQuery
 01088                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 1089        }
 1090
 4541091        if (filter.BoxSetLibraryFolders.Length > 0)
 1092        {
 01093            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 01094            baseQuery = baseQuery
 01095                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 1096        }
 1097
 4541098        if (filter.VideoTypes.Length > 0)
 1099        {
 1100            // Dvds and Blu-rays can either be stored in a folder structure or as an iso file
 1101            // => to find all matches we need to check both: VideoType and IsoType
 1102            // alternatively, we could provide specific IsoType filters
 01103            var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
 01104            var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray();
 01105            Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoT
 01106            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
 1107        }
 1108
 4541109        if (filter.Is3D.HasValue)
 1110        {
 01111            Expression<Func<BaseItemEntity, bool>> is3D = e => e.Data!.Contains("Video3DFormat");
 1112
 01113            baseQuery = filter.Is3D.Value
 01114                ? baseQuery.WhereItemOrDescendantMatches(context, is3D)
 01115                : baseQuery.WhereNeitherItemNorDescendantMatches(context, is3D);
 1116        }
 1117
 4541118        if (filter.IsPlaceHolder.HasValue)
 1119        {
 01120            Expression<Func<BaseItemEntity, bool>> isPlaceHolder = e => e.Data!.Contains("IsPlaceHolder\":true");
 1121
 01122            baseQuery = filter.IsPlaceHolder.Value
 01123                ? baseQuery.WhereItemOrDescendantMatches(context, isPlaceHolder)
 01124                : baseQuery.WhereNeitherItemNorDescendantMatches(context, isPlaceHolder);
 1125        }
 1126
 4541127        if (filter.HasSpecialFeature.HasValue)
 1128        {
 01129            var itemsWithExtras = context.BaseItems
 01130                .Where(extra => extra.OwnerId != null
 01131                    && extra.ExtraType != null
 01132                    && extra.ExtraType != BaseItemExtraType.Unknown
 01133                    && extra.ExtraType != BaseItemExtraType.Trailer
 01134                    && extra.ExtraType != BaseItemExtraType.ThemeSong
 01135                    && extra.ExtraType != BaseItemExtraType.ThemeVideo)
 01136                .Select(extra => extra.OwnerId!.Value)
 01137                .Distinct();
 1138
 01139            Expression<Func<BaseItemEntity, bool>> hasExtras = e => itemsWithExtras.Contains(e.Id);
 1140
 01141            baseQuery = filter.HasSpecialFeature.Value
 01142                ? baseQuery.WhereItemOrDescendantMatches(context, hasExtras)
 01143                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasExtras);
 1144        }
 1145
 4541146        if (filter.HasTrailer.HasValue)
 1147        {
 01148            var trailerOwnerIds = context.BaseItems
 01149                .Where(extra => extra.ExtraType == BaseItemExtraType.Trailer && extra.OwnerId != null)
 01150                .Select(extra => extra.OwnerId!.Value);
 1151
 01152            Expression<Func<BaseItemEntity, bool>> hasTrailer = e => trailerOwnerIds.Contains(e.Id);
 1153
 01154            baseQuery = filter.HasTrailer.Value
 01155                ? baseQuery.WhereItemOrDescendantMatches(context, hasTrailer)
 01156                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasTrailer);
 1157        }
 1158
 4541159        if (filter.HasThemeSong.HasValue)
 1160        {
 01161            var themeSongOwnerIds = context.BaseItems
 01162                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeSong && extra.OwnerId != null)
 01163                .Select(extra => extra.OwnerId!.Value);
 1164
 01165            Expression<Func<BaseItemEntity, bool>> hasThemeSong = e => themeSongOwnerIds.Contains(e.Id);
 1166
 01167            baseQuery = filter.HasThemeSong.Value
 01168                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeSong)
 01169                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeSong);
 1170        }
 1171
 4541172        if (filter.HasThemeVideo.HasValue)
 1173        {
 01174            var themeVideoOwnerIds = context.BaseItems
 01175                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeVideo && extra.OwnerId != null)
 01176                .Select(extra => extra.OwnerId!.Value);
 1177
 01178            Expression<Func<BaseItemEntity, bool>> hasThemeVideo = e => themeVideoOwnerIds.Contains(e.Id);
 1179
 01180            baseQuery = filter.HasThemeVideo.Value
 01181                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeVideo)
 01182                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeVideo);
 1183        }
 1184
 4541185        if (filter.AiredDuringSeason.HasValue)
 1186        {
 01187            var seasonNumber = filter.AiredDuringSeason.Value;
 01188            if (seasonNumber < 1)
 1189            {
 01190                baseQuery = baseQuery.Where(e => e.ParentIndexNumber == seasonNumber);
 1191            }
 1192            else
 1193            {
 01194                var seasonStr = seasonNumber.ToString(CultureInfo.InvariantCulture);
 01195                baseQuery = baseQuery.Where(e =>
 01196                    e.ParentIndexNumber == seasonNumber
 01197                    || (e.Data != null && (
 01198                        e.Data.Contains("\"AirsAfterSeasonNumber\":" + seasonStr)
 01199                        || e.Data.Contains("\"AirsBeforeSeasonNumber\":" + seasonStr))));
 1200            }
 1201        }
 1202
 4541203        if (filter.AdjacentTo.HasValue && !filter.AdjacentTo.Value.IsEmpty())
 1204        {
 01205            var adjacentToId = filter.AdjacentTo.Value;
 01206            var targetItem = context.BaseItems.Where(e => e.Id == adjacentToId).Select(e => new { e.SortName, e.Id }).Fi
 01207            if (targetItem is not null)
 1208            {
 01209                var targetSortName = targetItem.SortName ?? string.Empty;
 1210
 1211                // Fetch both prev and next adjacent items in a single query using Concat (UNION ALL).
 01212                var adjacentIds = context.BaseItems
 01213                    .Where(e => string.Compare(e.SortName, targetSortName) < 0)
 01214                    .OrderByDescending(e => e.SortName)
 01215                    .Select(e => e.Id)
 01216                    .Take(1)
 01217                    .Concat(
 01218                        context.BaseItems
 01219                            .Where(e => string.Compare(e.SortName, targetSortName) > 0)
 01220                            .OrderBy(e => e.SortName)
 01221                            .Select(e => e.Id)
 01222                            .Take(1))
 01223                    .ToList();
 1224
 01225                adjacentIds.Add(adjacentToId);
 01226                baseQuery = baseQuery.Where(e => adjacentIds.Contains(e.Id));
 1227            }
 1228        }
 1229
 4541230        return baseQuery;
 1231    }
 1232}

Methods/Properties

GetAllArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetAlbumArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetStudios(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetGenres(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetMusicGenres(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetStudioNames()
GetAllArtistNames()
GetMusicGenreNames()
GetGenreNames()
GetItemValueNames(System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Collections.Generic.IReadOnlyList`1<System.String>)
GetItemValues(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>,System.String)
BuildItemCountsByCleanName(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>)
.cctor()
.ctor(Microsoft.EntityFrameworkCore.IDbContextFactory`1<Jellyfin.Database.Implementations.JellyfinDbContext>,MediaBrowser.Controller.IServerApplicationHost,MediaBrowser.Controller.Persistence.IItemTypeLookup,MediaBrowser.Controller.Configuration.IServerConfigurationManager,Microsoft.Extensions.Logging.ILogger`1<Jellyfin.Server.Implementations.Item.BaseItemRepository>)
Map(Jellyfin.Database.Implementations.Entities.BaseItemEntity,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.IServerApplicationHost,Microsoft.Extensions.Logging.ILogger)
Map(MediaBrowser.Controller.Entities.BaseItem)
DeserializeBaseItem(Jellyfin.Database.Implementations.Entities.BaseItemEntity,Microsoft.Extensions.Logging.ILogger,MediaBrowser.Controller.IServerApplicationHost,System.Boolean)
PrepareFilterQuery(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemByNameTypesInQuery(MediaBrowser.Controller.Entities.InternalItemsQuery)
IsTypeInQuery(Jellyfin.Data.Enums.BaseItemKind,MediaBrowser.Controller.Entities.InternalItemsQuery)
EnableGroupByPresentationUniqueKey(MediaBrowser.Controller.Entities.InternalItemsQuery)
Map(System.Guid,MediaBrowser.Controller.Entities.ItemImageInfo)
Map(Jellyfin.Database.Implementations.Entities.BaseItemImageInfo,MediaBrowser.Controller.IServerApplicationHost)
GetPathToSave(System.String)
DeserializeBaseItem(Jellyfin.Database.Implementations.Entities.BaseItemEntity,System.Boolean)
PrepareItemQuery(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyQueryFilter(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyQueryPaging(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyGroupingFilter(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyBoxSetCollapsing(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Data.Enums.BaseItemKind[])
ApplyBoxSetCollapsingAll(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<System.Guid>,System.String)
ApplyNameFilters(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyNavigations(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyOrder(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.JellyfinDbContext)
ApplySeriesDatePlayedOrder(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.JellyfinDbContext,System.ValueTuple`2<Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Database.Implementations.Enums.SortOrder>[])
BuildAccessFilteredDescendantsQuery(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Guid)
ApplyAccessFiltering(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
BuildMaxParentalRatingFilter(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Model.Entities.ParentalRatingScore)
GetFullyPlayedFolderIdsQuery(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<System.Guid>,Jellyfin.Database.Implementations.Entities.User)
GetItemIdsList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItems(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetLatestItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Data.Enums.CollectionType)
LoadLatestByIds(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<System.Guid>,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetLatestTvShowItems(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Nullable`1<System.Int32>)
ItemExistsAsync()
RetrieveItem(System.Guid)
GetIsPlayed(Jellyfin.Database.Implementations.Entities.User,System.Guid,System.Boolean)
GetQueryFiltersLegacy(MediaBrowser.Controller.Entities.InternalItemsQuery)
.cctor()
TranslateQuery(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)