< Summary - Jellyfin

Line coverage
58%
Covered lines: 975
Uncovered lines: 704
Coverable lines: 1679
Total lines: 3407
Line coverage: 58%
Branch coverage
49%
Covered branches: 349
Total branches: 712
Branch coverage: 49%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 AM Line coverage: 28% (400/1424) Branch coverage: 43.6% (282/646) Total lines: 28535/20/2026 - 12:15:44 AM Line coverage: 28% (400/1424) Branch coverage: 39.6% (256/646) Total lines: 28535/22/2026 - 12:15:17 AM Line coverage: 28% (401/1429) Branch coverage: 39.6% (257/648) Total lines: 28626/1/2026 - 12:16:05 AM Line coverage: 27.9% (401/1437) Branch coverage: 39.5% (257/650) Total lines: 28726/8/2026 - 12:16:15 AM Line coverage: 28% (402/1433) Branch coverage: 39.6% (258/650) Total lines: 28656/15/2026 - 12:16:09 AM Line coverage: 28% (402/1432) Branch coverage: 39.6% (258/650) Total lines: 28647/6/2026 - 12:16:28 AM Line coverage: 29% (422/1452) Branch coverage: 39.8% (263/660) Total lines: 29007/21/2026 - 12:16:33 AM Line coverage: 29.1% (424/1454) Branch coverage: 39.8% (263/660) Total lines: 29057/22/2026 - 12:16:22 AM Line coverage: 29% (424/1462) Branch coverage: 39.8% (263/660) Total lines: 29137/23/2026 - 12:16:48 AM Line coverage: 28.4% (424/1491) Branch coverage: 39.8% (263/660) Total lines: 29497/27/2026 - 12:16:14 AM Line coverage: 27.7% (425/1532) Branch coverage: 38.1% (263/690) Total lines: 30398/2/2026 - 12:17:28 AM Line coverage: 30.6% (445/1450) Branch coverage: 39.5% (268/678) Total lines: 29548/3/2026 - 12:16:46 AM Line coverage: 31.1% (451/1448) Branch coverage: 39.7% (268/674) Total lines: 29698/5/2026 - 12:17:09 AM Line coverage: 31.5% (460/1457) Branch coverage: 39.7% (268/674) Total lines: 29828/6/2026 - 12:17:15 AM Line coverage: 31.7% (481/1516) Branch coverage: 37.9% (267/704) Total lines: 31408/9/2026 - 12:16:58 AM Line coverage: 37.3% (566/1514) Branch coverage: 39.1% (275/702) Total lines: 31358/11/2026 - 12:17:00 AM Line coverage: 37.4% (565/1510) Branch coverage: 39.3% (274/696) Total lines: 312610/29/2026 - 12:17:11 AM Line coverage: 58% (975/1679) Branch coverage: 49% (349/712) Total lines: 3407 5/16/2026 - 12:15:55 AM Line coverage: 28% (400/1424) Branch coverage: 43.6% (282/646) Total lines: 28535/20/2026 - 12:15:44 AM Line coverage: 28% (400/1424) Branch coverage: 39.6% (256/646) Total lines: 28535/22/2026 - 12:15:17 AM Line coverage: 28% (401/1429) Branch coverage: 39.6% (257/648) Total lines: 28626/1/2026 - 12:16:05 AM Line coverage: 27.9% (401/1437) Branch coverage: 39.5% (257/650) Total lines: 28726/8/2026 - 12:16:15 AM Line coverage: 28% (402/1433) Branch coverage: 39.6% (258/650) Total lines: 28656/15/2026 - 12:16:09 AM Line coverage: 28% (402/1432) Branch coverage: 39.6% (258/650) Total lines: 28647/6/2026 - 12:16:28 AM Line coverage: 29% (422/1452) Branch coverage: 39.8% (263/660) Total lines: 29007/21/2026 - 12:16:33 AM Line coverage: 29.1% (424/1454) Branch coverage: 39.8% (263/660) Total lines: 29057/22/2026 - 12:16:22 AM Line coverage: 29% (424/1462) Branch coverage: 39.8% (263/660) Total lines: 29137/23/2026 - 12:16:48 AM Line coverage: 28.4% (424/1491) Branch coverage: 39.8% (263/660) Total lines: 29497/27/2026 - 12:16:14 AM Line coverage: 27.7% (425/1532) Branch coverage: 38.1% (263/690) Total lines: 30398/2/2026 - 12:17:28 AM Line coverage: 30.6% (445/1450) Branch coverage: 39.5% (268/678) Total lines: 29548/3/2026 - 12:16:46 AM Line coverage: 31.1% (451/1448) Branch coverage: 39.7% (268/674) Total lines: 29698/5/2026 - 12:17:09 AM Line coverage: 31.5% (460/1457) Branch coverage: 39.7% (268/674) Total lines: 29828/6/2026 - 12:17:15 AM Line coverage: 31.7% (481/1516) Branch coverage: 37.9% (267/704) Total lines: 31408/9/2026 - 12:16:58 AM Line coverage: 37.3% (566/1514) Branch coverage: 39.1% (275/702) Total lines: 31358/11/2026 - 12:17:00 AM Line coverage: 37.4% (565/1510) Branch coverage: 39.3% (274/696) Total lines: 312610/29/2026 - 12:17:11 AM Line coverage: 58% (975/1679) Branch coverage: 49% (349/712) Total lines: 3407

Coverage delta

Coverage delta 21 -21

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: GetAllArtists(...)100%11100%
File 1: GetArtists(...)100%11100%
File 1: GetAlbumArtists(...)100%11100%
File 1: GetStudios(...)100%11100%
File 1: GetGenres(...)100%11100%
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: GetMediaStreamLanguages(...)100%210%
File 1: GetTagNames(...)100%11100%
File 1: GetItemValueNames(...)100%44100%
File 1: GetItemValues(...)83.33%121299.1%
File 1: BuildItemCountsByCleanName(...)75%121290.9%
File 1: BuildEpisodeCountsByCleanName(...)28.57%171475%
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(...)81.81%242283.33%
File 2: Map(...)100%210%
File 2: Map(...)100%210%
File 2: GetPathToSave(...)0%620%
File 2: DeserializeBaseItem(...)50%101088.88%
File 3: PrepareItemQuery(...)100%11100%
File 3: ApplyQueryFilter(...)100%11100%
File 3: ApplyAdjacencyFilter(...)25%12420%
File 3: ApplyQueryPaging(...)75%8885.71%
File 3: ApplyGroupingFilter(...)70%161061.11%
File 3: ApplyBoxSetCollapsing(...)0%620%
File 3: ApplyBoxSetCollapsingAll(...)100%210%
File 3: ApplyNameFilters(...)50%14640%
File 3: ApplyNavigations(...)83.33%181896.15%
File 3: ApplyOrder(...)70.58%423481.25%
File 3: ApplySeriesDatePlayedOrder(...)0%2040%
File 3: BuildAccessFilteredDescendantsQuery(...)100%210%
File 3: RequiresParentalRestrictions(...)0%4260%
File 3: ApplyAccessFiltering(...)100%22100%
File 3: ApplyAlternateVersionFiltering(...)100%11100%
File 3: ApplyTopParentFiltering(...)100%66100%
File 3: GetExemptedItemByNameTypes(...)100%44100%
File 3: ApplyItemByNameAccessFiltering(...)66.66%66100%
File 3: ApplyParentalRestrictions(...)50%47815.15%
File 3: ItemsBelowTaggedAncestor(...)100%11100%
File 3: BuildMaxParentalRatingFilter(...)100%11100%
File 3: GetAccessFilteredLeafItemsQuery(...)100%11100%
File 3: AsOrderKey(...)100%11100%
File 3: BuildHasDescendantFilter(...)100%11100%
File 4: GetItemIdsList(...)100%11100%
File 4: GetItems(...)62.5%9877.27%
File 4: GetItemList(...)75%4495.65%
File 4: GetLatestItemList(...)0%342180%
File 4: LoadLatestByIds(...)100%210%
File 4: LoadLatestByIds(...)0%620%
File 4: GetLatestMovieItems(...)100%210%
File 4: GetLatestTvShowItems(...)0%7482860%
File 4: ItemExistsAsync()100%11100%
File 4: RetrieveItem(...)50%4490.9%
File 4: GetIsPlayed(...)50%2287.5%
File 4: GetQueryFiltersLegacy(...)100%11100%
File 5: .cctor()100%11100%
File 5: get_IsFolderFilter()100%11100%
File 5: BuildIsPlayedFilter(...)100%11100%
File 5: BuildLeafIsPlayedFilter(...)100%11100%
File 5: NormalizeLanguage(...)100%22100%
File 5: VersionsMatchingDimension(...)100%11100%
File 5: TranslateQuery(...)46.77%1612037251.54%

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;
 8using Jellyfin.Database.Implementations.Entities;
 9using MediaBrowser.Controller.Entities;
 10using MediaBrowser.Model.Dto;
 11using MediaBrowser.Model.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 QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 22    {
 423        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 24    }
 25
 26    /// <inheritdoc />
 27    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 28    {
 529        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 30    }
 31
 32    /// <inheritdoc />
 33    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 34    {
 135        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 36    }
 37
 38    /// <inheritdoc />
 39    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 40    {
 141        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 42    }
 43
 44    /// <inheritdoc />
 45    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 46    {
 447        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 48    }
 49
 50    /// <inheritdoc />
 51    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 52    {
 053        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 54    }
 55
 56    /// <inheritdoc />
 57    public IReadOnlyList<string> GetStudioNames()
 58    {
 2059        return GetItemValueNames(_getStudiosValueTypes, [], []);
 60    }
 61
 62    /// <inheritdoc />
 63    public IReadOnlyList<string> GetAllArtistNames()
 64    {
 2065        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 66    }
 67
 68    /// <inheritdoc />
 69    public IReadOnlyList<string> GetMusicGenreNames()
 70    {
 2171        return GetItemValueNames(
 2172            _getGenreValueTypes,
 2173            _itemTypeLookup.MusicGenreTypes,
 2174            []);
 75    }
 76
 77    /// <inheritdoc />
 78    public IReadOnlyList<string> GetGenreNames()
 79    {
 2180        return GetItemValueNames(
 2181            _getGenreValueTypes,
 2182            [],
 2183            _itemTypeLookup.MusicGenreTypes);
 84    }
 85
 86    /// <inheritdoc />
 87    public IReadOnlyList<string> GetMediaStreamLanguages(InternalItemsQuery filter, MediaStreamType mediaStreamType)
 88    {
 089        ArgumentNullException.ThrowIfNull(filter);
 90
 091        using var context = _dbProvider.CreateDbContext();
 92
 093        return TranslateQuery(
 094            context.BaseItems.Include(e => e.MediaStreams).Where(e => e.Id != EF.Constant(PlaceholderId)),
 095            context,
 096            new InternalItemsQuery(filter.User)
 097            {
 098                IncludeOwnedItems = filter.IncludeOwnedItems,
 099                ExcludeItemTypes = filter.ExcludeItemTypes,
 0100                IncludeItemTypes = filter.IncludeItemTypes,
 0101                MediaTypes = filter.MediaTypes,
 0102                AncestorIds = filter.AncestorIds,
 0103                ItemIds = filter.ItemIds,
 0104                TopParentIds = filter.TopParentIds,
 0105                ParentId = filter.ParentId,
 0106                IsAiring = filter.IsAiring,
 0107                IsMovie = filter.IsMovie,
 0108                IsSports = filter.IsSports,
 0109                IsKids = filter.IsKids,
 0110                IsNews = filter.IsNews,
 0111                IsSeries = filter.IsSeries
 0112            })
 0113            .SelectMany(e => e.MediaStreams!)
 0114            .Where(e => e.StreamType == (MediaStreamTypeEntity)mediaStreamType)
 0115            .Select(s => string.IsNullOrEmpty(s.Language) ? "und" : s.Language) // und = undetermined
 0116            .Distinct()
 0117            .ToArray();
 0118    }
 119
 120    /// <inheritdoc />
 121    public IReadOnlyList<string> GetTagNames(InternalItemsQuery filter)
 122    {
 1123        ArgumentNullException.ThrowIfNull(filter);
 1124        PrepareFilterQuery(filter);
 125
 1126        using var context = _dbProvider.CreateDbContext();
 1127        var baseQuery = PrepareItemQuery(context, filter);
 1128        baseQuery = TranslateQuery(baseQuery, context, filter);
 129
 1130        var matchingItemIds = baseQuery.Select(e => e.Id);
 131
 132        // Project the join before grouping. Grouping over the ItemValue navigation instead makes EF
 133        // re-resolve the aggregate as a correlated subquery per group, which is orders of magnitude slower.
 1134        return context.ItemValuesMap
 1135            .AsNoTracking()
 1136            .Join(
 1137                context.ItemValues,
 1138                ivm => ivm.ItemValueId,
 1139                iv => iv.ItemValueId,
 1140                (ivm, iv) => new { ivm.ItemId, iv.Type, iv.CleanValue, iv.Value })
 1141            .Where(iv => iv.Type == ItemValueType.Tags)
 1142            .Where(iv => matchingItemIds.Contains(iv.ItemId))
 1143            .GroupBy(iv => iv.CleanValue)
 1144            .Select(g => g.Min(iv => iv.Value)!)
 1145            .OrderBy(t => t)
 1146            .ToArray();
 1147    }
 148
 149    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 150    {
 82151        using var context = _dbProvider.CreateDbContext();
 152
 82153        var maps = context.ItemValuesMap.AsNoTracking();
 82154        if (withItemTypes.Count > 0)
 155        {
 21156            maps = maps.Where(e => withItemTypes.Contains(e.Item.Type));
 157        }
 158
 82159        if (excludeItemTypes.Count > 0)
 160        {
 21161            maps = maps.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 162        }
 163
 82164        return context.ItemValues
 82165            .AsNoTracking()
 82166            .WhereOneOrMany(itemValueTypes, e => e.Type)
 82167            .Where(e => maps.Any(m => m.ItemValueId == e.ItemValueId))
 82168            .Select(e => new { e.CleanValue, e.Value })
 82169            .GroupBy(e => e.CleanValue)
 82170            .Select(g => g.Min(v => v.Value)!)
 82171            .ToArray();
 82172    }
 173
 174    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 175    {
 15176        ArgumentNullException.ThrowIfNull(filter);
 177
 15178        using var context = _dbProvider.CreateDbContext();
 179
 15180        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 15181        {
 15182            ExcludeItemTypes = filter.ExcludeItemTypes,
 15183            IncludeItemTypes = filter.IncludeItemTypes,
 15184            MediaTypes = filter.MediaTypes,
 15185            AncestorIds = filter.AncestorIds,
 15186            ItemIds = filter.ItemIds,
 15187            TopParentIds = filter.TopParentIds,
 15188            ParentId = filter.ParentId,
 15189            IsAiring = filter.IsAiring,
 15190            IsMovie = filter.IsMovie,
 15191            IsSports = filter.IsSports,
 15192            IsKids = filter.IsKids,
 15193            IsNews = filter.IsNews,
 15194            IsSeries = filter.IsSeries
 15195        });
 196
 15197        var innerQuery = PrepareItemQuery(context, filter)
 15198            .Where(e => e.Type == returnType)
 15199            .Where(e => context.ItemValuesMap
 15200                .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type) && ivm.ItemValue.CleanValue == e.CleanName)
 15201                .Join(
 15202                    innerQueryFilter,
 15203                    ivm => ivm.ItemId,
 15204                    g => g.Id,
 15205                    (ivm, g) => ivm.ItemId)
 15206                .Any());
 207
 15208        var outerQueryFilter = new InternalItemsQuery(filter.User)
 15209        {
 15210            IsPlayed = filter.IsPlayed,
 15211            IsFavorite = filter.IsFavorite,
 15212            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 15213            IsLiked = filter.IsLiked,
 15214            IsLocked = filter.IsLocked,
 15215            ImageTypes = filter.ImageTypes,
 15216            NameLessThan = filter.NameLessThan,
 15217            NameStartsWith = filter.NameStartsWith,
 15218            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 15219            Tags = filter.Tags,
 15220            OfficialRatings = filter.OfficialRatings,
 15221            StudioIds = filter.StudioIds,
 15222            GenreIds = filter.GenreIds,
 15223            Genres = filter.Genres,
 15224            Years = filter.Years,
 15225            NameContains = filter.NameContains,
 15226            SearchTerm = filter.SearchTerm,
 15227            ExcludeItemIds = filter.ExcludeItemIds,
 15228
 15229            // A genre, studio or artist carries none of the tags of the media it describes, so an
 15230            // allow list can only ever hide all of them. Reachability is settled by innerQueryFilter
 15231            // instead: a value gets this far only when an item the user may see carries it.
 15232            IncludeInheritedTags = []
 15233        };
 234
 235        // Collapse rows that share a PresentationUniqueKey (e.g. alternate versions) into one
 236        // representative id per group, then materialize the representative ids once.
 15237        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter);
 15238        var isMusicArtist = returnType == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 239        List<Guid> representativeIds;
 15240        if (isMusicArtist)
 241        {
 242            // For MusicArtist, prefer the entity from a library the user can actually access.
 243            // Materialize to prevent correlated per-group first-row queries which hurt performance.
 10244            var topParentIds = filter.TopParentIds;
 10245            representativeIds = masterQuery
 10246                .Select(e => new { e.Id, e.PresentationUniqueKey, e.TopParentId })
 10247                .AsEnumerable()
 10248                .GroupBy(e => e.PresentationUniqueKey)
 10249                .Select(g => g
 10250                    .OrderBy(e => topParentIds.Contains(e.TopParentId ?? Guid.Empty) ? 0 : 1)
 10251                    .ThenBy(e => e.Id)
 10252                    .First().Id)
 10253                .ToList();
 254        }
 255        else
 256        {
 5257            representativeIds = masterQuery
 5258                .GroupBy(e => e.PresentationUniqueKey)
 5259                .Select(g => g.Min(e => e.Id))
 5260                .ToList();
 261        }
 262
 15263        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 15264        if (filter.EnableTotalRecordCount)
 265        {
 14266            result.TotalRecordCount = representativeIds.Count;
 267        }
 268
 15269        var query = ApplyNavigations(
 15270                context.BaseItems.AsNoTracking().AsSingleQuery().WhereOneOrMany(representativeIds, e => e.Id),
 15271                filter);
 272
 15273        query = ApplyOrder(query, filter, context);
 274
 15275        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 276        {
 0277            query = query.Skip(filter.StartIndex.Value);
 278        }
 279
 15280        if (filter.Limit.HasValue)
 281        {
 1282            query = query.Take(filter.Limit.Value);
 283        }
 284
 15285        result.StartIndex = filter.StartIndex ?? 0;
 15286        var page = query.AsEnumerable().Where(e => e is not null).ToList();
 287
 15288        if (filter.DtoOptions.ContainsField(ItemFields.ItemCounts))
 289        {
 13290            var pageCleanNames = page
 13291                .Where(e => !string.IsNullOrEmpty(e.CleanName))
 13292                .Select(e => e.CleanName!)
 13293                .Distinct()
 13294                .ToList();
 295
 13296            var countsByCleanName = BuildItemCountsByCleanName(context, filter, itemValueTypes, pageCleanNames);
 13297            result.Items =
 13298            [
 13299                .. page
 13300                    .Select(e =>
 13301                    {
 13302                        var item = DeserializeBaseItem(e, filter.SkipDeserialization);
 13303                        countsByCleanName.TryGetValue(e.CleanName ?? string.Empty, out var itemCount);
 13304                        return (item, itemCount);
 13305                    })
 13306                    .Where(x => x.item is not null)
 13307                    .Select(x => (x.item!, x.itemCount))
 13308            ];
 309        }
 310        else
 311        {
 2312            result.Items =
 2313            [
 2314                .. page
 2315                    .Select(e => DeserializeBaseItem(e, filter.SkipDeserialization))
 2316                    .Where(item => item != null)
 2317                    .Select(item => (item!, (ItemCounts?)null))
 2318            ];
 319        }
 320
 15321        return result;
 15322    }
 323
 324    private Dictionary<string, ItemCounts> BuildItemCountsByCleanName(
 325        JellyfinDbContext context,
 326        InternalItemsQuery filter,
 327        IReadOnlyList<ItemValueType> itemValueTypes,
 328        IReadOnlyList<string> cleanNames)
 329    {
 13330        var countsByCleanName = new Dictionary<string, ItemCounts>();
 13331        if (cleanNames.Count == 0)
 332        {
 0333            return countsByCleanName;
 334        }
 335
 336        // The counts describe everything the value is attached to, not only the types the list was
 337        // filtered down to.
 13338        var scopeQuery = new InternalItemsQuery(filter.User)
 13339        {
 13340            ExcludeItemTypes = filter.ExcludeItemTypes,
 13341            MediaTypes = filter.MediaTypes,
 13342            AncestorIds = filter.AncestorIds,
 13343            ExcludeItemIds = filter.ExcludeItemIds,
 13344            ItemIds = filter.ItemIds,
 13345            TopParentIds = filter.TopParentIds,
 13346            ParentId = filter.ParentId,
 13347            IsPlayed = filter.IsPlayed
 13348        };
 349
 13350        var scopedItems = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId))
 13351        var valueLinks = context.ItemValuesMap
 13352            .AsNoTracking()
 13353            .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
 13354            .WhereOneOrMany(cleanNames, ivm => ivm.ItemValue.CleanValue);
 355
 13356        var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 13357        var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 358
 359        // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite
 360        // Instead, start from ItemValueMaps and join with BaseItems.
 13361        var rawCounts = valueLinks
 13362            .Join(
 13363                scopedItems,
 13364                ivm => ivm.ItemId,
 13365                e => e.Id,
 13366                (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type, e.SeriesId, e.Id })
 13367            .GroupBy(x => new { x.CleanName, x.Type, x.SeriesId })
 13368            .Select(g => new { g.Key.CleanName, g.Key.Type, g.Key.SeriesId, Count = g.Select(x => x.Id).Distinct().Count
 13369            .ToList();
 370
 371        // Only studios and genres pass down from a series to its episodes; an artist credit does not.
 13372        var inheritsToEpisodes = itemValueTypes.Contains(ItemValueType.Studios) || itemValueTypes.Contains(ItemValueType
 13373        var episodeCounts = inheritsToEpisodes
 13374            ? BuildEpisodeCountsByCleanName(
 13375                scopedItems,
 13376                valueLinks,
 13377                rawCounts
 13378                    .Where(x => x.Type == episodeTypeName)
 13379                    .Select(x => (x.CleanName, x.SeriesId, x.Count))
 13380                    .ToList(),
 13381                seriesTypeName,
 13382                episodeTypeName)
 13383            : rawCounts
 13384                .Where(x => x.Type == episodeTypeName)
 13385                .GroupBy(x => x.CleanName)
 13386                .ToDictionary(g => g.Key, g => g.Sum(x => x.Count));
 387
 66388        foreach (var group in rawCounts.GroupBy(x => x.CleanName))
 389        {
 20390            var counts = ItemCountBuilder.Build(_itemTypeLookup, group.Select(row => (row.Type, row.Count)));
 391
 392            // Episodes are counted separately: the value is usually only written on the series.
 20393            ItemCountBuilder.SetEpisodeCount(counts, episodeCounts.GetValueOrDefault(group.Key));
 20394            countsByCleanName[group.Key] = counts;
 395        }
 396
 397        // A value carried by nothing but the episodes below a tagged series has no row of its own.
 26398        foreach (var (cleanName, episodeCount) in episodeCounts)
 399        {
 0400            if (!countsByCleanName.ContainsKey(cleanName))
 401            {
 0402                var counts = new ItemCounts();
 0403                ItemCountBuilder.SetEpisodeCount(counts, episodeCount);
 0404                countsByCleanName[cleanName] = counts;
 405            }
 406        }
 407
 13408        return countsByCleanName;
 409    }
 410
 411    private static Dictionary<string, int> BuildEpisodeCountsByCleanName(
 412        IQueryable<BaseItemEntity> scopedItems,
 413        IQueryable<ItemValueMap> valueLinks,
 414        IReadOnlyList<(string CleanName, Guid? SeriesId, int Count)> taggedEpisodes,
 415        string seriesTypeName,
 416        string episodeTypeName)
 417    {
 418        // Resolved in steps rather than as one union: each of these drives off an index, while the
 419        // single-statement form leaves SQLite free to scan every episode in the library instead.
 5420        var taggedSeries = valueLinks
 5421            .Join(
 5422                scopedItems.Where(e => e.Type == seriesTypeName),
 5423                ivm => ivm.ItemId,
 5424                e => e.Id,
 5425                (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, SeriesId = e.Id })
 5426            .ToList();
 427
 5428        var seriesIds = taggedSeries.Select(x => x.SeriesId).Distinct().ToArray();
 5429        var episodesPerSeries = seriesIds.Length == 0
 5430            ? []
 5431            : scopedItems
 5432                .Where(e => e.Type == episodeTypeName && e.SeriesId != null)
 5433                .WhereOneOrMany(seriesIds, e => e.SeriesId!.Value)
 5434                .GroupBy(e => e.SeriesId!.Value)
 5435                .Select(g => new { SeriesId = g.Key, Count = g.Count() })
 5436                .ToDictionary(x => x.SeriesId, x => x.Count);
 437
 5438        var episodeCounts = new Dictionary<string, int>();
 5439        var seriesByCleanName = new Dictionary<string, HashSet<Guid>>();
 10440        foreach (var group in taggedSeries.GroupBy(x => x.CleanName))
 441        {
 0442            var series = group.Select(x => x.SeriesId).ToHashSet();
 0443            seriesByCleanName[group.Key] = series;
 0444            episodeCounts[group.Key] = series.Sum(id => episodesPerSeries.GetValueOrDefault(id));
 445        }
 446
 10447        foreach (var (cleanName, seriesId, count) in taggedEpisodes)
 448        {
 0449            if (seriesId is not null
 0450                && seriesByCleanName.TryGetValue(cleanName, out var series)
 0451                && series.Contains(seriesId.Value))
 452            {
 453                continue;
 454            }
 455
 0456            episodeCounts[cleanName] = episodeCounts.GetValueOrDefault(cleanName) + count;
 457        }
 458
 5459        return episodeCounts;
 460    }
 461}

/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>
 335    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
 343    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 344    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 345    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 346    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 347    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 48
 349    private static readonly BaseItemKind[] _itemByNameKinds =
 350    [
 351        BaseItemKind.Person,
 352        BaseItemKind.Genre,
 353        BaseItemKind.MusicGenre,
 354        BaseItemKind.MusicArtist,
 355        BaseItemKind.Studio
 356    ];
 57
 358    private static readonly (BaseItemKind Kind, IReadOnlyList<ItemValueType> ValueTypes)[] _itemByNameValueTypes =
 359    [
 360        (BaseItemKind.Genre, _getGenreValueTypes),
 361        (BaseItemKind.MusicGenre, _getGenreValueTypes),
 362        (BaseItemKind.MusicArtist, _getAllArtistsValueTypes),
 363        (BaseItemKind.Studio, _getStudiosValueTypes)
 364    ];
 65
 66    // The only folder kinds whose children form a single viewing sequence, so playback progress on a
 67    // child rolls up to them. Every other folder kind is a container that cannot be resumed.
 368    private static readonly BaseItemKind[] _resumableFolderKinds =
 369    [
 370        BaseItemKind.Series,
 371        BaseItemKind.Season
 372    ];
 73
 74    /// <summary>
 75    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
 76    /// </summary>
 77    /// <param name="dbProvider">The db factory.</param>
 78    /// <param name="appHost">The Application host.</param>
 79    /// <param name="itemTypeLookup">The static type lookup.</param>
 80    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
 81    /// <param name="logger">System logger.</param>
 82    public BaseItemRepository(
 83        IDbContextFactory<JellyfinDbContext> dbProvider,
 84        IServerApplicationHost appHost,
 85        IItemTypeLookup itemTypeLookup,
 86        IServerConfigurationManager serverConfigurationManager,
 87        ILogger<BaseItemRepository> logger)
 88    {
 12689        _dbProvider = dbProvider;
 12690        _appHost = appHost;
 12691        _itemTypeLookup = itemTypeLookup;
 12692        _serverConfigurationManager = serverConfigurationManager;
 12693        _logger = logger;
 12694    }
 95
 96    /// <summary>
 97    /// Maps a Entity to the DTO. Delegates to <see cref="BaseItemMapper"/>.
 98    /// </summary>
 99    /// <param name="entity">The database entity.</param>
 100    /// <param name="dto">The target DTO.</param>
 101    /// <param name="appHost">The application host.</param>
 102    /// <param name="logger">The logger.</param>
 103    /// <returns>The mapped DTO.</returns>
 104    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 105    {
 0106        return BaseItemMapper.Map(entity, dto, appHost);
 107    }
 108
 109    /// <summary>
 110    /// Maps a DTO to a database entity. Delegates to <see cref="BaseItemMapper"/>.
 111    /// </summary>
 112    /// <param name="dto">The DTO to map.</param>
 113    /// <returns>The mapped database entity.</returns>
 114    public BaseItemEntity Map(BaseItemDto dto)
 115    {
 10116        return BaseItemMapper.Map(dto, _appHost);
 117    }
 118
 119    /// <summary>
 120    /// Deserializes a BaseItemEntity and sets all properties.
 121    /// </summary>
 122    /// <param name="baseItemEntity">The entity to deserialize.</param>
 123    /// <param name="logger">The logger.</param>
 124    /// <param name="appHost">The application host.</param>
 125    /// <param name="skipDeserialization">Whether to skip JSON deserialization.</param>
 126    /// <returns>The deserialized item, or null.</returns>
 127    public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost
 128    {
 3129        return BaseItemMapper.DeserializeBaseItem(baseItemEntity, logger, appHost, skipDeserialization);
 130    }
 131
 132    /// <inheritdoc />
 133    public void PrepareFilterQuery(InternalItemsQuery query)
 134    {
 760135        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 136        {
 0137            query.Limit = query.Limit.Value + 4;
 138        }
 139
 760140        if (query.IsResumable ?? false)
 141        {
 2142            query.IsVirtualItem = false;
 143        }
 760144    }
 145
 146    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 147    {
 17148        var list = new List<string>();
 149
 17150        if (IsTypeInQuery(BaseItemKind.Person, query))
 151        {
 7152            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 153        }
 154
 17155        if (IsTypeInQuery(BaseItemKind.Genre, query))
 156        {
 7157            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 158        }
 159
 17160        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 161        {
 7162            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 163        }
 164
 17165        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 166        {
 8167            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 168        }
 169
 17170        if (IsTypeInQuery(BaseItemKind.Studio, query))
 171        {
 7172            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 173        }
 174
 17175        return list;
 176    }
 177
 178    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 179    {
 85180        if (query.ExcludeItemTypes.Contains(type))
 181        {
 0182            return false;
 183        }
 184
 85185        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 186    }
 187
 188    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 189    {
 751190        if (!query.GroupByPresentationUniqueKey)
 191        {
 262192            return false;
 193        }
 194
 195        // Resume queries surface the actually-played version (which may be an alternate sharing the
 196        // primary's presentation key). The resumable filter already keeps one version per group, so
 197        // presentation-key grouping must not collapse the surfaced version back onto the primary.
 489198        if (query.IsResumable == true)
 199        {
 2200            return false;
 201        }
 202
 487203        if (query.GroupBySeriesPresentationUniqueKey)
 204        {
 0205            return false;
 206        }
 207
 487208        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 209        {
 0210            return false;
 211        }
 212
 487213        if (query.User is null)
 214        {
 467215            return false;
 216        }
 217
 20218        if (query.IncludeItemTypes.Length == 0)
 219        {
 0220            return true;
 221        }
 222
 20223        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 20224            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 20225            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 20226            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 20227            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 20228            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 229    }
 230
 231    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 232    {
 0233        return BaseItemMapper.MapImageToEntity(baseItemId, e);
 234    }
 235
 236    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 237    {
 0238        return BaseItemMapper.MapImageFromEntity(e, appHost);
 239    }
 240
 241    private string? GetPathToSave(string path)
 242    {
 0243        if (path is null)
 244        {
 0245            return null;
 246        }
 247
 0248        return _appHost.ReverseVirtualPath(path);
 249    }
 250
 251    /// <inheritdoc />
 252    public BaseItemDto? DeserializeBaseItem(BaseItemEntity entity, bool skipDeserialization = false)
 253    {
 261254        ArgumentNullException.ThrowIfNull(entity, nameof(entity));
 261255        if (_serverConfigurationManager?.Configuration is null)
 256        {
 0257            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 258        }
 259
 261260        var typeToSerialise = BaseItemMapper.GetType(entity.Type);
 261261        return BaseItemMapper.DeserializeBaseItem(
 261262            entity,
 261263            _logger,
 261264            _appHost,
 261265            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 266    }
 267}

/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 Jellyfin.Server.Implementations.Extensions;
 16using MediaBrowser.Controller.Entities;
 17using MediaBrowser.Model.Entities;
 18using MediaBrowser.Model.Querying;
 19using Microsoft.EntityFrameworkCore;
 20using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 21
 22namespace Jellyfin.Server.Implementations.Item;
 23
 24public sealed partial class BaseItemRepository
 25{
 26    /// <inheritdoc />
 27    public IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 28    {
 74329        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 74330        dbQuery = dbQuery.AsSingleQuery();
 31
 74332        return dbQuery;
 33    }
 34
 35    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 36    {
 14137        dbQuery = TranslateQuery(dbQuery, context, filter);
 14138        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 14139        dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
 14140        dbQuery = ApplyQueryPaging(dbQuery, filter);
 14141        dbQuery = ApplyNavigations(dbQuery, filter);
 14142        return dbQuery;
 43    }
 44
 45    /// <summary>
 46    /// Trims an ordered query down to the AdjacentTo item and its immediate neighbours.
 47    /// </summary>
 48    private IQueryable<BaseItemEntity> ApplyAdjacencyFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuer
 49    {
 75150        if (filter.AdjacentTo.IsNullOrEmpty())
 51        {
 75152            return dbQuery;
 53        }
 54
 55        // Adjacency is relative to the result set and the order the query asked for, so the ids have
 56        // to be read back in that order.
 057        var orderedIds = dbQuery.Select(e => e.Id).ToList();
 058        var index = orderedIds.IndexOf(filter.AdjacentTo.Value);
 059        if (index < 0)
 60        {
 61            // The item isn't part of this result set, so it has no neighbours in it either.
 062            return dbQuery.Take(0);
 63        }
 64
 065        var start = Math.Max(index - 1, 0);
 066        var adjacentIds = orderedIds.GetRange(start, Math.Min(index + 2, orderedIds.Count) - start);
 67
 068        var adjacentQuery = context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => adjacentIds.Contains(e.Id));
 69
 070        return ApplyOrder(adjacentQuery, filter, context);
 71    }
 72
 73    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 74    {
 75175        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 76        {
 16477            var offset = filter.StartIndex ?? 0;
 78
 16479            if (offset > 0)
 80            {
 081                dbQuery = dbQuery.Skip(offset);
 82            }
 83
 16484            if (filter.Limit.HasValue)
 85            {
 16486                dbQuery = dbQuery.Take(filter.Limit.Value);
 87            }
 88        }
 89
 75190        return dbQuery;
 91    }
 92
 93    private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery
 94    {
 95        // Collapse duplicates sharing a presentation key (e.g. alternate versions), preferring the
 96        // primary version (PrimaryVersionId is null) so detail pages and actions target it instead
 97        // of an arbitrary alternate. Keep the grouped ids as an IQueryable sub-select; materializing
 98        // to a List would inline one bound parameter per id and hit SQLite's variable cap.
 75199        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 751100        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 101        {
 0102            var groupedIds = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey })
 0103                .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
 0104            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 105        }
 751106        else if (enableGroupByPresentationUniqueKey)
 107        {
 19108            var groupedIds = dbQuery.GroupBy(e => e.PresentationUniqueKey)
 19109                .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
 19110            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 111        }
 732112        else if (filter.GroupBySeriesPresentationUniqueKey)
 113        {
 0114            var groupedIds = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.Min(x => x.Id));
 0115            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 116        }
 117        else
 118        {
 732119            dbQuery = dbQuery.Distinct();
 120        }
 121
 751122        if (filter.CollapseBoxSetItems == true)
 123        {
 0124            dbQuery = ApplyBoxSetCollapsing(context, dbQuery, filter.CollapseBoxSetItemTypes);
 125
 126            // Name filters run after collapse so BoxSets match by their own name, not a child's.
 0127            dbQuery = ApplyNameFilters(dbQuery, filter);
 128        }
 129
 751130        dbQuery = ApplyOrder(dbQuery, filter, context);
 131
 751132        return dbQuery;
 133    }
 134
 135    private IQueryable<BaseItemEntity> ApplyBoxSetCollapsing(
 136        JellyfinDbContext context,
 137        IQueryable<BaseItemEntity> dbQuery,
 138        BaseItemKind[] collapsibleTypes)
 139    {
 0140        var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
 141
 0142        var currentIds = dbQuery.Select(e => e.Id);
 143
 0144        if (collapsibleTypes.Length == 0)
 145        {
 146            // Collapse all item types into box sets
 0147            return ApplyBoxSetCollapsingAll(context, currentIds, boxSetTypeName);
 148        }
 149
 150        // Only collapse specific item types, keep others untouched
 0151        var collapsibleTypeNames = collapsibleTypes.Select(t => _itemTypeLookup.BaseItemKindNames[t]).ToList();
 152
 153        // Categorize items in currentIds in a single pass to avoid multiple correlated EXISTS over BaseItems.
 0154        var categorized = context.BaseItems
 0155            .AsNoTracking()
 0156            .Where(bi => currentIds.Contains(bi.Id))
 0157            .Select(bi => new
 0158            {
 0159                bi.Id,
 0160                IsCollapsible = collapsibleTypeNames.Contains(bi.Type),
 0161                IsBoxSet = bi.Type == boxSetTypeName
 0162            });
 163
 0164        var collapsibleChildIds = categorized.Where(c => c.IsCollapsible).Select(c => c.Id);
 165
 166        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0167        var manualBoxSetLinks = context.LinkedChildren
 0168            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0169                && currentIds.Contains(lc.ChildId))
 0170            .Join(
 0171                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0172                lc => lc.ParentId,
 0173                bs => bs.Id,
 0174                (lc, bs) => new { lc.ChildId, lc.ParentId });
 175
 0176        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 177
 178        // Items whose type is NOT collapsible (always kept in results)
 0179        var nonCollapsibleIds = categorized.Where(c => !c.IsCollapsible).Select(c => c.Id);
 180
 181        // Collapsible items that are not a BoxSet themselves and not a manual child of any BoxSet
 0182        var collapsibleNotInBoxSet = categorized
 0183            .Where(c => c.IsCollapsible && !c.IsBoxSet)
 0184            .Select(c => c.Id)
 0185            .Where(id => !childrenInBoxSet.Contains(id));
 186
 187        // BoxSet IDs containing at least one collapsible child item from currentIds
 0188        var boxSetIds = manualBoxSetLinks
 0189            .Where(x => collapsibleChildIds.Contains(x.ChildId))
 0190            .Select(x => x.ParentId)
 0191            .Distinct();
 192
 0193        var collapsedIds = nonCollapsibleIds.Union(collapsibleNotInBoxSet).Union(boxSetIds);
 0194        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 195    }
 196
 197    private static IQueryable<BaseItemEntity> ApplyBoxSetCollapsingAll(
 198        JellyfinDbContext context,
 199        IQueryable<Guid> currentIds,
 200        string boxSetTypeName)
 201    {
 202        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0203        var manualBoxSetLinks = context.LinkedChildren
 0204            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0205                && currentIds.Contains(lc.ChildId))
 0206            .Join(
 0207                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0208                lc => lc.ParentId,
 0209                bs => bs.Id,
 0210                (lc, bs) => new { lc.ChildId, lc.ParentId });
 211
 0212        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 0213        var boxSetIds = manualBoxSetLinks.Select(x => x.ParentId).Distinct();
 214
 215        // Items in currentIds that are not BoxSets themselves and not a manual child of any BoxSet
 0216        var notInBoxSet = context.BaseItems
 0217            .AsNoTracking()
 0218            .Where(e => currentIds.Contains(e.Id) && e.Type != boxSetTypeName)
 0219            .Select(e => e.Id)
 0220            .Where(id => !childrenInBoxSet.Contains(id));
 221
 0222        var collapsedIds = notInBoxSet.Union(boxSetIds);
 0223        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 224    }
 225
 226    private static IQueryable<BaseItemEntity> ApplyNameFilters(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 227    {
 802228        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 229        {
 0230            var nameStartsWithLower = filter.NameStartsWith.ToLowerInvariant();
 0231            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().StartsWith(nameStartsWithLower));
 232        }
 233
 802234        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 235        {
 0236            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 0237            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(startsOrGreaterLower) >= 0);
 238        }
 239
 802240        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 241        {
 0242            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 0243            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(lessThanLower) < 0);
 244        }
 245
 802246        return dbQuery;
 247    }
 248
 249    /// <inheritdoc />
 250    public IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 251    {
 670252        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 253        {
 6254            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 255        }
 256
 670257        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 258        {
 658259            dbQuery = dbQuery.Include(e => e.Provider);
 260        }
 261
 670262        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 263        {
 658264            dbQuery = dbQuery.Include(e => e.LockedFields);
 265        }
 266
 670267        if (filter.DtoOptions.EnableUserData)
 268        {
 669269            dbQuery = dbQuery.Include(e => e.UserData);
 270        }
 271
 670272        if (filter.DtoOptions.EnableImages)
 273        {
 669274            dbQuery = dbQuery.Include(e => e.Images!.OrderBy(i => i.Id));
 275        }
 276
 277        // Include LinkedChildEntities for container types and videos that use them (BoxSet, Playlist,
 278        // CollectionFolder for manual linking; every video type for alternate versions).
 279        // When IncludeItemTypes is empty (any type may be returned), always include them to ensure
 280        // LinkedChildren are loaded before items are saved back, preventing accidental deletion.
 670281        var linkedChildTypes = new[]
 670282        {
 670283            BaseItemKind.BoxSet,
 670284            BaseItemKind.Playlist,
 670285            BaseItemKind.CollectionFolder,
 670286            BaseItemKind.Video,
 670287            BaseItemKind.Movie,
 670288            BaseItemKind.Episode,
 670289            BaseItemKind.MusicVideo,
 670290            BaseItemKind.Trailer
 670291        };
 670292        if (filter.IncludeItemTypes.Length == 0 || filter.IncludeItemTypes.Any(linkedChildTypes.Contains))
 293        {
 474294            dbQuery = dbQuery.Include(e => e.LinkedChildEntities!.OrderBy(l => l.SortOrder));
 295        }
 296
 670297        if (filter.IncludeExtras)
 298        {
 0299            dbQuery = dbQuery.Include(e => e.Extras);
 300        }
 301
 670302        return dbQuery;
 303    }
 304
 305    /// <inheritdoc />
 306    public IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDb
 307    {
 766308        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 766309        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 310
 311        // SeriesDatePlayed requires special handling to avoid correlated subqueries.
 312        // Instead of running a MAX() subquery per-row in ORDER BY, we pre-aggregate
 313        // max played dates per series in one query and left-join it.
 766314        if (!hasSearch && orderBy.Any(o => o.OrderBy == ItemSortBy.SeriesDatePlayed))
 315        {
 0316            return ApplySeriesDatePlayedOrder(query, filter, context, orderBy);
 317        }
 318
 766319        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 320
 766321        if (hasSearch)
 322        {
 0323            var relevanceExpression = OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!);
 0324            orderedQuery = query.OrderBy(relevanceExpression);
 325        }
 326
 327        // Folders carry no played flag of their own, so these two keys go through the same predicate
 328        // the isPlayed filter uses rather than through the stored-column lookup in OrderMapper.
 329        Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy) => sortBy switch
 330        {
 331            ItemSortBy.IsPlayed when filter.User is not null
 332                => AsOrderKey(BuildIsPlayedFilter(context, filter.User)),
 333            ItemSortBy.IsUnplayed when filter.User is not null
 334                => AsOrderKey(BuildIsPlayedFilter(context, filter.User).Not()),
 335            _ => OrderMapper.MapOrderByField(sortBy, filter, context)
 336        };
 337
 766338        if (orderBy.Length > 0)
 339        {
 175340            var firstOrdering = orderBy[0];
 175341            var expression = MapOrderByField(firstOrdering.OrderBy);
 342
 175343            if (orderedQuery is null)
 344            {
 175345                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 175346                    ? query.OrderBy(expression)
 175347                    : query.OrderByDescending(expression);
 348            }
 349            else
 350            {
 0351                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 0352                    ? orderedQuery.ThenBy(expression)
 0353                    : orderedQuery.ThenByDescending(expression);
 354            }
 355
 175356            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 357            {
 6358                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 6359                    ? orderedQuery.ThenBy(e => e.Name)
 6360                    : orderedQuery.ThenByDescending(e => e.Name);
 361            }
 362
 470363            foreach (var item in orderBy.Skip(1))
 364            {
 60365                expression = MapOrderByField(item.OrderBy);
 60366                orderedQuery = item.SortOrder == SortOrder.Ascending
 60367                    ? orderedQuery.ThenBy(expression)
 60368                    : orderedQuery.ThenByDescending(expression);
 369            }
 370        }
 371
 766372        if (orderedQuery is null)
 373        {
 591374            return query.OrderBy(e => e.SortName);
 375        }
 376
 377        // Add SortName as final tiebreaker
 175378        if (!hasSearch && (orderBy.Length == 0 || orderBy.All(o => o.OrderBy is not ItemSortBy.SortName and not ItemSort
 379        {
 111380            orderedQuery = orderedQuery.ThenBy(e => e.SortName);
 381        }
 382
 175383        return orderedQuery;
 384    }
 385
 386    private IQueryable<BaseItemEntity> ApplySeriesDatePlayedOrder(
 387        IQueryable<BaseItemEntity> query,
 388        InternalItemsQuery filter,
 389        JellyfinDbContext context,
 390        (ItemSortBy OrderBy, SortOrder SortOrder)[] orderBy)
 391    {
 392        // Pre-aggregate max played date per series key in ONE query.
 393        // This generates a single: SELECT SeriesPresentationUniqueKey, MAX(LastPlayedDate) ... GROUP BY
 394        // instead of a correlated subquery per outer row.
 0395        IQueryable<UserData> userDataQuery = filter.User is not null
 0396            ? context.UserData.Where(ud => ud.UserId == filter.User.Id && ud.Played)
 0397            : context.UserData.Where(ud => ud.Played);
 398
 0399        var seriesMaxDates = userDataQuery
 0400            .Join(
 0401                context.BaseItems,
 0402                ud => ud.ItemId,
 0403                bi => bi.Id,
 0404                (ud, bi) => new { bi.SeriesPresentationUniqueKey, ud.LastPlayedDate })
 0405            .Where(x => x.SeriesPresentationUniqueKey != null)
 0406            .GroupBy(x => x.SeriesPresentationUniqueKey)
 0407            .Select(g => new { SeriesKey = g.Key!, MaxDate = g.Max(x => x.LastPlayedDate) });
 408
 0409        var joined = query.LeftJoin(
 0410            seriesMaxDates,
 0411            e => e.PresentationUniqueKey,
 0412            s => s.SeriesKey,
 0413            (e, s) => new { Item = e, MaxDate = s != null ? s.MaxDate : (DateTime?)null });
 414
 0415        var seriesSort = orderBy.First(o => o.OrderBy == ItemSortBy.SeriesDatePlayed);
 416
 0417        return seriesSort.SortOrder == SortOrder.Ascending
 0418            ? joined.OrderBy(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item)
 0419            : joined.OrderByDescending(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item);
 420    }
 421
 422    /// <summary>
 423    /// Builds a query for descendants of an ancestor with user access filtering applied.
 424    /// Uses recursive CTE to traverse both hierarchical (AncestorIds) and linked (LinkedChildren) relationships.
 425    /// </summary>
 426    /// <inheritdoc />
 427    public IQueryable<BaseItemEntity> BuildAccessFilteredDescendantsQuery(
 428        JellyfinDbContext context,
 429        InternalItemsQuery filter,
 430        Guid ancestorId)
 431    {
 432        // Use recursive CTE to get all descendants (hierarchical and linked)
 0433        var allDescendantIds = DescendantQueryHelper.GetAllDescendantIds(context, ancestorId);
 434
 0435        var baseQuery = context.BaseItems
 0436            .AsNoTracking()
 0437            .Where(b => allDescendantIds.Contains(b.Id))
 0438            .Where(DescendantQueryHelper.IsCountableLeaf);
 439
 0440        return ApplyAccessFiltering(context, baseQuery, filter);
 441    }
 442
 443    /// <summary>
 444    /// Checks whether the user restricts access to items by parental rating or tags.
 445    /// </summary>
 446    /// <param name="filter">The query filter.</param>
 447    /// <returns><c>true</c> if the query carries parental restrictions.</returns>
 448    private static bool RequiresParentalRestrictions(InternalItemsQuery filter)
 0449        => filter.IncludeInheritedTags.Length > 0
 0450            || filter.ExcludeInheritedTags.Length > 0
 0451            || filter.MaxParentalRating is not null
 0452            || filter.BlockUnratedItems.Length > 0;
 453
 454    /// <summary>
 455    /// Applies user access filtering to a query.
 456    /// Includes TopParentIds, parental rating, and tag filtering.
 457    /// </summary>
 458    /// <inheritdoc />
 459    public IQueryable<BaseItemEntity> ApplyAccessFiltering(
 460        JellyfinDbContext context,
 461        IQueryable<BaseItemEntity> baseQuery,
 462        InternalItemsQuery filter)
 463    {
 33464        baseQuery = ApplyTopParentFiltering(context, baseQuery, filter);
 465
 33466        baseQuery = ApplyParentalRestrictions(context, baseQuery, filter);
 467
 468        // Hide alternate versions behind the primary of their library, and exclude owned non-extra
 469        // items. Extras (trailers, etc.) have OwnerId set but also have ExtraType set — keep those.
 33470        if (!filter.IncludeOwnedItems)
 471        {
 29472            baseQuery = ApplyAlternateVersionFiltering(context, baseQuery)
 29473                .Where(e => e.OwnerId == null || e.ExtraType != null);
 474        }
 475
 33476        return baseQuery;
 477    }
 478
 479    private static IQueryable<BaseItemEntity> ApplyAlternateVersionFiltering(
 480        JellyfinDbContext context,
 481        IQueryable<BaseItemEntity> baseQuery)
 715482        => baseQuery.Where(e => e.PrimaryVersionId == null
 715483            || !context.BaseItems.Any(p => p.Id == e.PrimaryVersionId && p.TopParentId == e.TopParentId));
 484
 485    /// <summary>
 486    /// Restricts a query to the libraries the user may open, exempting requested by-name items.
 487    /// </summary>
 488    /// <param name="context">The database context.</param>
 489    /// <param name="baseQuery">The query to filter.</param>
 490    /// <param name="filter">The query filter.</param>
 491    /// <returns>The filtered query.</returns>
 492    private IQueryable<BaseItemEntity> ApplyTopParentFiltering(
 493        JellyfinDbContext context,
 494        IQueryable<BaseItemEntity> baseQuery,
 495        InternalItemsQuery filter)
 496    {
 835497        var queryTopParentIds = filter.TopParentIds;
 835498        if (queryTopParentIds.Length == 0)
 499        {
 818500            return baseQuery;
 501        }
 502
 17503        var exemptedItemByNameTypes = GetExemptedItemByNameTypes(filter);
 17504        if (exemptedItemByNameTypes.Count == 0)
 505        {
 14506            return baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 507        }
 508
 3509        baseQuery = baseQuery.Where(e => exemptedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w == e.T
 3510        if (filter.UserHasContentRestrictions)
 511        {
 2512            baseQuery = ApplyItemByNameAccessFiltering(baseQuery, context, filter, exemptedItemByNameTypes, queryTopPare
 513        }
 514
 3515        return baseQuery;
 516    }
 517
 518    /// <summary>
 519    /// Returns the by-name types a query asks for, which carry no TopParentId to filter on.
 520    /// </summary>
 521    /// <param name="filter">The query filter.</param>
 522    /// <returns>The type names exempt from library filtering.</returns>
 523    private List<string> GetExemptedItemByNameTypes(InternalItemsQuery filter)
 524    {
 17525        var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 17526        if ((filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0)
 527        {
 2528            return includedItemByNameTypes;
 529        }
 530
 15531        return _itemByNameKinds.Where(filter.IncludeItemTypes.Contains).Select(e => _itemTypeLookup.BaseItemKindNames[e]
 532    }
 533
 534    /// <summary>
 535    /// Keeps a by-name row only when at least one item behind its name is reachable for the user.
 536    /// </summary>
 537    /// <param name="baseQuery">The query to filter.</param>
 538    /// <param name="context">The database context.</param>
 539    /// <param name="filter">The query filter.</param>
 540    /// <param name="itemByNameTypes">The exempted by-name type names.</param>
 541    /// <param name="topParentIds">The libraries the user may open.</param>
 542    /// <returns>The filtered query.</returns>
 543    private IQueryable<BaseItemEntity> ApplyItemByNameAccessFiltering(
 544        IQueryable<BaseItemEntity> baseQuery,
 545        JellyfinDbContext context,
 546        InternalItemsQuery filter,
 547        IReadOnlyList<string> itemByNameTypes,
 548        Guid[] topParentIds)
 549    {
 550        // IncludeOwnedItems: a credit on an alternate version of a reachable movie still counts.
 2551        var accessibleItems = ApplyAccessFiltering(
 2552            context,
 2553            context.BaseItems.AsNoTracking(),
 2554            new InternalItemsQuery(filter.User) { TopParentIds = topParentIds, IncludeOwnedItems = true });
 555
 2556        var personType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 2557        if (itemByNameTypes.Contains(personType))
 558        {
 2559            baseQuery = baseQuery.Where(e => e.Type != personType
 2560                || context.Peoples.Any(p => p.Name == e.Name
 2561                    && context.PeopleBaseItemMap.Any(m => m.PeopleId == p.Id && accessibleItems.Any(i => i.Id == m.ItemI
 562        }
 563
 20564        foreach (var (kind, valueTypes) in _itemByNameValueTypes)
 565        {
 8566            var typeName = _itemTypeLookup.BaseItemKindNames[kind];
 8567            if (!itemByNameTypes.Contains(typeName))
 568            {
 569                continue;
 570            }
 571
 8572            baseQuery = baseQuery.Where(e => e.Type != typeName
 8573                || context.ItemValues.Any(v => valueTypes.Contains(v.Type) && v.CleanValue == e.CleanName
 8574                    && context.ItemValuesMap.Any(m => m.ItemValueId == v.ItemValueId && accessibleItems.Any(i => i.Id ==
 575        }
 576
 2577        return baseQuery;
 578    }
 579
 580    /// <summary>
 581    /// Applies the user's parental rating and tag restrictions to a query.
 582    /// </summary>
 583    /// <param name="context">The database context.</param>
 584    /// <param name="baseQuery">The query to filter.</param>
 585    /// <param name="filter">The query filter.</param>
 586    /// <returns>The filtered query.</returns>
 587    private IQueryable<BaseItemEntity> ApplyParentalRestrictions(
 588        JellyfinDbContext context,
 589        IQueryable<BaseItemEntity> baseQuery,
 590        InternalItemsQuery filter)
 591    {
 592        // Apply parental rating filtering
 33593        if (filter.MaxParentalRating is not null)
 594        {
 0595            baseQuery = baseQuery.Where(BuildMaxParentalRatingFilter(context, filter.MaxParentalRating));
 596        }
 597
 598        // Apply block unrated items filtering
 33599        if (filter.BlockUnratedItems.Length > 0)
 600        {
 0601            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0602            baseQuery = baseQuery.Where(e =>
 0603                e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType));
 604        }
 605
 606        // Apply excluded tags filtering (blocked tags).
 607        // Pre-build the blocked-item-id set as a sub-select; then four index-seek Contains checks
 608        // instead of one EXISTS over a 4-way OR predicate that defeats index seeks.
 33609        if (filter.ExcludeInheritedTags.Length > 0)
 610        {
 0611            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0612            var blockedTagItemIds = context.ItemValuesMap
 0613                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 0614                .Select(f => f.ItemId);
 0615            var blockedByAncestor = ItemsBelowTaggedAncestor(context, blockedTagItemIds);
 616
 0617            baseQuery = baseQuery.Where(e =>
 0618                !blockedTagItemIds.Contains(e.Id)
 0619                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 0620                && !blockedByAncestor.Contains(e.Id)
 0621                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 622        }
 623
 624        // Apply included tags filtering (allowed tags - item must have at least one).
 33625        if (filter.IncludeInheritedTags.Length > 0)
 626        {
 0627            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0628            var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 0629            var allowedTagItemIds = context.ItemValuesMap
 0630                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 0631                .Select(f => f.ItemId);
 632
 0633            var allowedByAncestor = ItemsBelowTaggedAncestor(context, allowedTagItemIds);
 634
 0635            baseQuery = baseQuery.Where(e =>
 0636                allowedTagItemIds.Contains(e.Id)
 0637                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 0638                || allowedByAncestor.Contains(e.Id)
 0639                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 0640
 0641                // People don't carry the tags of the media they appear in and would never match
 0642                || e.Type == personTypeName);
 643        }
 644
 33645        return baseQuery;
 646    }
 647
 648    /// <summary>
 649    /// Reads back the items that carry one of the tagged items as an ancestor.
 650    /// </summary>
 651    /// <param name="context">The database context.</param>
 652    /// <param name="taggedItemIds">The ids of the items carrying the tag.</param>
 653    /// <returns>The ids of the items below one of them.</returns>
 654    private static IQueryable<Guid> ItemsBelowTaggedAncestor(JellyfinDbContext context, IQueryable<Guid> taggedItemIds)
 12655        => context.AncestorIds
 12656            .Where(a => taggedItemIds.Contains(a.ParentItemId))
 12657            .Select(a => a.ItemId);
 658
 659    /// <summary>
 660    /// Builds a filter expression for max parental rating that handles both rated items
 661    /// and unrated BoxSets/Playlists (which check linked children's ratings).
 662    /// </summary>
 663    private static Expression<Func<BaseItemEntity, bool>> BuildMaxParentalRatingFilter(
 664        JellyfinDbContext context,
 665        ParentalRatingScore maxRating)
 666    {
 60667        var maxScore = maxRating.Score;
 60668        var maxSubScore = maxRating.SubScore ?? 0;
 669
 670        // Only a manual link makes an item a container of other items.
 60671        var members = context.LinkedChildren
 60672            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual);
 673
 60674        return e =>
 60675            // The item's own rating, where it has one, has to be within the limit. An unrated item
 60676            // passes here; blocking those is what BlockUnratedItems does.
 60677            (e.InheritedParentalRatingValue == null
 60678                || e.InheritedParentalRatingValue < maxScore
 60679                || (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScor
 60680            // A container is only as visible as its members: a BoxSet or Playlist with nothing left
 60681            // in it for this user is hidden whatever rating it carries itself. BoxSet.IsVisible
 60682            // applies the same rule in memory, and a count has to agree with the listing it counts.
 60683            && (!members.Any(lc => lc.ParentId == e.Id)
 60684                || members.Any(lc => lc.ParentId == e.Id
 60685                    && (lc.Child!.InheritedParentalRatingValue == null
 60686                        || lc.Child.InheritedParentalRatingValue < maxScore
 60687                        || (lc.Child.InheritedParentalRatingValue == maxScore
 60688                            && (lc.Child.InheritedParentalRatingSubValue ?? 0) <= maxSubScore))));
 689    }
 690
 691    /// <inheritdoc />
 692    public IQueryable<BaseItemEntity> GetAccessFilteredLeafItemsQuery(JellyfinDbContext context, User user, bool include
 693    {
 21694        ArgumentNullException.ThrowIfNull(context);
 21695        ArgumentNullException.ThrowIfNull(user);
 696
 21697        var leafItems = context.BaseItems
 21698            .AsNoTracking()
 21699            .Where(DescendantQueryHelper.IsCountableLeaf);
 700
 21701        return ApplyAccessFiltering(context, leafItems, new InternalItemsQuery(user) { IncludeOwnedItems = includeOwnedI
 702    }
 703
 704    private static Expression<Func<BaseItemEntity, object?>> AsOrderKey(Expression<Func<BaseItemEntity, bool>> predicate
 6705        => Expression.Lambda<Func<BaseItemEntity, object?>>(Expression.Convert(predicate.Body, typeof(object)), predicat
 706
 707    /// <inheritdoc />
 708    public Expression<Func<BaseItemEntity, bool>> BuildHasDescendantFilter(JellyfinDbContext context, IQueryable<BaseIte
 709    {
 23710        ArgumentNullException.ThrowIfNull(context);
 23711        ArgumentNullException.ThrowIfNull(descendants);
 712
 713        // Descendants are reachable through the ancestor chain and - for BoxSets and Playlists - as
 714        // linked children, which can themselves be folders contributing their own descendants.
 715        // Every step is a correlated index seek, so only the rows the outer query keeps are visited
 716        // and a folder is left as soon as its first matching descendant is found.
 23717        return e => context.AncestorIds.Any(a => a.ParentItemId == e.Id && descendants.Any(d => d.Id == a.ItemId))
 23718            || context.LinkedChildren.Any(lc => lc.ParentId == e.Id
 23719                && (descendants.Any(d => d.Id == lc.ChildId)
 23720                    || context.AncestorIds.Any(a => a.ParentItemId == lc.ChildId && descendants.Any(d => d.Id == a.ItemI
 721    }
 722}

/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    {
 14123        ArgumentNullException.ThrowIfNull(filter);
 14124        PrepareFilterQuery(filter);
 25
 14126        using var context = _dbProvider.CreateDbContext();
 14127        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 14128    }
 29
 30    /// <inheritdoc />
 31    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
 32    {
 333        ArgumentNullException.ThrowIfNull(filter);
 334        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
 243        PrepareFilterQuery(filter);
 244        var result = new QueryResult<BaseItemDto>();
 45
 246        using var context = _dbProvider.CreateDbContext();
 47
 248        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 49
 250        dbQuery = TranslateQuery(dbQuery, context, filter);
 251        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 252        dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
 53
 254        if (filter.EnableTotalRecordCount)
 55        {
 256            result.TotalRecordCount = dbQuery.Count();
 57        }
 58
 259        dbQuery = ApplyQueryPaging(dbQuery, filter);
 260        dbQuery = ApplyNavigations(dbQuery, filter);
 61
 262        result.Items = dbQuery.AsEnumerable().Where(e => e != null).Select(w => DeserializeBaseItem(w, filter.SkipDeseri
 263        result.StartIndex = filter.StartIndex ?? 0;
 264        return result;
 265    }
 66
 67    /// <inheritdoc />
 68    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
 69    {
 60870        ArgumentNullException.ThrowIfNull(filter);
 60871        PrepareFilterQuery(filter);
 72
 60873        using var context = _dbProvider.CreateDbContext();
 60874        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 75
 60876        dbQuery = TranslateQuery(dbQuery, context, filter);
 77
 60878        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 60879        dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
 60880        dbQuery = ApplyQueryPaging(dbQuery, filter);
 81
 60882        var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
 60883        if (hasRandomSort)
 84        {
 10585            var orderedIds = dbQuery.AsNoTracking().Select(e => e.Id).ToList();
 10586            if (orderedIds.Count == 0)
 87            {
 10488                return Array.Empty<BaseItemDto>();
 89            }
 90
 191            var itemsById = ApplyNavigations(context.BaseItems.AsNoTracking().WhereOneOrMany(orderedIds, e => e.Id), fil
 192                .AsSplitQuery()
 193                .AsEnumerable()
 194                .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 195                .Where(dto => dto != null)
 196                .ToDictionary(i => i!.Id);
 97
 198            return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!;
 99        }
 100
 503101        dbQuery = ApplyNavigations(dbQuery, filter);
 102
 503103        return dbQuery.AsEnumerable().Where(e => e != null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializatio
 608104    }
 105
 106    /// <inheritdoc/>
 107    public IReadOnlyList<BaseItemDto> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 108    {
 0109        ArgumentNullException.ThrowIfNull(filter);
 0110        PrepareFilterQuery(filter);
 111
 112        // Early exit if collection type is not supported
 0113        if (collectionType is not CollectionType.movies and not CollectionType.tvshows and not CollectionType.music and 
 114        {
 0115            return [];
 116        }
 117
 0118        var limit = filter.Limit;
 0119        using var context = _dbProvider.CreateDbContext();
 120
 0121        var baseQuery = PrepareItemQuery(context, filter);
 0122        baseQuery = TranslateQuery(baseQuery, context, filter);
 123
 0124        if (collectionType is CollectionType.tvshows)
 125        {
 0126            return GetLatestTvShowItems(context, baseQuery, filter, limit);
 127        }
 128
 0129        if (collectionType is CollectionType.movies)
 130        {
 0131            return GetLatestMovieItems(context, baseQuery, filter, limit);
 132        }
 133
 0134        if (collectionType is CollectionType.unknown)
 135        {
 0136            var moviesQuery = baseQuery.Where(e => e.SeriesName == null);
 0137            var latestMovies = GetLatestMovieItems(context, moviesQuery, filter, limit);
 0138            var latestShows = GetLatestTvShowItems(context, baseQuery, filter, limit);
 139
 0140            return latestMovies.Concat(latestShows)
 0141                .OrderByDescending(dto => dto.DateCreated)
 0142                .ThenByDescending(dto => dto.Id)
 0143                .Take(limit ?? int.MaxValue)
 0144                .ToList();
 145        }
 146
 0147        var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]!;
 148        IQueryable<BaseItemEntity> topAlbumsQuery;
 149
 150        // When the query is scoped to whole libraries, read the newest albums directly by their own TopParentId.
 0151        if (filter.TopParentIds.Length > 0)
 152        {
 0153            topAlbumsQuery = context.BaseItems.AsNoTracking()
 0154                .Where(album => album.Type == musicAlbumTypeName
 0155                    && !album.IsVirtualItem
 0156                    && album.TopParentId.HasValue)
 0157                .WhereOneOrMany(filter.TopParentIds, album => album.TopParentId!.Value);
 158        }
 159        else
 160        {
 161            // Fallback (e.g. AncestorIds-scoped callers): albums that are the parent of a matching track.
 0162            var albumIdsWithMatchingTrack = context.AncestorIds
 0163                .Join(baseQuery, ai => ai.ItemId, t => t.Id, (ai, _) => ai.ParentItemId);
 0164            topAlbumsQuery = context.BaseItems.AsNoTracking()
 0165                .Where(album => album.Type == musicAlbumTypeName)
 0166                .Where(album => albumIdsWithMatchingTrack.Contains(album.Id));
 167        }
 168
 169        // The album is what gets returned, and neither branch above reads it through the
 170        // user's filters, so its own parental restrictions have to be applied here: a
 171        // matching track does not make an album the user may not see visible.
 0172        var orderedAlbums = ApplyParentalRestrictions(context, topAlbumsQuery, filter)
 0173            .OrderByDescending(album => album.DateCreated)
 0174            .ThenByDescending(album => album.Id);
 175
 0176        var albumIdsQuery = limit.HasValue
 0177            ? orderedAlbums.Take(limit.Value).Select(a => a.Id)
 0178            : orderedAlbums.Select(a => a.Id);
 179
 0180        return LoadLatestByIds(context, albumIdsQuery, filter);
 0181    }
 182
 183    // Keeping idsQuery deferred lets EF emit `WHERE Id IN (<subquery>)`.
 184    private IReadOnlyList<BaseItemDto> LoadLatestByIds(
 185        JellyfinDbContext context,
 186        IQueryable<Guid> idsQuery,
 187        InternalItemsQuery filter)
 188    {
 0189        var itemsQuery = ApplyNavigations(
 0190            context.BaseItems.AsNoTracking().Where(e => idsQuery.Contains(e.Id)),
 0191            filter);
 192
 0193        return itemsQuery
 0194            .OrderByDescending(e => e.DateCreated)
 0195            .ThenByDescending(e => e.Id)
 0196            .AsEnumerable()
 0197            .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 0198            .Where(dto => dto != null)
 0199            .ToArray()!;
 200    }
 201
 202    private IReadOnlyList<BaseItemDto> LoadLatestByIds(
 203        JellyfinDbContext context,
 204        List<Guid> ids,
 205        InternalItemsQuery filter)
 206    {
 0207        if (ids.Count == 0)
 208        {
 0209            return [];
 210        }
 211
 0212        var itemsQuery = ApplyNavigations(
 0213            context.BaseItems.AsNoTracking().WhereOneOrMany(ids, e => e.Id),
 0214            filter);
 215
 0216        return itemsQuery
 0217            .OrderByDescending(e => e.DateCreated)
 0218            .ThenByDescending(e => e.Id)
 0219            .AsEnumerable()
 0220            .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 0221            .Where(dto => dto != null)
 0222            .ToArray()!;
 223    }
 224
 225    /// <summary>
 226    /// Gets the latest movies, deduplicated so each movie only appears once.
 227    /// </summary>
 228    /// <param name="context">The database context.</param>
 229    /// <param name="baseQuery">The query to pull movies from, with filters already applied.</param>
 230    /// <param name="filter">The original query filter, used when loading the final items.</param>
 231    /// <param name="limit">How many items to return.</param>
 232    /// <returns>The latest movies, newest first.</returns>
 233    private IReadOnlyList<BaseItemDto> GetLatestMovieItems(
 234        JellyfinDbContext context,
 235        IQueryable<BaseItemEntity> baseQuery,
 236        InternalItemsQuery filter,
 237        int? limit)
 238    {
 239        // Pick, per PresentationUniqueKey, the newest item; return the newest `limit` of those.
 240        // Build up until limit by streaming through results and deduplicating on the fly.
 0241        var orderedIds = baseQuery
 0242            .Where(e => e.PresentationUniqueKey != null)
 0243            .OrderByDescending(e => e.DateCreated)
 0244            .ThenByDescending(e => e.Id)
 0245            .Select(e => new { e.Id, e.PresentationUniqueKey });
 246
 247        // DistinctBy and Take are lazy, so enumeration stops as soon as limit distinct keys are read.
 0248        var firstIds = orderedIds
 0249            .AsEnumerable()
 0250            .DistinctBy(row => row.PresentationUniqueKey)
 0251            .Select(row => row.Id)
 0252            .Take(limit ?? int.MaxValue)
 0253            .ToList();
 254
 0255        return LoadLatestByIds(context, firstIds, filter);
 256    }
 257
 258    /// <summary>
 259    /// Gets the latest TV show items with smart Season/Series container selection.
 260    /// </summary>
 261    /// <remarks>
 262    /// <para>
 263    /// This method implements intelligent container selection for TV shows in the "Latest" section.
 264    /// Instead of always showing individual episodes, it analyzes recent additions and may return
 265    /// a Season or Series container when multiple related episodes were recently added.
 266    /// </para>
 267    /// <para>
 268    /// The selection logic is:
 269    /// <list type="bullet">
 270    ///     <item>If recent episodes span multiple seasons → return the Series</item>
 271    ///     <item>If multiple recent episodes are from one season AND the series has multiple seasons → return the Seaso
 272    ///     <item>If multiple recent episodes are from one season AND the series has only one season → return the Series
 273    ///     <item>Otherwise → return the most recent Episode</item>
 274    /// </list>
 275    /// </para>
 276    /// </remarks>
 277    /// <param name="context">The database context.</param>
 278    /// <param name="baseQuery">The base query with filters already applied.</param>
 279    /// <param name="filter">The query filter options.</param>
 280    /// <param name="limit">Maximum number of items to return.</param>
 281    /// <returns>A list of BaseItemDto representing the latest TV content.</returns>
 282    private IReadOnlyList<BaseItemDto> GetLatestTvShowItems(JellyfinDbContext context, IQueryable<BaseItemEntity> baseQu
 283    {
 284        // Episodes added within this window are considered "recently added together"
 285        const double RecentAdditionWindowHours = 24.0;
 286
 287        // Step 1: Find the top N series with recently added content, ordered by most recent addition
 0288        var topSeriesWithDates = baseQuery
 0289            .Where(e => e.SeriesName != null)
 0290            .GroupBy(e => e.SeriesName)
 0291            .Select(g => new { SeriesName = g.Key!, MaxDate = g.Max(e => e.DateCreated) })
 0292            .OrderByDescending(g => g.MaxDate);
 293
 0294        if (limit.HasValue)
 295        {
 0296            topSeriesWithDates = topSeriesWithDates.Take(limit.Value).OrderByDescending(g => g.MaxDate);
 297        }
 298
 299        // Materialize series names and cutoff to avoid embedding the GroupBy+OrderBy
 300        // expression tree as a subquery inside the episode query.
 0301        var topSeriesData = topSeriesWithDates
 0302            .Select(g => new { g.SeriesName, g.MaxDate })
 0303            .ToList();
 0304        var topSeriesNames = topSeriesData.Select(g => g.SeriesName).ToList();
 305
 306        // Compute a global date cutoff: the oldest series' max date minus the window.
 307        // Episodes before this cutoff cannot be in any series' "recent additions" window,
 308        // so we can safely exclude them to avoid loading ancient episodes.
 0309        var globalCutoff = topSeriesData.Count > 0
 0310            ? topSeriesData.Min(g => g.MaxDate)?.AddHours(-RecentAdditionWindowHours)
 0311            : null;
 312
 313        // Restrict to episodes of the top series, optionally bounded by the global cutoff.
 0314        var episodeQuery = baseQuery.Where(e => e.SeriesName != null && topSeriesNames.Contains(e.SeriesName));
 0315        if (globalCutoff is not null)
 316        {
 0317            episodeQuery = episodeQuery.Where(e => e.DateCreated >= globalCutoff);
 318        }
 319
 320        // Lightweight projection: only the columns needed for the in-memory analysis below.
 0321        var allEpisodes = episodeQuery
 0322            .OrderByDescending(e => e.DateCreated)
 0323            .ThenByDescending(e => e.Id)
 0324            .Select(e => new { e.Id, e.SeriesName, e.DateCreated, e.SeasonId, e.SeriesId })
 0325            .AsEnumerable();
 326
 327        // Collect all season/series IDs we'll need to look up for count information
 0328        var allSeasonIds = new HashSet<Guid>();
 0329        var allSeriesIds = new HashSet<Guid>();
 330
 331        // Analysis data for each series: recent episode count, season IDs, and the most recent episode ID
 0332        var analysisData = new List<(
 0333            int RecentEpisodeCount,
 0334            List<Guid> SeasonIds,
 0335            Guid? FirstRecentSeriesId,
 0336            DateTime MaxDate,
 0337            Guid MostRecentEpisodeId)>();
 338
 339        // Step 3: Analyze each series to identify recent additions within the time window
 0340        foreach (var group in allEpisodes.GroupBy(e => e.SeriesName))
 341        {
 0342            var episodes = group.ToList();
 0343            var mostRecentDate = episodes[0].DateCreated ?? DateTime.MinValue;
 0344            var recentCutoff = mostRecentDate.AddHours(-RecentAdditionWindowHours);
 345
 346            // Find episodes added within the recent window
 0347            var recentEpisodeCount = 0;
 0348            var seasonIdSet = new HashSet<Guid>();
 0349            Guid? firstRecentSeriesId = null;
 350
 0351            foreach (var ep in episodes)
 352            {
 0353                if (ep.DateCreated >= recentCutoff)
 354                {
 0355                    recentEpisodeCount++;
 0356                    if (ep.SeasonId.HasValue)
 357                    {
 0358                        seasonIdSet.Add(ep.SeasonId.Value);
 359                    }
 360
 0361                    firstRecentSeriesId ??= ep.SeriesId;
 362                }
 363            }
 364
 0365            var seasonIds = seasonIdSet.ToList();
 0366            analysisData.Add((recentEpisodeCount, seasonIds, firstRecentSeriesId, mostRecentDate, episodes[0].Id));
 367
 368            // Track all unique season/series IDs for batch lookups
 0369            foreach (var sid in seasonIds)
 370            {
 0371                allSeasonIds.Add(sid);
 372            }
 373
 0374            if (firstRecentSeriesId.HasValue)
 375            {
 0376                allSeriesIds.Add(firstRecentSeriesId.Value);
 377            }
 378        }
 379
 380        // Step 4: Batch fetch counts - episodes per season and seasons per series
 381        // These counts help determine whether to show Season or Series as the container
 0382        var episodeType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 0383        var seasonType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Season];
 0384        var seasonEpisodeCounts = allSeasonIds.Count > 0
 0385            ? context.BaseItems
 0386                .AsNoTracking()
 0387                .Where(e => e.SeasonId.HasValue && allSeasonIds.Contains(e.SeasonId.Value) && e.Type == episodeType)
 0388                .GroupBy(e => e.SeasonId!.Value)
 0389                .Select(g => new { SeasonId = g.Key, Count = g.Count() })
 0390                .ToDictionary(x => x.SeasonId, x => x.Count)
 0391            : [];
 392
 0393        var seriesSeasonCounts = allSeriesIds.Count > 0
 0394            ? context.BaseItems
 0395                .AsNoTracking()
 0396                .Where(e => e.SeriesId.HasValue && allSeriesIds.Contains(e.SeriesId.Value) && e.Type == seasonType)
 0397                .GroupBy(e => e.SeriesId!.Value)
 0398                .Select(g => new { SeriesId = g.Key, Count = g.Count() })
 0399                .ToDictionary(x => x.SeriesId, x => x.Count)
 0400            : [];
 401
 402        // Step 5: Apply the container selection logic for each series.
 403        // For each series, decide which entity best represents the recent additions:
 404        //   - 1 episode added → show the Episode itself
 405        //   - Multiple episodes in 1 season (multi-season series) → show the Season
 406        //   - Multiple episodes in 1 season (single-season series) → show the Series
 407        //   - Episodes across multiple seasons → show the Series
 0408        var entitiesToFetch = new HashSet<Guid>();
 0409        var seriesResults = new List<(Guid? SeasonId, Guid? SeriesId, DateTime MaxDate, Guid MostRecentEpisodeId)>(analy
 410
 0411        foreach (var (recentEpisodeCount, seasonIds, firstRecentSeriesId, maxDate, mostRecentEpisodeId) in analysisData)
 412        {
 0413            Guid? seasonId = null;
 0414            Guid? seriesId = null;
 415
 0416            if (seasonIds.Count == 1)
 417            {
 418                // All recent episodes are from a single season
 0419                var sid = seasonIds[0];
 0420                var totalEpisodes = seasonEpisodeCounts.GetValueOrDefault(sid, 0);
 0421                var totalSeasonsInSeries = firstRecentSeriesId.HasValue
 0422                    ? seriesSeasonCounts.GetValueOrDefault(firstRecentSeriesId.Value, 1)
 0423                    : 1;
 424
 425                // Check if multiple episodes were added, or if all episodes in the season were added
 0426                var hasMultipleOrAllEpisodes = recentEpisodeCount > 1 || recentEpisodeCount == totalEpisodes;
 427
 0428                if (totalSeasonsInSeries > 1 && hasMultipleOrAllEpisodes)
 429                {
 430                    // Multi-season series with bulk additions: show the Season
 0431                    seasonId = sid;
 0432                    entitiesToFetch.Add(sid);
 433                }
 0434                else if (hasMultipleOrAllEpisodes && firstRecentSeriesId.HasValue)
 435                {
 436                    // Single-season series with bulk additions: show the Series
 0437                    seriesId = firstRecentSeriesId;
 0438                    entitiesToFetch.Add(firstRecentSeriesId.Value);
 439                }
 440
 441                // Otherwise: single episode, will fall through to show the Episode
 442            }
 0443            else if (seasonIds.Count > 1 && firstRecentSeriesId.HasValue)
 444            {
 445                // Recent episodes span multiple seasons: show the Series
 0446                seriesId = firstRecentSeriesId;
 0447                entitiesToFetch.Add(seriesId!.Value);
 448            }
 449
 0450            if (seasonId is null && seriesId is null)
 451            {
 0452                entitiesToFetch.Add(mostRecentEpisodeId);
 453            }
 454
 0455            seriesResults.Add((seasonId, seriesId, maxDate, mostRecentEpisodeId));
 456        }
 457
 458        // Step 5b: A container is what gets returned, so it has to pass the user's access
 459        // filters on its own - a matching episode does not make a Season or Series the user
 460        // may not see visible. Containers that don't pass are replaced by their episode.
 0461        if (RequiresParentalRestrictions(filter) && entitiesToFetch.Count > 0)
 462        {
 0463            var allowedContainerIds = ApplyParentalRestrictions(
 0464                    context,
 0465                    context.BaseItems.AsNoTracking().Where(e => entitiesToFetch.Contains(e.Id)),
 0466                    filter)
 0467                .Select(e => e.Id)
 0468                .ToHashSet();
 469
 0470            for (var i = 0; i < seriesResults.Count; i++)
 471            {
 0472                var (seasonId, seriesId, maxDate, mostRecentEpisodeId) = seriesResults[i];
 0473                if (seasonId.HasValue && !allowedContainerIds.Contains(seasonId.Value))
 474                {
 0475                    seasonId = null;
 476                }
 477
 0478                if (seriesId.HasValue && !allowedContainerIds.Contains(seriesId.Value))
 479                {
 0480                    seriesId = null;
 481                }
 482
 0483                if (seasonId is null && seriesId is null)
 484                {
 0485                    entitiesToFetch.Add(mostRecentEpisodeId);
 486                }
 487
 0488                seriesResults[i] = (seasonId, seriesId, maxDate, mostRecentEpisodeId);
 489            }
 490        }
 491
 492        // Step 6: Fetch the Season/Series entities we decided to return
 0493        var entities = entitiesToFetch.Count > 0
 0494            ? ApplyNavigations(
 0495                    context.BaseItems.AsNoTracking().Where(e => entitiesToFetch.Contains(e.Id)),
 0496                    filter)
 0497                .AsSingleQuery()
 0498                .ToDictionary(e => e.Id)
 0499            : [];
 500
 501        // Step 7: Build final results, preferring Season > Series > Episode.
 502        // All needed entities are already fetched in step 6 with navigation properties.
 0503        var results = new List<(BaseItemEntity Entity, DateTime MaxDate)>(seriesResults.Count);
 0504        foreach (var (seasonId, seriesId, maxDate, mostRecentEpisodeId) in seriesResults)
 505        {
 0506            if (seasonId.HasValue && entities.TryGetValue(seasonId.Value, out var seasonEntity))
 507            {
 0508                results.Add((seasonEntity, maxDate));
 0509                continue;
 510            }
 511
 0512            if (seriesId.HasValue && entities.TryGetValue(seriesId.Value, out var seriesEntity))
 513            {
 0514                results.Add((seriesEntity, maxDate));
 0515                continue;
 516            }
 517
 0518            if (entities.TryGetValue(mostRecentEpisodeId, out var episodeEntity))
 519            {
 0520                results.Add((episodeEntity, maxDate));
 521            }
 522        }
 523
 0524        var finalResults = results
 0525            .OrderByDescending(r => r.MaxDate)
 0526            .ThenByDescending(r => r.Entity.Id);
 527
 0528        if (limit.HasValue)
 529        {
 0530            finalResults = finalResults
 0531            .Take(limit.Value)
 0532            .OrderByDescending(r => r.MaxDate)
 0533            .ThenByDescending(r => r.Entity.Id);
 534        }
 535
 0536        return finalResults
 0537            .Select(r => DeserializeBaseItem(r.Entity, filter.SkipDeserialization))
 0538            .Where(dto => dto is not null)
 0539            .ToArray()!;
 540    }
 541
 542    /// <inheritdoc/>
 543    public async Task<bool> ItemExistsAsync(Guid id)
 544    {
 61545        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 61546        await using (dbContext.ConfigureAwait(false))
 547        {
 61548            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 549        }
 61550    }
 551
 552    /// <inheritdoc  />
 553    public BaseItemDto? RetrieveItem(Guid id)
 554    {
 110555        if (id.IsEmpty())
 556        {
 0557            throw new ArgumentException("Guid can't be empty", nameof(id));
 558        }
 559
 110560        using var context = _dbProvider.CreateDbContext();
 110561        var dbQuery = PrepareItemQuery(context, new()
 110562        {
 110563            DtoOptions = new()
 110564            {
 110565                EnableImages = true
 110566            }
 110567        });
 110568        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 110569            .Include(e => e.Provider)
 110570            .Include(e => e.LockedFields)
 110571            .Include(e => e.UserData)
 110572            .Include(e => e.Images)
 110573            .Include(e => e.LinkedChildEntities)
 110574            .AsSingleQuery();
 575
 110576        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 110577        if (item is null)
 578        {
 110579            return null;
 580        }
 581
 0582        return DeserializeBaseItem(item);
 110583    }
 584
 585    /// <inheritdoc />
 586    public bool GetIsPlayed(User user, Guid id, bool recursive)
 587    {
 2588        using var dbContext = _dbProvider.CreateDbContext();
 589
 2590        if (recursive)
 591        {
 2592            var descendantIds = DescendantQueryHelper.GetAllDescendantIds(dbContext, id);
 593
 2594            return dbContext.BaseItems
 2595                    .Where(e => descendantIds.Contains(e.Id) && !e.IsFolder && !e.IsVirtualItem)
 2596                    .All(BuildLeafIsPlayedFilter(dbContext, user.Id));
 597        }
 598
 0599        return dbContext.BaseItems.Where(e => e.ParentId == id).All(BuildLeafIsPlayedFilter(dbContext, user.Id));
 2600    }
 601
 602    /// <inheritdoc />
 603    public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery filter)
 604    {
 1605        ArgumentNullException.ThrowIfNull(filter);
 1606        PrepareFilterQuery(filter);
 607
 1608        using var context = _dbProvider.CreateDbContext();
 1609        var baseQuery = PrepareItemQuery(context, filter);
 1610        baseQuery = TranslateQuery(baseQuery, context, filter);
 611
 1612        var matchingItemIds = baseQuery.Select(e => e.Id);
 613
 1614        var years = baseQuery
 1615            .Where(e => e.ProductionYear != null && e.ProductionYear > 0)
 1616            .Select(e => e.ProductionYear!.Value)
 1617            .Distinct()
 1618            .OrderBy(y => y)
 1619            .ToArray();
 620
 1621        var officialRatings = baseQuery
 1622            .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty)
 1623            .Select(e => e.OfficialRating!)
 1624            .Distinct()
 1625            .OrderBy(r => r)
 1626            .ToArray();
 627
 1628        var tags = context.ItemValuesMap
 1629            .Join(
 1630                context.ItemValues,
 1631                ivm => ivm.ItemValueId,
 1632                iv => iv.ItemValueId,
 1633                (ivm, iv) => new { ivm.ItemId, iv.Type, iv.CleanValue, iv.Value })
 1634            .Where(iv => iv.Type == ItemValueType.Tags)
 1635            .Where(iv => matchingItemIds.Contains(iv.ItemId))
 1636            .GroupBy(iv => iv.CleanValue)
 1637            .Select(g => g.Min(iv => iv.Value))
 1638            .OrderBy(t => t)
 1639            .ToArray();
 640
 1641        var genres = context.ItemValuesMap
 1642            .Join(
 1643                context.ItemValues,
 1644                ivm => ivm.ItemValueId,
 1645                iv => iv.ItemValueId,
 1646                (ivm, iv) => new { ivm.ItemId, iv.Type, iv.CleanValue, iv.Value })
 1647            .Where(iv => iv.Type == ItemValueType.Genre)
 1648            .Where(iv => matchingItemIds.Contains(iv.ItemId))
 1649            .GroupBy(iv => iv.CleanValue)
 1650            .Select(g => g.Min(iv => iv.Value))
 1651            .OrderBy(g => g)
 1652            .ToArray();
 653
 1654        return new QueryFiltersLegacy
 1655        {
 1656            Years = years,
 1657            OfficialRatings = officialRatings,
 1658            Tags = tags,
 1659            Genres = genres
 1660        };
 1661    }
 662}

/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{
 328    private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
 29
 330    private static readonly string ImdbProviderName = MetadataProvider.Imdb.ToString().ToLowerInvariant();
 331    private static readonly string TmdbProviderName = MetadataProvider.Tmdb.ToString().ToLowerInvariant();
 332    private static readonly string TvdbProviderName = MetadataProvider.Tvdb.ToString().ToLowerInvariant();
 33
 34    // A fresh expression per access: EF rejects a query tree that reuses one lambda parameter
 35    // instance across several lambdas, and this filter is combined into a tree more than once.
 3836    private static Expression<Func<BaseItemEntity, bool>> IsFolderFilter => e => e.IsFolder;
 37
 38    // Shared by the isPlayed filter and the IsPlayed/IsUnplayed ordering so the two cannot disagree.
 39    private Expression<Func<BaseItemEntity, bool>> BuildIsPlayedFilter(JellyfinDbContext context, User user)
 40    {
 41        // Folders (Series, Seasons, BoxSets, albums, ...) carry no played state of their own and count
 42        // as played once no descendant is left unplayed.
 1743        var unplayedLeafItems = GetAccessFilteredLeafItemsQuery(context, user)
 1744            .Where(BuildLeafIsPlayedFilter(context, user.Id).Not());
 45
 1746        return IsFolderFilter.And(BuildHasDescendantFilter(context, unplayedLeafItems).Not())
 1747            .Or(IsFolderFilter.Not().And(BuildLeafIsPlayedFilter(context, user.Id)));
 48    }
 49
 50    private static Expression<Func<BaseItemEntity, bool>> BuildLeafIsPlayedFilter(JellyfinDbContext context, Guid userId
 51    {
 4052        var playedItemIds = context.UserData
 4053            .Where(ud => ud.UserId == userId && ud.Played)
 4054            .Select(ud => ud.ItemId);
 55
 56        // The primaries of every version group holding a played row, whichever version carries it.
 4057        var playedGroupIds = context.BaseItems
 4058            .Where(v => v.PrimaryVersionId != null
 4059                && context.UserData.Any(ud => ud.UserId == userId
 4060                    && ud.Played
 4061                    && (ud.ItemId == v.Id || ud.ItemId == v.PrimaryVersionId)))
 4062            .Select(v => v.PrimaryVersionId!.Value);
 63
 4064        return e => playedItemIds.Contains(e.Id)
 4065            || playedGroupIds.Contains(e.Id)
 4066            || (e.PrimaryVersionId != null && playedGroupIds.Contains(e.PrimaryVersionId.Value));
 67    }
 68
 69    // "und" is the language filters' stand-in for a track that declares no language at all.
 70    private static string NormalizeLanguage(string language)
 571        => string.Equals(language, "und", StringComparison.OrdinalIgnoreCase) ? "und" : language;
 72
 73    // The primary versions whose alternate version satisfies a dimension bound. Anchored on
 74    // PrimaryVersionId so the filtered index carries it rather than a scan of every item.
 75    private static IQueryable<Guid> VersionsMatchingDimension(JellyfinDbContext context, Expression<Func<BaseItemEntity,
 3076        => context.BaseItems
 3077            .Where(v => v.PrimaryVersionId != null)
 3078            .Where(bound)
 3079            .Select(v => v.PrimaryVersionId!.Value);
 80
 81    /// <inheritdoc />
 82    public IQueryable<BaseItemEntity> TranslateQuery(
 83        IQueryable<BaseItemEntity> baseQuery,
 84        JellyfinDbContext context,
 85        InternalItemsQuery filter)
 86    {
 87        const int HDWidth = 1200;
 88        const int UHDWidth = 3800;
 89        const int UHDHeight = 2100;
 90
 80291        var minWidth = filter.MinWidth;
 80292        var maxWidth = filter.MaxWidth;
 80293        var now = DateTime.UtcNow;
 94
 80295        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 96        {
 897            bool includeSD = false;
 898            bool includeHD = false;
 899            bool include4K = false;
 100
 8101            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 102            {
 3103                includeSD = true;
 104            }
 105
 8106            if (filter.IsHD.HasValue && filter.IsHD.Value)
 107            {
 2108                includeHD = true;
 109            }
 110
 8111            if (filter.Is4K.HasValue && filter.Is4K.Value)
 112            {
 3113                include4K = true;
 114            }
 115
 116            // A 4K remux of an SD primary is a version of the same item, so the bucket a caller filters
 117            // on is the best any of the item's versions offers, not just the primary file's. Three sets,
 118            // because a bucket is as much about what the version group does not have as what it does, and
 119            // because an unprobed primary can still be placed by a version that does carry dimensions.
 120            // The filtered PrimaryVersionId index keeps all three to the few items that have versions.
 8121            var versionsSd = VersionsMatchingDimension(context, v => v.Width > 0 && v.Width < HDWidth);
 8122            var versionsHd = VersionsMatchingDimension(context, v => v.Width >= HDWidth);
 8123            var versions4K = VersionsMatchingDimension(context, v => v.Width >= UHDWidth || v.Height >= UHDHeight);
 124
 125            // Only the SD test needs the Width > 0 guard against a row with no dimensions: such a row
 126            // cannot reach the HD or 4K bound anyway, and EF lowers the HD bucket's negated "not itself
 127            // 4K" guard to CASE WHEN ... THEN 0 ELSE 1, which already reads unknown as not 4K rather
 128            // than propagating a null. Folders (Series, BoxSets) answer on their descendants, bucketed
 129            // exactly as a top-level item is so that the two cannot disagree; the navigation properties
 130            // (a.Item, lc.Child) give EXISTS + JOIN rather than nested IN (SELECT ...).
 8131            baseQuery = baseQuery.Where(e =>
 8132                (!e.IsFolder
 8133                    && ((includeSD
 8134                            && ((e.Width > 0 && e.Width < HDWidth) || versionsSd.Contains(e.Id))
 8135                            && !versionsHd.Contains(e.Id)
 8136                            && !versions4K.Contains(e.Id))
 8137                        || (includeHD
 8138                            && (e.Width >= HDWidth || versionsHd.Contains(e.Id))
 8139                            && !(e.Width >= UHDWidth || e.Height >= UHDHeight)
 8140                            && !versions4K.Contains(e.Id))
 8141                        || (include4K
 8142                            && (e.Width >= UHDWidth || e.Height >= UHDHeight || versions4K.Contains(e.Id)))))
 8143                || (e.IsFolder
 8144                    && (e.Children!.Any(a =>
 8145                            (includeSD
 8146                                && ((a.Item.Width > 0 && a.Item.Width < HDWidth) || versionsSd.Contains(a.ItemId))
 8147                                && !versionsHd.Contains(a.ItemId)
 8148                                && !versions4K.Contains(a.ItemId))
 8149                            || (includeHD
 8150                                && (a.Item.Width >= HDWidth || versionsHd.Contains(a.ItemId))
 8151                                && !(a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight)
 8152                                && !versions4K.Contains(a.ItemId))
 8153                            || (include4K
 8154                                && (a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight || versions4K.Contains(a.Item
 8155                        || context.LinkedChildren.Any(lc =>
 8156                            lc.ParentId == e.Id
 8157                            && ((includeSD
 8158                                    && ((lc.Child!.Width > 0 && lc.Child!.Width < HDWidth) || versionsSd.Contains(lc.Chi
 8159                                    && !versionsHd.Contains(lc.ChildId)
 8160                                    && !versions4K.Contains(lc.ChildId))
 8161                                || (includeHD
 8162                                    && (lc.Child!.Width >= HDWidth || versionsHd.Contains(lc.ChildId))
 8163                                    && !(lc.Child!.Width >= UHDWidth || lc.Child!.Height >= UHDHeight)
 8164                                    && !versions4K.Contains(lc.ChildId))
 8165                                || (include4K
 8166                                    && (lc.Child!.Width >= UHDWidth || lc.Child!.Height >= UHDHeight || versions4K.Conta
 167        }
 168
 169        // Same reasoning as the resolution filter: a dimension bound is met if any version meets it.
 802170        if (minWidth.HasValue)
 171        {
 2172            var versionsWideEnough = VersionsMatchingDimension(context, v => v.Width >= minWidth);
 2173            baseQuery = baseQuery.Where(e => e.Width >= minWidth || versionsWideEnough.Contains(e.Id));
 174        }
 175
 802176        if (filter.MinHeight.HasValue)
 177        {
 0178            var minHeight = filter.MinHeight;
 0179            var versionsTallEnough = VersionsMatchingDimension(context, v => v.Height >= minHeight);
 0180            baseQuery = baseQuery.Where(e => e.Height >= minHeight || versionsTallEnough.Contains(e.Id));
 181        }
 182
 183        // An upper bound inverts that: it is met only if no version breaches it, since the item's
 184        // resolution is the best its version group offers.
 802185        if (maxWidth.HasValue)
 186        {
 2187            var versionsTooWide = VersionsMatchingDimension(context, v => v.Width > maxWidth);
 2188            baseQuery = baseQuery.Where(e => e.Width <= maxWidth && !versionsTooWide.Contains(e.Id));
 189        }
 190
 802191        if (filter.MaxHeight.HasValue)
 192        {
 2193            var maxHeight = filter.MaxHeight;
 2194            var versionsTooTall = VersionsMatchingDimension(context, v => v.Height > maxHeight);
 2195            baseQuery = baseQuery.Where(e => e.Height <= maxHeight && !versionsTooTall.Contains(e.Id));
 196        }
 197
 802198        if (filter.IsLocked.HasValue)
 199        {
 60200            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 201        }
 202
 802203        var tags = filter.Tags.ToList();
 802204        var excludeTags = filter.ExcludeTags.ToList();
 205
 802206        if (filter.IsMovie.HasValue)
 207        {
 6208            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 6209                && (filter.IncludeItemTypes.Length == 0
 6210                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 6211                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 212
 6213            if (!shouldIncludeAllMovieTypes)
 214            {
 0215                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 216            }
 217        }
 218
 802219        if (filter.IsSeries.HasValue)
 220        {
 0221            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 222        }
 223
 802224        if (filter.IsSports.HasValue)
 225        {
 0226            if (filter.IsSports.Value)
 227            {
 0228                tags.Add("Sports");
 229            }
 230            else
 231            {
 0232                excludeTags.Add("Sports");
 233            }
 234        }
 235
 802236        if (filter.IsNews.HasValue)
 237        {
 0238            if (filter.IsNews.Value)
 239            {
 0240                tags.Add("News");
 241            }
 242            else
 243            {
 0244                excludeTags.Add("News");
 245            }
 246        }
 247
 802248        if (filter.IsKids.HasValue)
 249        {
 0250            if (filter.IsKids.Value)
 251            {
 0252                tags.Add("Kids");
 253            }
 254            else
 255            {
 0256                excludeTags.Add("Kids");
 257            }
 258        }
 259
 802260        if (!string.IsNullOrEmpty(filter.SearchTerm))
 261        {
 0262            var cleanedSearchTerm = filter.SearchTerm.GetCleanValue();
 0263            var originalSearchTerm = filter.SearchTerm;
 0264            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 265            {
 0266                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 0267                var likeSearchTerm = $"%{originalSearchTerm.Trim('%')}%";
 0268                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 269            }
 270            else
 271            {
 0272                var likeSearchTerm = $"%{originalSearchTerm}%";
 0273                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 274            }
 275        }
 276
 802277        if (filter.IsFolder.HasValue)
 278        {
 27279            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 280        }
 281
 802282        var includeTypes = filter.IncludeItemTypes;
 283
 284        // Only specify excluded types if no included types are specified
 802285        if (filter.IncludeItemTypes.Length == 0)
 286        {
 457287            var excludeTypes = filter.ExcludeItemTypes;
 457288            if (excludeTypes.Length == 1)
 289            {
 0290                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 291                {
 0292                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 293                }
 294            }
 457295            else if (excludeTypes.Length > 1)
 296            {
 0297                var excludeTypeName = new List<string>();
 0298                foreach (var excludeType in excludeTypes)
 299                {
 0300                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 301                    {
 0302                        excludeTypeName.Add(baseItemKindName!);
 303                    }
 304                }
 305
 0306                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 307            }
 308        }
 309        else
 310        {
 345311            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 345312            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 313        }
 314
 802315        if (filter.ChannelIds.Count > 0)
 316        {
 0317            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 318        }
 319
 802320        if (!filter.ParentId.IsEmpty())
 321        {
 264322            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 323        }
 324
 802325        if (!string.IsNullOrWhiteSpace(filter.Path))
 326        {
 0327            var pathToQuery = GetPathToSave(filter.Path);
 0328            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 329        }
 330
 802331        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 332        {
 0333            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 334        }
 335
 802336        if (filter.MinCommunityRating.HasValue)
 337        {
 0338            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 339        }
 340
 802341        if (filter.MinIndexNumber.HasValue)
 342        {
 0343            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 344        }
 345
 802346        if (filter.MinParentAndIndexNumber.HasValue)
 347        {
 0348            baseQuery = baseQuery
 0349                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 350        }
 351
 802352        if (filter.MinDateCreated.HasValue)
 353        {
 0354            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 355        }
 356
 802357        if (filter.MinDateLastSaved.HasValue)
 358        {
 0359            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 360        }
 361
 802362        if (filter.MinDateLastSavedForUser.HasValue)
 363        {
 0364            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 365        }
 366
 802367        if (filter.IndexNumber.HasValue)
 368        {
 0369            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 370        }
 371
 802372        if (filter.ParentIndexNumber.HasValue)
 373        {
 0374            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 375        }
 376
 802377        if (filter.ParentIndexNumberNotEquals.HasValue)
 378        {
 0379            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 380        }
 381
 802382        var minEndDate = filter.MinEndDate;
 802383        var maxEndDate = filter.MaxEndDate;
 384
 802385        if (filter.HasAired.HasValue)
 386        {
 0387            if (filter.HasAired.Value)
 388            {
 0389                maxEndDate = DateTime.UtcNow;
 390            }
 391            else
 392            {
 0393                minEndDate = DateTime.UtcNow;
 394            }
 395        }
 396
 802397        if (minEndDate.HasValue)
 398        {
 0399            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 400        }
 401
 802402        if (maxEndDate.HasValue)
 403        {
 0404            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 405        }
 406
 802407        if (filter.MinStartDate.HasValue)
 408        {
 0409            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 410        }
 411
 802412        if (filter.MaxStartDate.HasValue)
 413        {
 0414            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 415        }
 416
 802417        if (filter.MinPremiereDate.HasValue)
 418        {
 0419            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 420        }
 421
 802422        if (filter.MaxPremiereDate.HasValue)
 423        {
 0424            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 425        }
 426
 802427        if (filter.TrailerTypes.Length > 0)
 428        {
 0429            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 0430            baseQuery = baseQuery.Where(e => e.TrailerTypes!.Any(w => trailerTypes.Contains(w.Id)));
 431        }
 432
 802433        if (filter.IsAiring.HasValue)
 434        {
 0435            if (filter.IsAiring.Value)
 436            {
 0437                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 438            }
 439            else
 440            {
 0441                baseQuery = baseQuery.Where(e => e.StartDate > now || e.EndDate < now);
 442            }
 443        }
 444
 802445        if (filter.PersonIds.Length > 0)
 446        {
 0447            var peopleEntityIds = context.BaseItems
 0448                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 0449                .Join(
 0450                    context.Peoples,
 0451                    b => b.Name,
 0452                    p => p.Name,
 0453                    (b, p) => p.Id);
 454
 0455            var personTypes = filter.PersonTypes;
 0456            baseQuery = baseQuery
 0457                .Where(e => context.PeopleBaseItemMap
 0458                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId) && (personTypes.Length == 0 || pe
 459        }
 460
 802461        if (!string.IsNullOrWhiteSpace(filter.Person))
 462        {
 0463            var personTypes = filter.PersonTypes;
 0464            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person && (personTypes.Length =
 465        }
 466
 802467        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 468        {
 0469            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 470        }
 471
 802472        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 473        {
 0474            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 475        }
 476
 802477        if (!string.IsNullOrWhiteSpace(filter.Name))
 478        {
 8479            var cleanName = filter.Name.GetCleanValue();
 8480            baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 481        }
 482
 802483        var nameContains = filter.NameContains;
 802484        if (!string.IsNullOrWhiteSpace(nameContains))
 485        {
 0486            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 487            {
 0488                nameContains = $"%{nameContains.Trim('%')}%";
 0489                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 490            }
 491            else
 492            {
 0493                var likeNameContains = $"%{nameContains}%";
 0494                baseQuery = baseQuery.Where(e =>
 0495                                    e.CleanName!.Contains(nameContains)
 0496                                    || EF.Functions.Like(e.OriginalTitle, likeNameContains));
 497            }
 498        }
 499
 500        // When box set collapsing is active, defer name-range filters to after the collapse.
 501        // Otherwise, items are filtered by their own name but then collapsed into a BoxSet
 502        // whose name may fall in a different range (e.g. "21 Jump Street" is under "#"
 503        // but its BoxSet "Jump Street Collection" should appear under "J").
 802504        if (filter.CollapseBoxSetItems != true)
 505        {
 802506            baseQuery = ApplyNameFilters(baseQuery, filter);
 507        }
 508
 802509        if (filter.ImageTypes.Length > 0)
 510        {
 163511            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 163512            baseQuery = baseQuery.Where(e => e.Images!.Any(w => imgTypes.Contains(w.ImageType)));
 513        }
 514
 802515        if (filter.IsLiked.HasValue)
 516        {
 0517            var likedIds = context.UserData
 0518                .Where(ud => ud.UserId == filter.User!.Id && ud.Rating >= UserItemData.MinLikeValue)
 0519                .Select(ud => ud.ItemId);
 520
 0521            baseQuery = filter.IsLiked.Value
 0522                ? baseQuery.Where(e => likedIds.Contains(e.Id))
 0523                : baseQuery.Where(e => !likedIds.Contains(e.Id));
 524        }
 525
 802526        if (filter.IsFavoriteOrLiked.HasValue || filter.IsFavorite.HasValue)
 527        {
 0528            var favoriteIds = context.UserData
 0529                .Where(ud => ud.UserId == filter.User!.Id && ud.IsFavorite)
 0530                .Select(ud => ud.ItemId);
 531
 0532            if (filter.IsFavoriteOrLiked.HasValue)
 533            {
 0534                baseQuery = filter.IsFavoriteOrLiked.Value
 0535                    ? baseQuery.Where(e => favoriteIds.Contains(e.Id))
 0536                    : baseQuery.Where(e => !favoriteIds.Contains(e.Id));
 537            }
 538
 0539            if (filter.IsFavorite.HasValue)
 540            {
 0541                baseQuery = filter.IsFavorite.Value
 0542                    ? baseQuery.Where(e => favoriteIds.Contains(e.Id))
 0543                    : baseQuery.Where(e => !favoriteIds.Contains(e.Id));
 544            }
 545        }
 546
 802547        if (filter.IsPlayed.HasValue)
 548        {
 11549            var isPlayedFilter = BuildIsPlayedFilter(context, filter.User!);
 550
 11551            baseQuery = baseQuery.Where(filter.IsPlayed.Value ? isPlayedFilter : isPlayedFilter.Not());
 552        }
 553
 802554        if (filter.IsResumable.HasValue)
 555        {
 2556            var userId = filter.User!.Id;
 2557            var isResumable = filter.IsResumable.Value;
 558
 559            // In-progress user data rows; alternate versions track their own progress.
 2560            var inProgress = context.UserData
 2561                .Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0);
 562
 563            // Series and Seasons are resumable when a descendant is in progress, or when they hold both
 564            // played and unplayed descendants (partially watched). Alternate versions keep their own
 565            // progress, so they count towards the in-progress check but not towards the played/unplayed one.
 2566            var leafItems = GetAccessFilteredLeafItemsQuery(context, filter.User!);
 2567            var inProgressLeafItems = GetAccessFilteredLeafItemsQuery(context, filter.User!, includeOwnedItems: true)
 2568                .Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0));
 569
 570            // Every other folder kind is a container rather than one continuous piece of media
 2571            var resumableFolderTypes = _resumableFolderKinds
 2572                .Select(kind => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(kind))
 2573                .ToArray();
 2574            var folderIsResumableFilter = IsFolderFilter.And(e => resumableFolderTypes.Contains(e.Type))
 2575                .And(BuildHasDescendantFilter(context, inProgressLeafItems)
 2576                    .Or(BuildHasDescendantFilter(context, leafItems.Where(BuildLeafIsPlayedFilter(context, userId)))
 2577                        .And(BuildHasDescendantFilter(context, leafItems.Where(BuildLeafIsPlayedFilter(context, userId).
 578
 2579            if (isResumable)
 580            {
 581                // Resume queries surface the version that was actually played, which may be an alternate.
 582                // Match each version on its own progress rather than coalescing onto the primary.
 2583                var inProgressIds = inProgress.Select(ud => ud.ItemId);
 584
 2585                baseQuery = baseQuery.Where(folderIsResumableFilter
 2586                    .Or(IsFolderFilter.Not().And(e => inProgressIds.Contains(e.Id))));
 587
 588                // When several versions of the same item are in progress, keep only the most recently played one, use i
 589                // Only in-progress siblings can eliminate a candidate: a version without progress has a NULL max LastPl
 590                // which is never greater and never ties. Restricting the sibling scan to the in-progress set keeps this
 591                // the user's Continue Watching count instead of forcing a full BaseItems scan (COALESCE keys are non-in
 592                // Items in no version group at all have no sibling that could eliminate them, so short-circuit the scan
 2593                baseQuery = baseQuery.Where(e => e.IsFolder
 2594                    || (e.PrimaryVersionId == null && !context.BaseItems.Any(a => a.PrimaryVersionId == e.Id))
 2595                    || !context.BaseItems
 2596                        .Where(s => s.Id != e.Id
 2597                            && inProgressIds.Contains(s.Id)
 2598                            && (s.PrimaryVersionId ?? s.Id) == (e.PrimaryVersionId ?? e.Id))
 2599                        .Any(s =>
 2600                            inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
 2601                                > inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
 2602                            || (inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
 2603                                    == inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
 2604                                && s.Id.CompareTo(e.Id) < 0)));
 605            }
 606            else
 607            {
 608                // Not-resumable queries operate on primaries only.
 0609                var resumableMovieIds = inProgress
 0610                    .Join(context.BaseItems, ud => ud.ItemId, bi => bi.Id, (ud, bi) => bi.PrimaryVersionId ?? bi.Id);
 611
 0612                baseQuery = baseQuery.Where(IsFolderFilter.And(folderIsResumableFilter.Not())
 0613                    .Or(IsFolderFilter.Not().And(e => !resumableMovieIds.Contains(e.Id))));
 614            }
 615        }
 616
 802617        if (filter.ArtistIds.Length > 0)
 618        {
 0619            baseQuery = baseQuery.WhereReferencedItem(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter
 620        }
 621
 802622        if (filter.AlbumArtistIds.Length > 0)
 623        {
 0624            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 625        }
 626
 802627        if (filter.ContributingArtistIds.Length > 0)
 628        {
 0629            var contributingNames = context.BaseItems
 0630                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 0631                .Select(b => b.CleanName);
 632
 0633            baseQuery = baseQuery.Where(e =>
 0634                e.ItemValues!.Any(ivm =>
 0635                    ivm.ItemValue.Type == ItemValueType.Artist &&
 0636                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 0637                &&
 0638                !e.ItemValues!.Any(ivm =>
 0639                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 0640                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 641        }
 642
 802643        if (filter.AlbumIds.Length > 0)
 644        {
 0645            baseQuery = baseQuery.Where(e => e.ParentId.HasValue && filter.AlbumIds.Contains(e.ParentId.Value));
 646        }
 647
 802648        if (filter.ExcludeArtistIds.Length > 0)
 649        {
 0650            baseQuery = baseQuery.WhereReferencedItem(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter
 651        }
 652
 802653        if (filter.GenreIds.Count > 0)
 654        {
 0655            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds);
 656        }
 657
 802658        if (filter.Genres.Count > 0)
 659        {
 0660            var cleanGenres = filter.Genres.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValu
 0661            baseQuery = baseQuery
 0662                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 663        }
 664
 802665        if (tags.Count > 0)
 666        {
 0667            var cleanValues = tags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, str
 0668            baseQuery = baseQuery
 0669                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 670        }
 671
 802672        if (excludeTags.Count > 0)
 673        {
 0674            var cleanValues = excludeTags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueM
 0675            baseQuery = baseQuery
 0676                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 677        }
 678
 802679        if (filter.StudioIds.Length > 0)
 680        {
 0681            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds);
 682        }
 683
 802684        if (filter.OfficialRatings.Length > 0)
 685        {
 0686            var ratings = filter.OfficialRatings;
 0687            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, e => ratings.Contains(e.OfficialRating));
 688        }
 689
 802690        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 802691        if (filter.MinParentalRating != null)
 692        {
 0693            var min = filter.MinParentalRating;
 0694            var minScore = min.Score;
 0695            var minSubScore = min.SubScore ?? 0;
 696
 0697            minParentalRatingFilter = e =>
 0698                e.InheritedParentalRatingValue == null ||
 0699                e.InheritedParentalRatingValue > minScore ||
 0700                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 701        }
 702
 802703        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 802704        if (filter.MaxParentalRating != null)
 705        {
 60706            maxParentalRatingFilter = BuildMaxParentalRatingFilter(context, filter.MaxParentalRating);
 707        }
 708
 802709        if (filter.HasParentalRating ?? false)
 710        {
 0711            if (minParentalRatingFilter != null)
 712            {
 0713                baseQuery = baseQuery.Where(minParentalRatingFilter);
 714            }
 715
 0716            if (maxParentalRatingFilter != null)
 717            {
 0718                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 719            }
 720        }
 802721        else if (filter.BlockUnratedItems.Length > 0)
 722        {
 0723            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0724            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 725
 0726            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 727            {
 0728                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 729            }
 0730            else if (minParentalRatingFilter != null)
 731            {
 0732                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 733            }
 0734            else if (maxParentalRatingFilter != null)
 735            {
 0736                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 737            }
 738            else
 739            {
 0740                baseQuery = baseQuery.Where(unratedItemFilter);
 741            }
 742        }
 802743        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 744        {
 60745            if (minParentalRatingFilter != null)
 746            {
 0747                baseQuery = baseQuery.Where(minParentalRatingFilter);
 748            }
 749
 60750            if (maxParentalRatingFilter != null)
 751            {
 60752                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 753            }
 754        }
 742755        else if (!filter.HasParentalRating ?? false)
 756        {
 0757            baseQuery = baseQuery
 0758                .Where(e => e.InheritedParentalRatingValue == null);
 759        }
 760
 802761        if (filter.HasOfficialRating.HasValue)
 762        {
 0763            Expression<Func<BaseItemEntity, bool>> hasRating =
 0764                e => e.OfficialRating != null && e.OfficialRating != string.Empty;
 765
 0766            baseQuery = filter.HasOfficialRating.Value
 0767                ? baseQuery.WhereItemOrDescendantMatches(context, hasRating)
 0768                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasRating);
 769        }
 770
 802771        if (filter.HasOverview.HasValue)
 772        {
 0773            if (filter.HasOverview.Value)
 774            {
 0775                baseQuery = baseQuery
 0776                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 777            }
 778            else
 779            {
 0780                baseQuery = baseQuery
 0781                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 782            }
 783        }
 784
 802785        if (filter.HasOwnerId.HasValue)
 786        {
 0787            if (filter.HasOwnerId.Value)
 788            {
 0789                baseQuery = baseQuery
 0790                    .Where(e => e.OwnerId != null);
 791            }
 792            else
 793            {
 0794                baseQuery = baseQuery
 0795                    .Where(e => e.OwnerId == null);
 796            }
 797        }
 802798        else if (filter.OwnerIds.Length == 0 && filter.ExtraTypes.Length == 0 && !filter.IncludeOwnedItems)
 799        {
 800            // Exclude owned non-extra items from general queries.
 801            // Extras (trailers, etc.) have OwnerId set but also have ExtraType set - keep those.
 766802            baseQuery = baseQuery.Where(e => e.OwnerId == null || e.ExtraType != null);
 803
 804            // Alternate versions (PrimaryVersionId set) are normally hidden behind their primary, but
 805            // resume queries keep them so the actually-played version can surface instead of collapsing
 806            // onto the primary, and the library scan keeps them so a merged version is not mistaken for
 807            // a new item.
 766808            if (filter.IsResumable != true && !filter.IncludeAlternateVersions)
 809            {
 686810                baseQuery = ApplyAlternateVersionFiltering(context, baseQuery);
 811            }
 812        }
 813
 802814        if (filter.OwnerIds.Length > 0)
 815        {
 6816            baseQuery = baseQuery.Where(e => e.OwnerId != null && filter.OwnerIds.Contains(e.OwnerId.Value));
 817        }
 818
 802819        if (filter.ExtraTypes.Length > 0)
 820        {
 821            // Convert ExtraType enum to BaseItemExtraType enum via int cast (same underlying values)
 0822            var extraTypeValues = filter.ExtraTypes.Select(e => (BaseItemExtraType?)(int)e).ToArray();
 0823            baseQuery = baseQuery.Where(e => e.ExtraType != null && extraTypeValues.Contains(e.ExtraType));
 824        }
 825
 802826        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 827        {
 3828            var lang = NormalizeLanguage(filter.HasNoAudioTrackWithLanguage);
 3829            var undetermined = string.Equals(lang, "und", StringComparison.Ordinal);
 3830            var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Audio, lang);
 831            // A track only an alternate version carries still belongs to the item a caller sees, so the
 832            // item's own streams alone do not decide this. Same for every stream filter below.
 3833            var versionsWithAudio = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
 3834            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
 835
 3836            baseQuery = baseQuery
 3837                .Where(e =>
 3838                    (!e.IsFolder
 3839                        && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Audio
 3840                            && (ms.Language == lang || (undetermined && string.IsNullOrEmpty(ms.Language))))
 3841                        && !versionsWithAudio.Contains(e.Id))
 3842                    || (e.IsFolder && !foldersWithAudio.Contains(e.Id)));
 843        }
 844
 802845        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 846        {
 0847            var lang = NormalizeLanguage(filter.HasNoInternalSubtitleTrackWithLanguage);
 0848            var undetermined = string.Equals(lang, "und", StringComparison.Ordinal);
 0849            var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang, IsExternal: false);
 0850            var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
 0851            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
 852
 0853            baseQuery = baseQuery
 0854                .Where(e =>
 0855                    (!e.IsFolder
 0856                        && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && !ms.IsExternal
 0857                            && (ms.Language == lang || (undetermined && string.IsNullOrEmpty(ms.Language))))
 0858                        && !versionsWithSubtitles.Contains(e.Id))
 0859                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 860        }
 861
 802862        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 863        {
 0864            var lang = NormalizeLanguage(filter.HasNoExternalSubtitleTrackWithLanguage);
 0865            var undetermined = string.Equals(lang, "und", StringComparison.Ordinal);
 0866            var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang, IsExternal: true);
 0867            var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
 0868            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
 869
 0870            baseQuery = baseQuery
 0871                .Where(e =>
 0872                    (!e.IsFolder
 0873                        && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.IsExternal
 0874                            && (ms.Language == lang || (undetermined && string.IsNullOrEmpty(ms.Language))))
 0875                        && !versionsWithSubtitles.Contains(e.Id))
 0876                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 877        }
 878
 802879        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 880        {
 2881            var lang = NormalizeLanguage(filter.HasNoSubtitleTrackWithLanguage);
 2882            var undetermined = string.Equals(lang, "und", StringComparison.Ordinal);
 2883            var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang);
 2884            var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
 2885            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
 886
 2887            baseQuery = baseQuery
 2888                .Where(e =>
 2889                    (!e.IsFolder
 2890                        && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle
 2891                            && (ms.Language == lang || (undetermined && string.IsNullOrEmpty(ms.Language))))
 2892                        && !versionsWithSubtitles.Contains(e.Id))
 2893                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 894        }
 895
 802896        if (filter.HasSubtitles.HasValue)
 897        {
 6898            var hasSubtitles = filter.HasSubtitles.Value;
 6899            var criteria = new HasSubtitles();
 6900            var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
 6901            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
 6902            if (hasSubtitles)
 903            {
 3904                baseQuery = baseQuery
 3905                    .Where(e =>
 3906                        (!e.IsFolder && (e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle)
 3907                            || versionsWithSubtitles.Contains(e.Id)))
 3908                        || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 909            }
 910            else
 911            {
 3912                baseQuery = baseQuery
 3913                    .Where(e =>
 3914                        (!e.IsFolder && !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle)
 3915                            && !versionsWithSubtitles.Contains(e.Id))
 3916                        || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 917            }
 918        }
 919
 802920        if (filter.SubtitleLanguages.Count > 0)
 921        {
 4922            var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, filter.SubtitleLanguages);
 4923            var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
 4924            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
 4925            baseQuery = baseQuery
 4926                .Where(e =>
 4927                    (!e.IsFolder && (e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle
 4928                        && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") &
 4929                        || versionsWithSubtitles.Contains(e.Id)))
 4930                    || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 931        }
 932
 802933        if (filter.AudioLanguages.Count > 0)
 934        {
 1935            var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Audio, filter.AudioLanguages);
 1936            var versionsWithAudio = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
 1937            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
 1938            baseQuery = baseQuery
 1939                .Where(e =>
 1940                    (!e.IsFolder && (e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio
 1941                        && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && stri
 1942                        || versionsWithAudio.Contains(e.Id)))
 1943                    || (e.IsFolder && foldersWithAudio.Contains(e.Id)));
 944        }
 945
 802946        if (filter.HasChapterImages.HasValue)
 947        {
 2948            var hasChapterImages = filter.HasChapterImages.Value;
 2949            var criteria = new HasChapterImages();
 2950            var versionsWithChapterImages = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
 2951            var foldersWithChapterImages = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
 2952            if (hasChapterImages)
 953            {
 2954                baseQuery = baseQuery
 2955                    .Where(e =>
 2956                        (!e.IsFolder && (e.Chapters!.Any(f => f.ImagePath != null)
 2957                            || versionsWithChapterImages.Contains(e.Id)))
 2958                        || (e.IsFolder && foldersWithChapterImages.Contains(e.Id)));
 959            }
 960            else
 961            {
 0962                baseQuery = baseQuery
 0963                    .Where(e =>
 0964                        (!e.IsFolder && !e.Chapters!.Any(f => f.ImagePath != null)
 0965                            && !versionsWithChapterImages.Contains(e.Id))
 0966                        || (e.IsFolder && !foldersWithChapterImages.Contains(e.Id)));
 967            }
 968        }
 969
 802970        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 971        {
 20972            baseQuery = baseQuery
 20973                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 974        }
 975
 802976        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 977        {
 0978            baseQuery = baseQuery
 0979                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 980        }
 981
 802982        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 983        {
 20984            baseQuery = baseQuery
 20985                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 986        }
 987
 802988        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 989        {
 20990            baseQuery = baseQuery
 20991                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 992        }
 993
 802994        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 995        {
 0996            baseQuery = baseQuery
 0997                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 998        }
 999
 8021000        if (filter.Years.Length > 0)
 1001        {
 01002            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 1003        }
 1004
 8021005        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 8021006        if (isVirtualItem.HasValue)
 1007        {
 291008            baseQuery = baseQuery
 291009                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 1010        }
 1011
 8021012        if (filter.IsSpecialSeason.HasValue)
 1013        {
 01014            if (filter.IsSpecialSeason.Value)
 1015            {
 01016                baseQuery = baseQuery
 01017                    .Where(e => e.IndexNumber == 0);
 1018            }
 1019            else
 1020            {
 01021                baseQuery = baseQuery
 01022                    .Where(e => e.IndexNumber != 0);
 1023            }
 1024        }
 1025
 8021026        if (filter.IsUnaired.HasValue)
 1027        {
 01028            if (filter.IsUnaired.Value)
 1029            {
 01030                baseQuery = baseQuery
 01031                    .Where(e => e.PremiereDate >= now);
 1032            }
 1033            else
 1034            {
 01035                baseQuery = baseQuery
 01036                    .Where(e => e.PremiereDate < now);
 1037            }
 1038        }
 1039
 8021040        if (filter.MediaTypes.Length > 0)
 1041        {
 271042            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 271043            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 1044        }
 1045
 8021046        if (filter.ItemIds.Length > 0)
 1047        {
 01048            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 1049        }
 1050
 8021051        if (filter.ExcludeItemIds.Length > 0)
 1052        {
 01053            baseQuery = baseQuery
 01054                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 1055        }
 1056
 8021057        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 1058        {
 01059            baseQuery = baseQuery.WhereExcludeProviderIds(filter.ExcludeProviderIds);
 1060        }
 1061
 8021062        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 1063        {
 01064            baseQuery = baseQuery.WhereHasAnyProviderId(filter.HasAnyProviderId);
 1065        }
 1066
 8021067        if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
 1068        {
 01069            baseQuery = baseQuery.WhereHasAnyProviderIds(filter.HasAnyProviderIds);
 1070        }
 1071
 8021072        if (filter.HasImdbId.HasValue)
 1073        {
 01074            baseQuery = filter.HasImdbId.Value
 01075                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == ImdbProviderName))
 01076                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != ImdbProviderName));
 1077        }
 1078
 8021079        if (filter.HasTmdbId.HasValue)
 1080        {
 01081            baseQuery = filter.HasTmdbId.Value
 01082                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TmdbProviderName))
 01083                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TmdbProviderName));
 1084        }
 1085
 8021086        if (filter.HasTvdbId.HasValue)
 1087        {
 01088            baseQuery = filter.HasTvdbId.Value
 01089                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TvdbProviderName))
 01090                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TvdbProviderName));
 1091        }
 1092
 8021093        baseQuery = ApplyTopParentFiltering(context, baseQuery, filter);
 1094
 8021095        if (filter.AncestorIds.Length > 0)
 1096        {
 1041097            var ancestorFilter = filter.AncestorIds.OneOrManyExpressionBuilder<AncestorId, Guid>(f => f.ParentItemId);
 1041098            baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
 1099        }
 1100
 8021101        if (filter.DescendantOfId.HasValue)
 1102        {
 41103            var descendantIds = DescendantQueryHelper.GetAllDescendantIds(context, filter.DescendantOfId.Value);
 41104            baseQuery = baseQuery.Where(e => descendantIds.Contains(e.Id));
 1105        }
 1106
 8021107        if (filter.LinkedChildAncestorIds.Length > 0)
 1108        {
 1109            // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ances
 01110            var linkedChildAncestorIds = filter.LinkedChildAncestorIds;
 01111            baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc =>
 01112                lc.ParentId == e.Id
 01113                && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId))));
 1114        }
 1115
 8021116        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 1117        {
 01118            baseQuery = baseQuery
 01119                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 1120        }
 1121
 8021122        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 1123        {
 01124            baseQuery = baseQuery
 01125                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 1126        }
 1127
 1128        // Pre-build the blocked-item-id set as a sub-select
 8021129        if (filter.ExcludeInheritedTags.Length > 0)
 1130        {
 51131            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 51132            var blockedTagItemIds = context.ItemValuesMap
 51133                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 51134                .Select(f => f.ItemId);
 51135            var blockedByAncestor = ItemsBelowTaggedAncestor(context, blockedTagItemIds);
 1136
 51137            baseQuery = baseQuery.Where(e =>
 51138                !blockedTagItemIds.Contains(e.Id)
 51139                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 51140                && !blockedByAncestor.Contains(e.Id)
 51141                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 1142        }
 1143
 8021144        if (filter.IncludeInheritedTags.Length > 0)
 1145        {
 71146            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 71147            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 71148            var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 71149            var allowedTagItemIds = context.ItemValuesMap
 71150                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 71151                .Select(f => f.ItemId);
 71152            var allowedByAncestor = ItemsBelowTaggedAncestor(context, allowedTagItemIds);
 1153
 71154            baseQuery = baseQuery.Where(e =>
 71155                allowedTagItemIds.Contains(e.Id)
 71156                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 71157                || allowedByAncestor.Contains(e.Id)
 71158                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 71159
 71160                // People don't carry the tags of the media they appear in and would never match
 71161                || e.Type == personTypeName
 71162
 71163                // A playlist should be accessible to its owner regardless of allowed tags
 71164                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 1165        }
 1166
 8021167        if (filter.SeriesStatuses.Length > 0)
 1168        {
 01169            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 01170            baseQuery = baseQuery
 01171                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 1172        }
 1173
 8021174        if (filter.BoxSetLibraryFolders.Length > 0)
 1175        {
 01176            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 01177            baseQuery = baseQuery
 01178                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 1179        }
 1180
 8021181        if (filter.VideoTypes.Length > 0)
 1182        {
 1183            // Dvds and Blu-rays can either be stored in a folder structure or as an iso file
 1184            // => to find all matches we need to check both: VideoType and IsoType
 1185            // alternatively, we could provide specific IsoType filters
 01186            var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
 01187            var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray();
 01188            Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoT
 01189            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
 1190        }
 1191
 8021192        if (filter.Is3D.HasValue)
 1193        {
 01194            Expression<Func<BaseItemEntity, bool>> is3D = e => e.Data!.Contains("Video3DFormat");
 1195
 01196            baseQuery = filter.Is3D.Value
 01197                ? baseQuery.WhereItemOrDescendantMatches(context, is3D)
 01198                : baseQuery.WhereNeitherItemNorDescendantMatches(context, is3D);
 1199        }
 1200
 8021201        if (filter.IsPlaceHolder.HasValue)
 1202        {
 01203            Expression<Func<BaseItemEntity, bool>> isPlaceHolder = e => e.Data!.Contains("IsPlaceHolder\":true");
 1204
 01205            baseQuery = filter.IsPlaceHolder.Value
 01206                ? baseQuery.WhereItemOrDescendantMatches(context, isPlaceHolder)
 01207                : baseQuery.WhereNeitherItemNorDescendantMatches(context, isPlaceHolder);
 1208        }
 1209
 1210        // An extra is owned by the single version of an item it is named after, so an extra on any
 1211        // version counts for the item itself
 1212        IQueryable<Guid> WithPrimaryVersions(IQueryable<Guid> ownerIds)
 1213            => ownerIds.Concat(context.BaseItems
 1214                .Where(version => version.PrimaryVersionId != null && ownerIds.Contains(version.Id))
 1215                .Select(version => version.PrimaryVersionId!.Value));
 1216
 8021217        if (filter.HasSpecialFeature.HasValue)
 1218        {
 01219            var itemsWithExtras = WithPrimaryVersions(context.BaseItems
 01220                .Where(extra => extra.OwnerId != null
 01221                    && extra.ExtraType != null
 01222                    && extra.ExtraType != BaseItemExtraType.Unknown
 01223                    && extra.ExtraType != BaseItemExtraType.Trailer
 01224                    && extra.ExtraType != BaseItemExtraType.ThemeSong
 01225                    && extra.ExtraType != BaseItemExtraType.ThemeVideo)
 01226                .Select(extra => extra.OwnerId!.Value))
 01227                .Distinct();
 1228
 01229            Expression<Func<BaseItemEntity, bool>> hasExtras = e => itemsWithExtras.Contains(e.Id);
 1230
 01231            baseQuery = filter.HasSpecialFeature.Value
 01232                ? baseQuery.WhereItemOrDescendantMatches(context, hasExtras)
 01233                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasExtras);
 1234        }
 1235
 8021236        if (filter.HasTrailer.HasValue)
 1237        {
 01238            var trailerOwnerIds = WithPrimaryVersions(context.BaseItems
 01239                .Where(extra => extra.ExtraType == BaseItemExtraType.Trailer && extra.OwnerId != null)
 01240                .Select(extra => extra.OwnerId!.Value));
 1241
 01242            Expression<Func<BaseItemEntity, bool>> hasTrailer = e => trailerOwnerIds.Contains(e.Id);
 1243
 01244            baseQuery = filter.HasTrailer.Value
 01245                ? baseQuery.WhereItemOrDescendantMatches(context, hasTrailer)
 01246                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasTrailer);
 1247        }
 1248
 8021249        if (filter.HasThemeSong.HasValue)
 1250        {
 01251            var themeSongOwnerIds = WithPrimaryVersions(context.BaseItems
 01252                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeSong && extra.OwnerId != null)
 01253                .Select(extra => extra.OwnerId!.Value));
 1254
 01255            Expression<Func<BaseItemEntity, bool>> hasThemeSong = e => themeSongOwnerIds.Contains(e.Id);
 1256
 01257            baseQuery = filter.HasThemeSong.Value
 01258                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeSong)
 01259                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeSong);
 1260        }
 1261
 8021262        if (filter.HasThemeVideo.HasValue)
 1263        {
 01264            var themeVideoOwnerIds = WithPrimaryVersions(context.BaseItems
 01265                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeVideo && extra.OwnerId != null)
 01266                .Select(extra => extra.OwnerId!.Value));
 1267
 01268            Expression<Func<BaseItemEntity, bool>> hasThemeVideo = e => themeVideoOwnerIds.Contains(e.Id);
 1269
 01270            baseQuery = filter.HasThemeVideo.Value
 01271                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeVideo)
 01272                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeVideo);
 1273        }
 1274
 8021275        if (filter.AiredDuringSeason.HasValue)
 1276        {
 01277            var seasonNumber = filter.AiredDuringSeason.Value;
 01278            if (seasonNumber < 1)
 1279            {
 01280                baseQuery = baseQuery.Where(e => e.ParentIndexNumber == seasonNumber);
 1281            }
 1282            else
 1283            {
 01284                var seasonStr = seasonNumber.ToString(CultureInfo.InvariantCulture);
 01285                baseQuery = baseQuery.Where(e =>
 01286                    e.ParentIndexNumber == seasonNumber
 01287                    || (e.Data != null && (
 01288                        e.Data.Contains("\"AirsAfterSeasonNumber\":" + seasonStr)
 01289                        || e.Data.Contains("\"AirsBeforeSeasonNumber\":" + seasonStr))));
 1290            }
 1291        }
 1292
 8021293        return baseQuery;
 1294    }
 1295}

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()
GetMediaStreamLanguages(MediaBrowser.Controller.Entities.InternalItemsQuery,MediaBrowser.Model.Entities.MediaStreamType)
GetTagNames(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemValueNames(System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Collections.Generic.IReadOnlyList`1<System.String>)
GetItemValues(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>,System.String)
BuildItemCountsByCleanName(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>,System.Collections.Generic.IReadOnlyList`1<System.String>)
BuildEpisodeCountsByCleanName(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.ItemValueMap>,System.Collections.Generic.IReadOnlyList`1<System.ValueTuple`3<System.String,System.Nullable`1<System.Guid>,System.Int32>>,System.String,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)
ApplyAdjacencyFilter(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,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)
RequiresParentalRestrictions(MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyAccessFiltering(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyAlternateVersionFiltering(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>)
ApplyTopParentFiltering(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetExemptedItemByNameTypes(MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyItemByNameAccessFiltering(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Guid[])
ApplyParentalRestrictions(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ItemsBelowTaggedAncestor(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<System.Guid>)
BuildMaxParentalRatingFilter(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Model.Entities.ParentalRatingScore)
GetAccessFilteredLeafItemsQuery(Jellyfin.Database.Implementations.JellyfinDbContext,Jellyfin.Database.Implementations.Entities.User,System.Boolean)
AsOrderKey(System.Linq.Expressions.Expression`1<System.Func`2<Jellyfin.Database.Implementations.Entities.BaseItemEntity,System.Boolean>>)
BuildHasDescendantFilter(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>)
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)
LoadLatestByIds(Jellyfin.Database.Implementations.JellyfinDbContext,System.Collections.Generic.List`1<System.Guid>,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetLatestMovieItems(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Nullable`1<System.Int32>)
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()
get_IsFolderFilter()
BuildIsPlayedFilter(Jellyfin.Database.Implementations.JellyfinDbContext,Jellyfin.Database.Implementations.Entities.User)
BuildLeafIsPlayedFilter(Jellyfin.Database.Implementations.JellyfinDbContext,System.Guid)
NormalizeLanguage(System.String)
VersionsMatchingDimension(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.Expressions.Expression`1<System.Func`2<Jellyfin.Database.Implementations.Entities.BaseItemEntity,System.Boolean>>)
TranslateQuery(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)