< 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: 330
Total branches: 694
Branch coverage: 47.5%
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(...)48.78%1448279.06%
Map(...)56.45%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    {
 2104        if (id.IsEmpty() || id.Equals(PlaceholderId))
 105        {
 0106            throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(id));
 107        }
 108
 2109        using var context = _dbProvider.CreateDbContext();
 2110        using var transaction = context.Database.BeginTransaction();
 111
 2112        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.
 2118        context.UserData
 2119            .Join(
 2120                context.UserData.Where(e => e.ItemId == id),
 2121                placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
 2122                userData => new { userData.UserId, userData.CustomDataKey },
 2123                (placeholder, userData) => placeholder)
 2124            .Where(e => e.ItemId == PlaceholderId)
 2125            .ExecuteDelete();
 126
 127        // Detach all user watch data
 2128        context.UserData.Where(e => e.ItemId == id)
 2129            .ExecuteUpdate(e => e
 2130                .SetProperty(f => f.RetentionDate, date)
 2131                .SetProperty(f => f.ItemId, PlaceholderId));
 132
 2133        context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
 2134        context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
 2135        context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
 2136        context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
 2137        context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
 2138        context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
 2139        context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
 2140        context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
 2141        context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
 2142        context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
 2143        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
 2144        context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
 2145        context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete();
 2146        context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
 2147        context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
 2148        context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
 2149        context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
 2150        context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete();
 2151        context.SaveChanges();
 2152        transaction.Commit();
 4153    }
 154
 155    /// <inheritdoc />
 156    public void UpdateInheritedValues()
 157    {
 13158        using var context = _dbProvider.CreateDbContext();
 13159        using var transaction = context.Database.BeginTransaction();
 160
 13161        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 162        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 13163        context.SaveChanges();
 164
 13165        transaction.Commit();
 26166    }
 167
 168    /// <inheritdoc />
 169    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 170    {
 13171        ArgumentNullException.ThrowIfNull(filter);
 13172        PrepareFilterQuery(filter);
 173
 13174        using var context = _dbProvider.CreateDbContext();
 13175        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 13176    }
 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    {
 13217        return GetItemValueNames(_getStudiosValueTypes, [], []);
 218    }
 219
 220    /// <inheritdoc />
 221    public IReadOnlyList<string> GetAllArtistNames()
 222    {
 13223        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 224    }
 225
 226    /// <inheritdoc />
 227    public IReadOnlyList<string> GetMusicGenreNames()
 228    {
 13229        return GetItemValueNames(
 13230            _getGenreValueTypes,
 13231            _itemTypeLookup.MusicGenreTypes,
 13232            []);
 233    }
 234
 235    /// <inheritdoc />
 236    public IReadOnlyList<string> GetGenreNames()
 237    {
 13238        return GetItemValueNames(
 13239            _getGenreValueTypes,
 13240            [],
 13241            _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    {
 316281        ArgumentNullException.ThrowIfNull(filter);
 316282        PrepareFilterQuery(filter);
 283
 316284        using var context = _dbProvider.CreateDbContext();
 316285        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 286
 316287        dbQuery = TranslateQuery(dbQuery, context, filter);
 288
 316289        dbQuery = ApplyGroupingFilter(dbQuery, filter);
 316290        dbQuery = ApplyQueryPaging(dbQuery, filter);
 291
 316292        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializ
 316293    }
 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        // }
 329396        dbQuery = dbQuery.Distinct();
 329397        dbQuery = ApplyOrder(dbQuery, filter);
 398
 329399        return dbQuery;
 400    }
 401
 402    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 403    {
 329404        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 405        {
 93406            var offset = filter.StartIndex ?? 0;
 407
 93408            if (offset > 0)
 409            {
 0410                dbQuery = dbQuery.Skip(offset);
 411            }
 412
 93413            if (filter.Limit.HasValue)
 414            {
 93415                dbQuery = dbQuery.Take(filter.Limit.Value);
 416            }
 417        }
 418
 329419        return dbQuery;
 420    }
 421
 422    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 423    {
 13424        dbQuery = TranslateQuery(dbQuery, context, filter);
 13425        dbQuery = ApplyOrder(dbQuery, filter);
 13426        dbQuery = ApplyGroupingFilter(dbQuery, filter);
 13427        dbQuery = ApplyQueryPaging(dbQuery, filter);
 13428        return dbQuery;
 429    }
 430
 431    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 432    {
 530433        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 530434        dbQuery = dbQuery.AsSingleQuery()
 530435            .Include(e => e.TrailerTypes)
 530436            .Include(e => e.Provider)
 530437            .Include(e => e.LockedFields);
 438
 530439        if (filter.DtoOptions.EnableImages)
 440        {
 530441            dbQuery = dbQuery.Include(e => e.Images);
 442        }
 443
 530444        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    {
 184529        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 
 184533        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 184534            .Select(a => a.GetType(k))
 184535            .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    {
 95560        UpdateOrInsertItems(items, cancellationToken);
 93561    }
 562
 563    /// <inheritdoc cref="IItemRepository"/>
 564    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 565    {
 95566        ArgumentNullException.ThrowIfNull(items);
 95567        cancellationToken.ThrowIfCancellationRequested();
 568
 93569        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 372570        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
 571        {
 93572            var ancestorIds = item.SupportsAncestors ?
 93573                item.GetAncestorIds().Distinct().ToList() :
 93574                null;
 575
 93576            var topParent = item.GetTopParent();
 577
 93578            var userdataKey = item.GetUserDataKeys();
 93579            var inheritedTags = item.GetInheritedTags();
 580
 93581            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 582        }
 583
 93584        using var context = _dbProvider.CreateDbContext();
 93585        using var transaction = context.Database.BeginTransaction();
 586
 93587        var ids = tuples.Select(f => f.Item.Id).ToArray();
 93588        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 93589        var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
 590
 372591        foreach (var item in tuples)
 592        {
 93593            var entity = Map(item.Item);
 594            // TODO: refactor this "inconsistency"
 93595            entity.TopParentId = item.TopParent?.Id;
 596
 93597            if (!existingItems.Any(e => e == entity.Id))
 598            {
 52599                context.BaseItems.Add(entity);
 600            }
 601            else
 602            {
 41603                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 41604                context.BaseItems.Attach(entity).State = EntityState.Modified;
 605            }
 606        }
 607
 93608        context.SaveChanges();
 609
 290610        foreach (var item in newItems)
 611        {
 612            // reattach old userData entries
 52613            var userKeys = item.UserDataKey.ToArray();
 52614            var retentionDate = (DateTime?)null;
 52615            context.UserData
 52616                .Where(e => e.ItemId == PlaceholderId)
 52617                .Where(e => userKeys.Contains(e.CustomDataKey))
 52618                .ExecuteUpdate(e => e
 52619                    .SetProperty(f => f.ItemId, item.Item.Id)
 52620                    .SetProperty(f => f.RetentionDate, retentionDate));
 621        }
 622
 93623        var itemValueMaps = tuples
 93624            .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 93625            .ToArray();
 93626        var allListedItemValues = itemValueMaps
 93627            .SelectMany(f => f.Values)
 93628            .Distinct()
 93629            .ToArray();
 93630        var existingValues = context.ItemValues
 93631            .Select(e => new
 93632            {
 93633                item = e,
 93634                Key = e.Type + "+" + e.Value
 93635            })
 93636            .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
 93637            .Select(e => e.item)
 93638            .ToArray();
 93639        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 93640        {
 93641            CleanValue = GetCleanValue(f.Value),
 93642            ItemValueId = Guid.NewGuid(),
 93643            Type = f.MagicNumber,
 93644            Value = f.Value
 93645        }).ToArray();
 93646        context.ItemValues.AddRange(missingItemValues);
 93647        context.SaveChanges();
 648
 93649        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 93650        var valueMap = itemValueMaps
 93651            .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type =
 93652            .ToArray();
 653
 93654        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 655
 372656        foreach (var item in valueMap)
 657        {
 93658            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 186659            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.
 93680            context.ItemValuesMap.RemoveRange(itemMappedValues);
 681        }
 682
 93683        context.SaveChanges();
 684
 372685        foreach (var item in tuples)
 686        {
 93687            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 688            {
 93689                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 93690                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 206691                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
 93710                context.AncestorIds.RemoveRange(existingAncestorIds);
 711            }
 712        }
 713
 93714        context.SaveChanges();
 93715        transaction.Commit();
 186716    }
 717
 718    /// <inheritdoc  />
 719    public BaseItemDto? RetrieveItem(Guid id)
 720    {
 214721        if (id.IsEmpty())
 722        {
 0723            throw new ArgumentException("Guid can't be empty", nameof(id));
 724        }
 725
 214726        using var context = _dbProvider.CreateDbContext();
 214727        var item = PrepareItemQuery(context, new()
 214728        {
 214729            DtoOptions = new()
 214730            {
 214731                EnableImages = true
 214732            }
 214733        }).FirstOrDefault(e => e.Id == id);
 214734        if (item is null)
 735        {
 214736            return null;
 737        }
 738
 0739        return DeserializeBaseItem(item);
 214740    }
 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    {
 92751        dto.Id = entity.Id;
 92752        dto.ParentId = entity.ParentId.GetValueOrDefault();
 92753        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 92754        dto.EndDate = entity.EndDate;
 92755        dto.CommunityRating = entity.CommunityRating;
 92756        dto.CustomRating = entity.CustomRating;
 92757        dto.IndexNumber = entity.IndexNumber;
 92758        dto.IsLocked = entity.IsLocked;
 92759        dto.Name = entity.Name;
 92760        dto.OfficialRating = entity.OfficialRating;
 92761        dto.Overview = entity.Overview;
 92762        dto.ParentIndexNumber = entity.ParentIndexNumber;
 92763        dto.PremiereDate = entity.PremiereDate;
 92764        dto.ProductionYear = entity.ProductionYear;
 92765        dto.SortName = entity.SortName;
 92766        dto.ForcedSortName = entity.ForcedSortName;
 92767        dto.RunTimeTicks = entity.RunTimeTicks;
 92768        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 92769        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 92770        dto.IsInMixedFolder = entity.IsInMixedFolder;
 92771        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 92772        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 92773        dto.CriticRating = entity.CriticRating;
 92774        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 92775        dto.OriginalTitle = entity.OriginalTitle;
 92776        dto.Album = entity.Album;
 92777        dto.LUFS = entity.LUFS;
 92778        dto.NormalizationGain = entity.NormalizationGain;
 92779        dto.IsVirtualItem = entity.IsVirtualItem;
 92780        dto.ExternalSeriesId = entity.ExternalSeriesId;
 92781        dto.Tagline = entity.Tagline;
 92782        dto.TotalBitrate = entity.TotalBitrate;
 92783        dto.ExternalId = entity.ExternalId;
 92784        dto.Size = entity.Size;
 92785        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 92786        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 92787        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 92788        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 92789        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 92790        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 92791        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 92792        dto.Width = entity.Width.GetValueOrDefault();
 92793        dto.Height = entity.Height.GetValueOrDefault();
 92794        if (entity.Provider is not null)
 795        {
 92796            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 797        }
 798
 92799        if (entity.ExtraType is not null)
 800        {
 0801            dto.ExtraType = (ExtraType)entity.ExtraType;
 802        }
 803
 92804        if (entity.LockedFields is not null)
 805        {
 92806            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 807        }
 808
 92809        if (entity.Audio is not null)
 810        {
 0811            dto.Audio = (ProgramAudio)entity.Audio;
 812        }
 813
 92814        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 92815        dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
 92816        dto.Studios = entity.Studios?.Split('|') ?? [];
 92817        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 818
 92819        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
 92827        if (dto is LiveTvChannel liveTvChannel)
 828        {
 0829            liveTvChannel.ServiceName = entity.ExternalServiceId;
 830        }
 831
 92832        if (dto is Trailer trailer)
 833        {
 0834            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 835        }
 836
 92837        if (dto is Video video)
 838        {
 0839            video.PrimaryVersionId = entity.PrimaryVersionId;
 840        }
 841
 92842        if (dto is IHasSeries hasSeriesName)
 843        {
 0844            hasSeriesName.SeriesName = entity.SeriesName;
 0845            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0846            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 847        }
 848
 92849        if (dto is Episode episode)
 850        {
 0851            episode.SeasonName = entity.SeasonName;
 0852            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 853        }
 854
 92855        if (dto is IHasArtist hasArtists)
 856        {
 0857            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 858        }
 859
 92860        if (dto is IHasAlbumArtist hasAlbumArtists)
 861        {
 0862            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 863        }
 864
 92865        if (dto is LiveTvProgram program)
 866        {
 0867            program.ShowId = entity.ShowId;
 868        }
 869
 92870        if (entity.Images is not null)
 871        {
 92872            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);
 92878        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
 92889        if (dto is Folder folder)
 890        {
 92891            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 892        }
 893
 92894        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    {
 93904        var dtoType = dto.GetType();
 93905        var entity = new BaseItemEntity()
 93906        {
 93907            Type = dtoType.ToString(),
 93908            Id = dto.Id
 93909        };
 910
 93911        if (TypeRequiresDeserialization(dtoType))
 912        {
 72913            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 914        }
 915
 93916        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 93917        entity.Path = GetPathToSave(dto.Path);
 93918        entity.EndDate = dto.EndDate;
 93919        entity.CommunityRating = dto.CommunityRating;
 93920        entity.CustomRating = dto.CustomRating;
 93921        entity.IndexNumber = dto.IndexNumber;
 93922        entity.IsLocked = dto.IsLocked;
 93923        entity.Name = dto.Name;
 93924        entity.CleanName = GetCleanValue(dto.Name);
 93925        entity.OfficialRating = dto.OfficialRating;
 93926        entity.Overview = dto.Overview;
 93927        entity.ParentIndexNumber = dto.ParentIndexNumber;
 93928        entity.PremiereDate = dto.PremiereDate;
 93929        entity.ProductionYear = dto.ProductionYear;
 93930        entity.SortName = dto.SortName;
 93931        entity.ForcedSortName = dto.ForcedSortName;
 93932        entity.RunTimeTicks = dto.RunTimeTicks;
 93933        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 93934        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 93935        entity.IsInMixedFolder = dto.IsInMixedFolder;
 93936        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 93937        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 93938        entity.CriticRating = dto.CriticRating;
 93939        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 93940        entity.OriginalTitle = dto.OriginalTitle;
 93941        entity.Album = dto.Album;
 93942        entity.LUFS = dto.LUFS;
 93943        entity.NormalizationGain = dto.NormalizationGain;
 93944        entity.IsVirtualItem = dto.IsVirtualItem;
 93945        entity.ExternalSeriesId = dto.ExternalSeriesId;
 93946        entity.Tagline = dto.Tagline;
 93947        entity.TotalBitrate = dto.TotalBitrate;
 93948        entity.ExternalId = dto.ExternalId;
 93949        entity.Size = dto.Size;
 93950        entity.Genres = string.Join('|', dto.Genres);
 93951        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 93952        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 93953        entity.ChannelId = dto.ChannelId;
 93954        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 93955        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 93956        entity.OwnerId = dto.OwnerId.ToString();
 93957        entity.Width = dto.Width;
 93958        entity.Height = dto.Height;
 93959        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 93960        {
 93961            Item = entity,
 93962            ProviderId = e.Key,
 93963            ProviderValue = e.Value
 93964        }).ToList();
 965
 93966        if (dto.Audio.HasValue)
 967        {
 0968            entity.Audio = (ProgramAudioEntity)dto.Audio;
 969        }
 970
 93971        if (dto.ExtraType.HasValue)
 972        {
 0973            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 974        }
 975
 93976        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 93977        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : n
 93978        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
 93979        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
 93980        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 93981            .Select(e => new BaseItemMetadataField()
 93982            {
 93983                Id = (int)e,
 93984                Item = entity,
 93985                ItemId = entity.Id
 93986            })
 93987            .ToArray() : null;
 988
 93989        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
 93997        if (dto is LiveTvChannel liveTvChannel)
 998        {
 0999            entity.ExternalServiceId = liveTvChannel.ServiceName;
 1000        }
 1001
 931002        if (dto is Video video)
 1003        {
 01004            entity.PrimaryVersionId = video.PrimaryVersionId;
 1005        }
 1006
 931007        if (dto is IHasSeries hasSeriesName)
 1008        {
 01009            entity.SeriesName = hasSeriesName.SeriesName;
 01010            entity.SeriesId = hasSeriesName.SeriesId;
 01011            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 1012        }
 1013
 931014        if (dto is Episode episode)
 1015        {
 01016            entity.SeasonName = episode.SeasonName;
 01017            entity.SeasonId = episode.SeasonId;
 1018        }
 1019
 931020        if (dto is IHasArtist hasArtists)
 1021        {
 01022            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
 1023        }
 1024
 931025        if (dto is IHasAlbumArtist hasAlbumArtists)
 1026        {
 01027            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 1028        }
 1029
 931030        if (dto is LiveTvProgram program)
 1031        {
 01032            entity.ShowId = program.ShowId;
 1033        }
 1034
 931035        if (dto.ImageInfos is not null)
 1036        {
 931037            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 1038        }
 1039
 931040        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;
 931052        entity.MediaType = dto.MediaType.ToString();
 931053        if (dto is IHasStartDate hasStartDate)
 1054        {
 01055            entity.StartDate = hasStartDate.StartDate;
 1056        }
 1057
 931058        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
 931063        if (dto is Folder folder)
 1064        {
 931065            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 931066            entity.IsFolder = folder.IsFolder;
 1067        }
 1068
 931069        return entity;
 1070    }
 1071
 1072    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 1073    {
 521074        using var context = _dbProvider.CreateDbContext();
 1075
 521076        var query = context.ItemValuesMap
 521077            .AsNoTracking()
 521078            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 521079        if (withItemTypes.Count > 0)
 1080        {
 131081            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 1082        }
 1083
 521084        if (excludeItemTypes.Count > 0)
 1085        {
 131086            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 1087        }
 1088
 1089        // query = query.DistinctBy(e => e.CleanValue);
 521090        return query.Select(e => e.ItemValue)
 521091            .GroupBy(e => e.CleanValue)
 521092            .Select(e => e.First().Value)
 521093            .ToArray();
 521094    }
 1095
 1096    private static bool TypeRequiresDeserialization(Type type)
 1097    {
 1851098        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 1099    }
 1100
 1101    private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 1102    {
 921103        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 921104        if (_serverConfigurationManager?.Configuration is null)
 1105        {
 01106            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 1107        }
 1108
 921109        var typeToSerialise = GetType(baseItemEntity.Type);
 921110        return BaseItemRepository.DeserializeBaseItem(
 921111            baseItemEntity,
 921112            _logger,
 921113            _appHost,
 921114            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    {
 921128        var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.
 921129        BaseItemDto? dto = null;
 921130        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1131        {
 1132            try
 1133            {
 201134                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 201135            }
 01136            catch (JsonException ex)
 1137            {
 01138                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01139            }
 1140        }
 1141
 921142        if (dto is null)
 1143        {
 721144            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1145        }
 1146
 921147        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    {
 3291320        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 1321        {
 01322            query.Limit = query.Limit.Value + 4;
 1323        }
 1324
 3291325        if (query.IsResumable ?? false)
 1326        {
 11327            query.IsVirtualItem = false;
 1328        }
 3291329    }
 1330
 1331    private string GetCleanValue(string value)
 1332    {
 961333        if (string.IsNullOrWhiteSpace(value))
 1334        {
 01335            return value;
 1336        }
 1337
 961338        return value.RemoveDiacritics().ToLowerInvariant();
 1339    }
 1340
 1341    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1342    {
 931343        var list = new List<(ItemValueType, string)>();
 1344
 931345        if (item is IHasArtist hasArtist)
 1346        {
 01347            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1348        }
 1349
 931350        if (item is IHasAlbumArtist hasAlbumArtist)
 1351        {
 01352            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1353        }
 1354
 931355        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 931356        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 931357        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1358
 1359        // keywords was 5
 1360
 931361        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1362
 1363        // Remove all invalid values.
 931364        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1365
 931366        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    {
 931400        if (path is null)
 1401        {
 01402            return null;
 1403        }
 1404
 931405        return _appHost.ReverseVirtualPath(path);
 1406    }
 1407
 1408    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1409    {
 141410        var list = new List<string>();
 1411
 141412        if (IsTypeInQuery(BaseItemKind.Person, query))
 1413        {
 11414            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1415        }
 1416
 141417        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1418        {
 11419            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1420        }
 1421
 141422        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1423        {
 11424            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1425        }
 1426
 141427        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1428        {
 11429            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1430        }
 1431
 141432        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1433        {
 11434            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1435        }
 1436
 141437        return list;
 1438    }
 1439
 1440    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1441    {
 701442        if (query.ExcludeItemTypes.Contains(type))
 1443        {
 01444            return false;
 1445        }
 1446
 701447        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    {
 3421487        var orderBy = filter.OrderBy;
 3421488        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1489
 3421490        if (hasSearch)
 1491        {
 01492            orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1493        }
 3421494        else if (orderBy.Count == 0)
 1495        {
 2481496            return query.OrderBy(e => e.SortName);
 1497        }
 1498
 941499        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1500
 941501        var firstOrdering = orderBy.FirstOrDefault();
 941502        if (firstOrdering != default)
 1503        {
 941504            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
 941505            if (firstOrdering.SortOrder == SortOrder.Ascending)
 1506            {
 931507                orderedQuery = query.OrderBy(expression);
 1508            }
 1509            else
 1510            {
 11511                orderedQuery = query.OrderByDescending(expression);
 1512            }
 1513
 941514            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
 2701527        foreach (var item in orderBy.Skip(1))
 1528        {
 411529            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter);
 411530            if (item.SortOrder == SortOrder.Ascending)
 1531            {
 411532                orderedQuery = orderedQuery!.ThenBy(expression);
 1533            }
 1534            else
 1535            {
 01536                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1537            }
 1538        }
 1539
 941540        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
 3291552        var minWidth = filter.MinWidth;
 3291553        var maxWidth = filter.MaxWidth;
 3291554        var now = DateTime.UtcNow;
 1555
 3291556        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
 3291583        if (minWidth.HasValue)
 1584        {
 01585            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1586        }
 1587
 3291588        if (filter.MinHeight.HasValue)
 1589        {
 01590            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1591        }
 1592
 3291593        if (maxWidth.HasValue)
 1594        {
 01595            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1596        }
 1597
 3291598        if (filter.MaxHeight.HasValue)
 1599        {
 01600            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1601        }
 1602
 3291603        if (filter.IsLocked.HasValue)
 1604        {
 391605            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1606        }
 1607
 3291608        var tags = filter.Tags.ToList();
 3291609        var excludeTags = filter.ExcludeTags.ToList();
 1610
 3291611        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        }
 3291620        else if (filter.IsMovie.HasValue)
 1621        {
 01622            baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
 1623        }
 1624
 3291625        if (filter.IsSeries.HasValue)
 1626        {
 01627            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1628        }
 1629
 3291630        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
 3291642        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
 3291654        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
 3291666        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
 3291672        if (filter.IsFolder.HasValue)
 1673        {
 211674            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1675        }
 1676
 3291677        var includeTypes = filter.IncludeItemTypes;
 1678
 1679        // Only specify excluded types if no included types are specified
 3291680        if (filter.IncludeItemTypes.Length == 0)
 1681        {
 2341682            var excludeTypes = filter.ExcludeItemTypes;
 2341683            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            }
 2341690            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        {
 951706            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 951707            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1708        }
 1709
 3291710        if (filter.ChannelIds.Count > 0)
 1711        {
 01712            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1713        }
 1714
 3291715        if (!filter.ParentId.IsEmpty())
 1716        {
 1581717            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1718        }
 1719
 3291720        if (!string.IsNullOrWhiteSpace(filter.Path))
 1721        {
 01722            baseQuery = baseQuery.Where(e => e.Path == filter.Path);
 1723        }
 1724
 3291725        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1726        {
 01727            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1728        }
 1729
 3291730        if (filter.MinCommunityRating.HasValue)
 1731        {
 01732            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1733        }
 1734
 3291735        if (filter.MinIndexNumber.HasValue)
 1736        {
 01737            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1738        }
 1739
 3291740        if (filter.MinParentAndIndexNumber.HasValue)
 1741        {
 01742            baseQuery = baseQuery
 01743                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1744        }
 1745
 3291746        if (filter.MinDateCreated.HasValue)
 1747        {
 01748            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1749        }
 1750
 3291751        if (filter.MinDateLastSaved.HasValue)
 1752        {
 01753            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1754        }
 1755
 3291756        if (filter.MinDateLastSavedForUser.HasValue)
 1757        {
 01758            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1759        }
 1760
 3291761        if (filter.IndexNumber.HasValue)
 1762        {
 01763            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1764        }
 1765
 3291766        if (filter.ParentIndexNumber.HasValue)
 1767        {
 01768            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1769        }
 1770
 3291771        if (filter.ParentIndexNumberNotEquals.HasValue)
 1772        {
 01773            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1774        }
 1775
 3291776        var minEndDate = filter.MinEndDate;
 3291777        var maxEndDate = filter.MaxEndDate;
 1778
 3291779        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
 3291791        if (minEndDate.HasValue)
 1792        {
 01793            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1794        }
 1795
 3291796        if (maxEndDate.HasValue)
 1797        {
 01798            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1799        }
 1800
 3291801        if (filter.MinStartDate.HasValue)
 1802        {
 01803            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1804        }
 1805
 3291806        if (filter.MaxStartDate.HasValue)
 1807        {
 01808            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1809        }
 1810
 3291811        if (filter.MinPremiereDate.HasValue)
 1812        {
 01813            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1814        }
 1815
 3291816        if (filter.MaxPremiereDate.HasValue)
 1817        {
 01818            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1819        }
 1820
 3291821        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
 3291827        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
 3291839        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
 3291847        if (!string.IsNullOrWhiteSpace(filter.Person))
 1848        {
 01849            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 1850        }
 1851
 3291852        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
 3291860        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 1861        {
 01862            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 1863        }
 1864
 3291865        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 1866        {
 01867            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 1868        }
 1869
 3291870        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
 3291877        var nameContains = filter.NameContains;
 3291878        if (!string.IsNullOrWhiteSpace(nameContains))
 1879        {
 01880            baseQuery = baseQuery.Where(e =>
 01881                e.CleanName!.Contains(nameContains)
 01882                || e.OriginalTitle!.ToLower().Contains(nameContains!));
 1883        }
 1884
 3291885        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 1886        {
 01887            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith));
 1888        }
 1889
 3291890        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
 3291896        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
 3291902        if (filter.ImageTypes.Length > 0)
 1903        {
 931904            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 931905            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 1906        }
 1907
 3291908        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
 3291914        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
 3291920        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
 3291926        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
 3291949        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
 3291963        if (filter.ArtistIds.Length > 0)
 1964        {
 01965            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ArtistIds);
 1966        }
 1967
 3291968        if (filter.AlbumArtistIds.Length > 0)
 1969        {
 01970            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 1971        }
 1972
 3291973        if (filter.ContributingArtistIds.Length > 0)
 1974        {
 01975            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ContributingArtistIds);
 1976        }
 1977
 3291978        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
 3291984        if (filter.ExcludeArtistIds.Length > 0)
 1985        {
 01986            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true);
 1987        }
 1988
 3291989        if (filter.GenreIds.Count > 0)
 1990        {
 01991            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 1992        }
 1993
 3291994        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
 3292001        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
 3292008        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
 3292015        if (filter.StudioIds.Length > 0)
 2016        {
 02017            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 2018        }
 2019
 3292020        if (filter.OfficialRatings.Length > 0)
 2021        {
 02022            baseQuery = baseQuery
 02023                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 2024        }
 2025
 3292026        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 3292027        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
 3292037        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 3292038        if (filter.MaxParentalRating != null)
 2039        {
 392040            var max = filter.MaxParentalRating;
 392041            maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue
 392042            if (max.SubScore != null)
 2043            {
 02044                maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScor
 2045            }
 2046        }
 2047
 3292048        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        }
 3292060        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        }
 3292082        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 2083        {
 392084            if (minParentalRatingFilter != null)
 2085            {
 02086                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2087            }
 2088
 392089            if (maxParentalRatingFilter != null)
 2090            {
 392091                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2092            }
 2093        }
 2902094        else if (!filter.HasParentalRating ?? false)
 2095        {
 02096            baseQuery = baseQuery
 02097                .Where(e => e.InheritedParentalRatingValue == null);
 2098        }
 2099
 3292100        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
 3292114        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
 3292128        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
 3292142        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
 3292148        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2149        {
 02150            baseQuery = baseQuery
 02151                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2152        }
 2153
 3292154        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2155        {
 02156            baseQuery = baseQuery
 02157                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2158        }
 2159
 3292160        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
 3292166        if (filter.HasSubtitles.HasValue)
 2167        {
 02168            baseQuery = baseQuery
 02169                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2170        }
 2171
 3292172        if (filter.HasChapterImages.HasValue)
 2173        {
 02174            baseQuery = baseQuery
 02175                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2176        }
 2177
 3292178        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2179        {
 132180            baseQuery = baseQuery
 132181                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 2182        }
 2183
 3292184        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2185        {
 132186            baseQuery = baseQuery
 132187                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2188        }
 2189
 3292190        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2191        {
 132192            baseQuery = baseQuery
 132193                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2194        }
 2195
 3292196        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2197        {
 132198            baseQuery = baseQuery
 132199                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2200        }
 2201
 3292202        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2203        {
 02204            baseQuery = baseQuery
 02205                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2206        }
 2207
 3292208        if (filter.Years.Length > 0)
 2209        {
 02210            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2211        }
 2212
 3292213        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 3292214        if (isVirtualItem.HasValue)
 2215        {
 222216            baseQuery = baseQuery
 222217                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2218        }
 2219
 3292220        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
 3292234        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
 3292248        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
 3292254        if (filter.ItemIds.Length > 0)
 2255        {
 02256            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2257        }
 2258
 3292259        if (filter.ExcludeItemIds.Length > 0)
 2260        {
 02261            baseQuery = baseQuery
 02262                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 2263        }
 2264
 3292265        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
 3292271        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
 3292277        if (filter.HasImdbId.HasValue)
 2278        {
 02279            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
 2280        }
 2281
 3292282        if (filter.HasTmdbId.HasValue)
 2283        {
 02284            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
 2285        }
 2286
 3292287        if (filter.HasTvdbId.HasValue)
 2288        {
 02289            baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
 2290        }
 2291
 3292292        var queryTopParentIds = filter.TopParentIds;
 2293
 3292294        if (queryTopParentIds.Length > 0)
 2295        {
 142296            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 142297            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 142298            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2299            {
 02300                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2301            }
 2302            else
 2303            {
 142304                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2305            }
 2306        }
 2307
 3292308        if (filter.AncestorIds.Length > 0)
 2309        {
 422310            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2311        }
 2312
 3292313        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
 3292319        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2320        {
 02321            baseQuery = baseQuery
 02322                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2323        }
 2324
 3292325        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
 3292332        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
 3292363        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
 3292370        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
 3292377        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
 3292384        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
 3292398        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
 3292412        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
 3292426        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
 3292440        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)