< Summary - Jellyfin

Line coverage
29%
Covered lines: 422
Uncovered lines: 1030
Coverable lines: 1452
Total lines: 2900
Line coverage: 29%
Branch coverage
39%
Covered branches: 263
Total branches: 660
Branch coverage: 39.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/26/2026 - 12:14:14 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: 28647/6/2026 - 12:16:28 AM Line coverage: 29% (422/1452) Branch coverage: 39.8% (263/660) Total lines: 2900 3/26/2026 - 12:14:14 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: 28647/6/2026 - 12:16:28 AM Line coverage: 29% (422/1452) Branch coverage: 39.8% (263/660) Total lines: 2900

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(...)63.63%242283.33%
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(...)50%231050%
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.6%5284539430.35%

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    {
 1857        return GetItemValueNames(_getStudiosValueTypes, [], []);
 58    }
 59
 60    /// <inheritdoc />
 61    public IReadOnlyList<string> GetAllArtistNames()
 62    {
 1863        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 64    }
 65
 66    /// <inheritdoc />
 67    public IReadOnlyList<string> GetMusicGenreNames()
 68    {
 1869        return GetItemValueNames(
 1870            _getGenreValueTypes,
 1871            _itemTypeLookup.MusicGenreTypes,
 1872            []);
 73    }
 74
 75    /// <inheritdoc />
 76    public IReadOnlyList<string> GetGenreNames()
 77    {
 1878        return GetItemValueNames(
 1879            _getGenreValueTypes,
 1880            [],
 1881            _itemTypeLookup.MusicGenreTypes);
 82    }
 83
 84    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 85    {
 7286        using var context = _dbProvider.CreateDbContext();
 87
 7288        var query = context.ItemValuesMap
 7289            .AsNoTracking()
 7290            .Where(e => itemValueTypes.Any(w => w == e.ItemValue.Type));
 7291        if (withItemTypes.Count > 0)
 92        {
 1893            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 94        }
 95
 7296        if (excludeItemTypes.Count > 0)
 97        {
 1898            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 99        }
 100
 72101        return query.Select(e => e.ItemValue)
 72102            .GroupBy(e => e.CleanValue)
 72103            .Select(g => g.Min(v => v.Value)!)
 72104            .ToArray();
 72105    }
 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    {
 3464        _dbProvider = dbProvider;
 3465        _appHost = appHost;
 3466        _itemTypeLookup = itemTypeLookup;
 3467        _serverConfigurationManager = serverConfigurationManager;
 3468        _logger = logger;
 3469    }
 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    {
 496110        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 111        {
 0112            query.Limit = query.Limit.Value + 4;
 113        }
 114
 496115        if (query.IsResumable ?? false)
 116        {
 1117            query.IsVirtualItem = false;
 118        }
 496119    }
 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    {
 496165        if (!query.GroupByPresentationUniqueKey)
 166        {
 150167            return false;
 168        }
 169
 170        // Resume queries surface the actually-played version (which may be an alternate sharing the
 171        // primary's presentation key). The resumable filter already keeps one version per group, so
 172        // presentation-key grouping must not collapse the surfaced version back onto the primary.
 346173        if (query.IsResumable == true)
 174        {
 1175            return false;
 176        }
 177
 345178        if (query.GroupBySeriesPresentationUniqueKey)
 179        {
 0180            return false;
 181        }
 182
 345183        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 184        {
 0185            return false;
 186        }
 187
 345188        if (query.User is null)
 189        {
 342190            return false;
 191        }
 192
 3193        if (query.IncludeItemTypes.Length == 0)
 194        {
 0195            return true;
 196        }
 197
 3198        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 3199            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 3200            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 3201            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 3202            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 3203            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 204    }
 205
 206    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 207    {
 0208        return BaseItemMapper.MapImageToEntity(baseItemId, e);
 209    }
 210
 211    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 212    {
 0213        return BaseItemMapper.MapImageFromEntity(e, appHost);
 214    }
 215
 216    private string? GetPathToSave(string path)
 217    {
 0218        if (path is null)
 219        {
 0220            return null;
 221        }
 222
 0223        return _appHost.ReverseVirtualPath(path);
 224    }
 225
 226    /// <inheritdoc />
 227    public BaseItemDto? DeserializeBaseItem(BaseItemEntity entity, bool skipDeserialization = false)
 228    {
 77229        ArgumentNullException.ThrowIfNull(entity, nameof(entity));
 77230        if (_serverConfigurationManager?.Configuration is null)
 231        {
 0232            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 233        }
 234
 77235        var typeToSerialise = BaseItemMapper.GetType(entity.Type);
 77236        return BaseItemMapper.DeserializeBaseItem(
 77237            entity,
 77238            _logger,
 77239            _appHost,
 77240            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 241    }
 242}

/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    {
 49628        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 49629        dbQuery = dbQuery.AsSingleQuery();
 30
 49631        return dbQuery;
 32    }
 33
 34    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 35    {
 9036        dbQuery = TranslateQuery(dbQuery, context, filter);
 9037        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 9038        dbQuery = ApplyQueryPaging(dbQuery, filter);
 9039        dbQuery = ApplyNavigations(dbQuery, filter);
 9040        return dbQuery;
 41    }
 42
 43    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 44    {
 49645        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 46        {
 11347            var offset = filter.StartIndex ?? 0;
 48
 11349            if (offset > 0)
 50            {
 051                dbQuery = dbQuery.Skip(offset);
 52            }
 53
 11354            if (filter.Limit.HasValue)
 55            {
 11356                dbQuery = dbQuery.Take(filter.Limit.Value);
 57            }
 58        }
 59
 49660        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), preferring the
 66        // primary version (PrimaryVersionId is null) so detail pages and actions target it instead
 67        // of an arbitrary alternate. Keep the grouped ids as an IQueryable sub-select; materializing
 68        // to a List would inline one bound parameter per id and hit SQLite's variable cap.
 49669        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 49670        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 71        {
 072            var groupedIds = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey })
 073                .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
 074            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 75        }
 49676        else if (enableGroupByPresentationUniqueKey)
 77        {
 278            var groupedIds = dbQuery.GroupBy(e => e.PresentationUniqueKey)
 279                .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
 280            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 81        }
 49482        else if (filter.GroupBySeriesPresentationUniqueKey)
 83        {
 084            var groupedIds = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.Min(x => x.Id));
 085            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 86        }
 87        else
 88        {
 49489            dbQuery = dbQuery.Distinct();
 90        }
 91
 49692        if (filter.CollapseBoxSetItems == true)
 93        {
 094            dbQuery = ApplyBoxSetCollapsing(context, dbQuery, filter.CollapseBoxSetItemTypes);
 95
 96            // Name filters run after collapse so BoxSets match by their own name, not a child's.
 097            dbQuery = ApplyNameFilters(dbQuery, filter);
 98        }
 99
 496100        dbQuery = ApplyOrder(dbQuery, filter, context);
 101
 496102        return dbQuery;
 103    }
 104
 105    private IQueryable<BaseItemEntity> ApplyBoxSetCollapsing(
 106        JellyfinDbContext context,
 107        IQueryable<BaseItemEntity> dbQuery,
 108        BaseItemKind[] collapsibleTypes)
 109    {
 0110        var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
 111
 0112        var currentIds = dbQuery.Select(e => e.Id);
 113
 0114        if (collapsibleTypes.Length == 0)
 115        {
 116            // Collapse all item types into box sets
 0117            return ApplyBoxSetCollapsingAll(context, currentIds, boxSetTypeName);
 118        }
 119
 120        // Only collapse specific item types, keep others untouched
 0121        var collapsibleTypeNames = collapsibleTypes.Select(t => _itemTypeLookup.BaseItemKindNames[t]).ToList();
 122
 123        // Categorize items in currentIds in a single pass to avoid multiple correlated EXISTS over BaseItems.
 0124        var categorized = context.BaseItems
 0125            .AsNoTracking()
 0126            .Where(bi => currentIds.Contains(bi.Id))
 0127            .Select(bi => new
 0128            {
 0129                bi.Id,
 0130                IsCollapsible = collapsibleTypeNames.Contains(bi.Type),
 0131                IsBoxSet = bi.Type == boxSetTypeName
 0132            });
 133
 0134        var collapsibleChildIds = categorized.Where(c => c.IsCollapsible).Select(c => c.Id);
 135
 136        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0137        var manualBoxSetLinks = context.LinkedChildren
 0138            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0139                && currentIds.Contains(lc.ChildId))
 0140            .Join(
 0141                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0142                lc => lc.ParentId,
 0143                bs => bs.Id,
 0144                (lc, bs) => new { lc.ChildId, lc.ParentId });
 145
 0146        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 147
 148        // Items whose type is NOT collapsible (always kept in results)
 0149        var nonCollapsibleIds = categorized.Where(c => !c.IsCollapsible).Select(c => c.Id);
 150
 151        // Collapsible items that are not a BoxSet themselves and not a manual child of any BoxSet
 0152        var collapsibleNotInBoxSet = categorized
 0153            .Where(c => c.IsCollapsible && !c.IsBoxSet)
 0154            .Select(c => c.Id)
 0155            .Where(id => !childrenInBoxSet.Contains(id));
 156
 157        // BoxSet IDs containing at least one collapsible child item from currentIds
 0158        var boxSetIds = manualBoxSetLinks
 0159            .Where(x => collapsibleChildIds.Contains(x.ChildId))
 0160            .Select(x => x.ParentId)
 0161            .Distinct();
 162
 0163        var collapsedIds = nonCollapsibleIds.Union(collapsibleNotInBoxSet).Union(boxSetIds);
 0164        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 165    }
 166
 167    private static IQueryable<BaseItemEntity> ApplyBoxSetCollapsingAll(
 168        JellyfinDbContext context,
 169        IQueryable<Guid> currentIds,
 170        string boxSetTypeName)
 171    {
 172        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0173        var manualBoxSetLinks = context.LinkedChildren
 0174            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0175                && currentIds.Contains(lc.ChildId))
 0176            .Join(
 0177                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0178                lc => lc.ParentId,
 0179                bs => bs.Id,
 0180                (lc, bs) => new { lc.ChildId, lc.ParentId });
 181
 0182        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 0183        var boxSetIds = manualBoxSetLinks.Select(x => x.ParentId).Distinct();
 184
 185        // Items in currentIds that are not BoxSets themselves and not a manual child of any BoxSet
 0186        var notInBoxSet = context.BaseItems
 0187            .AsNoTracking()
 0188            .Where(e => currentIds.Contains(e.Id) && e.Type != boxSetTypeName)
 0189            .Select(e => e.Id)
 0190            .Where(id => !childrenInBoxSet.Contains(id));
 191
 0192        var collapsedIds = notInBoxSet.Union(boxSetIds);
 0193        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 194    }
 195
 196    private static IQueryable<BaseItemEntity> ApplyNameFilters(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 197    {
 496198        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 199        {
 0200            var nameStartsWithLower = filter.NameStartsWith.ToLowerInvariant();
 0201            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().StartsWith(nameStartsWithLower));
 202        }
 203
 496204        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 205        {
 0206            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 0207            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(startsOrGreaterLower) >= 0);
 208        }
 209
 496210        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 211        {
 0212            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 0213            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(lessThanLower) < 0);
 214        }
 215
 496216        return dbQuery;
 217    }
 218
 219    /// <inheritdoc />
 220    public IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 221    {
 428222        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 223        {
 0224            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 225        }
 226
 428227        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 228        {
 427229            dbQuery = dbQuery.Include(e => e.Provider);
 230        }
 231
 428232        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 233        {
 427234            dbQuery = dbQuery.Include(e => e.LockedFields);
 235        }
 236
 428237        if (filter.DtoOptions.EnableUserData)
 238        {
 428239            dbQuery = dbQuery.Include(e => e.UserData);
 240        }
 241
 428242        if (filter.DtoOptions.EnableImages)
 243        {
 428244            dbQuery = dbQuery.Include(e => e.Images);
 245        }
 246
 247        // Include LinkedChildEntities for container types and videos that use them
 248        // (BoxSet, Playlist, CollectionFolder for manual linking; Video, Movie for alternate versions).
 249        // When IncludeItemTypes is empty (any type may be returned), always include them to ensure
 250        // LinkedChildren are loaded before items are saved back, preventing accidental deletion.
 428251        var linkedChildTypes = new[]
 428252        {
 428253            BaseItemKind.BoxSet,
 428254            BaseItemKind.Playlist,
 428255            BaseItemKind.CollectionFolder,
 428256            BaseItemKind.Video,
 428257            BaseItemKind.Movie
 428258        };
 428259        if (filter.IncludeItemTypes.Length == 0 || filter.IncludeItemTypes.Any(linkedChildTypes.Contains))
 260        {
 263261            dbQuery = dbQuery.Include(e => e.LinkedChildEntities);
 262        }
 263
 428264        if (filter.IncludeExtras)
 265        {
 0266            dbQuery = dbQuery.Include(e => e.Extras);
 267        }
 268
 428269        return dbQuery;
 270    }
 271
 272    /// <inheritdoc />
 273    public IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDb
 274    {
 496275        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 496276        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 277
 278        // SeriesDatePlayed requires special handling to avoid correlated subqueries.
 279        // Instead of running a MAX() subquery per-row in ORDER BY, we pre-aggregate
 280        // max played dates per series in one query and left-join it.
 496281        if (!hasSearch && orderBy.Any(o => o.OrderBy == ItemSortBy.SeriesDatePlayed))
 282        {
 0283            return ApplySeriesDatePlayedOrder(query, filter, context, orderBy);
 284        }
 285
 496286        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 287
 496288        if (hasSearch)
 289        {
 0290            var relevanceExpression = OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!);
 0291            orderedQuery = query.OrderBy(relevanceExpression);
 292        }
 293
 496294        if (orderBy.Length > 0)
 295        {
 120296            var firstOrdering = orderBy[0];
 120297            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 298
 120299            if (orderedQuery is null)
 300            {
 120301                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 120302                    ? query.OrderBy(expression)
 120303                    : query.OrderByDescending(expression);
 304            }
 305            else
 306            {
 0307                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 0308                    ? orderedQuery.ThenBy(expression)
 0309                    : orderedQuery.ThenByDescending(expression);
 310            }
 311
 120312            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 313            {
 6314                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 6315                    ? orderedQuery.ThenBy(e => e.Name)
 6316                    : orderedQuery.ThenByDescending(e => e.Name);
 317            }
 318
 330319            foreach (var item in orderBy.Skip(1))
 320            {
 45321                expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 45322                orderedQuery = item.SortOrder == SortOrder.Ascending
 45323                    ? orderedQuery.ThenBy(expression)
 45324                    : orderedQuery.ThenByDescending(expression);
 325            }
 326        }
 327
 496328        if (orderedQuery is null)
 329        {
 376330            return query.OrderBy(e => e.SortName);
 331        }
 332
 333        // Add SortName as final tiebreaker
 120334        if (!hasSearch && (orderBy.Length == 0 || orderBy.All(o => o.OrderBy is not ItemSortBy.SortName and not ItemSort
 335        {
 69336            orderedQuery = orderedQuery.ThenBy(e => e.SortName);
 337        }
 338
 120339        return orderedQuery;
 340    }
 341
 342    private IQueryable<BaseItemEntity> ApplySeriesDatePlayedOrder(
 343        IQueryable<BaseItemEntity> query,
 344        InternalItemsQuery filter,
 345        JellyfinDbContext context,
 346        (ItemSortBy OrderBy, SortOrder SortOrder)[] orderBy)
 347    {
 348        // Pre-aggregate max played date per series key in ONE query.
 349        // This generates a single: SELECT SeriesPresentationUniqueKey, MAX(LastPlayedDate) ... GROUP BY
 350        // instead of a correlated subquery per outer row.
 0351        IQueryable<UserData> userDataQuery = filter.User is not null
 0352            ? context.UserData.Where(ud => ud.UserId == filter.User.Id && ud.Played)
 0353            : context.UserData.Where(ud => ud.Played);
 354
 0355        var seriesMaxDates = userDataQuery
 0356            .Join(
 0357                context.BaseItems,
 0358                ud => ud.ItemId,
 0359                bi => bi.Id,
 0360                (ud, bi) => new { bi.SeriesPresentationUniqueKey, ud.LastPlayedDate })
 0361            .Where(x => x.SeriesPresentationUniqueKey != null)
 0362            .GroupBy(x => x.SeriesPresentationUniqueKey)
 0363            .Select(g => new { SeriesKey = g.Key!, MaxDate = g.Max(x => x.LastPlayedDate) });
 364
 0365        var joined = query.LeftJoin(
 0366            seriesMaxDates,
 0367            e => e.PresentationUniqueKey,
 0368            s => s.SeriesKey,
 0369            (e, s) => new { Item = e, MaxDate = s != null ? s.MaxDate : (DateTime?)null });
 370
 0371        var seriesSort = orderBy.First(o => o.OrderBy == ItemSortBy.SeriesDatePlayed);
 372
 0373        return seriesSort.SortOrder == SortOrder.Ascending
 0374            ? joined.OrderBy(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item)
 0375            : joined.OrderByDescending(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item);
 376    }
 377
 378    /// <summary>
 379    /// Builds a query for descendants of an ancestor with user access filtering applied.
 380    /// Uses recursive CTE to traverse both hierarchical (AncestorIds) and linked (LinkedChildren) relationships.
 381    /// </summary>
 382    /// <inheritdoc />
 383    public IQueryable<BaseItemEntity> BuildAccessFilteredDescendantsQuery(
 384        JellyfinDbContext context,
 385        InternalItemsQuery filter,
 386        Guid ancestorId)
 387    {
 388        // Use recursive CTE to get all descendants (hierarchical and linked)
 0389        var allDescendantIds = DescendantQueryHelper.GetAllDescendantIds(context, ancestorId);
 390
 0391        var baseQuery = context.BaseItems
 0392            .AsNoTracking()
 0393            .Where(b => allDescendantIds.Contains(b.Id) && !b.IsFolder && !b.IsVirtualItem);
 394
 0395        return ApplyAccessFiltering(context, baseQuery, filter);
 396    }
 397
 398    /// <summary>
 399    /// Applies user access filtering to a query.
 400    /// Includes TopParentIds, parental rating, and tag filtering.
 401    /// </summary>
 402    /// <inheritdoc />
 403    public IQueryable<BaseItemEntity> ApplyAccessFiltering(
 404        JellyfinDbContext context,
 405        IQueryable<BaseItemEntity> baseQuery,
 406        InternalItemsQuery filter)
 407    {
 408        // Apply TopParentIds filtering (library folder access)
 0409        if (filter.TopParentIds.Length > 0)
 410        {
 0411            var topParentIds = filter.TopParentIds;
 0412            baseQuery = baseQuery.Where(e => topParentIds.Contains(e.TopParentId!.Value));
 413        }
 414
 415        // Apply parental rating filtering
 0416        if (filter.MaxParentalRating is not null)
 417        {
 0418            baseQuery = baseQuery.Where(BuildMaxParentalRatingFilter(context, filter.MaxParentalRating));
 419        }
 420
 421        // Apply block unrated items filtering
 0422        if (filter.BlockUnratedItems.Length > 0)
 423        {
 0424            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0425            baseQuery = baseQuery.Where(e =>
 0426                e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType));
 427        }
 428
 429        // Apply excluded tags filtering (blocked tags).
 430        // Pre-build the blocked-item-id set as a sub-select; then four index-seek Contains checks
 431        // instead of one EXISTS over a 4-way OR predicate that defeats index seeks.
 0432        if (filter.ExcludeInheritedTags.Length > 0)
 433        {
 0434            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0435            var blockedTagItemIds = context.ItemValuesMap
 0436                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 0437                .Select(f => f.ItemId);
 438
 0439            baseQuery = baseQuery.Where(e =>
 0440                !blockedTagItemIds.Contains(e.Id)
 0441                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 0442                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 0443                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 444        }
 445
 446        // Apply included tags filtering (allowed tags - item must have at least one).
 0447        if (filter.IncludeInheritedTags.Length > 0)
 448        {
 0449            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0450            var allowedTagItemIds = context.ItemValuesMap
 0451                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 0452                .Select(f => f.ItemId);
 453
 0454            baseQuery = baseQuery.Where(e =>
 0455                allowedTagItemIds.Contains(e.Id)
 0456                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 0457                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 0458                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value)));
 459        }
 460
 461        // Exclude alternate versions (have PrimaryVersionId set) and owned non-extra items.
 462        // Extras (trailers, etc.) have OwnerId set but also have ExtraType set — keep those.
 0463        if (!filter.IncludeOwnedItems)
 464        {
 0465            baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 466        }
 467
 0468        return baseQuery;
 469    }
 470
 471    /// <summary>
 472    /// Builds a filter expression for max parental rating that handles both rated items
 473    /// and unrated BoxSets/Playlists (which check linked children's ratings).
 474    /// </summary>
 475    private static Expression<Func<BaseItemEntity, bool>> BuildMaxParentalRatingFilter(
 476        JellyfinDbContext context,
 477        ParentalRatingScore maxRating)
 478    {
 54479        var maxScore = maxRating.Score;
 54480        var maxSubScore = maxRating.SubScore ?? 0;
 54481        var linkedChildren = context.LinkedChildren;
 482
 54483        return e =>
 54484            // Item has a rating: check against limit
 54485            (e.InheritedParentalRatingValue != null
 54486                && (e.InheritedParentalRatingValue < maxScore
 54487                    || (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSub
 54488            // Item has no rating
 54489            || (e.InheritedParentalRatingValue == null
 54490                && (
 54491                    // No linked children (not a BoxSet/Playlist): pass as unrated
 54492                    !linkedChildren.Any(lc => lc.ParentId == e.Id)
 54493                    // Has linked children: at least one child must be within limits
 54494                    || linkedChildren.Any(lc => lc.ParentId == e.Id
 54495                        && (lc.Child!.InheritedParentalRatingValue == null
 54496                            || lc.Child.InheritedParentalRatingValue < maxScore
 54497                            || (lc.Child.InheritedParentalRatingValue == maxScore
 54498                                && (lc.Child.InheritedParentalRatingSubValue ?? 0) <= maxSubScore)))));
 499    }
 500
 501    /// <inheritdoc />
 502    public IQueryable<Guid> GetFullyPlayedFolderIdsQuery(JellyfinDbContext context, IQueryable<Guid> folderIds, User use
 503    {
 0504        ArgumentNullException.ThrowIfNull(context);
 0505        ArgumentNullException.ThrowIfNull(folderIds);
 0506        ArgumentNullException.ThrowIfNull(user);
 507
 0508        var filter = new InternalItemsQuery(user);
 0509        var userId = user.Id;
 510
 0511        var leafItems = context.BaseItems
 0512            .AsNoTracking()
 0513            .Where(b => !b.IsFolder && !b.IsVirtualItem);
 0514        leafItems = ApplyAccessFiltering(context, leafItems, filter);
 515
 0516        var playedLeafItems = leafItems
 0517            .Select(b => new { b.Id, Played = b.UserData!.Any(ud => ud.UserId == userId && ud.Played) });
 518
 0519        var ancestorLeaves = context.AncestorIds
 0520            .Where(a => folderIds.Contains(a.ParentItemId))
 0521            .Join(
 0522                playedLeafItems,
 0523                a => a.ItemId,
 0524                b => b.Id,
 0525                (a, b) => new { FolderId = a.ParentItemId, b.Id, b.Played });
 526
 0527        var linkedLeaves = context.LinkedChildren
 0528            .Where(lc => folderIds.Contains(lc.ParentId))
 0529            .Join(
 0530                playedLeafItems,
 0531                lc => lc.ChildId,
 0532                b => b.Id,
 0533                (lc, b) => new { FolderId = lc.ParentId, b.Id, b.Played });
 534
 0535        var linkedFolderLeaves = context.LinkedChildren
 0536            .Where(lc => folderIds.Contains(lc.ParentId))
 0537            .Join(
 0538                context.BaseItems.Where(b => b.IsFolder),
 0539                lc => lc.ChildId,
 0540                b => b.Id,
 0541                (lc, b) => new { lc.ParentId, FolderChildId = b.Id })
 0542            .Join(
 0543                context.AncestorIds,
 0544                x => x.FolderChildId,
 0545                a => a.ParentItemId,
 0546                (x, a) => new { x.ParentId, DescendantId = a.ItemId })
 0547            .Join(
 0548                playedLeafItems,
 0549                x => x.DescendantId,
 0550                b => b.Id,
 0551                (x, b) => new { FolderId = x.ParentId, b.Id, b.Played });
 552
 0553        return ancestorLeaves
 0554            .Union(linkedLeaves)
 0555            .Union(linkedFolderLeaves)
 0556            .GroupBy(x => x.FolderId)
 0557            .Where(g => g.Select(x => x.Id).Distinct().Count() == g.Where(x => x.Played).Select(x => x.Id).Distinct().Co
 0558            .Select(g => g.Key);
 559    }
 560}

/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    {
 9023        ArgumentNullException.ThrowIfNull(filter);
 9024        PrepareFilterQuery(filter);
 25
 9026        using var context = _dbProvider.CreateDbContext();
 9027        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 9028    }
 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    {
 40669        ArgumentNullException.ThrowIfNull(filter);
 40670        PrepareFilterQuery(filter);
 71
 40672        using var context = _dbProvider.CreateDbContext();
 40673        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 74
 40675        dbQuery = TranslateQuery(dbQuery, context, filter);
 76
 40677        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 40678        dbQuery = ApplyQueryPaging(dbQuery, filter);
 79
 40680        var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
 40681        if (hasRandomSort)
 82        {
 6883            var orderedIds = dbQuery.AsNoTracking().Select(e => e.Id).ToList();
 6884            if (orderedIds.Count == 0)
 85            {
 6886                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
 33899        dbQuery = ApplyNavigations(dbQuery, filter);
 100
 338101        return dbQuery.AsEnumerable().Where(e => e != null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializatio
 406102    }
 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    {
 90447        if (id.IsEmpty())
 448        {
 0449            throw new ArgumentException("Guid can't be empty", nameof(id));
 450        }
 451
 90452        using var context = _dbProvider.CreateDbContext();
 90453        var dbQuery = PrepareItemQuery(context, new()
 90454        {
 90455            DtoOptions = new()
 90456            {
 90457                EnableImages = true
 90458            }
 90459        });
 90460        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 90461            .Include(e => e.Provider)
 90462            .Include(e => e.LockedFields)
 90463            .Include(e => e.UserData)
 90464            .Include(e => e.Images)
 90465            .Include(e => e.LinkedChildEntities)
 90466            .AsSingleQuery();
 467
 90468        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 90469        if (item is null)
 470        {
 90471            return null;
 472        }
 473
 0474        return DeserializeBaseItem(item);
 90475    }
 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
 49644        var minWidth = filter.MinWidth;
 49645        var maxWidth = filter.MaxWidth;
 49646        var now = DateTime.UtcNow;
 47
 49648        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
 49692        if (minWidth.HasValue)
 93        {
 094            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 95        }
 96
 49697        if (filter.MinHeight.HasValue)
 98        {
 099            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 100        }
 101
 496102        if (maxWidth.HasValue)
 103        {
 0104            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 105        }
 106
 496107        if (filter.MaxHeight.HasValue)
 108        {
 0109            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 110        }
 111
 496112        if (filter.IsLocked.HasValue)
 113        {
 54114            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 115        }
 116
 496117        var tags = filter.Tags.ToList();
 496118        var excludeTags = filter.ExcludeTags.ToList();
 119
 496120        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
 496133        if (filter.IsSeries.HasValue)
 134        {
 0135            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 136        }
 137
 496138        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
 496150        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
 496162        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
 496174        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
 496191        if (filter.IsFolder.HasValue)
 192        {
 22193            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 194        }
 195
 496196        var includeTypes = filter.IncludeItemTypes;
 197
 198        // Only specify excluded types if no included types are specified
 496199        if (filter.IncludeItemTypes.Length == 0)
 200        {
 242201            var excludeTypes = filter.ExcludeItemTypes;
 242202            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            }
 242209            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        {
 254225            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 254226            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 227        }
 228
 496229        if (filter.ChannelIds.Count > 0)
 230        {
 0231            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 232        }
 233
 496234        if (!filter.ParentId.IsEmpty())
 235        {
 150236            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 237        }
 238
 496239        if (!string.IsNullOrWhiteSpace(filter.Path))
 240        {
 0241            var pathToQuery = GetPathToSave(filter.Path);
 0242            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 243        }
 244
 496245        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 246        {
 0247            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 248        }
 249
 496250        if (filter.MinCommunityRating.HasValue)
 251        {
 0252            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 253        }
 254
 496255        if (filter.MinIndexNumber.HasValue)
 256        {
 0257            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 258        }
 259
 496260        if (filter.MinParentAndIndexNumber.HasValue)
 261        {
 0262            baseQuery = baseQuery
 0263                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 264        }
 265
 496266        if (filter.MinDateCreated.HasValue)
 267        {
 0268            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 269        }
 270
 496271        if (filter.MinDateLastSaved.HasValue)
 272        {
 0273            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 274        }
 275
 496276        if (filter.MinDateLastSavedForUser.HasValue)
 277        {
 0278            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 279        }
 280
 496281        if (filter.IndexNumber.HasValue)
 282        {
 0283            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 284        }
 285
 496286        if (filter.ParentIndexNumber.HasValue)
 287        {
 0288            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 289        }
 290
 496291        if (filter.ParentIndexNumberNotEquals.HasValue)
 292        {
 0293            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 294        }
 295
 496296        var minEndDate = filter.MinEndDate;
 496297        var maxEndDate = filter.MaxEndDate;
 298
 496299        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
 496311        if (minEndDate.HasValue)
 312        {
 0313            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 314        }
 315
 496316        if (maxEndDate.HasValue)
 317        {
 0318            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 319        }
 320
 496321        if (filter.MinStartDate.HasValue)
 322        {
 0323            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 324        }
 325
 496326        if (filter.MaxStartDate.HasValue)
 327        {
 0328            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 329        }
 330
 496331        if (filter.MinPremiereDate.HasValue)
 332        {
 0333            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 334        }
 335
 496336        if (filter.MaxPremiereDate.HasValue)
 337        {
 0338            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 339        }
 340
 496341        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
 496347        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
 496359        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
 496374        if (!string.IsNullOrWhiteSpace(filter.Person))
 375        {
 0376            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 377        }
 378
 496379        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 380        {
 0381            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 382        }
 383
 496384        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 385        {
 0386            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 387        }
 388
 496389        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
 496403        var nameContains = filter.NameContains;
 496404        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").
 496424        if (filter.CollapseBoxSetItems != true)
 425        {
 496426            baseQuery = ApplyNameFilters(baseQuery, filter);
 427        }
 428
 496429        if (filter.ImageTypes.Length > 0)
 430        {
 113431            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 113432            baseQuery = baseQuery.Where(e => e.Images!.Any(w => imgTypes.Contains(w.ImageType)));
 433        }
 434
 496435        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
 496441        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
 496447        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
 496453        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
 496513        if (filter.IsResumable.HasValue)
 514        {
 1515            var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series);
 1516            var userId = filter.User!.Id;
 1517            var isResumable = filter.IsResumable.Value;
 1518            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 519
 520            // In-progress user data rows; alternate versions track their own progress.
 1521            var inProgress = context.UserData
 1522                .Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0);
 523
 1524            IQueryable<Guid>? resumableSeriesIds = null;
 1525            if (hasSeries)
 526            {
 527                // Aggregate per series in a single GROUP BY pass, instead of three full scans.
 0528                var seriesEpisodeStats = context.BaseItems
 0529                    .AsNoTracking()
 0530                    .Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
 0531                    .GroupBy(e => e.SeriesId!.Value)
 0532                    .Select(g => new
 0533                    {
 0534                        SeriesId = g.Key,
 0535                        HasInProgress = g.Any(e => e.UserData!.Any(ud => ud.UserId == userId && ud.PlaybackPositionTicks
 0536                        HasPlayed = g.Any(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played)),
 0537                        HasUnplayed = g.Any(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
 0538                    });
 539
 540                // A series is resumable if it has an in-progress episode,
 541                // or if it has both played and unplayed episodes (partially watched).
 0542                resumableSeriesIds = seriesEpisodeStats
 0543                    .Where(s => s.HasInProgress || (s.HasPlayed && s.HasUnplayed))
 0544                    .Select(s => s.SeriesId);
 545            }
 546
 1547            if (isResumable)
 548            {
 549                // Resume queries surface the version that was actually played, which may be an alternate.
 550                // Match each version on its own progress rather than coalescing onto the primary.
 1551                var inProgressIds = inProgress.Select(ud => ud.ItemId);
 552
 1553                baseQuery = hasSeries
 1554                    ? baseQuery.Where(e =>
 1555                        (e.Type == seriesTypeName && resumableSeriesIds!.Contains(e.Id))
 1556                        || (e.Type != seriesTypeName && inProgressIds.Contains(e.Id)))
 1557                    : baseQuery.Where(e => inProgressIds.Contains(e.Id));
 558
 559                // When several versions of the same item are in progress, keep only the most recently played one, use i
 1560                baseQuery = baseQuery.Where(e => e.Type == seriesTypeName || !context.BaseItems
 1561                    .Where(s => s.Id != e.Id && (s.PrimaryVersionId ?? s.Id) == (e.PrimaryVersionId ?? e.Id))
 1562                    .Any(s =>
 1563                        inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
 1564                            > inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
 1565                        || (inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
 1566                                == inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
 1567                            && s.Id.CompareTo(e.Id) < 0)));
 568            }
 569            else
 570            {
 571                // Not-resumable queries operate on primaries only.
 0572                var resumableMovieIds = inProgress
 0573                    .Join(context.BaseItems, ud => ud.ItemId, bi => bi.Id, (ud, bi) => bi.PrimaryVersionId ?? bi.Id);
 574
 0575                baseQuery = hasSeries
 0576                    ? baseQuery.Where(e =>
 0577                        (e.Type == seriesTypeName && !resumableSeriesIds!.Contains(e.Id))
 0578                        || (e.Type != seriesTypeName && !resumableMovieIds.Contains(e.Id)))
 0579                    : baseQuery.Where(e => !resumableMovieIds.Contains(e.Id));
 580            }
 581        }
 582
 496583        if (filter.ArtistIds.Length > 0)
 584        {
 0585            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 586        }
 587
 496588        if (filter.AlbumArtistIds.Length > 0)
 589        {
 0590            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 591        }
 592
 496593        if (filter.ContributingArtistIds.Length > 0)
 594        {
 0595            var contributingNames = context.BaseItems
 0596                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 0597                .Select(b => b.CleanName);
 598
 0599            baseQuery = baseQuery.Where(e =>
 0600                e.ItemValues!.Any(ivm =>
 0601                    ivm.ItemValue.Type == ItemValueType.Artist &&
 0602                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 0603                &&
 0604                !e.ItemValues!.Any(ivm =>
 0605                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 0606                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 607        }
 608
 496609        if (filter.AlbumIds.Length > 0)
 610        {
 0611            baseQuery = baseQuery.Where(e => e.ParentId.HasValue && filter.AlbumIds.Contains(e.ParentId.Value));
 612        }
 613
 496614        if (filter.ExcludeArtistIds.Length > 0)
 615        {
 0616            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 617        }
 618
 496619        if (filter.GenreIds.Count > 0)
 620        {
 0621            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 622        }
 623
 496624        if (filter.Genres.Count > 0)
 625        {
 0626            var cleanGenres = filter.Genres.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValu
 0627            baseQuery = baseQuery
 0628                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 629        }
 630
 496631        if (tags.Count > 0)
 632        {
 0633            var cleanValues = tags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, str
 0634            baseQuery = baseQuery
 0635                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 636        }
 637
 496638        if (excludeTags.Count > 0)
 639        {
 0640            var cleanValues = excludeTags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueM
 0641            baseQuery = baseQuery
 0642                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 643        }
 644
 496645        if (filter.StudioIds.Length > 0)
 646        {
 0647            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 648        }
 649
 496650        if (filter.OfficialRatings.Length > 0)
 651        {
 0652            var ratings = filter.OfficialRatings;
 0653            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, e => ratings.Contains(e.OfficialRating));
 654        }
 655
 496656        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 496657        if (filter.MinParentalRating != null)
 658        {
 0659            var min = filter.MinParentalRating;
 0660            var minScore = min.Score;
 0661            var minSubScore = min.SubScore ?? 0;
 662
 0663            minParentalRatingFilter = e =>
 0664                e.InheritedParentalRatingValue == null ||
 0665                e.InheritedParentalRatingValue > minScore ||
 0666                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 667        }
 668
 496669        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 496670        if (filter.MaxParentalRating != null)
 671        {
 54672            maxParentalRatingFilter = BuildMaxParentalRatingFilter(context, filter.MaxParentalRating);
 673        }
 674
 496675        if (filter.HasParentalRating ?? false)
 676        {
 0677            if (minParentalRatingFilter != null)
 678            {
 0679                baseQuery = baseQuery.Where(minParentalRatingFilter);
 680            }
 681
 0682            if (maxParentalRatingFilter != null)
 683            {
 0684                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 685            }
 686        }
 496687        else if (filter.BlockUnratedItems.Length > 0)
 688        {
 0689            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0690            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 691
 0692            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 693            {
 0694                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 695            }
 0696            else if (minParentalRatingFilter != null)
 697            {
 0698                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 699            }
 0700            else if (maxParentalRatingFilter != null)
 701            {
 0702                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 703            }
 704            else
 705            {
 0706                baseQuery = baseQuery.Where(unratedItemFilter);
 707            }
 708        }
 496709        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 710        {
 54711            if (minParentalRatingFilter != null)
 712            {
 0713                baseQuery = baseQuery.Where(minParentalRatingFilter);
 714            }
 715
 54716            if (maxParentalRatingFilter != null)
 717            {
 54718                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 719            }
 720        }
 442721        else if (!filter.HasParentalRating ?? false)
 722        {
 0723            baseQuery = baseQuery
 0724                .Where(e => e.InheritedParentalRatingValue == null);
 725        }
 726
 496727        if (filter.HasOfficialRating.HasValue)
 728        {
 0729            Expression<Func<BaseItemEntity, bool>> hasRating =
 0730                e => e.OfficialRating != null && e.OfficialRating != string.Empty;
 731
 0732            baseQuery = filter.HasOfficialRating.Value
 0733                ? baseQuery.WhereItemOrDescendantMatches(context, hasRating)
 0734                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasRating);
 735        }
 736
 496737        if (filter.HasOverview.HasValue)
 738        {
 0739            if (filter.HasOverview.Value)
 740            {
 0741                baseQuery = baseQuery
 0742                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 743            }
 744            else
 745            {
 0746                baseQuery = baseQuery
 0747                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 748            }
 749        }
 750
 496751        if (filter.HasOwnerId.HasValue)
 752        {
 0753            if (filter.HasOwnerId.Value)
 754            {
 0755                baseQuery = baseQuery
 0756                    .Where(e => e.OwnerId != null);
 757            }
 758            else
 759            {
 0760                baseQuery = baseQuery
 0761                    .Where(e => e.OwnerId == null);
 762            }
 763        }
 496764        else if (filter.OwnerIds.Length == 0 && filter.ExtraTypes.Length == 0 && !filter.IncludeOwnedItems)
 765        {
 766            // Exclude owned non-extra items from general queries.
 767            // Extras (trailers, etc.) have OwnerId set but also have ExtraType set - keep those.
 768            // Alternate versions (PrimaryVersionId set) are normally excluded too, but resume queries
 769            // keep them so the actually-played version can surface instead of collapsing onto the primary.
 465770            baseQuery = filter.IsResumable == true
 465771                ? baseQuery.Where(e => e.OwnerId == null || e.ExtraType != null)
 465772                : baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 773        }
 774
 496775        if (filter.OwnerIds.Length > 0)
 776        {
 6777            baseQuery = baseQuery.Where(e => e.OwnerId != null && filter.OwnerIds.Contains(e.OwnerId.Value));
 778        }
 779
 496780        if (filter.ExtraTypes.Length > 0)
 781        {
 782            // Convert ExtraType enum to BaseItemExtraType enum via int cast (same underlying values)
 0783            var extraTypeValues = filter.ExtraTypes.Select(e => (BaseItemExtraType?)(int)e).ToArray();
 0784            baseQuery = baseQuery.Where(e => e.ExtraType != null && extraTypeValues.Contains(e.ExtraType));
 785        }
 786
 496787        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 788        {
 0789            var lang = filter.HasNoAudioTrackWithLanguage;
 0790            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 791
 0792            baseQuery = baseQuery
 0793                .Where(e =>
 0794                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Audio && ms.Langua
 0795                    || (e.IsFolder && !foldersWithAudio.Contains(e.Id)));
 796        }
 797
 496798        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 799        {
 0800            var lang = filter.HasNoInternalSubtitleTrackWithLanguage;
 0801            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 802
 0803            baseQuery = baseQuery
 0804                .Where(e =>
 0805                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && !ms.Is
 0806                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 807        }
 808
 496809        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 810        {
 0811            var lang = filter.HasNoExternalSubtitleTrackWithLanguage;
 0812            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 813
 0814            baseQuery = baseQuery
 0815                .Where(e =>
 0816                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.IsE
 0817                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 818        }
 819
 496820        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 821        {
 0822            var lang = filter.HasNoSubtitleTrackWithLanguage;
 0823            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 824
 0825            baseQuery = baseQuery
 0826                .Where(e =>
 0827                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.Lan
 0828                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 829        }
 830
 496831        if (filter.HasSubtitles.HasValue)
 832        {
 0833            var hasSubtitles = filter.HasSubtitles.Value;
 0834            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasSubtitles());
 0835            if (hasSubtitles)
 836            {
 0837                baseQuery = baseQuery
 0838                    .Where(e =>
 0839                        (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0840                        || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 841            }
 842            else
 843            {
 0844                baseQuery = baseQuery
 0845                    .Where(e =>
 0846                        (!e.IsFolder && !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0847                        || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 848            }
 849        }
 850
 496851        if (filter.SubtitleLanguages.Count > 0)
 852        {
 0853            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 0854            baseQuery = baseQuery
 0855                .Where(e =>
 0856                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle
 0857                     && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && s
 0858                    || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 859        }
 860
 496861        if (filter.AudioLanguages.Count > 0)
 862        {
 0863            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 0864            baseQuery = baseQuery
 0865                .Where(e =>
 0866                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio
 0867                     && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.
 0868                    || (e.IsFolder && foldersWithAudio.Contains(e.Id)));
 869        }
 870
 496871        if (filter.HasChapterImages.HasValue)
 872        {
 0873            var hasChapterImages = filter.HasChapterImages.Value;
 0874            var foldersWithChapterImages = DescendantQueryHelper.GetFolderIdsMatching(context, new HasChapterImages());
 0875            if (hasChapterImages)
 876            {
 0877                baseQuery = baseQuery
 0878                    .Where(e =>
 0879                        (!e.IsFolder && e.Chapters!.Any(f => f.ImagePath != null))
 0880                        || (e.IsFolder && foldersWithChapterImages.Contains(e.Id)));
 881            }
 882            else
 883            {
 0884                baseQuery = baseQuery
 0885                    .Where(e =>
 0886                        (!e.IsFolder && !e.Chapters!.Any(f => f.ImagePath != null))
 0887                        || (e.IsFolder && !foldersWithChapterImages.Contains(e.Id)));
 888            }
 889        }
 890
 496891        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 892        {
 18893            baseQuery = baseQuery
 18894                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 895        }
 896
 496897        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 898        {
 18899            baseQuery = baseQuery
 18900                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 901        }
 902
 496903        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 904        {
 18905            baseQuery = baseQuery
 18906                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 907        }
 908
 496909        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 910        {
 18911            baseQuery = baseQuery
 18912                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 913        }
 914
 496915        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 916        {
 0917            baseQuery = baseQuery
 0918                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 919        }
 920
 496921        if (filter.Years.Length > 0)
 922        {
 0923            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 924        }
 925
 496926        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 496927        if (isVirtualItem.HasValue)
 928        {
 23929            baseQuery = baseQuery
 23930                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 931        }
 932
 496933        if (filter.IsSpecialSeason.HasValue)
 934        {
 0935            if (filter.IsSpecialSeason.Value)
 936            {
 0937                baseQuery = baseQuery
 0938                    .Where(e => e.IndexNumber == 0);
 939            }
 940            else
 941            {
 0942                baseQuery = baseQuery
 0943                    .Where(e => e.IndexNumber != 0);
 944            }
 945        }
 946
 496947        if (filter.IsUnaired.HasValue)
 948        {
 0949            if (filter.IsUnaired.Value)
 950            {
 0951                baseQuery = baseQuery
 0952                    .Where(e => e.PremiereDate >= now);
 953            }
 954            else
 955            {
 0956                baseQuery = baseQuery
 0957                    .Where(e => e.PremiereDate < now);
 958            }
 959        }
 960
 496961        if (filter.MediaTypes.Length > 0)
 962        {
 22963            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 22964            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 965        }
 966
 496967        if (filter.ItemIds.Length > 0)
 968        {
 0969            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 970        }
 971
 496972        if (filter.ExcludeItemIds.Length > 0)
 973        {
 0974            baseQuery = baseQuery
 0975                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 976        }
 977
 496978        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 979        {
 0980            baseQuery = baseQuery.WhereExcludeProviderIds(filter.ExcludeProviderIds);
 981        }
 982
 496983        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 984        {
 0985            baseQuery = baseQuery.WhereHasAnyProviderId(filter.HasAnyProviderId);
 986        }
 987
 496988        if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
 989        {
 0990            baseQuery = baseQuery.WhereHasAnyProviderIds(filter.HasAnyProviderIds);
 991        }
 992
 496993        if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
 994        {
 0995            var includeAny = filter.HasAnyProviderIds
 0996                .SelectMany(kvp => kvp.Value.Select(v => $"{kvp.Key}:{v}"))
 0997                .ToArray();
 0998            if (includeAny.Length > 0)
 999            {
 01000                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 1001            }
 1002        }
 1003
 4961004        if (filter.HasImdbId.HasValue)
 1005        {
 01006            baseQuery = filter.HasImdbId.Value
 01007                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == ImdbProviderName))
 01008                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != ImdbProviderName));
 1009        }
 1010
 4961011        if (filter.HasTmdbId.HasValue)
 1012        {
 01013            baseQuery = filter.HasTmdbId.Value
 01014                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TmdbProviderName))
 01015                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TmdbProviderName));
 1016        }
 1017
 4961018        if (filter.HasTvdbId.HasValue)
 1019        {
 01020            baseQuery = filter.HasTvdbId.Value
 01021                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TvdbProviderName))
 01022                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TvdbProviderName));
 1023        }
 1024
 4961025        var queryTopParentIds = filter.TopParentIds;
 1026
 4961027        if (queryTopParentIds.Length > 0)
 1028        {
 151029            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 151030            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 151031            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 1032            {
 01033                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 1034            }
 1035            else
 1036            {
 151037                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 1038            }
 1039        }
 1040
 4961041        if (filter.AncestorIds.Length > 0)
 1042        {
 461043            var ancestorFilter = filter.AncestorIds.OneOrManyExpressionBuilder<AncestorId, Guid>(f => f.ParentItemId);
 461044            baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
 1045        }
 1046
 4961047        if (filter.LinkedChildAncestorIds.Length > 0)
 1048        {
 1049            // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ances
 01050            var linkedChildAncestorIds = filter.LinkedChildAncestorIds;
 01051            baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc =>
 01052                lc.ParentId == e.Id
 01053                && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId))));
 1054        }
 1055
 4961056        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 1057        {
 01058            baseQuery = baseQuery
 01059                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 1060        }
 1061
 4961062        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 1063        {
 01064            baseQuery = baseQuery
 01065                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 1066        }
 1067
 1068        // Pre-build the blocked-item-id set as a sub-select
 4961069        if (filter.ExcludeInheritedTags.Length > 0)
 1070        {
 01071            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01072            var blockedTagItemIds = context.ItemValuesMap
 01073                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 01074                .Select(f => f.ItemId);
 1075
 01076            baseQuery = baseQuery.Where(e =>
 01077                !blockedTagItemIds.Contains(e.Id)
 01078                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 01079                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 01080                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 1081        }
 1082
 4961083        if (filter.IncludeInheritedTags.Length > 0)
 1084        {
 01085            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01086            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 01087            var allowedTagItemIds = context.ItemValuesMap
 01088                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 01089                .Select(f => f.ItemId);
 1090
 01091            baseQuery = baseQuery.Where(e =>
 01092                allowedTagItemIds.Contains(e.Id)
 01093                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 01094                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 01095                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 01096
 01097                // A playlist should be accessible to its owner regardless of allowed tags
 01098                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 1099        }
 1100
 4961101        if (filter.SeriesStatuses.Length > 0)
 1102        {
 01103            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 01104            baseQuery = baseQuery
 01105                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 1106        }
 1107
 4961108        if (filter.BoxSetLibraryFolders.Length > 0)
 1109        {
 01110            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 01111            baseQuery = baseQuery
 01112                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 1113        }
 1114
 4961115        if (filter.VideoTypes.Length > 0)
 1116        {
 1117            // Dvds and Blu-rays can either be stored in a folder structure or as an iso file
 1118            // => to find all matches we need to check both: VideoType and IsoType
 1119            // alternatively, we could provide specific IsoType filters
 01120            var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
 01121            var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray();
 01122            Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoT
 01123            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
 1124        }
 1125
 4961126        if (filter.Is3D.HasValue)
 1127        {
 01128            Expression<Func<BaseItemEntity, bool>> is3D = e => e.Data!.Contains("Video3DFormat");
 1129
 01130            baseQuery = filter.Is3D.Value
 01131                ? baseQuery.WhereItemOrDescendantMatches(context, is3D)
 01132                : baseQuery.WhereNeitherItemNorDescendantMatches(context, is3D);
 1133        }
 1134
 4961135        if (filter.IsPlaceHolder.HasValue)
 1136        {
 01137            Expression<Func<BaseItemEntity, bool>> isPlaceHolder = e => e.Data!.Contains("IsPlaceHolder\":true");
 1138
 01139            baseQuery = filter.IsPlaceHolder.Value
 01140                ? baseQuery.WhereItemOrDescendantMatches(context, isPlaceHolder)
 01141                : baseQuery.WhereNeitherItemNorDescendantMatches(context, isPlaceHolder);
 1142        }
 1143
 4961144        if (filter.HasSpecialFeature.HasValue)
 1145        {
 01146            var itemsWithExtras = context.BaseItems
 01147                .Where(extra => extra.OwnerId != null
 01148                    && extra.ExtraType != null
 01149                    && extra.ExtraType != BaseItemExtraType.Unknown
 01150                    && extra.ExtraType != BaseItemExtraType.Trailer
 01151                    && extra.ExtraType != BaseItemExtraType.ThemeSong
 01152                    && extra.ExtraType != BaseItemExtraType.ThemeVideo)
 01153                .Select(extra => extra.OwnerId!.Value)
 01154                .Distinct();
 1155
 01156            Expression<Func<BaseItemEntity, bool>> hasExtras = e => itemsWithExtras.Contains(e.Id);
 1157
 01158            baseQuery = filter.HasSpecialFeature.Value
 01159                ? baseQuery.WhereItemOrDescendantMatches(context, hasExtras)
 01160                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasExtras);
 1161        }
 1162
 4961163        if (filter.HasTrailer.HasValue)
 1164        {
 01165            var trailerOwnerIds = context.BaseItems
 01166                .Where(extra => extra.ExtraType == BaseItemExtraType.Trailer && extra.OwnerId != null)
 01167                .Select(extra => extra.OwnerId!.Value);
 1168
 01169            Expression<Func<BaseItemEntity, bool>> hasTrailer = e => trailerOwnerIds.Contains(e.Id);
 1170
 01171            baseQuery = filter.HasTrailer.Value
 01172                ? baseQuery.WhereItemOrDescendantMatches(context, hasTrailer)
 01173                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasTrailer);
 1174        }
 1175
 4961176        if (filter.HasThemeSong.HasValue)
 1177        {
 01178            var themeSongOwnerIds = context.BaseItems
 01179                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeSong && extra.OwnerId != null)
 01180                .Select(extra => extra.OwnerId!.Value);
 1181
 01182            Expression<Func<BaseItemEntity, bool>> hasThemeSong = e => themeSongOwnerIds.Contains(e.Id);
 1183
 01184            baseQuery = filter.HasThemeSong.Value
 01185                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeSong)
 01186                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeSong);
 1187        }
 1188
 4961189        if (filter.HasThemeVideo.HasValue)
 1190        {
 01191            var themeVideoOwnerIds = context.BaseItems
 01192                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeVideo && extra.OwnerId != null)
 01193                .Select(extra => extra.OwnerId!.Value);
 1194
 01195            Expression<Func<BaseItemEntity, bool>> hasThemeVideo = e => themeVideoOwnerIds.Contains(e.Id);
 1196
 01197            baseQuery = filter.HasThemeVideo.Value
 01198                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeVideo)
 01199                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeVideo);
 1200        }
 1201
 4961202        if (filter.AiredDuringSeason.HasValue)
 1203        {
 01204            var seasonNumber = filter.AiredDuringSeason.Value;
 01205            if (seasonNumber < 1)
 1206            {
 01207                baseQuery = baseQuery.Where(e => e.ParentIndexNumber == seasonNumber);
 1208            }
 1209            else
 1210            {
 01211                var seasonStr = seasonNumber.ToString(CultureInfo.InvariantCulture);
 01212                baseQuery = baseQuery.Where(e =>
 01213                    e.ParentIndexNumber == seasonNumber
 01214                    || (e.Data != null && (
 01215                        e.Data.Contains("\"AirsAfterSeasonNumber\":" + seasonStr)
 01216                        || e.Data.Contains("\"AirsBeforeSeasonNumber\":" + seasonStr))));
 1217            }
 1218        }
 1219
 4961220        if (filter.AdjacentTo.HasValue && !filter.AdjacentTo.Value.IsEmpty())
 1221        {
 01222            var adjacentToId = filter.AdjacentTo.Value;
 01223            var targetItem = context.BaseItems.Where(e => e.Id == adjacentToId).Select(e => new { e.SortName, e.Id }).Fi
 01224            if (targetItem is not null)
 1225            {
 01226                var targetSortName = targetItem.SortName ?? string.Empty;
 1227
 1228                // Fetch both prev and next adjacent items in a single query using Concat (UNION ALL).
 01229                var adjacentIds = context.BaseItems
 01230                    .Where(e => string.Compare(e.SortName, targetSortName) < 0)
 01231                    .OrderByDescending(e => e.SortName)
 01232                    .Select(e => e.Id)
 01233                    .Take(1)
 01234                    .Concat(
 01235                        context.BaseItems
 01236                            .Where(e => string.Compare(e.SortName, targetSortName) > 0)
 01237                            .OrderBy(e => e.SortName)
 01238                            .Select(e => e.Id)
 01239                            .Take(1))
 01240                    .ToList();
 1241
 01242                adjacentIds.Add(adjacentToId);
 01243                baseQuery = baseQuery.Where(e => adjacentIds.Contains(e.Id));
 1244            }
 1245        }
 1246
 4961247        return baseQuery;
 1248    }
 1249}

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)