< Summary - Jellyfin

Line coverage
28%
Covered lines: 402
Uncovered lines: 1030
Coverable lines: 1432
Total lines: 2864
Line coverage: 28%
Branch coverage
39%
Covered branches: 258
Total branches: 650
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 3/5/2026 - 12:13:57 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: 28626/1/2026 - 12:16:05 AM Line coverage: 27.9% (401/1437) Branch coverage: 39.5% (257/650) Total lines: 28726/8/2026 - 12:16:15 AM Line coverage: 28% (402/1433) Branch coverage: 39.6% (258/650) Total lines: 28656/15/2026 - 12:16:09 AM Line coverage: 28% (402/1432) Branch coverage: 39.6% (258/650) Total lines: 2864 3/5/2026 - 12:13:57 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: 28626/1/2026 - 12:16:05 AM Line coverage: 27.9% (401/1437) Branch coverage: 39.5% (257/650) Total lines: 28726/8/2026 - 12:16:15 AM Line coverage: 28% (402/1433) Branch coverage: 39.6% (258/650) Total lines: 28656/15/2026 - 12:16:09 AM Line coverage: 28% (402/1432) Branch coverage: 39.6% (258/650) Total lines: 2864

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%210140%
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.67%5542138628.25%

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    {
 1757        return GetItemValueNames(_getStudiosValueTypes, [], []);
 58    }
 59
 60    /// <inheritdoc />
 61    public IReadOnlyList<string> GetAllArtistNames()
 62    {
 1763        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 64    }
 65
 66    /// <inheritdoc />
 67    public IReadOnlyList<string> GetMusicGenreNames()
 68    {
 1769        return GetItemValueNames(
 1770            _getGenreValueTypes,
 1771            _itemTypeLookup.MusicGenreTypes,
 1772            []);
 73    }
 74
 75    /// <inheritdoc />
 76    public IReadOnlyList<string> GetGenreNames()
 77    {
 1778        return GetItemValueNames(
 1779            _getGenreValueTypes,
 1780            [],
 1781            _itemTypeLookup.MusicGenreTypes);
 82    }
 83
 84    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 85    {
 6886        using var context = _dbProvider.CreateDbContext();
 87
 6888        var query = context.ItemValuesMap
 6889            .AsNoTracking()
 6890            .Where(e => itemValueTypes.Any(w => w == e.ItemValue.Type));
 6891        if (withItemTypes.Count > 0)
 92        {
 1793            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 94        }
 95
 6896        if (excludeItemTypes.Count > 0)
 97        {
 1798            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 99        }
 100
 68101        return query.Select(e => e.ItemValue)
 68102            .GroupBy(e => e.CleanValue)
 68103            .Select(g => g.Min(v => v.Value)!)
 68104            .ToArray();
 68105    }
 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. For MusicArtist, prefer the entity from a library the user
 174        // can actually access,since the same artist can have a folder in multiple libraries.
 175        // Keep as an IQueryable sub-select so paging is applied AFTER
 176        // ApplyOrder runs the caller's actual sort.
 0177        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter);
 0178        var isMusicArtist = returnType == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 0179        var representativeIds = isMusicArtist
 0180            ? masterQuery
 0181                .GroupBy(e => e.PresentationUniqueKey)
 0182                .Select(g => g
 0183                    .OrderBy(e => filter.TopParentIds.Contains(e.TopParentId ?? Guid.Empty) ? 0 : 1)
 0184                    .ThenBy(e => e.Id)
 0185                    .First().Id)
 0186            : masterQuery
 0187                .GroupBy(e => e.PresentationUniqueKey)
 0188                .Select(g => g.Min(e => e.Id));
 189
 0190        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 0191        if (filter.EnableTotalRecordCount)
 192        {
 0193            result.TotalRecordCount = representativeIds.Count();
 194        }
 195
 0196        var query = ApplyNavigations(
 0197                context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => representativeIds.Contains(e.Id)),
 0198                filter);
 199
 0200        query = ApplyOrder(query, filter, context);
 201
 0202        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 203        {
 0204            query = query.Skip(filter.StartIndex.Value);
 205        }
 206
 0207        if (filter.Limit.HasValue)
 208        {
 0209            query = query.Take(filter.Limit.Value);
 210        }
 211
 0212        result.StartIndex = filter.StartIndex ?? 0;
 0213        if (filter.IncludeItemTypes.Length > 0)
 214        {
 0215            var countsByCleanName = BuildItemCountsByCleanName(context, filter, itemValueTypes);
 0216            result.Items =
 0217            [
 0218                .. query
 0219                    .AsEnumerable()
 0220                    .Where(e => e is not null)
 0221                    .Select(e =>
 0222                    {
 0223                        var item = DeserializeBaseItem(e, filter.SkipDeserialization);
 0224                        countsByCleanName.TryGetValue(e.CleanName ?? string.Empty, out var itemCount);
 0225                        return (item, itemCount);
 0226                    })
 0227                    .Where(x => x.item is not null)
 0228                    .Select(x => (x.item!, x.itemCount))
 0229            ];
 230        }
 231        else
 232        {
 0233            result.Items =
 0234            [
 0235                .. query
 0236                    .AsEnumerable()
 0237                    .Where(e => e != null)
 0238                    .Select(e => DeserializeBaseItem(e, filter.SkipDeserialization))
 0239                    .Where(item => item != null)
 0240                    .Select(item => (item!, (ItemCounts?)null))
 0241            ];
 242        }
 243
 0244        return result;
 0245    }
 246
 247    private Dictionary<string, ItemCounts> BuildItemCountsByCleanName(
 248        Database.Implementations.JellyfinDbContext context,
 249        InternalItemsQuery filter,
 250        IReadOnlyList<ItemValueType> itemValueTypes)
 251    {
 0252        var typeSubQuery = new InternalItemsQuery(filter.User)
 0253        {
 0254            ExcludeItemTypes = filter.ExcludeItemTypes,
 0255            IncludeItemTypes = filter.IncludeItemTypes,
 0256            MediaTypes = filter.MediaTypes,
 0257            AncestorIds = filter.AncestorIds,
 0258            ExcludeItemIds = filter.ExcludeItemIds,
 0259            ItemIds = filter.ItemIds,
 0260            TopParentIds = filter.TopParentIds,
 0261            ParentId = filter.ParentId,
 0262            IsPlayed = filter.IsPlayed
 0263        };
 264
 0265        var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 0266            .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 267
 0268        var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 0269        var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 0270        var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 0271        var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 0272        var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 0273        var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 0274        var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 0275        var itemIds = itemCountQuery.Select(e => e.Id);
 276
 277        // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite
 278        // Instead, start from ItemValueMaps and join with BaseItems
 0279        return context.ItemValuesMap
 0280            .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
 0281            .Where(ivm => itemIds.Contains(ivm.ItemId))
 0282            .Join(
 0283                context.BaseItems,
 0284                ivm => ivm.ItemId,
 0285                e => e.Id,
 0286                (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type })
 0287            .GroupBy(x => new { x.CleanName, x.Type })
 0288            .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() })
 0289            .GroupBy(x => x.CleanName)
 0290            .ToDictionary(
 0291                g => g.Key,
 0292                g => new ItemCounts
 0293                {
 0294                    SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count),
 0295                    EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count),
 0296                    MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count),
 0297                    AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count),
 0298                    ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count),
 0299                    SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count),
 0300                    TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count),
 0301                });
 302    }
 303}

/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    {
 468110        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 111        {
 0112            query.Limit = query.Limit.Value + 4;
 113        }
 114
 468115        if (query.IsResumable ?? false)
 116        {
 1117            query.IsVirtualItem = false;
 118        }
 468119    }
 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    {
 468165        if (!query.GroupByPresentationUniqueKey)
 166        {
 142167            return false;
 168        }
 169
 326170        if (query.GroupBySeriesPresentationUniqueKey)
 171        {
 0172            return false;
 173        }
 174
 326175        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 176        {
 0177            return false;
 178        }
 179
 326180        if (query.User is null)
 181        {
 324182            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    {
 47028        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 47029        dbQuery = dbQuery.AsSingleQuery();
 30
 47031        return dbQuery;
 32    }
 33
 34    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 35    {
 8536        dbQuery = TranslateQuery(dbQuery, context, filter);
 8537        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 8538        dbQuery = ApplyQueryPaging(dbQuery, filter);
 8539        dbQuery = ApplyNavigations(dbQuery, filter);
 8540        return dbQuery;
 41    }
 42
 43    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 44    {
 46845        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 46        {
 10747            var offset = filter.StartIndex ?? 0;
 48
 10749            if (offset > 0)
 50            {
 051                dbQuery = dbQuery.Skip(offset);
 52            }
 53
 10754            if (filter.Limit.HasValue)
 55            {
 10756                dbQuery = dbQuery.Take(filter.Limit.Value);
 57            }
 58        }
 59
 46860        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.
 46868        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 46869        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        }
 46874        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        }
 46779        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        {
 46786            dbQuery = dbQuery.Distinct();
 87        }
 88
 46889        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
 46897        dbQuery = ApplyOrder(dbQuery, filter, context);
 98
 46899        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    {
 468195        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 196        {
 0197            var nameStartsWithLower = filter.NameStartsWith.ToLowerInvariant();
 0198            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().StartsWith(nameStartsWithLower));
 199        }
 200
 468201        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
 468207        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
 468213        return dbQuery;
 214    }
 215
 216    /// <inheritdoc />
 217    public IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 218    {
 403219        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 220        {
 0221            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 222        }
 223
 403224        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 225        {
 402226            dbQuery = dbQuery.Include(e => e.Provider);
 227        }
 228
 403229        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 230        {
 402231            dbQuery = dbQuery.Include(e => e.LockedFields);
 232        }
 233
 403234        if (filter.DtoOptions.EnableUserData)
 235        {
 403236            dbQuery = dbQuery.Include(e => e.UserData);
 237        }
 238
 403239        if (filter.DtoOptions.EnableImages)
 240        {
 403241            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.
 403248        var linkedChildTypes = new[]
 403249        {
 403250            BaseItemKind.BoxSet,
 403251            BaseItemKind.Playlist,
 403252            BaseItemKind.CollectionFolder,
 403253            BaseItemKind.Video,
 403254            BaseItemKind.Movie
 403255        };
 403256        if (filter.IncludeItemTypes.Length == 0 || filter.IncludeItemTypes.Any(linkedChildTypes.Contains))
 257        {
 247258            dbQuery = dbQuery.Include(e => e.LinkedChildEntities);
 259        }
 260
 403261        if (filter.IncludeExtras)
 262        {
 0263            dbQuery = dbQuery.Include(e => e.Extras);
 264        }
 265
 403266        return dbQuery;
 267    }
 268
 269    /// <inheritdoc />
 270    public IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDb
 271    {
 468272        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 468273        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.
 468278        if (!hasSearch && orderBy.Any(o => o.OrderBy == ItemSortBy.SeriesDatePlayed))
 279        {
 0280            return ApplySeriesDatePlayedOrder(query, filter, context, orderBy);
 281        }
 282
 468283        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 284
 468285        if (hasSearch)
 286        {
 0287            var relevanceExpression = OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!);
 0288            orderedQuery = query.OrderBy(relevanceExpression);
 289        }
 290
 468291        if (orderBy.Length > 0)
 292        {
 114293            var firstOrdering = orderBy[0];
 114294            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 295
 114296            if (orderedQuery is null)
 297            {
 114298                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 114299                    ? query.OrderBy(expression)
 114300                    : query.OrderByDescending(expression);
 301            }
 302            else
 303            {
 0304                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 0305                    ? orderedQuery.ThenBy(expression)
 0306                    : orderedQuery.ThenByDescending(expression);
 307            }
 308
 114309            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
 312316            foreach (var item in orderBy.Skip(1))
 317            {
 42318                expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 42319                orderedQuery = item.SortOrder == SortOrder.Ascending
 42320                    ? orderedQuery.ThenBy(expression)
 42321                    : orderedQuery.ThenByDescending(expression);
 322            }
 323        }
 324
 468325        if (orderedQuery is null)
 326        {
 354327            return query.OrderBy(e => e.SortName);
 328        }
 329
 330        // Add SortName as final tiebreaker
 114331        if (!hasSearch && (orderBy.Length == 0 || orderBy.All(o => o.OrderBy is not ItemSortBy.SortName and not ItemSort
 332        {
 66333            orderedQuery = orderedQuery.ThenBy(e => e.SortName);
 334        }
 335
 114336        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    {
 51476        var maxScore = maxRating.Score;
 51477        var maxSubScore = maxRating.SubScore ?? 0;
 51478        var linkedChildren = context.LinkedChildren;
 479
 51480        return e =>
 51481            // Item has a rating: check against limit
 51482            (e.InheritedParentalRatingValue != null
 51483                && (e.InheritedParentalRatingValue < maxScore
 51484                    || (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSub
 51485            // Item has no rating
 51486            || (e.InheritedParentalRatingValue == null
 51487                && (
 51488                    // No linked children (not a BoxSet/Playlist): pass as unrated
 51489                    !linkedChildren.Any(lc => lc.ParentId == e.Id)
 51490                    // Has linked children: at least one child must be within limits
 51491                    || linkedChildren.Any(lc => lc.ParentId == e.Id
 51492                        && (lc.Child!.InheritedParentalRatingValue == null
 51493                            || lc.Child.InheritedParentalRatingValue < maxScore
 51494                            || (lc.Child.InheritedParentalRatingValue == maxScore
 51495                                && (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    {
 8523        ArgumentNullException.ThrowIfNull(filter);
 8524        PrepareFilterQuery(filter);
 25
 8526        using var context = _dbProvider.CreateDbContext();
 8527        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 8528    }
 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    {
 38369        ArgumentNullException.ThrowIfNull(filter);
 38370        PrepareFilterQuery(filter);
 71
 38372        using var context = _dbProvider.CreateDbContext();
 38373        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 74
 38375        dbQuery = TranslateQuery(dbQuery, context, filter);
 76
 38377        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 38378        dbQuery = ApplyQueryPaging(dbQuery, filter);
 79
 38380        var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
 38381        if (hasRandomSort)
 82        {
 6583            var orderedIds = dbQuery.AsNoTracking().Select(e => e.Id).ToList();
 6584            if (orderedIds.Count == 0)
 85            {
 6586                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
 31899        dbQuery = ApplyNavigations(dbQuery, filter);
 100
 318101        return dbQuery.AsEnumerable().Where(e => e != null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializatio
 383102    }
 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    {
 23437        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 23438        await using (dbContext.ConfigureAwait(false))
 439        {
 23440            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 441        }
 23442    }
 443
 444    /// <inheritdoc  />
 445    public BaseItemDto? RetrieveItem(Guid id)
 446    {
 87447        if (id.IsEmpty())
 448        {
 0449            throw new ArgumentException("Guid can't be empty", nameof(id));
 450        }
 451
 87452        using var context = _dbProvider.CreateDbContext();
 87453        var dbQuery = PrepareItemQuery(context, new()
 87454        {
 87455            DtoOptions = new()
 87456            {
 87457                EnableImages = true
 87458            }
 87459        });
 87460        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 87461            .Include(e => e.Provider)
 87462            .Include(e => e.LockedFields)
 87463            .Include(e => e.UserData)
 87464            .Include(e => e.Images)
 87465            .Include(e => e.LinkedChildEntities)
 87466            .AsSingleQuery();
 467
 87468        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 87469        if (item is null)
 470        {
 87471            return null;
 472        }
 473
 0474        return DeserializeBaseItem(item);
 87475    }
 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
 46844        var minWidth = filter.MinWidth;
 46845        var maxWidth = filter.MaxWidth;
 46846        var now = DateTime.UtcNow;
 47
 46848        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
 46892        if (minWidth.HasValue)
 93        {
 094            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 95        }
 96
 46897        if (filter.MinHeight.HasValue)
 98        {
 099            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 100        }
 101
 468102        if (maxWidth.HasValue)
 103        {
 0104            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 105        }
 106
 468107        if (filter.MaxHeight.HasValue)
 108        {
 0109            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 110        }
 111
 468112        if (filter.IsLocked.HasValue)
 113        {
 51114            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 115        }
 116
 468117        var tags = filter.Tags.ToList();
 468118        var excludeTags = filter.ExcludeTags.ToList();
 119
 468120        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
 468133        if (filter.IsSeries.HasValue)
 134        {
 0135            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 136        }
 137
 468138        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
 468150        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
 468162        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
 468174        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
 468191        if (filter.IsFolder.HasValue)
 192        {
 21193            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 194        }
 195
 468196        var includeTypes = filter.IncludeItemTypes;
 197
 198        // Only specify excluded types if no included types are specified
 468199        if (filter.IncludeItemTypes.Length == 0)
 200        {
 229201            var excludeTypes = filter.ExcludeItemTypes;
 229202            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            }
 229209            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        {
 239225            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 239226            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 227        }
 228
 468229        if (filter.ChannelIds.Count > 0)
 230        {
 0231            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 232        }
 233
 468234        if (!filter.ParentId.IsEmpty())
 235        {
 142236            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 237        }
 238
 468239        if (!string.IsNullOrWhiteSpace(filter.Path))
 240        {
 0241            var pathToQuery = GetPathToSave(filter.Path);
 0242            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 243        }
 244
 468245        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 246        {
 0247            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 248        }
 249
 468250        if (filter.MinCommunityRating.HasValue)
 251        {
 0252            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 253        }
 254
 468255        if (filter.MinIndexNumber.HasValue)
 256        {
 0257            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 258        }
 259
 468260        if (filter.MinParentAndIndexNumber.HasValue)
 261        {
 0262            baseQuery = baseQuery
 0263                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 264        }
 265
 468266        if (filter.MinDateCreated.HasValue)
 267        {
 0268            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 269        }
 270
 468271        if (filter.MinDateLastSaved.HasValue)
 272        {
 0273            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 274        }
 275
 468276        if (filter.MinDateLastSavedForUser.HasValue)
 277        {
 0278            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 279        }
 280
 468281        if (filter.IndexNumber.HasValue)
 282        {
 0283            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 284        }
 285
 468286        if (filter.ParentIndexNumber.HasValue)
 287        {
 0288            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 289        }
 290
 468291        if (filter.ParentIndexNumberNotEquals.HasValue)
 292        {
 0293            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 294        }
 295
 468296        var minEndDate = filter.MinEndDate;
 468297        var maxEndDate = filter.MaxEndDate;
 298
 468299        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
 468311        if (minEndDate.HasValue)
 312        {
 0313            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 314        }
 315
 468316        if (maxEndDate.HasValue)
 317        {
 0318            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 319        }
 320
 468321        if (filter.MinStartDate.HasValue)
 322        {
 0323            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 324        }
 325
 468326        if (filter.MaxStartDate.HasValue)
 327        {
 0328            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 329        }
 330
 468331        if (filter.MinPremiereDate.HasValue)
 332        {
 0333            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 334        }
 335
 468336        if (filter.MaxPremiereDate.HasValue)
 337        {
 0338            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 339        }
 340
 468341        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
 468347        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
 468359        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
 468374        if (!string.IsNullOrWhiteSpace(filter.Person))
 375        {
 0376            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 377        }
 378
 468379        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 380        {
 0381            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 382        }
 383
 468384        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 385        {
 0386            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 387        }
 388
 468389        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
 468403        var nameContains = filter.NameContains;
 468404        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").
 468424        if (filter.CollapseBoxSetItems != true)
 425        {
 468426            baseQuery = ApplyNameFilters(baseQuery, filter);
 427        }
 428
 468429        if (filter.ImageTypes.Length > 0)
 430        {
 107431            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 107432            baseQuery = baseQuery.Where(e => e.Images!.Any(w => imgTypes.Contains(w.ImageType)));
 433        }
 434
 468435        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
 468441        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
 468447        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
 468453        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
 468513        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
 468561        if (filter.ArtistIds.Length > 0)
 562        {
 0563            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 564        }
 565
 468566        if (filter.AlbumArtistIds.Length > 0)
 567        {
 0568            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 569        }
 570
 468571        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
 468587        if (filter.AlbumIds.Length > 0)
 588        {
 0589            baseQuery = baseQuery.Where(e => e.ParentId.HasValue && filter.AlbumIds.Contains(e.ParentId.Value));
 590        }
 591
 468592        if (filter.ExcludeArtistIds.Length > 0)
 593        {
 0594            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 595        }
 596
 468597        if (filter.GenreIds.Count > 0)
 598        {
 0599            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 600        }
 601
 468602        if (filter.Genres.Count > 0)
 603        {
 0604            var cleanGenres = filter.Genres.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValu
 0605            baseQuery = baseQuery
 0606                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 607        }
 608
 468609        if (tags.Count > 0)
 610        {
 0611            var cleanValues = tags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, str
 0612            baseQuery = baseQuery
 0613                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 614        }
 615
 468616        if (excludeTags.Count > 0)
 617        {
 0618            var cleanValues = excludeTags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueM
 0619            baseQuery = baseQuery
 0620                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 621        }
 622
 468623        if (filter.StudioIds.Length > 0)
 624        {
 0625            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 626        }
 627
 468628        if (filter.OfficialRatings.Length > 0)
 629        {
 0630            var ratings = filter.OfficialRatings;
 0631            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, e => ratings.Contains(e.OfficialRating));
 632        }
 633
 468634        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 468635        if (filter.MinParentalRating != null)
 636        {
 0637            var min = filter.MinParentalRating;
 0638            var minScore = min.Score;
 0639            var minSubScore = min.SubScore ?? 0;
 640
 0641            minParentalRatingFilter = e =>
 0642                e.InheritedParentalRatingValue == null ||
 0643                e.InheritedParentalRatingValue > minScore ||
 0644                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 645        }
 646
 468647        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 468648        if (filter.MaxParentalRating != null)
 649        {
 51650            maxParentalRatingFilter = BuildMaxParentalRatingFilter(context, filter.MaxParentalRating);
 651        }
 652
 468653        if (filter.HasParentalRating ?? false)
 654        {
 0655            if (minParentalRatingFilter != null)
 656            {
 0657                baseQuery = baseQuery.Where(minParentalRatingFilter);
 658            }
 659
 0660            if (maxParentalRatingFilter != null)
 661            {
 0662                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 663            }
 664        }
 468665        else if (filter.BlockUnratedItems.Length > 0)
 666        {
 0667            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0668            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 669
 0670            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 671            {
 0672                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 673            }
 0674            else if (minParentalRatingFilter != null)
 675            {
 0676                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 677            }
 0678            else if (maxParentalRatingFilter != null)
 679            {
 0680                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 681            }
 682            else
 683            {
 0684                baseQuery = baseQuery.Where(unratedItemFilter);
 685            }
 686        }
 468687        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 688        {
 51689            if (minParentalRatingFilter != null)
 690            {
 0691                baseQuery = baseQuery.Where(minParentalRatingFilter);
 692            }
 693
 51694            if (maxParentalRatingFilter != null)
 695            {
 51696                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 697            }
 698        }
 417699        else if (!filter.HasParentalRating ?? false)
 700        {
 0701            baseQuery = baseQuery
 0702                .Where(e => e.InheritedParentalRatingValue == null);
 703        }
 704
 468705        if (filter.HasOfficialRating.HasValue)
 706        {
 0707            Expression<Func<BaseItemEntity, bool>> hasRating =
 0708                e => e.OfficialRating != null && e.OfficialRating != string.Empty;
 709
 0710            baseQuery = filter.HasOfficialRating.Value
 0711                ? baseQuery.WhereItemOrDescendantMatches(context, hasRating)
 0712                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasRating);
 713        }
 714
 468715        if (filter.HasOverview.HasValue)
 716        {
 0717            if (filter.HasOverview.Value)
 718            {
 0719                baseQuery = baseQuery
 0720                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 721            }
 722            else
 723            {
 0724                baseQuery = baseQuery
 0725                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 726            }
 727        }
 728
 468729        if (filter.HasOwnerId.HasValue)
 730        {
 0731            if (filter.HasOwnerId.Value)
 732            {
 0733                baseQuery = baseQuery
 0734                    .Where(e => e.OwnerId != null);
 735            }
 736            else
 737            {
 0738                baseQuery = baseQuery
 0739                    .Where(e => e.OwnerId == null);
 740            }
 741        }
 468742        else if (filter.OwnerIds.Length == 0 && filter.ExtraTypes.Length == 0 && !filter.IncludeOwnedItems)
 743        {
 744            // Exclude alternate versions and owned non-extra items from general queries.
 745            // Alternate versions have PrimaryVersionId set (pointing to their primary).
 746            // Extras (trailers, etc.) have OwnerId set but also have ExtraType set - keep those.
 440747            baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 748        }
 749
 468750        if (filter.OwnerIds.Length > 0)
 751        {
 6752            baseQuery = baseQuery.Where(e => e.OwnerId != null && filter.OwnerIds.Contains(e.OwnerId.Value));
 753        }
 754
 468755        if (filter.ExtraTypes.Length > 0)
 756        {
 757            // Convert ExtraType enum to BaseItemExtraType enum via int cast (same underlying values)
 0758            var extraTypeValues = filter.ExtraTypes.Select(e => (BaseItemExtraType?)(int)e).ToArray();
 0759            baseQuery = baseQuery.Where(e => e.ExtraType != null && extraTypeValues.Contains(e.ExtraType));
 760        }
 761
 468762        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 763        {
 0764            var lang = filter.HasNoAudioTrackWithLanguage;
 0765            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 766
 0767            baseQuery = baseQuery
 0768                .Where(e =>
 0769                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Audio && ms.Langua
 0770                    || (e.IsFolder && !foldersWithAudio.Contains(e.Id)));
 771        }
 772
 468773        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 774        {
 0775            var lang = filter.HasNoInternalSubtitleTrackWithLanguage;
 0776            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 777
 0778            baseQuery = baseQuery
 0779                .Where(e =>
 0780                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && !ms.Is
 0781                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 782        }
 783
 468784        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 785        {
 0786            var lang = filter.HasNoExternalSubtitleTrackWithLanguage;
 0787            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 788
 0789            baseQuery = baseQuery
 0790                .Where(e =>
 0791                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.IsE
 0792                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 793        }
 794
 468795        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 796        {
 0797            var lang = filter.HasNoSubtitleTrackWithLanguage;
 0798            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 799
 0800            baseQuery = baseQuery
 0801                .Where(e =>
 0802                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.Lan
 0803                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 804        }
 805
 468806        if (filter.HasSubtitles.HasValue)
 807        {
 0808            var hasSubtitles = filter.HasSubtitles.Value;
 0809            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasSubtitles());
 0810            if (hasSubtitles)
 811            {
 0812                baseQuery = baseQuery
 0813                    .Where(e =>
 0814                        (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0815                        || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 816            }
 817            else
 818            {
 0819                baseQuery = baseQuery
 0820                    .Where(e =>
 0821                        (!e.IsFolder && !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0822                        || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 823            }
 824        }
 825
 468826        if (filter.SubtitleLanguages.Count > 0)
 827        {
 0828            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 0829            baseQuery = baseQuery
 0830                .Where(e =>
 0831                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle
 0832                     && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && s
 0833                    || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 834        }
 835
 468836        if (filter.AudioLanguages.Count > 0)
 837        {
 0838            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 0839            baseQuery = baseQuery
 0840                .Where(e =>
 0841                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio
 0842                     && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.
 0843                    || (e.IsFolder && foldersWithAudio.Contains(e.Id)));
 844        }
 845
 468846        if (filter.HasChapterImages.HasValue)
 847        {
 0848            var hasChapterImages = filter.HasChapterImages.Value;
 0849            var foldersWithChapterImages = DescendantQueryHelper.GetFolderIdsMatching(context, new HasChapterImages());
 0850            if (hasChapterImages)
 851            {
 0852                baseQuery = baseQuery
 0853                    .Where(e =>
 0854                        (!e.IsFolder && e.Chapters!.Any(f => f.ImagePath != null))
 0855                        || (e.IsFolder && foldersWithChapterImages.Contains(e.Id)));
 856            }
 857            else
 858            {
 0859                baseQuery = baseQuery
 0860                    .Where(e =>
 0861                        (!e.IsFolder && !e.Chapters!.Any(f => f.ImagePath != null))
 0862                        || (e.IsFolder && !foldersWithChapterImages.Contains(e.Id)));
 863            }
 864        }
 865
 468866        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 867        {
 17868            baseQuery = baseQuery
 17869                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 870        }
 871
 468872        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 873        {
 17874            baseQuery = baseQuery
 17875                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 876        }
 877
 468878        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 879        {
 17880            baseQuery = baseQuery
 17881                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 882        }
 883
 468884        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 885        {
 17886            baseQuery = baseQuery
 17887                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 888        }
 889
 468890        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 891        {
 0892            baseQuery = baseQuery
 0893                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 894        }
 895
 468896        if (filter.Years.Length > 0)
 897        {
 0898            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 899        }
 900
 468901        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 468902        if (isVirtualItem.HasValue)
 903        {
 22904            baseQuery = baseQuery
 22905                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 906        }
 907
 468908        if (filter.IsSpecialSeason.HasValue)
 909        {
 0910            if (filter.IsSpecialSeason.Value)
 911            {
 0912                baseQuery = baseQuery
 0913                    .Where(e => e.IndexNumber == 0);
 914            }
 915            else
 916            {
 0917                baseQuery = baseQuery
 0918                    .Where(e => e.IndexNumber != 0);
 919            }
 920        }
 921
 468922        if (filter.IsUnaired.HasValue)
 923        {
 0924            if (filter.IsUnaired.Value)
 925            {
 0926                baseQuery = baseQuery
 0927                    .Where(e => e.PremiereDate >= now);
 928            }
 929            else
 930            {
 0931                baseQuery = baseQuery
 0932                    .Where(e => e.PremiereDate < now);
 933            }
 934        }
 935
 468936        if (filter.MediaTypes.Length > 0)
 937        {
 21938            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 21939            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 940        }
 941
 468942        if (filter.ItemIds.Length > 0)
 943        {
 0944            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 945        }
 946
 468947        if (filter.ExcludeItemIds.Length > 0)
 948        {
 0949            baseQuery = baseQuery
 0950                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 951        }
 952
 468953        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 954        {
 0955            baseQuery = baseQuery.WhereExcludeProviderIds(filter.ExcludeProviderIds);
 956        }
 957
 468958        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 959        {
 0960            baseQuery = baseQuery.WhereHasAnyProviderId(filter.HasAnyProviderId);
 961        }
 962
 468963        if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
 964        {
 0965            baseQuery = baseQuery.WhereHasAnyProviderIds(filter.HasAnyProviderIds);
 966        }
 967
 468968        if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
 969        {
 0970            var includeAny = filter.HasAnyProviderIds
 0971                .SelectMany(kvp => kvp.Value.Select(v => $"{kvp.Key}:{v}"))
 0972                .ToArray();
 0973            if (includeAny.Length > 0)
 974            {
 0975                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 976            }
 977        }
 978
 468979        if (filter.HasImdbId.HasValue)
 980        {
 0981            baseQuery = filter.HasImdbId.Value
 0982                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == ImdbProviderName))
 0983                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != ImdbProviderName));
 984        }
 985
 468986        if (filter.HasTmdbId.HasValue)
 987        {
 0988            baseQuery = filter.HasTmdbId.Value
 0989                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TmdbProviderName))
 0990                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TmdbProviderName));
 991        }
 992
 468993        if (filter.HasTvdbId.HasValue)
 994        {
 0995            baseQuery = filter.HasTvdbId.Value
 0996                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TvdbProviderName))
 0997                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TvdbProviderName));
 998        }
 999
 4681000        var queryTopParentIds = filter.TopParentIds;
 1001
 4681002        if (queryTopParentIds.Length > 0)
 1003        {
 151004            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 151005            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 151006            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 1007            {
 01008                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 1009            }
 1010            else
 1011            {
 151012                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 1013            }
 1014        }
 1015
 4681016        if (filter.AncestorIds.Length > 0)
 1017        {
 431018            var ancestorFilter = filter.AncestorIds.OneOrManyExpressionBuilder<AncestorId, Guid>(f => f.ParentItemId);
 431019            baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
 1020        }
 1021
 4681022        if (filter.LinkedChildAncestorIds.Length > 0)
 1023        {
 1024            // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ances
 01025            var linkedChildAncestorIds = filter.LinkedChildAncestorIds;
 01026            baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc =>
 01027                lc.ParentId == e.Id
 01028                && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId))));
 1029        }
 1030
 4681031        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 1032        {
 01033            baseQuery = baseQuery
 01034                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 1035        }
 1036
 4681037        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 1038        {
 01039            baseQuery = baseQuery
 01040                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 1041        }
 1042
 1043        // Pre-build the blocked-item-id set as a sub-select
 4681044        if (filter.ExcludeInheritedTags.Length > 0)
 1045        {
 01046            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01047            var blockedTagItemIds = context.ItemValuesMap
 01048                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 01049                .Select(f => f.ItemId);
 1050
 01051            baseQuery = baseQuery.Where(e =>
 01052                !blockedTagItemIds.Contains(e.Id)
 01053                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 01054                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 01055                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 1056        }
 1057
 4681058        if (filter.IncludeInheritedTags.Length > 0)
 1059        {
 01060            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01061            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 01062            var allowedTagItemIds = context.ItemValuesMap
 01063                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 01064                .Select(f => f.ItemId);
 1065
 01066            baseQuery = baseQuery.Where(e =>
 01067                allowedTagItemIds.Contains(e.Id)
 01068                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 01069                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 01070                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 01071
 01072                // A playlist should be accessible to its owner regardless of allowed tags
 01073                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 1074        }
 1075
 4681076        if (filter.SeriesStatuses.Length > 0)
 1077        {
 01078            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 01079            baseQuery = baseQuery
 01080                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 1081        }
 1082
 4681083        if (filter.BoxSetLibraryFolders.Length > 0)
 1084        {
 01085            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 01086            baseQuery = baseQuery
 01087                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 1088        }
 1089
 4681090        if (filter.VideoTypes.Length > 0)
 1091        {
 1092            // Dvds and Blu-rays can either be stored in a folder structure or as an iso file
 1093            // => to find all matches we need to check both: VideoType and IsoType
 1094            // alternatively, we could provide specific IsoType filters
 01095            var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
 01096            var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray();
 01097            Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoT
 01098            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
 1099        }
 1100
 4681101        if (filter.Is3D.HasValue)
 1102        {
 01103            Expression<Func<BaseItemEntity, bool>> is3D = e => e.Data!.Contains("Video3DFormat");
 1104
 01105            baseQuery = filter.Is3D.Value
 01106                ? baseQuery.WhereItemOrDescendantMatches(context, is3D)
 01107                : baseQuery.WhereNeitherItemNorDescendantMatches(context, is3D);
 1108        }
 1109
 4681110        if (filter.IsPlaceHolder.HasValue)
 1111        {
 01112            Expression<Func<BaseItemEntity, bool>> isPlaceHolder = e => e.Data!.Contains("IsPlaceHolder\":true");
 1113
 01114            baseQuery = filter.IsPlaceHolder.Value
 01115                ? baseQuery.WhereItemOrDescendantMatches(context, isPlaceHolder)
 01116                : baseQuery.WhereNeitherItemNorDescendantMatches(context, isPlaceHolder);
 1117        }
 1118
 4681119        if (filter.HasSpecialFeature.HasValue)
 1120        {
 01121            var itemsWithExtras = context.BaseItems
 01122                .Where(extra => extra.OwnerId != null
 01123                    && extra.ExtraType != null
 01124                    && extra.ExtraType != BaseItemExtraType.Unknown
 01125                    && extra.ExtraType != BaseItemExtraType.Trailer
 01126                    && extra.ExtraType != BaseItemExtraType.ThemeSong
 01127                    && extra.ExtraType != BaseItemExtraType.ThemeVideo)
 01128                .Select(extra => extra.OwnerId!.Value)
 01129                .Distinct();
 1130
 01131            Expression<Func<BaseItemEntity, bool>> hasExtras = e => itemsWithExtras.Contains(e.Id);
 1132
 01133            baseQuery = filter.HasSpecialFeature.Value
 01134                ? baseQuery.WhereItemOrDescendantMatches(context, hasExtras)
 01135                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasExtras);
 1136        }
 1137
 4681138        if (filter.HasTrailer.HasValue)
 1139        {
 01140            var trailerOwnerIds = context.BaseItems
 01141                .Where(extra => extra.ExtraType == BaseItemExtraType.Trailer && extra.OwnerId != null)
 01142                .Select(extra => extra.OwnerId!.Value);
 1143
 01144            Expression<Func<BaseItemEntity, bool>> hasTrailer = e => trailerOwnerIds.Contains(e.Id);
 1145
 01146            baseQuery = filter.HasTrailer.Value
 01147                ? baseQuery.WhereItemOrDescendantMatches(context, hasTrailer)
 01148                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasTrailer);
 1149        }
 1150
 4681151        if (filter.HasThemeSong.HasValue)
 1152        {
 01153            var themeSongOwnerIds = context.BaseItems
 01154                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeSong && extra.OwnerId != null)
 01155                .Select(extra => extra.OwnerId!.Value);
 1156
 01157            Expression<Func<BaseItemEntity, bool>> hasThemeSong = e => themeSongOwnerIds.Contains(e.Id);
 1158
 01159            baseQuery = filter.HasThemeSong.Value
 01160                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeSong)
 01161                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeSong);
 1162        }
 1163
 4681164        if (filter.HasThemeVideo.HasValue)
 1165        {
 01166            var themeVideoOwnerIds = context.BaseItems
 01167                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeVideo && extra.OwnerId != null)
 01168                .Select(extra => extra.OwnerId!.Value);
 1169
 01170            Expression<Func<BaseItemEntity, bool>> hasThemeVideo = e => themeVideoOwnerIds.Contains(e.Id);
 1171
 01172            baseQuery = filter.HasThemeVideo.Value
 01173                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeVideo)
 01174                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeVideo);
 1175        }
 1176
 4681177        if (filter.AiredDuringSeason.HasValue)
 1178        {
 01179            var seasonNumber = filter.AiredDuringSeason.Value;
 01180            if (seasonNumber < 1)
 1181            {
 01182                baseQuery = baseQuery.Where(e => e.ParentIndexNumber == seasonNumber);
 1183            }
 1184            else
 1185            {
 01186                var seasonStr = seasonNumber.ToString(CultureInfo.InvariantCulture);
 01187                baseQuery = baseQuery.Where(e =>
 01188                    e.ParentIndexNumber == seasonNumber
 01189                    || (e.Data != null && (
 01190                        e.Data.Contains("\"AirsAfterSeasonNumber\":" + seasonStr)
 01191                        || e.Data.Contains("\"AirsBeforeSeasonNumber\":" + seasonStr))));
 1192            }
 1193        }
 1194
 4681195        if (filter.AdjacentTo.HasValue && !filter.AdjacentTo.Value.IsEmpty())
 1196        {
 01197            var adjacentToId = filter.AdjacentTo.Value;
 01198            var targetItem = context.BaseItems.Where(e => e.Id == adjacentToId).Select(e => new { e.SortName, e.Id }).Fi
 01199            if (targetItem is not null)
 1200            {
 01201                var targetSortName = targetItem.SortName ?? string.Empty;
 1202
 1203                // Fetch both prev and next adjacent items in a single query using Concat (UNION ALL).
 01204                var adjacentIds = context.BaseItems
 01205                    .Where(e => string.Compare(e.SortName, targetSortName) < 0)
 01206                    .OrderByDescending(e => e.SortName)
 01207                    .Select(e => e.Id)
 01208                    .Take(1)
 01209                    .Concat(
 01210                        context.BaseItems
 01211                            .Where(e => string.Compare(e.SortName, targetSortName) > 0)
 01212                            .OrderBy(e => e.SortName)
 01213                            .Select(e => e.Id)
 01214                            .Take(1))
 01215                    .ToList();
 1216
 01217                adjacentIds.Add(adjacentToId);
 01218                baseQuery = baseQuery.Where(e => adjacentIds.Contains(e.Id));
 1219            }
 1220        }
 1221
 4681222        return baseQuery;
 1223    }
 1224}

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)