< Summary - Jellyfin

Line coverage
28%
Covered lines: 397
Uncovered lines: 1006
Coverable lines: 1403
Total lines: 2814
Line coverage: 28.2%
Branch coverage
43%
Covered branches: 277
Total branches: 636
Branch coverage: 43.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 51.8% (684/1318) Branch coverage: 50.6% (383/756) Total lines: 26871/29/2026 - 12:13:32 AM Line coverage: 51.4% (689/1338) Branch coverage: 50.5% (386/764) Total lines: 27353/11/2026 - 12:13:45 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/12/2026 - 12:13:54 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.9% (389/764) Total lines: 27364/13/2026 - 12:14:08 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/15/2026 - 12:14:34 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.6% (387/764) Total lines: 27364/16/2026 - 12:15:18 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/19/2026 - 12:14:27 AM Line coverage: 51.8% (717/1384) Branch coverage: 50.3% (386/766) Total lines: 27365/4/2026 - 12:15:16 AM Line coverage: 28.4% (397/1394) Branch coverage: 43.6% (277/634) Total lines: 28075/6/2026 - 12:15:23 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 2814 1/23/2026 - 12:11:06 AM Line coverage: 51.8% (684/1318) Branch coverage: 50.6% (383/756) Total lines: 26871/29/2026 - 12:13:32 AM Line coverage: 51.4% (689/1338) Branch coverage: 50.5% (386/764) Total lines: 27353/11/2026 - 12:13:45 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/12/2026 - 12:13:54 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.9% (389/764) Total lines: 27364/13/2026 - 12:14:08 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/15/2026 - 12:14:34 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.6% (387/764) Total lines: 27364/16/2026 - 12:15:18 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/19/2026 - 12:14:27 AM Line coverage: 51.8% (717/1384) Branch coverage: 50.3% (386/766) Total lines: 27365/4/2026 - 12:15:16 AM Line coverage: 28.4% (397/1394) Branch coverage: 43.6% (277/634) Total lines: 28075/6/2026 - 12:15:23 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 2814

Coverage delta

Coverage delta 24 -24

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: GetAllArtists(...)100%210%
File 1: GetArtists(...)100%210%
File 1: GetAlbumArtists(...)100%210%
File 1: GetStudios(...)100%210%
File 1: GetGenres(...)100%210%
File 1: GetMusicGenres(...)100%210%
File 1: GetStudioNames()100%11100%
File 1: GetAllArtistNames()100%11100%
File 1: GetMusicGenreNames()100%11100%
File 1: GetGenreNames()100%11100%
File 1: GetItemValueNames(...)100%44100%
File 1: GetItemValues(...)0%156120%
File 2: .cctor()100%11100%
File 2: .ctor(...)100%11100%
File 2: Map(...)100%210%
File 2: Map(...)100%11100%
File 2: DeserializeBaseItem(...)100%11100%
File 2: PrepareFilterQuery(...)83.33%6680%
File 2: GetItemByNameTypesInQuery(...)100%1010100%
File 2: IsTypeInQuery(...)75%5466.66%
File 2: EnableGroupByPresentationUniqueKey(...)65%212087.5%
File 2: Map(...)100%210%
File 2: Map(...)100%210%
File 2: GetPathToSave(...)0%620%
File 2: DeserializeBaseItem(...)50%101088.88%
File 3: PrepareItemQuery(...)100%11100%
File 3: ApplyQueryFilter(...)100%11100%
File 3: ApplyQueryPaging(...)87.5%8885.71%
File 3: ApplyGroupingFilter(...)70%151062.5%
File 3: ApplyBoxSetCollapsing(...)0%620%
File 3: ApplyBoxSetCollapsingAll(...)100%210%
File 3: ApplyNameFilters(...)50%14640%
File 3: ApplyNavigations(...)83.33%181891.3%
File 3: ApplyOrder(...)79.41%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(...)50%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(...)45.18%5161037428.45%

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 Jellyfin.Extensions;
 9using MediaBrowser.Controller.Entities;
 10using MediaBrowser.Model.Dto;
 11using MediaBrowser.Model.Querying;
 12using Microsoft.EntityFrameworkCore;
 13using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 14
 15namespace Jellyfin.Server.Implementations.Item;
 16
 17public sealed partial class BaseItemRepository
 18{
 19    /// <inheritdoc />
 20    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 21    {
 022        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 23    }
 24
 25    /// <inheritdoc />
 26    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 27    {
 028        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 29    }
 30
 31    /// <inheritdoc />
 32    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 33    {
 034        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 35    }
 36
 37    /// <inheritdoc />
 38    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 39    {
 040        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 41    }
 42
 43    /// <inheritdoc />
 44    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 45    {
 046        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 47    }
 48
 49    /// <inheritdoc />
 50    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 51    {
 052        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 53    }
 54
 55    /// <inheritdoc />
 56    public IReadOnlyList<string> GetStudioNames()
 57    {
 1658        return GetItemValueNames(_getStudiosValueTypes, [], []);
 59    }
 60
 61    /// <inheritdoc />
 62    public IReadOnlyList<string> GetAllArtistNames()
 63    {
 1664        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 65    }
 66
 67    /// <inheritdoc />
 68    public IReadOnlyList<string> GetMusicGenreNames()
 69    {
 1670        return GetItemValueNames(
 1671            _getGenreValueTypes,
 1672            _itemTypeLookup.MusicGenreTypes,
 1673            []);
 74    }
 75
 76    /// <inheritdoc />
 77    public IReadOnlyList<string> GetGenreNames()
 78    {
 1679        return GetItemValueNames(
 1680            _getGenreValueTypes,
 1681            [],
 1682            _itemTypeLookup.MusicGenreTypes);
 83    }
 84
 85    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 86    {
 6487        using var context = _dbProvider.CreateDbContext();
 88
 6489        var query = context.ItemValuesMap
 6490            .AsNoTracking()
 6491            .Where(e => itemValueTypes.Any(w => w == e.ItemValue.Type));
 6492        if (withItemTypes.Count > 0)
 93        {
 1694            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 95        }
 96
 6497        if (excludeItemTypes.Count > 0)
 98        {
 1699            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 100        }
 101
 64102        return query.Select(e => e.ItemValue)
 64103            .GroupBy(e => e.CleanValue)
 64104            .Select(g => g.Min(v => v.Value)!)
 64105            .ToArray();
 64106    }
 107
 108    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 109    {
 0110        ArgumentNullException.ThrowIfNull(filter);
 111
 0112        if (!filter.Limit.HasValue)
 113        {
 0114            filter.EnableTotalRecordCount = false;
 115        }
 116
 0117        using var context = _dbProvider.CreateDbContext();
 118
 0119        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 0120        {
 0121            ExcludeItemTypes = filter.ExcludeItemTypes,
 0122            IncludeItemTypes = filter.IncludeItemTypes,
 0123            MediaTypes = filter.MediaTypes,
 0124            AncestorIds = filter.AncestorIds,
 0125            ItemIds = filter.ItemIds,
 0126            TopParentIds = filter.TopParentIds,
 0127            ParentId = filter.ParentId,
 0128            IsAiring = filter.IsAiring,
 0129            IsMovie = filter.IsMovie,
 0130            IsSports = filter.IsSports,
 0131            IsKids = filter.IsKids,
 0132            IsNews = filter.IsNews,
 0133            IsSeries = filter.IsSeries
 0134        });
 135
 136        // Keep this as an IQueryable sub-select. Materializing to a list would inline one
 137        // bound parameter per CleanValue and hit SQLite's variable cap on libraries with
 138        // high-cardinality value types (e.g. tens of thousands of artists).
 0139        var matchingCleanValues = context.ItemValuesMap
 0140            .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
 0141            .Join(
 0142                innerQueryFilter,
 0143                ivm => ivm.ItemId,
 0144                g => g.Id,
 0145                (ivm, g) => ivm.ItemValue.CleanValue)
 0146            .Distinct();
 147
 0148        var innerQuery = PrepareItemQuery(context, filter)
 0149            .Where(e => e.Type == returnType)
 0150            .Where(e => matchingCleanValues.Contains(e.CleanName!));
 151
 0152        var outerQueryFilter = new InternalItemsQuery(filter.User)
 0153        {
 0154            IsPlayed = filter.IsPlayed,
 0155            IsFavorite = filter.IsFavorite,
 0156            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 0157            IsLiked = filter.IsLiked,
 0158            IsLocked = filter.IsLocked,
 0159            NameLessThan = filter.NameLessThan,
 0160            NameStartsWith = filter.NameStartsWith,
 0161            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 0162            Tags = filter.Tags,
 0163            OfficialRatings = filter.OfficialRatings,
 0164            StudioIds = filter.StudioIds,
 0165            GenreIds = filter.GenreIds,
 0166            Genres = filter.Genres,
 0167            Years = filter.Years,
 0168            NameContains = filter.NameContains,
 0169            SearchTerm = filter.SearchTerm,
 0170            ExcludeItemIds = filter.ExcludeItemIds
 0171        };
 172
 173        // Build the master query and collapse rows that share a PresentationUniqueKey
 174        // (e.g. alternate versions) by picking the lowest Id per group.
 0175        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter);
 176
 0177        var orderedMasterQuery = ApplyOrder(masterQuery, filter, context)
 0178            .GroupBy(e => e.PresentationUniqueKey)
 0179            .Select(g => g.Min(e => e.Id));
 180
 0181        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 0182        if (filter.EnableTotalRecordCount)
 183        {
 0184            result.TotalRecordCount = orderedMasterQuery.Count();
 185        }
 186
 0187        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 188        {
 0189            orderedMasterQuery = orderedMasterQuery.Skip(filter.StartIndex.Value);
 190        }
 191
 0192        if (filter.Limit.HasValue)
 193        {
 0194            orderedMasterQuery = orderedMasterQuery.Take(filter.Limit.Value);
 195        }
 196
 0197        var masterIds = orderedMasterQuery.ToList();
 198
 0199        var query = ApplyNavigations(
 0200                context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => masterIds.Contains(e.Id)),
 0201                filter);
 202
 0203        query = ApplyOrder(query, filter, context);
 204
 0205        if (filter.IncludeItemTypes.Length > 0)
 206        {
 0207            var typeSubQuery = new InternalItemsQuery(filter.User)
 0208            {
 0209                ExcludeItemTypes = filter.ExcludeItemTypes,
 0210                IncludeItemTypes = filter.IncludeItemTypes,
 0211                MediaTypes = filter.MediaTypes,
 0212                AncestorIds = filter.AncestorIds,
 0213                ExcludeItemIds = filter.ExcludeItemIds,
 0214                ItemIds = filter.ItemIds,
 0215                TopParentIds = filter.TopParentIds,
 0216                ParentId = filter.ParentId,
 0217                IsPlayed = filter.IsPlayed
 0218            };
 219
 0220            var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(Placehol
 0221                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 222
 0223            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 0224            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 0225            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 0226            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 0227            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 0228            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 0229            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 0230            var itemIds = itemCountQuery.Select(e => e.Id);
 231
 232            // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQ
 233            // Instead, start from ItemValueMaps and join with BaseItems
 0234            var countsByCleanName = context.ItemValuesMap
 0235                .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
 0236                .Where(ivm => itemIds.Contains(ivm.ItemId))
 0237                .Join(
 0238                    context.BaseItems,
 0239                    ivm => ivm.ItemId,
 0240                    e => e.Id,
 0241                    (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type })
 0242                .GroupBy(x => new { x.CleanName, x.Type })
 0243                .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() })
 0244                .GroupBy(x => x.CleanName)
 0245                .ToDictionary(
 0246                    g => g.Key,
 0247                    g => new ItemCounts
 0248                    {
 0249                        SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count),
 0250                        EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count),
 0251                        MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count),
 0252                        AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count),
 0253                        ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count),
 0254                        SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count),
 0255                        TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count),
 0256                    });
 257
 0258            result.StartIndex = filter.StartIndex ?? 0;
 0259            result.Items =
 0260            [
 0261                .. query
 0262                    .AsEnumerable()
 0263                    .Where(e => e is not null)
 0264                    .Select(e =>
 0265                    {
 0266                        var item = DeserializeBaseItem(e, filter.SkipDeserialization);
 0267                        countsByCleanName.TryGetValue(e.CleanName ?? string.Empty, out var itemCount);
 0268                        return (item, itemCount);
 0269                    })
 0270                    .Where(x => x.item is not null)
 0271                    .Select(x => (x.item!, x.itemCount))
 0272            ];
 273        }
 274        else
 275        {
 0276            result.StartIndex = filter.StartIndex ?? 0;
 0277            result.Items =
 0278            [
 0279                .. query
 0280                    .AsEnumerable()
 0281                    .Where(e => e != null)
 0282                    .Select(e => DeserializeBaseItem(e, filter.SkipDeserialization))
 0283                    .Where(item => item != null)
 0284                    .Select(item => (item!, (ItemCounts?)null))
 0285            ];
 286        }
 287
 0288        return result;
 0289    }
 290}

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

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

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

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

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

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

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2#pragma warning disable CA1304 // Specify CultureInfo
 3#pragma warning disable CA1309 // Use ordinal string comparison
 4#pragma warning disable CA1311 // Specify a culture or use an invariant version
 5#pragma warning disable CA1307 // Specify StringComparison for clarity
 6#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 7
 8using System;
 9using System.Collections.Generic;
 10using System.Globalization;
 11using System.Linq;
 12using System.Linq.Expressions;
 13using Jellyfin.Data.Enums;
 14using Jellyfin.Database.Implementations;
 15using Jellyfin.Database.Implementations.Entities;
 16using Jellyfin.Database.Implementations.MatchCriteria;
 17using Jellyfin.Extensions;
 18using Jellyfin.Server.Implementations.Extensions;
 19using MediaBrowser.Controller.Entities;
 20using MediaBrowser.Model.Entities;
 21using Microsoft.EntityFrameworkCore;
 22using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 23
 24namespace Jellyfin.Server.Implementations.Item;
 25
 26public sealed partial class BaseItemRepository
 27{
 128    private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
 29
 130    private static readonly string ImdbProviderName = MetadataProvider.Imdb.ToString().ToLowerInvariant();
 131    private static readonly string TmdbProviderName = MetadataProvider.Tmdb.ToString().ToLowerInvariant();
 132    private static readonly string TvdbProviderName = MetadataProvider.Tvdb.ToString().ToLowerInvariant();
 33
 34    /// <inheritdoc />
 35    public IQueryable<BaseItemEntity> TranslateQuery(
 36        IQueryable<BaseItemEntity> baseQuery,
 37        JellyfinDbContext context,
 38        InternalItemsQuery filter)
 39    {
 40        const int HDWidth = 1200;
 41        const int UHDWidth = 3800;
 42        const int UHDHeight = 2100;
 43
 45444        var minWidth = filter.MinWidth;
 45445        var maxWidth = filter.MaxWidth;
 45446        var now = DateTime.UtcNow;
 47
 45448        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 49        {
 050            bool includeSD = false;
 051            bool includeHD = false;
 052            bool include4K = false;
 53
 054            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 55            {
 056                includeSD = true;
 57            }
 58
 059            if (filter.IsHD.HasValue && filter.IsHD.Value)
 60            {
 061                includeHD = true;
 62            }
 63
 064            if (filter.Is4K.HasValue && filter.Is4K.Value)
 65            {
 066                include4K = true;
 67            }
 68
 69            // Non-folders: check own resolution directly (no subquery).
 70            // Folders (Series, BoxSets): EXISTS check on descendants/linked children.
 71            // Using navigation properties (a.Item, lc.Child) produces efficient
 72            // EXISTS + JOIN instead of nested IN (SELECT ...) subqueries.
 073            baseQuery = baseQuery.Where(e =>
 074                (!e.IsFolder && e.Width > 0
 075                    && ((includeSD && e.Width < HDWidth)
 076                        || (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight))
 077                        || (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight))))
 078                || (e.IsFolder
 079                    && (e.Children!.Any(a =>
 080                            a.Item.Width > 0
 081                            && ((includeSD && a.Item.Width < HDWidth)
 082                                || (includeHD && a.Item.Width >= HDWidth && !(a.Item.Width >= UHDWidth || a.Item.Height 
 083                                || (include4K && (a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight))))
 084                        || context.LinkedChildren.Any(lc =>
 085                            lc.ParentId == e.Id
 086                            && lc.Child!.Width > 0
 087                            && ((includeSD && lc.Child.Width < HDWidth)
 088                                || (includeHD && lc.Child.Width >= HDWidth && !(lc.Child.Width >= UHDWidth || lc.Child.H
 089                                || (include4K && (lc.Child.Width >= UHDWidth || lc.Child.Height >= UHDHeight)))))));
 90        }
 91
 45492        if (minWidth.HasValue)
 93        {
 094            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 95        }
 96
 45497        if (filter.MinHeight.HasValue)
 98        {
 099            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 100        }
 101
 454102        if (maxWidth.HasValue)
 103        {
 0104            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 105        }
 106
 454107        if (filter.MaxHeight.HasValue)
 108        {
 0109            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 110        }
 111
 454112        if (filter.IsLocked.HasValue)
 113        {
 48114            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 115        }
 116
 454117        var tags = filter.Tags.ToList();
 454118        var excludeTags = filter.ExcludeTags.ToList();
 119
 454120        if (filter.IsMovie.HasValue)
 121        {
 0122            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 0123                && (filter.IncludeItemTypes.Length == 0
 0124                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 0125                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 126
 0127            if (!shouldIncludeAllMovieTypes)
 128            {
 0129                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 130            }
 131        }
 132
 454133        if (filter.IsSeries.HasValue)
 134        {
 0135            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 136        }
 137
 454138        if (filter.IsSports.HasValue)
 139        {
 0140            if (filter.IsSports.Value)
 141            {
 0142                tags.Add("Sports");
 143            }
 144            else
 145            {
 0146                excludeTags.Add("Sports");
 147            }
 148        }
 149
 454150        if (filter.IsNews.HasValue)
 151        {
 0152            if (filter.IsNews.Value)
 153            {
 0154                tags.Add("News");
 155            }
 156            else
 157            {
 0158                excludeTags.Add("News");
 159            }
 160        }
 161
 454162        if (filter.IsKids.HasValue)
 163        {
 0164            if (filter.IsKids.Value)
 165            {
 0166                tags.Add("Kids");
 167            }
 168            else
 169            {
 0170                excludeTags.Add("Kids");
 171            }
 172        }
 173
 454174        if (!string.IsNullOrEmpty(filter.SearchTerm))
 175        {
 0176            var cleanedSearchTerm = filter.SearchTerm.GetCleanValue();
 0177            var originalSearchTerm = filter.SearchTerm;
 0178            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 179            {
 0180                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 0181                var likeSearchTerm = $"%{originalSearchTerm.Trim('%')}%";
 0182                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 183            }
 184            else
 185            {
 0186                var likeSearchTerm = $"%{originalSearchTerm}%";
 0187                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 188            }
 189        }
 190
 454191        if (filter.IsFolder.HasValue)
 192        {
 21193            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 194        }
 195
 454196        var includeTypes = filter.IncludeItemTypes;
 197
 198        // Only specify excluded types if no included types are specified
 454199        if (filter.IncludeItemTypes.Length == 0)
 200        {
 230201            var excludeTypes = filter.ExcludeItemTypes;
 230202            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            }
 230209            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        {
 224225            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 224226            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 227        }
 228
 454229        if (filter.ChannelIds.Count > 0)
 230        {
 0231            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 232        }
 233
 454234        if (!filter.ParentId.IsEmpty())
 235        {
 144236            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 237        }
 238
 454239        if (!string.IsNullOrWhiteSpace(filter.Path))
 240        {
 0241            var pathToQuery = GetPathToSave(filter.Path);
 0242            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 243        }
 244
 454245        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 246        {
 0247            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 248        }
 249
 454250        if (filter.MinCommunityRating.HasValue)
 251        {
 0252            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 253        }
 254
 454255        if (filter.MinIndexNumber.HasValue)
 256        {
 0257            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 258        }
 259
 454260        if (filter.MinParentAndIndexNumber.HasValue)
 261        {
 0262            baseQuery = baseQuery
 0263                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 264        }
 265
 454266        if (filter.MinDateCreated.HasValue)
 267        {
 0268            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 269        }
 270
 454271        if (filter.MinDateLastSaved.HasValue)
 272        {
 0273            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 274        }
 275
 454276        if (filter.MinDateLastSavedForUser.HasValue)
 277        {
 0278            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 279        }
 280
 454281        if (filter.IndexNumber.HasValue)
 282        {
 0283            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 284        }
 285
 454286        if (filter.ParentIndexNumber.HasValue)
 287        {
 0288            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 289        }
 290
 454291        if (filter.ParentIndexNumberNotEquals.HasValue)
 292        {
 0293            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 294        }
 295
 454296        var minEndDate = filter.MinEndDate;
 454297        var maxEndDate = filter.MaxEndDate;
 298
 454299        if (filter.HasAired.HasValue)
 300        {
 0301            if (filter.HasAired.Value)
 302            {
 0303                maxEndDate = DateTime.UtcNow;
 304            }
 305            else
 306            {
 0307                minEndDate = DateTime.UtcNow;
 308            }
 309        }
 310
 454311        if (minEndDate.HasValue)
 312        {
 0313            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 314        }
 315
 454316        if (maxEndDate.HasValue)
 317        {
 0318            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 319        }
 320
 454321        if (filter.MinStartDate.HasValue)
 322        {
 0323            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 324        }
 325
 454326        if (filter.MaxStartDate.HasValue)
 327        {
 0328            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 329        }
 330
 454331        if (filter.MinPremiereDate.HasValue)
 332        {
 0333            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 334        }
 335
 454336        if (filter.MaxPremiereDate.HasValue)
 337        {
 0338            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 339        }
 340
 454341        if (filter.TrailerTypes.Length > 0)
 342        {
 0343            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 0344            baseQuery = baseQuery.Where(e => e.TrailerTypes!.Any(w => trailerTypes.Contains(w.Id)));
 345        }
 346
 454347        if (filter.IsAiring.HasValue)
 348        {
 0349            if (filter.IsAiring.Value)
 350            {
 0351                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 352            }
 353            else
 354            {
 0355                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 356            }
 357        }
 358
 454359        if (filter.PersonIds.Length > 0)
 360        {
 0361            var peopleEntityIds = context.BaseItems
 0362                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 0363                .Join(
 0364                    context.Peoples,
 0365                    b => b.Name,
 0366                    p => p.Name,
 0367                    (b, p) => p.Id);
 368
 0369            baseQuery = baseQuery
 0370                .Where(e => context.PeopleBaseItemMap
 0371                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 372        }
 373
 454374        if (!string.IsNullOrWhiteSpace(filter.Person))
 375        {
 0376            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 377        }
 378
 454379        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 380        {
 0381            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 382        }
 383
 454384        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 385        {
 0386            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 387        }
 388
 454389        if (!string.IsNullOrWhiteSpace(filter.Name))
 390        {
 3391            if (filter.UseRawName == true)
 392            {
 0393                baseQuery = baseQuery.Where(e => e.Name == filter.Name);
 394            }
 395            else
 396            {
 3397                var cleanName = filter.Name.GetCleanValue();
 3398                baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 399            }
 400        }
 401
 454402        var nameContains = filter.NameContains;
 454403        if (!string.IsNullOrWhiteSpace(nameContains))
 404        {
 0405            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 406            {
 0407                nameContains = $"%{nameContains.Trim('%')}%";
 0408                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 409            }
 410            else
 411            {
 0412                var likeNameContains = $"%{nameContains}%";
 0413                baseQuery = baseQuery.Where(e =>
 0414                                    e.CleanName!.Contains(nameContains)
 0415                                    || EF.Functions.Like(e.OriginalTitle, likeNameContains));
 416            }
 417        }
 418
 419        // When box set collapsing is active, defer name-range filters to after the collapse.
 420        // Otherwise, items are filtered by their own name but then collapsed into a BoxSet
 421        // whose name may fall in a different range (e.g. "21 Jump Street" is under "#"
 422        // but its BoxSet "Jump Street Collection" should appear under "J").
 454423        if (filter.CollapseBoxSetItems != true)
 424        {
 454425            baseQuery = ApplyNameFilters(baseQuery, filter);
 426        }
 427
 454428        if (filter.ImageTypes.Length > 0)
 429        {
 104430            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 104431            baseQuery = baseQuery.Where(e => e.Images!.Any(w => imgTypes.Contains(w.ImageType)));
 432        }
 433
 454434        if (filter.IsLiked.HasValue)
 435        {
 0436            var isLiked = filter.IsLiked.Value;
 0437            baseQuery = baseQuery.Where(e => e.UserData!.Any(ud => ud.UserId == filter.User!.Id && ud.Rating >= UserItem
 438        }
 439
 454440        if (filter.IsFavoriteOrLiked.HasValue)
 441        {
 0442            var isFavoriteOrLiked = filter.IsFavoriteOrLiked.Value;
 0443            baseQuery = baseQuery.Where(e => e.UserData!.Any(ud => ud.UserId == filter.User!.Id && ud.IsFavorite) == isF
 444        }
 445
 454446        if (filter.IsFavorite.HasValue)
 447        {
 0448            var isFavorite = filter.IsFavorite.Value;
 0449            baseQuery = baseQuery.Where(e => e.UserData!.Any(ud => ud.UserId == filter.User!.Id && ud.IsFavorite) == isF
 450        }
 451
 454452        if (filter.IsPlayed.HasValue)
 453        {
 0454            var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series);
 0455            var hasBoxSet = filter.IncludeItemTypes.Contains(BaseItemKind.BoxSet);
 456
 0457            if (hasSeries || hasBoxSet)
 458            {
 0459                var userId = filter.User!.Id;
 0460                var isPlayed = filter.IsPlayed.Value;
 0461                var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 0462                var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
 463
 464                // Series: played = at least one episode AND all episodes played; unplayed = otherwise.
 0465                IQueryable<Guid> playedSeriesIds = hasSeries
 0466                    ? context.BaseItems
 0467                        .AsNoTracking()
 0468                        .Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
 0469                        .GroupBy(e => e.SeriesId!.Value)
 0470                        .Where(g => !g.Any(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played)))
 0471                        .Select(g => g.Key)
 0472                    : Enumerable.Empty<Guid>().AsQueryable();
 473
 474                // BoxSet: played = all children played.
 0475                IQueryable<Guid> playedBoxSetIds = hasBoxSet
 0476                    ? GetFullyPlayedFolderIdsQuery(
 0477                        context,
 0478                        baseQuery.Where(e => e.Type == boxSetTypeName).Select(e => e.Id),
 0479                        filter.User!)
 0480                    : Enumerable.Empty<Guid>().AsQueryable();
 481
 482                // Non-folder items: check UserData directly
 0483                var playedItemIds = context.UserData
 0484                    .Where(ud => ud.UserId == userId && ud.Played)
 0485                    .Select(ud => ud.ItemId);
 486
 0487                if (isPlayed)
 488                {
 0489                    baseQuery = baseQuery.Where(e =>
 0490                        (e.Type == seriesTypeName && playedSeriesIds.Contains(e.Id))
 0491                        || (e.Type == boxSetTypeName && playedBoxSetIds.Contains(e.Id))
 0492                        || (e.Type != seriesTypeName && e.Type != boxSetTypeName && playedItemIds.Contains(e.Id)));
 493                }
 494                else
 495                {
 0496                    baseQuery = baseQuery.Where(e =>
 0497                        (e.Type == seriesTypeName && !playedSeriesIds.Contains(e.Id))
 0498                        || (e.Type == boxSetTypeName && !playedBoxSetIds.Contains(e.Id))
 0499                        || (e.Type != seriesTypeName && e.Type != boxSetTypeName && !playedItemIds.Contains(e.Id)));
 500                }
 501            }
 502            else
 503            {
 0504                var playedItemIds = context.UserData
 0505                    .Where(ud => ud.UserId == filter.User!.Id && ud.Played)
 0506                    .Select(ud => ud.ItemId);
 0507                var isPlayedItem = filter.IsPlayed.Value;
 0508                baseQuery = baseQuery.Where(e => playedItemIds.Contains(e.Id) == isPlayedItem);
 509            }
 510        }
 511
 454512        if (filter.IsResumable.HasValue)
 513        {
 1514            var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series);
 515
 1516            if (hasSeries)
 517            {
 0518                var userId = filter.User!.Id;
 0519                var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 0520                var isResumable = filter.IsResumable.Value;
 521
 522                // Aggregate per series in a single GROUP BY pass, instead of three full scans.
 0523                var seriesEpisodeStats = context.BaseItems
 0524                    .AsNoTracking()
 0525                    .Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
 0526                    .GroupBy(e => e.SeriesId!.Value)
 0527                    .Select(g => new
 0528                    {
 0529                        SeriesId = g.Key,
 0530                        HasInProgress = g.Any(e => e.UserData!.Any(ud => ud.UserId == userId && ud.PlaybackPositionTicks
 0531                        HasPlayed = g.Any(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played)),
 0532                        HasUnplayed = g.Any(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
 0533                    });
 534
 535                // A series is resumable if it has an in-progress episode,
 536                // or if it has both played and unplayed episodes (partially watched).
 0537                var resumableSeriesIds = seriesEpisodeStats
 0538                    .Where(s => s.HasInProgress || (s.HasPlayed && s.HasUnplayed))
 0539                    .Select(s => s.SeriesId);
 540
 541                // Non-series items: resumable if PlaybackPositionTicks > 0
 0542                var resumableItemIds = context.UserData
 0543                    .Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0)
 0544                    .Select(ud => ud.ItemId);
 545
 0546                baseQuery = baseQuery.Where(e =>
 0547                    (e.Type == seriesTypeName && resumableSeriesIds.Contains(e.Id) == isResumable)
 0548                    || (e.Type != seriesTypeName && resumableItemIds.Contains(e.Id) == isResumable));
 549            }
 550            else
 551            {
 1552                var resumableItemIds = context.UserData
 1553                    .Where(ud => ud.UserId == filter.User!.Id && ud.PlaybackPositionTicks > 0)
 1554                    .Select(ud => ud.ItemId);
 1555                var isResumable = filter.IsResumable.Value;
 1556                baseQuery = baseQuery.Where(e => resumableItemIds.Contains(e.Id) == isResumable);
 557            }
 558        }
 559
 454560        if (filter.ArtistIds.Length > 0)
 561        {
 0562            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 563        }
 564
 454565        if (filter.AlbumArtistIds.Length > 0)
 566        {
 0567            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 568        }
 569
 454570        if (filter.ContributingArtistIds.Length > 0)
 571        {
 0572            var contributingNames = context.BaseItems
 0573                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 0574                .Select(b => b.CleanName);
 575
 0576            baseQuery = baseQuery.Where(e =>
 0577                e.ItemValues!.Any(ivm =>
 0578                    ivm.ItemValue.Type == ItemValueType.Artist &&
 0579                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 0580                &&
 0581                !e.ItemValues!.Any(ivm =>
 0582                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 0583                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 584        }
 585
 454586        if (filter.AlbumIds.Length > 0)
 587        {
 0588            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 0589            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 590        }
 591
 454592        if (filter.ExcludeArtistIds.Length > 0)
 593        {
 0594            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 595        }
 596
 454597        if (filter.GenreIds.Count > 0)
 598        {
 0599            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 600        }
 601
 454602        if (filter.Genres.Count > 0)
 603        {
 0604            var cleanGenres = filter.Genres.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValu
 0605            baseQuery = baseQuery
 0606                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 607        }
 608
 454609        if (tags.Count > 0)
 610        {
 0611            var cleanValues = tags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, str
 0612            baseQuery = baseQuery
 0613                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 614        }
 615
 454616        if (excludeTags.Count > 0)
 617        {
 0618            var cleanValues = excludeTags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueM
 0619            baseQuery = baseQuery
 0620                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 621        }
 622
 454623        if (filter.StudioIds.Length > 0)
 624        {
 0625            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 626        }
 627
 454628        if (filter.OfficialRatings.Length > 0)
 629        {
 0630            var ratings = filter.OfficialRatings;
 0631            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, e => ratings.Contains(e.OfficialRating));
 632        }
 633
 454634        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 454635        if (filter.MinParentalRating != null)
 636        {
 0637            var min = filter.MinParentalRating;
 0638            var minScore = min.Score;
 0639            var minSubScore = min.SubScore ?? 0;
 640
 0641            minParentalRatingFilter = e =>
 0642                e.InheritedParentalRatingValue == null ||
 0643                e.InheritedParentalRatingValue > minScore ||
 0644                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 645        }
 646
 454647        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 454648        if (filter.MaxParentalRating != null)
 649        {
 48650            maxParentalRatingFilter = BuildMaxParentalRatingFilter(context, filter.MaxParentalRating);
 651        }
 652
 454653        if (filter.HasParentalRating ?? false)
 654        {
 0655            if (minParentalRatingFilter != null)
 656            {
 0657                baseQuery = baseQuery.Where(minParentalRatingFilter);
 658            }
 659
 0660            if (maxParentalRatingFilter != null)
 661            {
 0662                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 663            }
 664        }
 454665        else if (filter.BlockUnratedItems.Length > 0)
 666        {
 0667            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0668            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 669
 0670            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 671            {
 0672                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 673            }
 0674            else if (minParentalRatingFilter != null)
 675            {
 0676                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 677            }
 0678            else if (maxParentalRatingFilter != null)
 679            {
 0680                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 681            }
 682            else
 683            {
 0684                baseQuery = baseQuery.Where(unratedItemFilter);
 685            }
 686        }
 454687        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 688        {
 48689            if (minParentalRatingFilter != null)
 690            {
 0691                baseQuery = baseQuery.Where(minParentalRatingFilter);
 692            }
 693
 48694            if (maxParentalRatingFilter != null)
 695            {
 48696                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 697            }
 698        }
 406699        else if (!filter.HasParentalRating ?? false)
 700        {
 0701            baseQuery = baseQuery
 0702                .Where(e => e.InheritedParentalRatingValue == null);
 703        }
 704
 454705        if (filter.HasOfficialRating.HasValue)
 706        {
 0707            Expression<Func<BaseItemEntity, bool>> hasRating =
 0708                e => e.OfficialRating != null && e.OfficialRating != string.Empty;
 709
 0710            baseQuery = filter.HasOfficialRating.Value
 0711                ? baseQuery.WhereItemOrDescendantMatches(context, hasRating)
 0712                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasRating);
 713        }
 714
 454715        if (filter.HasOverview.HasValue)
 716        {
 0717            if (filter.HasOverview.Value)
 718            {
 0719                baseQuery = baseQuery
 0720                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 721            }
 722            else
 723            {
 0724                baseQuery = baseQuery
 0725                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 726            }
 727        }
 728
 454729        if (filter.HasOwnerId.HasValue)
 730        {
 0731            if (filter.HasOwnerId.Value)
 732            {
 0733                baseQuery = baseQuery
 0734                    .Where(e => e.OwnerId != null);
 735            }
 736            else
 737            {
 0738                baseQuery = baseQuery
 0739                    .Where(e => e.OwnerId == null);
 740            }
 741        }
 454742        else if (filter.OwnerIds.Length == 0 && filter.ExtraTypes.Length == 0 && !filter.IncludeOwnedItems)
 743        {
 744            // Exclude alternate versions and owned non-extra items from general queries.
 745            // Alternate versions have PrimaryVersionId set (pointing to their primary).
 746            // Extras (trailers, etc.) have OwnerId set but also have ExtraType set - keep those.
 427747            baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 748        }
 749
 454750        if (filter.OwnerIds.Length > 0)
 751        {
 6752            baseQuery = baseQuery.Where(e => e.OwnerId != null && filter.OwnerIds.Contains(e.OwnerId.Value));
 753        }
 754
 454755        if (filter.ExtraTypes.Length > 0)
 756        {
 757            // Convert ExtraType enum to BaseItemExtraType enum via int cast (same underlying values)
 0758            var extraTypeValues = filter.ExtraTypes.Select(e => (BaseItemExtraType?)(int)e).ToArray();
 0759            baseQuery = baseQuery.Where(e => e.ExtraType != null && extraTypeValues.Contains(e.ExtraType));
 760        }
 761
 454762        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 763        {
 0764            var lang = filter.HasNoAudioTrackWithLanguage;
 0765            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 766
 0767            baseQuery = baseQuery
 0768                .Where(e =>
 0769                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Audio && ms.Langua
 0770                    || (e.IsFolder && !foldersWithAudio.Contains(e.Id)));
 771        }
 772
 454773        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 774        {
 0775            var lang = filter.HasNoInternalSubtitleTrackWithLanguage;
 0776            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 777
 0778            baseQuery = baseQuery
 0779                .Where(e =>
 0780                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && !ms.Is
 0781                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 782        }
 783
 454784        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 785        {
 0786            var lang = filter.HasNoExternalSubtitleTrackWithLanguage;
 0787            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 788
 0789            baseQuery = baseQuery
 0790                .Where(e =>
 0791                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.IsE
 0792                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 793        }
 794
 454795        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 796        {
 0797            var lang = filter.HasNoSubtitleTrackWithLanguage;
 0798            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 799
 0800            baseQuery = baseQuery
 0801                .Where(e =>
 0802                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.Lan
 0803                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 804        }
 805
 454806        if (filter.HasSubtitles.HasValue)
 807        {
 0808            var hasSubtitles = filter.HasSubtitles.Value;
 0809            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasSubtitles());
 0810            if (hasSubtitles)
 811            {
 0812                baseQuery = baseQuery
 0813                    .Where(e =>
 0814                        (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0815                        || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 816            }
 817            else
 818            {
 0819                baseQuery = baseQuery
 0820                    .Where(e =>
 0821                        (!e.IsFolder && !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0822                        || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 823            }
 824        }
 825
 454826        if (filter.HasChapterImages.HasValue)
 827        {
 0828            var hasChapterImages = filter.HasChapterImages.Value;
 0829            var foldersWithChapterImages = DescendantQueryHelper.GetFolderIdsMatching(context, new HasChapterImages());
 0830            if (hasChapterImages)
 831            {
 0832                baseQuery = baseQuery
 0833                    .Where(e =>
 0834                        (!e.IsFolder && e.Chapters!.Any(f => f.ImagePath != null))
 0835                        || (e.IsFolder && foldersWithChapterImages.Contains(e.Id)));
 836            }
 837            else
 838            {
 0839                baseQuery = baseQuery
 0840                    .Where(e =>
 0841                        (!e.IsFolder && !e.Chapters!.Any(f => f.ImagePath != null))
 0842                        || (e.IsFolder && !foldersWithChapterImages.Contains(e.Id)));
 843            }
 844        }
 845
 454846        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 847        {
 16848            baseQuery = baseQuery
 16849                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 850        }
 851
 454852        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 853        {
 16854            baseQuery = baseQuery
 16855                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 856        }
 857
 454858        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 859        {
 16860            baseQuery = baseQuery
 16861                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 862        }
 863
 454864        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 865        {
 16866            baseQuery = baseQuery
 16867                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 868        }
 869
 454870        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 871        {
 0872            baseQuery = baseQuery
 0873                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 874        }
 875
 454876        if (filter.Years.Length > 0)
 877        {
 0878            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 879        }
 880
 454881        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 454882        if (isVirtualItem.HasValue)
 883        {
 22884            baseQuery = baseQuery
 22885                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 886        }
 887
 454888        if (filter.IsSpecialSeason.HasValue)
 889        {
 0890            if (filter.IsSpecialSeason.Value)
 891            {
 0892                baseQuery = baseQuery
 0893                    .Where(e => e.IndexNumber == 0);
 894            }
 895            else
 896            {
 0897                baseQuery = baseQuery
 0898                    .Where(e => e.IndexNumber != 0);
 899            }
 900        }
 901
 454902        if (filter.IsUnaired.HasValue)
 903        {
 0904            if (filter.IsUnaired.Value)
 905            {
 0906                baseQuery = baseQuery
 0907                    .Where(e => e.PremiereDate >= now);
 908            }
 909            else
 910            {
 0911                baseQuery = baseQuery
 0912                    .Where(e => e.PremiereDate < now);
 913            }
 914        }
 915
 454916        if (filter.MediaTypes.Length > 0)
 917        {
 21918            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 21919            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 920        }
 921
 454922        if (filter.ItemIds.Length > 0)
 923        {
 0924            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 925        }
 926
 454927        if (filter.ExcludeItemIds.Length > 0)
 928        {
 0929            baseQuery = baseQuery
 0930                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 931        }
 932
 454933        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 934        {
 0935            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 0936            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 937        }
 938
 454939        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 940        {
 941            // Allow setting a null or empty value to get all items that have the specified provider set.
 0942            var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArra
 0943            if (includeAny.Length > 0)
 944            {
 0945                baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
 946            }
 947
 0948            var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Ke
 0949            if (includeSelected.Length > 0)
 950            {
 0951                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 952            }
 953        }
 954
 454955        if (filter.HasImdbId.HasValue)
 956        {
 0957            baseQuery = filter.HasImdbId.Value
 0958                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == ImdbProviderName))
 0959                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != ImdbProviderName));
 960        }
 961
 454962        if (filter.HasTmdbId.HasValue)
 963        {
 0964            baseQuery = filter.HasTmdbId.Value
 0965                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TmdbProviderName))
 0966                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TmdbProviderName));
 967        }
 968
 454969        if (filter.HasTvdbId.HasValue)
 970        {
 0971            baseQuery = filter.HasTvdbId.Value
 0972                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TvdbProviderName))
 0973                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TvdbProviderName));
 974        }
 975
 454976        var queryTopParentIds = filter.TopParentIds;
 977
 454978        if (queryTopParentIds.Length > 0)
 979        {
 15980            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 15981            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 15982            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 983            {
 0984                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 985            }
 986            else
 987            {
 15988                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 989            }
 990        }
 991
 454992        if (filter.AncestorIds.Length > 0)
 993        {
 43994            var ancestorFilter = filter.AncestorIds.OneOrManyExpressionBuilder<AncestorId, Guid>(f => f.ParentItemId);
 43995            baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
 996        }
 997
 454998        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 999        {
 01000            baseQuery = baseQuery
 01001                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 1002        }
 1003
 4541004        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 1005        {
 01006            baseQuery = baseQuery
 01007                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 1008        }
 1009
 1010        // Pre-build the blocked-item-id set as a sub-select
 4541011        if (filter.ExcludeInheritedTags.Length > 0)
 1012        {
 01013            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01014            var blockedTagItemIds = context.ItemValuesMap
 01015                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 01016                .Select(f => f.ItemId);
 1017
 01018            baseQuery = baseQuery.Where(e =>
 01019                !blockedTagItemIds.Contains(e.Id)
 01020                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 01021                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 01022                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 1023        }
 1024
 4541025        if (filter.IncludeInheritedTags.Length > 0)
 1026        {
 01027            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01028            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 01029            var allowedTagItemIds = context.ItemValuesMap
 01030                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 01031                .Select(f => f.ItemId);
 1032
 01033            baseQuery = baseQuery.Where(e =>
 01034                allowedTagItemIds.Contains(e.Id)
 01035                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 01036                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 01037                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 01038
 01039                // A playlist should be accessible to its owner regardless of allowed tags
 01040                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 1041        }
 1042
 4541043        if (filter.SeriesStatuses.Length > 0)
 1044        {
 01045            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 01046            baseQuery = baseQuery
 01047                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 1048        }
 1049
 4541050        if (filter.BoxSetLibraryFolders.Length > 0)
 1051        {
 01052            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 01053            baseQuery = baseQuery
 01054                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 1055        }
 1056
 4541057        if (filter.VideoTypes.Length > 0)
 1058        {
 01059            var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
 01060            Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f));
 01061            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
 1062        }
 1063
 4541064        if (filter.Is3D.HasValue)
 1065        {
 01066            Expression<Func<BaseItemEntity, bool>> is3D = e => e.Data!.Contains("Video3DFormat");
 1067
 01068            baseQuery = filter.Is3D.Value
 01069                ? baseQuery.WhereItemOrDescendantMatches(context, is3D)
 01070                : baseQuery.WhereNeitherItemNorDescendantMatches(context, is3D);
 1071        }
 1072
 4541073        if (filter.IsPlaceHolder.HasValue)
 1074        {
 01075            Expression<Func<BaseItemEntity, bool>> isPlaceHolder = e => e.Data!.Contains("IsPlaceHolder\":true");
 1076
 01077            baseQuery = filter.IsPlaceHolder.Value
 01078                ? baseQuery.WhereItemOrDescendantMatches(context, isPlaceHolder)
 01079                : baseQuery.WhereNeitherItemNorDescendantMatches(context, isPlaceHolder);
 1080        }
 1081
 4541082        if (filter.HasSpecialFeature.HasValue)
 1083        {
 01084            var itemsWithExtras = context.BaseItems
 01085                .Where(extra => extra.OwnerId != null
 01086                    && extra.ExtraType != null
 01087                    && extra.ExtraType != BaseItemExtraType.Unknown
 01088                    && extra.ExtraType != BaseItemExtraType.Trailer
 01089                    && extra.ExtraType != BaseItemExtraType.ThemeSong
 01090                    && extra.ExtraType != BaseItemExtraType.ThemeVideo)
 01091                .Select(extra => extra.OwnerId!.Value)
 01092                .Distinct();
 1093
 01094            Expression<Func<BaseItemEntity, bool>> hasExtras = e => itemsWithExtras.Contains(e.Id);
 1095
 01096            baseQuery = filter.HasSpecialFeature.Value
 01097                ? baseQuery.WhereItemOrDescendantMatches(context, hasExtras)
 01098                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasExtras);
 1099        }
 1100
 4541101        if (filter.HasTrailer.HasValue)
 1102        {
 01103            var trailerOwnerIds = context.BaseItems
 01104                .Where(extra => extra.ExtraType == BaseItemExtraType.Trailer && extra.OwnerId != null)
 01105                .Select(extra => extra.OwnerId!.Value);
 1106
 01107            Expression<Func<BaseItemEntity, bool>> hasTrailer = e => trailerOwnerIds.Contains(e.Id);
 1108
 01109            baseQuery = filter.HasTrailer.Value
 01110                ? baseQuery.WhereItemOrDescendantMatches(context, hasTrailer)
 01111                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasTrailer);
 1112        }
 1113
 4541114        if (filter.HasThemeSong.HasValue)
 1115        {
 01116            var themeSongOwnerIds = context.BaseItems
 01117                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeSong && extra.OwnerId != null)
 01118                .Select(extra => extra.OwnerId!.Value);
 1119
 01120            Expression<Func<BaseItemEntity, bool>> hasThemeSong = e => themeSongOwnerIds.Contains(e.Id);
 1121
 01122            baseQuery = filter.HasThemeSong.Value
 01123                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeSong)
 01124                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeSong);
 1125        }
 1126
 4541127        if (filter.HasThemeVideo.HasValue)
 1128        {
 01129            var themeVideoOwnerIds = context.BaseItems
 01130                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeVideo && extra.OwnerId != null)
 01131                .Select(extra => extra.OwnerId!.Value);
 1132
 01133            Expression<Func<BaseItemEntity, bool>> hasThemeVideo = e => themeVideoOwnerIds.Contains(e.Id);
 1134
 01135            baseQuery = filter.HasThemeVideo.Value
 01136                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeVideo)
 01137                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeVideo);
 1138        }
 1139
 4541140        if (filter.AiredDuringSeason.HasValue)
 1141        {
 01142            var seasonNumber = filter.AiredDuringSeason.Value;
 01143            if (seasonNumber < 1)
 1144            {
 01145                baseQuery = baseQuery.Where(e => e.ParentIndexNumber == seasonNumber);
 1146            }
 1147            else
 1148            {
 01149                var seasonStr = seasonNumber.ToString(CultureInfo.InvariantCulture);
 01150                baseQuery = baseQuery.Where(e =>
 01151                    e.ParentIndexNumber == seasonNumber
 01152                    || (e.Data != null && (
 01153                        e.Data.Contains("\"AirsAfterSeasonNumber\":" + seasonStr)
 01154                        || e.Data.Contains("\"AirsBeforeSeasonNumber\":" + seasonStr))));
 1155            }
 1156        }
 1157
 4541158        if (filter.AdjacentTo.HasValue && !filter.AdjacentTo.Value.IsEmpty())
 1159        {
 01160            var adjacentToId = filter.AdjacentTo.Value;
 01161            var targetItem = context.BaseItems.Where(e => e.Id == adjacentToId).Select(e => new { e.SortName, e.Id }).Fi
 01162            if (targetItem is not null)
 1163            {
 01164                var targetSortName = targetItem.SortName ?? string.Empty;
 1165
 1166                // Fetch both prev and next adjacent items in a single query using Concat (UNION ALL).
 01167                var adjacentIds = context.BaseItems
 01168                    .Where(e => string.Compare(e.SortName, targetSortName) < 0)
 01169                    .OrderByDescending(e => e.SortName)
 01170                    .Select(e => e.Id)
 01171                    .Take(1)
 01172                    .Concat(
 01173                        context.BaseItems
 01174                            .Where(e => string.Compare(e.SortName, targetSortName) > 0)
 01175                            .OrderBy(e => e.SortName)
 01176                            .Select(e => e.Id)
 01177                            .Take(1))
 01178                    .ToList();
 1179
 01180                adjacentIds.Add(adjacentToId);
 01181                baseQuery = baseQuery.Where(e => adjacentIds.Contains(e.Id));
 1182            }
 1183        }
 1184
 4541185        return baseQuery;
 1186    }
 1187}

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