< Summary - Jellyfin

Line coverage
28%
Covered lines: 424
Uncovered lines: 1067
Coverable lines: 1491
Total lines: 2949
Line coverage: 28.4%
Branch coverage
39%
Covered branches: 263
Total branches: 660
Branch coverage: 39.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 4/15/2026 - 12:14:34 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.6% (387/764) Total lines: 27364/16/2026 - 12:15:18 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/19/2026 - 12:14:27 AM Line coverage: 51.8% (717/1384) Branch coverage: 50.3% (386/766) Total lines: 27365/4/2026 - 12:15:16 AM Line coverage: 28.4% (397/1394) Branch coverage: 43.6% (277/634) Total lines: 28075/6/2026 - 12:15:23 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 28145/11/2026 - 12:15:59 AM Line coverage: 28.2% (397/1404) Branch coverage: 43.5% (277/636) Total lines: 28155/12/2026 - 12:15:57 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 28185/16/2026 - 12:15:55 AM Line coverage: 28% (400/1424) Branch coverage: 43.6% (282/646) Total lines: 28535/20/2026 - 12:15:44 AM Line coverage: 28% (400/1424) Branch coverage: 39.6% (256/646) Total lines: 28535/22/2026 - 12:15:17 AM Line coverage: 28% (401/1429) Branch coverage: 39.6% (257/648) Total lines: 28626/1/2026 - 12:16:05 AM Line coverage: 27.9% (401/1437) Branch coverage: 39.5% (257/650) Total lines: 28726/8/2026 - 12:16:15 AM Line coverage: 28% (402/1433) Branch coverage: 39.6% (258/650) Total lines: 28656/15/2026 - 12:16:09 AM Line coverage: 28% (402/1432) Branch coverage: 39.6% (258/650) Total lines: 28647/6/2026 - 12:16:28 AM Line coverage: 29% (422/1452) Branch coverage: 39.8% (263/660) Total lines: 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: 2949 4/15/2026 - 12:14:34 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.6% (387/764) Total lines: 27364/16/2026 - 12:15:18 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 27364/19/2026 - 12:14:27 AM Line coverage: 51.8% (717/1384) Branch coverage: 50.3% (386/766) Total lines: 27365/4/2026 - 12:15:16 AM Line coverage: 28.4% (397/1394) Branch coverage: 43.6% (277/634) Total lines: 28075/6/2026 - 12:15:23 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 28145/11/2026 - 12:15:59 AM Line coverage: 28.2% (397/1404) Branch coverage: 43.5% (277/636) Total lines: 28155/12/2026 - 12:15:57 AM Line coverage: 28.2% (397/1403) Branch coverage: 43.5% (277/636) Total lines: 28185/16/2026 - 12:15:55 AM Line coverage: 28% (400/1424) Branch coverage: 43.6% (282/646) Total lines: 28535/20/2026 - 12:15:44 AM Line coverage: 28% (400/1424) Branch coverage: 39.6% (256/646) Total lines: 28535/22/2026 - 12:15:17 AM Line coverage: 28% (401/1429) Branch coverage: 39.6% (257/648) Total lines: 28626/1/2026 - 12:16:05 AM Line coverage: 27.9% (401/1437) Branch coverage: 39.5% (257/650) Total lines: 28726/8/2026 - 12:16:15 AM Line coverage: 28% (402/1433) Branch coverage: 39.6% (258/650) Total lines: 28656/15/2026 - 12:16:09 AM Line coverage: 28% (402/1432) Branch coverage: 39.6% (258/650) Total lines: 28647/6/2026 - 12:16:28 AM Line coverage: 29% (422/1452) Branch coverage: 39.8% (263/660) Total lines: 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: 2949

Coverage delta

Coverage delta 24 -24

Metrics

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

File(s)

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Linq;
 6using Jellyfin.Data.Enums;
 7using Jellyfin.Database.Implementations.Entities;
 8using MediaBrowser.Controller.Entities;
 9using MediaBrowser.Model.Dto;
 10using MediaBrowser.Model.Entities;
 11using MediaBrowser.Model.Querying;
 12using Microsoft.EntityFrameworkCore;
 13using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 14
 15namespace Jellyfin.Server.Implementations.Item;
 16
 17public sealed partial class BaseItemRepository
 18{
 19    /// <inheritdoc />
 20    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 21    {
 022        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 23    }
 24
 25    /// <inheritdoc />
 26    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 27    {
 028        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 29    }
 30
 31    /// <inheritdoc />
 32    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 33    {
 034        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 35    }
 36
 37    /// <inheritdoc />
 38    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 39    {
 040        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 41    }
 42
 43    /// <inheritdoc />
 44    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 45    {
 046        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 47    }
 48
 49    /// <inheritdoc />
 50    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 51    {
 052        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 53    }
 54
 55    /// <inheritdoc />
 56    public IReadOnlyList<string> GetStudioNames()
 57    {
 1858        return GetItemValueNames(_getStudiosValueTypes, [], []);
 59    }
 60
 61    /// <inheritdoc />
 62    public IReadOnlyList<string> GetAllArtistNames()
 63    {
 1864        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 65    }
 66
 67    /// <inheritdoc />
 68    public IReadOnlyList<string> GetMusicGenreNames()
 69    {
 1870        return GetItemValueNames(
 1871            _getGenreValueTypes,
 1872            _itemTypeLookup.MusicGenreTypes,
 1873            []);
 74    }
 75
 76    /// <inheritdoc />
 77    public IReadOnlyList<string> GetGenreNames()
 78    {
 1879        return GetItemValueNames(
 1880            _getGenreValueTypes,
 1881            [],
 1882            _itemTypeLookup.MusicGenreTypes);
 83    }
 84
 85    /// <inheritdoc />
 86    public IReadOnlyList<string> GetMediaStreamLanguages(InternalItemsQuery filter, MediaStreamType mediaStreamType)
 87    {
 088        ArgumentNullException.ThrowIfNull(filter);
 89
 090        using var context = _dbProvider.CreateDbContext();
 91
 092        return TranslateQuery(
 093            context.BaseItems.Include(e => e.MediaStreams).Where(e => e.Id != EF.Constant(PlaceholderId)),
 094            context,
 095            new InternalItemsQuery(filter.User)
 096            {
 097                IncludeOwnedItems = filter.IncludeOwnedItems,
 098                ExcludeItemTypes = filter.ExcludeItemTypes,
 099                IncludeItemTypes = filter.IncludeItemTypes,
 0100                MediaTypes = filter.MediaTypes,
 0101                AncestorIds = filter.AncestorIds,
 0102                ItemIds = filter.ItemIds,
 0103                TopParentIds = filter.TopParentIds,
 0104                ParentId = filter.ParentId,
 0105                IsAiring = filter.IsAiring,
 0106                IsMovie = filter.IsMovie,
 0107                IsSports = filter.IsSports,
 0108                IsKids = filter.IsKids,
 0109                IsNews = filter.IsNews,
 0110                IsSeries = filter.IsSeries
 0111            })
 0112            .Where(e => e.MediaStreams != null)
 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    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 121    {
 72122        using var context = _dbProvider.CreateDbContext();
 123
 72124        var query = context.ItemValuesMap
 72125            .AsNoTracking()
 72126            .Where(e => itemValueTypes.Any(w => w == e.ItemValue.Type));
 72127        if (withItemTypes.Count > 0)
 128        {
 18129            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 130        }
 131
 72132        if (excludeItemTypes.Count > 0)
 133        {
 18134            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 135        }
 136
 72137        return query.Select(e => e.ItemValue)
 72138            .GroupBy(e => e.CleanValue)
 72139            .Select(g => g.Min(v => v.Value)!)
 72140            .ToArray();
 72141    }
 142
 143    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 144    {
 0145        ArgumentNullException.ThrowIfNull(filter);
 146
 0147        if (!filter.Limit.HasValue)
 148        {
 0149            filter.EnableTotalRecordCount = false;
 150        }
 151
 0152        using var context = _dbProvider.CreateDbContext();
 153
 0154        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 0155        {
 0156            ExcludeItemTypes = filter.ExcludeItemTypes,
 0157            IncludeItemTypes = filter.IncludeItemTypes,
 0158            MediaTypes = filter.MediaTypes,
 0159            AncestorIds = filter.AncestorIds,
 0160            ItemIds = filter.ItemIds,
 0161            TopParentIds = filter.TopParentIds,
 0162            ParentId = filter.ParentId,
 0163            IsAiring = filter.IsAiring,
 0164            IsMovie = filter.IsMovie,
 0165            IsSports = filter.IsSports,
 0166            IsKids = filter.IsKids,
 0167            IsNews = filter.IsNews,
 0168            IsSeries = filter.IsSeries
 0169        });
 170
 171        // Keep this as an IQueryable sub-select. Materializing to a list would inline one
 172        // bound parameter per CleanValue and hit SQLite's variable cap on libraries with
 173        // high-cardinality value types (e.g. tens of thousands of artists).
 0174        var matchingCleanValues = context.ItemValuesMap
 0175            .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
 0176            .Join(
 0177                innerQueryFilter,
 0178                ivm => ivm.ItemId,
 0179                g => g.Id,
 0180                (ivm, g) => ivm.ItemValue.CleanValue)
 0181            .Distinct();
 182
 0183        var innerQuery = PrepareItemQuery(context, filter)
 0184            .Where(e => e.Type == returnType)
 0185            .Where(e => matchingCleanValues.Contains(e.CleanName!));
 186
 0187        var outerQueryFilter = new InternalItemsQuery(filter.User)
 0188        {
 0189            IsPlayed = filter.IsPlayed,
 0190            IsFavorite = filter.IsFavorite,
 0191            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 0192            IsLiked = filter.IsLiked,
 0193            IsLocked = filter.IsLocked,
 0194            NameLessThan = filter.NameLessThan,
 0195            NameStartsWith = filter.NameStartsWith,
 0196            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 0197            Tags = filter.Tags,
 0198            OfficialRatings = filter.OfficialRatings,
 0199            StudioIds = filter.StudioIds,
 0200            GenreIds = filter.GenreIds,
 0201            Genres = filter.Genres,
 0202            Years = filter.Years,
 0203            NameContains = filter.NameContains,
 0204            SearchTerm = filter.SearchTerm,
 0205            ExcludeItemIds = filter.ExcludeItemIds
 0206        };
 207
 208        // Collapse rows that share a PresentationUniqueKey (e.g. alternate versions) by picking
 209        // the lowest Id per group. For MusicArtist, prefer the entity from a library the user
 210        // can actually access,since the same artist can have a folder in multiple libraries.
 211        // Keep as an IQueryable sub-select so paging is applied AFTER
 212        // ApplyOrder runs the caller's actual sort.
 0213        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter);
 0214        var isMusicArtist = returnType == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 0215        var representativeIds = isMusicArtist
 0216            ? masterQuery
 0217                .GroupBy(e => e.PresentationUniqueKey)
 0218                .Select(g => g
 0219                    .OrderBy(e => filter.TopParentIds.Contains(e.TopParentId ?? Guid.Empty) ? 0 : 1)
 0220                    .ThenBy(e => e.Id)
 0221                    .First().Id)
 0222            : masterQuery
 0223                .GroupBy(e => e.PresentationUniqueKey)
 0224                .Select(g => g.Min(e => e.Id));
 225
 0226        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 0227        if (filter.EnableTotalRecordCount)
 228        {
 0229            result.TotalRecordCount = representativeIds.Count();
 230        }
 231
 0232        var query = ApplyNavigations(
 0233                context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => representativeIds.Contains(e.Id)),
 0234                filter);
 235
 0236        query = ApplyOrder(query, filter, context);
 237
 0238        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 239        {
 0240            query = query.Skip(filter.StartIndex.Value);
 241        }
 242
 0243        if (filter.Limit.HasValue)
 244        {
 0245            query = query.Take(filter.Limit.Value);
 246        }
 247
 0248        result.StartIndex = filter.StartIndex ?? 0;
 0249        if (filter.IncludeItemTypes.Length > 0)
 250        {
 0251            var countsByCleanName = BuildItemCountsByCleanName(context, filter, itemValueTypes);
 0252            result.Items =
 0253            [
 0254                .. query
 0255                    .AsEnumerable()
 0256                    .Where(e => e is not null)
 0257                    .Select(e =>
 0258                    {
 0259                        var item = DeserializeBaseItem(e, filter.SkipDeserialization);
 0260                        countsByCleanName.TryGetValue(e.CleanName ?? string.Empty, out var itemCount);
 0261                        return (item, itemCount);
 0262                    })
 0263                    .Where(x => x.item is not null)
 0264                    .Select(x => (x.item!, x.itemCount))
 0265            ];
 266        }
 267        else
 268        {
 0269            result.Items =
 0270            [
 0271                .. query
 0272                    .AsEnumerable()
 0273                    .Where(e => e != null)
 0274                    .Select(e => DeserializeBaseItem(e, filter.SkipDeserialization))
 0275                    .Where(item => item != null)
 0276                    .Select(item => (item!, (ItemCounts?)null))
 0277            ];
 278        }
 279
 0280        return result;
 0281    }
 282
 283    private Dictionary<string, ItemCounts> BuildItemCountsByCleanName(
 284        Database.Implementations.JellyfinDbContext context,
 285        InternalItemsQuery filter,
 286        IReadOnlyList<ItemValueType> itemValueTypes)
 287    {
 0288        var typeSubQuery = new InternalItemsQuery(filter.User)
 0289        {
 0290            ExcludeItemTypes = filter.ExcludeItemTypes,
 0291            IncludeItemTypes = filter.IncludeItemTypes,
 0292            MediaTypes = filter.MediaTypes,
 0293            AncestorIds = filter.AncestorIds,
 0294            ExcludeItemIds = filter.ExcludeItemIds,
 0295            ItemIds = filter.ItemIds,
 0296            TopParentIds = filter.TopParentIds,
 0297            ParentId = filter.ParentId,
 0298            IsPlayed = filter.IsPlayed
 0299        };
 300
 0301        var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 0302            .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 303
 0304        var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 0305        var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 0306        var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 0307        var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 0308        var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 0309        var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 0310        var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 0311        var itemIds = itemCountQuery.Select(e => e.Id);
 312
 313        // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite
 314        // Instead, start from ItemValueMaps and join with BaseItems
 0315        return context.ItemValuesMap
 0316            .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
 0317            .Where(ivm => itemIds.Contains(ivm.ItemId))
 0318            .Join(
 0319                context.BaseItems,
 0320                ivm => ivm.ItemId,
 0321                e => e.Id,
 0322                (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type })
 0323            .GroupBy(x => new { x.CleanName, x.Type })
 0324            .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() })
 0325            .GroupBy(x => x.CleanName)
 0326            .ToDictionary(
 0327                g => g.Key,
 0328                g => new ItemCounts
 0329                {
 0330                    SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count),
 0331                    EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count),
 0332                    MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count),
 0333                    AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count),
 0334                    ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count),
 0335                    SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count),
 0336                    TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count),
 0337                });
 338    }
 339}

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using Jellyfin.Data.Enums;
 5using Jellyfin.Database.Implementations;
 6using Jellyfin.Database.Implementations.Entities;
 7using Jellyfin.Extensions;
 8using MediaBrowser.Controller;
 9using MediaBrowser.Controller.Channels;
 10using MediaBrowser.Controller.Configuration;
 11using MediaBrowser.Controller.Entities;
 12using MediaBrowser.Controller.Persistence;
 13using Microsoft.EntityFrameworkCore;
 14using Microsoft.Extensions.Logging;
 15using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 16using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 17
 18namespace Jellyfin.Server.Implementations.Item;
 19
 20/*
 21    All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null
 22    This is done as the code isn't actually executed client side, but only the expressions are interpret and the compile
 23    This is your only warning/message regarding this topic.
 24*/
 25
 26/// <summary>
 27/// Handles all storage logic for BaseItems.
 28/// </summary>
 29public sealed partial class BaseItemRepository
 30    : IItemRepository, IItemQueryHelpers
 31{
 32    /// <summary>
 33    /// Gets the placeholder id for UserData detached items.
 34    /// </summary>
 135    public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
 36
 37    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 38    private readonly IServerApplicationHost _appHost;
 39    private readonly IItemTypeLookup _itemTypeLookup;
 40    private readonly IServerConfigurationManager _serverConfigurationManager;
 41    private readonly ILogger<BaseItemRepository> _logger;
 42
 143    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 144    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 145    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 146    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 147    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 48
 49    /// <summary>
 50    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
 51    /// </summary>
 52    /// <param name="dbProvider">The db factory.</param>
 53    /// <param name="appHost">The Application host.</param>
 54    /// <param name="itemTypeLookup">The static type lookup.</param>
 55    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
 56    /// <param name="logger">System logger.</param>
 57    public BaseItemRepository(
 58        IDbContextFactory<JellyfinDbContext> dbProvider,
 59        IServerApplicationHost appHost,
 60        IItemTypeLookup itemTypeLookup,
 61        IServerConfigurationManager serverConfigurationManager,
 62        ILogger<BaseItemRepository> logger)
 63    {
 3464        _dbProvider = dbProvider;
 3465        _appHost = appHost;
 3466        _itemTypeLookup = itemTypeLookup;
 3467        _serverConfigurationManager = serverConfigurationManager;
 3468        _logger = logger;
 3469    }
 70
 71    /// <summary>
 72    /// Maps a Entity to the DTO. Delegates to <see cref="BaseItemMapper"/>.
 73    /// </summary>
 74    /// <param name="entity">The database entity.</param>
 75    /// <param name="dto">The target DTO.</param>
 76    /// <param name="appHost">The application host.</param>
 77    /// <param name="logger">The logger.</param>
 78    /// <returns>The mapped DTO.</returns>
 79    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 80    {
 081        return BaseItemMapper.Map(entity, dto, appHost);
 82    }
 83
 84    /// <summary>
 85    /// Maps a DTO to a database entity. Delegates to <see cref="BaseItemMapper"/>.
 86    /// </summary>
 87    /// <param name="dto">The DTO to map.</param>
 88    /// <returns>The mapped database entity.</returns>
 89    public BaseItemEntity Map(BaseItemDto dto)
 90    {
 1091        return BaseItemMapper.Map(dto, _appHost);
 92    }
 93
 94    /// <summary>
 95    /// Deserializes a BaseItemEntity and sets all properties.
 96    /// </summary>
 97    /// <param name="baseItemEntity">The entity to deserialize.</param>
 98    /// <param name="logger">The logger.</param>
 99    /// <param name="appHost">The application host.</param>
 100    /// <param name="skipDeserialization">Whether to skip JSON deserialization.</param>
 101    /// <returns>The deserialized item, or null.</returns>
 102    public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost
 103    {
 3104        return BaseItemMapper.DeserializeBaseItem(baseItemEntity, logger, appHost, skipDeserialization);
 105    }
 106
 107    /// <inheritdoc />
 108    public void PrepareFilterQuery(InternalItemsQuery query)
 109    {
 493110        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 111        {
 0112            query.Limit = query.Limit.Value + 4;
 113        }
 114
 493115        if (query.IsResumable ?? false)
 116        {
 1117            query.IsVirtualItem = false;
 118        }
 493119    }
 120
 121    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 122    {
 15123        var list = new List<string>();
 124
 15125        if (IsTypeInQuery(BaseItemKind.Person, query))
 126        {
 1127            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 128        }
 129
 15130        if (IsTypeInQuery(BaseItemKind.Genre, query))
 131        {
 1132            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 133        }
 134
 15135        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 136        {
 1137            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 138        }
 139
 15140        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 141        {
 1142            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 143        }
 144
 15145        if (IsTypeInQuery(BaseItemKind.Studio, query))
 146        {
 1147            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 148        }
 149
 15150        return list;
 151    }
 152
 153    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 154    {
 75155        if (query.ExcludeItemTypes.Contains(type))
 156        {
 0157            return false;
 158        }
 159
 75160        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 161    }
 162
 163    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 164    {
 493165        if (!query.GroupByPresentationUniqueKey)
 166        {
 149167            return false;
 168        }
 169
 170        // Resume queries surface the actually-played version (which may be an alternate sharing the
 171        // primary's presentation key). The resumable filter already keeps one version per group, so
 172        // presentation-key grouping must not collapse the surfaced version back onto the primary.
 344173        if (query.IsResumable == true)
 174        {
 1175            return false;
 176        }
 177
 343178        if (query.GroupBySeriesPresentationUniqueKey)
 179        {
 0180            return false;
 181        }
 182
 343183        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 184        {
 0185            return false;
 186        }
 187
 343188        if (query.User is null)
 189        {
 340190            return false;
 191        }
 192
 3193        if (query.IncludeItemTypes.Length == 0)
 194        {
 0195            return true;
 196        }
 197
 3198        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 3199            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 3200            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 3201            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 3202            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 3203            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 204    }
 205
 206    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 207    {
 0208        return BaseItemMapper.MapImageToEntity(baseItemId, e);
 209    }
 210
 211    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 212    {
 0213        return BaseItemMapper.MapImageFromEntity(e, appHost);
 214    }
 215
 216    private string? GetPathToSave(string path)
 217    {
 0218        if (path is null)
 219        {
 0220            return null;
 221        }
 222
 0223        return _appHost.ReverseVirtualPath(path);
 224    }
 225
 226    /// <inheritdoc />
 227    public BaseItemDto? DeserializeBaseItem(BaseItemEntity entity, bool skipDeserialization = false)
 228    {
 77229        ArgumentNullException.ThrowIfNull(entity, nameof(entity));
 77230        if (_serverConfigurationManager?.Configuration is null)
 231        {
 0232            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 233        }
 234
 77235        var typeToSerialise = BaseItemMapper.GetType(entity.Type);
 77236        return BaseItemMapper.DeserializeBaseItem(
 77237            entity,
 77238            _logger,
 77239            _appHost,
 77240            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 241    }
 242}

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2#pragma warning disable CA1304 // Specify CultureInfo
 3#pragma warning disable CA1311 // Specify a culture or use an invariant version
 4#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 5
 6using System;
 7using System.Collections.Generic;
 8using System.Linq;
 9using System.Linq.Expressions;
 10using Jellyfin.Data.Enums;
 11using Jellyfin.Database.Implementations;
 12using Jellyfin.Database.Implementations.Entities;
 13using Jellyfin.Database.Implementations.Enums;
 14using Jellyfin.Extensions;
 15using MediaBrowser.Controller.Entities;
 16using MediaBrowser.Model.Entities;
 17using MediaBrowser.Model.Querying;
 18using Microsoft.EntityFrameworkCore;
 19using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 20
 21namespace Jellyfin.Server.Implementations.Item;
 22
 23public sealed partial class BaseItemRepository
 24{
 25    /// <inheritdoc />
 26    public IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 27    {
 49328        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 49329        dbQuery = dbQuery.AsSingleQuery();
 30
 49331        return dbQuery;
 32    }
 33
 34    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 35    {
 9036        dbQuery = TranslateQuery(dbQuery, context, filter);
 9037        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 9038        dbQuery = ApplyQueryPaging(dbQuery, filter);
 9039        dbQuery = ApplyNavigations(dbQuery, filter);
 9040        return dbQuery;
 41    }
 42
 43    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 44    {
 49345        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 46        {
 11247            var offset = filter.StartIndex ?? 0;
 48
 11249            if (offset > 0)
 50            {
 051                dbQuery = dbQuery.Skip(offset);
 52            }
 53
 11254            if (filter.Limit.HasValue)
 55            {
 11256                dbQuery = dbQuery.Take(filter.Limit.Value);
 57            }
 58        }
 59
 49360        return dbQuery;
 61    }
 62
 63    private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery
 64    {
 65        // Collapse duplicates sharing a presentation key (e.g. alternate versions), preferring the
 66        // primary version (PrimaryVersionId is null) so detail pages and actions target it instead
 67        // of an arbitrary alternate. Keep the grouped ids as an IQueryable sub-select; materializing
 68        // to a List would inline one bound parameter per id and hit SQLite's variable cap.
 49369        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 49370        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 71        {
 072            var groupedIds = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey })
 073                .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
 074            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 75        }
 49376        else if (enableGroupByPresentationUniqueKey)
 77        {
 278            var groupedIds = dbQuery.GroupBy(e => e.PresentationUniqueKey)
 279                .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
 280            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 81        }
 49182        else if (filter.GroupBySeriesPresentationUniqueKey)
 83        {
 084            var groupedIds = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.Min(x => x.Id));
 085            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 86        }
 87        else
 88        {
 49189            dbQuery = dbQuery.Distinct();
 90        }
 91
 49392        if (filter.CollapseBoxSetItems == true)
 93        {
 094            dbQuery = ApplyBoxSetCollapsing(context, dbQuery, filter.CollapseBoxSetItemTypes);
 95
 96            // Name filters run after collapse so BoxSets match by their own name, not a child's.
 097            dbQuery = ApplyNameFilters(dbQuery, filter);
 98        }
 99
 493100        dbQuery = ApplyOrder(dbQuery, filter, context);
 101
 493102        return dbQuery;
 103    }
 104
 105    private IQueryable<BaseItemEntity> ApplyBoxSetCollapsing(
 106        JellyfinDbContext context,
 107        IQueryable<BaseItemEntity> dbQuery,
 108        BaseItemKind[] collapsibleTypes)
 109    {
 0110        var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
 111
 0112        var currentIds = dbQuery.Select(e => e.Id);
 113
 0114        if (collapsibleTypes.Length == 0)
 115        {
 116            // Collapse all item types into box sets
 0117            return ApplyBoxSetCollapsingAll(context, currentIds, boxSetTypeName);
 118        }
 119
 120        // Only collapse specific item types, keep others untouched
 0121        var collapsibleTypeNames = collapsibleTypes.Select(t => _itemTypeLookup.BaseItemKindNames[t]).ToList();
 122
 123        // Categorize items in currentIds in a single pass to avoid multiple correlated EXISTS over BaseItems.
 0124        var categorized = context.BaseItems
 0125            .AsNoTracking()
 0126            .Where(bi => currentIds.Contains(bi.Id))
 0127            .Select(bi => new
 0128            {
 0129                bi.Id,
 0130                IsCollapsible = collapsibleTypeNames.Contains(bi.Type),
 0131                IsBoxSet = bi.Type == boxSetTypeName
 0132            });
 133
 0134        var collapsibleChildIds = categorized.Where(c => c.IsCollapsible).Select(c => c.Id);
 135
 136        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0137        var manualBoxSetLinks = context.LinkedChildren
 0138            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0139                && currentIds.Contains(lc.ChildId))
 0140            .Join(
 0141                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0142                lc => lc.ParentId,
 0143                bs => bs.Id,
 0144                (lc, bs) => new { lc.ChildId, lc.ParentId });
 145
 0146        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 147
 148        // Items whose type is NOT collapsible (always kept in results)
 0149        var nonCollapsibleIds = categorized.Where(c => !c.IsCollapsible).Select(c => c.Id);
 150
 151        // Collapsible items that are not a BoxSet themselves and not a manual child of any BoxSet
 0152        var collapsibleNotInBoxSet = categorized
 0153            .Where(c => c.IsCollapsible && !c.IsBoxSet)
 0154            .Select(c => c.Id)
 0155            .Where(id => !childrenInBoxSet.Contains(id));
 156
 157        // BoxSet IDs containing at least one collapsible child item from currentIds
 0158        var boxSetIds = manualBoxSetLinks
 0159            .Where(x => collapsibleChildIds.Contains(x.ChildId))
 0160            .Select(x => x.ParentId)
 0161            .Distinct();
 162
 0163        var collapsedIds = nonCollapsibleIds.Union(collapsibleNotInBoxSet).Union(boxSetIds);
 0164        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 165    }
 166
 167    private static IQueryable<BaseItemEntity> ApplyBoxSetCollapsingAll(
 168        JellyfinDbContext context,
 169        IQueryable<Guid> currentIds,
 170        string boxSetTypeName)
 171    {
 172        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0173        var manualBoxSetLinks = context.LinkedChildren
 0174            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0175                && currentIds.Contains(lc.ChildId))
 0176            .Join(
 0177                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0178                lc => lc.ParentId,
 0179                bs => bs.Id,
 0180                (lc, bs) => new { lc.ChildId, lc.ParentId });
 181
 0182        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 0183        var boxSetIds = manualBoxSetLinks.Select(x => x.ParentId).Distinct();
 184
 185        // Items in currentIds that are not BoxSets themselves and not a manual child of any BoxSet
 0186        var notInBoxSet = context.BaseItems
 0187            .AsNoTracking()
 0188            .Where(e => currentIds.Contains(e.Id) && e.Type != boxSetTypeName)
 0189            .Select(e => e.Id)
 0190            .Where(id => !childrenInBoxSet.Contains(id));
 191
 0192        var collapsedIds = notInBoxSet.Union(boxSetIds);
 0193        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 194    }
 195
 196    private static IQueryable<BaseItemEntity> ApplyNameFilters(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 197    {
 493198        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 199        {
 0200            var nameStartsWithLower = filter.NameStartsWith.ToLowerInvariant();
 0201            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().StartsWith(nameStartsWithLower));
 202        }
 203
 493204        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 205        {
 0206            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 0207            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(startsOrGreaterLower) >= 0);
 208        }
 209
 493210        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 211        {
 0212            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 0213            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(lessThanLower) < 0);
 214        }
 215
 493216        return dbQuery;
 217    }
 218
 219    /// <inheritdoc />
 220    public IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 221    {
 425222        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 223        {
 0224            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 225        }
 226
 425227        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 228        {
 424229            dbQuery = dbQuery.Include(e => e.Provider);
 230        }
 231
 425232        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 233        {
 424234            dbQuery = dbQuery.Include(e => e.LockedFields);
 235        }
 236
 425237        if (filter.DtoOptions.EnableUserData)
 238        {
 425239            dbQuery = dbQuery.Include(e => e.UserData);
 240        }
 241
 425242        if (filter.DtoOptions.EnableImages)
 243        {
 425244            dbQuery = dbQuery.Include(e => e.Images);
 245        }
 246
 247        // Include LinkedChildEntities for container types and videos that use them
 248        // (BoxSet, Playlist, CollectionFolder for manual linking; Video, Movie for alternate versions).
 249        // When IncludeItemTypes is empty (any type may be returned), always include them to ensure
 250        // LinkedChildren are loaded before items are saved back, preventing accidental deletion.
 425251        var linkedChildTypes = new[]
 425252        {
 425253            BaseItemKind.BoxSet,
 425254            BaseItemKind.Playlist,
 425255            BaseItemKind.CollectionFolder,
 425256            BaseItemKind.Video,
 425257            BaseItemKind.Movie
 425258        };
 425259        if (filter.IncludeItemTypes.Length == 0 || filter.IncludeItemTypes.Any(linkedChildTypes.Contains))
 260        {
 260261            dbQuery = dbQuery.Include(e => e.LinkedChildEntities);
 262        }
 263
 425264        if (filter.IncludeExtras)
 265        {
 0266            dbQuery = dbQuery.Include(e => e.Extras);
 267        }
 268
 425269        return dbQuery;
 270    }
 271
 272    /// <inheritdoc />
 273    public IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDb
 274    {
 493275        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 493276        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 277
 278        // SeriesDatePlayed requires special handling to avoid correlated subqueries.
 279        // Instead of running a MAX() subquery per-row in ORDER BY, we pre-aggregate
 280        // max played dates per series in one query and left-join it.
 493281        if (!hasSearch && orderBy.Any(o => o.OrderBy == ItemSortBy.SeriesDatePlayed))
 282        {
 0283            return ApplySeriesDatePlayedOrder(query, filter, context, orderBy);
 284        }
 285
 493286        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 287
 493288        if (hasSearch)
 289        {
 0290            var relevanceExpression = OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!);
 0291            orderedQuery = query.OrderBy(relevanceExpression);
 292        }
 293
 493294        if (orderBy.Length > 0)
 295        {
 119296            var firstOrdering = orderBy[0];
 119297            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 298
 119299            if (orderedQuery is null)
 300            {
 119301                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 119302                    ? query.OrderBy(expression)
 119303                    : query.OrderByDescending(expression);
 304            }
 305            else
 306            {
 0307                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 0308                    ? orderedQuery.ThenBy(expression)
 0309                    : orderedQuery.ThenByDescending(expression);
 310            }
 311
 119312            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 313            {
 6314                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 6315                    ? orderedQuery.ThenBy(e => e.Name)
 6316                    : orderedQuery.ThenByDescending(e => e.Name);
 317            }
 318
 326319            foreach (var item in orderBy.Skip(1))
 320            {
 44321                expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 44322                orderedQuery = item.SortOrder == SortOrder.Ascending
 44323                    ? orderedQuery.ThenBy(expression)
 44324                    : orderedQuery.ThenByDescending(expression);
 325            }
 326        }
 327
 493328        if (orderedQuery is null)
 329        {
 374330            return query.OrderBy(e => e.SortName);
 331        }
 332
 333        // Add SortName as final tiebreaker
 119334        if (!hasSearch && (orderBy.Length == 0 || orderBy.All(o => o.OrderBy is not ItemSortBy.SortName and not ItemSort
 335        {
 69336            orderedQuery = orderedQuery.ThenBy(e => e.SortName);
 337        }
 338
 119339        return orderedQuery;
 340    }
 341
 342    private IQueryable<BaseItemEntity> ApplySeriesDatePlayedOrder(
 343        IQueryable<BaseItemEntity> query,
 344        InternalItemsQuery filter,
 345        JellyfinDbContext context,
 346        (ItemSortBy OrderBy, SortOrder SortOrder)[] orderBy)
 347    {
 348        // Pre-aggregate max played date per series key in ONE query.
 349        // This generates a single: SELECT SeriesPresentationUniqueKey, MAX(LastPlayedDate) ... GROUP BY
 350        // instead of a correlated subquery per outer row.
 0351        IQueryable<UserData> userDataQuery = filter.User is not null
 0352            ? context.UserData.Where(ud => ud.UserId == filter.User.Id && ud.Played)
 0353            : context.UserData.Where(ud => ud.Played);
 354
 0355        var seriesMaxDates = userDataQuery
 0356            .Join(
 0357                context.BaseItems,
 0358                ud => ud.ItemId,
 0359                bi => bi.Id,
 0360                (ud, bi) => new { bi.SeriesPresentationUniqueKey, ud.LastPlayedDate })
 0361            .Where(x => x.SeriesPresentationUniqueKey != null)
 0362            .GroupBy(x => x.SeriesPresentationUniqueKey)
 0363            .Select(g => new { SeriesKey = g.Key!, MaxDate = g.Max(x => x.LastPlayedDate) });
 364
 0365        var joined = query.LeftJoin(
 0366            seriesMaxDates,
 0367            e => e.PresentationUniqueKey,
 0368            s => s.SeriesKey,
 0369            (e, s) => new { Item = e, MaxDate = s != null ? s.MaxDate : (DateTime?)null });
 370
 0371        var seriesSort = orderBy.First(o => o.OrderBy == ItemSortBy.SeriesDatePlayed);
 372
 0373        return seriesSort.SortOrder == SortOrder.Ascending
 0374            ? joined.OrderBy(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item)
 0375            : joined.OrderByDescending(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item);
 376    }
 377
 378    /// <summary>
 379    /// Builds a query for descendants of an ancestor with user access filtering applied.
 380    /// Uses recursive CTE to traverse both hierarchical (AncestorIds) and linked (LinkedChildren) relationships.
 381    /// </summary>
 382    /// <inheritdoc />
 383    public IQueryable<BaseItemEntity> BuildAccessFilteredDescendantsQuery(
 384        JellyfinDbContext context,
 385        InternalItemsQuery filter,
 386        Guid ancestorId)
 387    {
 388        // Use recursive CTE to get all descendants (hierarchical and linked)
 0389        var allDescendantIds = DescendantQueryHelper.GetAllDescendantIds(context, ancestorId);
 390
 0391        var baseQuery = context.BaseItems
 0392            .AsNoTracking()
 0393            .Where(b => allDescendantIds.Contains(b.Id) && !b.IsFolder && !b.IsVirtualItem);
 394
 0395        return ApplyAccessFiltering(context, baseQuery, filter);
 396    }
 397
 398    /// <summary>
 399    /// Applies user access filtering to a query.
 400    /// Includes TopParentIds, parental rating, and tag filtering.
 401    /// </summary>
 402    /// <inheritdoc />
 403    public IQueryable<BaseItemEntity> ApplyAccessFiltering(
 404        JellyfinDbContext context,
 405        IQueryable<BaseItemEntity> baseQuery,
 406        InternalItemsQuery filter)
 407    {
 408        // Apply TopParentIds filtering (library folder access)
 0409        if (filter.TopParentIds.Length > 0)
 410        {
 0411            var topParentIds = filter.TopParentIds;
 0412            baseQuery = baseQuery.Where(e => topParentIds.Contains(e.TopParentId!.Value));
 413        }
 414
 415        // Apply parental rating filtering
 0416        if (filter.MaxParentalRating is not null)
 417        {
 0418            baseQuery = baseQuery.Where(BuildMaxParentalRatingFilter(context, filter.MaxParentalRating));
 419        }
 420
 421        // Apply block unrated items filtering
 0422        if (filter.BlockUnratedItems.Length > 0)
 423        {
 0424            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0425            baseQuery = baseQuery.Where(e =>
 0426                e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType));
 427        }
 428
 429        // Apply excluded tags filtering (blocked tags).
 430        // Pre-build the blocked-item-id set as a sub-select; then four index-seek Contains checks
 431        // instead of one EXISTS over a 4-way OR predicate that defeats index seeks.
 0432        if (filter.ExcludeInheritedTags.Length > 0)
 433        {
 0434            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0435            var blockedTagItemIds = context.ItemValuesMap
 0436                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 0437                .Select(f => f.ItemId);
 438
 0439            baseQuery = baseQuery.Where(e =>
 0440                !blockedTagItemIds.Contains(e.Id)
 0441                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 0442                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 0443                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 444        }
 445
 446        // Apply included tags filtering (allowed tags - item must have at least one).
 0447        if (filter.IncludeInheritedTags.Length > 0)
 448        {
 0449            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0450            var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 0451            var allowedTagItemIds = context.ItemValuesMap
 0452                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 0453                .Select(f => f.ItemId);
 454
 0455            baseQuery = baseQuery.Where(e =>
 0456                allowedTagItemIds.Contains(e.Id)
 0457                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 0458                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 0459                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 0460
 0461                // People don't carry the tags of the media they appear in and would never match
 0462                || e.Type == personTypeName);
 463        }
 464
 465        // Exclude alternate versions (have PrimaryVersionId set) and owned non-extra items.
 466        // Extras (trailers, etc.) have OwnerId set but also have ExtraType set — keep those.
 0467        if (!filter.IncludeOwnedItems)
 468        {
 0469            baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 470        }
 471
 0472        return baseQuery;
 473    }
 474
 475    /// <summary>
 476    /// Builds a filter expression for max parental rating that handles both rated items
 477    /// and unrated BoxSets/Playlists (which check linked children's ratings).
 478    /// </summary>
 479    private static Expression<Func<BaseItemEntity, bool>> BuildMaxParentalRatingFilter(
 480        JellyfinDbContext context,
 481        ParentalRatingScore maxRating)
 482    {
 54483        var maxScore = maxRating.Score;
 54484        var maxSubScore = maxRating.SubScore ?? 0;
 54485        var linkedChildren = context.LinkedChildren;
 486
 54487        return e =>
 54488            // Item has a rating: check against limit
 54489            (e.InheritedParentalRatingValue != null
 54490                && (e.InheritedParentalRatingValue < maxScore
 54491                    || (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSub
 54492            // Item has no rating
 54493            || (e.InheritedParentalRatingValue == null
 54494                && (
 54495                    // No linked children (not a BoxSet/Playlist): pass as unrated
 54496                    !linkedChildren.Any(lc => lc.ParentId == e.Id)
 54497                    // Has linked children: at least one child must be within limits
 54498                    || linkedChildren.Any(lc => lc.ParentId == e.Id
 54499                        && (lc.Child!.InheritedParentalRatingValue == null
 54500                            || lc.Child.InheritedParentalRatingValue < maxScore
 54501                            || (lc.Child.InheritedParentalRatingValue == maxScore
 54502                                && (lc.Child.InheritedParentalRatingSubValue ?? 0) <= maxSubScore)))));
 503    }
 504
 505    /// <inheritdoc />
 506    public IQueryable<Guid> GetFullyPlayedFolderIdsQuery(JellyfinDbContext context, IQueryable<Guid> folderIds, User use
 507    {
 0508        ArgumentNullException.ThrowIfNull(context);
 0509        ArgumentNullException.ThrowIfNull(folderIds);
 0510        ArgumentNullException.ThrowIfNull(user);
 511
 0512        var filter = new InternalItemsQuery(user);
 0513        var userId = user.Id;
 514
 0515        var leafItems = context.BaseItems
 0516            .AsNoTracking()
 0517            .Where(b => !b.IsFolder && !b.IsVirtualItem);
 0518        leafItems = ApplyAccessFiltering(context, leafItems, filter);
 519
 0520        var playedLeafItems = leafItems
 0521            .Select(b => new { b.Id, Played = b.UserData!.Any(ud => ud.UserId == userId && ud.Played) });
 522
 0523        var ancestorLeaves = context.AncestorIds
 0524            .Where(a => folderIds.Contains(a.ParentItemId))
 0525            .Join(
 0526                playedLeafItems,
 0527                a => a.ItemId,
 0528                b => b.Id,
 0529                (a, b) => new { FolderId = a.ParentItemId, b.Id, b.Played });
 530
 0531        var linkedLeaves = context.LinkedChildren
 0532            .Where(lc => folderIds.Contains(lc.ParentId))
 0533            .Join(
 0534                playedLeafItems,
 0535                lc => lc.ChildId,
 0536                b => b.Id,
 0537                (lc, b) => new { FolderId = lc.ParentId, b.Id, b.Played });
 538
 0539        var linkedFolderLeaves = context.LinkedChildren
 0540            .Where(lc => folderIds.Contains(lc.ParentId))
 0541            .Join(
 0542                context.BaseItems.Where(b => b.IsFolder),
 0543                lc => lc.ChildId,
 0544                b => b.Id,
 0545                (lc, b) => new { lc.ParentId, FolderChildId = b.Id })
 0546            .Join(
 0547                context.AncestorIds,
 0548                x => x.FolderChildId,
 0549                a => a.ParentItemId,
 0550                (x, a) => new { x.ParentId, DescendantId = a.ItemId })
 0551            .Join(
 0552                playedLeafItems,
 0553                x => x.DescendantId,
 0554                b => b.Id,
 0555                (x, b) => new { FolderId = x.ParentId, b.Id, b.Played });
 556
 0557        return ancestorLeaves
 0558            .Union(linkedLeaves)
 0559            .Union(linkedFolderLeaves)
 0560            .GroupBy(x => x.FolderId)
 0561            .Where(g => g.Select(x => x.Id).Distinct().Count() == g.Where(x => x.Played).Select(x => x.Id).Distinct().Co
 0562            .Select(g => g.Key);
 563    }
 564}

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

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

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2#pragma warning disable CA1304 // Specify CultureInfo
 3#pragma warning disable CA1309 // Use ordinal string comparison
 4#pragma warning disable CA1311 // Specify a culture or use an invariant version
 5#pragma warning disable CA1307 // Specify StringComparison for clarity
 6#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 7
 8using System;
 9using System.Collections.Generic;
 10using System.Globalization;
 11using System.Linq;
 12using System.Linq.Expressions;
 13using Jellyfin.Data.Enums;
 14using Jellyfin.Database.Implementations;
 15using Jellyfin.Database.Implementations.Entities;
 16using Jellyfin.Database.Implementations.MatchCriteria;
 17using Jellyfin.Extensions;
 18using Jellyfin.Server.Implementations.Extensions;
 19using MediaBrowser.Controller.Entities;
 20using MediaBrowser.Model.Entities;
 21using Microsoft.EntityFrameworkCore;
 22using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 23
 24namespace Jellyfin.Server.Implementations.Item;
 25
 26public sealed partial class BaseItemRepository
 27{
 128    private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
 29
 130    private static readonly string ImdbProviderName = MetadataProvider.Imdb.ToString().ToLowerInvariant();
 131    private static readonly string TmdbProviderName = MetadataProvider.Tmdb.ToString().ToLowerInvariant();
 132    private static readonly string TvdbProviderName = MetadataProvider.Tvdb.ToString().ToLowerInvariant();
 33
 34    /// <inheritdoc />
 35    public IQueryable<BaseItemEntity> TranslateQuery(
 36        IQueryable<BaseItemEntity> baseQuery,
 37        JellyfinDbContext context,
 38        InternalItemsQuery filter)
 39    {
 40        const int HDWidth = 1200;
 41        const int UHDWidth = 3800;
 42        const int UHDHeight = 2100;
 43
 49344        var minWidth = filter.MinWidth;
 49345        var maxWidth = filter.MaxWidth;
 49346        var now = DateTime.UtcNow;
 47
 49348        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 49        {
 050            bool includeSD = false;
 051            bool includeHD = false;
 052            bool include4K = false;
 53
 054            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 55            {
 056                includeSD = true;
 57            }
 58
 059            if (filter.IsHD.HasValue && filter.IsHD.Value)
 60            {
 061                includeHD = true;
 62            }
 63
 064            if (filter.Is4K.HasValue && filter.Is4K.Value)
 65            {
 066                include4K = true;
 67            }
 68
 69            // Non-folders: check own resolution directly (no subquery).
 70            // Folders (Series, BoxSets): EXISTS check on descendants/linked children.
 71            // Using navigation properties (a.Item, lc.Child) produces efficient
 72            // EXISTS + JOIN instead of nested IN (SELECT ...) subqueries.
 073            baseQuery = baseQuery.Where(e =>
 074                (!e.IsFolder && e.Width > 0
 075                    && ((includeSD && e.Width < HDWidth)
 076                        || (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight))
 077                        || (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight))))
 078                || (e.IsFolder
 079                    && (e.Children!.Any(a =>
 080                            a.Item.Width > 0
 081                            && ((includeSD && a.Item.Width < HDWidth)
 082                                || (includeHD && a.Item.Width >= HDWidth && !(a.Item.Width >= UHDWidth || a.Item.Height 
 083                                || (include4K && (a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight))))
 084                        || context.LinkedChildren.Any(lc =>
 085                            lc.ParentId == e.Id
 086                            && lc.Child!.Width > 0
 087                            && ((includeSD && lc.Child.Width < HDWidth)
 088                                || (includeHD && lc.Child.Width >= HDWidth && !(lc.Child.Width >= UHDWidth || lc.Child.H
 089                                || (include4K && (lc.Child.Width >= UHDWidth || lc.Child.Height >= UHDHeight)))))));
 90        }
 91
 49392        if (minWidth.HasValue)
 93        {
 094            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 95        }
 96
 49397        if (filter.MinHeight.HasValue)
 98        {
 099            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 100        }
 101
 493102        if (maxWidth.HasValue)
 103        {
 0104            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 105        }
 106
 493107        if (filter.MaxHeight.HasValue)
 108        {
 0109            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 110        }
 111
 493112        if (filter.IsLocked.HasValue)
 113        {
 54114            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 115        }
 116
 493117        var tags = filter.Tags.ToList();
 493118        var excludeTags = filter.ExcludeTags.ToList();
 119
 493120        if (filter.IsMovie.HasValue)
 121        {
 0122            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 0123                && (filter.IncludeItemTypes.Length == 0
 0124                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 0125                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 126
 0127            if (!shouldIncludeAllMovieTypes)
 128            {
 0129                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 130            }
 131        }
 132
 493133        if (filter.IsSeries.HasValue)
 134        {
 0135            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 136        }
 137
 493138        if (filter.IsSports.HasValue)
 139        {
 0140            if (filter.IsSports.Value)
 141            {
 0142                tags.Add("Sports");
 143            }
 144            else
 145            {
 0146                excludeTags.Add("Sports");
 147            }
 148        }
 149
 493150        if (filter.IsNews.HasValue)
 151        {
 0152            if (filter.IsNews.Value)
 153            {
 0154                tags.Add("News");
 155            }
 156            else
 157            {
 0158                excludeTags.Add("News");
 159            }
 160        }
 161
 493162        if (filter.IsKids.HasValue)
 163        {
 0164            if (filter.IsKids.Value)
 165            {
 0166                tags.Add("Kids");
 167            }
 168            else
 169            {
 0170                excludeTags.Add("Kids");
 171            }
 172        }
 173
 493174        if (!string.IsNullOrEmpty(filter.SearchTerm))
 175        {
 0176            var cleanedSearchTerm = filter.SearchTerm.GetCleanValue();
 0177            var originalSearchTerm = filter.SearchTerm;
 0178            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 179            {
 0180                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 0181                var likeSearchTerm = $"%{originalSearchTerm.Trim('%')}%";
 0182                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 183            }
 184            else
 185            {
 0186                var likeSearchTerm = $"%{originalSearchTerm}%";
 0187                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 188            }
 189        }
 190
 493191        if (filter.IsFolder.HasValue)
 192        {
 22193            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 194        }
 195
 493196        var includeTypes = filter.IncludeItemTypes;
 197
 198        // Only specify excluded types if no included types are specified
 493199        if (filter.IncludeItemTypes.Length == 0)
 200        {
 240201            var excludeTypes = filter.ExcludeItemTypes;
 240202            if (excludeTypes.Length == 1)
 203            {
 0204                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 205                {
 0206                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 207                }
 208            }
 240209            else if (excludeTypes.Length > 1)
 210            {
 0211                var excludeTypeName = new List<string>();
 0212                foreach (var excludeType in excludeTypes)
 213                {
 0214                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 215                    {
 0216                        excludeTypeName.Add(baseItemKindName!);
 217                    }
 218                }
 219
 0220                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 221            }
 222        }
 223        else
 224        {
 253225            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 253226            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 227        }
 228
 493229        if (filter.ChannelIds.Count > 0)
 230        {
 0231            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 232        }
 233
 493234        if (!filter.ParentId.IsEmpty())
 235        {
 149236            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 237        }
 238
 493239        if (!string.IsNullOrWhiteSpace(filter.Path))
 240        {
 0241            var pathToQuery = GetPathToSave(filter.Path);
 0242            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 243        }
 244
 493245        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 246        {
 0247            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 248        }
 249
 493250        if (filter.MinCommunityRating.HasValue)
 251        {
 0252            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 253        }
 254
 493255        if (filter.MinIndexNumber.HasValue)
 256        {
 0257            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 258        }
 259
 493260        if (filter.MinParentAndIndexNumber.HasValue)
 261        {
 0262            baseQuery = baseQuery
 0263                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 264        }
 265
 493266        if (filter.MinDateCreated.HasValue)
 267        {
 0268            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 269        }
 270
 493271        if (filter.MinDateLastSaved.HasValue)
 272        {
 0273            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 274        }
 275
 493276        if (filter.MinDateLastSavedForUser.HasValue)
 277        {
 0278            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 279        }
 280
 493281        if (filter.IndexNumber.HasValue)
 282        {
 0283            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 284        }
 285
 493286        if (filter.ParentIndexNumber.HasValue)
 287        {
 0288            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 289        }
 290
 493291        if (filter.ParentIndexNumberNotEquals.HasValue)
 292        {
 0293            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 294        }
 295
 493296        var minEndDate = filter.MinEndDate;
 493297        var maxEndDate = filter.MaxEndDate;
 298
 493299        if (filter.HasAired.HasValue)
 300        {
 0301            if (filter.HasAired.Value)
 302            {
 0303                maxEndDate = DateTime.UtcNow;
 304            }
 305            else
 306            {
 0307                minEndDate = DateTime.UtcNow;
 308            }
 309        }
 310
 493311        if (minEndDate.HasValue)
 312        {
 0313            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 314        }
 315
 493316        if (maxEndDate.HasValue)
 317        {
 0318            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 319        }
 320
 493321        if (filter.MinStartDate.HasValue)
 322        {
 0323            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 324        }
 325
 493326        if (filter.MaxStartDate.HasValue)
 327        {
 0328            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 329        }
 330
 493331        if (filter.MinPremiereDate.HasValue)
 332        {
 0333            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 334        }
 335
 493336        if (filter.MaxPremiereDate.HasValue)
 337        {
 0338            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 339        }
 340
 493341        if (filter.TrailerTypes.Length > 0)
 342        {
 0343            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 0344            baseQuery = baseQuery.Where(e => e.TrailerTypes!.Any(w => trailerTypes.Contains(w.Id)));
 345        }
 346
 493347        if (filter.IsAiring.HasValue)
 348        {
 0349            if (filter.IsAiring.Value)
 350            {
 0351                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 352            }
 353            else
 354            {
 0355                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 356            }
 357        }
 358
 493359        if (filter.PersonIds.Length > 0)
 360        {
 0361            var peopleEntityIds = context.BaseItems
 0362                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 0363                .Join(
 0364                    context.Peoples,
 0365                    b => b.Name,
 0366                    p => p.Name,
 0367                    (b, p) => p.Id);
 368
 0369            baseQuery = baseQuery
 0370                .Where(e => context.PeopleBaseItemMap
 0371                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 372        }
 373
 493374        if (!string.IsNullOrWhiteSpace(filter.Person))
 375        {
 0376            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 377        }
 378
 493379        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 380        {
 0381            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 382        }
 383
 493384        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 385        {
 0386            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 387        }
 388
 493389        if (!string.IsNullOrWhiteSpace(filter.Name))
 390        {
 3391            if (filter.UseRawName == true)
 392            {
 0393                var nameLower = filter.Name.ToLowerInvariant();
 0394                baseQuery = baseQuery.Where(e => e.Name!.ToLower() == nameLower);
 395            }
 396            else
 397            {
 3398                var cleanName = filter.Name.GetCleanValue();
 3399                baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 400            }
 401        }
 402
 493403        var nameContains = filter.NameContains;
 493404        if (!string.IsNullOrWhiteSpace(nameContains))
 405        {
 0406            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 407            {
 0408                nameContains = $"%{nameContains.Trim('%')}%";
 0409                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 410            }
 411            else
 412            {
 0413                var likeNameContains = $"%{nameContains}%";
 0414                baseQuery = baseQuery.Where(e =>
 0415                                    e.CleanName!.Contains(nameContains)
 0416                                    || EF.Functions.Like(e.OriginalTitle, likeNameContains));
 417            }
 418        }
 419
 420        // When box set collapsing is active, defer name-range filters to after the collapse.
 421        // Otherwise, items are filtered by their own name but then collapsed into a BoxSet
 422        // whose name may fall in a different range (e.g. "21 Jump Street" is under "#"
 423        // but its BoxSet "Jump Street Collection" should appear under "J").
 493424        if (filter.CollapseBoxSetItems != true)
 425        {
 493426            baseQuery = ApplyNameFilters(baseQuery, filter);
 427        }
 428
 493429        if (filter.ImageTypes.Length > 0)
 430        {
 112431            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 112432            baseQuery = baseQuery.Where(e => e.Images!.Any(w => imgTypes.Contains(w.ImageType)));
 433        }
 434
 493435        if (filter.IsLiked.HasValue)
 436        {
 0437            var isLiked = filter.IsLiked.Value;
 0438            baseQuery = baseQuery.Where(e => e.UserData!.Any(ud => ud.UserId == filter.User!.Id && ud.Rating >= UserItem
 439        }
 440
 493441        if (filter.IsFavoriteOrLiked.HasValue)
 442        {
 0443            var isFavoriteOrLiked = filter.IsFavoriteOrLiked.Value;
 0444            baseQuery = baseQuery.Where(e => e.UserData!.Any(ud => ud.UserId == filter.User!.Id && ud.IsFavorite) == isF
 445        }
 446
 493447        if (filter.IsFavorite.HasValue)
 448        {
 0449            var isFavorite = filter.IsFavorite.Value;
 0450            baseQuery = baseQuery.Where(e => e.UserData!.Any(ud => ud.UserId == filter.User!.Id && ud.IsFavorite) == isF
 451        }
 452
 493453        if (filter.IsPlayed.HasValue)
 454        {
 0455            var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series);
 0456            var hasBoxSet = filter.IncludeItemTypes.Contains(BaseItemKind.BoxSet);
 457
 0458            if (hasSeries || hasBoxSet)
 459            {
 0460                var userId = filter.User!.Id;
 0461                var isPlayed = filter.IsPlayed.Value;
 0462                var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 0463                var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
 464
 465                // Series: played = at least one episode AND all episodes played; unplayed = otherwise.
 0466                IQueryable<Guid> playedSeriesIds = hasSeries
 0467                    ? context.BaseItems
 0468                        .AsNoTracking()
 0469                        .Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
 0470                        .GroupBy(e => e.SeriesId!.Value)
 0471                        .Where(g => !g.Any(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played)))
 0472                        .Select(g => g.Key)
 0473                    : Enumerable.Empty<Guid>().AsQueryable();
 474
 475                // BoxSet: played = all children played.
 0476                IQueryable<Guid> playedBoxSetIds = hasBoxSet
 0477                    ? GetFullyPlayedFolderIdsQuery(
 0478                        context,
 0479                        baseQuery.Where(e => e.Type == boxSetTypeName).Select(e => e.Id),
 0480                        filter.User!)
 0481                    : Enumerable.Empty<Guid>().AsQueryable();
 482
 483                // Non-folder items: check UserData directly
 0484                var playedItemIds = context.UserData
 0485                    .Where(ud => ud.UserId == userId && ud.Played)
 0486                    .Select(ud => ud.ItemId);
 487
 0488                if (isPlayed)
 489                {
 0490                    baseQuery = baseQuery.Where(e =>
 0491                        (e.Type == seriesTypeName && playedSeriesIds.Contains(e.Id))
 0492                        || (e.Type == boxSetTypeName && playedBoxSetIds.Contains(e.Id))
 0493                        || (e.Type != seriesTypeName && e.Type != boxSetTypeName && playedItemIds.Contains(e.Id)));
 494                }
 495                else
 496                {
 0497                    baseQuery = baseQuery.Where(e =>
 0498                        (e.Type == seriesTypeName && !playedSeriesIds.Contains(e.Id))
 0499                        || (e.Type == boxSetTypeName && !playedBoxSetIds.Contains(e.Id))
 0500                        || (e.Type != seriesTypeName && e.Type != boxSetTypeName && !playedItemIds.Contains(e.Id)));
 501                }
 502            }
 503            else
 504            {
 0505                var playedItemIds = context.UserData
 0506                    .Where(ud => ud.UserId == filter.User!.Id && ud.Played)
 0507                    .Select(ud => ud.ItemId);
 0508                var isPlayedItem = filter.IsPlayed.Value;
 0509                baseQuery = baseQuery.Where(e => playedItemIds.Contains(e.Id) == isPlayedItem);
 510            }
 511        }
 512
 493513        if (filter.IsResumable.HasValue)
 514        {
 1515            var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series);
 1516            var userId = filter.User!.Id;
 1517            var isResumable = filter.IsResumable.Value;
 1518            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 519
 520            // In-progress user data rows; alternate versions track their own progress.
 1521            var inProgress = context.UserData
 1522                .Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0);
 523
 1524            IQueryable<Guid>? resumableSeriesIds = null;
 1525            if (hasSeries)
 526            {
 527                // Aggregate per series in a single GROUP BY pass, instead of three full scans.
 0528                var seriesEpisodeStats = context.BaseItems
 0529                    .AsNoTracking()
 0530                    .Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
 0531                    .GroupBy(e => e.SeriesId!.Value)
 0532                    .Select(g => new
 0533                    {
 0534                        SeriesId = g.Key,
 0535                        HasInProgress = g.Any(e => e.UserData!.Any(ud => ud.UserId == userId && ud.PlaybackPositionTicks
 0536                        HasPlayed = g.Any(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played)),
 0537                        HasUnplayed = g.Any(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
 0538                    });
 539
 540                // A series is resumable if it has an in-progress episode,
 541                // or if it has both played and unplayed episodes (partially watched).
 0542                resumableSeriesIds = seriesEpisodeStats
 0543                    .Where(s => s.HasInProgress || (s.HasPlayed && s.HasUnplayed))
 0544                    .Select(s => s.SeriesId);
 545            }
 546
 1547            if (isResumable)
 548            {
 549                // Resume queries surface the version that was actually played, which may be an alternate.
 550                // Match each version on its own progress rather than coalescing onto the primary.
 1551                var inProgressIds = inProgress.Select(ud => ud.ItemId);
 552
 1553                baseQuery = hasSeries
 1554                    ? baseQuery.Where(e =>
 1555                        (e.Type == seriesTypeName && resumableSeriesIds!.Contains(e.Id))
 1556                        || (e.Type != seriesTypeName && inProgressIds.Contains(e.Id)))
 1557                    : baseQuery.Where(e => inProgressIds.Contains(e.Id));
 558
 559                // When several versions of the same item are in progress, keep only the most recently played one, use i
 560                // Only in-progress siblings can eliminate a candidate: a version without progress has a NULL max LastPl
 561                // which is never greater and never ties. Restricting the sibling scan to the in-progress set keeps this
 562                // the user's Continue Watching count instead of forcing a full BaseItems scan (COALESCE keys are non-in
 1563                baseQuery = baseQuery.Where(e => e.Type == seriesTypeName || !context.BaseItems
 1564                    .Where(s => s.Id != e.Id
 1565                        && inProgressIds.Contains(s.Id)
 1566                        && (s.PrimaryVersionId ?? s.Id) == (e.PrimaryVersionId ?? e.Id))
 1567                    .Any(s =>
 1568                        inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
 1569                            > inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
 1570                        || (inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
 1571                                == inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
 1572                            && s.Id.CompareTo(e.Id) < 0)));
 573            }
 574            else
 575            {
 576                // Not-resumable queries operate on primaries only.
 0577                var resumableMovieIds = inProgress
 0578                    .Join(context.BaseItems, ud => ud.ItemId, bi => bi.Id, (ud, bi) => bi.PrimaryVersionId ?? bi.Id);
 579
 0580                baseQuery = hasSeries
 0581                    ? baseQuery.Where(e =>
 0582                        (e.Type == seriesTypeName && !resumableSeriesIds!.Contains(e.Id))
 0583                        || (e.Type != seriesTypeName && !resumableMovieIds.Contains(e.Id)))
 0584                    : baseQuery.Where(e => !resumableMovieIds.Contains(e.Id));
 585            }
 586        }
 587
 493588        if (filter.ArtistIds.Length > 0)
 589        {
 0590            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 591        }
 592
 493593        if (filter.AlbumArtistIds.Length > 0)
 594        {
 0595            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 596        }
 597
 493598        if (filter.ContributingArtistIds.Length > 0)
 599        {
 0600            var contributingNames = context.BaseItems
 0601                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 0602                .Select(b => b.CleanName);
 603
 0604            baseQuery = baseQuery.Where(e =>
 0605                e.ItemValues!.Any(ivm =>
 0606                    ivm.ItemValue.Type == ItemValueType.Artist &&
 0607                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 0608                &&
 0609                !e.ItemValues!.Any(ivm =>
 0610                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 0611                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 612        }
 613
 493614        if (filter.AlbumIds.Length > 0)
 615        {
 0616            baseQuery = baseQuery.Where(e => e.ParentId.HasValue && filter.AlbumIds.Contains(e.ParentId.Value));
 617        }
 618
 493619        if (filter.ExcludeArtistIds.Length > 0)
 620        {
 0621            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 622        }
 623
 493624        if (filter.GenreIds.Count > 0)
 625        {
 0626            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 627        }
 628
 493629        if (filter.Genres.Count > 0)
 630        {
 0631            var cleanGenres = filter.Genres.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValu
 0632            baseQuery = baseQuery
 0633                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 634        }
 635
 493636        if (tags.Count > 0)
 637        {
 0638            var cleanValues = tags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, str
 0639            baseQuery = baseQuery
 0640                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 641        }
 642
 493643        if (excludeTags.Count > 0)
 644        {
 0645            var cleanValues = excludeTags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueM
 0646            baseQuery = baseQuery
 0647                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 648        }
 649
 493650        if (filter.StudioIds.Length > 0)
 651        {
 0652            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 653        }
 654
 493655        if (filter.OfficialRatings.Length > 0)
 656        {
 0657            var ratings = filter.OfficialRatings;
 0658            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, e => ratings.Contains(e.OfficialRating));
 659        }
 660
 493661        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 493662        if (filter.MinParentalRating != null)
 663        {
 0664            var min = filter.MinParentalRating;
 0665            var minScore = min.Score;
 0666            var minSubScore = min.SubScore ?? 0;
 667
 0668            minParentalRatingFilter = e =>
 0669                e.InheritedParentalRatingValue == null ||
 0670                e.InheritedParentalRatingValue > minScore ||
 0671                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 672        }
 673
 493674        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 493675        if (filter.MaxParentalRating != null)
 676        {
 54677            maxParentalRatingFilter = BuildMaxParentalRatingFilter(context, filter.MaxParentalRating);
 678        }
 679
 493680        if (filter.HasParentalRating ?? false)
 681        {
 0682            if (minParentalRatingFilter != null)
 683            {
 0684                baseQuery = baseQuery.Where(minParentalRatingFilter);
 685            }
 686
 0687            if (maxParentalRatingFilter != null)
 688            {
 0689                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 690            }
 691        }
 493692        else if (filter.BlockUnratedItems.Length > 0)
 693        {
 0694            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0695            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 696
 0697            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 698            {
 0699                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 700            }
 0701            else if (minParentalRatingFilter != null)
 702            {
 0703                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 704            }
 0705            else if (maxParentalRatingFilter != null)
 706            {
 0707                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 708            }
 709            else
 710            {
 0711                baseQuery = baseQuery.Where(unratedItemFilter);
 712            }
 713        }
 493714        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 715        {
 54716            if (minParentalRatingFilter != null)
 717            {
 0718                baseQuery = baseQuery.Where(minParentalRatingFilter);
 719            }
 720
 54721            if (maxParentalRatingFilter != null)
 722            {
 54723                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 724            }
 725        }
 439726        else if (!filter.HasParentalRating ?? false)
 727        {
 0728            baseQuery = baseQuery
 0729                .Where(e => e.InheritedParentalRatingValue == null);
 730        }
 731
 493732        if (filter.HasOfficialRating.HasValue)
 733        {
 0734            Expression<Func<BaseItemEntity, bool>> hasRating =
 0735                e => e.OfficialRating != null && e.OfficialRating != string.Empty;
 736
 0737            baseQuery = filter.HasOfficialRating.Value
 0738                ? baseQuery.WhereItemOrDescendantMatches(context, hasRating)
 0739                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasRating);
 740        }
 741
 493742        if (filter.HasOverview.HasValue)
 743        {
 0744            if (filter.HasOverview.Value)
 745            {
 0746                baseQuery = baseQuery
 0747                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 748            }
 749            else
 750            {
 0751                baseQuery = baseQuery
 0752                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 753            }
 754        }
 755
 493756        if (filter.HasOwnerId.HasValue)
 757        {
 0758            if (filter.HasOwnerId.Value)
 759            {
 0760                baseQuery = baseQuery
 0761                    .Where(e => e.OwnerId != null);
 762            }
 763            else
 764            {
 0765                baseQuery = baseQuery
 0766                    .Where(e => e.OwnerId == null);
 767            }
 768        }
 493769        else if (filter.OwnerIds.Length == 0 && filter.ExtraTypes.Length == 0 && !filter.IncludeOwnedItems)
 770        {
 771            // Exclude owned non-extra items from general queries.
 772            // Extras (trailers, etc.) have OwnerId set but also have ExtraType set - keep those.
 773            // Alternate versions (PrimaryVersionId set) are normally excluded too, but resume queries
 774            // keep them so the actually-played version can surface instead of collapsing onto the primary.
 462775            baseQuery = filter.IsResumable == true
 462776                ? baseQuery.Where(e => e.OwnerId == null || e.ExtraType != null)
 462777                : baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 778        }
 779
 493780        if (filter.OwnerIds.Length > 0)
 781        {
 6782            baseQuery = baseQuery.Where(e => e.OwnerId != null && filter.OwnerIds.Contains(e.OwnerId.Value));
 783        }
 784
 493785        if (filter.ExtraTypes.Length > 0)
 786        {
 787            // Convert ExtraType enum to BaseItemExtraType enum via int cast (same underlying values)
 0788            var extraTypeValues = filter.ExtraTypes.Select(e => (BaseItemExtraType?)(int)e).ToArray();
 0789            baseQuery = baseQuery.Where(e => e.ExtraType != null && extraTypeValues.Contains(e.ExtraType));
 790        }
 791
 493792        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 793        {
 0794            var lang = filter.HasNoAudioTrackWithLanguage;
 0795            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 796
 0797            baseQuery = baseQuery
 0798                .Where(e =>
 0799                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Audio && ms.Langua
 0800                    || (e.IsFolder && !foldersWithAudio.Contains(e.Id)));
 801        }
 802
 493803        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 804        {
 0805            var lang = filter.HasNoInternalSubtitleTrackWithLanguage;
 0806            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 807
 0808            baseQuery = baseQuery
 0809                .Where(e =>
 0810                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && !ms.Is
 0811                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 812        }
 813
 493814        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 815        {
 0816            var lang = filter.HasNoExternalSubtitleTrackWithLanguage;
 0817            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 818
 0819            baseQuery = baseQuery
 0820                .Where(e =>
 0821                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.IsE
 0822                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 823        }
 824
 493825        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 826        {
 0827            var lang = filter.HasNoSubtitleTrackWithLanguage;
 0828            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 829
 0830            baseQuery = baseQuery
 0831                .Where(e =>
 0832                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.Lan
 0833                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 834        }
 835
 493836        if (filter.HasSubtitles.HasValue)
 837        {
 0838            var hasSubtitles = filter.HasSubtitles.Value;
 0839            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasSubtitles());
 0840            if (hasSubtitles)
 841            {
 0842                baseQuery = baseQuery
 0843                    .Where(e =>
 0844                        (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0845                        || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 846            }
 847            else
 848            {
 0849                baseQuery = baseQuery
 0850                    .Where(e =>
 0851                        (!e.IsFolder && !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0852                        || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 853            }
 854        }
 855
 493856        if (filter.SubtitleLanguages.Count > 0)
 857        {
 0858            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 0859            baseQuery = baseQuery
 0860                .Where(e =>
 0861                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle
 0862                     && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && s
 0863                    || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 864        }
 865
 493866        if (filter.AudioLanguages.Count > 0)
 867        {
 0868            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 0869            baseQuery = baseQuery
 0870                .Where(e =>
 0871                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio
 0872                     && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.
 0873                    || (e.IsFolder && foldersWithAudio.Contains(e.Id)));
 874        }
 875
 493876        if (filter.HasChapterImages.HasValue)
 877        {
 0878            var hasChapterImages = filter.HasChapterImages.Value;
 0879            var foldersWithChapterImages = DescendantQueryHelper.GetFolderIdsMatching(context, new HasChapterImages());
 0880            if (hasChapterImages)
 881            {
 0882                baseQuery = baseQuery
 0883                    .Where(e =>
 0884                        (!e.IsFolder && e.Chapters!.Any(f => f.ImagePath != null))
 0885                        || (e.IsFolder && foldersWithChapterImages.Contains(e.Id)));
 886            }
 887            else
 888            {
 0889                baseQuery = baseQuery
 0890                    .Where(e =>
 0891                        (!e.IsFolder && !e.Chapters!.Any(f => f.ImagePath != null))
 0892                        || (e.IsFolder && !foldersWithChapterImages.Contains(e.Id)));
 893            }
 894        }
 895
 493896        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 897        {
 18898            baseQuery = baseQuery
 18899                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 900        }
 901
 493902        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 903        {
 18904            baseQuery = baseQuery
 18905                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 906        }
 907
 493908        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 909        {
 18910            baseQuery = baseQuery
 18911                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 912        }
 913
 493914        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 915        {
 18916            baseQuery = baseQuery
 18917                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 918        }
 919
 493920        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 921        {
 0922            baseQuery = baseQuery
 0923                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 924        }
 925
 493926        if (filter.Years.Length > 0)
 927        {
 0928            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 929        }
 930
 493931        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 493932        if (isVirtualItem.HasValue)
 933        {
 23934            baseQuery = baseQuery
 23935                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 936        }
 937
 493938        if (filter.IsSpecialSeason.HasValue)
 939        {
 0940            if (filter.IsSpecialSeason.Value)
 941            {
 0942                baseQuery = baseQuery
 0943                    .Where(e => e.IndexNumber == 0);
 944            }
 945            else
 946            {
 0947                baseQuery = baseQuery
 0948                    .Where(e => e.IndexNumber != 0);
 949            }
 950        }
 951
 493952        if (filter.IsUnaired.HasValue)
 953        {
 0954            if (filter.IsUnaired.Value)
 955            {
 0956                baseQuery = baseQuery
 0957                    .Where(e => e.PremiereDate >= now);
 958            }
 959            else
 960            {
 0961                baseQuery = baseQuery
 0962                    .Where(e => e.PremiereDate < now);
 963            }
 964        }
 965
 493966        if (filter.MediaTypes.Length > 0)
 967        {
 22968            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 22969            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 970        }
 971
 493972        if (filter.ItemIds.Length > 0)
 973        {
 0974            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 975        }
 976
 493977        if (filter.ExcludeItemIds.Length > 0)
 978        {
 0979            baseQuery = baseQuery
 0980                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 981        }
 982
 493983        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 984        {
 0985            baseQuery = baseQuery.WhereExcludeProviderIds(filter.ExcludeProviderIds);
 986        }
 987
 493988        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 989        {
 0990            baseQuery = baseQuery.WhereHasAnyProviderId(filter.HasAnyProviderId);
 991        }
 992
 493993        if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
 994        {
 0995            baseQuery = baseQuery.WhereHasAnyProviderIds(filter.HasAnyProviderIds);
 996        }
 997
 493998        if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
 999        {
 01000            var includeAny = filter.HasAnyProviderIds
 01001                .SelectMany(kvp => kvp.Value.Select(v => $"{kvp.Key}:{v}"))
 01002                .ToArray();
 01003            if (includeAny.Length > 0)
 1004            {
 01005                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 1006            }
 1007        }
 1008
 4931009        if (filter.HasImdbId.HasValue)
 1010        {
 01011            baseQuery = filter.HasImdbId.Value
 01012                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == ImdbProviderName))
 01013                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != ImdbProviderName));
 1014        }
 1015
 4931016        if (filter.HasTmdbId.HasValue)
 1017        {
 01018            baseQuery = filter.HasTmdbId.Value
 01019                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TmdbProviderName))
 01020                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TmdbProviderName));
 1021        }
 1022
 4931023        if (filter.HasTvdbId.HasValue)
 1024        {
 01025            baseQuery = filter.HasTvdbId.Value
 01026                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TvdbProviderName))
 01027                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TvdbProviderName));
 1028        }
 1029
 4931030        var queryTopParentIds = filter.TopParentIds;
 1031
 4931032        if (queryTopParentIds.Length > 0)
 1033        {
 151034            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 151035            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 151036            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 1037            {
 01038                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 1039            }
 1040            else
 1041            {
 151042                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 1043            }
 1044        }
 1045
 4931046        if (filter.AncestorIds.Length > 0)
 1047        {
 451048            var ancestorFilter = filter.AncestorIds.OneOrManyExpressionBuilder<AncestorId, Guid>(f => f.ParentItemId);
 451049            baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
 1050        }
 1051
 4931052        if (filter.LinkedChildAncestorIds.Length > 0)
 1053        {
 1054            // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ances
 01055            var linkedChildAncestorIds = filter.LinkedChildAncestorIds;
 01056            baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc =>
 01057                lc.ParentId == e.Id
 01058                && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId))));
 1059        }
 1060
 4931061        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 1062        {
 01063            baseQuery = baseQuery
 01064                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 1065        }
 1066
 4931067        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 1068        {
 01069            baseQuery = baseQuery
 01070                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 1071        }
 1072
 1073        // Pre-build the blocked-item-id set as a sub-select
 4931074        if (filter.ExcludeInheritedTags.Length > 0)
 1075        {
 01076            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01077            var blockedTagItemIds = context.ItemValuesMap
 01078                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 01079                .Select(f => f.ItemId);
 1080
 01081            baseQuery = baseQuery.Where(e =>
 01082                !blockedTagItemIds.Contains(e.Id)
 01083                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 01084                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 01085                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 1086        }
 1087
 4931088        if (filter.IncludeInheritedTags.Length > 0)
 1089        {
 01090            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01091            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 01092            var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 01093            var allowedTagItemIds = context.ItemValuesMap
 01094                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 01095                .Select(f => f.ItemId);
 1096
 01097            baseQuery = baseQuery.Where(e =>
 01098                allowedTagItemIds.Contains(e.Id)
 01099                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 01100                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 01101                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 01102
 01103                // People don't carry the tags of the media they appear in and would never match
 01104                || e.Type == personTypeName
 01105
 01106                // A playlist should be accessible to its owner regardless of allowed tags
 01107                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 1108        }
 1109
 4931110        if (filter.SeriesStatuses.Length > 0)
 1111        {
 01112            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 01113            baseQuery = baseQuery
 01114                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 1115        }
 1116
 4931117        if (filter.BoxSetLibraryFolders.Length > 0)
 1118        {
 01119            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 01120            baseQuery = baseQuery
 01121                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 1122        }
 1123
 4931124        if (filter.VideoTypes.Length > 0)
 1125        {
 1126            // Dvds and Blu-rays can either be stored in a folder structure or as an iso file
 1127            // => to find all matches we need to check both: VideoType and IsoType
 1128            // alternatively, we could provide specific IsoType filters
 01129            var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
 01130            var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray();
 01131            Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoT
 01132            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
 1133        }
 1134
 4931135        if (filter.Is3D.HasValue)
 1136        {
 01137            Expression<Func<BaseItemEntity, bool>> is3D = e => e.Data!.Contains("Video3DFormat");
 1138
 01139            baseQuery = filter.Is3D.Value
 01140                ? baseQuery.WhereItemOrDescendantMatches(context, is3D)
 01141                : baseQuery.WhereNeitherItemNorDescendantMatches(context, is3D);
 1142        }
 1143
 4931144        if (filter.IsPlaceHolder.HasValue)
 1145        {
 01146            Expression<Func<BaseItemEntity, bool>> isPlaceHolder = e => e.Data!.Contains("IsPlaceHolder\":true");
 1147
 01148            baseQuery = filter.IsPlaceHolder.Value
 01149                ? baseQuery.WhereItemOrDescendantMatches(context, isPlaceHolder)
 01150                : baseQuery.WhereNeitherItemNorDescendantMatches(context, isPlaceHolder);
 1151        }
 1152
 4931153        if (filter.HasSpecialFeature.HasValue)
 1154        {
 01155            var itemsWithExtras = context.BaseItems
 01156                .Where(extra => extra.OwnerId != null
 01157                    && extra.ExtraType != null
 01158                    && extra.ExtraType != BaseItemExtraType.Unknown
 01159                    && extra.ExtraType != BaseItemExtraType.Trailer
 01160                    && extra.ExtraType != BaseItemExtraType.ThemeSong
 01161                    && extra.ExtraType != BaseItemExtraType.ThemeVideo)
 01162                .Select(extra => extra.OwnerId!.Value)
 01163                .Distinct();
 1164
 01165            Expression<Func<BaseItemEntity, bool>> hasExtras = e => itemsWithExtras.Contains(e.Id);
 1166
 01167            baseQuery = filter.HasSpecialFeature.Value
 01168                ? baseQuery.WhereItemOrDescendantMatches(context, hasExtras)
 01169                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasExtras);
 1170        }
 1171
 4931172        if (filter.HasTrailer.HasValue)
 1173        {
 01174            var trailerOwnerIds = context.BaseItems
 01175                .Where(extra => extra.ExtraType == BaseItemExtraType.Trailer && extra.OwnerId != null)
 01176                .Select(extra => extra.OwnerId!.Value);
 1177
 01178            Expression<Func<BaseItemEntity, bool>> hasTrailer = e => trailerOwnerIds.Contains(e.Id);
 1179
 01180            baseQuery = filter.HasTrailer.Value
 01181                ? baseQuery.WhereItemOrDescendantMatches(context, hasTrailer)
 01182                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasTrailer);
 1183        }
 1184
 4931185        if (filter.HasThemeSong.HasValue)
 1186        {
 01187            var themeSongOwnerIds = context.BaseItems
 01188                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeSong && extra.OwnerId != null)
 01189                .Select(extra => extra.OwnerId!.Value);
 1190
 01191            Expression<Func<BaseItemEntity, bool>> hasThemeSong = e => themeSongOwnerIds.Contains(e.Id);
 1192
 01193            baseQuery = filter.HasThemeSong.Value
 01194                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeSong)
 01195                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeSong);
 1196        }
 1197
 4931198        if (filter.HasThemeVideo.HasValue)
 1199        {
 01200            var themeVideoOwnerIds = context.BaseItems
 01201                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeVideo && extra.OwnerId != null)
 01202                .Select(extra => extra.OwnerId!.Value);
 1203
 01204            Expression<Func<BaseItemEntity, bool>> hasThemeVideo = e => themeVideoOwnerIds.Contains(e.Id);
 1205
 01206            baseQuery = filter.HasThemeVideo.Value
 01207                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeVideo)
 01208                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeVideo);
 1209        }
 1210
 4931211        if (filter.AiredDuringSeason.HasValue)
 1212        {
 01213            var seasonNumber = filter.AiredDuringSeason.Value;
 01214            if (seasonNumber < 1)
 1215            {
 01216                baseQuery = baseQuery.Where(e => e.ParentIndexNumber == seasonNumber);
 1217            }
 1218            else
 1219            {
 01220                var seasonStr = seasonNumber.ToString(CultureInfo.InvariantCulture);
 01221                baseQuery = baseQuery.Where(e =>
 01222                    e.ParentIndexNumber == seasonNumber
 01223                    || (e.Data != null && (
 01224                        e.Data.Contains("\"AirsAfterSeasonNumber\":" + seasonStr)
 01225                        || e.Data.Contains("\"AirsBeforeSeasonNumber\":" + seasonStr))));
 1226            }
 1227        }
 1228
 4931229        if (filter.AdjacentTo.HasValue && !filter.AdjacentTo.Value.IsEmpty())
 1230        {
 01231            var adjacentToId = filter.AdjacentTo.Value;
 01232            var targetItem = context.BaseItems.Where(e => e.Id == adjacentToId).Select(e => new { e.SortName, e.Id }).Fi
 01233            if (targetItem is not null)
 1234            {
 01235                var targetSortName = targetItem.SortName ?? string.Empty;
 1236
 1237                // Fetch both prev and next adjacent items in a single query using Concat (UNION ALL).
 01238                var adjacentIds = context.BaseItems
 01239                    .Where(e => string.Compare(e.SortName, targetSortName) < 0)
 01240                    .OrderByDescending(e => e.SortName)
 01241                    .Select(e => e.Id)
 01242                    .Take(1)
 01243                    .Concat(
 01244                        context.BaseItems
 01245                            .Where(e => string.Compare(e.SortName, targetSortName) > 0)
 01246                            .OrderBy(e => e.SortName)
 01247                            .Select(e => e.Id)
 01248                            .Take(1))
 01249                    .ToList();
 1250
 01251                adjacentIds.Add(adjacentToId);
 01252                baseQuery = baseQuery.Where(e => adjacentIds.Contains(e.Id));
 1253            }
 1254        }
 1255
 4931256        return baseQuery;
 1257    }
 1258}

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