< 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: 587
Uncovered lines: 554
Coverable lines: 1141
Total lines: 2327
Line coverage: 51.4%
Branch coverage
48%
Covered branches: 313
Total branches: 648
Branch coverage: 48.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
DeleteItem(...)50%2296%
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%26835%
GetItemList(...)100%11100%
GetLatestItemList(...)0%4260%
GetNextUpSeriesKeys(...)0%620%
ApplyGroupingFilter(...)100%11100%
ApplyQueryPaging(...)87.5%8885.71%
ApplyQueryFilter(...)100%11100%
PrepareItemQuery(...)100%22100%
GetCount(...)100%210%
GetType(...)100%11100%
SaveImages(...)100%210%
SaveItems(...)100%11100%
UpdateOrInsertItems(...)84.61%272688.5%
RetrieveItem(...)50%4485.71%
Map(...)47.36%1297679.06%
Map(...)50%725079.46%
GetItemValueNames(...)100%44100%
TypeRequiresDeserialization(...)100%11100%
DeserialiseBaseItem(...)50%101088.88%
DeserialiseBaseItem(...)83.33%151272.72%
GetItemValues(...)0%210140%
PrepareFilterQuery(...)83.33%6680%
GetCleanValue(...)50%2266.66%
GetItemValuesToSave(...)50%4481.81%
Map(...)0%620%
Map(...)0%4260%
GetPathToSave(...)50%2266.66%
GetItemByNameTypesInQuery(...)100%1010100%
IsTypeInQuery(...)75%5466.66%
EnableGroupByPresentationUniqueKey(...)0%420200%
ApplyOrder(...)57.69%332678.26%
TranslateQuery(...)46.19%2579634239.85%

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 Jellyfin.Data.Enums;
 18using Jellyfin.Database.Implementations;
 19using Jellyfin.Database.Implementations.Entities;
 20using Jellyfin.Database.Implementations.Enums;
 21using Jellyfin.Extensions;
 22using Jellyfin.Extensions.Json;
 23using Jellyfin.Server.Implementations.Extensions;
 24using MediaBrowser.Common;
 25using MediaBrowser.Controller;
 26using MediaBrowser.Controller.Channels;
 27using MediaBrowser.Controller.Configuration;
 28using MediaBrowser.Controller.Entities;
 29using MediaBrowser.Controller.Entities.Audio;
 30using MediaBrowser.Controller.Entities.TV;
 31using MediaBrowser.Controller.LiveTv;
 32using MediaBrowser.Controller.Persistence;
 33using MediaBrowser.Model.Dto;
 34using MediaBrowser.Model.Entities;
 35using MediaBrowser.Model.LiveTv;
 36using MediaBrowser.Model.Querying;
 37using Microsoft.EntityFrameworkCore;
 38using Microsoft.Extensions.Logging;
 39using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 40using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 41
 42namespace Jellyfin.Server.Implementations.Item;
 43
 44/*
 45    All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null
 46    This is done as the code isn't actually executed client side, but only the expressions are interpret and the compile
 47    This is your only warning/message regarding this topic.
 48*/
 49
 50/// <summary>
 51/// Handles all storage logic for BaseItems.
 52/// </summary>
 53public sealed class BaseItemRepository
 54    : IItemRepository
 55{
 56    /// <summary>
 57    /// This holds all the types in the running assemblies
 58    /// so that we can de-serialize properly when we don't have strong types.
 59    /// </summary>
 160    private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
 61    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 62    private readonly IServerApplicationHost _appHost;
 63    private readonly IItemTypeLookup _itemTypeLookup;
 64    private readonly IServerConfigurationManager _serverConfigurationManager;
 65    private readonly ILogger<BaseItemRepository> _logger;
 66
 167    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 168    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 169    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 170    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 171    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 72
 73    /// <summary>
 74    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
 75    /// </summary>
 76    /// <param name="dbProvider">The db factory.</param>
 77    /// <param name="appHost">The Application host.</param>
 78    /// <param name="itemTypeLookup">The static type lookup.</param>
 79    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
 80    /// <param name="logger">System logger.</param>
 81    public BaseItemRepository(
 82        IDbContextFactory<JellyfinDbContext> dbProvider,
 83        IServerApplicationHost appHost,
 84        IItemTypeLookup itemTypeLookup,
 85        IServerConfigurationManager serverConfigurationManager,
 86        ILogger<BaseItemRepository> logger)
 87    {
 2188        _dbProvider = dbProvider;
 2189        _appHost = appHost;
 2190        _itemTypeLookup = itemTypeLookup;
 2191        _serverConfigurationManager = serverConfigurationManager;
 2192        _logger = logger;
 2193    }
 94
 95    /// <inheritdoc />
 96    public void DeleteItem(Guid id)
 97    {
 298        if (id.IsEmpty())
 99        {
 0100            throw new ArgumentException("Guid can't be empty", nameof(id));
 101        }
 102
 2103        using var context = _dbProvider.CreateDbContext();
 2104        using var transaction = context.Database.BeginTransaction();
 2105        context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
 2106        context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
 2107        context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
 2108        context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
 2109        context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
 2110        context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
 2111        context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
 2112        context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
 2113        context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
 2114        context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
 2115        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
 2116        context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
 2117        context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete();
 2118        context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
 2119        context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
 2120        context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
 2121        context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
 2122        context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete();
 2123        context.SaveChanges();
 2124        transaction.Commit();
 4125    }
 126
 127    /// <inheritdoc />
 128    public void UpdateInheritedValues()
 129    {
 9130        using var context = _dbProvider.CreateDbContext();
 9131        using var transaction = context.Database.BeginTransaction();
 132
 9133        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 134        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 9135        context.SaveChanges();
 136
 9137        transaction.Commit();
 18138    }
 139
 140    /// <inheritdoc />
 141    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 142    {
 9143        ArgumentNullException.ThrowIfNull(filter);
 9144        PrepareFilterQuery(filter);
 145
 9146        using var context = _dbProvider.CreateDbContext();
 9147        return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray();
 9148    }
 149
 150    /// <inheritdoc />
 151    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 152    {
 0153        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 154    }
 155
 156    /// <inheritdoc />
 157    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 158    {
 0159        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 160    }
 161
 162    /// <inheritdoc />
 163    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 164    {
 0165        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 166    }
 167
 168    /// <inheritdoc />
 169    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 170    {
 0171        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 172    }
 173
 174    /// <inheritdoc />
 175    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 176    {
 0177        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 178    }
 179
 180    /// <inheritdoc />
 181    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 182    {
 0183        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 184    }
 185
 186    /// <inheritdoc />
 187    public IReadOnlyList<string> GetStudioNames()
 188    {
 9189        return GetItemValueNames(_getStudiosValueTypes, [], []);
 190    }
 191
 192    /// <inheritdoc />
 193    public IReadOnlyList<string> GetAllArtistNames()
 194    {
 9195        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 196    }
 197
 198    /// <inheritdoc />
 199    public IReadOnlyList<string> GetMusicGenreNames()
 200    {
 9201        return GetItemValueNames(
 9202            _getGenreValueTypes,
 9203            _itemTypeLookup.MusicGenreTypes,
 9204            []);
 205    }
 206
 207    /// <inheritdoc />
 208    public IReadOnlyList<string> GetGenreNames()
 209    {
 9210        return GetItemValueNames(
 9211            _getGenreValueTypes,
 9212            [],
 9213            _itemTypeLookup.MusicGenreTypes);
 214    }
 215
 216    /// <inheritdoc />
 217    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
 218    {
 1219        ArgumentNullException.ThrowIfNull(filter);
 1220        if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
 221        {
 1222            var returnList = GetItemList(filter);
 1223            return new QueryResult<BaseItemDto>(
 1224                filter.StartIndex,
 1225                returnList.Count,
 1226                returnList);
 227        }
 228
 0229        PrepareFilterQuery(filter);
 0230        var result = new QueryResult<BaseItemDto>();
 231
 0232        using var context = _dbProvider.CreateDbContext();
 233
 0234        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 235
 0236        dbQuery = TranslateQuery(dbQuery, context, filter);
 0237        if (filter.EnableTotalRecordCount)
 238        {
 0239            result.TotalRecordCount = dbQuery.Count();
 240        }
 241
 0242        dbQuery = ApplyGroupingFilter(dbQuery, filter);
 0243        dbQuery = ApplyQueryPaging(dbQuery, filter);
 244
 0245        result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDe
 0246        result.StartIndex = filter.StartIndex ?? 0;
 0247        return result;
 0248    }
 249
 250    /// <inheritdoc />
 251    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
 252    {
 274253        ArgumentNullException.ThrowIfNull(filter);
 274254        PrepareFilterQuery(filter);
 255
 274256        using var context = _dbProvider.CreateDbContext();
 273257        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 258
 273259        dbQuery = TranslateQuery(dbQuery, context, filter);
 260
 273261        dbQuery = ApplyGroupingFilter(dbQuery, filter);
 273262        dbQuery = ApplyQueryPaging(dbQuery, filter);
 263
 273264        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserializ
 273265    }
 266
 267    /// <inheritdoc/>
 268    public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 269    {
 0270        ArgumentNullException.ThrowIfNull(filter);
 0271        PrepareFilterQuery(filter);
 272
 273        // Early exit if collection type is not tvshows or music
 0274        if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music)
 275        {
 0276            return Array.Empty<BaseItem>();
 277        }
 278
 0279        using var context = _dbProvider.CreateDbContext();
 280
 281        // Subquery to group by SeriesNames/Album and get the max Date Created for each group.
 0282        var subquery = PrepareItemQuery(context, filter);
 0283        subquery = TranslateQuery(subquery, context, filter);
 0284        var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album)
 0285            .Select(g => new
 0286            {
 0287                Key = g.Key,
 0288                MaxDateCreated = g.Max(a => a.DateCreated)
 0289            })
 0290            .OrderByDescending(g => g.MaxDateCreated)
 0291            .Select(g => g);
 292
 0293        if (filter.Limit.HasValue)
 294        {
 0295            subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
 296        }
 297
 0298        filter.Limit = null;
 299
 0300        var mainquery = PrepareItemQuery(context, filter);
 0301        mainquery = TranslateQuery(mainquery, context, filter);
 0302        mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
 0303        mainquery = ApplyGroupingFilter(mainquery, filter);
 0304        mainquery = ApplyQueryPaging(mainquery, filter);
 305
 0306        return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserial
 0307    }
 308
 309    /// <inheritdoc />
 310    public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
 311    {
 0312        ArgumentNullException.ThrowIfNull(filter);
 0313        ArgumentNullException.ThrowIfNull(filter.User);
 314
 0315        using var context = _dbProvider.CreateDbContext();
 316
 0317        var query = context.BaseItems
 0318            .AsNoTracking()
 0319            .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
 0320            .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
 0321            .Join(
 0322                context.UserData.AsNoTracking(),
 0323                i => new { UserId = filter.User.Id, ItemId = i.Id },
 0324                u => new { UserId = u.UserId, ItemId = u.ItemId },
 0325                (entity, data) => new { Item = entity, UserData = data })
 0326            .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
 0327            .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
 0328            .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
 0329            .OrderByDescending(g => g.LastPlayedDate)
 0330            .Select(g => g.Key!);
 331
 0332        if (filter.Limit.HasValue)
 333        {
 0334            query = query.Take(filter.Limit.Value);
 335        }
 336
 0337        return query.ToArray();
 0338    }
 339
 340    private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter
 341    {
 342        // This whole block is needed to filter duplicate entries on request
 343        // for the time being it cannot be used because it would destroy the ordering
 344        // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions b
 345        // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
 346
 347        // var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 348        // if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 349        // {
 350        //     dbQuery = ApplyOrder(dbQuery, filter);
 351        //     dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e =
 352        // }
 353        // else if (enableGroupByPresentationUniqueKey)
 354        // {
 355        //     dbQuery = ApplyOrder(dbQuery, filter);
 356        //     dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First());
 357        // }
 358        // else if (filter.GroupBySeriesPresentationUniqueKey)
 359        // {
 360        //     dbQuery = ApplyOrder(dbQuery, filter);
 361        //     dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First());
 362        // }
 363        // else
 364        // {
 365        //     dbQuery = dbQuery.Distinct();
 366        //     dbQuery = ApplyOrder(dbQuery, filter);
 367        // }
 282368        dbQuery = dbQuery.Distinct();
 282369        dbQuery = ApplyOrder(dbQuery, filter);
 370
 282371        return dbQuery;
 372    }
 373
 374    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 375    {
 282376        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 377        {
 78378            var offset = filter.StartIndex ?? 0;
 379
 78380            if (offset > 0)
 381            {
 0382                dbQuery = dbQuery.Skip(offset);
 383            }
 384
 78385            if (filter.Limit.HasValue)
 386            {
 78387                dbQuery = dbQuery.Take(filter.Limit.Value);
 388            }
 389        }
 390
 282391        return dbQuery;
 392    }
 393
 394    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 395    {
 9396        dbQuery = TranslateQuery(dbQuery, context, filter);
 9397        dbQuery = ApplyOrder(dbQuery, filter);
 9398        dbQuery = ApplyGroupingFilter(dbQuery, filter);
 9399        dbQuery = ApplyQueryPaging(dbQuery, filter);
 9400        return dbQuery;
 401    }
 402
 403    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 404    {
 488405        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 488406        dbQuery = dbQuery.AsSingleQuery()
 488407            .Include(e => e.TrailerTypes)
 488408            .Include(e => e.Provider)
 488409            .Include(e => e.LockedFields);
 410
 488411        if (filter.DtoOptions.EnableImages)
 412        {
 488413            dbQuery = dbQuery.Include(e => e.Images);
 414        }
 415
 488416        return dbQuery;
 417    }
 418
 419    /// <inheritdoc/>
 420    public int GetCount(InternalItemsQuery filter)
 421    {
 0422        ArgumentNullException.ThrowIfNull(filter);
 423        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0424        PrepareFilterQuery(filter);
 425
 0426        using var context = _dbProvider.CreateDbContext();
 0427        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 428
 0429        return dbQuery.Count();
 0430    }
 431
 432#pragma warning disable CA1307 // Specify StringComparison for clarity
 433    /// <summary>
 434    /// Gets the type.
 435    /// </summary>
 436    /// <param name="typeName">Name of the type.</param>
 437    /// <returns>Type.</returns>
 438    /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
 439    private static Type? GetType(string typeName)
 440    {
 168441        ArgumentException.ThrowIfNullOrEmpty(typeName);
 442
 443        // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static 
 444        // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are 
 168445        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 168446            .Select(a => a.GetType(k))
 168447            .FirstOrDefault(t => t is not null));
 448    }
 449
 450    /// <inheritdoc  />
 451    public void SaveImages(BaseItemDto item)
 452    {
 0453        ArgumentNullException.ThrowIfNull(item);
 454
 0455        var images = item.ImageInfos.Select(e => Map(item.Id, e));
 0456        using var context = _dbProvider.CreateDbContext();
 0457        context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
 0458        context.BaseItemImageInfos.AddRange(images);
 0459        context.SaveChanges();
 0460    }
 461
 462    /// <inheritdoc  />
 463    public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 464    {
 95465        UpdateOrInsertItems(items, cancellationToken);
 93466    }
 467
 468    /// <inheritdoc cref="IItemRepository"/>
 469    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 470    {
 95471        ArgumentNullException.ThrowIfNull(items);
 95472        cancellationToken.ThrowIfCancellationRequested();
 473
 93474        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 372475        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()))
 476        {
 93477            var ancestorIds = item.SupportsAncestors ?
 93478                item.GetAncestorIds().Distinct().ToList() :
 93479                null;
 480
 93481            var topParent = item.GetTopParent();
 482
 93483            var userdataKey = item.GetUserDataKeys();
 93484            var inheritedTags = item.GetInheritedTags();
 485
 93486            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 487        }
 488
 93489        using var context = _dbProvider.CreateDbContext();
 93490        using var transaction = context.Database.BeginTransaction();
 491
 93492        var ids = tuples.Select(f => f.Item.Id).ToArray();
 93493        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 494
 372495        foreach (var item in tuples)
 496        {
 93497            var entity = Map(item.Item);
 498            // TODO: refactor this "inconsistency"
 93499            entity.TopParentId = item.TopParent?.Id;
 500
 93501            if (!existingItems.Any(e => e == entity.Id))
 502            {
 49503                context.BaseItems.Add(entity);
 504            }
 505            else
 506            {
 44507                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 44508                context.BaseItems.Attach(entity).State = EntityState.Modified;
 509            }
 510        }
 511
 93512        context.SaveChanges();
 513
 93514        var itemValueMaps = tuples
 93515            .Select(e => (Item: e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 93516            .ToArray();
 93517        var allListedItemValues = itemValueMaps
 93518            .SelectMany(f => f.Values)
 93519            .Distinct()
 93520            .ToArray();
 93521        var existingValues = context.ItemValues
 93522            .Select(e => new
 93523            {
 93524                item = e,
 93525                Key = e.Type + "+" + e.Value
 93526            })
 93527            .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
 93528            .Select(e => e.item)
 93529            .ToArray();
 93530        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 93531        {
 93532            CleanValue = GetCleanValue(f.Value),
 93533            ItemValueId = Guid.NewGuid(),
 93534            Type = f.MagicNumber,
 93535            Value = f.Value
 93536        }).ToArray();
 93537        context.ItemValues.AddRange(missingItemValues);
 93538        context.SaveChanges();
 539
 93540        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 93541        var valueMap = itemValueMaps
 93542            .Select(f => (Item: f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.
 93543            .ToArray();
 544
 93545        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 546
 372547        foreach (var item in valueMap)
 548        {
 93549            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 186550            foreach (var itemValue in item.Values)
 551            {
 0552                var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
 0553                if (existingItem is null)
 554                {
 0555                    context.ItemValuesMap.Add(new ItemValueMap()
 0556                    {
 0557                        Item = null!,
 0558                        ItemId = item.Item.Id,
 0559                        ItemValue = null!,
 0560                        ItemValueId = itemValue.ItemValueId
 0561                    });
 562                }
 563                else
 564                {
 565                    // map exists, remove from list so its been handled.
 0566                    itemMappedValues.Remove(existingItem);
 567                }
 568            }
 569
 570            // all still listed values are not in the new list so remove them.
 93571            context.ItemValuesMap.RemoveRange(itemMappedValues);
 572        }
 573
 93574        context.SaveChanges();
 575
 372576        foreach (var item in tuples)
 577        {
 93578            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 579            {
 93580                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 93581                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 210582                foreach (var ancestorId in validAncestorIds)
 583                {
 12584                    var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
 12585                    if (existingAncestorId is null)
 586                    {
 2587                        context.AncestorIds.Add(new AncestorId()
 2588                        {
 2589                            ParentItemId = ancestorId,
 2590                            ItemId = item.Item.Id,
 2591                            Item = null!,
 2592                            ParentItem = null!
 2593                        });
 594                    }
 595                    else
 596                    {
 10597                        existingAncestorIds.Remove(existingAncestorId);
 598                    }
 599                }
 600
 93601                context.AncestorIds.RemoveRange(existingAncestorIds);
 602            }
 603        }
 604
 93605        context.SaveChanges();
 93606        transaction.Commit();
 186607    }
 608
 609    /// <inheritdoc  />
 610    public BaseItemDto? RetrieveItem(Guid id)
 611    {
 215612        if (id.IsEmpty())
 613        {
 0614            throw new ArgumentException("Guid can't be empty", nameof(id));
 615        }
 616
 215617        using var context = _dbProvider.CreateDbContext();
 215618        var item = PrepareItemQuery(context, new()
 215619        {
 215620            DtoOptions = new()
 215621            {
 215622                EnableImages = true
 215623            }
 215624        }).FirstOrDefault(e => e.Id == id);
 215625        if (item is null)
 626        {
 215627            return null;
 628        }
 629
 0630        return DeserialiseBaseItem(item);
 215631    }
 632
 633    /// <summary>
 634    /// Maps a Entity to the DTO.
 635    /// </summary>
 636    /// <param name="entity">The entity.</param>
 637    /// <param name="dto">The dto base instance.</param>
 638    /// <param name="appHost">The Application server Host.</param>
 639    /// <returns>The dto to map.</returns>
 640    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost)
 641    {
 84642        dto.Id = entity.Id;
 84643        dto.ParentId = entity.ParentId.GetValueOrDefault();
 84644        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 84645        dto.EndDate = entity.EndDate;
 84646        dto.CommunityRating = entity.CommunityRating;
 84647        dto.CustomRating = entity.CustomRating;
 84648        dto.IndexNumber = entity.IndexNumber;
 84649        dto.IsLocked = entity.IsLocked;
 84650        dto.Name = entity.Name;
 84651        dto.OfficialRating = entity.OfficialRating;
 84652        dto.Overview = entity.Overview;
 84653        dto.ParentIndexNumber = entity.ParentIndexNumber;
 84654        dto.PremiereDate = entity.PremiereDate;
 84655        dto.ProductionYear = entity.ProductionYear;
 84656        dto.SortName = entity.SortName;
 84657        dto.ForcedSortName = entity.ForcedSortName;
 84658        dto.RunTimeTicks = entity.RunTimeTicks;
 84659        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 84660        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 84661        dto.IsInMixedFolder = entity.IsInMixedFolder;
 84662        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 84663        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 84664        dto.CriticRating = entity.CriticRating;
 84665        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 84666        dto.OriginalTitle = entity.OriginalTitle;
 84667        dto.Album = entity.Album;
 84668        dto.LUFS = entity.LUFS;
 84669        dto.NormalizationGain = entity.NormalizationGain;
 84670        dto.IsVirtualItem = entity.IsVirtualItem;
 84671        dto.ExternalSeriesId = entity.ExternalSeriesId;
 84672        dto.Tagline = entity.Tagline;
 84673        dto.TotalBitrate = entity.TotalBitrate;
 84674        dto.ExternalId = entity.ExternalId;
 84675        dto.Size = entity.Size;
 84676        dto.Genres = entity.Genres?.Split('|') ?? [];
 84677        dto.DateCreated = entity.DateCreated.GetValueOrDefault();
 84678        dto.DateModified = entity.DateModified.GetValueOrDefault();
 84679        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 84680        dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault();
 84681        dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault();
 84682        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 84683        dto.Width = entity.Width.GetValueOrDefault();
 84684        dto.Height = entity.Height.GetValueOrDefault();
 84685        if (entity.Provider is not null)
 686        {
 84687            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 688        }
 689
 84690        if (entity.ExtraType is not null)
 691        {
 0692            dto.ExtraType = (ExtraType)entity.ExtraType;
 693        }
 694
 84695        if (entity.LockedFields is not null)
 696        {
 84697            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 698        }
 699
 84700        if (entity.Audio is not null)
 701        {
 0702            dto.Audio = (ProgramAudio)entity.Audio;
 703        }
 704
 84705        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 84706        dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
 84707        dto.Studios = entity.Studios?.Split('|') ?? [];
 84708        dto.Tags = entity.Tags?.Split('|') ?? [];
 709
 84710        if (dto is IHasProgramAttributes hasProgramAttributes)
 711        {
 0712            hasProgramAttributes.IsMovie = entity.IsMovie;
 0713            hasProgramAttributes.IsSeries = entity.IsSeries;
 0714            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0715            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 716        }
 717
 84718        if (dto is LiveTvChannel liveTvChannel)
 719        {
 0720            liveTvChannel.ServiceName = entity.ExternalServiceId;
 721        }
 722
 84723        if (dto is Trailer trailer)
 724        {
 0725            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 726        }
 727
 84728        if (dto is Video video)
 729        {
 0730            video.PrimaryVersionId = entity.PrimaryVersionId;
 731        }
 732
 84733        if (dto is IHasSeries hasSeriesName)
 734        {
 0735            hasSeriesName.SeriesName = entity.SeriesName;
 0736            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0737            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 738        }
 739
 84740        if (dto is Episode episode)
 741        {
 0742            episode.SeasonName = entity.SeasonName;
 0743            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 744        }
 745
 84746        if (dto is IHasArtist hasArtists)
 747        {
 0748            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 749        }
 750
 84751        if (dto is IHasAlbumArtist hasAlbumArtists)
 752        {
 0753            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 754        }
 755
 84756        if (dto is LiveTvProgram program)
 757        {
 0758            program.ShowId = entity.ShowId;
 759        }
 760
 84761        if (entity.Images is not null)
 762        {
 84763            dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
 764        }
 765
 766        // dto.Type = entity.Type;
 767        // dto.Data = entity.Data;
 768        // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
 84769        if (dto is IHasStartDate hasStartDate)
 770        {
 0771            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 772        }
 773
 774        // Fields that are present in the DB but are never actually used
 775        // dto.UnratedType = entity.UnratedType;
 776        // dto.TopParentId = entity.TopParentId;
 777        // dto.CleanName = entity.CleanName;
 778        // dto.UserDataKey = entity.UserDataKey;
 779
 84780        if (dto is Folder folder)
 781        {
 84782            folder.DateLastMediaAdded = entity.DateLastMediaAdded;
 783        }
 784
 84785        return dto;
 786    }
 787
 788    /// <summary>
 789    /// Maps a Entity to the DTO.
 790    /// </summary>
 791    /// <param name="dto">The entity.</param>
 792    /// <returns>The dto to map.</returns>
 793    public BaseItemEntity Map(BaseItemDto dto)
 794    {
 93795        var dtoType = dto.GetType();
 93796        var entity = new BaseItemEntity()
 93797        {
 93798            Type = dtoType.ToString(),
 93799            Id = dto.Id
 93800        };
 801
 93802        if (TypeRequiresDeserialization(dtoType))
 803        {
 72804            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 805        }
 806
 93807        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 93808        entity.Path = GetPathToSave(dto.Path);
 93809        entity.EndDate = dto.EndDate;
 93810        entity.CommunityRating = dto.CommunityRating;
 93811        entity.CustomRating = dto.CustomRating;
 93812        entity.IndexNumber = dto.IndexNumber;
 93813        entity.IsLocked = dto.IsLocked;
 93814        entity.Name = dto.Name;
 93815        entity.CleanName = GetCleanValue(dto.Name);
 93816        entity.OfficialRating = dto.OfficialRating;
 93817        entity.Overview = dto.Overview;
 93818        entity.ParentIndexNumber = dto.ParentIndexNumber;
 93819        entity.PremiereDate = dto.PremiereDate;
 93820        entity.ProductionYear = dto.ProductionYear;
 93821        entity.SortName = dto.SortName;
 93822        entity.ForcedSortName = dto.ForcedSortName;
 93823        entity.RunTimeTicks = dto.RunTimeTicks;
 93824        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 93825        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 93826        entity.IsInMixedFolder = dto.IsInMixedFolder;
 93827        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 93828        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 93829        entity.CriticRating = dto.CriticRating;
 93830        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 93831        entity.OriginalTitle = dto.OriginalTitle;
 93832        entity.Album = dto.Album;
 93833        entity.LUFS = dto.LUFS;
 93834        entity.NormalizationGain = dto.NormalizationGain;
 93835        entity.IsVirtualItem = dto.IsVirtualItem;
 93836        entity.ExternalSeriesId = dto.ExternalSeriesId;
 93837        entity.Tagline = dto.Tagline;
 93838        entity.TotalBitrate = dto.TotalBitrate;
 93839        entity.ExternalId = dto.ExternalId;
 93840        entity.Size = dto.Size;
 93841        entity.Genres = string.Join('|', dto.Genres);
 93842        entity.DateCreated = dto.DateCreated;
 93843        entity.DateModified = dto.DateModified;
 93844        entity.ChannelId = dto.ChannelId;
 93845        entity.DateLastRefreshed = dto.DateLastRefreshed;
 93846        entity.DateLastSaved = dto.DateLastSaved;
 93847        entity.OwnerId = dto.OwnerId.ToString();
 93848        entity.Width = dto.Width;
 93849        entity.Height = dto.Height;
 93850        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 93851        {
 93852            Item = entity,
 93853            ProviderId = e.Key,
 93854            ProviderValue = e.Value
 93855        }).ToList();
 856
 93857        if (dto.Audio.HasValue)
 858        {
 0859            entity.Audio = (ProgramAudioEntity)dto.Audio;
 860        }
 861
 93862        if (dto.ExtraType.HasValue)
 863        {
 0864            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 865        }
 866
 93867        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 93868        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : n
 93869        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
 93870        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
 93871        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 93872            .Select(e => new BaseItemMetadataField()
 93873            {
 93874                Id = (int)e,
 93875                Item = entity,
 93876                ItemId = entity.Id
 93877            })
 93878            .ToArray() : null;
 879
 93880        if (dto is IHasProgramAttributes hasProgramAttributes)
 881        {
 0882            entity.IsMovie = hasProgramAttributes.IsMovie;
 0883            entity.IsSeries = hasProgramAttributes.IsSeries;
 0884            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 0885            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 886        }
 887
 93888        if (dto is LiveTvChannel liveTvChannel)
 889        {
 0890            entity.ExternalServiceId = liveTvChannel.ServiceName;
 891        }
 892
 93893        if (dto is Video video)
 894        {
 0895            entity.PrimaryVersionId = video.PrimaryVersionId;
 896        }
 897
 93898        if (dto is IHasSeries hasSeriesName)
 899        {
 0900            entity.SeriesName = hasSeriesName.SeriesName;
 0901            entity.SeriesId = hasSeriesName.SeriesId;
 0902            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 903        }
 904
 93905        if (dto is Episode episode)
 906        {
 0907            entity.SeasonName = episode.SeasonName;
 0908            entity.SeasonId = episode.SeasonId;
 909        }
 910
 93911        if (dto is IHasArtist hasArtists)
 912        {
 0913            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
 914        }
 915
 93916        if (dto is IHasAlbumArtist hasAlbumArtists)
 917        {
 0918            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 919        }
 920
 93921        if (dto is LiveTvProgram program)
 922        {
 0923            entity.ShowId = program.ShowId;
 924        }
 925
 93926        if (dto.ImageInfos is not null)
 927        {
 93928            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 929        }
 930
 93931        if (dto is Trailer trailer)
 932        {
 0933            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 0934            {
 0935                Id = (int)e,
 0936                Item = entity,
 0937                ItemId = entity.Id
 0938            }).ToArray() ?? [];
 939        }
 940
 941        // dto.Type = entity.Type;
 942        // dto.Data = entity.Data;
 93943        entity.MediaType = dto.MediaType.ToString();
 93944        if (dto is IHasStartDate hasStartDate)
 945        {
 0946            entity.StartDate = hasStartDate.StartDate;
 947        }
 948
 93949        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 950
 951        // Fields that are present in the DB but are never actually used
 952        // dto.UserDataKey = entity.UserDataKey;
 953
 93954        if (dto is Folder folder)
 955        {
 93956            entity.DateLastMediaAdded = folder.DateLastMediaAdded;
 93957            entity.IsFolder = folder.IsFolder;
 958        }
 959
 93960        return entity;
 961    }
 962
 963    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 964    {
 36965        using var context = _dbProvider.CreateDbContext();
 966
 36967        var query = context.ItemValuesMap
 36968            .AsNoTracking()
 36969            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 36970        if (withItemTypes.Count > 0)
 971        {
 9972            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 973        }
 974
 36975        if (excludeItemTypes.Count > 0)
 976        {
 9977            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 978        }
 979
 980        // query = query.DistinctBy(e => e.CleanValue);
 36981        return query.Select(e => e.ItemValue)
 36982            .GroupBy(e => e.CleanValue)
 36983            .Select(e => e.First().Value)
 36984            .ToArray();
 36985    }
 986
 987    private static bool TypeRequiresDeserialization(Type type)
 988    {
 177989        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 990    }
 991
 992    private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 993    {
 84994        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 84995        if (_serverConfigurationManager?.Configuration is null)
 996        {
 0997            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 998        }
 999
 841000        var typeToSerialise = GetType(baseItemEntity.Type);
 841001        return BaseItemRepository.DeserialiseBaseItem(
 841002            baseItemEntity,
 841003            _logger,
 841004            _appHost,
 841005            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 1006    }
 1007
 1008    /// <summary>
 1009    /// Deserialises a BaseItemEntity and sets all properties.
 1010    /// </summary>
 1011    /// <param name="baseItemEntity">The DB entity.</param>
 1012    /// <param name="logger">Logger.</param>
 1013    /// <param name="appHost">The application server Host.</param>
 1014    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 1015    /// <returns>A mapped BaseItem.</returns>
 1016    /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
 1017    public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost?
 1018    {
 841019        var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unknown type.
 841020        BaseItemDto? dto = null;
 841021        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1022        {
 1023            try
 1024            {
 201025                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 201026            }
 01027            catch (JsonException ex)
 1028            {
 01029                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01030            }
 1031        }
 1032
 841033        if (dto is null)
 1034        {
 641035            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1036        }
 1037
 841038        return Map(baseItemEntity, dto, appHost);
 1039    }
 1040
 1041    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 1042    {
 01043        ArgumentNullException.ThrowIfNull(filter);
 1044
 01045        if (!filter.Limit.HasValue)
 1046        {
 01047            filter.EnableTotalRecordCount = false;
 1048        }
 1049
 01050        using var context = _dbProvider.CreateDbContext();
 1051
 01052        var innerQueryFilter = TranslateQuery(context.BaseItems, context, new InternalItemsQuery(filter.User)
 01053        {
 01054            ExcludeItemTypes = filter.ExcludeItemTypes,
 01055            IncludeItemTypes = filter.IncludeItemTypes,
 01056            MediaTypes = filter.MediaTypes,
 01057            AncestorIds = filter.AncestorIds,
 01058            ItemIds = filter.ItemIds,
 01059            TopParentIds = filter.TopParentIds,
 01060            ParentId = filter.ParentId,
 01061            IsAiring = filter.IsAiring,
 01062            IsMovie = filter.IsMovie,
 01063            IsSports = filter.IsSports,
 01064            IsKids = filter.IsKids,
 01065            IsNews = filter.IsNews,
 01066            IsSeries = filter.IsSeries
 01067        });
 1068
 01069        var innerQuery = PrepareItemQuery(context, filter)
 01070            .Where(e => e.Type == returnType)
 01071            .Where(e => context.ItemValues!
 01072                .Where(f => itemValueTypes.Contains(f.Type))
 01073                .Where(f => innerQueryFilter.Any(g => f.BaseItemsMap!.Any(w => w.ItemId == g.Id)))
 01074                .Select(f => f.CleanValue)
 01075                .Contains(e.CleanName));
 1076
 01077        var outerQueryFilter = new InternalItemsQuery(filter.User)
 01078        {
 01079            IsPlayed = filter.IsPlayed,
 01080            IsFavorite = filter.IsFavorite,
 01081            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 01082            IsLiked = filter.IsLiked,
 01083            IsLocked = filter.IsLocked,
 01084            NameLessThan = filter.NameLessThan,
 01085            NameStartsWith = filter.NameStartsWith,
 01086            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 01087            Tags = filter.Tags,
 01088            OfficialRatings = filter.OfficialRatings,
 01089            StudioIds = filter.StudioIds,
 01090            GenreIds = filter.GenreIds,
 01091            Genres = filter.Genres,
 01092            Years = filter.Years,
 01093            NameContains = filter.NameContains,
 01094            SearchTerm = filter.SearchTerm,
 01095            ExcludeItemIds = filter.ExcludeItemIds
 01096        };
 1097
 01098        var query = TranslateQuery(innerQuery, context, outerQueryFilter)
 01099            .GroupBy(e => e.PresentationUniqueKey);
 1100
 01101        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 01102        if (filter.EnableTotalRecordCount)
 1103        {
 01104            result.TotalRecordCount = query.Count();
 1105        }
 1106
 01107        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 1108        {
 01109            var offset = filter.StartIndex ?? 0;
 1110
 01111            if (offset > 0)
 1112            {
 01113                query = query.Skip(offset);
 1114            }
 1115
 01116            if (filter.Limit.HasValue)
 1117            {
 01118                query = query.Take(filter.Limit.Value);
 1119            }
 1120        }
 1121
 01122        IQueryable<BaseItemEntity>? itemCountQuery = null;
 1123
 01124        if (filter.IncludeItemTypes.Length > 0)
 1125        {
 1126            // if we are to include more then one type, sub query those items beforehand.
 1127
 01128            var typeSubQuery = new InternalItemsQuery(filter.User)
 01129            {
 01130                ExcludeItemTypes = filter.ExcludeItemTypes,
 01131                IncludeItemTypes = filter.IncludeItemTypes,
 01132                MediaTypes = filter.MediaTypes,
 01133                AncestorIds = filter.AncestorIds,
 01134                ExcludeItemIds = filter.ExcludeItemIds,
 01135                ItemIds = filter.ItemIds,
 01136                TopParentIds = filter.TopParentIds,
 01137                ParentId = filter.ParentId,
 01138                IsPlayed = filter.IsPlayed
 01139            };
 1140
 01141            itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, typeSubQuery)
 01142                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 1143
 01144            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 01145            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 01146            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 01147            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 01148            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 01149            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 01150            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 1151
 01152            var resultQuery = query.Select(e => new
 01153            {
 01154                item = e.AsQueryable()
 01155                        .Include(e => e.TrailerTypes)
 01156                        .Include(e => e.Provider)
 01157                        .Include(e => e.LockedFields)
 01158                        .Include(e => e.Images)
 01159                        .AsSingleQuery().First(),
 01160                // TODO: This is bad refactor!
 01161                itemCount = new ItemCounts()
 01162                {
 01163                    SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
 01164                    EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
 01165                    MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
 01166                    AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
 01167                    ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
 01168                    SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
 01169                    TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
 01170                }
 01171            });
 1172
 01173            result.StartIndex = filter.StartIndex ?? 0;
 01174            result.Items =
 01175            [
 01176                .. resultQuery
 01177                    .AsEnumerable()
 01178                    .Where(e => e is not null)
 01179                    .Select(e =>
 01180                    {
 01181                        return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
 01182                    })
 01183            ];
 1184        }
 1185        else
 1186        {
 01187            result.StartIndex = filter.StartIndex ?? 0;
 01188            result.Items =
 01189            [
 01190                .. query
 01191                    .Select(e => e.First())
 01192                    .AsEnumerable()
 01193                    .Where(e => e is not null)
 01194                    .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
 01195                    {
 01196                        return (DeserialiseBaseItem(e, filter.SkipDeserialization), null);
 01197                    })
 01198            ];
 1199        }
 1200
 01201        return result;
 01202    }
 1203
 1204    private static void PrepareFilterQuery(InternalItemsQuery query)
 1205    {
 2831206        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 1207        {
 01208            query.Limit = query.Limit.Value + 4;
 1209        }
 1210
 2831211        if (query.IsResumable ?? false)
 1212        {
 11213            query.IsVirtualItem = false;
 1214        }
 2831215    }
 1216
 1217    private string GetCleanValue(string value)
 1218    {
 961219        if (string.IsNullOrWhiteSpace(value))
 1220        {
 01221            return value;
 1222        }
 1223
 961224        return value.RemoveDiacritics().ToLowerInvariant();
 1225    }
 1226
 1227    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1228    {
 931229        var list = new List<(ItemValueType, string)>();
 1230
 931231        if (item is IHasArtist hasArtist)
 1232        {
 01233            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1234        }
 1235
 931236        if (item is IHasAlbumArtist hasAlbumArtist)
 1237        {
 01238            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1239        }
 1240
 931241        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 931242        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 931243        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1244
 1245        // keywords was 5
 1246
 931247        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1248
 1249        // Remove all invalid values.
 931250        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1251
 931252        return list;
 1253    }
 1254
 1255    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1256    {
 01257        return new BaseItemImageInfo()
 01258        {
 01259            ItemId = baseItemId,
 01260            Id = Guid.NewGuid(),
 01261            Path = e.Path,
 01262            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 01263            DateModified = e.DateModified,
 01264            Height = e.Height,
 01265            Width = e.Width,
 01266            ImageType = (ImageInfoImageType)e.Type,
 01267            Item = null!
 01268        };
 1269    }
 1270
 1271    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 1272    {
 01273        return new ItemImageInfo()
 01274        {
 01275            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 01276            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 01277            DateModified = e.DateModified,
 01278            Height = e.Height,
 01279            Width = e.Width,
 01280            Type = (ImageType)e.ImageType
 01281        };
 1282    }
 1283
 1284    private string? GetPathToSave(string path)
 1285    {
 931286        if (path is null)
 1287        {
 01288            return null;
 1289        }
 1290
 931291        return _appHost.ReverseVirtualPath(path);
 1292    }
 1293
 1294    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1295    {
 161296        var list = new List<string>();
 1297
 161298        if (IsTypeInQuery(BaseItemKind.Person, query))
 1299        {
 11300            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1301        }
 1302
 161303        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1304        {
 11305            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1306        }
 1307
 161308        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1309        {
 11310            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1311        }
 1312
 161313        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1314        {
 11315            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1316        }
 1317
 161318        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1319        {
 11320            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1321        }
 1322
 161323        return list;
 1324    }
 1325
 1326    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1327    {
 801328        if (query.ExcludeItemTypes.Contains(type))
 1329        {
 01330            return false;
 1331        }
 1332
 801333        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 1334    }
 1335
 1336    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 1337    {
 01338        if (!query.GroupByPresentationUniqueKey)
 1339        {
 01340            return false;
 1341        }
 1342
 01343        if (query.GroupBySeriesPresentationUniqueKey)
 1344        {
 01345            return false;
 1346        }
 1347
 01348        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 1349        {
 01350            return false;
 1351        }
 1352
 01353        if (query.User is null)
 1354        {
 01355            return false;
 1356        }
 1357
 01358        if (query.IncludeItemTypes.Length == 0)
 1359        {
 01360            return true;
 1361        }
 1362
 01363        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 01364            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 01365            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01366            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 01367            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 01368            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 1369    }
 1370
 1371    private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter)
 1372    {
 2911373        var orderBy = filter.OrderBy;
 2911374        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1375
 2911376        if (hasSearch)
 1377        {
 01378            orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1379        }
 2911380        else if (orderBy.Count == 0)
 1381        {
 2121382            return query.OrderBy(e => e.SortName);
 1383        }
 1384
 791385        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1386
 791387        var firstOrdering = orderBy.FirstOrDefault();
 791388        if (firstOrdering != default)
 1389        {
 791390            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
 791391            if (firstOrdering.SortOrder == SortOrder.Ascending)
 1392            {
 781393                orderedQuery = query.OrderBy(expression);
 1394            }
 1395            else
 1396            {
 11397                orderedQuery = query.OrderByDescending(expression);
 1398            }
 1399
 791400            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 1401            {
 01402                if (firstOrdering.SortOrder is SortOrder.Ascending)
 1403                {
 01404                    orderedQuery = orderedQuery.ThenBy(e => e.Name);
 1405                }
 1406                else
 1407                {
 01408                    orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
 1409                }
 1410            }
 1411        }
 1412
 2301413        foreach (var item in orderBy.Skip(1))
 1414        {
 361415            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter);
 361416            if (item.SortOrder == SortOrder.Ascending)
 1417            {
 361418                orderedQuery = orderedQuery!.ThenBy(expression);
 1419            }
 1420            else
 1421            {
 01422                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1423            }
 1424        }
 1425
 791426        return orderedQuery ?? query;
 1427    }
 1428
 1429    private IQueryable<BaseItemEntity> TranslateQuery(
 1430        IQueryable<BaseItemEntity> baseQuery,
 1431        JellyfinDbContext context,
 1432        InternalItemsQuery filter)
 1433    {
 1434        const int HDWidth = 1200;
 1435        const int UHDWidth = 3800;
 1436        const int UHDHeight = 2100;
 1437
 2821438        var minWidth = filter.MinWidth;
 2821439        var maxWidth = filter.MaxWidth;
 2821440        var now = DateTime.UtcNow;
 1441
 2821442        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 1443        {
 01444            bool includeSD = false;
 01445            bool includeHD = false;
 01446            bool include4K = false;
 1447
 01448            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 1449            {
 01450                includeSD = true;
 1451            }
 1452
 01453            if (filter.IsHD.HasValue && filter.IsHD.Value)
 1454            {
 01455                includeHD = true;
 1456            }
 1457
 01458            if (filter.Is4K.HasValue && filter.Is4K.Value)
 1459            {
 01460                include4K = true;
 1461            }
 1462
 01463            baseQuery = baseQuery.Where(e =>
 01464                (includeSD && e.Width < HDWidth) ||
 01465                (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
 01466                (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
 1467        }
 1468
 2821469        if (minWidth.HasValue)
 1470        {
 01471            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1472        }
 1473
 2821474        if (filter.MinHeight.HasValue)
 1475        {
 01476            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1477        }
 1478
 2821479        if (maxWidth.HasValue)
 1480        {
 01481            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1482        }
 1483
 2821484        if (filter.MaxHeight.HasValue)
 1485        {
 01486            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1487        }
 1488
 2821489        if (filter.IsLocked.HasValue)
 1490        {
 271491            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1492        }
 1493
 2821494        var tags = filter.Tags.ToList();
 2821495        var excludeTags = filter.ExcludeTags.ToList();
 1496
 2821497        if (filter.IsMovie == true)
 1498        {
 01499            if (filter.IncludeItemTypes.Length == 0
 01500                || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01501                || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 1502            {
 01503                baseQuery = baseQuery.Where(e => e.IsMovie);
 1504            }
 1505        }
 2821506        else if (filter.IsMovie.HasValue)
 1507        {
 01508            baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
 1509        }
 1510
 2821511        if (filter.IsSeries.HasValue)
 1512        {
 01513            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1514        }
 1515
 2821516        if (filter.IsSports.HasValue)
 1517        {
 01518            if (filter.IsSports.Value)
 1519            {
 01520                tags.Add("Sports");
 1521            }
 1522            else
 1523            {
 01524                excludeTags.Add("Sports");
 1525            }
 1526        }
 1527
 2821528        if (filter.IsNews.HasValue)
 1529        {
 01530            if (filter.IsNews.Value)
 1531            {
 01532                tags.Add("News");
 1533            }
 1534            else
 1535            {
 01536                excludeTags.Add("News");
 1537            }
 1538        }
 1539
 2821540        if (filter.IsKids.HasValue)
 1541        {
 01542            if (filter.IsKids.Value)
 1543            {
 01544                tags.Add("Kids");
 1545            }
 1546            else
 1547            {
 01548                excludeTags.Add("Kids");
 1549            }
 1550        }
 1551
 2821552        if (!string.IsNullOrEmpty(filter.SearchTerm))
 1553        {
 01554            var searchTerm = filter.SearchTerm.ToLower();
 01555            baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && 
 1556        }
 1557
 2821558        if (filter.IsFolder.HasValue)
 1559        {
 211560            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1561        }
 1562
 2821563        var includeTypes = filter.IncludeItemTypes;
 1564
 1565        // Only specify excluded types if no included types are specified
 2821566        if (filter.IncludeItemTypes.Length == 0)
 1567        {
 2091568            var excludeTypes = filter.ExcludeItemTypes;
 2091569            if (excludeTypes.Length == 1)
 1570            {
 01571                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 1572                {
 01573                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 1574                }
 1575            }
 2091576            else if (excludeTypes.Length > 1)
 1577            {
 01578                var excludeTypeName = new List<string>();
 01579                foreach (var excludeType in excludeTypes)
 1580                {
 01581                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 1582                    {
 01583                        excludeTypeName.Add(baseItemKindName!);
 1584                    }
 1585                }
 1586
 01587                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 1588            }
 1589        }
 1590        else
 1591        {
 731592            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 731593            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1594        }
 1595
 2821596        if (filter.ChannelIds.Count > 0)
 1597        {
 01598            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1599        }
 1600
 2821601        if (!filter.ParentId.IsEmpty())
 1602        {
 1421603            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1604        }
 1605
 2821606        if (!string.IsNullOrWhiteSpace(filter.Path))
 1607        {
 01608            baseQuery = baseQuery.Where(e => e.Path == filter.Path);
 1609        }
 1610
 2821611        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1612        {
 01613            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1614        }
 1615
 2821616        if (filter.MinCommunityRating.HasValue)
 1617        {
 01618            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1619        }
 1620
 2821621        if (filter.MinIndexNumber.HasValue)
 1622        {
 01623            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1624        }
 1625
 2821626        if (filter.MinParentAndIndexNumber.HasValue)
 1627        {
 01628            baseQuery = baseQuery
 01629                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1630        }
 1631
 2821632        if (filter.MinDateCreated.HasValue)
 1633        {
 01634            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1635        }
 1636
 2821637        if (filter.MinDateLastSaved.HasValue)
 1638        {
 01639            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1640        }
 1641
 2821642        if (filter.MinDateLastSavedForUser.HasValue)
 1643        {
 01644            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1645        }
 1646
 2821647        if (filter.IndexNumber.HasValue)
 1648        {
 01649            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1650        }
 1651
 2821652        if (filter.ParentIndexNumber.HasValue)
 1653        {
 01654            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1655        }
 1656
 2821657        if (filter.ParentIndexNumberNotEquals.HasValue)
 1658        {
 01659            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1660        }
 1661
 2821662        var minEndDate = filter.MinEndDate;
 2821663        var maxEndDate = filter.MaxEndDate;
 1664
 2821665        if (filter.HasAired.HasValue)
 1666        {
 01667            if (filter.HasAired.Value)
 1668            {
 01669                maxEndDate = DateTime.UtcNow;
 1670            }
 1671            else
 1672            {
 01673                minEndDate = DateTime.UtcNow;
 1674            }
 1675        }
 1676
 2821677        if (minEndDate.HasValue)
 1678        {
 01679            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1680        }
 1681
 2821682        if (maxEndDate.HasValue)
 1683        {
 01684            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1685        }
 1686
 2821687        if (filter.MinStartDate.HasValue)
 1688        {
 01689            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1690        }
 1691
 2821692        if (filter.MaxStartDate.HasValue)
 1693        {
 01694            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1695        }
 1696
 2821697        if (filter.MinPremiereDate.HasValue)
 1698        {
 01699            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1700        }
 1701
 2821702        if (filter.MaxPremiereDate.HasValue)
 1703        {
 01704            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1705        }
 1706
 2821707        if (filter.TrailerTypes.Length > 0)
 1708        {
 01709            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 01710            baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
 1711        }
 1712
 2821713        if (filter.IsAiring.HasValue)
 1714        {
 01715            if (filter.IsAiring.Value)
 1716            {
 01717                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 1718            }
 1719            else
 1720            {
 01721                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 1722            }
 1723        }
 1724
 2821725        if (filter.PersonIds.Length > 0)
 1726        {
 01727            baseQuery = baseQuery
 01728                .Where(e =>
 01729                    context.PeopleBaseItemMap.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).A
 01730                        .Any(f => f.ItemId == e.Id));
 1731        }
 1732
 2821733        if (!string.IsNullOrWhiteSpace(filter.Person))
 1734        {
 01735            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 1736        }
 1737
 2821738        if (!string.IsNullOrWhiteSpace(filter.MinSortName))
 1739        {
 1740            // this does not makes sense.
 1741            // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
 1742            // whereClauses.Add("SortName>=@MinSortName");
 1743            // statement?.TryBind("@MinSortName", query.MinSortName);
 1744        }
 1745
 2821746        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 1747        {
 01748            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 1749        }
 1750
 2821751        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 1752        {
 01753            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 1754        }
 1755
 2821756        if (!string.IsNullOrWhiteSpace(filter.Name))
 1757        {
 31758            var cleanName = GetCleanValue(filter.Name);
 31759            baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 1760        }
 1761
 1762        // These are the same, for now
 2821763        var nameContains = filter.NameContains;
 2821764        if (!string.IsNullOrWhiteSpace(nameContains))
 1765        {
 01766            baseQuery = baseQuery.Where(e =>
 01767                e.CleanName!.Contains(nameContains)
 01768                || e.OriginalTitle!.ToLower().Contains(nameContains!));
 1769        }
 1770
 2821771        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 1772        {
 01773            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith) || e.Name!.StartsWith(filter.
 1774        }
 1775
 2821776        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 1777        {
 1778            // i hate this
 01779            baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!
 1780        }
 1781
 2821782        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 1783        {
 1784            // i hate this
 01785            baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDef
 1786        }
 1787
 2821788        if (filter.ImageTypes.Length > 0)
 1789        {
 781790            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 781791            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 1792        }
 1793
 2821794        if (filter.IsLiked.HasValue)
 1795        {
 01796            baseQuery = baseQuery
 01797                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLike
 1798        }
 1799
 2821800        if (filter.IsFavoriteOrLiked.HasValue)
 1801        {
 01802            baseQuery = baseQuery
 01803                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 1804        }
 1805
 2821806        if (filter.IsFavorite.HasValue)
 1807        {
 01808            baseQuery = baseQuery
 01809                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 1810        }
 1811
 2821812        if (filter.IsPlayed.HasValue)
 1813        {
 1814            // We should probably figure this out for all folders, but for right now, this is the only place where we ne
 01815            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
 1816            {
 01817                baseQuery = baseQuery.Where(e => context.BaseItems
 01818                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
 01819                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
 01820                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
 1821            }
 1822            else
 1823            {
 01824                baseQuery = baseQuery
 01825                    .Select(e => new
 01826                    {
 01827                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).Firs
 01828                        Item = e
 01829                    })
 01830                    .Where(e => e.IsPlayed == filter.IsPlayed)
 01831                    .Select(f => f.Item);
 1832            }
 1833        }
 1834
 2821835        if (filter.IsResumable.HasValue)
 1836        {
 11837            if (filter.IsResumable.Value)
 1838            {
 11839                baseQuery = baseQuery
 11840                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks >
 1841            }
 1842            else
 1843            {
 01844                baseQuery = baseQuery
 01845                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks =
 1846            }
 1847        }
 1848
 2821849        if (filter.ArtistIds.Length > 0)
 1850        {
 01851            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ArtistIds);
 1852        }
 1853
 2821854        if (filter.AlbumArtistIds.Length > 0)
 1855        {
 01856            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.AlbumArtistIds);
 1857        }
 1858
 2821859        if (filter.ContributingArtistIds.Length > 0)
 1860        {
 01861            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ContributingArtistIds);
 1862        }
 1863
 2821864        if (filter.AlbumIds.Length > 0)
 1865        {
 01866            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 01867            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 1868        }
 1869
 2821870        if (filter.ExcludeArtistIds.Length > 0)
 1871        {
 01872            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true);
 1873        }
 1874
 2821875        if (filter.GenreIds.Count > 0)
 1876        {
 01877            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 1878        }
 1879
 2821880        if (filter.Genres.Count > 0)
 1881        {
 01882            var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValue
 01883            baseQuery = baseQuery
 01884                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 1885        }
 1886
 2821887        if (tags.Count > 0)
 1888        {
 01889            var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, stri
 01890            baseQuery = baseQuery
 01891                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 1892        }
 1893
 2821894        if (excludeTags.Count > 0)
 1895        {
 01896            var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMa
 01897            baseQuery = baseQuery
 01898                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 1899        }
 1900
 2821901        if (filter.StudioIds.Length > 0)
 1902        {
 01903            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 1904        }
 1905
 2821906        if (filter.OfficialRatings.Length > 0)
 1907        {
 01908            baseQuery = baseQuery
 01909                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 1910        }
 1911
 2821912        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 2821913        if (filter.MinParentalRating != null)
 1914        {
 01915            var min = filter.MinParentalRating;
 01916            minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue
 01917            if (min.SubScore != null)
 1918            {
 01919                minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScor
 1920            }
 1921        }
 1922
 2821923        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 2821924        if (filter.MaxParentalRating != null)
 1925        {
 271926            var max = filter.MaxParentalRating;
 271927            maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue
 271928            if (max.SubScore != null)
 1929            {
 01930                maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScor
 1931            }
 1932        }
 1933
 2821934        if (filter.HasParentalRating ?? false)
 1935        {
 01936            if (minParentalRatingFilter != null)
 1937            {
 01938                baseQuery = baseQuery.Where(minParentalRatingFilter);
 1939            }
 1940
 01941            if (maxParentalRatingFilter != null)
 1942            {
 01943                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 1944            }
 1945        }
 2821946        else if (filter.BlockUnratedItems.Length > 0)
 1947        {
 01948            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 01949            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 1950
 01951            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 1952            {
 01953                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 1954            }
 01955            else if (minParentalRatingFilter != null)
 1956            {
 01957                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 1958            }
 01959            else if (maxParentalRatingFilter != null)
 1960            {
 01961                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 1962            }
 1963            else
 1964            {
 01965                baseQuery = baseQuery.Where(unratedItemFilter);
 1966            }
 1967        }
 2821968        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 1969        {
 271970            if (minParentalRatingFilter != null)
 1971            {
 01972                baseQuery = baseQuery.Where(minParentalRatingFilter);
 1973            }
 1974
 271975            if (maxParentalRatingFilter != null)
 1976            {
 271977                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 1978            }
 1979        }
 2551980        else if (!filter.HasParentalRating ?? false)
 1981        {
 01982            baseQuery = baseQuery
 01983                .Where(e => e.InheritedParentalRatingValue == null);
 1984        }
 1985
 2821986        if (filter.HasOfficialRating.HasValue)
 1987        {
 01988            if (filter.HasOfficialRating.Value)
 1989            {
 01990                baseQuery = baseQuery
 01991                    .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
 1992            }
 1993            else
 1994            {
 01995                baseQuery = baseQuery
 01996                    .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
 1997            }
 1998        }
 1999
 2822000        if (filter.HasOverview.HasValue)
 2001        {
 02002            if (filter.HasOverview.Value)
 2003            {
 02004                baseQuery = baseQuery
 02005                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 2006            }
 2007            else
 2008            {
 02009                baseQuery = baseQuery
 02010                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 2011            }
 2012        }
 2013
 2822014        if (filter.HasOwnerId.HasValue)
 2015        {
 02016            if (filter.HasOwnerId.Value)
 2017            {
 02018                baseQuery = baseQuery
 02019                    .Where(e => e.OwnerId != null);
 2020            }
 2021            else
 2022            {
 02023                baseQuery = baseQuery
 02024                    .Where(e => e.OwnerId == null);
 2025            }
 2026        }
 2027
 2822028        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 2029        {
 02030            baseQuery = baseQuery
 02031                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filte
 2032        }
 2033
 2822034        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2035        {
 02036            baseQuery = baseQuery
 02037                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2038        }
 2039
 2822040        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2041        {
 02042            baseQuery = baseQuery
 02043                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2044        }
 2045
 2822046        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 2047        {
 02048            baseQuery = baseQuery
 02049                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == fi
 2050        }
 2051
 2822052        if (filter.HasSubtitles.HasValue)
 2053        {
 02054            baseQuery = baseQuery
 02055                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2056        }
 2057
 2822058        if (filter.HasChapterImages.HasValue)
 2059        {
 02060            baseQuery = baseQuery
 02061                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2062        }
 2063
 2822064        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2065        {
 92066            baseQuery = baseQuery
 92067                .Where(e => e.ParentId.HasValue && !context.BaseItems.Any(f => f.Id == e.ParentId.Value));
 2068        }
 2069
 2822070        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2071        {
 92072            baseQuery = baseQuery
 92073                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2074        }
 2075
 2822076        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2077        {
 92078            baseQuery = baseQuery
 92079                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2080        }
 2081
 2822082        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2083        {
 92084            baseQuery = baseQuery
 92085                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2086        }
 2087
 2822088        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2089        {
 02090            baseQuery = baseQuery
 02091                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2092        }
 2093
 2822094        if (filter.Years.Length > 0)
 2095        {
 02096            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2097        }
 2098
 2822099        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 2822100        if (isVirtualItem.HasValue)
 2101        {
 222102            baseQuery = baseQuery
 222103                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2104        }
 2105
 2822106        if (filter.IsSpecialSeason.HasValue)
 2107        {
 02108            if (filter.IsSpecialSeason.Value)
 2109            {
 02110                baseQuery = baseQuery
 02111                    .Where(e => e.IndexNumber == 0);
 2112            }
 2113            else
 2114            {
 02115                baseQuery = baseQuery
 02116                    .Where(e => e.IndexNumber != 0);
 2117            }
 2118        }
 2119
 2822120        if (filter.IsUnaired.HasValue)
 2121        {
 02122            if (filter.IsUnaired.Value)
 2123            {
 02124                baseQuery = baseQuery
 02125                    .Where(e => e.PremiereDate >= now);
 2126            }
 2127            else
 2128            {
 02129                baseQuery = baseQuery
 02130                    .Where(e => e.PremiereDate < now);
 2131            }
 2132        }
 2133
 2822134        if (filter.MediaTypes.Length > 0)
 2135        {
 212136            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 212137            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 2138        }
 2139
 2822140        if (filter.ItemIds.Length > 0)
 2141        {
 02142            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2143        }
 2144
 2822145        if (filter.ExcludeItemIds.Length > 0)
 2146        {
 02147            baseQuery = baseQuery
 02148                .Where(e => !filter.ItemIds.Contains(e.Id));
 2149        }
 2150
 2822151        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 2152        {
 02153            baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w
 2154        }
 2155
 2822156        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 2157        {
 02158            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Ke
 2159        }
 2160
 2822161        if (filter.HasImdbId.HasValue)
 2162        {
 02163            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
 2164        }
 2165
 2822166        if (filter.HasTmdbId.HasValue)
 2167        {
 02168            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
 2169        }
 2170
 2822171        if (filter.HasTvdbId.HasValue)
 2172        {
 02173            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
 2174        }
 2175
 2822176        var queryTopParentIds = filter.TopParentIds;
 2177
 2822178        if (queryTopParentIds.Length > 0)
 2179        {
 162180            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 162181            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 162182            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2183            {
 02184                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2185            }
 2186            else
 2187            {
 162188                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2189            }
 2190        }
 2191
 2822192        if (filter.AncestorIds.Length > 0)
 2193        {
 372194            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2195        }
 2196
 2822197        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 2198        {
 02199            baseQuery = baseQuery
 02200                .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqu
 2201        }
 2202
 2822203        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2204        {
 02205            baseQuery = baseQuery
 02206                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2207        }
 2208
 2822209        if (filter.ExcludeInheritedTags.Length > 0)
 2210        {
 02211            baseQuery = baseQuery
 02212                .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
 02213                    .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
 2214        }
 2215
 2822216        if (filter.IncludeInheritedTags.Length > 0)
 2217        {
 2218            // Episodes do not store inherit tags from their parents in the database, and the tag may be still required 
 2219            // In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s 
 02220            if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
 2221            {
 02222                baseQuery = baseQuery
 02223                    .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags)
 02224                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
 02225                        ||
 02226                        (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(w 
 02227                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
 2228            }
 2229
 2230            // A playlist should be accessible to its owner regardless of allowed tags.
 02231            else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
 2232            {
 02233                baseQuery = baseQuery
 02234                    .Where(e =>
 02235                    e.Parents!
 02236                        .Any(f =>
 02237                            f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeIn
 02238                            || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 2239                // d        ^^ this is stupid it hate this.
 2240            }
 2241            else
 2242            {
 02243                baseQuery = baseQuery
 02244                    .Where(e => e.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.
 2245            }
 2246        }
 2247
 2822248        if (filter.SeriesStatuses.Length > 0)
 2249        {
 02250            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 02251            baseQuery = baseQuery
 02252                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 2253        }
 2254
 2822255        if (filter.BoxSetLibraryFolders.Length > 0)
 2256        {
 02257            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 02258            baseQuery = baseQuery
 02259                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 2260        }
 2261
 2822262        if (filter.VideoTypes.Length > 0)
 2263        {
 02264            var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
 02265            baseQuery = baseQuery
 02266                .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
 2267        }
 2268
 2822269        if (filter.Is3D.HasValue)
 2270        {
 02271            if (filter.Is3D.Value)
 2272            {
 02273                baseQuery = baseQuery
 02274                    .Where(e => e.Data!.Contains("Video3DFormat"));
 2275            }
 2276            else
 2277            {
 02278                baseQuery = baseQuery
 02279                    .Where(e => !e.Data!.Contains("Video3DFormat"));
 2280            }
 2281        }
 2282
 2822283        if (filter.IsPlaceHolder.HasValue)
 2284        {
 02285            if (filter.IsPlaceHolder.Value)
 2286            {
 02287                baseQuery = baseQuery
 02288                    .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
 2289            }
 2290            else
 2291            {
 02292                baseQuery = baseQuery
 02293                    .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
 2294            }
 2295        }
 2296
 2822297        if (filter.HasSpecialFeature.HasValue)
 2298        {
 02299            if (filter.HasSpecialFeature.Value)
 2300            {
 02301                baseQuery = baseQuery
 02302                    .Where(e => e.ExtraIds != null);
 2303            }
 2304            else
 2305            {
 02306                baseQuery = baseQuery
 02307                    .Where(e => e.ExtraIds == null);
 2308            }
 2309        }
 2310
 2822311        if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
 2312        {
 02313            if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo
 2314            {
 02315                baseQuery = baseQuery
 02316                    .Where(e => e.ExtraIds != null);
 2317            }
 2318            else
 2319            {
 02320                baseQuery = baseQuery
 02321                    .Where(e => e.ExtraIds == null);
 2322            }
 2323        }
 2324
 2822325        return baseQuery;
 2326    }
 2327}

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.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(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)
GetType(System.String)
SaveImages(MediaBrowser.Controller.Entities.BaseItem)
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)
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)
DeserialiseBaseItem(Jellyfin.Database.Implementations.Entities.BaseItemEntity,System.Boolean)
DeserialiseBaseItem(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)
TranslateQuery(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)