< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Item.BaseItemRepository
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
Line coverage
51%
Covered lines: 689
Uncovered lines: 649
Coverable lines: 1338
Total lines: 2735
Line coverage: 51.4%
Branch coverage
50%
Covered branches: 386
Total branches: 764
Branch coverage: 50.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 11/15/2025 - 12:10:12 AM Line coverage: 50.9% (666/1306) Branch coverage: 48.9% (356/728) Total lines: 260711/18/2025 - 12:11:25 AM Line coverage: 50.9% (667/1309) Branch coverage: 49% (356/726) Total lines: 260711/29/2025 - 12:11:22 AM Line coverage: 50.9% (667/1309) Branch coverage: 49.1% (357/726) Total lines: 260711/30/2025 - 12:09:51 AM Line coverage: 50.9% (667/1309) Branch coverage: 49% (356/726) Total lines: 260712/4/2025 - 12:11:49 AM Line coverage: 50.9% (668/1312) Branch coverage: 49% (358/730) Total lines: 261212/9/2025 - 12:12:43 AM Line coverage: 51.5% (683/1326) Branch coverage: 49.7% (368/740) Total lines: 265212/10/2025 - 12:13:43 AM Line coverage: 51.5% (681/1322) Branch coverage: 49.3% (369/748) Total lines: 264212/14/2025 - 12:12:01 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/20/2025 - 12:12:47 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.7% (371/746) Total lines: 265412/21/2025 - 12:12:15 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/29/2025 - 12:13:19 AM Line coverage: 51.9% (685/1318) Branch coverage: 50% (378/756) Total lines: 26761/10/2026 - 12:12:36 AM Line coverage: 52.3% (692/1322) Branch coverage: 50.6% (383/756) Total lines: 26821/11/2026 - 12:11:48 AM Line coverage: 52.3% (692/1322) Branch coverage: 50.9% (383/752) Total lines: 26711/19/2026 - 12:13:54 AM Line coverage: 51.8% (684/1318) Branch coverage: 50.6% (383/756) Total lines: 26871/29/2026 - 12:13:32 AM Line coverage: 51.4% (689/1338) Branch coverage: 50.5% (386/764) Total lines: 2735 11/15/2025 - 12:10:12 AM Line coverage: 50.9% (666/1306) Branch coverage: 48.9% (356/728) Total lines: 260711/18/2025 - 12:11:25 AM Line coverage: 50.9% (667/1309) Branch coverage: 49% (356/726) Total lines: 260711/29/2025 - 12:11:22 AM Line coverage: 50.9% (667/1309) Branch coverage: 49.1% (357/726) Total lines: 260711/30/2025 - 12:09:51 AM Line coverage: 50.9% (667/1309) Branch coverage: 49% (356/726) Total lines: 260712/4/2025 - 12:11:49 AM Line coverage: 50.9% (668/1312) Branch coverage: 49% (358/730) Total lines: 261212/9/2025 - 12:12:43 AM Line coverage: 51.5% (683/1326) Branch coverage: 49.7% (368/740) Total lines: 265212/10/2025 - 12:13:43 AM Line coverage: 51.5% (681/1322) Branch coverage: 49.3% (369/748) Total lines: 264212/14/2025 - 12:12:01 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/20/2025 - 12:12:47 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.7% (371/746) Total lines: 265412/21/2025 - 12:12:15 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/29/2025 - 12:13:19 AM Line coverage: 51.9% (685/1318) Branch coverage: 50% (378/756) Total lines: 26761/10/2026 - 12:12:36 AM Line coverage: 52.3% (692/1322) Branch coverage: 50.6% (383/756) Total lines: 26821/11/2026 - 12:11:48 AM Line coverage: 52.3% (692/1322) Branch coverage: 50.9% (383/752) Total lines: 26711/19/2026 - 12:13:54 AM Line coverage: 51.8% (684/1318) Branch coverage: 50.6% (383/756) Total lines: 26871/29/2026 - 12:13:32 AM Line coverage: 51.4% (689/1338) Branch coverage: 50.5% (386/764) Total lines: 2735

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
DeleteItem(...)50%6697.56%
UpdateInheritedValues()100%11100%
GetItemIdsList(...)100%11100%
GetAllArtists(...)100%210%
GetArtists(...)100%210%
GetAlbumArtists(...)100%210%
GetStudios(...)100%210%
GetGenres(...)100%210%
GetMusicGenres(...)100%210%
GetStudioNames()100%11100%
GetAllArtistNames()100%11100%
GetMusicGenreNames()100%11100%
GetGenreNames()100%11100%
GetItems(...)50%27833.33%
GetItemList(...)75%4471.42%
GetLatestItemList(...)0%7280%
GetNextUpSeriesKeys(...)0%2040%
ApplyGroupingFilter(...)75%10869.23%
ApplyNavigations(...)83.33%121290.9%
ApplyQueryPaging(...)75%9880%
ApplyQueryFilter(...)100%11100%
PrepareItemQuery(...)100%11100%
GetCount(...)100%210%
GetItemCounts(...)0%420200%
GetType(...)100%11100%
SaveItems(...)100%11100%
UpdateOrInsertItems(...)82.35%363487.09%
RetrieveItem(...)50%4490%
Map(...)50%1428279.31%
Map(...)54.83%956279.46%
GetItemValueNames(...)100%44100%
TypeRequiresDeserialization(...)100%11100%
DeserializeBaseItem(...)50%101088.88%
DeserializeBaseItem(...)83.33%161270.58%
GetItemValues(...)0%272160%
PrepareFilterQuery(...)87.5%9880%
GetCleanValue(...)91.66%121294.11%
GetItemValuesToSave(...)50%4481.81%
Map(...)0%620%
Map(...)0%7280%
GetPathToSave(...)50%2266.66%
GetItemByNameTypesInQuery(...)100%1010100%
IsTypeInQuery(...)75%5466.66%
EnableGroupByPresentationUniqueKey(...)65%212087.5%
ApplyOrder(...)56.66%573068.96%
TranslateQuery(...)45.11%3064434836.99%
GetIsPlayed(...)0%620%
TraverseHirachyDown(...)50%8884.21%
FindArtists(...)0%2040%

File(s)

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2// Do not enforce that because EFCore cannot deal with cultures well.
 3#pragma warning disable CA1304 // Specify CultureInfo
 4#pragma warning disable CA1311 // Specify a culture or use an invariant version
 5#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 6
 7using System;
 8using System.Collections.Concurrent;
 9using System.Collections.Generic;
 10using System.Globalization;
 11using System.Linq;
 12using System.Linq.Expressions;
 13using System.Reflection;
 14using System.Text;
 15using System.Text.Json;
 16using System.Threading;
 17using System.Threading.Tasks;
 18using Jellyfin.Data.Enums;
 19using Jellyfin.Database.Implementations;
 20using Jellyfin.Database.Implementations.Entities;
 21using Jellyfin.Database.Implementations.Enums;
 22using Jellyfin.Extensions;
 23using Jellyfin.Extensions.Json;
 24using Jellyfin.Server.Implementations.Extensions;
 25using MediaBrowser.Common;
 26using MediaBrowser.Controller;
 27using MediaBrowser.Controller.Channels;
 28using MediaBrowser.Controller.Configuration;
 29using MediaBrowser.Controller.Entities;
 30using MediaBrowser.Controller.Entities.Audio;
 31using MediaBrowser.Controller.Entities.TV;
 32using MediaBrowser.Controller.LiveTv;
 33using MediaBrowser.Controller.Persistence;
 34using MediaBrowser.Model.Dto;
 35using MediaBrowser.Model.Entities;
 36using MediaBrowser.Model.LiveTv;
 37using MediaBrowser.Model.Querying;
 38using Microsoft.EntityFrameworkCore;
 39using Microsoft.Extensions.Logging;
 40using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 41using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 42
 43namespace Jellyfin.Server.Implementations.Item;
 44
 45/*
 46    All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null
 47    This is done as the code isn't actually executed client side, but only the expressions are interpret and the compile
 48    This is your only warning/message regarding this topic.
 49*/
 50
 51/// <summary>
 52/// Handles all storage logic for BaseItems.
 53/// </summary>
 54public sealed class BaseItemRepository
 55    : IItemRepository
 56{
 57    /// <summary>
 58    /// Gets the placeholder id for UserData detached items.
 59    /// </summary>
 260    public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
 61
 62    /// <summary>
 63    /// This holds all the types in the running assemblies
 64    /// so that we can de-serialize properly when we don't have strong types.
 65    /// </summary>
 266    private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
 67    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 68    private readonly IServerApplicationHost _appHost;
 69    private readonly IItemTypeLookup _itemTypeLookup;
 70    private readonly IServerConfigurationManager _serverConfigurationManager;
 71    private readonly ILogger<BaseItemRepository> _logger;
 72
 273    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 274    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 275    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 276    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 277    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 278    private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
 79
 80    /// <summary>
 81    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
 82    /// </summary>
 83    /// <param name="dbProvider">The db factory.</param>
 84    /// <param name="appHost">The Application host.</param>
 85    /// <param name="itemTypeLookup">The static type lookup.</param>
 86    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
 87    /// <param name="logger">System logger.</param>
 88    public BaseItemRepository(
 89        IDbContextFactory<JellyfinDbContext> dbProvider,
 90        IServerApplicationHost appHost,
 91        IItemTypeLookup itemTypeLookup,
 92        IServerConfigurationManager serverConfigurationManager,
 93        ILogger<BaseItemRepository> logger)
 94    {
 3195        _dbProvider = dbProvider;
 3196        _appHost = appHost;
 3197        _itemTypeLookup = itemTypeLookup;
 3198        _serverConfigurationManager = serverConfigurationManager;
 3199        _logger = logger;
 31100    }
 101
 102    /// <inheritdoc />
 103    public void DeleteItem(params IReadOnlyList<Guid> ids)
 104    {
 1105        if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
 106        {
 0107            throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
 108        }
 109
 1110        using var context = _dbProvider.CreateDbContext();
 1111        using var transaction = context.Database.BeginTransaction();
 112
 1113        var date = (DateTime?)DateTime.UtcNow;
 114
 1115        var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
 116
 117        // Remove any UserData entries for the placeholder item that would conflict with the UserData
 118        // being detached from the item being deleted. This is necessary because, during an update,
 119        // UserData may be reattached to a new entry, but some entries can be left behind.
 120        // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
 1121        context.UserData
 1122            .Join(
 1123                context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
 1124                placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
 1125                userData => new { userData.UserId, userData.CustomDataKey },
 1126                (placeholder, userData) => placeholder)
 1127            .Where(e => e.ItemId == PlaceholderId)
 1128            .ExecuteDelete();
 129
 130        // Detach all user watch data
 1131        context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
 1132            .ExecuteUpdate(e => e
 1133                .SetProperty(f => f.RetentionDate, date)
 1134                .SetProperty(f => f.ItemId, PlaceholderId));
 135
 1136        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1137        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
 1138        context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1139        context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1140        context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1141        context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1142        context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1143        context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
 1144        context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1145        context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1146        context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1147        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
 1148        context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1149        context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1150        context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1151        context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1152        var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distin
 1153        context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1154        context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
 1155        context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1156        context.SaveChanges();
 1157        transaction.Commit();
 2158    }
 159
 160    /// <inheritdoc />
 161    public void UpdateInheritedValues()
 162    {
 16163        using var context = _dbProvider.CreateDbContext();
 16164        using var transaction = context.Database.BeginTransaction();
 165
 16166        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 167        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 16168        context.SaveChanges();
 169
 16170        transaction.Commit();
 32171    }
 172
 173    /// <inheritdoc />
 174    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 175    {
 16176        ArgumentNullException.ThrowIfNull(filter);
 16177        PrepareFilterQuery(filter);
 178
 16179        using var context = _dbProvider.CreateDbContext();
 16180        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 16181    }
 182
 183    /// <inheritdoc />
 184    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 185    {
 0186        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 187    }
 188
 189    /// <inheritdoc />
 190    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 191    {
 0192        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 193    }
 194
 195    /// <inheritdoc />
 196    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 197    {
 0198        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 199    }
 200
 201    /// <inheritdoc />
 202    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 203    {
 0204        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 205    }
 206
 207    /// <inheritdoc />
 208    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 209    {
 0210        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 211    }
 212
 213    /// <inheritdoc />
 214    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 215    {
 0216        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 217    }
 218
 219    /// <inheritdoc />
 220    public IReadOnlyList<string> GetStudioNames()
 221    {
 16222        return GetItemValueNames(_getStudiosValueTypes, [], []);
 223    }
 224
 225    /// <inheritdoc />
 226    public IReadOnlyList<string> GetAllArtistNames()
 227    {
 16228        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 229    }
 230
 231    /// <inheritdoc />
 232    public IReadOnlyList<string> GetMusicGenreNames()
 233    {
 16234        return GetItemValueNames(
 16235            _getGenreValueTypes,
 16236            _itemTypeLookup.MusicGenreTypes,
 16237            []);
 238    }
 239
 240    /// <inheritdoc />
 241    public IReadOnlyList<string> GetGenreNames()
 242    {
 16243        return GetItemValueNames(
 16244            _getGenreValueTypes,
 16245            [],
 16246            _itemTypeLookup.MusicGenreTypes);
 247    }
 248
 249    /// <inheritdoc />
 250    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
 251    {
 1252        ArgumentNullException.ThrowIfNull(filter);
 1253        if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0))
 254        {
 1255            var returnList = GetItemList(filter);
 1256            return new QueryResult<BaseItemDto>(
 1257                filter.StartIndex,
 1258                returnList.Count,
 1259                returnList);
 260        }
 261
 0262        PrepareFilterQuery(filter);
 0263        var result = new QueryResult<BaseItemDto>();
 264
 0265        using var context = _dbProvider.CreateDbContext();
 266
 0267        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 268
 0269        dbQuery = TranslateQuery(dbQuery, context, filter);
 0270        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 271
 0272        if (filter.EnableTotalRecordCount)
 273        {
 0274            result.TotalRecordCount = dbQuery.Count();
 275        }
 276
 0277        dbQuery = ApplyQueryPaging(dbQuery, filter);
 0278        dbQuery = ApplyNavigations(dbQuery, filter);
 279
 0280        result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDe
 0281        result.StartIndex = filter.StartIndex ?? 0;
 0282        return result;
 0283    }
 284
 285    /// <inheritdoc />
 286    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
 287    {
 320288        ArgumentNullException.ThrowIfNull(filter);
 320289        PrepareFilterQuery(filter);
 290
 320291        using var context = _dbProvider.CreateDbContext();
 320292        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 293
 320294        dbQuery = TranslateQuery(dbQuery, context, filter);
 295
 320296        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 320297        dbQuery = ApplyQueryPaging(dbQuery, filter);
 298
 320299        var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
 320300        if (hasRandomSort)
 301        {
 62302            var orderedIds = dbQuery.Select(e => e.Id).ToList();
 62303            if (orderedIds.Count == 0)
 304            {
 62305                return Array.Empty<BaseItemDto>();
 306            }
 307
 0308            var itemsById = ApplyNavigations(context.BaseItems.Where(e => orderedIds.Contains(e.Id)), filter)
 0309                .AsEnumerable()
 0310                .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 0311                .Where(dto => dto is not null)
 0312                .ToDictionary(i => i!.Id);
 313
 0314            return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!;
 315        }
 316
 258317        dbQuery = ApplyNavigations(dbQuery, filter);
 318
 258319        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializ
 320320    }
 321
 322    /// <inheritdoc/>
 323    public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 324    {
 0325        ArgumentNullException.ThrowIfNull(filter);
 0326        PrepareFilterQuery(filter);
 327
 328        // Early exit if collection type is not tvshows or music
 0329        if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music)
 330        {
 0331            return Array.Empty<BaseItem>();
 332        }
 333
 0334        using var context = _dbProvider.CreateDbContext();
 335
 336        // Subquery to group by SeriesNames/Album and get the max Date Created for each group.
 0337        var subquery = PrepareItemQuery(context, filter);
 0338        subquery = TranslateQuery(subquery, context, filter);
 0339        var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album)
 0340            .Select(g => new
 0341            {
 0342                Key = g.Key,
 0343                MaxDateCreated = g.Max(a => a.DateCreated)
 0344            })
 0345            .OrderByDescending(g => g.MaxDateCreated)
 0346            .Select(g => g);
 347
 0348        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 349        {
 0350            subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
 351        }
 352
 0353        filter.Limit = null;
 354
 0355        var mainquery = PrepareItemQuery(context, filter);
 0356        mainquery = TranslateQuery(mainquery, context, filter);
 0357        mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
 0358        mainquery = ApplyGroupingFilter(context, mainquery, filter);
 0359        mainquery = ApplyQueryPaging(mainquery, filter);
 360
 0361        mainquery = ApplyNavigations(mainquery, filter);
 362
 0363        return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserial
 0364    }
 365
 366    /// <inheritdoc />
 367    public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
 368    {
 0369        ArgumentNullException.ThrowIfNull(filter);
 0370        ArgumentNullException.ThrowIfNull(filter.User);
 371
 0372        using var context = _dbProvider.CreateDbContext();
 373
 0374        var query = context.BaseItems
 0375            .AsNoTracking()
 0376            .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
 0377            .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
 0378            .Join(
 0379                context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
 0380                i => new { UserId = filter.User.Id, ItemId = i.Id },
 0381                u => new { UserId = u.UserId, ItemId = u.ItemId },
 0382                (entity, data) => new { Item = entity, UserData = data })
 0383            .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
 0384            .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
 0385            .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
 0386            .OrderByDescending(g => g.LastPlayedDate)
 0387            .Select(g => g.Key!);
 388
 0389        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 390        {
 0391            query = query.Take(filter.Limit.Value);
 392        }
 393
 0394        return query.ToArray();
 0395    }
 396
 397    private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery
 398    {
 399        // This whole block is needed to filter duplicate entries on request
 400        // for the time being it cannot be used because it would destroy the ordering
 401        // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions b
 402        // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
 403
 336404        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 336405        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 406        {
 0407            var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(
 0408            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 409        }
 336410        else if (enableGroupByPresentationUniqueKey)
 411        {
 1412            var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!
 1413            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 414        }
 335415        else if (filter.GroupBySeriesPresentationUniqueKey)
 416        {
 0417            var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e
 0418            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 419        }
 420        else
 421        {
 335422            dbQuery = dbQuery.Distinct();
 423        }
 424
 336425        dbQuery = ApplyOrder(dbQuery, filter, context);
 426
 336427        return dbQuery;
 428    }
 429
 430    private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 431    {
 274432        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 433        {
 0434            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 435        }
 436
 274437        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 438        {
 273439            dbQuery = dbQuery.Include(e => e.Provider);
 440        }
 441
 274442        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 443        {
 273444            dbQuery = dbQuery.Include(e => e.LockedFields);
 445        }
 446
 274447        if (filter.DtoOptions.EnableUserData)
 448        {
 274449            dbQuery = dbQuery.Include(e => e.UserData);
 450        }
 451
 274452        if (filter.DtoOptions.EnableImages)
 453        {
 274454            dbQuery = dbQuery.Include(e => e.Images);
 455        }
 456
 274457        return dbQuery;
 458    }
 459
 460    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 461    {
 336462        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 463        {
 0464            dbQuery = dbQuery.Skip(filter.StartIndex.Value);
 465        }
 466
 336467        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 468        {
 104469            dbQuery = dbQuery.Take(filter.Limit.Value);
 470        }
 471
 336472        return dbQuery;
 473    }
 474
 475    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 476    {
 16477        dbQuery = TranslateQuery(dbQuery, context, filter);
 16478        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 16479        dbQuery = ApplyQueryPaging(dbQuery, filter);
 16480        dbQuery = ApplyNavigations(dbQuery, filter);
 16481        return dbQuery;
 482    }
 483
 484    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 485    {
 406486        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 406487        dbQuery = dbQuery.AsSingleQuery();
 488
 406489        return dbQuery;
 490    }
 491
 492    /// <inheritdoc/>
 493    public int GetCount(InternalItemsQuery filter)
 494    {
 0495        ArgumentNullException.ThrowIfNull(filter);
 496        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0497        PrepareFilterQuery(filter);
 498
 0499        using var context = _dbProvider.CreateDbContext();
 0500        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 501
 0502        return dbQuery.Count();
 0503    }
 504
 505    /// <inheritdoc />
 506    public ItemCounts GetItemCounts(InternalItemsQuery filter)
 507    {
 0508        ArgumentNullException.ThrowIfNull(filter);
 509        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0510        PrepareFilterQuery(filter);
 511
 0512        using var context = _dbProvider.CreateDbContext();
 0513        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 514
 0515        var counts = dbQuery
 0516            .GroupBy(x => x.Type)
 0517            .Select(x => new { x.Key, Count = x.Count() })
 0518            .ToArray();
 519
 0520        var lookup = _itemTypeLookup.BaseItemKindNames;
 0521        var result = new ItemCounts();
 0522        foreach (var count in counts)
 523        {
 0524            if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
 525            {
 0526                result.AlbumCount = count.Count;
 527            }
 0528            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
 529            {
 0530                result.ArtistCount = count.Count;
 531            }
 0532            else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
 533            {
 0534                result.EpisodeCount = count.Count;
 535            }
 0536            else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
 537            {
 0538                result.MovieCount = count.Count;
 539            }
 0540            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
 541            {
 0542                result.MusicVideoCount = count.Count;
 543            }
 0544            else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
 545            {
 0546                result.ProgramCount = count.Count;
 547            }
 0548            else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
 549            {
 0550                result.SeriesCount = count.Count;
 551            }
 0552            else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
 553            {
 0554                result.SongCount = count.Count;
 555            }
 0556            else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
 557            {
 0558                result.TrailerCount = count.Count;
 559            }
 560        }
 561
 0562        return result;
 0563    }
 564
 565#pragma warning disable CA1307 // Specify StringComparison for clarity
 566    /// <summary>
 567    /// Gets the type.
 568    /// </summary>
 569    /// <param name="typeName">Name of the type.</param>
 570    /// <returns>Type.</returns>
 571    /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
 572    private static Type? GetType(string typeName)
 573    {
 145574        ArgumentException.ThrowIfNullOrEmpty(typeName);
 575
 576        // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static 
 577        // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are 
 145578        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 145579            .Select(a => a.GetType(k))
 145580            .FirstOrDefault(t => t is not null));
 581    }
 582
 583    /// <inheritdoc  />
 584    public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default)
 585    {
 586        ArgumentNullException.ThrowIfNull(item);
 587
 588        var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray();
 589
 590        var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 591        await using (context.ConfigureAwait(false))
 592        {
 593            if (!await context.BaseItems
 594                .AnyAsync(bi => bi.Id == item.Id, cancellationToken)
 595                .ConfigureAwait(false))
 596            {
 597                _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
 598                return;
 599            }
 600
 601            await context.BaseItemImageInfos
 602                .Where(e => e.ItemId == item.Id)
 603                .ExecuteDeleteAsync(cancellationToken)
 604                .ConfigureAwait(false);
 605
 606            await context.BaseItemImageInfos
 607                .AddRangeAsync(images, cancellationToken)
 608                .ConfigureAwait(false);
 609
 610            await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 611        }
 612    }
 613
 614    /// <inheritdoc  />
 615    public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 616    {
 111617        UpdateOrInsertItems(items, cancellationToken);
 109618    }
 619
 620    /// <inheritdoc cref="IItemRepository"/>
 621    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 622    {
 111623        ArgumentNullException.ThrowIfNull(items);
 111624        cancellationToken.ThrowIfCancellationRequested();
 625
 109626        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 436627        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
 628        {
 109629            var ancestorIds = item.SupportsAncestors ?
 109630                item.GetAncestorIds().Distinct().ToList() :
 109631                null;
 632
 109633            var topParent = item.GetTopParent();
 634
 109635            var userdataKey = item.GetUserDataKeys();
 109636            var inheritedTags = item.GetInheritedTags();
 637
 109638            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 639        }
 640
 109641        using var context = _dbProvider.CreateDbContext();
 109642        using var transaction = context.Database.BeginTransaction();
 643
 109644        var ids = tuples.Select(f => f.Item.Id).ToArray();
 109645        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 646
 436647        foreach (var item in tuples)
 648        {
 109649            var entity = Map(item.Item);
 650            // TODO: refactor this "inconsistency"
 109651            entity.TopParentId = item.TopParent?.Id;
 652
 109653            if (!existingItems.Any(e => e == entity.Id))
 654            {
 59655                context.BaseItems.Add(entity);
 656            }
 657            else
 658            {
 50659                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 50660                context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 50661                context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 662
 50663                if (entity.Images is { Count: > 0 })
 664                {
 0665                    context.BaseItemImageInfos.AddRange(entity.Images);
 666                }
 667
 50668                if (entity.LockedFields is { Count: > 0 })
 669                {
 0670                    context.BaseItemMetadataFields.AddRange(entity.LockedFields);
 671                }
 672
 50673                context.BaseItems.Attach(entity).State = EntityState.Modified;
 674            }
 675        }
 676
 109677        context.SaveChanges();
 678
 109679        var itemValueMaps = tuples
 109680            .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 109681            .ToArray();
 109682        var allListedItemValues = itemValueMaps
 109683            .SelectMany(f => f.Values)
 109684            .Distinct()
 109685            .ToArray();
 109686        var existingValues = context.ItemValues
 109687            .Select(e => new
 109688            {
 109689                item = e,
 109690                Key = e.Type + "+" + e.Value
 109691            })
 109692            .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
 109693            .Select(e => e.item)
 109694            .ToArray();
 109695        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 109696        {
 109697            CleanValue = GetCleanValue(f.Value),
 109698            ItemValueId = Guid.NewGuid(),
 109699            Type = f.MagicNumber,
 109700            Value = f.Value
 109701        }).ToArray();
 109702        context.ItemValues.AddRange(missingItemValues);
 109703        context.SaveChanges();
 704
 109705        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 109706        var valueMap = itemValueMaps
 109707            .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type =
 109708            .ToArray();
 709
 109710        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 711
 436712        foreach (var item in valueMap)
 713        {
 109714            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 218715            foreach (var itemValue in item.Values)
 716            {
 0717                var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
 0718                if (existingItem is null)
 719                {
 0720                    context.ItemValuesMap.Add(new ItemValueMap()
 0721                    {
 0722                        Item = null!,
 0723                        ItemId = item.Item.Id,
 0724                        ItemValue = null!,
 0725                        ItemValueId = itemValue.ItemValueId
 0726                    });
 727                }
 728                else
 729                {
 730                    // map exists, remove from list so its been handled.
 0731                    itemMappedValues.Remove(existingItem);
 732                }
 733            }
 734
 735            // all still listed values are not in the new list so remove them.
 109736            context.ItemValuesMap.RemoveRange(itemMappedValues);
 737        }
 738
 109739        context.SaveChanges();
 740
 436741        foreach (var item in tuples)
 742        {
 109743            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 744            {
 109745                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 109746                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 272747                foreach (var ancestorId in validAncestorIds)
 748                {
 27749                    var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
 27750                    if (existingAncestorId is null)
 751                    {
 23752                        context.AncestorIds.Add(new AncestorId()
 23753                        {
 23754                            ParentItemId = ancestorId,
 23755                            ItemId = item.Item.Id,
 23756                            Item = null!,
 23757                            ParentItem = null!
 23758                        });
 759                    }
 760                    else
 761                    {
 4762                        existingAncestorIds.Remove(existingAncestorId);
 763                    }
 764                }
 765
 109766                context.AncestorIds.RemoveRange(existingAncestorIds);
 767            }
 768        }
 769
 109770        context.SaveChanges();
 109771        transaction.Commit();
 218772    }
 773
 774    /// <inheritdoc  />
 775    public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
 776    {
 777        ArgumentNullException.ThrowIfNull(item);
 778        cancellationToken.ThrowIfCancellationRequested();
 779
 780        var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 781
 782        await using (dbContext.ConfigureAwait(false))
 783        {
 784            var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
 785            await using (transaction.ConfigureAwait(false))
 786            {
 787                var userKeys = item.GetUserDataKeys().ToArray();
 788                var retentionDate = (DateTime?)null;
 789
 790                await dbContext.UserData
 791                    .Where(e => e.ItemId == PlaceholderId)
 792                    .Where(e => userKeys.Contains(e.CustomDataKey))
 793                    .ExecuteUpdateAsync(
 794                        e => e
 795                            .SetProperty(f => f.ItemId, item.Id)
 796                            .SetProperty(f => f.RetentionDate, retentionDate),
 797                        cancellationToken).ConfigureAwait(false);
 798
 799                // Rehydrate the cached userdata
 800                item.UserData = await dbContext.UserData
 801                    .AsNoTracking()
 802                    .Where(e => e.ItemId == item.Id)
 803                    .ToArrayAsync(cancellationToken)
 804                    .ConfigureAwait(false);
 805
 806                await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
 807            }
 808        }
 809    }
 810
 811    /// <inheritdoc  />
 812    public BaseItemDto? RetrieveItem(Guid id)
 813    {
 86814        if (id.IsEmpty())
 815        {
 0816            throw new ArgumentException("Guid can't be empty", nameof(id));
 817        }
 818
 86819        using var context = _dbProvider.CreateDbContext();
 86820        var dbQuery = PrepareItemQuery(context, new()
 86821        {
 86822            DtoOptions = new()
 86823            {
 86824                EnableImages = true
 86825            }
 86826        });
 86827        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 86828            .Include(e => e.Provider)
 86829            .Include(e => e.LockedFields)
 86830            .Include(e => e.UserData)
 86831            .Include(e => e.Images);
 832
 86833        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 86834        if (item is null)
 835        {
 86836            return null;
 837        }
 838
 0839        return DeserializeBaseItem(item);
 86840    }
 841
 842    /// <summary>
 843    /// Maps a Entity to the DTO.
 844    /// </summary>
 845    /// <param name="entity">The entity.</param>
 846    /// <param name="dto">The dto base instance.</param>
 847    /// <param name="appHost">The Application server Host.</param>
 848    /// <param name="logger">The applogger.</param>
 849    /// <returns>The dto to map.</returns>
 850    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 851    {
 72852        dto.Id = entity.Id;
 72853        dto.ParentId = entity.ParentId.GetValueOrDefault();
 72854        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 72855        dto.EndDate = entity.EndDate;
 72856        dto.CommunityRating = entity.CommunityRating;
 72857        dto.CustomRating = entity.CustomRating;
 72858        dto.IndexNumber = entity.IndexNumber;
 72859        dto.IsLocked = entity.IsLocked;
 72860        dto.Name = entity.Name;
 72861        dto.OfficialRating = entity.OfficialRating;
 72862        dto.Overview = entity.Overview;
 72863        dto.ParentIndexNumber = entity.ParentIndexNumber;
 72864        dto.PremiereDate = entity.PremiereDate;
 72865        dto.ProductionYear = entity.ProductionYear;
 72866        dto.SortName = entity.SortName;
 72867        dto.ForcedSortName = entity.ForcedSortName;
 72868        dto.RunTimeTicks = entity.RunTimeTicks;
 72869        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 72870        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 72871        dto.IsInMixedFolder = entity.IsInMixedFolder;
 72872        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 72873        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 72874        dto.CriticRating = entity.CriticRating;
 72875        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 72876        dto.OriginalTitle = entity.OriginalTitle;
 72877        dto.Album = entity.Album;
 72878        dto.LUFS = entity.LUFS;
 72879        dto.NormalizationGain = entity.NormalizationGain;
 72880        dto.IsVirtualItem = entity.IsVirtualItem;
 72881        dto.ExternalSeriesId = entity.ExternalSeriesId;
 72882        dto.Tagline = entity.Tagline;
 72883        dto.TotalBitrate = entity.TotalBitrate;
 72884        dto.ExternalId = entity.ExternalId;
 72885        dto.Size = entity.Size;
 72886        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 72887        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 72888        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 72889        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 72890        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 72891        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 72892        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 72893        dto.Width = entity.Width.GetValueOrDefault();
 72894        dto.Height = entity.Height.GetValueOrDefault();
 72895        dto.UserData = entity.UserData;
 896
 72897        if (entity.Provider is not null)
 898        {
 71899            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 900        }
 901
 72902        if (entity.ExtraType is not null)
 903        {
 0904            dto.ExtraType = (ExtraType)entity.ExtraType;
 905        }
 906
 72907        if (entity.LockedFields is not null)
 908        {
 71909            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 910        }
 911
 72912        if (entity.Audio is not null)
 913        {
 0914            dto.Audio = (ProgramAudio)entity.Audio;
 915        }
 916
 72917        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 72918        dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 72919        dto.Studios = entity.Studios?.Split('|') ?? [];
 72920        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 921
 72922        if (dto is IHasProgramAttributes hasProgramAttributes)
 923        {
 0924            hasProgramAttributes.IsMovie = entity.IsMovie;
 0925            hasProgramAttributes.IsSeries = entity.IsSeries;
 0926            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0927            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 928        }
 929
 72930        if (dto is LiveTvChannel liveTvChannel)
 931        {
 0932            liveTvChannel.ServiceName = entity.ExternalServiceId;
 933        }
 934
 72935        if (dto is Trailer trailer)
 936        {
 0937            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 938        }
 939
 72940        if (dto is Video video)
 941        {
 1942            video.PrimaryVersionId = entity.PrimaryVersionId;
 943        }
 944
 72945        if (dto is IHasSeries hasSeriesName)
 946        {
 0947            hasSeriesName.SeriesName = entity.SeriesName;
 0948            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0949            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 950        }
 951
 72952        if (dto is Episode episode)
 953        {
 0954            episode.SeasonName = entity.SeasonName;
 0955            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 956        }
 957
 72958        if (dto is IHasArtist hasArtists)
 959        {
 0960            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 961        }
 962
 72963        if (dto is IHasAlbumArtist hasAlbumArtists)
 964        {
 0965            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 966        }
 967
 72968        if (dto is LiveTvProgram program)
 969        {
 0970            program.ShowId = entity.ShowId;
 971        }
 972
 72973        if (entity.Images is not null)
 974        {
 71975            dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
 976        }
 977
 978        // dto.Type = entity.Type;
 979        // dto.Data = entity.Data;
 980        // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
 72981        if (dto is IHasStartDate hasStartDate)
 982        {
 0983            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 984        }
 985
 986        // Fields that are present in the DB but are never actually used
 987        // dto.UnratedType = entity.UnratedType;
 988        // dto.TopParentId = entity.TopParentId;
 989        // dto.CleanName = entity.CleanName;
 990        // dto.UserDataKey = entity.UserDataKey;
 991
 72992        if (dto is Folder folder)
 993        {
 71994            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 995        }
 996
 72997        return dto;
 998    }
 999
 1000    /// <summary>
 1001    /// Maps a Entity to the DTO.
 1002    /// </summary>
 1003    /// <param name="dto">The entity.</param>
 1004    /// <returns>The dto to map.</returns>
 1005    public BaseItemEntity Map(BaseItemDto dto)
 1006    {
 1191007        var dtoType = dto.GetType();
 1191008        var entity = new BaseItemEntity()
 1191009        {
 1191010            Type = dtoType.ToString(),
 1191011            Id = dto.Id
 1191012        };
 1013
 1191014        if (TypeRequiresDeserialization(dtoType))
 1015        {
 981016            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 1017        }
 1018
 1191019        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 1191020        entity.Path = GetPathToSave(dto.Path);
 1191021        entity.EndDate = dto.EndDate;
 1191022        entity.CommunityRating = dto.CommunityRating;
 1191023        entity.CustomRating = dto.CustomRating;
 1191024        entity.IndexNumber = dto.IndexNumber;
 1191025        entity.IsLocked = dto.IsLocked;
 1191026        entity.Name = dto.Name;
 1191027        entity.CleanName = GetCleanValue(dto.Name);
 1191028        entity.OfficialRating = dto.OfficialRating;
 1191029        entity.Overview = dto.Overview;
 1191030        entity.ParentIndexNumber = dto.ParentIndexNumber;
 1191031        entity.PremiereDate = dto.PremiereDate;
 1191032        entity.ProductionYear = dto.ProductionYear;
 1191033        entity.SortName = dto.SortName;
 1191034        entity.ForcedSortName = dto.ForcedSortName;
 1191035        entity.RunTimeTicks = dto.RunTimeTicks;
 1191036        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 1191037        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 1191038        entity.IsInMixedFolder = dto.IsInMixedFolder;
 1191039        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 1191040        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 1191041        entity.CriticRating = dto.CriticRating;
 1191042        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 1191043        entity.OriginalTitle = dto.OriginalTitle;
 1191044        entity.Album = dto.Album;
 1191045        entity.LUFS = dto.LUFS;
 1191046        entity.NormalizationGain = dto.NormalizationGain;
 1191047        entity.IsVirtualItem = dto.IsVirtualItem;
 1191048        entity.ExternalSeriesId = dto.ExternalSeriesId;
 1191049        entity.Tagline = dto.Tagline;
 1191050        entity.TotalBitrate = dto.TotalBitrate;
 1191051        entity.ExternalId = dto.ExternalId;
 1191052        entity.Size = dto.Size;
 1191053        entity.Genres = string.Join('|', dto.Genres);
 1191054        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 1191055        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 1191056        entity.ChannelId = dto.ChannelId;
 1191057        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 1191058        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 1191059        entity.OwnerId = dto.OwnerId.ToString();
 1191060        entity.Width = dto.Width;
 1191061        entity.Height = dto.Height;
 1191062        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 1191063        {
 1191064            Item = entity,
 1191065            ProviderId = e.Key,
 1191066            ProviderValue = e.Value
 1191067        }).ToList();
 1068
 1191069        if (dto.Audio.HasValue)
 1070        {
 01071            entity.Audio = (ProgramAudioEntity)dto.Audio;
 1072        }
 1073
 1191074        if (dto.ExtraType.HasValue)
 1075        {
 01076            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 1077        }
 1078
 1191079        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 1191080        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Wher
 1191081        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
 1191082        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
 1191083        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 1191084            .Select(e => new BaseItemMetadataField()
 1191085            {
 1191086                Id = (int)e,
 1191087                Item = entity,
 1191088                ItemId = entity.Id
 1191089            })
 1191090            .ToArray() : null;
 1091
 1191092        if (dto is IHasProgramAttributes hasProgramAttributes)
 1093        {
 01094            entity.IsMovie = hasProgramAttributes.IsMovie;
 01095            entity.IsSeries = hasProgramAttributes.IsSeries;
 01096            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 01097            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 1098        }
 1099
 1191100        if (dto is LiveTvChannel liveTvChannel)
 1101        {
 01102            entity.ExternalServiceId = liveTvChannel.ServiceName;
 1103        }
 1104
 1191105        if (dto is Video video)
 1106        {
 01107            entity.PrimaryVersionId = video.PrimaryVersionId;
 1108        }
 1109
 1191110        if (dto is IHasSeries hasSeriesName)
 1111        {
 01112            entity.SeriesName = hasSeriesName.SeriesName;
 01113            entity.SeriesId = hasSeriesName.SeriesId;
 01114            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 1115        }
 1116
 1191117        if (dto is Episode episode)
 1118        {
 01119            entity.SeasonName = episode.SeasonName;
 01120            entity.SeasonId = episode.SeasonId;
 1121        }
 1122
 1191123        if (dto is IHasArtist hasArtists)
 1124        {
 01125            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
 1126        }
 1127
 1191128        if (dto is IHasAlbumArtist hasAlbumArtists)
 1129        {
 01130            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 1131        }
 1132
 1191133        if (dto is LiveTvProgram program)
 1134        {
 01135            entity.ShowId = program.ShowId;
 1136        }
 1137
 1191138        if (dto.ImageInfos is not null)
 1139        {
 1191140            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 1141        }
 1142
 1191143        if (dto is Trailer trailer)
 1144        {
 01145            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 01146            {
 01147                Id = (int)e,
 01148                Item = entity,
 01149                ItemId = entity.Id
 01150            }).ToArray() ?? [];
 1151        }
 1152
 1153        // dto.Type = entity.Type;
 1154        // dto.Data = entity.Data;
 1191155        entity.MediaType = dto.MediaType.ToString();
 1191156        if (dto is IHasStartDate hasStartDate)
 1157        {
 01158            entity.StartDate = hasStartDate.StartDate;
 1159        }
 1160
 1191161        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 1162
 1163        // Fields that are present in the DB but are never actually used
 1164        // dto.UserDataKey = entity.UserDataKey;
 1165
 1191166        if (dto is Folder folder)
 1167        {
 1191168            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 1191169            entity.IsFolder = folder.IsFolder;
 1170        }
 1171
 1191172        return entity;
 1173    }
 1174
 1175    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 1176    {
 641177        using var context = _dbProvider.CreateDbContext();
 1178
 641179        var query = context.ItemValuesMap
 641180            .AsNoTracking()
 641181            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 641182        if (withItemTypes.Count > 0)
 1183        {
 161184            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 1185        }
 1186
 641187        if (excludeItemTypes.Count > 0)
 1188        {
 161189            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 1190        }
 1191
 1192        // query = query.DistinctBy(e => e.CleanValue);
 641193        return query.Select(e => e.ItemValue)
 641194            .GroupBy(e => e.CleanValue)
 641195            .Select(e => e.First().Value)
 641196            .ToArray();
 641197    }
 1198
 1199    private static bool TypeRequiresDeserialization(Type type)
 1200    {
 1911201        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 1202    }
 1203
 1204    private BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 1205    {
 711206        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 711207        if (_serverConfigurationManager?.Configuration is null)
 1208        {
 01209            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 1210        }
 1211
 711212        var typeToSerialise = GetType(baseItemEntity.Type);
 711213        return BaseItemRepository.DeserializeBaseItem(
 711214            baseItemEntity,
 711215            _logger,
 711216            _appHost,
 711217            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 1218    }
 1219
 1220    /// <summary>
 1221    /// Deserializes a BaseItemEntity and sets all properties.
 1222    /// </summary>
 1223    /// <param name="baseItemEntity">The DB entity.</param>
 1224    /// <param name="logger">Logger.</param>
 1225    /// <param name="appHost">The application server Host.</param>
 1226    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 1227    /// <returns>A mapped BaseItem, or null if the item type is unknown.</returns>
 1228    public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost
 1229    {
 741230        var type = GetType(baseItemEntity.Type);
 741231        if (type is null)
 1232        {
 21233            logger.LogWarning(
 21234                "Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database c
 21235                baseItemEntity.Id,
 21236                baseItemEntity.Type);
 21237            return null;
 1238        }
 1239
 721240        BaseItemDto? dto = null;
 721241        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1242        {
 1243            try
 1244            {
 121245                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 121246            }
 01247            catch (JsonException ex)
 1248            {
 01249                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01250            }
 1251        }
 1252
 721253        if (dto is null)
 1254        {
 601255            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1256        }
 1257
 721258        return Map(baseItemEntity, dto, appHost, logger);
 1259    }
 1260
 1261    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 1262    {
 01263        ArgumentNullException.ThrowIfNull(filter);
 1264
 01265        if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
 1266        {
 01267            filter.EnableTotalRecordCount = false;
 1268        }
 1269
 01270        using var context = _dbProvider.CreateDbContext();
 1271
 01272        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 01273        {
 01274            ExcludeItemTypes = filter.ExcludeItemTypes,
 01275            IncludeItemTypes = filter.IncludeItemTypes,
 01276            MediaTypes = filter.MediaTypes,
 01277            AncestorIds = filter.AncestorIds,
 01278            ItemIds = filter.ItemIds,
 01279            TopParentIds = filter.TopParentIds,
 01280            ParentId = filter.ParentId,
 01281            IsAiring = filter.IsAiring,
 01282            IsMovie = filter.IsMovie,
 01283            IsSports = filter.IsSports,
 01284            IsKids = filter.IsKids,
 01285            IsNews = filter.IsNews,
 01286            IsSeries = filter.IsSeries
 01287        });
 1288
 01289        var itemValuesQuery = context.ItemValues
 01290            .Where(f => itemValueTypes.Contains(f.Type))
 01291            .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
 01292            .Join(
 01293                innerQueryFilter,
 01294                fw => fw.w.ItemId,
 01295                g => g.Id,
 01296                (fw, g) => fw.f.CleanValue);
 1297
 01298        var innerQuery = PrepareItemQuery(context, filter)
 01299            .Where(e => e.Type == returnType)
 01300            .Where(e => itemValuesQuery.Contains(e.CleanName));
 1301
 01302        var outerQueryFilter = new InternalItemsQuery(filter.User)
 01303        {
 01304            IsPlayed = filter.IsPlayed,
 01305            IsFavorite = filter.IsFavorite,
 01306            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 01307            IsLiked = filter.IsLiked,
 01308            IsLocked = filter.IsLocked,
 01309            NameLessThan = filter.NameLessThan,
 01310            NameStartsWith = filter.NameStartsWith,
 01311            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 01312            Tags = filter.Tags,
 01313            OfficialRatings = filter.OfficialRatings,
 01314            StudioIds = filter.StudioIds,
 01315            GenreIds = filter.GenreIds,
 01316            Genres = filter.Genres,
 01317            Years = filter.Years,
 01318            NameContains = filter.NameContains,
 01319            SearchTerm = filter.SearchTerm,
 01320            ExcludeItemIds = filter.ExcludeItemIds
 01321        };
 1322
 01323        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
 01324            .GroupBy(e => e.PresentationUniqueKey)
 01325            .Select(e => e.FirstOrDefault())
 01326            .Select(e => e!.Id);
 1327
 01328        var query = context.BaseItems
 01329            .Include(e => e.TrailerTypes)
 01330            .Include(e => e.Provider)
 01331            .Include(e => e.LockedFields)
 01332            .Include(e => e.Images)
 01333            .AsSingleQuery()
 01334            .Where(e => masterQuery.Contains(e.Id));
 1335
 01336        query = ApplyOrder(query, filter, context);
 1337
 01338        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 01339        if (filter.EnableTotalRecordCount)
 1340        {
 01341            result.TotalRecordCount = query.Count();
 1342        }
 1343
 01344        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 1345        {
 01346            query = query.Skip(filter.StartIndex.Value);
 1347        }
 1348
 01349        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 1350        {
 01351            query = query.Take(filter.Limit.Value);
 1352        }
 1353
 01354        IQueryable<BaseItemEntity>? itemCountQuery = null;
 1355
 01356        if (filter.IncludeItemTypes.Length > 0)
 1357        {
 1358            // if we are to include more then one type, sub query those items beforehand.
 1359
 01360            var typeSubQuery = new InternalItemsQuery(filter.User)
 01361            {
 01362                ExcludeItemTypes = filter.ExcludeItemTypes,
 01363                IncludeItemTypes = filter.IncludeItemTypes,
 01364                MediaTypes = filter.MediaTypes,
 01365                AncestorIds = filter.AncestorIds,
 01366                ExcludeItemIds = filter.ExcludeItemIds,
 01367                ItemIds = filter.ItemIds,
 01368                TopParentIds = filter.TopParentIds,
 01369                ParentId = filter.ParentId,
 01370                IsPlayed = filter.IsPlayed
 01371            };
 1372
 01373            itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 01374                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 1375
 01376            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 01377            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 01378            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 01379            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 01380            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 01381            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 01382            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 1383
 01384            var resultQuery = query.Select(e => new
 01385            {
 01386                item = e,
 01387                // TODO: This is bad refactor!
 01388                itemCount = new ItemCounts()
 01389                {
 01390                    SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
 01391                    EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
 01392                    MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
 01393                    AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
 01394                    ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
 01395                    SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
 01396                    TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
 01397                }
 01398            });
 1399
 01400            result.StartIndex = filter.StartIndex ?? 0;
 01401            result.Items =
 01402            [
 01403                .. resultQuery
 01404                    .AsEnumerable()
 01405                    .Where(e => e is not null)
 01406                    .Select(e => (Item: DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount))
 01407                    .Where(e => e.Item is not null)
 01408                    .Select(e => (e.Item!, e.itemCount))
 01409            ];
 1410        }
 1411        else
 1412        {
 01413            result.StartIndex = filter.StartIndex ?? 0;
 01414            result.Items =
 01415            [
 01416                .. query
 01417                    .AsEnumerable()
 01418                    .Where(e => e is not null)
 01419                    .Select(e => (Item: DeserializeBaseItem(e, filter.SkipDeserialization), ItemCounts: (ItemCounts?)nul
 01420                    .Where(e => e.Item is not null)
 01421                    .Select(e => (e.Item!, e.ItemCounts))
 01422            ];
 1423        }
 1424
 01425        return result;
 01426    }
 1427
 1428    private static void PrepareFilterQuery(InternalItemsQuery query)
 1429    {
 3361430        if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
 1431        {
 01432            query.Limit = query.Limit.Value + 4;
 1433        }
 1434
 3361435        if (query.IsResumable ?? false)
 1436        {
 11437            query.IsVirtualItem = false;
 1438        }
 3361439    }
 1440
 1441    /// <summary>
 1442    /// Gets the clean value for search and sorting purposes.
 1443    /// </summary>
 1444    /// <param name="value">The value to clean.</param>
 1445    /// <returns>The cleaned value.</returns>
 1446    public static string GetCleanValue(string value)
 1447    {
 1221448        if (string.IsNullOrWhiteSpace(value))
 1449        {
 01450            return value;
 1451        }
 1452
 1221453        var noDiacritics = value.RemoveDiacritics();
 1454
 1455        // Build a string where any punctuation or symbol is treated as a separator (space).
 1221456        var sb = new StringBuilder(noDiacritics.Length);
 1221457        var previousWasSpace = false;
 20441458        foreach (var ch in noDiacritics)
 1459        {
 1460            char outCh;
 9001461            if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
 1462            {
 8781463                outCh = ch;
 1464            }
 1465            else
 1466            {
 221467                outCh = ' ';
 1468            }
 1469
 1470            // normalize any whitespace character to a single ASCII space.
 9001471            if (char.IsWhiteSpace(outCh))
 1472            {
 501473                if (!previousWasSpace)
 1474                {
 391475                    sb.Append(' ');
 391476                    previousWasSpace = true;
 1477                }
 1478            }
 1479            else
 1480            {
 8501481                sb.Append(outCh);
 8501482                previousWasSpace = false;
 1483            }
 1484        }
 1485
 1486        // trim leading/trailing spaces that may have been added.
 1221487        var collapsed = sb.ToString().Trim();
 1221488        return collapsed.ToLowerInvariant();
 1489    }
 1490
 1491    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1492    {
 1091493        var list = new List<(ItemValueType, string)>();
 1494
 1091495        if (item is IHasArtist hasArtist)
 1496        {
 01497            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1498        }
 1499
 1091500        if (item is IHasAlbumArtist hasAlbumArtist)
 1501        {
 01502            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1503        }
 1504
 1091505        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 1091506        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 1091507        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1508
 1509        // keywords was 5
 1510
 1091511        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1512
 1513        // Remove all invalid values.
 1091514        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1515
 1091516        return list;
 1517    }
 1518
 1519    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1520    {
 01521        return new BaseItemImageInfo()
 01522        {
 01523            ItemId = baseItemId,
 01524            Id = Guid.NewGuid(),
 01525            Path = e.Path,
 01526            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 01527            DateModified = e.DateModified,
 01528            Height = e.Height,
 01529            Width = e.Width,
 01530            ImageType = (ImageInfoImageType)e.Type,
 01531            Item = null!
 01532        };
 1533    }
 1534
 1535    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 1536    {
 01537        return new ItemImageInfo()
 01538        {
 01539            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 01540            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 01541            DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
 01542            Height = e.Height,
 01543            Width = e.Width,
 01544            Type = (ImageType)e.ImageType
 01545        };
 1546    }
 1547
 1548    private string? GetPathToSave(string path)
 1549    {
 1191550        if (path is null)
 1551        {
 101552            return null;
 1553        }
 1554
 1091555        return _appHost.ReverseVirtualPath(path);
 1556    }
 1557
 1558    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1559    {
 151560        var list = new List<string>();
 1561
 151562        if (IsTypeInQuery(BaseItemKind.Person, query))
 1563        {
 11564            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1565        }
 1566
 151567        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1568        {
 11569            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1570        }
 1571
 151572        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1573        {
 11574            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1575        }
 1576
 151577        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1578        {
 11579            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1580        }
 1581
 151582        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1583        {
 11584            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1585        }
 1586
 151587        return list;
 1588    }
 1589
 1590    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1591    {
 751592        if (query.ExcludeItemTypes.Contains(type))
 1593        {
 01594            return false;
 1595        }
 1596
 751597        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 1598    }
 1599
 1600    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 1601    {
 3361602        if (!query.GroupByPresentationUniqueKey)
 1603        {
 1421604            return false;
 1605        }
 1606
 1941607        if (query.GroupBySeriesPresentationUniqueKey)
 1608        {
 01609            return false;
 1610        }
 1611
 1941612        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 1613        {
 01614            return false;
 1615        }
 1616
 1941617        if (query.User is null)
 1618        {
 1921619            return false;
 1620        }
 1621
 21622        if (query.IncludeItemTypes.Length == 0)
 1623        {
 11624            return true;
 1625        }
 1626
 11627        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 11628            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 11629            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 11630            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 11631            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 11632            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 1633    }
 1634
 1635    private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinD
 1636    {
 3361637        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 3361638        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1639
 3361640        if (hasSearch)
 1641        {
 01642            orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1643        }
 3361644        else if (orderBy.Length == 0)
 1645        {
 2311646            return query.OrderBy(e => e.SortName);
 1647        }
 1648
 1051649        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1650
 1651        // When searching, prioritize by match quality: exact match > prefix match > contains
 1051652        if (hasSearch)
 1653        {
 01654            orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!));
 1655        }
 1656
 1051657        var firstOrdering = orderBy.FirstOrDefault();
 1051658        if (firstOrdering != default)
 1659        {
 1051660            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 1051661            if (orderedQuery is null)
 1662            {
 1663                // No search relevance ordering, start fresh
 1051664                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 1051665                    ? query.OrderBy(expression)
 1051666                    : query.OrderByDescending(expression);
 1667            }
 1668            else
 1669            {
 1670                // Search relevance ordering already applied, chain with ThenBy
 01671                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 01672                    ? orderedQuery.ThenBy(expression)
 01673                    : orderedQuery.ThenByDescending(expression);
 1674            }
 1675
 1051676            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 1677            {
 01678                orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending
 01679                    ? orderedQuery.ThenBy(e => e.Name)
 01680                    : orderedQuery.ThenByDescending(e => e.Name);
 1681            }
 1682        }
 1683
 2941684        foreach (var item in orderBy.Skip(1))
 1685        {
 421686            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 421687            if (item.SortOrder == SortOrder.Ascending)
 1688            {
 421689                orderedQuery = orderedQuery!.ThenBy(expression);
 1690            }
 1691            else
 1692            {
 01693                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1694            }
 1695        }
 1696
 1051697        return orderedQuery ?? query;
 1698    }
 1699
 1700    private IQueryable<BaseItemEntity> TranslateQuery(
 1701        IQueryable<BaseItemEntity> baseQuery,
 1702        JellyfinDbContext context,
 1703        InternalItemsQuery filter)
 1704    {
 1705        const int HDWidth = 1200;
 1706        const int UHDWidth = 3800;
 1707        const int UHDHeight = 2100;
 1708
 3361709        var minWidth = filter.MinWidth;
 3361710        var maxWidth = filter.MaxWidth;
 3361711        var now = DateTime.UtcNow;
 1712
 3361713        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 1714        {
 01715            bool includeSD = false;
 01716            bool includeHD = false;
 01717            bool include4K = false;
 1718
 01719            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 1720            {
 01721                includeSD = true;
 1722            }
 1723
 01724            if (filter.IsHD.HasValue && filter.IsHD.Value)
 1725            {
 01726                includeHD = true;
 1727            }
 1728
 01729            if (filter.Is4K.HasValue && filter.Is4K.Value)
 1730            {
 01731                include4K = true;
 1732            }
 1733
 01734            baseQuery = baseQuery.Where(e =>
 01735                (includeSD && e.Width < HDWidth) ||
 01736                (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
 01737                (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
 1738        }
 1739
 3361740        if (minWidth.HasValue)
 1741        {
 01742            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1743        }
 1744
 3361745        if (filter.MinHeight.HasValue)
 1746        {
 01747            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1748        }
 1749
 3361750        if (maxWidth.HasValue)
 1751        {
 01752            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1753        }
 1754
 3361755        if (filter.MaxHeight.HasValue)
 1756        {
 01757            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1758        }
 1759
 3361760        if (filter.IsLocked.HasValue)
 1761        {
 481762            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1763        }
 1764
 3361765        var tags = filter.Tags.ToList();
 3361766        var excludeTags = filter.ExcludeTags.ToList();
 1767
 3361768        if (filter.IsMovie.HasValue)
 1769        {
 01770            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 01771                && (filter.IncludeItemTypes.Length == 0
 01772                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01773                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 1774
 01775            if (!shouldIncludeAllMovieTypes)
 1776            {
 01777                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 1778            }
 1779        }
 1780
 3361781        if (filter.IsSeries.HasValue)
 1782        {
 01783            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1784        }
 1785
 3361786        if (filter.IsSports.HasValue)
 1787        {
 01788            if (filter.IsSports.Value)
 1789            {
 01790                tags.Add("Sports");
 1791            }
 1792            else
 1793            {
 01794                excludeTags.Add("Sports");
 1795            }
 1796        }
 1797
 3361798        if (filter.IsNews.HasValue)
 1799        {
 01800            if (filter.IsNews.Value)
 1801            {
 01802                tags.Add("News");
 1803            }
 1804            else
 1805            {
 01806                excludeTags.Add("News");
 1807            }
 1808        }
 1809
 3361810        if (filter.IsKids.HasValue)
 1811        {
 01812            if (filter.IsKids.Value)
 1813            {
 01814                tags.Add("Kids");
 1815            }
 1816            else
 1817            {
 01818                excludeTags.Add("Kids");
 1819            }
 1820        }
 1821
 3361822        if (!string.IsNullOrEmpty(filter.SearchTerm))
 1823        {
 01824            var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
 01825            var originalSearchTerm = filter.SearchTerm.ToLower();
 01826            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 1827            {
 01828                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 01829                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 1830            }
 1831            else
 1832            {
 01833                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 1834            }
 1835        }
 1836
 3361837        if (filter.IsFolder.HasValue)
 1838        {
 211839            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1840        }
 1841
 3361842        var includeTypes = filter.IncludeItemTypes;
 1843
 1844        // Only specify excluded types if no included types are specified
 3361845        if (filter.IncludeItemTypes.Length == 0)
 1846        {
 2221847            var excludeTypes = filter.ExcludeItemTypes;
 2221848            if (excludeTypes.Length == 1)
 1849            {
 01850                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 1851                {
 01852                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 1853                }
 1854            }
 2221855            else if (excludeTypes.Length > 1)
 1856            {
 01857                var excludeTypeName = new List<string>();
 01858                foreach (var excludeType in excludeTypes)
 1859                {
 01860                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 1861                    {
 01862                        excludeTypeName.Add(baseItemKindName!);
 1863                    }
 1864                }
 1865
 01866                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 1867            }
 1868        }
 1869        else
 1870        {
 1141871            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 1141872            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1873        }
 1874
 3361875        if (filter.ChannelIds.Count > 0)
 1876        {
 01877            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1878        }
 1879
 3361880        if (!filter.ParentId.IsEmpty())
 1881        {
 1421882            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1883        }
 1884
 3361885        if (!string.IsNullOrWhiteSpace(filter.Path))
 1886        {
 01887            var pathToQuery = GetPathToSave(filter.Path);
 01888            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 1889        }
 1890
 3361891        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1892        {
 01893            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1894        }
 1895
 3361896        if (filter.MinCommunityRating.HasValue)
 1897        {
 01898            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1899        }
 1900
 3361901        if (filter.MinIndexNumber.HasValue)
 1902        {
 01903            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1904        }
 1905
 3361906        if (filter.MinParentAndIndexNumber.HasValue)
 1907        {
 01908            baseQuery = baseQuery
 01909                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1910        }
 1911
 3361912        if (filter.MinDateCreated.HasValue)
 1913        {
 01914            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1915        }
 1916
 3361917        if (filter.MinDateLastSaved.HasValue)
 1918        {
 01919            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1920        }
 1921
 3361922        if (filter.MinDateLastSavedForUser.HasValue)
 1923        {
 01924            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1925        }
 1926
 3361927        if (filter.IndexNumber.HasValue)
 1928        {
 01929            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1930        }
 1931
 3361932        if (filter.ParentIndexNumber.HasValue)
 1933        {
 01934            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1935        }
 1936
 3361937        if (filter.ParentIndexNumberNotEquals.HasValue)
 1938        {
 01939            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1940        }
 1941
 3361942        var minEndDate = filter.MinEndDate;
 3361943        var maxEndDate = filter.MaxEndDate;
 1944
 3361945        if (filter.HasAired.HasValue)
 1946        {
 01947            if (filter.HasAired.Value)
 1948            {
 01949                maxEndDate = DateTime.UtcNow;
 1950            }
 1951            else
 1952            {
 01953                minEndDate = DateTime.UtcNow;
 1954            }
 1955        }
 1956
 3361957        if (minEndDate.HasValue)
 1958        {
 01959            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1960        }
 1961
 3361962        if (maxEndDate.HasValue)
 1963        {
 01964            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1965        }
 1966
 3361967        if (filter.MinStartDate.HasValue)
 1968        {
 01969            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1970        }
 1971
 3361972        if (filter.MaxStartDate.HasValue)
 1973        {
 01974            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1975        }
 1976
 3361977        if (filter.MinPremiereDate.HasValue)
 1978        {
 01979            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1980        }
 1981
 3361982        if (filter.MaxPremiereDate.HasValue)
 1983        {
 01984            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1985        }
 1986
 3361987        if (filter.TrailerTypes.Length > 0)
 1988        {
 01989            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 01990            baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
 1991        }
 1992
 3361993        if (filter.IsAiring.HasValue)
 1994        {
 01995            if (filter.IsAiring.Value)
 1996            {
 01997                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 1998            }
 1999            else
 2000            {
 02001                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 2002            }
 2003        }
 2004
 3362005        if (filter.PersonIds.Length > 0)
 2006        {
 02007            var peopleEntityIds = context.BaseItems
 02008                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 02009                .Join(
 02010                    context.Peoples,
 02011                    b => b.Name,
 02012                    p => p.Name,
 02013                    (b, p) => p.Id);
 2014
 02015            baseQuery = baseQuery
 02016                .Where(e => context.PeopleBaseItemMap
 02017                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 2018        }
 2019
 3362020        if (!string.IsNullOrWhiteSpace(filter.Person))
 2021        {
 02022            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 2023        }
 2024
 3362025        if (!string.IsNullOrWhiteSpace(filter.MinSortName))
 2026        {
 2027            // this does not makes sense.
 2028            // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
 2029            // whereClauses.Add("SortName>=@MinSortName");
 2030            // statement?.TryBind("@MinSortName", query.MinSortName);
 2031        }
 2032
 3362033        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 2034        {
 02035            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 2036        }
 2037
 3362038        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 2039        {
 02040            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 2041        }
 2042
 3362043        if (!string.IsNullOrWhiteSpace(filter.Name))
 2044        {
 32045            if (filter.UseRawName == true)
 2046            {
 02047                baseQuery = baseQuery.Where(e => e.Name == filter.Name);
 2048            }
 2049            else
 2050            {
 32051                var cleanName = GetCleanValue(filter.Name);
 32052                baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 2053            }
 2054        }
 2055
 2056        // These are the same, for now
 3362057        var nameContains = filter.NameContains;
 3362058        if (!string.IsNullOrWhiteSpace(nameContains))
 2059        {
 02060            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 2061            {
 02062                nameContains = $"%{nameContains.Trim('%')}%";
 02063                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 2064            }
 2065            else
 2066            {
 02067                baseQuery = baseQuery.Where(e =>
 02068                                    e.CleanName!.Contains(nameContains)
 02069                                    || e.OriginalTitle!.ToLower().Contains(nameContains!));
 2070            }
 2071        }
 2072
 3362073        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 2074        {
 02075            var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
 02076            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
 2077        }
 2078
 3362079        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 2080        {
 02081            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 02082            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
 2083        }
 2084
 3362085        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 2086        {
 02087            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 02088            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
 2089        }
 2090
 3362091        if (filter.ImageTypes.Length > 0)
 2092        {
 1042093            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 1042094            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 2095        }
 2096
 3362097        if (filter.IsLiked.HasValue)
 2098        {
 02099            baseQuery = baseQuery
 02100                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLike
 2101        }
 2102
 3362103        if (filter.IsFavoriteOrLiked.HasValue)
 2104        {
 02105            baseQuery = baseQuery
 02106                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2107        }
 2108
 3362109        if (filter.IsFavorite.HasValue)
 2110        {
 02111            baseQuery = baseQuery
 02112                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2113        }
 2114
 3362115        if (filter.IsPlayed.HasValue)
 2116        {
 2117            // We should probably figure this out for all folders, but for right now, this is the only place where we ne
 02118            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
 2119            {
 02120                baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
 02121                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
 02122                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
 02123                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
 2124            }
 2125            else
 2126            {
 02127                baseQuery = baseQuery
 02128                    .Select(e => new
 02129                    {
 02130                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).Firs
 02131                        Item = e
 02132                    })
 02133                    .Where(e => e.IsPlayed == filter.IsPlayed)
 02134                    .Select(f => f.Item);
 2135            }
 2136        }
 2137
 3362138        if (filter.IsResumable.HasValue)
 2139        {
 12140            if (filter.IsResumable.Value)
 2141            {
 12142                baseQuery = baseQuery
 12143                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks >
 2144            }
 2145            else
 2146            {
 02147                baseQuery = baseQuery
 02148                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks =
 2149            }
 2150        }
 2151
 3362152        if (filter.ArtistIds.Length > 0)
 2153        {
 02154            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2155        }
 2156
 3362157        if (filter.AlbumArtistIds.Length > 0)
 2158        {
 02159            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 2160        }
 2161
 3362162        if (filter.ContributingArtistIds.Length > 0)
 2163        {
 02164            var contributingNames = context.BaseItems
 02165                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 02166                .Select(b => b.CleanName);
 2167
 02168            baseQuery = baseQuery.Where(e =>
 02169                e.ItemValues!.Any(ivm =>
 02170                    ivm.ItemValue.Type == ItemValueType.Artist &&
 02171                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 02172                &&
 02173                !e.ItemValues!.Any(ivm =>
 02174                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 02175                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 2176        }
 2177
 3362178        if (filter.AlbumIds.Length > 0)
 2179        {
 02180            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 02181            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 2182        }
 2183
 3362184        if (filter.ExcludeArtistIds.Length > 0)
 2185        {
 02186            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2187        }
 2188
 3362189        if (filter.GenreIds.Count > 0)
 2190        {
 02191            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 2192        }
 2193
 3362194        if (filter.Genres.Count > 0)
 2195        {
 02196            var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValue
 02197            baseQuery = baseQuery
 02198                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 2199        }
 2200
 3362201        if (tags.Count > 0)
 2202        {
 02203            var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, stri
 02204            baseQuery = baseQuery
 02205                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 2206        }
 2207
 3362208        if (excludeTags.Count > 0)
 2209        {
 02210            var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMa
 02211            baseQuery = baseQuery
 02212                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 2213        }
 2214
 3362215        if (filter.StudioIds.Length > 0)
 2216        {
 02217            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 2218        }
 2219
 3362220        if (filter.OfficialRatings.Length > 0)
 2221        {
 02222            baseQuery = baseQuery
 02223                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 2224        }
 2225
 3362226        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 3362227        if (filter.MinParentalRating != null)
 2228        {
 02229            var min = filter.MinParentalRating;
 02230            var minScore = min.Score;
 02231            var minSubScore = min.SubScore ?? 0;
 2232
 02233            minParentalRatingFilter = e =>
 02234                e.InheritedParentalRatingValue == null ||
 02235                e.InheritedParentalRatingValue > minScore ||
 02236                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 2237        }
 2238
 3362239        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 3362240        if (filter.MaxParentalRating != null)
 2241        {
 482242            var max = filter.MaxParentalRating;
 482243            var maxScore = max.Score;
 482244            var maxSubScore = max.SubScore ?? 0;
 2245
 482246            maxParentalRatingFilter = e =>
 482247                e.InheritedParentalRatingValue == null ||
 482248                e.InheritedParentalRatingValue < maxScore ||
 482249                (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
 2250        }
 2251
 3362252        if (filter.HasParentalRating ?? false)
 2253        {
 02254            if (minParentalRatingFilter != null)
 2255            {
 02256                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2257            }
 2258
 02259            if (maxParentalRatingFilter != null)
 2260            {
 02261                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2262            }
 2263        }
 3362264        else if (filter.BlockUnratedItems.Length > 0)
 2265        {
 02266            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 02267            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 2268
 02269            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 2270            {
 02271                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 2272            }
 02273            else if (minParentalRatingFilter != null)
 2274            {
 02275                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 2276            }
 02277            else if (maxParentalRatingFilter != null)
 2278            {
 02279                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 2280            }
 2281            else
 2282            {
 02283                baseQuery = baseQuery.Where(unratedItemFilter);
 2284            }
 2285        }
 3362286        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 2287        {
 482288            if (minParentalRatingFilter != null)
 2289            {
 02290                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2291            }
 2292
 482293            if (maxParentalRatingFilter != null)
 2294            {
 482295                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2296            }
 2297        }
 2882298        else if (!filter.HasParentalRating ?? false)
 2299        {
 02300            baseQuery = baseQuery
 02301                .Where(e => e.InheritedParentalRatingValue == null);
 2302        }
 2303
 3362304        if (filter.HasOfficialRating.HasValue)
 2305        {
 02306            if (filter.HasOfficialRating.Value)
 2307            {
 02308                baseQuery = baseQuery
 02309                    .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
 2310            }
 2311            else
 2312            {
 02313                baseQuery = baseQuery
 02314                    .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
 2315            }
 2316        }
 2317
 3362318        if (filter.HasOverview.HasValue)
 2319        {
 02320            if (filter.HasOverview.Value)
 2321            {
 02322                baseQuery = baseQuery
 02323                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 2324            }
 2325            else
 2326            {
 02327                baseQuery = baseQuery
 02328                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 2329            }
 2330        }
 2331
 3362332        if (filter.HasOwnerId.HasValue)
 2333        {
 02334            if (filter.HasOwnerId.Value)
 2335            {
 02336                baseQuery = baseQuery
 02337                    .Where(e => e.OwnerId != null);
 2338            }
 2339            else
 2340            {
 02341                baseQuery = baseQuery
 02342                    .Where(e => e.OwnerId == null);
 2343            }
 2344        }
 2345
 3362346        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 2347        {
 02348            baseQuery = baseQuery
 02349                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filte
 2350        }
 2351
 3362352        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2353        {
 02354            baseQuery = baseQuery
 02355                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2356        }
 2357
 3362358        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2359        {
 02360            baseQuery = baseQuery
 02361                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2362        }
 2363
 3362364        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 2365        {
 02366            baseQuery = baseQuery
 02367                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == fi
 2368        }
 2369
 3362370        if (filter.HasSubtitles.HasValue)
 2371        {
 02372            baseQuery = baseQuery
 02373                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2374        }
 2375
 3362376        if (filter.HasChapterImages.HasValue)
 2377        {
 02378            baseQuery = baseQuery
 02379                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2380        }
 2381
 3362382        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2383        {
 162384            baseQuery = baseQuery
 162385                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 2386        }
 2387
 3362388        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2389        {
 162390            baseQuery = baseQuery
 162391                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2392        }
 2393
 3362394        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2395        {
 162396            baseQuery = baseQuery
 162397                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2398        }
 2399
 3362400        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2401        {
 162402            baseQuery = baseQuery
 162403                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2404        }
 2405
 3362406        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2407        {
 02408            baseQuery = baseQuery
 02409                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2410        }
 2411
 3362412        if (filter.Years.Length > 0)
 2413        {
 02414            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2415        }
 2416
 3362417        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 3362418        if (isVirtualItem.HasValue)
 2419        {
 222420            baseQuery = baseQuery
 222421                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2422        }
 2423
 3362424        if (filter.IsSpecialSeason.HasValue)
 2425        {
 02426            if (filter.IsSpecialSeason.Value)
 2427            {
 02428                baseQuery = baseQuery
 02429                    .Where(e => e.IndexNumber == 0);
 2430            }
 2431            else
 2432            {
 02433                baseQuery = baseQuery
 02434                    .Where(e => e.IndexNumber != 0);
 2435            }
 2436        }
 2437
 3362438        if (filter.IsUnaired.HasValue)
 2439        {
 02440            if (filter.IsUnaired.Value)
 2441            {
 02442                baseQuery = baseQuery
 02443                    .Where(e => e.PremiereDate >= now);
 2444            }
 2445            else
 2446            {
 02447                baseQuery = baseQuery
 02448                    .Where(e => e.PremiereDate < now);
 2449            }
 2450        }
 2451
 3362452        if (filter.MediaTypes.Length > 0)
 2453        {
 212454            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 212455            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 2456        }
 2457
 3362458        if (filter.ItemIds.Length > 0)
 2459        {
 02460            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2461        }
 2462
 3362463        if (filter.ExcludeItemIds.Length > 0)
 2464        {
 02465            baseQuery = baseQuery
 02466                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 2467        }
 2468
 3362469        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 2470        {
 02471            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 02472            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 2473        }
 2474
 3362475        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 2476        {
 2477            // Allow setting a null or empty value to get all items that have the specified provider set.
 02478            var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArra
 02479            if (includeAny.Length > 0)
 2480            {
 02481                baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
 2482            }
 2483
 02484            var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Ke
 02485            if (includeSelected.Length > 0)
 2486            {
 02487                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 2488            }
 2489        }
 2490
 3362491        if (filter.HasImdbId.HasValue)
 2492        {
 02493            baseQuery = filter.HasImdbId.Value
 02494                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().T
 02495                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().T
 2496        }
 2497
 3362498        if (filter.HasTmdbId.HasValue)
 2499        {
 02500            baseQuery = filter.HasTmdbId.Value
 02501                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().T
 02502                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().T
 2503        }
 2504
 3362505        if (filter.HasTvdbId.HasValue)
 2506        {
 02507            baseQuery = filter.HasTvdbId.Value
 02508                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().T
 02509                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().T
 2510        }
 2511
 3362512        var queryTopParentIds = filter.TopParentIds;
 2513
 3362514        if (queryTopParentIds.Length > 0)
 2515        {
 152516            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 152517            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 152518            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2519            {
 02520                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2521            }
 2522            else
 2523            {
 152524                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2525            }
 2526        }
 2527
 3362528        if (filter.AncestorIds.Length > 0)
 2529        {
 432530            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2531        }
 2532
 3362533        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 2534        {
 02535            baseQuery = baseQuery
 02536                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 2537        }
 2538
 3362539        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2540        {
 02541            baseQuery = baseQuery
 02542                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2543        }
 2544
 3362545        if (filter.ExcludeInheritedTags.Length > 0)
 2546        {
 02547            var excludedTags = filter.ExcludeInheritedTags;
 02548            baseQuery = baseQuery.Where(e =>
 02549                !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.Clea
 02550                && (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.
 2551        }
 2552
 3362553        if (filter.IncludeInheritedTags.Length > 0)
 2554        {
 02555            var includeTags = filter.IncludeInheritedTags;
 02556            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 02557            baseQuery = baseQuery.Where(e =>
 02558                e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanV
 02559
 02560                // For seasons and episodes, we also need to check the parent series' tags.
 02561                || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Ty
 02562
 02563                // A playlist should be accessible to its owner regardless of allowed tags
 02564                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 2565        }
 2566
 3362567        if (filter.SeriesStatuses.Length > 0)
 2568        {
 02569            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 02570            baseQuery = baseQuery
 02571                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 2572        }
 2573
 3362574        if (filter.BoxSetLibraryFolders.Length > 0)
 2575        {
 02576            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 02577            baseQuery = baseQuery
 02578                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 2579        }
 2580
 3362581        if (filter.VideoTypes.Length > 0)
 2582        {
 02583            var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
 02584            baseQuery = baseQuery
 02585                .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
 2586        }
 2587
 3362588        if (filter.Is3D.HasValue)
 2589        {
 02590            if (filter.Is3D.Value)
 2591            {
 02592                baseQuery = baseQuery
 02593                    .Where(e => e.Data!.Contains("Video3DFormat"));
 2594            }
 2595            else
 2596            {
 02597                baseQuery = baseQuery
 02598                    .Where(e => !e.Data!.Contains("Video3DFormat"));
 2599            }
 2600        }
 2601
 3362602        if (filter.IsPlaceHolder.HasValue)
 2603        {
 02604            if (filter.IsPlaceHolder.Value)
 2605            {
 02606                baseQuery = baseQuery
 02607                    .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
 2608            }
 2609            else
 2610            {
 02611                baseQuery = baseQuery
 02612                    .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
 2613            }
 2614        }
 2615
 3362616        if (filter.HasSpecialFeature.HasValue)
 2617        {
 02618            if (filter.HasSpecialFeature.Value)
 2619            {
 02620                baseQuery = baseQuery
 02621                    .Where(e => e.ExtraIds != null);
 2622            }
 2623            else
 2624            {
 02625                baseQuery = baseQuery
 02626                    .Where(e => e.ExtraIds == null);
 2627            }
 2628        }
 2629
 3362630        if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
 2631        {
 02632            if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo
 2633            {
 02634                baseQuery = baseQuery
 02635                    .Where(e => e.ExtraIds != null);
 2636            }
 2637            else
 2638            {
 02639                baseQuery = baseQuery
 02640                    .Where(e => e.ExtraIds == null);
 2641            }
 2642        }
 2643
 3362644        return baseQuery;
 2645    }
 2646
 2647    /// <inheritdoc/>
 2648    public async Task<bool> ItemExistsAsync(Guid id)
 2649    {
 2650        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 2651        await using (dbContext.ConfigureAwait(false))
 2652        {
 2653            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 2654        }
 2655    }
 2656
 2657    /// <inheritdoc/>
 2658    public bool GetIsPlayed(User user, Guid id, bool recursive)
 2659    {
 02660        using var dbContext = _dbProvider.CreateDbContext();
 2661
 02662        if (recursive)
 2663        {
 02664            var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
 2665
 02666            return dbContext.BaseItems
 02667                    .Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
 02668                    .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
 2669        }
 2670
 02671        return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.P
 02672    }
 2673
 2674    private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseIte
 2675    {
 12676        var folderStack = new HashSet<Guid>()
 12677            {
 12678                parentId
 12679            };
 12680        var folderList = new HashSet<Guid>()
 12681            {
 12682                parentId
 12683            };
 2684
 22685        while (folderStack.Count != 0)
 2686        {
 12687            var items = folderStack.ToArray();
 12688            folderStack.Clear();
 12689            var query = dbContext.BaseItems
 12690                .WhereOneOrMany(items, e => e.ParentId!.Value);
 2691
 12692            if (filter != null)
 2693            {
 02694                query = query.Where(filter);
 2695            }
 2696
 22697            foreach (var item in query.Select(e => e.Id).ToArray())
 2698            {
 02699                if (folderList.Add(item))
 2700                {
 02701                    folderStack.Add(item);
 2702                }
 2703            }
 2704        }
 2705
 12706        return folderList;
 2707    }
 2708
 2709    /// <inheritdoc/>
 2710    public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
 2711    {
 02712        using var dbContext = _dbProvider.CreateDbContext();
 2713
 02714        var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 02715            .Where(e => artistNames.Contains(e.Name))
 02716            .ToArray();
 2717
 02718        var lookup = artists
 02719            .GroupBy(e => e.Name!)
 02720            .ToDictionary(
 02721                g => g.Key,
 02722                g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
 2723
 02724        var result = new Dictionary<string, MusicArtist[]>(artistNames.Count);
 02725        foreach (var name in artistNames)
 2726        {
 02727            if (lookup.TryGetValue(name, out var artistArray))
 2728            {
 02729                result[name] = artistArray;
 2730            }
 2731        }
 2732
 02733        return result;
 02734    }
 2735}

Methods/Properties

.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>)
DeleteItem(System.Collections.Generic.IReadOnlyList`1<System.Guid>)
UpdateInheritedValues()
GetItemIdsList(MediaBrowser.Controller.Entities.InternalItemsQuery)
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()
GetItems(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetLatestItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Data.Enums.CollectionType)
GetNextUpSeriesKeys(MediaBrowser.Controller.Entities.InternalItemsQuery,System.DateTime)
ApplyGroupingFilter(Jellyfin.Database.Implementations.JellyfinDbContext,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)
ApplyQueryPaging(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyQueryFilter(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)
PrepareItemQuery(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetCount(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemCounts(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetType(System.String)
SaveItems(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,System.Threading.CancellationToken)
UpdateOrInsertItems(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,System.Threading.CancellationToken)
RetrieveItem(System.Guid)
Map(Jellyfin.Database.Implementations.Entities.BaseItemEntity,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.IServerApplicationHost,Microsoft.Extensions.Logging.ILogger)
Map(MediaBrowser.Controller.Entities.BaseItem)
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>)
TypeRequiresDeserialization(System.Type)
DeserializeBaseItem(Jellyfin.Database.Implementations.Entities.BaseItemEntity,System.Boolean)
DeserializeBaseItem(Jellyfin.Database.Implementations.Entities.BaseItemEntity,Microsoft.Extensions.Logging.ILogger,MediaBrowser.Controller.IServerApplicationHost,System.Boolean)
GetItemValues(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>,System.String)
PrepareFilterQuery(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetCleanValue(System.String)
GetItemValuesToSave(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.List`1<System.String>)
Map(System.Guid,MediaBrowser.Controller.Entities.ItemImageInfo)
Map(Jellyfin.Database.Implementations.Entities.BaseItemImageInfo,MediaBrowser.Controller.IServerApplicationHost)
GetPathToSave(System.String)
GetItemByNameTypesInQuery(MediaBrowser.Controller.Entities.InternalItemsQuery)
IsTypeInQuery(Jellyfin.Data.Enums.BaseItemKind,MediaBrowser.Controller.Entities.InternalItemsQuery)
EnableGroupByPresentationUniqueKey(MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyOrder(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.JellyfinDbContext)
TranslateQuery(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetIsPlayed(Jellyfin.Database.Implementations.Entities.User,System.Guid,System.Boolean)
TraverseHirachyDown(System.Guid,Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.Expressions.Expression`1<System.Func`2<Jellyfin.Database.Implementations.Entities.BaseItemEntity,System.Boolean>>)
FindArtists(System.Collections.Generic.IReadOnlyList`1<System.String>)