< Summary - Jellyfin

Line coverage
37%
Covered lines: 565
Uncovered lines: 945
Coverable lines: 1510
Total lines: 3126
Line coverage: 37.4%
Branch coverage
39%
Covered branches: 274
Total branches: 696
Branch coverage: 39.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/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: 29497/27/2026 - 12:16:14 AM Line coverage: 27.7% (425/1532) Branch coverage: 38.1% (263/690) Total lines: 30398/2/2026 - 12:17:28 AM Line coverage: 30.6% (445/1450) Branch coverage: 39.5% (268/678) Total lines: 29548/3/2026 - 12:16:46 AM Line coverage: 31.1% (451/1448) Branch coverage: 39.7% (268/674) Total lines: 29698/5/2026 - 12:17:09 AM Line coverage: 31.5% (460/1457) Branch coverage: 39.7% (268/674) Total lines: 29828/6/2026 - 12:17:15 AM Line coverage: 31.7% (481/1516) Branch coverage: 37.9% (267/704) Total lines: 31408/9/2026 - 12:16:58 AM Line coverage: 37.3% (566/1514) Branch coverage: 39.1% (275/702) Total lines: 31358/11/2026 - 12:17:00 AM Line coverage: 37.4% (565/1510) Branch coverage: 39.3% (274/696) Total lines: 3126 5/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: 29497/27/2026 - 12:16:14 AM Line coverage: 27.7% (425/1532) Branch coverage: 38.1% (263/690) Total lines: 30398/2/2026 - 12:17:28 AM Line coverage: 30.6% (445/1450) Branch coverage: 39.5% (268/678) Total lines: 29548/3/2026 - 12:16:46 AM Line coverage: 31.1% (451/1448) Branch coverage: 39.7% (268/674) Total lines: 29698/5/2026 - 12:17:09 AM Line coverage: 31.5% (460/1457) Branch coverage: 39.7% (268/674) Total lines: 29828/6/2026 - 12:17:15 AM Line coverage: 31.7% (481/1516) Branch coverage: 37.9% (267/704) Total lines: 31408/9/2026 - 12:16:58 AM Line coverage: 37.3% (566/1514) Branch coverage: 39.1% (275/702) Total lines: 31358/11/2026 - 12:17:00 AM Line coverage: 37.4% (565/1510) Branch coverage: 39.3% (274/696) Total lines: 3126

Coverage delta

Coverage delta 6 -6

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: GetAllArtists(...)100%210%
File 1: GetArtists(...)100%11100%
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(...)66.66%131280.76%
File 1: BuildItemCountsByCleanName(...)0%342180%
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: ApplyAdjacencyFilter(...)25%12420%
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%181892.3%
File 3: ApplyOrder(...)70.58%423481.25%
File 3: ApplySeriesDatePlayedOrder(...)0%2040%
File 3: BuildAccessFilteredDescendantsQuery(...)100%210%
File 3: RequiresParentalRestrictions(...)0%4260%
File 3: ApplyAccessFiltering(...)100%22100%
File 3: ApplyTopParentFiltering(...)50%8660%
File 3: GetExemptedItemByNameTypes(...)50%4475%
File 3: ApplyItemByNameAccessFiltering(...)0%4260%
File 3: ApplyParentalRestrictions(...)50%46816.12%
File 3: BuildMaxParentalRatingFilter(...)100%11100%
File 3: GetAccessFilteredLeafItemsQuery(...)100%11100%
File 3: BuildHasDescendantFilter(...)100%11100%
File 4: GetItemIdsList(...)100%11100%
File 4: GetItems(...)37.5%28831.81%
File 4: GetItemList(...)75%4469.56%
File 4: GetLatestItemList(...)0%210140%
File 4: LoadLatestByIds(...)100%210%
File 4: LoadLatestByIds(...)0%620%
File 4: GetLatestTvShowItems(...)0%7482860%
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: get_IsFolderFilter()100%11100%
File 5: TranslateQuery(...)40.81%4156337032.99%

File(s)

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Linq;
 6using Jellyfin.Data.Enums;
 7using Jellyfin.Database.Implementations;
 8using Jellyfin.Database.Implementations.Entities;
 9using MediaBrowser.Controller.Entities;
 10using MediaBrowser.Model.Dto;
 11using MediaBrowser.Model.Entities;
 12using MediaBrowser.Model.Querying;
 13using Microsoft.EntityFrameworkCore;
 14using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 15
 16namespace Jellyfin.Server.Implementations.Item;
 17
 18public sealed partial class BaseItemRepository
 19{
 20    /// <inheritdoc />
 21    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 22    {
 023        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 24    }
 25
 26    /// <inheritdoc />
 27    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 28    {
 429        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 30    }
 31
 32    /// <inheritdoc />
 33    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 34    {
 035        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 36    }
 37
 38    /// <inheritdoc />
 39    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 40    {
 041        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 42    }
 43
 44    /// <inheritdoc />
 45    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 46    {
 047        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 48    }
 49
 50    /// <inheritdoc />
 51    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 52    {
 053        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 54    }
 55
 56    /// <inheritdoc />
 57    public IReadOnlyList<string> GetStudioNames()
 58    {
 1859        return GetItemValueNames(_getStudiosValueTypes, [], []);
 60    }
 61
 62    /// <inheritdoc />
 63    public IReadOnlyList<string> GetAllArtistNames()
 64    {
 1865        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 66    }
 67
 68    /// <inheritdoc />
 69    public IReadOnlyList<string> GetMusicGenreNames()
 70    {
 1871        return GetItemValueNames(
 1872            _getGenreValueTypes,
 1873            _itemTypeLookup.MusicGenreTypes,
 1874            []);
 75    }
 76
 77    /// <inheritdoc />
 78    public IReadOnlyList<string> GetGenreNames()
 79    {
 1880        return GetItemValueNames(
 1881            _getGenreValueTypes,
 1882            [],
 1883            _itemTypeLookup.MusicGenreTypes);
 84    }
 85
 86    /// <inheritdoc />
 87    public IReadOnlyList<string> GetMediaStreamLanguages(InternalItemsQuery filter, MediaStreamType mediaStreamType)
 88    {
 089        ArgumentNullException.ThrowIfNull(filter);
 90
 091        using var context = _dbProvider.CreateDbContext();
 92
 093        return TranslateQuery(
 094            context.BaseItems.Include(e => e.MediaStreams).Where(e => e.Id != EF.Constant(PlaceholderId)),
 095            context,
 096            new InternalItemsQuery(filter.User)
 097            {
 098                IncludeOwnedItems = filter.IncludeOwnedItems,
 099                ExcludeItemTypes = filter.ExcludeItemTypes,
 0100                IncludeItemTypes = filter.IncludeItemTypes,
 0101                MediaTypes = filter.MediaTypes,
 0102                AncestorIds = filter.AncestorIds,
 0103                ItemIds = filter.ItemIds,
 0104                TopParentIds = filter.TopParentIds,
 0105                ParentId = filter.ParentId,
 0106                IsAiring = filter.IsAiring,
 0107                IsMovie = filter.IsMovie,
 0108                IsSports = filter.IsSports,
 0109                IsKids = filter.IsKids,
 0110                IsNews = filter.IsNews,
 0111                IsSeries = filter.IsSeries
 0112            })
 0113            .SelectMany(e => e.MediaStreams!)
 0114            .Where(e => e.StreamType == (MediaStreamTypeEntity)mediaStreamType)
 0115            .Select(s => string.IsNullOrEmpty(s.Language) ? "und" : s.Language) // und = undetermined
 0116            .Distinct()
 0117            .ToArray();
 0118    }
 119
 120    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    {
 4145        ArgumentNullException.ThrowIfNull(filter);
 146
 4147        using var context = _dbProvider.CreateDbContext();
 148
 4149        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 4150        {
 4151            ExcludeItemTypes = filter.ExcludeItemTypes,
 4152            IncludeItemTypes = filter.IncludeItemTypes,
 4153            MediaTypes = filter.MediaTypes,
 4154            AncestorIds = filter.AncestorIds,
 4155            ItemIds = filter.ItemIds,
 4156            TopParentIds = filter.TopParentIds,
 4157            ParentId = filter.ParentId,
 4158            IsAiring = filter.IsAiring,
 4159            IsMovie = filter.IsMovie,
 4160            IsSports = filter.IsSports,
 4161            IsKids = filter.IsKids,
 4162            IsNews = filter.IsNews,
 4163            IsSeries = filter.IsSeries
 4164        });
 165
 4166        var innerQuery = PrepareItemQuery(context, filter)
 4167            .Where(e => e.Type == returnType)
 4168            .Where(e => context.ItemValuesMap
 4169                .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type) && ivm.ItemValue.CleanValue == e.CleanName)
 4170                .Join(
 4171                    innerQueryFilter,
 4172                    ivm => ivm.ItemId,
 4173                    g => g.Id,
 4174                    (ivm, g) => ivm.ItemId)
 4175                .Any());
 176
 4177        var outerQueryFilter = new InternalItemsQuery(filter.User)
 4178        {
 4179            IsPlayed = filter.IsPlayed,
 4180            IsFavorite = filter.IsFavorite,
 4181            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 4182            IsLiked = filter.IsLiked,
 4183            IsLocked = filter.IsLocked,
 4184            NameLessThan = filter.NameLessThan,
 4185            NameStartsWith = filter.NameStartsWith,
 4186            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 4187            Tags = filter.Tags,
 4188            OfficialRatings = filter.OfficialRatings,
 4189            StudioIds = filter.StudioIds,
 4190            GenreIds = filter.GenreIds,
 4191            Genres = filter.Genres,
 4192            Years = filter.Years,
 4193            NameContains = filter.NameContains,
 4194            SearchTerm = filter.SearchTerm,
 4195            ExcludeItemIds = filter.ExcludeItemIds
 4196        };
 197
 198        // Collapse rows that share a PresentationUniqueKey (e.g. alternate versions) into one
 199        // representative id per group, then materialize the representative ids once.
 4200        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter);
 4201        var isMusicArtist = returnType == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 202        List<Guid> representativeIds;
 4203        if (isMusicArtist)
 204        {
 205            // For MusicArtist, prefer the entity from a library the user can actually access.
 206            // Materialize to prevent correlated per-group first-row queries which hurt performance.
 4207            var topParentIds = filter.TopParentIds;
 4208            representativeIds = masterQuery
 4209                .Select(e => new { e.Id, e.PresentationUniqueKey, e.TopParentId })
 4210                .AsEnumerable()
 4211                .GroupBy(e => e.PresentationUniqueKey)
 4212                .Select(g => g
 4213                    .OrderBy(e => topParentIds.Contains(e.TopParentId ?? Guid.Empty) ? 0 : 1)
 4214                    .ThenBy(e => e.Id)
 4215                    .First().Id)
 4216                .ToList();
 217        }
 218        else
 219        {
 0220            representativeIds = masterQuery
 0221                .GroupBy(e => e.PresentationUniqueKey)
 0222                .Select(g => g.Min(e => e.Id))
 0223                .ToList();
 224        }
 225
 4226        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 4227        if (filter.EnableTotalRecordCount)
 228        {
 3229            result.TotalRecordCount = representativeIds.Count;
 230        }
 231
 4232        var query = ApplyNavigations(
 4233                context.BaseItems.AsNoTracking().AsSingleQuery().WhereOneOrMany(representativeIds, e => e.Id),
 4234                filter);
 235
 4236        query = ApplyOrder(query, filter, context);
 237
 4238        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 239        {
 0240            query = query.Skip(filter.StartIndex.Value);
 241        }
 242
 4243        if (filter.Limit.HasValue)
 244        {
 1245            query = query.Take(filter.Limit.Value);
 246        }
 247
 4248        result.StartIndex = filter.StartIndex ?? 0;
 4249        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        {
 4269            result.Items =
 4270            [
 4271                .. query
 4272                    .AsEnumerable()
 4273                    .Where(e => e != null)
 4274                    .Select(e => DeserializeBaseItem(e, filter.SkipDeserialization))
 4275                    .Where(item => item != null)
 4276                    .Select(item => (item!, (ItemCounts?)null))
 4277            ];
 278        }
 279
 4280        return result;
 4281    }
 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        var rawCounts = 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            .AsEnumerable();
 326
 0327        var countsByCleanName = new Dictionary<string, ItemCounts>();
 0328        foreach (var group in rawCounts.GroupBy(x => x.CleanName))
 329        {
 0330            var counts = new ItemCounts();
 0331            foreach (var row in group)
 332            {
 0333                if (row.Type == seriesTypeName)
 334                {
 0335                    counts.SeriesCount += row.Count;
 336                }
 0337                else if (row.Type == episodeTypeName)
 338                {
 0339                    counts.EpisodeCount += row.Count;
 340                }
 0341                else if (row.Type == movieTypeName)
 342                {
 0343                    counts.MovieCount += row.Count;
 344                }
 0345                else if (row.Type == musicAlbumTypeName)
 346                {
 0347                    counts.AlbumCount += row.Count;
 348                }
 0349                else if (row.Type == musicArtistTypeName)
 350                {
 0351                    counts.ArtistCount += row.Count;
 352                }
 0353                else if (row.Type == audioTypeName)
 354                {
 0355                    counts.SongCount += row.Count;
 356                }
 0357                else if (row.Type == trailerTypeName)
 358                {
 0359                    counts.TrailerCount += row.Count;
 360                }
 361            }
 362
 0363            countsByCleanName[group.Key] = counts;
 364        }
 365
 0366        return countsByCleanName;
 367    }
 368}

/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>
 235    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
 243    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 244    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 245    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 246    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 247    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 48
 249    private static readonly BaseItemKind[] _itemByNameKinds =
 250    [
 251        BaseItemKind.Person,
 252        BaseItemKind.Genre,
 253        BaseItemKind.MusicGenre,
 254        BaseItemKind.MusicArtist,
 255        BaseItemKind.Studio
 256    ];
 57
 258    private static readonly (BaseItemKind Kind, IReadOnlyList<ItemValueType> ValueTypes)[] _itemByNameValueTypes =
 259    [
 260        (BaseItemKind.Genre, _getGenreValueTypes),
 261        (BaseItemKind.MusicGenre, _getGenreValueTypes),
 262        (BaseItemKind.MusicArtist, _getAllArtistsValueTypes),
 263        (BaseItemKind.Studio, _getStudiosValueTypes)
 264    ];
 65
 66    // The only folder kinds whose children form a single viewing sequence, so playback progress on a
 67    // child rolls up to them. Every other folder kind is a container that cannot be resumed.
 268    private static readonly BaseItemKind[] _resumableFolderKinds =
 269    [
 270        BaseItemKind.Series,
 271        BaseItemKind.Season
 272    ];
 73
 74    /// <summary>
 75    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
 76    /// </summary>
 77    /// <param name="dbProvider">The db factory.</param>
 78    /// <param name="appHost">The Application host.</param>
 79    /// <param name="itemTypeLookup">The static type lookup.</param>
 80    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
 81    /// <param name="logger">System logger.</param>
 82    public BaseItemRepository(
 83        IDbContextFactory<JellyfinDbContext> dbProvider,
 84        IServerApplicationHost appHost,
 85        IItemTypeLookup itemTypeLookup,
 86        IServerConfigurationManager serverConfigurationManager,
 87        ILogger<BaseItemRepository> logger)
 88    {
 3889        _dbProvider = dbProvider;
 3890        _appHost = appHost;
 3891        _itemTypeLookup = itemTypeLookup;
 3892        _serverConfigurationManager = serverConfigurationManager;
 3893        _logger = logger;
 3894    }
 95
 96    /// <summary>
 97    /// Maps a Entity to the DTO. Delegates to <see cref="BaseItemMapper"/>.
 98    /// </summary>
 99    /// <param name="entity">The database entity.</param>
 100    /// <param name="dto">The target DTO.</param>
 101    /// <param name="appHost">The application host.</param>
 102    /// <param name="logger">The logger.</param>
 103    /// <returns>The mapped DTO.</returns>
 104    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 105    {
 0106        return BaseItemMapper.Map(entity, dto, appHost);
 107    }
 108
 109    /// <summary>
 110    /// Maps a DTO to a database entity. Delegates to <see cref="BaseItemMapper"/>.
 111    /// </summary>
 112    /// <param name="dto">The DTO to map.</param>
 113    /// <returns>The mapped database entity.</returns>
 114    public BaseItemEntity Map(BaseItemDto dto)
 115    {
 10116        return BaseItemMapper.Map(dto, _appHost);
 117    }
 118
 119    /// <summary>
 120    /// Deserializes a BaseItemEntity and sets all properties.
 121    /// </summary>
 122    /// <param name="baseItemEntity">The entity to deserialize.</param>
 123    /// <param name="logger">The logger.</param>
 124    /// <param name="appHost">The application host.</param>
 125    /// <param name="skipDeserialization">Whether to skip JSON deserialization.</param>
 126    /// <returns>The deserialized item, or null.</returns>
 127    public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost
 128    {
 3129        return BaseItemMapper.DeserializeBaseItem(baseItemEntity, logger, appHost, skipDeserialization);
 130    }
 131
 132    /// <inheritdoc />
 133    public void PrepareFilterQuery(InternalItemsQuery query)
 134    {
 497135        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 136        {
 0137            query.Limit = query.Limit.Value + 4;
 138        }
 139
 497140        if (query.IsResumable ?? false)
 141        {
 1142            query.IsVirtualItem = false;
 143        }
 497144    }
 145
 146    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 147    {
 18148        var list = new List<string>();
 149
 18150        if (IsTypeInQuery(BaseItemKind.Person, query))
 151        {
 1152            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 153        }
 154
 18155        if (IsTypeInQuery(BaseItemKind.Genre, query))
 156        {
 1157            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 158        }
 159
 18160        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 161        {
 1162            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 163        }
 164
 18165        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 166        {
 1167            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 168        }
 169
 18170        if (IsTypeInQuery(BaseItemKind.Studio, query))
 171        {
 1172            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 173        }
 174
 18175        return list;
 176    }
 177
 178    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 179    {
 90180        if (query.ExcludeItemTypes.Contains(type))
 181        {
 0182            return false;
 183        }
 184
 90185        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 186    }
 187
 188    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 189    {
 497190        if (!query.GroupByPresentationUniqueKey)
 191        {
 149192            return false;
 193        }
 194
 195        // Resume queries surface the actually-played version (which may be an alternate sharing the
 196        // primary's presentation key). The resumable filter already keeps one version per group, so
 197        // presentation-key grouping must not collapse the surfaced version back onto the primary.
 348198        if (query.IsResumable == true)
 199        {
 1200            return false;
 201        }
 202
 347203        if (query.GroupBySeriesPresentationUniqueKey)
 204        {
 0205            return false;
 206        }
 207
 347208        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 209        {
 0210            return false;
 211        }
 212
 347213        if (query.User is null)
 214        {
 344215            return false;
 216        }
 217
 3218        if (query.IncludeItemTypes.Length == 0)
 219        {
 0220            return true;
 221        }
 222
 3223        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 3224            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 3225            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 3226            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 3227            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 3228            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 229    }
 230
 231    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 232    {
 0233        return BaseItemMapper.MapImageToEntity(baseItemId, e);
 234    }
 235
 236    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 237    {
 0238        return BaseItemMapper.MapImageFromEntity(e, appHost);
 239    }
 240
 241    private string? GetPathToSave(string path)
 242    {
 0243        if (path is null)
 244        {
 0245            return null;
 246        }
 247
 0248        return _appHost.ReverseVirtualPath(path);
 249    }
 250
 251    /// <inheritdoc />
 252    public BaseItemDto? DeserializeBaseItem(BaseItemEntity entity, bool skipDeserialization = false)
 253    {
 86254        ArgumentNullException.ThrowIfNull(entity, nameof(entity));
 86255        if (_serverConfigurationManager?.Configuration is null)
 256        {
 0257            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 258        }
 259
 86260        var typeToSerialise = BaseItemMapper.GetType(entity.Type);
 86261        return BaseItemMapper.DeserializeBaseItem(
 86262            entity,
 86263            _logger,
 86264            _appHost,
 86265            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 266    }
 267}

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2#pragma warning disable CA1304 // Specify CultureInfo
 3#pragma warning disable CA1311 // Specify a culture or use an invariant version
 4#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 5
 6using System;
 7using System.Collections.Generic;
 8using System.Linq;
 9using System.Linq.Expressions;
 10using Jellyfin.Data.Enums;
 11using Jellyfin.Database.Implementations;
 12using Jellyfin.Database.Implementations.Entities;
 13using Jellyfin.Database.Implementations.Enums;
 14using Jellyfin.Extensions;
 15using 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    {
 50128        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 50129        dbQuery = dbQuery.AsSingleQuery();
 30
 50131        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 = ApplyAdjacencyFilter(context, dbQuery, filter);
 9039        dbQuery = ApplyQueryPaging(dbQuery, filter);
 9040        dbQuery = ApplyNavigations(dbQuery, filter);
 9041        return dbQuery;
 42    }
 43
 44    /// <summary>
 45    /// Trims an ordered query down to the AdjacentTo item and its immediate neighbours.
 46    /// </summary>
 47    private IQueryable<BaseItemEntity> ApplyAdjacencyFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuer
 48    {
 49749        if (filter.AdjacentTo.IsNullOrEmpty())
 50        {
 49751            return dbQuery;
 52        }
 53
 54        // Adjacency is relative to the result set and the order the query asked for, so the ids have
 55        // to be read back in that order.
 056        var orderedIds = dbQuery.Select(e => e.Id).ToList();
 057        var index = orderedIds.IndexOf(filter.AdjacentTo.Value);
 058        if (index < 0)
 59        {
 60            // The item isn't part of this result set, so it has no neighbours in it either.
 061            return dbQuery.Take(0);
 62        }
 63
 064        var start = Math.Max(index - 1, 0);
 065        var adjacentIds = orderedIds.GetRange(start, Math.Min(index + 2, orderedIds.Count) - start);
 66
 067        var adjacentQuery = context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => adjacentIds.Contains(e.Id));
 68
 069        return ApplyOrder(adjacentQuery, filter, context);
 70    }
 71
 72    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 73    {
 49774        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 75        {
 11576            var offset = filter.StartIndex ?? 0;
 77
 11578            if (offset > 0)
 79            {
 080                dbQuery = dbQuery.Skip(offset);
 81            }
 82
 11583            if (filter.Limit.HasValue)
 84            {
 11585                dbQuery = dbQuery.Take(filter.Limit.Value);
 86            }
 87        }
 88
 49789        return dbQuery;
 90    }
 91
 92    private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery
 93    {
 94        // Collapse duplicates sharing a presentation key (e.g. alternate versions), preferring the
 95        // primary version (PrimaryVersionId is null) so detail pages and actions target it instead
 96        // of an arbitrary alternate. Keep the grouped ids as an IQueryable sub-select; materializing
 97        // to a List would inline one bound parameter per id and hit SQLite's variable cap.
 49798        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 49799        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 100        {
 0101            var groupedIds = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey })
 0102                .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
 0103            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 104        }
 497105        else if (enableGroupByPresentationUniqueKey)
 106        {
 2107            var groupedIds = dbQuery.GroupBy(e => e.PresentationUniqueKey)
 2108                .Select(g => g.Where(e => e.PrimaryVersionId == null).Min(e => (Guid?)e.Id) ?? g.Min(e => (Guid?)e.Id));
 2109            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 110        }
 495111        else if (filter.GroupBySeriesPresentationUniqueKey)
 112        {
 0113            var groupedIds = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.Min(x => x.Id));
 0114            dbQuery = context.BaseItems.AsNoTracking().Where(e => groupedIds.Contains(e.Id));
 115        }
 116        else
 117        {
 495118            dbQuery = dbQuery.Distinct();
 119        }
 120
 497121        if (filter.CollapseBoxSetItems == true)
 122        {
 0123            dbQuery = ApplyBoxSetCollapsing(context, dbQuery, filter.CollapseBoxSetItemTypes);
 124
 125            // Name filters run after collapse so BoxSets match by their own name, not a child's.
 0126            dbQuery = ApplyNameFilters(dbQuery, filter);
 127        }
 128
 497129        dbQuery = ApplyOrder(dbQuery, filter, context);
 130
 497131        return dbQuery;
 132    }
 133
 134    private IQueryable<BaseItemEntity> ApplyBoxSetCollapsing(
 135        JellyfinDbContext context,
 136        IQueryable<BaseItemEntity> dbQuery,
 137        BaseItemKind[] collapsibleTypes)
 138    {
 0139        var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
 140
 0141        var currentIds = dbQuery.Select(e => e.Id);
 142
 0143        if (collapsibleTypes.Length == 0)
 144        {
 145            // Collapse all item types into box sets
 0146            return ApplyBoxSetCollapsingAll(context, currentIds, boxSetTypeName);
 147        }
 148
 149        // Only collapse specific item types, keep others untouched
 0150        var collapsibleTypeNames = collapsibleTypes.Select(t => _itemTypeLookup.BaseItemKindNames[t]).ToList();
 151
 152        // Categorize items in currentIds in a single pass to avoid multiple correlated EXISTS over BaseItems.
 0153        var categorized = context.BaseItems
 0154            .AsNoTracking()
 0155            .Where(bi => currentIds.Contains(bi.Id))
 0156            .Select(bi => new
 0157            {
 0158                bi.Id,
 0159                IsCollapsible = collapsibleTypeNames.Contains(bi.Type),
 0160                IsBoxSet = bi.Type == boxSetTypeName
 0161            });
 162
 0163        var collapsibleChildIds = categorized.Where(c => c.IsCollapsible).Select(c => c.Id);
 164
 165        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0166        var manualBoxSetLinks = context.LinkedChildren
 0167            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0168                && currentIds.Contains(lc.ChildId))
 0169            .Join(
 0170                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0171                lc => lc.ParentId,
 0172                bs => bs.Id,
 0173                (lc, bs) => new { lc.ChildId, lc.ParentId });
 174
 0175        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 176
 177        // Items whose type is NOT collapsible (always kept in results)
 0178        var nonCollapsibleIds = categorized.Where(c => !c.IsCollapsible).Select(c => c.Id);
 179
 180        // Collapsible items that are not a BoxSet themselves and not a manual child of any BoxSet
 0181        var collapsibleNotInBoxSet = categorized
 0182            .Where(c => c.IsCollapsible && !c.IsBoxSet)
 0183            .Select(c => c.Id)
 0184            .Where(id => !childrenInBoxSet.Contains(id));
 185
 186        // BoxSet IDs containing at least one collapsible child item from currentIds
 0187        var boxSetIds = manualBoxSetLinks
 0188            .Where(x => collapsibleChildIds.Contains(x.ChildId))
 0189            .Select(x => x.ParentId)
 0190            .Distinct();
 191
 0192        var collapsedIds = nonCollapsibleIds.Union(collapsibleNotInBoxSet).Union(boxSetIds);
 0193        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 194    }
 195
 196    private static IQueryable<BaseItemEntity> ApplyBoxSetCollapsingAll(
 197        JellyfinDbContext context,
 198        IQueryable<Guid> currentIds,
 199        string boxSetTypeName)
 200    {
 201        // Single JOIN: manual links to BoxSet parents, restricted to currentIds children.
 0202        var manualBoxSetLinks = context.LinkedChildren
 0203            .Where(lc => lc.ChildType == Database.Implementations.Entities.LinkedChildType.Manual
 0204                && currentIds.Contains(lc.ChildId))
 0205            .Join(
 0206                context.BaseItems.Where(bs => bs.Type == boxSetTypeName),
 0207                lc => lc.ParentId,
 0208                bs => bs.Id,
 0209                (lc, bs) => new { lc.ChildId, lc.ParentId });
 210
 0211        var childrenInBoxSet = manualBoxSetLinks.Select(x => x.ChildId).Distinct();
 0212        var boxSetIds = manualBoxSetLinks.Select(x => x.ParentId).Distinct();
 213
 214        // Items in currentIds that are not BoxSets themselves and not a manual child of any BoxSet
 0215        var notInBoxSet = context.BaseItems
 0216            .AsNoTracking()
 0217            .Where(e => currentIds.Contains(e.Id) && e.Type != boxSetTypeName)
 0218            .Select(e => e.Id)
 0219            .Where(id => !childrenInBoxSet.Contains(id));
 220
 0221        var collapsedIds = notInBoxSet.Union(boxSetIds);
 0222        return context.BaseItems.AsNoTracking().Where(e => collapsedIds.Contains(e.Id));
 223    }
 224
 225    private static IQueryable<BaseItemEntity> ApplyNameFilters(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 226    {
 505227        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 228        {
 0229            var nameStartsWithLower = filter.NameStartsWith.ToLowerInvariant();
 0230            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().StartsWith(nameStartsWithLower));
 231        }
 232
 505233        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 234        {
 0235            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 0236            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(startsOrGreaterLower) >= 0);
 237        }
 238
 505239        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 240        {
 0241            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 0242            dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(lessThanLower) < 0);
 243        }
 244
 505245        return dbQuery;
 246    }
 247
 248    /// <inheritdoc />
 249    public IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 250    {
 430251        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 252        {
 0253            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 254        }
 255
 430256        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 257        {
 429258            dbQuery = dbQuery.Include(e => e.Provider);
 259        }
 260
 430261        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 262        {
 429263            dbQuery = dbQuery.Include(e => e.LockedFields);
 264        }
 265
 430266        if (filter.DtoOptions.EnableUserData)
 267        {
 430268            dbQuery = dbQuery.Include(e => e.UserData);
 269        }
 270
 430271        if (filter.DtoOptions.EnableImages)
 272        {
 430273            dbQuery = dbQuery.Include(e => e.Images);
 274        }
 275
 276        // Include LinkedChildEntities for container types and videos that use them (BoxSet, Playlist,
 277        // CollectionFolder for manual linking; every video type for alternate versions).
 278        // When IncludeItemTypes is empty (any type may be returned), always include them to ensure
 279        // LinkedChildren are loaded before items are saved back, preventing accidental deletion.
 430280        var linkedChildTypes = new[]
 430281        {
 430282            BaseItemKind.BoxSet,
 430283            BaseItemKind.Playlist,
 430284            BaseItemKind.CollectionFolder,
 430285            BaseItemKind.Video,
 430286            BaseItemKind.Movie,
 430287            BaseItemKind.Episode,
 430288            BaseItemKind.MusicVideo,
 430289            BaseItemKind.Trailer
 430290        };
 430291        if (filter.IncludeItemTypes.Length == 0 || filter.IncludeItemTypes.Any(linkedChildTypes.Contains))
 292        {
 265293            dbQuery = dbQuery.Include(e => e.LinkedChildEntities);
 294        }
 295
 430296        if (filter.IncludeExtras)
 297        {
 0298            dbQuery = dbQuery.Include(e => e.Extras);
 299        }
 300
 430301        return dbQuery;
 302    }
 303
 304    /// <inheritdoc />
 305    public IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDb
 306    {
 501307        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 501308        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 309
 310        // SeriesDatePlayed requires special handling to avoid correlated subqueries.
 311        // Instead of running a MAX() subquery per-row in ORDER BY, we pre-aggregate
 312        // max played dates per series in one query and left-join it.
 501313        if (!hasSearch && orderBy.Any(o => o.OrderBy == ItemSortBy.SeriesDatePlayed))
 314        {
 0315            return ApplySeriesDatePlayedOrder(query, filter, context, orderBy);
 316        }
 317
 501318        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 319
 501320        if (hasSearch)
 321        {
 0322            var relevanceExpression = OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!);
 0323            orderedQuery = query.OrderBy(relevanceExpression);
 324        }
 325
 501326        if (orderBy.Length > 0)
 327        {
 122328            var firstOrdering = orderBy[0];
 122329            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 330
 122331            if (orderedQuery is null)
 332            {
 122333                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 122334                    ? query.OrderBy(expression)
 122335                    : query.OrderByDescending(expression);
 336            }
 337            else
 338            {
 0339                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 0340                    ? orderedQuery.ThenBy(expression)
 0341                    : orderedQuery.ThenByDescending(expression);
 342            }
 343
 122344            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 345            {
 6346                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 6347                    ? orderedQuery.ThenBy(e => e.Name)
 6348                    : orderedQuery.ThenByDescending(e => e.Name);
 349            }
 350
 332351            foreach (var item in orderBy.Skip(1))
 352            {
 44353                expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 44354                orderedQuery = item.SortOrder == SortOrder.Ascending
 44355                    ? orderedQuery.ThenBy(expression)
 44356                    : orderedQuery.ThenByDescending(expression);
 357            }
 358        }
 359
 501360        if (orderedQuery is null)
 361        {
 379362            return query.OrderBy(e => e.SortName);
 363        }
 364
 365        // Add SortName as final tiebreaker
 122366        if (!hasSearch && (orderBy.Length == 0 || orderBy.All(o => o.OrderBy is not ItemSortBy.SortName and not ItemSort
 367        {
 72368            orderedQuery = orderedQuery.ThenBy(e => e.SortName);
 369        }
 370
 122371        return orderedQuery;
 372    }
 373
 374    private IQueryable<BaseItemEntity> ApplySeriesDatePlayedOrder(
 375        IQueryable<BaseItemEntity> query,
 376        InternalItemsQuery filter,
 377        JellyfinDbContext context,
 378        (ItemSortBy OrderBy, SortOrder SortOrder)[] orderBy)
 379    {
 380        // Pre-aggregate max played date per series key in ONE query.
 381        // This generates a single: SELECT SeriesPresentationUniqueKey, MAX(LastPlayedDate) ... GROUP BY
 382        // instead of a correlated subquery per outer row.
 0383        IQueryable<UserData> userDataQuery = filter.User is not null
 0384            ? context.UserData.Where(ud => ud.UserId == filter.User.Id && ud.Played)
 0385            : context.UserData.Where(ud => ud.Played);
 386
 0387        var seriesMaxDates = userDataQuery
 0388            .Join(
 0389                context.BaseItems,
 0390                ud => ud.ItemId,
 0391                bi => bi.Id,
 0392                (ud, bi) => new { bi.SeriesPresentationUniqueKey, ud.LastPlayedDate })
 0393            .Where(x => x.SeriesPresentationUniqueKey != null)
 0394            .GroupBy(x => x.SeriesPresentationUniqueKey)
 0395            .Select(g => new { SeriesKey = g.Key!, MaxDate = g.Max(x => x.LastPlayedDate) });
 396
 0397        var joined = query.LeftJoin(
 0398            seriesMaxDates,
 0399            e => e.PresentationUniqueKey,
 0400            s => s.SeriesKey,
 0401            (e, s) => new { Item = e, MaxDate = s != null ? s.MaxDate : (DateTime?)null });
 402
 0403        var seriesSort = orderBy.First(o => o.OrderBy == ItemSortBy.SeriesDatePlayed);
 404
 0405        return seriesSort.SortOrder == SortOrder.Ascending
 0406            ? joined.OrderBy(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item)
 0407            : joined.OrderByDescending(x => x.MaxDate).ThenBy(x => x.Item.SortName).Select(x => x.Item);
 408    }
 409
 410    /// <summary>
 411    /// Builds a query for descendants of an ancestor with user access filtering applied.
 412    /// Uses recursive CTE to traverse both hierarchical (AncestorIds) and linked (LinkedChildren) relationships.
 413    /// </summary>
 414    /// <inheritdoc />
 415    public IQueryable<BaseItemEntity> BuildAccessFilteredDescendantsQuery(
 416        JellyfinDbContext context,
 417        InternalItemsQuery filter,
 418        Guid ancestorId)
 419    {
 420        // Use recursive CTE to get all descendants (hierarchical and linked)
 0421        var allDescendantIds = DescendantQueryHelper.GetAllDescendantIds(context, ancestorId);
 422
 0423        var baseQuery = context.BaseItems
 0424            .AsNoTracking()
 0425            .Where(b => allDescendantIds.Contains(b.Id))
 0426            .Where(DescendantQueryHelper.IsCountableLeaf);
 427
 0428        return ApplyAccessFiltering(context, baseQuery, filter);
 429    }
 430
 431    /// <summary>
 432    /// Checks whether the user restricts access to items by parental rating or tags.
 433    /// </summary>
 434    /// <param name="filter">The query filter.</param>
 435    /// <returns><c>true</c> if the query carries parental restrictions.</returns>
 436    private static bool RequiresParentalRestrictions(InternalItemsQuery filter)
 0437        => filter.IncludeInheritedTags.Length > 0
 0438            || filter.ExcludeInheritedTags.Length > 0
 0439            || filter.MaxParentalRating is not null
 0440            || filter.BlockUnratedItems.Length > 0;
 441
 442    /// <summary>
 443    /// Applies user access filtering to a query.
 444    /// Includes TopParentIds, parental rating, and tag filtering.
 445    /// </summary>
 446    /// <inheritdoc />
 447    public IQueryable<BaseItemEntity> ApplyAccessFiltering(
 448        JellyfinDbContext context,
 449        IQueryable<BaseItemEntity> baseQuery,
 450        InternalItemsQuery filter)
 451    {
 2452        baseQuery = ApplyTopParentFiltering(context, baseQuery, filter);
 453
 2454        baseQuery = ApplyParentalRestrictions(context, baseQuery, filter);
 455
 456        // Exclude alternate versions (have PrimaryVersionId set) and owned non-extra items.
 457        // Extras (trailers, etc.) have OwnerId set but also have ExtraType set — keep those.
 2458        if (!filter.IncludeOwnedItems)
 459        {
 1460            baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 461        }
 462
 2463        return baseQuery;
 464    }
 465
 466    /// <summary>
 467    /// Restricts a query to the libraries the user may open, exempting requested by-name items.
 468    /// </summary>
 469    /// <param name="context">The database context.</param>
 470    /// <param name="baseQuery">The query to filter.</param>
 471    /// <param name="filter">The query filter.</param>
 472    /// <returns>The filtered query.</returns>
 473    private IQueryable<BaseItemEntity> ApplyTopParentFiltering(
 474        JellyfinDbContext context,
 475        IQueryable<BaseItemEntity> baseQuery,
 476        InternalItemsQuery filter)
 477    {
 507478        var queryTopParentIds = filter.TopParentIds;
 507479        if (queryTopParentIds.Length == 0)
 480        {
 489481            return baseQuery;
 482        }
 483
 18484        var exemptedItemByNameTypes = GetExemptedItemByNameTypes(filter);
 18485        if (exemptedItemByNameTypes.Count == 0)
 486        {
 18487            return baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 488        }
 489
 0490        baseQuery = baseQuery.Where(e => exemptedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w == e.T
 0491        if (filter.UserHasContentRestrictions)
 492        {
 0493            baseQuery = ApplyItemByNameAccessFiltering(baseQuery, context, filter, exemptedItemByNameTypes, queryTopPare
 494        }
 495
 0496        return baseQuery;
 497    }
 498
 499    /// <summary>
 500    /// Returns the by-name types a query asks for, which carry no TopParentId to filter on.
 501    /// </summary>
 502    /// <param name="filter">The query filter.</param>
 503    /// <returns>The type names exempt from library filtering.</returns>
 504    private List<string> GetExemptedItemByNameTypes(InternalItemsQuery filter)
 505    {
 18506        var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 18507        if ((filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0)
 508        {
 0509            return includedItemByNameTypes;
 510        }
 511
 18512        return _itemByNameKinds.Where(filter.IncludeItemTypes.Contains).Select(e => _itemTypeLookup.BaseItemKindNames[e]
 513    }
 514
 515    /// <summary>
 516    /// Keeps a by-name row only when at least one item behind its name is reachable for the user.
 517    /// </summary>
 518    /// <param name="baseQuery">The query to filter.</param>
 519    /// <param name="context">The database context.</param>
 520    /// <param name="filter">The query filter.</param>
 521    /// <param name="itemByNameTypes">The exempted by-name type names.</param>
 522    /// <param name="topParentIds">The libraries the user may open.</param>
 523    /// <returns>The filtered query.</returns>
 524    private IQueryable<BaseItemEntity> ApplyItemByNameAccessFiltering(
 525        IQueryable<BaseItemEntity> baseQuery,
 526        JellyfinDbContext context,
 527        InternalItemsQuery filter,
 528        IReadOnlyList<string> itemByNameTypes,
 529        Guid[] topParentIds)
 530    {
 531        // IncludeOwnedItems: a credit on an alternate version of a reachable movie still counts.
 0532        var accessibleItems = ApplyAccessFiltering(
 0533            context,
 0534            context.BaseItems.AsNoTracking(),
 0535            new InternalItemsQuery(filter.User) { TopParentIds = topParentIds, IncludeOwnedItems = true });
 536
 0537        var personType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 0538        if (itemByNameTypes.Contains(personType))
 539        {
 0540            baseQuery = baseQuery.Where(e => e.Type != personType
 0541                || context.Peoples.Any(p => p.Name == e.Name
 0542                    && context.PeopleBaseItemMap.Any(m => m.PeopleId == p.Id && accessibleItems.Any(i => i.Id == m.ItemI
 543        }
 544
 0545        foreach (var (kind, valueTypes) in _itemByNameValueTypes)
 546        {
 0547            var typeName = _itemTypeLookup.BaseItemKindNames[kind];
 0548            if (!itemByNameTypes.Contains(typeName))
 549            {
 550                continue;
 551            }
 552
 0553            baseQuery = baseQuery.Where(e => e.Type != typeName
 0554                || context.ItemValues.Any(v => valueTypes.Contains(v.Type) && v.CleanValue == e.CleanName
 0555                    && context.ItemValuesMap.Any(m => m.ItemValueId == v.ItemValueId && accessibleItems.Any(i => i.Id ==
 556        }
 557
 0558        return baseQuery;
 559    }
 560
 561    /// <summary>
 562    /// Applies the user's parental rating and tag restrictions to a query.
 563    /// </summary>
 564    /// <param name="context">The database context.</param>
 565    /// <param name="baseQuery">The query to filter.</param>
 566    /// <param name="filter">The query filter.</param>
 567    /// <returns>The filtered query.</returns>
 568    private IQueryable<BaseItemEntity> ApplyParentalRestrictions(
 569        JellyfinDbContext context,
 570        IQueryable<BaseItemEntity> baseQuery,
 571        InternalItemsQuery filter)
 572    {
 573        // Apply parental rating filtering
 2574        if (filter.MaxParentalRating is not null)
 575        {
 0576            baseQuery = baseQuery.Where(BuildMaxParentalRatingFilter(context, filter.MaxParentalRating));
 577        }
 578
 579        // Apply block unrated items filtering
 2580        if (filter.BlockUnratedItems.Length > 0)
 581        {
 0582            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0583            baseQuery = baseQuery.Where(e =>
 0584                e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType));
 585        }
 586
 587        // Apply excluded tags filtering (blocked tags).
 588        // Pre-build the blocked-item-id set as a sub-select; then four index-seek Contains checks
 589        // instead of one EXISTS over a 4-way OR predicate that defeats index seeks.
 2590        if (filter.ExcludeInheritedTags.Length > 0)
 591        {
 0592            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0593            var blockedTagItemIds = context.ItemValuesMap
 0594                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 0595                .Select(f => f.ItemId);
 596
 0597            baseQuery = baseQuery.Where(e =>
 0598                !blockedTagItemIds.Contains(e.Id)
 0599                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 0600                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 0601                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 602        }
 603
 604        // Apply included tags filtering (allowed tags - item must have at least one).
 2605        if (filter.IncludeInheritedTags.Length > 0)
 606        {
 0607            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 0608            var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 0609            var allowedTagItemIds = context.ItemValuesMap
 0610                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 0611                .Select(f => f.ItemId);
 612
 0613            baseQuery = baseQuery.Where(e =>
 0614                allowedTagItemIds.Contains(e.Id)
 0615                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 0616                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 0617                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 0618
 0619                // People don't carry the tags of the media they appear in and would never match
 0620                || e.Type == personTypeName);
 621        }
 622
 2623        return baseQuery;
 624    }
 625
 626    /// <summary>
 627    /// Builds a filter expression for max parental rating that handles both rated items
 628    /// and unrated BoxSets/Playlists (which check linked children's ratings).
 629    /// </summary>
 630    private static Expression<Func<BaseItemEntity, bool>> BuildMaxParentalRatingFilter(
 631        JellyfinDbContext context,
 632        ParentalRatingScore maxRating)
 633    {
 54634        var maxScore = maxRating.Score;
 54635        var maxSubScore = maxRating.SubScore ?? 0;
 54636        var linkedChildren = context.LinkedChildren;
 637
 54638        return e =>
 54639            // Item has a rating: check against limit
 54640            (e.InheritedParentalRatingValue != null
 54641                && (e.InheritedParentalRatingValue < maxScore
 54642                    || (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSub
 54643            // Item has no rating
 54644            || (e.InheritedParentalRatingValue == null
 54645                && (
 54646                    // No linked children (not a BoxSet/Playlist): pass as unrated
 54647                    !linkedChildren.Any(lc => lc.ParentId == e.Id)
 54648                    // Has linked children: at least one child must be within limits
 54649                    || linkedChildren.Any(lc => lc.ParentId == e.Id
 54650                        && (lc.Child!.InheritedParentalRatingValue == null
 54651                            || lc.Child.InheritedParentalRatingValue < maxScore
 54652                            || (lc.Child.InheritedParentalRatingValue == maxScore
 54653                                && (lc.Child.InheritedParentalRatingSubValue ?? 0) <= maxSubScore)))));
 654    }
 655
 656    /// <inheritdoc />
 657    public IQueryable<BaseItemEntity> GetAccessFilteredLeafItemsQuery(JellyfinDbContext context, User user, bool include
 658    {
 2659        ArgumentNullException.ThrowIfNull(context);
 2660        ArgumentNullException.ThrowIfNull(user);
 661
 2662        var leafItems = context.BaseItems
 2663            .AsNoTracking()
 2664            .Where(DescendantQueryHelper.IsCountableLeaf);
 665
 2666        return ApplyAccessFiltering(context, leafItems, new InternalItemsQuery(user) { IncludeOwnedItems = includeOwnedI
 667    }
 668
 669    /// <inheritdoc />
 670    public Expression<Func<BaseItemEntity, bool>> BuildHasDescendantFilter(JellyfinDbContext context, IQueryable<BaseIte
 671    {
 3672        ArgumentNullException.ThrowIfNull(context);
 3673        ArgumentNullException.ThrowIfNull(descendants);
 674
 675        // Descendants are reachable through the ancestor chain and - for BoxSets and Playlists - as
 676        // linked children, which can themselves be folders contributing their own descendants.
 677        // Every step is a correlated index seek, so only the rows the outer query keeps are visited
 678        // and a folder is left as soon as its first matching descendant is found.
 3679        return e => context.AncestorIds.Any(a => a.ParentItemId == e.Id && descendants.Any(d => d.Id == a.ItemId))
 3680            || context.LinkedChildren.Any(lc => lc.ParentId == e.Id
 3681                && (descendants.Any(d => d.Id == lc.ChildId)
 3682                    || context.AncestorIds.Any(a => a.ParentItemId == lc.ChildId && descendants.Any(d => d.Id == a.ItemI
 683    }
 684}

/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);
 052        dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
 53
 054        if (filter.EnableTotalRecordCount)
 55        {
 056            result.TotalRecordCount = dbQuery.Count();
 57        }
 58
 059        dbQuery = ApplyQueryPaging(dbQuery, filter);
 060        dbQuery = ApplyNavigations(dbQuery, filter);
 61
 062        result.Items = dbQuery.AsEnumerable().Where(e => e != null).Select(w => DeserializeBaseItem(w, filter.SkipDeseri
 063        result.StartIndex = filter.StartIndex ?? 0;
 064        return result;
 065    }
 66
 67    /// <inheritdoc />
 68    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
 69    {
 40770        ArgumentNullException.ThrowIfNull(filter);
 40771        PrepareFilterQuery(filter);
 72
 40773        using var context = _dbProvider.CreateDbContext();
 40774        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 75
 40776        dbQuery = TranslateQuery(dbQuery, context, filter);
 77
 40778        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 40779        dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
 40780        dbQuery = ApplyQueryPaging(dbQuery, filter);
 81
 40782        var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
 40783        if (hasRandomSort)
 84        {
 7185            var orderedIds = dbQuery.AsNoTracking().Select(e => e.Id).ToList();
 7186            if (orderedIds.Count == 0)
 87            {
 7188                return Array.Empty<BaseItemDto>();
 89            }
 90
 091            var itemsById = ApplyNavigations(context.BaseItems.AsNoTracking().WhereOneOrMany(orderedIds, e => e.Id), fil
 092                .AsSplitQuery()
 093                .AsEnumerable()
 094                .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 095                .Where(dto => dto != null)
 096                .ToDictionary(i => i!.Id);
 97
 098            return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!;
 99        }
 100
 336101        dbQuery = ApplyNavigations(dbQuery, filter);
 102
 336103        return dbQuery.AsEnumerable().Where(e => e != null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializatio
 407104    }
 105
 106    /// <inheritdoc/>
 107    public IReadOnlyList<BaseItemDto> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 108    {
 0109        ArgumentNullException.ThrowIfNull(filter);
 0110        PrepareFilterQuery(filter);
 111
 112        // Early exit if collection type is not supported
 0113        if (collectionType is not CollectionType.movies and not CollectionType.tvshows and not CollectionType.music)
 114        {
 0115            return [];
 116        }
 117
 0118        var limit = filter.Limit;
 0119        using var context = _dbProvider.CreateDbContext();
 120
 0121        var baseQuery = PrepareItemQuery(context, filter);
 0122        baseQuery = TranslateQuery(baseQuery, context, filter);
 123
 0124        if (collectionType == CollectionType.tvshows)
 125        {
 0126            return GetLatestTvShowItems(context, baseQuery, filter, limit);
 127        }
 128
 0129        if (collectionType is CollectionType.movies)
 130        {
 131            // Pick, per PresentationUniqueKey, the newest item; return the newest `limit` of those.
 132            // Build up until limit by streaming through results and deduplicating on the fly.
 0133            var orderedIds = baseQuery
 0134                .Where(e => e.PresentationUniqueKey != null)
 0135                .OrderByDescending(e => e.DateCreated)
 0136                .ThenByDescending(e => e.Id)
 0137                .Select(e => new { e.Id, e.PresentationUniqueKey });
 138
 139            // DistinctBy and Take are lazy, so enumeration stops as soon as limit distinct keys are read.
 0140            var firstIds = orderedIds
 0141                .AsEnumerable()
 0142                .DistinctBy(row => row.PresentationUniqueKey)
 0143                .Select(row => row.Id)
 0144                .Take(limit ?? int.MaxValue)
 0145                .ToList();
 146
 0147            return LoadLatestByIds(context, firstIds, filter);
 148        }
 149
 0150        var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]!;
 151        IQueryable<BaseItemEntity> topAlbumsQuery;
 152
 153        // When the query is scoped to whole libraries, read the newest albums directly by their own TopParentId.
 0154        if (filter.TopParentIds.Length > 0)
 155        {
 0156            topAlbumsQuery = context.BaseItems.AsNoTracking()
 0157                .Where(album => album.Type == musicAlbumTypeName
 0158                    && !album.IsVirtualItem
 0159                    && album.TopParentId.HasValue)
 0160                .WhereOneOrMany(filter.TopParentIds, album => album.TopParentId!.Value);
 161        }
 162        else
 163        {
 164            // Fallback (e.g. AncestorIds-scoped callers): albums that are the parent of a matching track.
 0165            var albumIdsWithMatchingTrack = context.AncestorIds
 0166                .Join(baseQuery, ai => ai.ItemId, t => t.Id, (ai, _) => ai.ParentItemId);
 0167            topAlbumsQuery = context.BaseItems.AsNoTracking()
 0168                .Where(album => album.Type == musicAlbumTypeName)
 0169                .Where(album => albumIdsWithMatchingTrack.Contains(album.Id));
 170        }
 171
 172        // The album is what gets returned, and neither branch above reads it through the
 173        // user's filters, so its own parental restrictions have to be applied here: a
 174        // matching track does not make an album the user may not see visible.
 0175        var orderedAlbums = ApplyParentalRestrictions(context, topAlbumsQuery, filter)
 0176            .OrderByDescending(album => album.DateCreated)
 0177            .ThenByDescending(album => album.Id);
 178
 0179        var albumIdsQuery = limit.HasValue
 0180            ? orderedAlbums.Take(limit.Value).Select(a => a.Id)
 0181            : orderedAlbums.Select(a => a.Id);
 182
 0183        return LoadLatestByIds(context, albumIdsQuery, filter);
 0184    }
 185
 186    // Keeping idsQuery deferred lets EF emit `WHERE Id IN (<subquery>)`.
 187    private IReadOnlyList<BaseItemDto> LoadLatestByIds(
 188        JellyfinDbContext context,
 189        IQueryable<Guid> idsQuery,
 190        InternalItemsQuery filter)
 191    {
 0192        var itemsQuery = ApplyNavigations(
 0193            context.BaseItems.AsNoTracking().Where(e => idsQuery.Contains(e.Id)),
 0194            filter);
 195
 0196        return itemsQuery
 0197            .OrderByDescending(e => e.DateCreated)
 0198            .ThenByDescending(e => e.Id)
 0199            .AsEnumerable()
 0200            .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 0201            .Where(dto => dto != null)
 0202            .ToArray()!;
 203    }
 204
 205    private IReadOnlyList<BaseItemDto> LoadLatestByIds(
 206        JellyfinDbContext context,
 207        List<Guid> ids,
 208        InternalItemsQuery filter)
 209    {
 0210        if (ids.Count == 0)
 211        {
 0212            return [];
 213        }
 214
 0215        var itemsQuery = ApplyNavigations(
 0216            context.BaseItems.AsNoTracking().WhereOneOrMany(ids, e => e.Id),
 0217            filter);
 218
 0219        return itemsQuery
 0220            .OrderByDescending(e => e.DateCreated)
 0221            .ThenByDescending(e => e.Id)
 0222            .AsEnumerable()
 0223            .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 0224            .Where(dto => dto != null)
 0225            .ToArray()!;
 226    }
 227
 228    /// <summary>
 229    /// Gets the latest TV show items with smart Season/Series container selection.
 230    /// </summary>
 231    /// <remarks>
 232    /// <para>
 233    /// This method implements intelligent container selection for TV shows in the "Latest" section.
 234    /// Instead of always showing individual episodes, it analyzes recent additions and may return
 235    /// a Season or Series container when multiple related episodes were recently added.
 236    /// </para>
 237    /// <para>
 238    /// The selection logic is:
 239    /// <list type="bullet">
 240    ///     <item>If recent episodes span multiple seasons → return the Series</item>
 241    ///     <item>If multiple recent episodes are from one season AND the series has multiple seasons → return the Seaso
 242    ///     <item>If multiple recent episodes are from one season AND the series has only one season → return the Series
 243    ///     <item>Otherwise → return the most recent Episode</item>
 244    /// </list>
 245    /// </para>
 246    /// </remarks>
 247    /// <param name="context">The database context.</param>
 248    /// <param name="baseQuery">The base query with filters already applied.</param>
 249    /// <param name="filter">The query filter options.</param>
 250    /// <param name="limit">Maximum number of items to return.</param>
 251    /// <returns>A list of BaseItemDto representing the latest TV content.</returns>
 252    private IReadOnlyList<BaseItemDto> GetLatestTvShowItems(JellyfinDbContext context, IQueryable<BaseItemEntity> baseQu
 253    {
 254        // Episodes added within this window are considered "recently added together"
 255        const double RecentAdditionWindowHours = 24.0;
 256
 257        // Step 1: Find the top N series with recently added content, ordered by most recent addition
 0258        var topSeriesWithDates = baseQuery
 0259            .Where(e => e.SeriesName != null)
 0260            .GroupBy(e => e.SeriesName)
 0261            .Select(g => new { SeriesName = g.Key!, MaxDate = g.Max(e => e.DateCreated) })
 0262            .OrderByDescending(g => g.MaxDate);
 263
 0264        if (limit.HasValue)
 265        {
 0266            topSeriesWithDates = topSeriesWithDates.Take(limit.Value).OrderByDescending(g => g.MaxDate);
 267        }
 268
 269        // Materialize series names and cutoff to avoid embedding the GroupBy+OrderBy
 270        // expression tree as a subquery inside the episode query.
 0271        var topSeriesData = topSeriesWithDates
 0272            .Select(g => new { g.SeriesName, g.MaxDate })
 0273            .ToList();
 0274        var topSeriesNames = topSeriesData.Select(g => g.SeriesName).ToList();
 275
 276        // Compute a global date cutoff: the oldest series' max date minus the window.
 277        // Episodes before this cutoff cannot be in any series' "recent additions" window,
 278        // so we can safely exclude them to avoid loading ancient episodes.
 0279        var globalCutoff = topSeriesData.Count > 0
 0280            ? topSeriesData.Min(g => g.MaxDate)?.AddHours(-RecentAdditionWindowHours)
 0281            : null;
 282
 283        // Restrict to episodes of the top series, optionally bounded by the global cutoff.
 0284        var episodeQuery = baseQuery.Where(e => e.SeriesName != null && topSeriesNames.Contains(e.SeriesName));
 0285        if (globalCutoff is not null)
 286        {
 0287            episodeQuery = episodeQuery.Where(e => e.DateCreated >= globalCutoff);
 288        }
 289
 290        // Lightweight projection: only the columns needed for the in-memory analysis below.
 0291        var allEpisodes = episodeQuery
 0292            .OrderByDescending(e => e.DateCreated)
 0293            .ThenByDescending(e => e.Id)
 0294            .Select(e => new { e.Id, e.SeriesName, e.DateCreated, e.SeasonId, e.SeriesId })
 0295            .AsEnumerable();
 296
 297        // Collect all season/series IDs we'll need to look up for count information
 0298        var allSeasonIds = new HashSet<Guid>();
 0299        var allSeriesIds = new HashSet<Guid>();
 300
 301        // Analysis data for each series: recent episode count, season IDs, and the most recent episode ID
 0302        var analysisData = new List<(
 0303            int RecentEpisodeCount,
 0304            List<Guid> SeasonIds,
 0305            Guid? FirstRecentSeriesId,
 0306            DateTime MaxDate,
 0307            Guid MostRecentEpisodeId)>();
 308
 309        // Step 3: Analyze each series to identify recent additions within the time window
 0310        foreach (var group in allEpisodes.GroupBy(e => e.SeriesName))
 311        {
 0312            var episodes = group.ToList();
 0313            var mostRecentDate = episodes[0].DateCreated ?? DateTime.MinValue;
 0314            var recentCutoff = mostRecentDate.AddHours(-RecentAdditionWindowHours);
 315
 316            // Find episodes added within the recent window
 0317            var recentEpisodeCount = 0;
 0318            var seasonIdSet = new HashSet<Guid>();
 0319            Guid? firstRecentSeriesId = null;
 320
 0321            foreach (var ep in episodes)
 322            {
 0323                if (ep.DateCreated >= recentCutoff)
 324                {
 0325                    recentEpisodeCount++;
 0326                    if (ep.SeasonId.HasValue)
 327                    {
 0328                        seasonIdSet.Add(ep.SeasonId.Value);
 329                    }
 330
 0331                    firstRecentSeriesId ??= ep.SeriesId;
 332                }
 333            }
 334
 0335            var seasonIds = seasonIdSet.ToList();
 0336            analysisData.Add((recentEpisodeCount, seasonIds, firstRecentSeriesId, mostRecentDate, episodes[0].Id));
 337
 338            // Track all unique season/series IDs for batch lookups
 0339            foreach (var sid in seasonIds)
 340            {
 0341                allSeasonIds.Add(sid);
 342            }
 343
 0344            if (firstRecentSeriesId.HasValue)
 345            {
 0346                allSeriesIds.Add(firstRecentSeriesId.Value);
 347            }
 348        }
 349
 350        // Step 4: Batch fetch counts - episodes per season and seasons per series
 351        // These counts help determine whether to show Season or Series as the container
 0352        var episodeType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 0353        var seasonType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Season];
 0354        var seasonEpisodeCounts = allSeasonIds.Count > 0
 0355            ? context.BaseItems
 0356                .AsNoTracking()
 0357                .Where(e => e.SeasonId.HasValue && allSeasonIds.Contains(e.SeasonId.Value) && e.Type == episodeType)
 0358                .GroupBy(e => e.SeasonId!.Value)
 0359                .Select(g => new { SeasonId = g.Key, Count = g.Count() })
 0360                .ToDictionary(x => x.SeasonId, x => x.Count)
 0361            : [];
 362
 0363        var seriesSeasonCounts = allSeriesIds.Count > 0
 0364            ? context.BaseItems
 0365                .AsNoTracking()
 0366                .Where(e => e.SeriesId.HasValue && allSeriesIds.Contains(e.SeriesId.Value) && e.Type == seasonType)
 0367                .GroupBy(e => e.SeriesId!.Value)
 0368                .Select(g => new { SeriesId = g.Key, Count = g.Count() })
 0369                .ToDictionary(x => x.SeriesId, x => x.Count)
 0370            : [];
 371
 372        // Step 5: Apply the container selection logic for each series.
 373        // For each series, decide which entity best represents the recent additions:
 374        //   - 1 episode added → show the Episode itself
 375        //   - Multiple episodes in 1 season (multi-season series) → show the Season
 376        //   - Multiple episodes in 1 season (single-season series) → show the Series
 377        //   - Episodes across multiple seasons → show the Series
 0378        var entitiesToFetch = new HashSet<Guid>();
 0379        var seriesResults = new List<(Guid? SeasonId, Guid? SeriesId, DateTime MaxDate, Guid MostRecentEpisodeId)>(analy
 380
 0381        foreach (var (recentEpisodeCount, seasonIds, firstRecentSeriesId, maxDate, mostRecentEpisodeId) in analysisData)
 382        {
 0383            Guid? seasonId = null;
 0384            Guid? seriesId = null;
 385
 0386            if (seasonIds.Count == 1)
 387            {
 388                // All recent episodes are from a single season
 0389                var sid = seasonIds[0];
 0390                var totalEpisodes = seasonEpisodeCounts.GetValueOrDefault(sid, 0);
 0391                var totalSeasonsInSeries = firstRecentSeriesId.HasValue
 0392                    ? seriesSeasonCounts.GetValueOrDefault(firstRecentSeriesId.Value, 1)
 0393                    : 1;
 394
 395                // Check if multiple episodes were added, or if all episodes in the season were added
 0396                var hasMultipleOrAllEpisodes = recentEpisodeCount > 1 || recentEpisodeCount == totalEpisodes;
 397
 0398                if (totalSeasonsInSeries > 1 && hasMultipleOrAllEpisodes)
 399                {
 400                    // Multi-season series with bulk additions: show the Season
 0401                    seasonId = sid;
 0402                    entitiesToFetch.Add(sid);
 403                }
 0404                else if (hasMultipleOrAllEpisodes && firstRecentSeriesId.HasValue)
 405                {
 406                    // Single-season series with bulk additions: show the Series
 0407                    seriesId = firstRecentSeriesId;
 0408                    entitiesToFetch.Add(firstRecentSeriesId.Value);
 409                }
 410
 411                // Otherwise: single episode, will fall through to show the Episode
 412            }
 0413            else if (seasonIds.Count > 1 && firstRecentSeriesId.HasValue)
 414            {
 415                // Recent episodes span multiple seasons: show the Series
 0416                seriesId = firstRecentSeriesId;
 0417                entitiesToFetch.Add(seriesId!.Value);
 418            }
 419
 0420            if (seasonId is null && seriesId is null)
 421            {
 0422                entitiesToFetch.Add(mostRecentEpisodeId);
 423            }
 424
 0425            seriesResults.Add((seasonId, seriesId, maxDate, mostRecentEpisodeId));
 426        }
 427
 428        // Step 5b: A container is what gets returned, so it has to pass the user's access
 429        // filters on its own - a matching episode does not make a Season or Series the user
 430        // may not see visible. Containers that don't pass are replaced by their episode.
 0431        if (RequiresParentalRestrictions(filter) && entitiesToFetch.Count > 0)
 432        {
 0433            var allowedContainerIds = ApplyParentalRestrictions(
 0434                    context,
 0435                    context.BaseItems.AsNoTracking().Where(e => entitiesToFetch.Contains(e.Id)),
 0436                    filter)
 0437                .Select(e => e.Id)
 0438                .ToHashSet();
 439
 0440            for (var i = 0; i < seriesResults.Count; i++)
 441            {
 0442                var (seasonId, seriesId, maxDate, mostRecentEpisodeId) = seriesResults[i];
 0443                if (seasonId.HasValue && !allowedContainerIds.Contains(seasonId.Value))
 444                {
 0445                    seasonId = null;
 446                }
 447
 0448                if (seriesId.HasValue && !allowedContainerIds.Contains(seriesId.Value))
 449                {
 0450                    seriesId = null;
 451                }
 452
 0453                if (seasonId is null && seriesId is null)
 454                {
 0455                    entitiesToFetch.Add(mostRecentEpisodeId);
 456                }
 457
 0458                seriesResults[i] = (seasonId, seriesId, maxDate, mostRecentEpisodeId);
 459            }
 460        }
 461
 462        // Step 6: Fetch the Season/Series entities we decided to return
 0463        var entities = entitiesToFetch.Count > 0
 0464            ? ApplyNavigations(
 0465                    context.BaseItems.AsNoTracking().Where(e => entitiesToFetch.Contains(e.Id)),
 0466                    filter)
 0467                .AsSingleQuery()
 0468                .ToDictionary(e => e.Id)
 0469            : [];
 470
 471        // Step 7: Build final results, preferring Season > Series > Episode.
 472        // All needed entities are already fetched in step 6 with navigation properties.
 0473        var results = new List<(BaseItemEntity Entity, DateTime MaxDate)>(seriesResults.Count);
 0474        foreach (var (seasonId, seriesId, maxDate, mostRecentEpisodeId) in seriesResults)
 475        {
 0476            if (seasonId.HasValue && entities.TryGetValue(seasonId.Value, out var seasonEntity))
 477            {
 0478                results.Add((seasonEntity, maxDate));
 0479                continue;
 480            }
 481
 0482            if (seriesId.HasValue && entities.TryGetValue(seriesId.Value, out var seriesEntity))
 483            {
 0484                results.Add((seriesEntity, maxDate));
 0485                continue;
 486            }
 487
 0488            if (entities.TryGetValue(mostRecentEpisodeId, out var episodeEntity))
 489            {
 0490                results.Add((episodeEntity, maxDate));
 491            }
 492        }
 493
 0494        var finalResults = results
 0495            .OrderByDescending(r => r.MaxDate)
 0496            .ThenByDescending(r => r.Entity.Id);
 497
 0498        if (limit.HasValue)
 499        {
 0500            finalResults = finalResults
 0501            .Take(limit.Value)
 0502            .OrderByDescending(r => r.MaxDate)
 0503            .ThenByDescending(r => r.Entity.Id);
 504        }
 505
 0506        return finalResults
 0507            .Select(r => DeserializeBaseItem(r.Entity, filter.SkipDeserialization))
 0508            .Where(dto => dto is not null)
 0509            .ToArray()!;
 510    }
 511
 512    /// <inheritdoc/>
 513    public async Task<bool> ItemExistsAsync(Guid id)
 514    {
 26515        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 26516        await using (dbContext.ConfigureAwait(false))
 517        {
 26518            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 519        }
 26520    }
 521
 522    /// <inheritdoc  />
 523    public BaseItemDto? RetrieveItem(Guid id)
 524    {
 90525        if (id.IsEmpty())
 526        {
 0527            throw new ArgumentException("Guid can't be empty", nameof(id));
 528        }
 529
 90530        using var context = _dbProvider.CreateDbContext();
 90531        var dbQuery = PrepareItemQuery(context, new()
 90532        {
 90533            DtoOptions = new()
 90534            {
 90535                EnableImages = true
 90536            }
 90537        });
 90538        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 90539            .Include(e => e.Provider)
 90540            .Include(e => e.LockedFields)
 90541            .Include(e => e.UserData)
 90542            .Include(e => e.Images)
 90543            .Include(e => e.LinkedChildEntities)
 90544            .AsSingleQuery();
 545
 90546        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 90547        if (item is null)
 548        {
 90549            return null;
 550        }
 551
 0552        return DeserializeBaseItem(item);
 90553    }
 554
 555    /// <inheritdoc />
 556    public bool GetIsPlayed(User user, Guid id, bool recursive)
 557    {
 0558        using var dbContext = _dbProvider.CreateDbContext();
 559
 0560        if (recursive)
 561        {
 0562            var descendantIds = DescendantQueryHelper.GetAllDescendantIds(dbContext, id);
 563
 0564            return dbContext.BaseItems
 0565                    .Where(e => descendantIds.Contains(e.Id) && !e.IsFolder && !e.IsVirtualItem)
 0566                    .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
 567        }
 568
 0569        return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.P
 0570    }
 571
 572    /// <inheritdoc />
 573    public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery filter)
 574    {
 0575        ArgumentNullException.ThrowIfNull(filter);
 0576        PrepareFilterQuery(filter);
 577
 0578        using var context = _dbProvider.CreateDbContext();
 0579        var baseQuery = PrepareItemQuery(context, filter);
 0580        baseQuery = TranslateQuery(baseQuery, context, filter);
 581
 0582        var matchingItemIds = baseQuery.Select(e => e.Id);
 583
 0584        var years = baseQuery
 0585            .Where(e => e.ProductionYear != null && e.ProductionYear > 0)
 0586            .Select(e => e.ProductionYear!.Value)
 0587            .Distinct()
 0588            .OrderBy(y => y)
 0589            .ToArray();
 590
 0591        var officialRatings = baseQuery
 0592            .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty)
 0593            .Select(e => e.OfficialRating!)
 0594            .Distinct()
 0595            .OrderBy(r => r)
 0596            .ToArray();
 597
 0598        var tags = context.ItemValuesMap
 0599            .Where(ivm => ivm.ItemValue.Type == ItemValueType.Tags)
 0600            .Where(ivm => matchingItemIds.Contains(ivm.ItemId))
 0601            .Select(ivm => ivm.ItemValue)
 0602            .GroupBy(iv => iv.CleanValue)
 0603            .Select(g => g.Min(iv => iv.Value))
 0604            .OrderBy(t => t)
 0605            .ToArray();
 606
 0607        var genres = context.ItemValuesMap
 0608            .Where(ivm => ivm.ItemValue.Type == ItemValueType.Genre)
 0609            .Where(ivm => matchingItemIds.Contains(ivm.ItemId))
 0610            .Select(ivm => ivm.ItemValue)
 0611            .GroupBy(iv => iv.CleanValue)
 0612            .Select(g => g.Min(iv => iv.Value))
 0613            .OrderBy(g => g)
 0614            .ToArray();
 615
 0616        return new QueryFiltersLegacy
 0617        {
 0618            Years = years,
 0619            OfficialRatings = officialRatings,
 0620            Tags = tags,
 0621            Genres = genres
 0622        };
 0623    }
 624}

/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{
 228    private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
 29
 230    private static readonly string ImdbProviderName = MetadataProvider.Imdb.ToString().ToLowerInvariant();
 231    private static readonly string TmdbProviderName = MetadataProvider.Tmdb.ToString().ToLowerInvariant();
 232    private static readonly string TvdbProviderName = MetadataProvider.Tvdb.ToString().ToLowerInvariant();
 33
 34    // A fresh expression per access: EF rejects a query tree that reuses one lambda parameter
 35    // instance across several lambdas, and this filter is combined into a tree more than once.
 236    private static Expression<Func<BaseItemEntity, bool>> IsFolderFilter => e => e.IsFolder;
 37
 38    /// <inheritdoc />
 39    public IQueryable<BaseItemEntity> TranslateQuery(
 40        IQueryable<BaseItemEntity> baseQuery,
 41        JellyfinDbContext context,
 42        InternalItemsQuery filter)
 43    {
 44        const int HDWidth = 1200;
 45        const int UHDWidth = 3800;
 46        const int UHDHeight = 2100;
 47
 50548        var minWidth = filter.MinWidth;
 50549        var maxWidth = filter.MaxWidth;
 50550        var now = DateTime.UtcNow;
 51
 50552        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 53        {
 054            bool includeSD = false;
 055            bool includeHD = false;
 056            bool include4K = false;
 57
 058            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 59            {
 060                includeSD = true;
 61            }
 62
 063            if (filter.IsHD.HasValue && filter.IsHD.Value)
 64            {
 065                includeHD = true;
 66            }
 67
 068            if (filter.Is4K.HasValue && filter.Is4K.Value)
 69            {
 070                include4K = true;
 71            }
 72
 73            // Non-folders: check own resolution directly (no subquery).
 74            // Folders (Series, BoxSets): EXISTS check on descendants/linked children.
 75            // Using navigation properties (a.Item, lc.Child) produces efficient
 76            // EXISTS + JOIN instead of nested IN (SELECT ...) subqueries.
 077            baseQuery = baseQuery.Where(e =>
 078                (!e.IsFolder && e.Width > 0
 079                    && ((includeSD && e.Width < HDWidth)
 080                        || (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight))
 081                        || (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight))))
 082                || (e.IsFolder
 083                    && (e.Children!.Any(a =>
 084                            a.Item.Width > 0
 085                            && ((includeSD && a.Item.Width < HDWidth)
 086                                || (includeHD && a.Item.Width >= HDWidth && !(a.Item.Width >= UHDWidth || a.Item.Height 
 087                                || (include4K && (a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight))))
 088                        || context.LinkedChildren.Any(lc =>
 089                            lc.ParentId == e.Id
 090                            && lc.Child!.Width > 0
 091                            && ((includeSD && lc.Child.Width < HDWidth)
 092                                || (includeHD && lc.Child.Width >= HDWidth && !(lc.Child.Width >= UHDWidth || lc.Child.H
 093                                || (include4K && (lc.Child.Width >= UHDWidth || lc.Child.Height >= UHDHeight)))))));
 94        }
 95
 50596        if (minWidth.HasValue)
 97        {
 098            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 99        }
 100
 505101        if (filter.MinHeight.HasValue)
 102        {
 0103            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 104        }
 105
 505106        if (maxWidth.HasValue)
 107        {
 0108            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 109        }
 110
 505111        if (filter.MaxHeight.HasValue)
 112        {
 0113            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 114        }
 115
 505116        if (filter.IsLocked.HasValue)
 117        {
 54118            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 119        }
 120
 505121        var tags = filter.Tags.ToList();
 505122        var excludeTags = filter.ExcludeTags.ToList();
 123
 505124        if (filter.IsMovie.HasValue)
 125        {
 0126            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 0127                && (filter.IncludeItemTypes.Length == 0
 0128                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 0129                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 130
 0131            if (!shouldIncludeAllMovieTypes)
 132            {
 0133                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 134            }
 135        }
 136
 505137        if (filter.IsSeries.HasValue)
 138        {
 0139            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 140        }
 141
 505142        if (filter.IsSports.HasValue)
 143        {
 0144            if (filter.IsSports.Value)
 145            {
 0146                tags.Add("Sports");
 147            }
 148            else
 149            {
 0150                excludeTags.Add("Sports");
 151            }
 152        }
 153
 505154        if (filter.IsNews.HasValue)
 155        {
 0156            if (filter.IsNews.Value)
 157            {
 0158                tags.Add("News");
 159            }
 160            else
 161            {
 0162                excludeTags.Add("News");
 163            }
 164        }
 165
 505166        if (filter.IsKids.HasValue)
 167        {
 0168            if (filter.IsKids.Value)
 169            {
 0170                tags.Add("Kids");
 171            }
 172            else
 173            {
 0174                excludeTags.Add("Kids");
 175            }
 176        }
 177
 505178        if (!string.IsNullOrEmpty(filter.SearchTerm))
 179        {
 0180            var cleanedSearchTerm = filter.SearchTerm.GetCleanValue();
 0181            var originalSearchTerm = filter.SearchTerm;
 0182            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 183            {
 0184                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 0185                var likeSearchTerm = $"%{originalSearchTerm.Trim('%')}%";
 0186                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 187            }
 188            else
 189            {
 0190                var likeSearchTerm = $"%{originalSearchTerm}%";
 0191                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 192            }
 193        }
 194
 505195        if (filter.IsFolder.HasValue)
 196        {
 22197            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 198        }
 199
 505200        var includeTypes = filter.IncludeItemTypes;
 201
 202        // Only specify excluded types if no included types are specified
 505203        if (filter.IncludeItemTypes.Length == 0)
 204        {
 248205            var excludeTypes = filter.ExcludeItemTypes;
 248206            if (excludeTypes.Length == 1)
 207            {
 0208                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 209                {
 0210                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 211                }
 212            }
 248213            else if (excludeTypes.Length > 1)
 214            {
 0215                var excludeTypeName = new List<string>();
 0216                foreach (var excludeType in excludeTypes)
 217                {
 0218                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 219                    {
 0220                        excludeTypeName.Add(baseItemKindName!);
 221                    }
 222                }
 223
 0224                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 225            }
 226        }
 227        else
 228        {
 257229            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 257230            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 231        }
 232
 505233        if (filter.ChannelIds.Count > 0)
 234        {
 0235            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 236        }
 237
 505238        if (!filter.ParentId.IsEmpty())
 239        {
 149240            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 241        }
 242
 505243        if (!string.IsNullOrWhiteSpace(filter.Path))
 244        {
 0245            var pathToQuery = GetPathToSave(filter.Path);
 0246            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 247        }
 248
 505249        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 250        {
 0251            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 252        }
 253
 505254        if (filter.MinCommunityRating.HasValue)
 255        {
 0256            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 257        }
 258
 505259        if (filter.MinIndexNumber.HasValue)
 260        {
 0261            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 262        }
 263
 505264        if (filter.MinParentAndIndexNumber.HasValue)
 265        {
 0266            baseQuery = baseQuery
 0267                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 268        }
 269
 505270        if (filter.MinDateCreated.HasValue)
 271        {
 0272            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 273        }
 274
 505275        if (filter.MinDateLastSaved.HasValue)
 276        {
 0277            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 278        }
 279
 505280        if (filter.MinDateLastSavedForUser.HasValue)
 281        {
 0282            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 283        }
 284
 505285        if (filter.IndexNumber.HasValue)
 286        {
 0287            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 288        }
 289
 505290        if (filter.ParentIndexNumber.HasValue)
 291        {
 0292            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 293        }
 294
 505295        if (filter.ParentIndexNumberNotEquals.HasValue)
 296        {
 0297            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 298        }
 299
 505300        var minEndDate = filter.MinEndDate;
 505301        var maxEndDate = filter.MaxEndDate;
 302
 505303        if (filter.HasAired.HasValue)
 304        {
 0305            if (filter.HasAired.Value)
 306            {
 0307                maxEndDate = DateTime.UtcNow;
 308            }
 309            else
 310            {
 0311                minEndDate = DateTime.UtcNow;
 312            }
 313        }
 314
 505315        if (minEndDate.HasValue)
 316        {
 0317            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 318        }
 319
 505320        if (maxEndDate.HasValue)
 321        {
 0322            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 323        }
 324
 505325        if (filter.MinStartDate.HasValue)
 326        {
 0327            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 328        }
 329
 505330        if (filter.MaxStartDate.HasValue)
 331        {
 0332            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 333        }
 334
 505335        if (filter.MinPremiereDate.HasValue)
 336        {
 0337            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 338        }
 339
 505340        if (filter.MaxPremiereDate.HasValue)
 341        {
 0342            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 343        }
 344
 505345        if (filter.TrailerTypes.Length > 0)
 346        {
 0347            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 0348            baseQuery = baseQuery.Where(e => e.TrailerTypes!.Any(w => trailerTypes.Contains(w.Id)));
 349        }
 350
 505351        if (filter.IsAiring.HasValue)
 352        {
 0353            if (filter.IsAiring.Value)
 354            {
 0355                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 356            }
 357            else
 358            {
 0359                baseQuery = baseQuery.Where(e => e.StartDate > now || e.EndDate < now);
 360            }
 361        }
 362
 505363        if (filter.PersonIds.Length > 0)
 364        {
 0365            var peopleEntityIds = context.BaseItems
 0366                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 0367                .Join(
 0368                    context.Peoples,
 0369                    b => b.Name,
 0370                    p => p.Name,
 0371                    (b, p) => p.Id);
 372
 0373            var personTypes = filter.PersonTypes;
 0374            baseQuery = baseQuery
 0375                .Where(e => context.PeopleBaseItemMap
 0376                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId) && (personTypes.Length == 0 || pe
 377        }
 378
 505379        if (!string.IsNullOrWhiteSpace(filter.Person))
 380        {
 0381            var personTypes = filter.PersonTypes;
 0382            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person && (personTypes.Length =
 383        }
 384
 505385        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 386        {
 0387            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 388        }
 389
 505390        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 391        {
 0392            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 393        }
 394
 505395        if (!string.IsNullOrWhiteSpace(filter.Name))
 396        {
 3397            if (filter.UseRawName == true)
 398            {
 0399                var nameLower = filter.Name.ToLowerInvariant();
 0400                baseQuery = baseQuery.Where(e => e.Name!.ToLower() == nameLower);
 401            }
 402            else
 403            {
 3404                var cleanName = filter.Name.GetCleanValue();
 3405                baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 406            }
 407        }
 408
 505409        var nameContains = filter.NameContains;
 505410        if (!string.IsNullOrWhiteSpace(nameContains))
 411        {
 0412            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 413            {
 0414                nameContains = $"%{nameContains.Trim('%')}%";
 0415                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 416            }
 417            else
 418            {
 0419                var likeNameContains = $"%{nameContains}%";
 0420                baseQuery = baseQuery.Where(e =>
 0421                                    e.CleanName!.Contains(nameContains)
 0422                                    || EF.Functions.Like(e.OriginalTitle, likeNameContains));
 423            }
 424        }
 425
 426        // When box set collapsing is active, defer name-range filters to after the collapse.
 427        // Otherwise, items are filtered by their own name but then collapsed into a BoxSet
 428        // whose name may fall in a different range (e.g. "21 Jump Street" is under "#"
 429        // but its BoxSet "Jump Street Collection" should appear under "J").
 505430        if (filter.CollapseBoxSetItems != true)
 431        {
 505432            baseQuery = ApplyNameFilters(baseQuery, filter);
 433        }
 434
 505435        if (filter.ImageTypes.Length > 0)
 436        {
 115437            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 115438            baseQuery = baseQuery.Where(e => e.Images!.Any(w => imgTypes.Contains(w.ImageType)));
 439        }
 440
 505441        if (filter.IsLiked.HasValue)
 442        {
 0443            var likedIds = context.UserData
 0444                .Where(ud => ud.UserId == filter.User!.Id && ud.Rating >= UserItemData.MinLikeValue)
 0445                .Select(ud => ud.ItemId);
 446
 0447            baseQuery = filter.IsLiked.Value
 0448                ? baseQuery.Where(e => likedIds.Contains(e.Id))
 0449                : baseQuery.Where(e => !likedIds.Contains(e.Id));
 450        }
 451
 505452        if (filter.IsFavoriteOrLiked.HasValue || filter.IsFavorite.HasValue)
 453        {
 0454            var favoriteIds = context.UserData
 0455                .Where(ud => ud.UserId == filter.User!.Id && ud.IsFavorite)
 0456                .Select(ud => ud.ItemId);
 457
 0458            if (filter.IsFavoriteOrLiked.HasValue)
 459            {
 0460                baseQuery = filter.IsFavoriteOrLiked.Value
 0461                    ? baseQuery.Where(e => favoriteIds.Contains(e.Id))
 0462                    : baseQuery.Where(e => !favoriteIds.Contains(e.Id));
 463            }
 464
 0465            if (filter.IsFavorite.HasValue)
 466            {
 0467                baseQuery = filter.IsFavorite.Value
 0468                    ? baseQuery.Where(e => favoriteIds.Contains(e.Id))
 0469                    : baseQuery.Where(e => !favoriteIds.Contains(e.Id));
 470            }
 471        }
 472
 505473        if (filter.IsPlayed.HasValue)
 474        {
 0475            var userId = filter.User!.Id;
 476
 477            // Leaf items carry their own played state.
 0478            var playedItemIds = context.UserData
 0479                .Where(ud => ud.UserId == userId && ud.Played)
 0480                .Select(ud => ud.ItemId);
 481
 482            // Folders (Series, Seasons, BoxSets, albums, ...) have none and count as played once no
 483            // descendant is left unplayed, matching what the DTO reports for them. This has to key off
 484            // the item itself rather than off the requested item types: tag and collection listings mix
 485            // folders and leaf items in a single query.
 0486            var unplayedLeafItems = GetAccessFilteredLeafItemsQuery(context, filter.User!)
 0487                .Where(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played));
 488
 0489            var isPlayedFilter = IsFolderFilter.And(BuildHasDescendantFilter(context, unplayedLeafItems).Not())
 0490                .Or(IsFolderFilter.Not().And(e => playedItemIds.Contains(e.Id)));
 491
 0492            baseQuery = baseQuery.Where(filter.IsPlayed.Value ? isPlayedFilter : isPlayedFilter.Not());
 493        }
 494
 505495        if (filter.IsResumable.HasValue)
 496        {
 1497            var userId = filter.User!.Id;
 1498            var isResumable = filter.IsResumable.Value;
 499
 500            // In-progress user data rows; alternate versions track their own progress.
 1501            var inProgress = context.UserData
 1502                .Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0);
 503
 504            // Series and Seasons are resumable when a descendant is in progress, or when they hold both
 505            // played and unplayed descendants (partially watched). Alternate versions keep their own
 506            // progress, so they count towards the in-progress check but not towards the played/unplayed one.
 1507            var leafItems = GetAccessFilteredLeafItemsQuery(context, filter.User!);
 1508            var inProgressLeafItems = GetAccessFilteredLeafItemsQuery(context, filter.User!, includeOwnedItems: true)
 1509                .Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0));
 510
 511            // Every other folder kind is a container rather than one continuous piece of media
 1512            var resumableFolderTypes = _resumableFolderKinds
 1513                .Select(kind => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(kind))
 1514                .ToArray();
 1515            var folderIsResumableFilter = IsFolderFilter.And(e => resumableFolderTypes.Contains(e.Type))
 1516                .And(BuildHasDescendantFilter(context, inProgressLeafItems)
 1517                    .Or(BuildHasDescendantFilter(context, leafItems.Where(e => e.UserData!.Any(ud => ud.UserId == userId
 1518                        .And(BuildHasDescendantFilter(context, leafItems.Where(e => !e.UserData!.Any(ud => ud.UserId == 
 519
 1520            if (isResumable)
 521            {
 522                // Resume queries surface the version that was actually played, which may be an alternate.
 523                // Match each version on its own progress rather than coalescing onto the primary.
 1524                var inProgressIds = inProgress.Select(ud => ud.ItemId);
 525
 1526                baseQuery = baseQuery.Where(folderIsResumableFilter
 1527                    .Or(IsFolderFilter.Not().And(e => inProgressIds.Contains(e.Id))));
 528
 529                // When several versions of the same item are in progress, keep only the most recently played one, use i
 530                // Only in-progress siblings can eliminate a candidate: a version without progress has a NULL max LastPl
 531                // which is never greater and never ties. Restricting the sibling scan to the in-progress set keeps this
 532                // the user's Continue Watching count instead of forcing a full BaseItems scan (COALESCE keys are non-in
 533                // Items in no version group at all have no sibling that could eliminate them, so short-circuit the scan
 1534                baseQuery = baseQuery.Where(e => e.IsFolder
 1535                    || (e.PrimaryVersionId == null && !context.BaseItems.Any(a => a.PrimaryVersionId == e.Id))
 1536                    || !context.BaseItems
 1537                        .Where(s => s.Id != e.Id
 1538                            && inProgressIds.Contains(s.Id)
 1539                            && (s.PrimaryVersionId ?? s.Id) == (e.PrimaryVersionId ?? e.Id))
 1540                        .Any(s =>
 1541                            inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
 1542                                > inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
 1543                            || (inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
 1544                                    == inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
 1545                                && s.Id.CompareTo(e.Id) < 0)));
 546            }
 547            else
 548            {
 549                // Not-resumable queries operate on primaries only.
 0550                var resumableMovieIds = inProgress
 0551                    .Join(context.BaseItems, ud => ud.ItemId, bi => bi.Id, (ud, bi) => bi.PrimaryVersionId ?? bi.Id);
 552
 0553                baseQuery = baseQuery.Where(IsFolderFilter.And(folderIsResumableFilter.Not())
 0554                    .Or(IsFolderFilter.Not().And(e => !resumableMovieIds.Contains(e.Id))));
 555            }
 556        }
 557
 505558        if (filter.ArtistIds.Length > 0)
 559        {
 0560            baseQuery = baseQuery.WhereReferencedItem(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter
 561        }
 562
 505563        if (filter.AlbumArtistIds.Length > 0)
 564        {
 0565            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 566        }
 567
 505568        if (filter.ContributingArtistIds.Length > 0)
 569        {
 0570            var contributingNames = context.BaseItems
 0571                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 0572                .Select(b => b.CleanName);
 573
 0574            baseQuery = baseQuery.Where(e =>
 0575                e.ItemValues!.Any(ivm =>
 0576                    ivm.ItemValue.Type == ItemValueType.Artist &&
 0577                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 0578                &&
 0579                !e.ItemValues!.Any(ivm =>
 0580                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 0581                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 582        }
 583
 505584        if (filter.AlbumIds.Length > 0)
 585        {
 0586            baseQuery = baseQuery.Where(e => e.ParentId.HasValue && filter.AlbumIds.Contains(e.ParentId.Value));
 587        }
 588
 505589        if (filter.ExcludeArtistIds.Length > 0)
 590        {
 0591            baseQuery = baseQuery.WhereReferencedItem(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter
 592        }
 593
 505594        if (filter.GenreIds.Count > 0)
 595        {
 0596            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds);
 597        }
 598
 505599        if (filter.Genres.Count > 0)
 600        {
 0601            var cleanGenres = filter.Genres.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValu
 0602            baseQuery = baseQuery
 0603                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 604        }
 605
 505606        if (tags.Count > 0)
 607        {
 0608            var cleanValues = tags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, str
 0609            baseQuery = baseQuery
 0610                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 611        }
 612
 505613        if (excludeTags.Count > 0)
 614        {
 0615            var cleanValues = excludeTags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueM
 0616            baseQuery = baseQuery
 0617                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 618        }
 619
 505620        if (filter.StudioIds.Length > 0)
 621        {
 0622            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds);
 623        }
 624
 505625        if (filter.OfficialRatings.Length > 0)
 626        {
 0627            var ratings = filter.OfficialRatings;
 0628            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, e => ratings.Contains(e.OfficialRating));
 629        }
 630
 505631        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 505632        if (filter.MinParentalRating != null)
 633        {
 0634            var min = filter.MinParentalRating;
 0635            var minScore = min.Score;
 0636            var minSubScore = min.SubScore ?? 0;
 637
 0638            minParentalRatingFilter = e =>
 0639                e.InheritedParentalRatingValue == null ||
 0640                e.InheritedParentalRatingValue > minScore ||
 0641                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 642        }
 643
 505644        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 505645        if (filter.MaxParentalRating != null)
 646        {
 54647            maxParentalRatingFilter = BuildMaxParentalRatingFilter(context, filter.MaxParentalRating);
 648        }
 649
 505650        if (filter.HasParentalRating ?? false)
 651        {
 0652            if (minParentalRatingFilter != null)
 653            {
 0654                baseQuery = baseQuery.Where(minParentalRatingFilter);
 655            }
 656
 0657            if (maxParentalRatingFilter != null)
 658            {
 0659                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 660            }
 661        }
 505662        else if (filter.BlockUnratedItems.Length > 0)
 663        {
 0664            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 0665            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 666
 0667            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 668            {
 0669                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 670            }
 0671            else if (minParentalRatingFilter != null)
 672            {
 0673                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 674            }
 0675            else if (maxParentalRatingFilter != null)
 676            {
 0677                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 678            }
 679            else
 680            {
 0681                baseQuery = baseQuery.Where(unratedItemFilter);
 682            }
 683        }
 505684        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 685        {
 54686            if (minParentalRatingFilter != null)
 687            {
 0688                baseQuery = baseQuery.Where(minParentalRatingFilter);
 689            }
 690
 54691            if (maxParentalRatingFilter != null)
 692            {
 54693                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 694            }
 695        }
 451696        else if (!filter.HasParentalRating ?? false)
 697        {
 0698            baseQuery = baseQuery
 0699                .Where(e => e.InheritedParentalRatingValue == null);
 700        }
 701
 505702        if (filter.HasOfficialRating.HasValue)
 703        {
 0704            Expression<Func<BaseItemEntity, bool>> hasRating =
 0705                e => e.OfficialRating != null && e.OfficialRating != string.Empty;
 706
 0707            baseQuery = filter.HasOfficialRating.Value
 0708                ? baseQuery.WhereItemOrDescendantMatches(context, hasRating)
 0709                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasRating);
 710        }
 711
 505712        if (filter.HasOverview.HasValue)
 713        {
 0714            if (filter.HasOverview.Value)
 715            {
 0716                baseQuery = baseQuery
 0717                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 718            }
 719            else
 720            {
 0721                baseQuery = baseQuery
 0722                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 723            }
 724        }
 725
 505726        if (filter.HasOwnerId.HasValue)
 727        {
 0728            if (filter.HasOwnerId.Value)
 729            {
 0730                baseQuery = baseQuery
 0731                    .Where(e => e.OwnerId != null);
 732            }
 733            else
 734            {
 0735                baseQuery = baseQuery
 0736                    .Where(e => e.OwnerId == null);
 737            }
 738        }
 505739        else if (filter.OwnerIds.Length == 0 && filter.ExtraTypes.Length == 0 && !filter.IncludeOwnedItems)
 740        {
 741            // Exclude owned non-extra items from general queries.
 742            // Extras (trailers, etc.) have OwnerId set but also have ExtraType set - keep those.
 743            // Alternate versions (PrimaryVersionId set) are normally excluded too, but resume queries
 744            // keep them so the actually-played version can surface instead of collapsing onto the primary.
 474745            baseQuery = filter.IsResumable == true
 474746                ? baseQuery.Where(e => e.OwnerId == null || e.ExtraType != null)
 474747                : baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));
 748        }
 749
 505750        if (filter.OwnerIds.Length > 0)
 751        {
 6752            baseQuery = baseQuery.Where(e => e.OwnerId != null && filter.OwnerIds.Contains(e.OwnerId.Value));
 753        }
 754
 505755        if (filter.ExtraTypes.Length > 0)
 756        {
 757            // Convert ExtraType enum to BaseItemExtraType enum via int cast (same underlying values)
 0758            var extraTypeValues = filter.ExtraTypes.Select(e => (BaseItemExtraType?)(int)e).ToArray();
 0759            baseQuery = baseQuery.Where(e => e.ExtraType != null && extraTypeValues.Contains(e.ExtraType));
 760        }
 761
 505762        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 763        {
 0764            var lang = filter.HasNoAudioTrackWithLanguage;
 0765            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 766
 0767            baseQuery = baseQuery
 0768                .Where(e =>
 0769                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Audio && ms.Langua
 0770                    || (e.IsFolder && !foldersWithAudio.Contains(e.Id)));
 771        }
 772
 505773        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 774        {
 0775            var lang = filter.HasNoInternalSubtitleTrackWithLanguage;
 0776            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 777
 0778            baseQuery = baseQuery
 0779                .Where(e =>
 0780                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && !ms.Is
 0781                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 782        }
 783
 505784        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 785        {
 0786            var lang = filter.HasNoExternalSubtitleTrackWithLanguage;
 0787            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 788
 0789            baseQuery = baseQuery
 0790                .Where(e =>
 0791                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.IsE
 0792                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 793        }
 794
 505795        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 796        {
 0797            var lang = filter.HasNoSubtitleTrackWithLanguage;
 0798            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 799
 0800            baseQuery = baseQuery
 0801                .Where(e =>
 0802                    (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.Lan
 0803                    || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 804        }
 805
 505806        if (filter.HasSubtitles.HasValue)
 807        {
 0808            var hasSubtitles = filter.HasSubtitles.Value;
 0809            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasSubtitles());
 0810            if (hasSubtitles)
 811            {
 0812                baseQuery = baseQuery
 0813                    .Where(e =>
 0814                        (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0815                        || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 816            }
 817            else
 818            {
 0819                baseQuery = baseQuery
 0820                    .Where(e =>
 0821                        (!e.IsFolder && !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle))
 0822                        || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id)));
 823            }
 824        }
 825
 505826        if (filter.SubtitleLanguages.Count > 0)
 827        {
 0828            var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaS
 0829            baseQuery = baseQuery
 0830                .Where(e =>
 0831                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle
 0832                     && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && s
 0833                    || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
 834        }
 835
 505836        if (filter.AudioLanguages.Count > 0)
 837        {
 0838            var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStrea
 0839            baseQuery = baseQuery
 0840                .Where(e =>
 0841                    (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio
 0842                     && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.
 0843                    || (e.IsFolder && foldersWithAudio.Contains(e.Id)));
 844        }
 845
 505846        if (filter.HasChapterImages.HasValue)
 847        {
 0848            var hasChapterImages = filter.HasChapterImages.Value;
 0849            var foldersWithChapterImages = DescendantQueryHelper.GetFolderIdsMatching(context, new HasChapterImages());
 0850            if (hasChapterImages)
 851            {
 0852                baseQuery = baseQuery
 0853                    .Where(e =>
 0854                        (!e.IsFolder && e.Chapters!.Any(f => f.ImagePath != null))
 0855                        || (e.IsFolder && foldersWithChapterImages.Contains(e.Id)));
 856            }
 857            else
 858            {
 0859                baseQuery = baseQuery
 0860                    .Where(e =>
 0861                        (!e.IsFolder && !e.Chapters!.Any(f => f.ImagePath != null))
 0862                        || (e.IsFolder && !foldersWithChapterImages.Contains(e.Id)));
 863            }
 864        }
 865
 505866        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 867        {
 18868            baseQuery = baseQuery
 18869                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 870        }
 871
 505872        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 873        {
 18874            baseQuery = baseQuery
 18875                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 876        }
 877
 505878        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 879        {
 18880            baseQuery = baseQuery
 18881                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 882        }
 883
 505884        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 885        {
 18886            baseQuery = baseQuery
 18887                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 888        }
 889
 505890        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 891        {
 0892            baseQuery = baseQuery
 0893                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 894        }
 895
 505896        if (filter.Years.Length > 0)
 897        {
 0898            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 899        }
 900
 505901        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 505902        if (isVirtualItem.HasValue)
 903        {
 23904            baseQuery = baseQuery
 23905                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 906        }
 907
 505908        if (filter.IsSpecialSeason.HasValue)
 909        {
 0910            if (filter.IsSpecialSeason.Value)
 911            {
 0912                baseQuery = baseQuery
 0913                    .Where(e => e.IndexNumber == 0);
 914            }
 915            else
 916            {
 0917                baseQuery = baseQuery
 0918                    .Where(e => e.IndexNumber != 0);
 919            }
 920        }
 921
 505922        if (filter.IsUnaired.HasValue)
 923        {
 0924            if (filter.IsUnaired.Value)
 925            {
 0926                baseQuery = baseQuery
 0927                    .Where(e => e.PremiereDate >= now);
 928            }
 929            else
 930            {
 0931                baseQuery = baseQuery
 0932                    .Where(e => e.PremiereDate < now);
 933            }
 934        }
 935
 505936        if (filter.MediaTypes.Length > 0)
 937        {
 22938            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 22939            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 940        }
 941
 505942        if (filter.ItemIds.Length > 0)
 943        {
 0944            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 945        }
 946
 505947        if (filter.ExcludeItemIds.Length > 0)
 948        {
 0949            baseQuery = baseQuery
 0950                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 951        }
 952
 505953        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 954        {
 0955            baseQuery = baseQuery.WhereExcludeProviderIds(filter.ExcludeProviderIds);
 956        }
 957
 505958        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 959        {
 0960            baseQuery = baseQuery.WhereHasAnyProviderId(filter.HasAnyProviderId);
 961        }
 962
 505963        if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
 964        {
 0965            baseQuery = baseQuery.WhereHasAnyProviderIds(filter.HasAnyProviderIds);
 966        }
 967
 505968        if (filter.HasImdbId.HasValue)
 969        {
 0970            baseQuery = filter.HasImdbId.Value
 0971                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == ImdbProviderName))
 0972                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != ImdbProviderName));
 973        }
 974
 505975        if (filter.HasTmdbId.HasValue)
 976        {
 0977            baseQuery = filter.HasTmdbId.Value
 0978                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TmdbProviderName))
 0979                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TmdbProviderName));
 980        }
 981
 505982        if (filter.HasTvdbId.HasValue)
 983        {
 0984            baseQuery = filter.HasTvdbId.Value
 0985                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == TvdbProviderName))
 0986                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != TvdbProviderName));
 987        }
 988
 505989        baseQuery = ApplyTopParentFiltering(context, baseQuery, filter);
 990
 505991        if (filter.AncestorIds.Length > 0)
 992        {
 45993            var ancestorFilter = filter.AncestorIds.OneOrManyExpressionBuilder<AncestorId, Guid>(f => f.ParentItemId);
 45994            baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
 995        }
 996
 505997        if (filter.LinkedChildAncestorIds.Length > 0)
 998        {
 999            // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ances
 01000            var linkedChildAncestorIds = filter.LinkedChildAncestorIds;
 01001            baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc =>
 01002                lc.ParentId == e.Id
 01003                && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId))));
 1004        }
 1005
 5051006        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 1007        {
 01008            baseQuery = baseQuery
 01009                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 1010        }
 1011
 5051012        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 1013        {
 01014            baseQuery = baseQuery
 01015                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 1016        }
 1017
 1018        // Pre-build the blocked-item-id set as a sub-select
 5051019        if (filter.ExcludeInheritedTags.Length > 0)
 1020        {
 01021            var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01022            var blockedTagItemIds = context.ItemValuesMap
 01023                .Where(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
 01024                .Select(f => f.ItemId);
 1025
 01026            baseQuery = baseQuery.Where(e =>
 01027                !blockedTagItemIds.Contains(e.Id)
 01028                && !(e.SeriesId.HasValue && blockedTagItemIds.Contains(e.SeriesId.Value))
 01029                && !e.Parents!.Any(p => blockedTagItemIds.Contains(p.ParentItemId))
 01030                && !(e.TopParentId.HasValue && blockedTagItemIds.Contains(e.TopParentId.Value)));
 1031        }
 1032
 5051033        if (filter.IncludeInheritedTags.Length > 0)
 1034        {
 01035            var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
 01036            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 01037            var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
 01038            var allowedTagItemIds = context.ItemValuesMap
 01039                .Where(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
 01040                .Select(f => f.ItemId);
 1041
 01042            baseQuery = baseQuery.Where(e =>
 01043                allowedTagItemIds.Contains(e.Id)
 01044                || (e.SeriesId.HasValue && allowedTagItemIds.Contains(e.SeriesId.Value))
 01045                || e.Parents!.Any(p => allowedTagItemIds.Contains(p.ParentItemId))
 01046                || (e.TopParentId.HasValue && allowedTagItemIds.Contains(e.TopParentId.Value))
 01047
 01048                // People don't carry the tags of the media they appear in and would never match
 01049                || e.Type == personTypeName
 01050
 01051                // A playlist should be accessible to its owner regardless of allowed tags
 01052                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 1053        }
 1054
 5051055        if (filter.SeriesStatuses.Length > 0)
 1056        {
 01057            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 01058            baseQuery = baseQuery
 01059                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 1060        }
 1061
 5051062        if (filter.BoxSetLibraryFolders.Length > 0)
 1063        {
 01064            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 01065            baseQuery = baseQuery
 01066                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 1067        }
 1068
 5051069        if (filter.VideoTypes.Length > 0)
 1070        {
 1071            // Dvds and Blu-rays can either be stored in a folder structure or as an iso file
 1072            // => to find all matches we need to check both: VideoType and IsoType
 1073            // alternatively, we could provide specific IsoType filters
 01074            var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
 01075            var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray();
 01076            Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoT
 01077            baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
 1078        }
 1079
 5051080        if (filter.Is3D.HasValue)
 1081        {
 01082            Expression<Func<BaseItemEntity, bool>> is3D = e => e.Data!.Contains("Video3DFormat");
 1083
 01084            baseQuery = filter.Is3D.Value
 01085                ? baseQuery.WhereItemOrDescendantMatches(context, is3D)
 01086                : baseQuery.WhereNeitherItemNorDescendantMatches(context, is3D);
 1087        }
 1088
 5051089        if (filter.IsPlaceHolder.HasValue)
 1090        {
 01091            Expression<Func<BaseItemEntity, bool>> isPlaceHolder = e => e.Data!.Contains("IsPlaceHolder\":true");
 1092
 01093            baseQuery = filter.IsPlaceHolder.Value
 01094                ? baseQuery.WhereItemOrDescendantMatches(context, isPlaceHolder)
 01095                : baseQuery.WhereNeitherItemNorDescendantMatches(context, isPlaceHolder);
 1096        }
 1097
 1098        // An extra is owned by the single version of an item it is named after, so an extra on any
 1099        // version counts for the item itself
 1100        IQueryable<Guid> WithPrimaryVersions(IQueryable<Guid> ownerIds)
 1101            => ownerIds.Concat(context.BaseItems
 1102                .Where(version => version.PrimaryVersionId != null && ownerIds.Contains(version.Id))
 1103                .Select(version => version.PrimaryVersionId!.Value));
 1104
 5051105        if (filter.HasSpecialFeature.HasValue)
 1106        {
 01107            var itemsWithExtras = WithPrimaryVersions(context.BaseItems
 01108                .Where(extra => extra.OwnerId != null
 01109                    && extra.ExtraType != null
 01110                    && extra.ExtraType != BaseItemExtraType.Unknown
 01111                    && extra.ExtraType != BaseItemExtraType.Trailer
 01112                    && extra.ExtraType != BaseItemExtraType.ThemeSong
 01113                    && extra.ExtraType != BaseItemExtraType.ThemeVideo)
 01114                .Select(extra => extra.OwnerId!.Value))
 01115                .Distinct();
 1116
 01117            Expression<Func<BaseItemEntity, bool>> hasExtras = e => itemsWithExtras.Contains(e.Id);
 1118
 01119            baseQuery = filter.HasSpecialFeature.Value
 01120                ? baseQuery.WhereItemOrDescendantMatches(context, hasExtras)
 01121                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasExtras);
 1122        }
 1123
 5051124        if (filter.HasTrailer.HasValue)
 1125        {
 01126            var trailerOwnerIds = WithPrimaryVersions(context.BaseItems
 01127                .Where(extra => extra.ExtraType == BaseItemExtraType.Trailer && extra.OwnerId != null)
 01128                .Select(extra => extra.OwnerId!.Value));
 1129
 01130            Expression<Func<BaseItemEntity, bool>> hasTrailer = e => trailerOwnerIds.Contains(e.Id);
 1131
 01132            baseQuery = filter.HasTrailer.Value
 01133                ? baseQuery.WhereItemOrDescendantMatches(context, hasTrailer)
 01134                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasTrailer);
 1135        }
 1136
 5051137        if (filter.HasThemeSong.HasValue)
 1138        {
 01139            var themeSongOwnerIds = WithPrimaryVersions(context.BaseItems
 01140                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeSong && extra.OwnerId != null)
 01141                .Select(extra => extra.OwnerId!.Value));
 1142
 01143            Expression<Func<BaseItemEntity, bool>> hasThemeSong = e => themeSongOwnerIds.Contains(e.Id);
 1144
 01145            baseQuery = filter.HasThemeSong.Value
 01146                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeSong)
 01147                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeSong);
 1148        }
 1149
 5051150        if (filter.HasThemeVideo.HasValue)
 1151        {
 01152            var themeVideoOwnerIds = WithPrimaryVersions(context.BaseItems
 01153                .Where(extra => extra.ExtraType == BaseItemExtraType.ThemeVideo && extra.OwnerId != null)
 01154                .Select(extra => extra.OwnerId!.Value));
 1155
 01156            Expression<Func<BaseItemEntity, bool>> hasThemeVideo = e => themeVideoOwnerIds.Contains(e.Id);
 1157
 01158            baseQuery = filter.HasThemeVideo.Value
 01159                ? baseQuery.WhereItemOrDescendantMatches(context, hasThemeVideo)
 01160                : baseQuery.WhereNeitherItemNorDescendantMatches(context, hasThemeVideo);
 1161        }
 1162
 5051163        if (filter.AiredDuringSeason.HasValue)
 1164        {
 01165            var seasonNumber = filter.AiredDuringSeason.Value;
 01166            if (seasonNumber < 1)
 1167            {
 01168                baseQuery = baseQuery.Where(e => e.ParentIndexNumber == seasonNumber);
 1169            }
 1170            else
 1171            {
 01172                var seasonStr = seasonNumber.ToString(CultureInfo.InvariantCulture);
 01173                baseQuery = baseQuery.Where(e =>
 01174                    e.ParentIndexNumber == seasonNumber
 01175                    || (e.Data != null && (
 01176                        e.Data.Contains("\"AirsAfterSeasonNumber\":" + seasonStr)
 01177                        || e.Data.Contains("\"AirsBeforeSeasonNumber\":" + seasonStr))));
 1178            }
 1179        }
 1180
 5051181        return baseQuery;
 1182    }
 1183}

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)
ApplyAdjacencyFilter(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyQueryPaging(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyGroupingFilter(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyBoxSetCollapsing(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Data.Enums.BaseItemKind[])
ApplyBoxSetCollapsingAll(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<System.Guid>,System.String)
ApplyNameFilters(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyNavigations(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyOrder(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.JellyfinDbContext)
ApplySeriesDatePlayedOrder(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.JellyfinDbContext,System.ValueTuple`2<Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Database.Implementations.Enums.SortOrder>[])
BuildAccessFilteredDescendantsQuery(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Guid)
RequiresParentalRestrictions(MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyAccessFiltering(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyTopParentFiltering(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetExemptedItemByNameTypes(MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyItemByNameAccessFiltering(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Guid[])
ApplyParentalRestrictions(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
BuildMaxParentalRatingFilter(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Model.Entities.ParentalRatingScore)
GetAccessFilteredLeafItemsQuery(Jellyfin.Database.Implementations.JellyfinDbContext,Jellyfin.Database.Implementations.Entities.User,System.Boolean)
BuildHasDescendantFilter(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>)
GetItemIdsList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItems(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetLatestItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Data.Enums.CollectionType)
LoadLatestByIds(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<System.Guid>,MediaBrowser.Controller.Entities.InternalItemsQuery)
LoadLatestByIds(Jellyfin.Database.Implementations.JellyfinDbContext,System.Collections.Generic.List`1<System.Guid>,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetLatestTvShowItems(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery,System.Nullable`1<System.Int32>)
ItemExistsAsync()
RetrieveItem(System.Guid)
GetIsPlayed(Jellyfin.Database.Implementations.Entities.User,System.Guid,System.Boolean)
GetQueryFiltersLegacy(MediaBrowser.Controller.Entities.InternalItemsQuery)
.cctor()
get_IsFolderFilter()
TranslateQuery(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)