< 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
50%
Covered lines: 611
Uncovered lines: 593
Coverable lines: 1204
Total lines: 2452
Line coverage: 50.7%
Branch coverage
47%
Covered branches: 329
Total branches: 694
Branch coverage: 47.4%
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%4497.36%
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%
GetItemCounts(...)0%420200%
GetType(...)100%11100%
SaveImages(...)0%620%
SaveItems(...)100%11100%
UpdateOrInsertItems(...)85.71%292889.69%
RetrieveItem(...)50%4485.71%
Map(...)50%1448279.06%
Map(...)53.22%956279.46%
GetItemValueNames(...)100%44100%
TypeRequiresDeserialization(...)100%11100%
DeserializeBaseItem(...)50%101088.88%
DeserializeBaseItem(...)83.33%151272.72%
GetItemValues(...)0%210140%
PrepareFilterQuery(...)83.33%6680%
GetCleanValue(...)50%2266.66%
GetItemValuesToSave(...)50%4481.81%
Map(...)0%620%
Map(...)0%7280%
GetPathToSave(...)50%2266.66%
GetItemByNameTypesInQuery(...)100%1010100%
IsTypeInQuery(...)75%5466.66%
EnableGroupByPresentationUniqueKey(...)0%420200%
ApplyOrder(...)57.69%332678.26%
TranslateQuery(...)46.19%2592334239.75%

File(s)

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2// Do not enforce that because EFCore cannot deal with cultures well.
 3#pragma warning disable CA1304 // Specify CultureInfo
 4#pragma warning disable CA1311 // Specify a culture or use an invariant version
 5#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 6
 7using System;
 8using System.Collections.Concurrent;
 9using System.Collections.Generic;
 10using System.Globalization;
 11using System.Linq;
 12using System.Linq.Expressions;
 13using System.Reflection;
 14using System.Text;
 15using System.Text.Json;
 16using System.Threading;
 17using System.Threading.Tasks;
 18using Jellyfin.Data.Enums;
 19using Jellyfin.Database.Implementations;
 20using Jellyfin.Database.Implementations.Entities;
 21using Jellyfin.Database.Implementations.Enums;
 22using Jellyfin.Extensions;
 23using Jellyfin.Extensions.Json;
 24using Jellyfin.Server.Implementations.Extensions;
 25using MediaBrowser.Common;
 26using MediaBrowser.Controller;
 27using MediaBrowser.Controller.Channels;
 28using MediaBrowser.Controller.Configuration;
 29using MediaBrowser.Controller.Entities;
 30using MediaBrowser.Controller.Entities.Audio;
 31using MediaBrowser.Controller.Entities.TV;
 32using MediaBrowser.Controller.LiveTv;
 33using MediaBrowser.Controller.Persistence;
 34using MediaBrowser.Model.Dto;
 35using MediaBrowser.Model.Entities;
 36using MediaBrowser.Model.LiveTv;
 37using MediaBrowser.Model.Querying;
 38using Microsoft.EntityFrameworkCore;
 39using Microsoft.Extensions.Logging;
 40using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 41using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 42
 43namespace Jellyfin.Server.Implementations.Item;
 44
 45/*
 46    All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null
 47    This is done as the code isn't actually executed client side, but only the expressions are interpret and the compile
 48    This is your only warning/message regarding this topic.
 49*/
 50
 51/// <summary>
 52/// Handles all storage logic for BaseItems.
 53/// </summary>
 54public sealed class BaseItemRepository
 55    : IItemRepository
 56{
 57    /// <summary>
 58    /// Gets the placeholder id for UserData detached items.
 59    /// </summary>
 160    public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
 61
 62    /// <summary>
 63    /// This holds all the types in the running assemblies
 64    /// so that we can de-serialize properly when we don't have strong types.
 65    /// </summary>
 166    private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
 67    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 68    private readonly IServerApplicationHost _appHost;
 69    private readonly IItemTypeLookup _itemTypeLookup;
 70    private readonly IServerConfigurationManager _serverConfigurationManager;
 71    private readonly ILogger<BaseItemRepository> _logger;
 72
 173    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 174    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 175    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 176    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 177    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 78
 79    /// <summary>
 80    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
 81    /// </summary>
 82    /// <param name="dbProvider">The db factory.</param>
 83    /// <param name="appHost">The Application host.</param>
 84    /// <param name="itemTypeLookup">The static type lookup.</param>
 85    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
 86    /// <param name="logger">System logger.</param>
 87    public BaseItemRepository(
 88        IDbContextFactory<JellyfinDbContext> dbProvider,
 89        IServerApplicationHost appHost,
 90        IItemTypeLookup itemTypeLookup,
 91        IServerConfigurationManager serverConfigurationManager,
 92        ILogger<BaseItemRepository> logger)
 93    {
 2194        _dbProvider = dbProvider;
 2195        _appHost = appHost;
 2196        _itemTypeLookup = itemTypeLookup;
 2197        _serverConfigurationManager = serverConfigurationManager;
 2198        _logger = logger;
 2199    }
 100
 101    /// <inheritdoc />
 102    public void DeleteItem(Guid id)
 103    {
 3104        if (id.IsEmpty() || id.Equals(PlaceholderId))
 105        {
 0106            throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(id));
 107        }
 108
 3109        using var context = _dbProvider.CreateDbContext();
 3110        using var transaction = context.Database.BeginTransaction();
 111
 3112        var date = (DateTime?)DateTime.UtcNow;
 113
 114        // Remove any UserData entries for the placeholder item that would conflict with the UserData
 115        // being detached from the item being deleted. This is necessary because, during an update,
 116        // UserData may be reattached to a new entry, but some entries can be left behind.
 117        // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
 3118        context.UserData
 3119            .Join(
 3120                context.UserData.Where(e => e.ItemId == id),
 3121                placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
 3122                userData => new { userData.UserId, userData.CustomDataKey },
 3123                (placeholder, userData) => placeholder)
 3124            .Where(e => e.ItemId == PlaceholderId)
 3125            .ExecuteDelete();
 126
 127        // Detach all user watch data
 3128        context.UserData.Where(e => e.ItemId == id)
 3129            .ExecuteUpdate(e => e
 3130                .SetProperty(f => f.RetentionDate, date)
 3131                .SetProperty(f => f.ItemId, PlaceholderId));
 132
 3133        context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
 3134        context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
 3135        context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
 3136        context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
 3137        context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
 3138        context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
 3139        context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
 3140        context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
 3141        context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
 3142        context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
 3143        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
 3144        context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
 3145        context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete();
 3146        context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
 3147        context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
 3148        context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
 3149        context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
 3150        context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete();
 3151        context.SaveChanges();
 3152        transaction.Commit();
 6153    }
 154
 155    /// <inheritdoc />
 156    public void UpdateInheritedValues()
 157    {
 9158        using var context = _dbProvider.CreateDbContext();
 9159        using var transaction = context.Database.BeginTransaction();
 160
 9161        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 162        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 9163        context.SaveChanges();
 164
 9165        transaction.Commit();
 18166    }
 167
 168    /// <inheritdoc />
 169    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 170    {
 9171        ArgumentNullException.ThrowIfNull(filter);
 9172        PrepareFilterQuery(filter);
 173
 9174        using var context = _dbProvider.CreateDbContext();
 9175        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 9176    }
 177
 178    /// <inheritdoc />
 179    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 180    {
 0181        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 182    }
 183
 184    /// <inheritdoc />
 185    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 186    {
 0187        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 188    }
 189
 190    /// <inheritdoc />
 191    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 192    {
 0193        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 194    }
 195
 196    /// <inheritdoc />
 197    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 198    {
 0199        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 200    }
 201
 202    /// <inheritdoc />
 203    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 204    {
 0205        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 206    }
 207
 208    /// <inheritdoc />
 209    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 210    {
 0211        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 212    }
 213
 214    /// <inheritdoc />
 215    public IReadOnlyList<string> GetStudioNames()
 216    {
 9217        return GetItemValueNames(_getStudiosValueTypes, [], []);
 218    }
 219
 220    /// <inheritdoc />
 221    public IReadOnlyList<string> GetAllArtistNames()
 222    {
 9223        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 224    }
 225
 226    /// <inheritdoc />
 227    public IReadOnlyList<string> GetMusicGenreNames()
 228    {
 9229        return GetItemValueNames(
 9230            _getGenreValueTypes,
 9231            _itemTypeLookup.MusicGenreTypes,
 9232            []);
 233    }
 234
 235    /// <inheritdoc />
 236    public IReadOnlyList<string> GetGenreNames()
 237    {
 9238        return GetItemValueNames(
 9239            _getGenreValueTypes,
 9240            [],
 9241            _itemTypeLookup.MusicGenreTypes);
 242    }
 243
 244    /// <inheritdoc />
 245    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
 246    {
 1247        ArgumentNullException.ThrowIfNull(filter);
 1248        if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
 249        {
 1250            var returnList = GetItemList(filter);
 1251            return new QueryResult<BaseItemDto>(
 1252                filter.StartIndex,
 1253                returnList.Count,
 1254                returnList);
 255        }
 256
 0257        PrepareFilterQuery(filter);
 0258        var result = new QueryResult<BaseItemDto>();
 259
 0260        using var context = _dbProvider.CreateDbContext();
 261
 0262        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 263
 0264        dbQuery = TranslateQuery(dbQuery, context, filter);
 0265        if (filter.EnableTotalRecordCount)
 266        {
 0267            result.TotalRecordCount = dbQuery.Count();
 268        }
 269
 0270        dbQuery = ApplyGroupingFilter(dbQuery, filter);
 0271        dbQuery = ApplyQueryPaging(dbQuery, filter);
 272
 0273        result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDe
 0274        result.StartIndex = filter.StartIndex ?? 0;
 0275        return result;
 0276    }
 277
 278    /// <inheritdoc />
 279    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
 280    {
 273281        ArgumentNullException.ThrowIfNull(filter);
 273282        PrepareFilterQuery(filter);
 283
 273284        using var context = _dbProvider.CreateDbContext();
 272285        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 286
 272287        dbQuery = TranslateQuery(dbQuery, context, filter);
 288
 272289        dbQuery = ApplyGroupingFilter(dbQuery, filter);
 272290        dbQuery = ApplyQueryPaging(dbQuery, filter);
 291
 272292        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializ
 272293    }
 294
 295    /// <inheritdoc/>
 296    public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 297    {
 0298        ArgumentNullException.ThrowIfNull(filter);
 0299        PrepareFilterQuery(filter);
 300
 301        // Early exit if collection type is not tvshows or music
 0302        if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music)
 303        {
 0304            return Array.Empty<BaseItem>();
 305        }
 306
 0307        using var context = _dbProvider.CreateDbContext();
 308
 309        // Subquery to group by SeriesNames/Album and get the max Date Created for each group.
 0310        var subquery = PrepareItemQuery(context, filter);
 0311        subquery = TranslateQuery(subquery, context, filter);
 0312        var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album)
 0313            .Select(g => new
 0314            {
 0315                Key = g.Key,
 0316                MaxDateCreated = g.Max(a => a.DateCreated)
 0317            })
 0318            .OrderByDescending(g => g.MaxDateCreated)
 0319            .Select(g => g);
 320
 0321        if (filter.Limit.HasValue)
 322        {
 0323            subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
 324        }
 325
 0326        filter.Limit = null;
 327
 0328        var mainquery = PrepareItemQuery(context, filter);
 0329        mainquery = TranslateQuery(mainquery, context, filter);
 0330        mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
 0331        mainquery = ApplyGroupingFilter(mainquery, filter);
 0332        mainquery = ApplyQueryPaging(mainquery, filter);
 333
 0334        return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserial
 0335    }
 336
 337    /// <inheritdoc />
 338    public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
 339    {
 0340        ArgumentNullException.ThrowIfNull(filter);
 0341        ArgumentNullException.ThrowIfNull(filter.User);
 342
 0343        using var context = _dbProvider.CreateDbContext();
 344
 0345        var query = context.BaseItems
 0346            .AsNoTracking()
 0347            .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
 0348            .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
 0349            .Join(
 0350                context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
 0351                i => new { UserId = filter.User.Id, ItemId = i.Id },
 0352                u => new { UserId = u.UserId, ItemId = u.ItemId },
 0353                (entity, data) => new { Item = entity, UserData = data })
 0354            .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
 0355            .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
 0356            .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
 0357            .OrderByDescending(g => g.LastPlayedDate)
 0358            .Select(g => g.Key!);
 359
 0360        if (filter.Limit.HasValue)
 361        {
 0362            query = query.Take(filter.Limit.Value);
 363        }
 364
 0365        return query.ToArray();
 0366    }
 367
 368    private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter
 369    {
 370        // This whole block is needed to filter duplicate entries on request
 371        // for the time being it cannot be used because it would destroy the ordering
 372        // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions b
 373        // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
 374
 375        // var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 376        // if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 377        // {
 378        //     dbQuery = ApplyOrder(dbQuery, filter);
 379        //     dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e =
 380        // }
 381        // else if (enableGroupByPresentationUniqueKey)
 382        // {
 383        //     dbQuery = ApplyOrder(dbQuery, filter);
 384        //     dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First());
 385        // }
 386        // else if (filter.GroupBySeriesPresentationUniqueKey)
 387        // {
 388        //     dbQuery = ApplyOrder(dbQuery, filter);
 389        //     dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First());
 390        // }
 391        // else
 392        // {
 393        //     dbQuery = dbQuery.Distinct();
 394        //     dbQuery = ApplyOrder(dbQuery, filter);
 395        // }
 281396        dbQuery = dbQuery.Distinct();
 281397        dbQuery = ApplyOrder(dbQuery, filter);
 398
 281399        return dbQuery;
 400    }
 401
 402    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 403    {
 281404        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 405        {
 80406            var offset = filter.StartIndex ?? 0;
 407
 80408            if (offset > 0)
 409            {
 0410                dbQuery = dbQuery.Skip(offset);
 411            }
 412
 80413            if (filter.Limit.HasValue)
 414            {
 80415                dbQuery = dbQuery.Take(filter.Limit.Value);
 416            }
 417        }
 418
 281419        return dbQuery;
 420    }
 421
 422    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 423    {
 9424        dbQuery = TranslateQuery(dbQuery, context, filter);
 9425        dbQuery = ApplyOrder(dbQuery, filter);
 9426        dbQuery = ApplyGroupingFilter(dbQuery, filter);
 9427        dbQuery = ApplyQueryPaging(dbQuery, filter);
 9428        return dbQuery;
 429    }
 430
 431    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 432    {
 488433        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 488434        dbQuery = dbQuery.AsSingleQuery()
 488435            .Include(e => e.TrailerTypes)
 488436            .Include(e => e.Provider)
 488437            .Include(e => e.LockedFields);
 438
 488439        if (filter.DtoOptions.EnableImages)
 440        {
 488441            dbQuery = dbQuery.Include(e => e.Images);
 442        }
 443
 488444        return dbQuery;
 445    }
 446
 447    /// <inheritdoc/>
 448    public int GetCount(InternalItemsQuery filter)
 449    {
 0450        ArgumentNullException.ThrowIfNull(filter);
 451        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0452        PrepareFilterQuery(filter);
 453
 0454        using var context = _dbProvider.CreateDbContext();
 0455        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 456
 0457        return dbQuery.Count();
 0458    }
 459
 460    /// <inheritdoc />
 461    public ItemCounts GetItemCounts(InternalItemsQuery filter)
 462    {
 0463        ArgumentNullException.ThrowIfNull(filter);
 464        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0465        PrepareFilterQuery(filter);
 466
 0467        using var context = _dbProvider.CreateDbContext();
 0468        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 469
 0470        var counts = dbQuery
 0471            .GroupBy(x => x.Type)
 0472            .Select(x => new { x.Key, Count = x.Count() })
 0473            .AsEnumerable();
 474
 0475        var lookup = _itemTypeLookup.BaseItemKindNames;
 0476        var result = new ItemCounts();
 0477        foreach (var count in counts)
 478        {
 0479            if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
 480            {
 0481                result.AlbumCount = count.Count;
 482            }
 0483            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
 484            {
 0485                result.ArtistCount = count.Count;
 486            }
 0487            else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
 488            {
 0489                result.EpisodeCount = count.Count;
 490            }
 0491            else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
 492            {
 0493                result.MovieCount = count.Count;
 494            }
 0495            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
 496            {
 0497                result.MusicVideoCount = count.Count;
 498            }
 0499            else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
 500            {
 0501                result.ProgramCount = count.Count;
 502            }
 0503            else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
 504            {
 0505                result.SeriesCount = count.Count;
 506            }
 0507            else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
 508            {
 0509                result.SongCount = count.Count;
 510            }
 0511            else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
 512            {
 0513                result.TrailerCount = count.Count;
 514            }
 515        }
 516
 0517        return result;
 0518    }
 519
 520#pragma warning disable CA1307 // Specify StringComparison for clarity
 521    /// <summary>
 522    /// Gets the type.
 523    /// </summary>
 524    /// <param name="typeName">Name of the type.</param>
 525    /// <returns>Type.</returns>
 526    /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
 527    private static Type? GetType(string typeName)
 528    {
 176529        ArgumentException.ThrowIfNullOrEmpty(typeName);
 530
 531        // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static 
 532        // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are 
 176533        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 176534            .Select(a => a.GetType(k))
 176535            .FirstOrDefault(t => t is not null));
 536    }
 537
 538    /// <inheritdoc  />
 539    public void SaveImages(BaseItemDto item)
 540    {
 0541        ArgumentNullException.ThrowIfNull(item);
 542
 0543        var images = item.ImageInfos.Select(e => Map(item.Id, e));
 0544        using var context = _dbProvider.CreateDbContext();
 545
 0546        if (!context.BaseItems.Any(bi => bi.Id == item.Id))
 547        {
 0548            _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
 0549            return;
 550        }
 551
 0552        context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
 0553        context.BaseItemImageInfos.AddRange(images);
 0554        context.SaveChanges();
 0555    }
 556
 557    /// <inheritdoc  />
 558    public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 559    {
 87560        UpdateOrInsertItems(items, cancellationToken);
 85561    }
 562
 563    /// <inheritdoc cref="IItemRepository"/>
 564    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 565    {
 87566        ArgumentNullException.ThrowIfNull(items);
 87567        cancellationToken.ThrowIfCancellationRequested();
 568
 85569        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 340570        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
 571        {
 85572            var ancestorIds = item.SupportsAncestors ?
 85573                item.GetAncestorIds().Distinct().ToList() :
 85574                null;
 575
 85576            var topParent = item.GetTopParent();
 577
 85578            var userdataKey = item.GetUserDataKeys();
 85579            var inheritedTags = item.GetInheritedTags();
 580
 85581            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 582        }
 583
 85584        using var context = _dbProvider.CreateDbContext();
 85585        using var transaction = context.Database.BeginTransaction();
 586
 85587        var ids = tuples.Select(f => f.Item.Id).ToArray();
 85588        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 85589        var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
 590
 340591        foreach (var item in tuples)
 592        {
 85593            var entity = Map(item.Item);
 594            // TODO: refactor this "inconsistency"
 85595            entity.TopParentId = item.TopParent?.Id;
 596
 85597            if (!existingItems.Any(e => e == entity.Id))
 598            {
 49599                context.BaseItems.Add(entity);
 600            }
 601            else
 602            {
 36603                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 36604                context.BaseItems.Attach(entity).State = EntityState.Modified;
 605            }
 606        }
 607
 85608        context.SaveChanges();
 609
 268610        foreach (var item in newItems)
 611        {
 612            // reattach old userData entries
 49613            var userKeys = item.UserDataKey.ToArray();
 49614            var retentionDate = (DateTime?)null;
 49615            context.UserData
 49616                .Where(e => e.ItemId == PlaceholderId)
 49617                .Where(e => userKeys.Contains(e.CustomDataKey))
 49618                .ExecuteUpdate(e => e
 49619                    .SetProperty(f => f.ItemId, item.Item.Id)
 49620                    .SetProperty(f => f.RetentionDate, retentionDate));
 621        }
 622
 85623        var itemValueMaps = tuples
 85624            .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 85625            .ToArray();
 85626        var allListedItemValues = itemValueMaps
 85627            .SelectMany(f => f.Values)
 85628            .Distinct()
 85629            .ToArray();
 85630        var existingValues = context.ItemValues
 85631            .Select(e => new
 85632            {
 85633                item = e,
 85634                Key = e.Type + "+" + e.Value
 85635            })
 85636            .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
 85637            .Select(e => e.item)
 85638            .ToArray();
 85639        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 85640        {
 85641            CleanValue = GetCleanValue(f.Value),
 85642            ItemValueId = Guid.NewGuid(),
 85643            Type = f.MagicNumber,
 85644            Value = f.Value
 85645        }).ToArray();
 85646        context.ItemValues.AddRange(missingItemValues);
 85647        context.SaveChanges();
 648
 85649        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 85650        var valueMap = itemValueMaps
 85651            .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type =
 85652            .ToArray();
 653
 85654        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 655
 340656        foreach (var item in valueMap)
 657        {
 85658            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 170659            foreach (var itemValue in item.Values)
 660            {
 0661                var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
 0662                if (existingItem is null)
 663                {
 0664                    context.ItemValuesMap.Add(new ItemValueMap()
 0665                    {
 0666                        Item = null!,
 0667                        ItemId = item.Item.Id,
 0668                        ItemValue = null!,
 0669                        ItemValueId = itemValue.ItemValueId
 0670                    });
 671                }
 672                else
 673                {
 674                    // map exists, remove from list so its been handled.
 0675                    itemMappedValues.Remove(existingItem);
 676                }
 677            }
 678
 679            // all still listed values are not in the new list so remove them.
 85680            context.ItemValuesMap.RemoveRange(itemMappedValues);
 681        }
 682
 85683        context.SaveChanges();
 684
 340685        foreach (var item in tuples)
 686        {
 85687            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 688            {
 85689                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 85690                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 190691                foreach (var ancestorId in validAncestorIds)
 692                {
 10693                    var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
 10694                    if (existingAncestorId is null)
 695                    {
 2696                        context.AncestorIds.Add(new AncestorId()
 2697                        {
 2698                            ParentItemId = ancestorId,
 2699                            ItemId = item.Item.Id,
 2700                            Item = null!,
 2701                            ParentItem = null!
 2702                        });
 703                    }
 704                    else
 705                    {
 8706                        existingAncestorIds.Remove(existingAncestorId);
 707                    }
 708                }
 709
 85710                context.AncestorIds.RemoveRange(existingAncestorIds);
 711            }
 712        }
 713
 85714        context.SaveChanges();
 85715        transaction.Commit();
 170716    }
 717
 718    /// <inheritdoc  />
 719    public BaseItemDto? RetrieveItem(Guid id)
 720    {
 216721        if (id.IsEmpty())
 722        {
 0723            throw new ArgumentException("Guid can't be empty", nameof(id));
 724        }
 725
 216726        using var context = _dbProvider.CreateDbContext();
 216727        var item = PrepareItemQuery(context, new()
 216728        {
 216729            DtoOptions = new()
 216730            {
 216731                EnableImages = true
 216732            }
 216733        }).FirstOrDefault(e => e.Id == id);
 216734        if (item is null)
 735        {
 216736            return null;
 737        }
 738
 0739        return DeserializeBaseItem(item);
 216740    }
 741
 742    /// <summary>
 743    /// Maps a Entity to the DTO.
 744    /// </summary>
 745    /// <param name="entity">The entity.</param>
 746    /// <param name="dto">The dto base instance.</param>
 747    /// <param name="appHost">The Application server Host.</param>
 748    /// <returns>The dto to map.</returns>
 749    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost)
 750    {
 88751        dto.Id = entity.Id;
 88752        dto.ParentId = entity.ParentId.GetValueOrDefault();
 88753        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 88754        dto.EndDate = entity.EndDate;
 88755        dto.CommunityRating = entity.CommunityRating;
 88756        dto.CustomRating = entity.CustomRating;
 88757        dto.IndexNumber = entity.IndexNumber;
 88758        dto.IsLocked = entity.IsLocked;
 88759        dto.Name = entity.Name;
 88760        dto.OfficialRating = entity.OfficialRating;
 88761        dto.Overview = entity.Overview;
 88762        dto.ParentIndexNumber = entity.ParentIndexNumber;
 88763        dto.PremiereDate = entity.PremiereDate;
 88764        dto.ProductionYear = entity.ProductionYear;
 88765        dto.SortName = entity.SortName;
 88766        dto.ForcedSortName = entity.ForcedSortName;
 88767        dto.RunTimeTicks = entity.RunTimeTicks;
 88768        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 88769        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 88770        dto.IsInMixedFolder = entity.IsInMixedFolder;
 88771        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 88772        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 88773        dto.CriticRating = entity.CriticRating;
 88774        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 88775        dto.OriginalTitle = entity.OriginalTitle;
 88776        dto.Album = entity.Album;
 88777        dto.LUFS = entity.LUFS;
 88778        dto.NormalizationGain = entity.NormalizationGain;
 88779        dto.IsVirtualItem = entity.IsVirtualItem;
 88780        dto.ExternalSeriesId = entity.ExternalSeriesId;
 88781        dto.Tagline = entity.Tagline;
 88782        dto.TotalBitrate = entity.TotalBitrate;
 88783        dto.ExternalId = entity.ExternalId;
 88784        dto.Size = entity.Size;
 88785        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 88786        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 88787        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 88788        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 88789        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 88790        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 88791        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 88792        dto.Width = entity.Width.GetValueOrDefault();
 88793        dto.Height = entity.Height.GetValueOrDefault();
 88794        if (entity.Provider is not null)
 795        {
 88796            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 797        }
 798
 88799        if (entity.ExtraType is not null)
 800        {
 0801            dto.ExtraType = (ExtraType)entity.ExtraType;
 802        }
 803
 88804        if (entity.LockedFields is not null)
 805        {
 88806            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 807        }
 808
 88809        if (entity.Audio is not null)
 810        {
 0811            dto.Audio = (ProgramAudio)entity.Audio;
 812        }
 813
 88814        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 88815        dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
 88816        dto.Studios = entity.Studios?.Split('|') ?? [];
 88817        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 818
 88819        if (dto is IHasProgramAttributes hasProgramAttributes)
 820        {
 0821            hasProgramAttributes.IsMovie = entity.IsMovie;
 0822            hasProgramAttributes.IsSeries = entity.IsSeries;
 0823            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0824            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 825        }
 826
 88827        if (dto is LiveTvChannel liveTvChannel)
 828        {
 0829            liveTvChannel.ServiceName = entity.ExternalServiceId;
 830        }
 831
 88832        if (dto is Trailer trailer)
 833        {
 0834            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 835        }
 836
 88837        if (dto is Video video)
 838        {
 0839            video.PrimaryVersionId = entity.PrimaryVersionId;
 840        }
 841
 88842        if (dto is IHasSeries hasSeriesName)
 843        {
 0844            hasSeriesName.SeriesName = entity.SeriesName;
 0845            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0846            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 847        }
 848
 88849        if (dto is Episode episode)
 850        {
 0851            episode.SeasonName = entity.SeasonName;
 0852            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 853        }
 854
 88855        if (dto is IHasArtist hasArtists)
 856        {
 0857            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 858        }
 859
 88860        if (dto is IHasAlbumArtist hasAlbumArtists)
 861        {
 0862            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 863        }
 864
 88865        if (dto is LiveTvProgram program)
 866        {
 0867            program.ShowId = entity.ShowId;
 868        }
 869
 88870        if (entity.Images is not null)
 871        {
 88872            dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
 873        }
 874
 875        // dto.Type = entity.Type;
 876        // dto.Data = entity.Data;
 877        // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
 88878        if (dto is IHasStartDate hasStartDate)
 879        {
 0880            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 881        }
 882
 883        // Fields that are present in the DB but are never actually used
 884        // dto.UnratedType = entity.UnratedType;
 885        // dto.TopParentId = entity.TopParentId;
 886        // dto.CleanName = entity.CleanName;
 887        // dto.UserDataKey = entity.UserDataKey;
 888
 88889        if (dto is Folder folder)
 890        {
 88891            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 892        }
 893
 88894        return dto;
 895    }
 896
 897    /// <summary>
 898    /// Maps a Entity to the DTO.
 899    /// </summary>
 900    /// <param name="dto">The entity.</param>
 901    /// <returns>The dto to map.</returns>
 902    public BaseItemEntity Map(BaseItemDto dto)
 903    {
 85904        var dtoType = dto.GetType();
 85905        var entity = new BaseItemEntity()
 85906        {
 85907            Type = dtoType.ToString(),
 85908            Id = dto.Id
 85909        };
 910
 85911        if (TypeRequiresDeserialization(dtoType))
 912        {
 64913            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 914        }
 915
 85916        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 85917        entity.Path = GetPathToSave(dto.Path);
 85918        entity.EndDate = dto.EndDate;
 85919        entity.CommunityRating = dto.CommunityRating;
 85920        entity.CustomRating = dto.CustomRating;
 85921        entity.IndexNumber = dto.IndexNumber;
 85922        entity.IsLocked = dto.IsLocked;
 85923        entity.Name = dto.Name;
 85924        entity.CleanName = GetCleanValue(dto.Name);
 85925        entity.OfficialRating = dto.OfficialRating;
 85926        entity.Overview = dto.Overview;
 85927        entity.ParentIndexNumber = dto.ParentIndexNumber;
 85928        entity.PremiereDate = dto.PremiereDate;
 85929        entity.ProductionYear = dto.ProductionYear;
 85930        entity.SortName = dto.SortName;
 85931        entity.ForcedSortName = dto.ForcedSortName;
 85932        entity.RunTimeTicks = dto.RunTimeTicks;
 85933        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 85934        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 85935        entity.IsInMixedFolder = dto.IsInMixedFolder;
 85936        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 85937        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 85938        entity.CriticRating = dto.CriticRating;
 85939        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 85940        entity.OriginalTitle = dto.OriginalTitle;
 85941        entity.Album = dto.Album;
 85942        entity.LUFS = dto.LUFS;
 85943        entity.NormalizationGain = dto.NormalizationGain;
 85944        entity.IsVirtualItem = dto.IsVirtualItem;
 85945        entity.ExternalSeriesId = dto.ExternalSeriesId;
 85946        entity.Tagline = dto.Tagline;
 85947        entity.TotalBitrate = dto.TotalBitrate;
 85948        entity.ExternalId = dto.ExternalId;
 85949        entity.Size = dto.Size;
 85950        entity.Genres = string.Join('|', dto.Genres);
 85951        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 85952        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 85953        entity.ChannelId = dto.ChannelId;
 85954        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 85955        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 85956        entity.OwnerId = dto.OwnerId.ToString();
 85957        entity.Width = dto.Width;
 85958        entity.Height = dto.Height;
 85959        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 85960        {
 85961            Item = entity,
 85962            ProviderId = e.Key,
 85963            ProviderValue = e.Value
 85964        }).ToList();
 965
 85966        if (dto.Audio.HasValue)
 967        {
 0968            entity.Audio = (ProgramAudioEntity)dto.Audio;
 969        }
 970
 85971        if (dto.ExtraType.HasValue)
 972        {
 0973            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 974        }
 975
 85976        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 85977        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : n
 85978        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
 85979        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
 85980        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 85981            .Select(e => new BaseItemMetadataField()
 85982            {
 85983                Id = (int)e,
 85984                Item = entity,
 85985                ItemId = entity.Id
 85986            })
 85987            .ToArray() : null;
 988
 85989        if (dto is IHasProgramAttributes hasProgramAttributes)
 990        {
 0991            entity.IsMovie = hasProgramAttributes.IsMovie;
 0992            entity.IsSeries = hasProgramAttributes.IsSeries;
 0993            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 0994            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 995        }
 996
 85997        if (dto is LiveTvChannel liveTvChannel)
 998        {
 0999            entity.ExternalServiceId = liveTvChannel.ServiceName;
 1000        }
 1001
 851002        if (dto is Video video)
 1003        {
 01004            entity.PrimaryVersionId = video.PrimaryVersionId;
 1005        }
 1006
 851007        if (dto is IHasSeries hasSeriesName)
 1008        {
 01009            entity.SeriesName = hasSeriesName.SeriesName;
 01010            entity.SeriesId = hasSeriesName.SeriesId;
 01011            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 1012        }
 1013
 851014        if (dto is Episode episode)
 1015        {
 01016            entity.SeasonName = episode.SeasonName;
 01017            entity.SeasonId = episode.SeasonId;
 1018        }
 1019
 851020        if (dto is IHasArtist hasArtists)
 1021        {
 01022            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
 1023        }
 1024
 851025        if (dto is IHasAlbumArtist hasAlbumArtists)
 1026        {
 01027            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 1028        }
 1029
 851030        if (dto is LiveTvProgram program)
 1031        {
 01032            entity.ShowId = program.ShowId;
 1033        }
 1034
 851035        if (dto.ImageInfos is not null)
 1036        {
 851037            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 1038        }
 1039
 851040        if (dto is Trailer trailer)
 1041        {
 01042            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 01043            {
 01044                Id = (int)e,
 01045                Item = entity,
 01046                ItemId = entity.Id
 01047            }).ToArray() ?? [];
 1048        }
 1049
 1050        // dto.Type = entity.Type;
 1051        // dto.Data = entity.Data;
 851052        entity.MediaType = dto.MediaType.ToString();
 851053        if (dto is IHasStartDate hasStartDate)
 1054        {
 01055            entity.StartDate = hasStartDate.StartDate;
 1056        }
 1057
 851058        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 1059
 1060        // Fields that are present in the DB but are never actually used
 1061        // dto.UserDataKey = entity.UserDataKey;
 1062
 851063        if (dto is Folder folder)
 1064        {
 851065            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 851066            entity.IsFolder = folder.IsFolder;
 1067        }
 1068
 851069        return entity;
 1070    }
 1071
 1072    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 1073    {
 361074        using var context = _dbProvider.CreateDbContext();
 1075
 361076        var query = context.ItemValuesMap
 361077            .AsNoTracking()
 361078            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 361079        if (withItemTypes.Count > 0)
 1080        {
 91081            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 1082        }
 1083
 361084        if (excludeItemTypes.Count > 0)
 1085        {
 91086            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 1087        }
 1088
 1089        // query = query.DistinctBy(e => e.CleanValue);
 361090        return query.Select(e => e.ItemValue)
 361091            .GroupBy(e => e.CleanValue)
 361092            .Select(e => e.First().Value)
 361093            .ToArray();
 361094    }
 1095
 1096    private static bool TypeRequiresDeserialization(Type type)
 1097    {
 1731098        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 1099    }
 1100
 1101    private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 1102    {
 881103        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 881104        if (_serverConfigurationManager?.Configuration is null)
 1105        {
 01106            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 1107        }
 1108
 881109        var typeToSerialise = GetType(baseItemEntity.Type);
 881110        return BaseItemRepository.DeserializeBaseItem(
 881111            baseItemEntity,
 881112            _logger,
 881113            _appHost,
 881114            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 1115    }
 1116
 1117    /// <summary>
 1118    /// Deserializes a BaseItemEntity and sets all properties.
 1119    /// </summary>
 1120    /// <param name="baseItemEntity">The DB entity.</param>
 1121    /// <param name="logger">Logger.</param>
 1122    /// <param name="appHost">The application server Host.</param>
 1123    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 1124    /// <returns>A mapped BaseItem.</returns>
 1125    /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
 1126    public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost?
 1127    {
 881128        var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.
 881129        BaseItemDto? dto = null;
 881130        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1131        {
 1132            try
 1133            {
 231134                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 231135            }
 01136            catch (JsonException ex)
 1137            {
 01138                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01139            }
 1140        }
 1141
 881142        if (dto is null)
 1143        {
 651144            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1145        }
 1146
 881147        return Map(baseItemEntity, dto, appHost);
 1148    }
 1149
 1150    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 1151    {
 01152        ArgumentNullException.ThrowIfNull(filter);
 1153
 01154        if (!filter.Limit.HasValue)
 1155        {
 01156            filter.EnableTotalRecordCount = false;
 1157        }
 1158
 01159        using var context = _dbProvider.CreateDbContext();
 1160
 01161        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 01162        {
 01163            ExcludeItemTypes = filter.ExcludeItemTypes,
 01164            IncludeItemTypes = filter.IncludeItemTypes,
 01165            MediaTypes = filter.MediaTypes,
 01166            AncestorIds = filter.AncestorIds,
 01167            ItemIds = filter.ItemIds,
 01168            TopParentIds = filter.TopParentIds,
 01169            ParentId = filter.ParentId,
 01170            IsAiring = filter.IsAiring,
 01171            IsMovie = filter.IsMovie,
 01172            IsSports = filter.IsSports,
 01173            IsKids = filter.IsKids,
 01174            IsNews = filter.IsNews,
 01175            IsSeries = filter.IsSeries
 01176        });
 1177
 01178        var itemValuesQuery = context.ItemValues
 01179            .Where(f => itemValueTypes.Contains(f.Type))
 01180            .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
 01181            .Join(
 01182                innerQueryFilter,
 01183                fw => fw.w.ItemId,
 01184                g => g.Id,
 01185                (fw, g) => fw.f.CleanValue);
 1186
 01187        var innerQuery = PrepareItemQuery(context, filter)
 01188            .Where(e => e.Type == returnType)
 01189            .Where(e => itemValuesQuery.Contains(e.CleanName));
 1190
 01191        var outerQueryFilter = new InternalItemsQuery(filter.User)
 01192        {
 01193            IsPlayed = filter.IsPlayed,
 01194            IsFavorite = filter.IsFavorite,
 01195            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 01196            IsLiked = filter.IsLiked,
 01197            IsLocked = filter.IsLocked,
 01198            NameLessThan = filter.NameLessThan,
 01199            NameStartsWith = filter.NameStartsWith,
 01200            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 01201            Tags = filter.Tags,
 01202            OfficialRatings = filter.OfficialRatings,
 01203            StudioIds = filter.StudioIds,
 01204            GenreIds = filter.GenreIds,
 01205            Genres = filter.Genres,
 01206            Years = filter.Years,
 01207            NameContains = filter.NameContains,
 01208            SearchTerm = filter.SearchTerm,
 01209            ExcludeItemIds = filter.ExcludeItemIds
 01210        };
 1211
 01212        var query = TranslateQuery(innerQuery, context, outerQueryFilter)
 01213            .GroupBy(e => e.PresentationUniqueKey);
 1214
 01215        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 01216        if (filter.EnableTotalRecordCount)
 1217        {
 01218            result.TotalRecordCount = query.Count();
 1219        }
 1220
 01221        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 1222        {
 01223            var offset = filter.StartIndex ?? 0;
 1224
 01225            if (offset > 0)
 1226            {
 01227                query = query.Skip(offset);
 1228            }
 1229
 01230            if (filter.Limit.HasValue)
 1231            {
 01232                query = query.Take(filter.Limit.Value);
 1233            }
 1234        }
 1235
 01236        IQueryable<BaseItemEntity>? itemCountQuery = null;
 1237
 01238        if (filter.IncludeItemTypes.Length > 0)
 1239        {
 1240            // if we are to include more then one type, sub query those items beforehand.
 1241
 01242            var typeSubQuery = new InternalItemsQuery(filter.User)
 01243            {
 01244                ExcludeItemTypes = filter.ExcludeItemTypes,
 01245                IncludeItemTypes = filter.IncludeItemTypes,
 01246                MediaTypes = filter.MediaTypes,
 01247                AncestorIds = filter.AncestorIds,
 01248                ExcludeItemIds = filter.ExcludeItemIds,
 01249                ItemIds = filter.ItemIds,
 01250                TopParentIds = filter.TopParentIds,
 01251                ParentId = filter.ParentId,
 01252                IsPlayed = filter.IsPlayed
 01253            };
 1254
 01255            itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 01256                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 1257
 01258            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 01259            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 01260            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 01261            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 01262            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 01263            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 01264            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 1265
 01266            var resultQuery = query.Select(e => new
 01267            {
 01268                item = e.AsQueryable()
 01269                        .Include(e => e.TrailerTypes)
 01270                        .Include(e => e.Provider)
 01271                        .Include(e => e.LockedFields)
 01272                        .Include(e => e.Images)
 01273                        .AsSingleQuery().First(),
 01274                // TODO: This is bad refactor!
 01275                itemCount = new ItemCounts()
 01276                {
 01277                    SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
 01278                    EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
 01279                    MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
 01280                    AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
 01281                    ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
 01282                    SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
 01283                    TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
 01284                }
 01285            });
 1286
 01287            result.StartIndex = filter.StartIndex ?? 0;
 01288            result.Items =
 01289            [
 01290                .. resultQuery
 01291                    .AsEnumerable()
 01292                    .Where(e => e is not null)
 01293                    .Select(e =>
 01294                    {
 01295                        return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
 01296                    })
 01297            ];
 1298        }
 1299        else
 1300        {
 01301            result.StartIndex = filter.StartIndex ?? 0;
 01302            result.Items =
 01303            [
 01304                .. query
 01305                    .Select(e => e.First())
 01306                    .AsEnumerable()
 01307                    .Where(e => e is not null)
 01308                    .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
 01309                    {
 01310                        return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
 01311                    })
 01312            ];
 1313        }
 1314
 01315        return result;
 01316    }
 1317
 1318    private static void PrepareFilterQuery(InternalItemsQuery query)
 1319    {
 2821320        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 1321        {
 01322            query.Limit = query.Limit.Value + 4;
 1323        }
 1324
 2821325        if (query.IsResumable ?? false)
 1326        {
 11327            query.IsVirtualItem = false;
 1328        }
 2821329    }
 1330
 1331    private string GetCleanValue(string value)
 1332    {
 881333        if (string.IsNullOrWhiteSpace(value))
 1334        {
 01335            return value;
 1336        }
 1337
 881338        return value.RemoveDiacritics().ToLowerInvariant();
 1339    }
 1340
 1341    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1342    {
 851343        var list = new List<(ItemValueType, string)>();
 1344
 851345        if (item is IHasArtist hasArtist)
 1346        {
 01347            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1348        }
 1349
 851350        if (item is IHasAlbumArtist hasAlbumArtist)
 1351        {
 01352            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1353        }
 1354
 851355        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 851356        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 851357        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1358
 1359        // keywords was 5
 1360
 851361        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1362
 1363        // Remove all invalid values.
 851364        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1365
 851366        return list;
 1367    }
 1368
 1369    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1370    {
 01371        return new BaseItemImageInfo()
 01372        {
 01373            ItemId = baseItemId,
 01374            Id = Guid.NewGuid(),
 01375            Path = e.Path,
 01376            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 01377            DateModified = e.DateModified,
 01378            Height = e.Height,
 01379            Width = e.Width,
 01380            ImageType = (ImageInfoImageType)e.Type,
 01381            Item = null!
 01382        };
 1383    }
 1384
 1385    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 1386    {
 01387        return new ItemImageInfo()
 01388        {
 01389            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 01390            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 01391            DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
 01392            Height = e.Height,
 01393            Width = e.Width,
 01394            Type = (ImageType)e.ImageType
 01395        };
 1396    }
 1397
 1398    private string? GetPathToSave(string path)
 1399    {
 851400        if (path is null)
 1401        {
 01402            return null;
 1403        }
 1404
 851405        return _appHost.ReverseVirtualPath(path);
 1406    }
 1407
 1408    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1409    {
 171410        var list = new List<string>();
 1411
 171412        if (IsTypeInQuery(BaseItemKind.Person, query))
 1413        {
 11414            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1415        }
 1416
 171417        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1418        {
 11419            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1420        }
 1421
 171422        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1423        {
 11424            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1425        }
 1426
 171427        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1428        {
 11429            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1430        }
 1431
 171432        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1433        {
 11434            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1435        }
 1436
 171437        return list;
 1438    }
 1439
 1440    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1441    {
 851442        if (query.ExcludeItemTypes.Contains(type))
 1443        {
 01444            return false;
 1445        }
 1446
 851447        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 1448    }
 1449
 1450    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 1451    {
 01452        if (!query.GroupByPresentationUniqueKey)
 1453        {
 01454            return false;
 1455        }
 1456
 01457        if (query.GroupBySeriesPresentationUniqueKey)
 1458        {
 01459            return false;
 1460        }
 1461
 01462        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 1463        {
 01464            return false;
 1465        }
 1466
 01467        if (query.User is null)
 1468        {
 01469            return false;
 1470        }
 1471
 01472        if (query.IncludeItemTypes.Length == 0)
 1473        {
 01474            return true;
 1475        }
 1476
 01477        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 01478            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 01479            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01480            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 01481            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 01482            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 1483    }
 1484
 1485    private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter)
 1486    {
 2901487        var orderBy = filter.OrderBy;
 2901488        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1489
 2901490        if (hasSearch)
 1491        {
 01492            orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1493        }
 2901494        else if (orderBy.Count == 0)
 1495        {
 2091496            return query.OrderBy(e => e.SortName);
 1497        }
 1498
 811499        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1500
 811501        var firstOrdering = orderBy.FirstOrDefault();
 811502        if (firstOrdering != default)
 1503        {
 811504            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
 811505            if (firstOrdering.SortOrder == SortOrder.Ascending)
 1506            {
 801507                orderedQuery = query.OrderBy(expression);
 1508            }
 1509            else
 1510            {
 11511                orderedQuery = query.OrderByDescending(expression);
 1512            }
 1513
 811514            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 1515            {
 01516                if (firstOrdering.SortOrder is SortOrder.Ascending)
 1517                {
 01518                    orderedQuery = orderedQuery.ThenBy(e => e.Name);
 1519                }
 1520                else
 1521                {
 01522                    orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
 1523                }
 1524            }
 1525        }
 1526
 2361527        foreach (var item in orderBy.Skip(1))
 1528        {
 371529            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter);
 371530            if (item.SortOrder == SortOrder.Ascending)
 1531            {
 371532                orderedQuery = orderedQuery!.ThenBy(expression);
 1533            }
 1534            else
 1535            {
 01536                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1537            }
 1538        }
 1539
 811540        return orderedQuery ?? query;
 1541    }
 1542
 1543    private IQueryable<BaseItemEntity> TranslateQuery(
 1544        IQueryable<BaseItemEntity> baseQuery,
 1545        JellyfinDbContext context,
 1546        InternalItemsQuery filter)
 1547    {
 1548        const int HDWidth = 1200;
 1549        const int UHDWidth = 3800;
 1550        const int UHDHeight = 2100;
 1551
 2811552        var minWidth = filter.MinWidth;
 2811553        var maxWidth = filter.MaxWidth;
 2811554        var now = DateTime.UtcNow;
 1555
 2811556        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 1557        {
 01558            bool includeSD = false;
 01559            bool includeHD = false;
 01560            bool include4K = false;
 1561
 01562            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 1563            {
 01564                includeSD = true;
 1565            }
 1566
 01567            if (filter.IsHD.HasValue && filter.IsHD.Value)
 1568            {
 01569                includeHD = true;
 1570            }
 1571
 01572            if (filter.Is4K.HasValue && filter.Is4K.Value)
 1573            {
 01574                include4K = true;
 1575            }
 1576
 01577            baseQuery = baseQuery.Where(e =>
 01578                (includeSD && e.Width < HDWidth) ||
 01579                (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
 01580                (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
 1581        }
 1582
 2811583        if (minWidth.HasValue)
 1584        {
 01585            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1586        }
 1587
 2811588        if (filter.MinHeight.HasValue)
 1589        {
 01590            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1591        }
 1592
 2811593        if (maxWidth.HasValue)
 1594        {
 01595            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1596        }
 1597
 2811598        if (filter.MaxHeight.HasValue)
 1599        {
 01600            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1601        }
 1602
 2811603        if (filter.IsLocked.HasValue)
 1604        {
 271605            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1606        }
 1607
 2811608        var tags = filter.Tags.ToList();
 2811609        var excludeTags = filter.ExcludeTags.ToList();
 1610
 2811611        if (filter.IsMovie == true)
 1612        {
 01613            if (filter.IncludeItemTypes.Length == 0
 01614                || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01615                || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 1616            {
 01617                baseQuery = baseQuery.Where(e => e.IsMovie);
 1618            }
 1619        }
 2811620        else if (filter.IsMovie.HasValue)
 1621        {
 01622            baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
 1623        }
 1624
 2811625        if (filter.IsSeries.HasValue)
 1626        {
 01627            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1628        }
 1629
 2811630        if (filter.IsSports.HasValue)
 1631        {
 01632            if (filter.IsSports.Value)
 1633            {
 01634                tags.Add("Sports");
 1635            }
 1636            else
 1637            {
 01638                excludeTags.Add("Sports");
 1639            }
 1640        }
 1641
 2811642        if (filter.IsNews.HasValue)
 1643        {
 01644            if (filter.IsNews.Value)
 1645            {
 01646                tags.Add("News");
 1647            }
 1648            else
 1649            {
 01650                excludeTags.Add("News");
 1651            }
 1652        }
 1653
 2811654        if (filter.IsKids.HasValue)
 1655        {
 01656            if (filter.IsKids.Value)
 1657            {
 01658                tags.Add("Kids");
 1659            }
 1660            else
 1661            {
 01662                excludeTags.Add("Kids");
 1663            }
 1664        }
 1665
 2811666        if (!string.IsNullOrEmpty(filter.SearchTerm))
 1667        {
 01668            var searchTerm = filter.SearchTerm.ToLower();
 01669            baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && 
 1670        }
 1671
 2811672        if (filter.IsFolder.HasValue)
 1673        {
 211674            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1675        }
 1676
 2811677        var includeTypes = filter.IncludeItemTypes;
 1678
 1679        // Only specify excluded types if no included types are specified
 2811680        if (filter.IncludeItemTypes.Length == 0)
 1681        {
 2071682            var excludeTypes = filter.ExcludeItemTypes;
 2071683            if (excludeTypes.Length == 1)
 1684            {
 01685                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 1686                {
 01687                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 1688                }
 1689            }
 2071690            else if (excludeTypes.Length > 1)
 1691            {
 01692                var excludeTypeName = new List<string>();
 01693                foreach (var excludeType in excludeTypes)
 1694                {
 01695                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 1696                    {
 01697                        excludeTypeName.Add(baseItemKindName!);
 1698                    }
 1699                }
 1700
 01701                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 1702            }
 1703        }
 1704        else
 1705        {
 741706            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 741707            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1708        }
 1709
 2811710        if (filter.ChannelIds.Count > 0)
 1711        {
 01712            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1713        }
 1714
 2811715        if (!filter.ParentId.IsEmpty())
 1716        {
 1391717            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1718        }
 1719
 2811720        if (!string.IsNullOrWhiteSpace(filter.Path))
 1721        {
 01722            baseQuery = baseQuery.Where(e => e.Path == filter.Path);
 1723        }
 1724
 2811725        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1726        {
 01727            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1728        }
 1729
 2811730        if (filter.MinCommunityRating.HasValue)
 1731        {
 01732            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1733        }
 1734
 2811735        if (filter.MinIndexNumber.HasValue)
 1736        {
 01737            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1738        }
 1739
 2811740        if (filter.MinParentAndIndexNumber.HasValue)
 1741        {
 01742            baseQuery = baseQuery
 01743                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1744        }
 1745
 2811746        if (filter.MinDateCreated.HasValue)
 1747        {
 01748            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1749        }
 1750
 2811751        if (filter.MinDateLastSaved.HasValue)
 1752        {
 01753            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1754        }
 1755
 2811756        if (filter.MinDateLastSavedForUser.HasValue)
 1757        {
 01758            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1759        }
 1760
 2811761        if (filter.IndexNumber.HasValue)
 1762        {
 01763            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1764        }
 1765
 2811766        if (filter.ParentIndexNumber.HasValue)
 1767        {
 01768            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1769        }
 1770
 2811771        if (filter.ParentIndexNumberNotEquals.HasValue)
 1772        {
 01773            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1774        }
 1775
 2811776        var minEndDate = filter.MinEndDate;
 2811777        var maxEndDate = filter.MaxEndDate;
 1778
 2811779        if (filter.HasAired.HasValue)
 1780        {
 01781            if (filter.HasAired.Value)
 1782            {
 01783                maxEndDate = DateTime.UtcNow;
 1784            }
 1785            else
 1786            {
 01787                minEndDate = DateTime.UtcNow;
 1788            }
 1789        }
 1790
 2811791        if (minEndDate.HasValue)
 1792        {
 01793            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1794        }
 1795
 2811796        if (maxEndDate.HasValue)
 1797        {
 01798            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1799        }
 1800
 2811801        if (filter.MinStartDate.HasValue)
 1802        {
 01803            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1804        }
 1805
 2811806        if (filter.MaxStartDate.HasValue)
 1807        {
 01808            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1809        }
 1810
 2811811        if (filter.MinPremiereDate.HasValue)
 1812        {
 01813            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1814        }
 1815
 2811816        if (filter.MaxPremiereDate.HasValue)
 1817        {
 01818            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1819        }
 1820
 2811821        if (filter.TrailerTypes.Length > 0)
 1822        {
 01823            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 01824            baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
 1825        }
 1826
 2811827        if (filter.IsAiring.HasValue)
 1828        {
 01829            if (filter.IsAiring.Value)
 1830            {
 01831                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 1832            }
 1833            else
 1834            {
 01835                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 1836            }
 1837        }
 1838
 2811839        if (filter.PersonIds.Length > 0)
 1840        {
 01841            baseQuery = baseQuery
 01842                .Where(e =>
 01843                    context.PeopleBaseItemMap.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).A
 01844                        .Any(f => f.ItemId == e.Id));
 1845        }
 1846
 2811847        if (!string.IsNullOrWhiteSpace(filter.Person))
 1848        {
 01849            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 1850        }
 1851
 2811852        if (!string.IsNullOrWhiteSpace(filter.MinSortName))
 1853        {
 1854            // this does not makes sense.
 1855            // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
 1856            // whereClauses.Add("SortName>=@MinSortName");
 1857            // statement?.TryBind("@MinSortName", query.MinSortName);
 1858        }
 1859
 2811860        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 1861        {
 01862            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 1863        }
 1864
 2811865        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 1866        {
 01867            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 1868        }
 1869
 2811870        if (!string.IsNullOrWhiteSpace(filter.Name))
 1871        {
 31872            var cleanName = GetCleanValue(filter.Name);
 31873            baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 1874        }
 1875
 1876        // These are the same, for now
 2811877        var nameContains = filter.NameContains;
 2811878        if (!string.IsNullOrWhiteSpace(nameContains))
 1879        {
 01880            baseQuery = baseQuery.Where(e =>
 01881                e.CleanName!.Contains(nameContains)
 01882                || e.OriginalTitle!.ToLower().Contains(nameContains!));
 1883        }
 1884
 2811885        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 1886        {
 01887            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith) || e.Name!.StartsWith(filter.
 1888        }
 1889
 2811890        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 1891        {
 1892            // i hate this
 01893            baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!
 1894        }
 1895
 2811896        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 1897        {
 1898            // i hate this
 01899            baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDef
 1900        }
 1901
 2811902        if (filter.ImageTypes.Length > 0)
 1903        {
 801904            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 801905            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 1906        }
 1907
 2811908        if (filter.IsLiked.HasValue)
 1909        {
 01910            baseQuery = baseQuery
 01911                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLike
 1912        }
 1913
 2811914        if (filter.IsFavoriteOrLiked.HasValue)
 1915        {
 01916            baseQuery = baseQuery
 01917                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 1918        }
 1919
 2811920        if (filter.IsFavorite.HasValue)
 1921        {
 01922            baseQuery = baseQuery
 01923                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 1924        }
 1925
 2811926        if (filter.IsPlayed.HasValue)
 1927        {
 1928            // We should probably figure this out for all folders, but for right now, this is the only place where we ne
 01929            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
 1930            {
 01931                baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
 01932                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
 01933                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
 01934                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
 1935            }
 1936            else
 1937            {
 01938                baseQuery = baseQuery
 01939                    .Select(e => new
 01940                    {
 01941                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).Firs
 01942                        Item = e
 01943                    })
 01944                    .Where(e => e.IsPlayed == filter.IsPlayed)
 01945                    .Select(f => f.Item);
 1946            }
 1947        }
 1948
 2811949        if (filter.IsResumable.HasValue)
 1950        {
 11951            if (filter.IsResumable.Value)
 1952            {
 11953                baseQuery = baseQuery
 11954                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks >
 1955            }
 1956            else
 1957            {
 01958                baseQuery = baseQuery
 01959                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks =
 1960            }
 1961        }
 1962
 2811963        if (filter.ArtistIds.Length > 0)
 1964        {
 01965            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ArtistIds);
 1966        }
 1967
 2811968        if (filter.AlbumArtistIds.Length > 0)
 1969        {
 01970            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 1971        }
 1972
 2811973        if (filter.ContributingArtistIds.Length > 0)
 1974        {
 01975            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ContributingArtistIds);
 1976        }
 1977
 2811978        if (filter.AlbumIds.Length > 0)
 1979        {
 01980            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 01981            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 1982        }
 1983
 2811984        if (filter.ExcludeArtistIds.Length > 0)
 1985        {
 01986            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true);
 1987        }
 1988
 2811989        if (filter.GenreIds.Count > 0)
 1990        {
 01991            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 1992        }
 1993
 2811994        if (filter.Genres.Count > 0)
 1995        {
 01996            var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValue
 01997            baseQuery = baseQuery
 01998                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 1999        }
 2000
 2812001        if (tags.Count > 0)
 2002        {
 02003            var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, stri
 02004            baseQuery = baseQuery
 02005                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 2006        }
 2007
 2812008        if (excludeTags.Count > 0)
 2009        {
 02010            var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMa
 02011            baseQuery = baseQuery
 02012                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 2013        }
 2014
 2812015        if (filter.StudioIds.Length > 0)
 2016        {
 02017            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 2018        }
 2019
 2812020        if (filter.OfficialRatings.Length > 0)
 2021        {
 02022            baseQuery = baseQuery
 02023                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 2024        }
 2025
 2812026        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 2812027        if (filter.MinParentalRating != null)
 2028        {
 02029            var min = filter.MinParentalRating;
 02030            minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue
 02031            if (min.SubScore != null)
 2032            {
 02033                minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScor
 2034            }
 2035        }
 2036
 2812037        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 2812038        if (filter.MaxParentalRating != null)
 2039        {
 272040            var max = filter.MaxParentalRating;
 272041            maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue
 272042            if (max.SubScore != null)
 2043            {
 02044                maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScor
 2045            }
 2046        }
 2047
 2812048        if (filter.HasParentalRating ?? false)
 2049        {
 02050            if (minParentalRatingFilter != null)
 2051            {
 02052                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2053            }
 2054
 02055            if (maxParentalRatingFilter != null)
 2056            {
 02057                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2058            }
 2059        }
 2812060        else if (filter.BlockUnratedItems.Length > 0)
 2061        {
 02062            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 02063            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 2064
 02065            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 2066            {
 02067                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 2068            }
 02069            else if (minParentalRatingFilter != null)
 2070            {
 02071                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 2072            }
 02073            else if (maxParentalRatingFilter != null)
 2074            {
 02075                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 2076            }
 2077            else
 2078            {
 02079                baseQuery = baseQuery.Where(unratedItemFilter);
 2080            }
 2081        }
 2812082        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 2083        {
 272084            if (minParentalRatingFilter != null)
 2085            {
 02086                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2087            }
 2088
 272089            if (maxParentalRatingFilter != null)
 2090            {
 272091                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2092            }
 2093        }
 2542094        else if (!filter.HasParentalRating ?? false)
 2095        {
 02096            baseQuery = baseQuery
 02097                .Where(e => e.InheritedParentalRatingValue == null);
 2098        }
 2099
 2812100        if (filter.HasOfficialRating.HasValue)
 2101        {
 02102            if (filter.HasOfficialRating.Value)
 2103            {
 02104                baseQuery = baseQuery
 02105                    .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
 2106            }
 2107            else
 2108            {
 02109                baseQuery = baseQuery
 02110                    .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
 2111            }
 2112        }
 2113
 2812114        if (filter.HasOverview.HasValue)
 2115        {
 02116            if (filter.HasOverview.Value)
 2117            {
 02118                baseQuery = baseQuery
 02119                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 2120            }
 2121            else
 2122            {
 02123                baseQuery = baseQuery
 02124                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 2125            }
 2126        }
 2127
 2812128        if (filter.HasOwnerId.HasValue)
 2129        {
 02130            if (filter.HasOwnerId.Value)
 2131            {
 02132                baseQuery = baseQuery
 02133                    .Where(e => e.OwnerId != null);
 2134            }
 2135            else
 2136            {
 02137                baseQuery = baseQuery
 02138                    .Where(e => e.OwnerId == null);
 2139            }
 2140        }
 2141
 2812142        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 2143        {
 02144            baseQuery = baseQuery
 02145                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filte
 2146        }
 2147
 2812148        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2149        {
 02150            baseQuery = baseQuery
 02151                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2152        }
 2153
 2812154        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2155        {
 02156            baseQuery = baseQuery
 02157                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2158        }
 2159
 2812160        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 2161        {
 02162            baseQuery = baseQuery
 02163                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == fi
 2164        }
 2165
 2812166        if (filter.HasSubtitles.HasValue)
 2167        {
 02168            baseQuery = baseQuery
 02169                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2170        }
 2171
 2812172        if (filter.HasChapterImages.HasValue)
 2173        {
 02174            baseQuery = baseQuery
 02175                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2176        }
 2177
 2812178        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2179        {
 92180            baseQuery = baseQuery
 92181                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 2182        }
 2183
 2812184        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2185        {
 92186            baseQuery = baseQuery
 92187                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2188        }
 2189
 2812190        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2191        {
 92192            baseQuery = baseQuery
 92193                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2194        }
 2195
 2812196        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2197        {
 92198            baseQuery = baseQuery
 92199                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2200        }
 2201
 2812202        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2203        {
 02204            baseQuery = baseQuery
 02205                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2206        }
 2207
 2812208        if (filter.Years.Length > 0)
 2209        {
 02210            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2211        }
 2212
 2812213        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 2812214        if (isVirtualItem.HasValue)
 2215        {
 222216            baseQuery = baseQuery
 222217                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2218        }
 2219
 2812220        if (filter.IsSpecialSeason.HasValue)
 2221        {
 02222            if (filter.IsSpecialSeason.Value)
 2223            {
 02224                baseQuery = baseQuery
 02225                    .Where(e => e.IndexNumber == 0);
 2226            }
 2227            else
 2228            {
 02229                baseQuery = baseQuery
 02230                    .Where(e => e.IndexNumber != 0);
 2231            }
 2232        }
 2233
 2812234        if (filter.IsUnaired.HasValue)
 2235        {
 02236            if (filter.IsUnaired.Value)
 2237            {
 02238                baseQuery = baseQuery
 02239                    .Where(e => e.PremiereDate >= now);
 2240            }
 2241            else
 2242            {
 02243                baseQuery = baseQuery
 02244                    .Where(e => e.PremiereDate < now);
 2245            }
 2246        }
 2247
 2812248        if (filter.MediaTypes.Length > 0)
 2249        {
 212250            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 212251            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 2252        }
 2253
 2812254        if (filter.ItemIds.Length > 0)
 2255        {
 02256            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2257        }
 2258
 2812259        if (filter.ExcludeItemIds.Length > 0)
 2260        {
 02261            baseQuery = baseQuery
 02262                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 2263        }
 2264
 2812265        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 2266        {
 02267            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 02268            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 2269        }
 2270
 2812271        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 2272        {
 02273            var include = filter.HasAnyProviderId.Select(e => $"{e.Key}:{e.Value}").ToArray();
 02274            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => inc
 2275        }
 2276
 2812277        if (filter.HasImdbId.HasValue)
 2278        {
 02279            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
 2280        }
 2281
 2812282        if (filter.HasTmdbId.HasValue)
 2283        {
 02284            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
 2285        }
 2286
 2812287        if (filter.HasTvdbId.HasValue)
 2288        {
 02289            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
 2290        }
 2291
 2812292        var queryTopParentIds = filter.TopParentIds;
 2293
 2812294        if (queryTopParentIds.Length > 0)
 2295        {
 172296            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 172297            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 172298            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2299            {
 02300                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2301            }
 2302            else
 2303            {
 172304                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2305            }
 2306        }
 2307
 2812308        if (filter.AncestorIds.Length > 0)
 2309        {
 382310            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2311        }
 2312
 2812313        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 2314        {
 02315            baseQuery = baseQuery
 02316                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 2317        }
 2318
 2812319        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2320        {
 02321            baseQuery = baseQuery
 02322                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2323        }
 2324
 2812325        if (filter.ExcludeInheritedTags.Length > 0)
 2326        {
 02327            baseQuery = baseQuery
 02328                .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Typ
 02329                .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
 2330        }
 2331
 2812332        if (filter.IncludeInheritedTags.Length > 0)
 2333        {
 2334            // Episodes do not store inherit tags from their parents in the database, and the tag may be still required 
 2335            // In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s 
 02336            if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
 2337            {
 02338                baseQuery = baseQuery
 02339                    .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.
 02340                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
 02341                        ||
 02342                        (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.Item
 02343                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
 2344            }
 2345
 2346            // A playlist should be accessible to its owner regardless of allowed tags.
 02347            else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
 2348            {
 02349                baseQuery = baseQuery
 02350                    .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.
 02351                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
 02352                        || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
 2353                // d        ^^ this is stupid it hate this.
 2354            }
 2355            else
 2356            {
 02357                baseQuery = baseQuery
 02358                    .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.
 02359                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
 2360            }
 2361        }
 2362
 2812363        if (filter.SeriesStatuses.Length > 0)
 2364        {
 02365            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 02366            baseQuery = baseQuery
 02367                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 2368        }
 2369
 2812370        if (filter.BoxSetLibraryFolders.Length > 0)
 2371        {
 02372            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 02373            baseQuery = baseQuery
 02374                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 2375        }
 2376
 2812377        if (filter.VideoTypes.Length > 0)
 2378        {
 02379            var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
 02380            baseQuery = baseQuery
 02381                .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
 2382        }
 2383
 2812384        if (filter.Is3D.HasValue)
 2385        {
 02386            if (filter.Is3D.Value)
 2387            {
 02388                baseQuery = baseQuery
 02389                    .Where(e => e.Data!.Contains("Video3DFormat"));
 2390            }
 2391            else
 2392            {
 02393                baseQuery = baseQuery
 02394                    .Where(e => !e.Data!.Contains("Video3DFormat"));
 2395            }
 2396        }
 2397
 2812398        if (filter.IsPlaceHolder.HasValue)
 2399        {
 02400            if (filter.IsPlaceHolder.Value)
 2401            {
 02402                baseQuery = baseQuery
 02403                    .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
 2404            }
 2405            else
 2406            {
 02407                baseQuery = baseQuery
 02408                    .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
 2409            }
 2410        }
 2411
 2812412        if (filter.HasSpecialFeature.HasValue)
 2413        {
 02414            if (filter.HasSpecialFeature.Value)
 2415            {
 02416                baseQuery = baseQuery
 02417                    .Where(e => e.ExtraIds != null);
 2418            }
 2419            else
 2420            {
 02421                baseQuery = baseQuery
 02422                    .Where(e => e.ExtraIds == null);
 2423            }
 2424        }
 2425
 2812426        if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
 2427        {
 02428            if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo
 2429            {
 02430                baseQuery = baseQuery
 02431                    .Where(e => e.ExtraIds != null);
 2432            }
 2433            else
 2434            {
 02435                baseQuery = baseQuery
 02436                    .Where(e => e.ExtraIds == null);
 2437            }
 2438        }
 2439
 2812440        return baseQuery;
 2441    }
 2442
 2443    /// <inheritdoc/>
 2444    public async Task<bool> ItemExistsAsync(Guid id)
 2445    {
 2446        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 2447        await using (dbContext.ConfigureAwait(false))
 2448        {
 2449            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 2450        }
 2451    }
 2452}

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)
GetItemCounts(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)
DeserializeBaseItem(Jellyfin.Database.Implementations.Entities.BaseItemEntity,System.Boolean)
DeserializeBaseItem(Jellyfin.Database.Implementations.Entities.BaseItemEntity,Microsoft.Extensions.Logging.ILogger,MediaBrowser.Controller.IServerApplicationHost,System.Boolean)
GetItemValues(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>,System.String)
PrepareFilterQuery(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetCleanValue(System.String)
GetItemValuesToSave(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.List`1<System.String>)
Map(System.Guid,MediaBrowser.Controller.Entities.ItemImageInfo)
Map(Jellyfin.Database.Implementations.Entities.BaseItemImageInfo,MediaBrowser.Controller.IServerApplicationHost)
GetPathToSave(System.String)
GetItemByNameTypesInQuery(MediaBrowser.Controller.Entities.InternalItemsQuery)
IsTypeInQuery(Jellyfin.Data.Enums.BaseItemKind,MediaBrowser.Controller.Entities.InternalItemsQuery)
EnableGroupByPresentationUniqueKey(MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyOrder(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
TranslateQuery(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)