< Summary - Jellyfin

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

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/6/2025 - 12:11:15 AM Line coverage: 50.9% (668/1312) Branch coverage: 49% (358/730) Total lines: 261212/9/2025 - 12:12:43 AM Line coverage: 51.5% (683/1326) Branch coverage: 49.7% (368/740) Total lines: 265212/10/2025 - 12:13:43 AM Line coverage: 51.5% (681/1322) Branch coverage: 49.3% (369/748) Total lines: 264212/14/2025 - 12:12:01 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/20/2025 - 12:12:47 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.7% (371/746) Total lines: 265412/21/2025 - 12:12:15 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/29/2025 - 12:13:19 AM Line coverage: 51.9% (685/1318) Branch coverage: 50% (378/756) Total lines: 26761/10/2026 - 12:12:36 AM Line coverage: 52.3% (692/1322) Branch coverage: 50.6% (383/756) Total lines: 26821/11/2026 - 12:11:48 AM Line coverage: 52.3% (692/1322) Branch coverage: 50.9% (383/752) Total lines: 26711/19/2026 - 12:13:54 AM Line coverage: 51.8% (684/1318) Branch coverage: 50.6% (383/756) Total lines: 26871/29/2026 - 12:13:32 AM Line coverage: 51.4% (689/1338) Branch coverage: 50.5% (386/764) Total lines: 27353/11/2026 - 12:13:45 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 2736 12/6/2025 - 12:11:15 AM Line coverage: 50.9% (668/1312) Branch coverage: 49% (358/730) Total lines: 261212/9/2025 - 12:12:43 AM Line coverage: 51.5% (683/1326) Branch coverage: 49.7% (368/740) Total lines: 265212/10/2025 - 12:13:43 AM Line coverage: 51.5% (681/1322) Branch coverage: 49.3% (369/748) Total lines: 264212/14/2025 - 12:12:01 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/20/2025 - 12:12:47 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.7% (371/746) Total lines: 265412/21/2025 - 12:12:15 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/29/2025 - 12:13:19 AM Line coverage: 51.9% (685/1318) Branch coverage: 50% (378/756) Total lines: 26761/10/2026 - 12:12:36 AM Line coverage: 52.3% (692/1322) Branch coverage: 50.6% (383/756) Total lines: 26821/11/2026 - 12:11:48 AM Line coverage: 52.3% (692/1322) Branch coverage: 50.9% (383/752) Total lines: 26711/19/2026 - 12:13:54 AM Line coverage: 51.8% (684/1318) Branch coverage: 50.6% (383/756) Total lines: 26871/29/2026 - 12:13:32 AM Line coverage: 51.4% (689/1338) Branch coverage: 50.5% (386/764) Total lines: 27353/11/2026 - 12:13:45 AM Line coverage: 51.4% (688/1337) Branch coverage: 50.5% (386/764) Total lines: 2736

Coverage delta

Coverage delta 1 -1

Metrics

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

File(s)

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2// Do not enforce that because EFCore cannot deal with cultures well.
 3#pragma warning disable CA1304 // Specify CultureInfo
 4#pragma warning disable CA1311 // Specify a culture or use an invariant version
 5#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 6
 7using System;
 8using System.Collections.Concurrent;
 9using System.Collections.Generic;
 10using System.Globalization;
 11using System.Linq;
 12using System.Linq.Expressions;
 13using System.Reflection;
 14using System.Text;
 15using System.Text.Json;
 16using System.Threading;
 17using System.Threading.Tasks;
 18using Jellyfin.Data.Enums;
 19using Jellyfin.Database.Implementations;
 20using Jellyfin.Database.Implementations.Entities;
 21using Jellyfin.Database.Implementations.Enums;
 22using Jellyfin.Extensions;
 23using Jellyfin.Extensions.Json;
 24using Jellyfin.Server.Implementations.Extensions;
 25using MediaBrowser.Common;
 26using MediaBrowser.Controller;
 27using MediaBrowser.Controller.Channels;
 28using MediaBrowser.Controller.Configuration;
 29using MediaBrowser.Controller.Entities;
 30using MediaBrowser.Controller.Entities.Audio;
 31using MediaBrowser.Controller.Entities.TV;
 32using MediaBrowser.Controller.LiveTv;
 33using MediaBrowser.Controller.Persistence;
 34using MediaBrowser.Model.Dto;
 35using MediaBrowser.Model.Entities;
 36using MediaBrowser.Model.LiveTv;
 37using MediaBrowser.Model.Querying;
 38using Microsoft.EntityFrameworkCore;
 39using Microsoft.Extensions.Logging;
 40using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 41using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 42
 43namespace Jellyfin.Server.Implementations.Item;
 44
 45/*
 46    All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null
 47    This is done as the code isn't actually executed client side, but only the expressions are interpret and the compile
 48    This is your only warning/message regarding this topic.
 49*/
 50
 51/// <summary>
 52/// Handles all storage logic for BaseItems.
 53/// </summary>
 54public sealed class BaseItemRepository
 55    : IItemRepository
 56{
 57    /// <summary>
 58    /// Gets the placeholder id for UserData detached items.
 59    /// </summary>
 260    public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
 61
 62    /// <summary>
 63    /// This holds all the types in the running assemblies
 64    /// so that we can de-serialize properly when we don't have strong types.
 65    /// </summary>
 266    private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
 67    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 68    private readonly IServerApplicationHost _appHost;
 69    private readonly IItemTypeLookup _itemTypeLookup;
 70    private readonly IServerConfigurationManager _serverConfigurationManager;
 71    private readonly ILogger<BaseItemRepository> _logger;
 72
 273    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 274    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 275    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 276    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 277    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 278    private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
 79
 80    /// <summary>
 81    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
 82    /// </summary>
 83    /// <param name="dbProvider">The db factory.</param>
 84    /// <param name="appHost">The Application host.</param>
 85    /// <param name="itemTypeLookup">The static type lookup.</param>
 86    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
 87    /// <param name="logger">System logger.</param>
 88    public BaseItemRepository(
 89        IDbContextFactory<JellyfinDbContext> dbProvider,
 90        IServerApplicationHost appHost,
 91        IItemTypeLookup itemTypeLookup,
 92        IServerConfigurationManager serverConfigurationManager,
 93        ILogger<BaseItemRepository> logger)
 94    {
 3195        _dbProvider = dbProvider;
 3196        _appHost = appHost;
 3197        _itemTypeLookup = itemTypeLookup;
 3198        _serverConfigurationManager = serverConfigurationManager;
 3199        _logger = logger;
 31100    }
 101
 102    /// <inheritdoc />
 103    public void DeleteItem(params IReadOnlyList<Guid> ids)
 104    {
 1105        if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
 106        {
 0107            throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
 108        }
 109
 1110        using var context = _dbProvider.CreateDbContext();
 1111        using var transaction = context.Database.BeginTransaction();
 112
 1113        var date = (DateTime?)DateTime.UtcNow;
 114
 1115        var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
 116
 117        // Remove any UserData entries for the placeholder item that would conflict with the UserData
 118        // being detached from the item being deleted. This is necessary because, during an update,
 119        // UserData may be reattached to a new entry, but some entries can be left behind.
 120        // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
 1121        context.UserData
 1122            .Join(
 1123                context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
 1124                placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
 1125                userData => new { userData.UserId, userData.CustomDataKey },
 1126                (placeholder, userData) => placeholder)
 1127            .Where(e => e.ItemId == PlaceholderId)
 1128            .ExecuteDelete();
 129
 130        // Detach all user watch data
 1131        context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
 1132            .ExecuteUpdate(e => e
 1133                .SetProperty(f => f.RetentionDate, date)
 1134                .SetProperty(f => f.ItemId, PlaceholderId));
 135
 1136        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1137        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
 1138        context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1139        context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1140        context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1141        context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1142        context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1143        context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
 1144        context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1145        context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1146        context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1147        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
 1148        context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1149        context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1150        context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1151        context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1152        var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distin
 1153        context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1154        context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
 1155        context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1156        context.SaveChanges();
 1157        transaction.Commit();
 2158    }
 159
 160    /// <inheritdoc />
 161    public void UpdateInheritedValues()
 162    {
 15163        using var context = _dbProvider.CreateDbContext();
 15164        using var transaction = context.Database.BeginTransaction();
 165
 15166        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 167        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 15168        context.SaveChanges();
 169
 15170        transaction.Commit();
 30171    }
 172
 173    /// <inheritdoc />
 174    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 175    {
 16176        ArgumentNullException.ThrowIfNull(filter);
 16177        PrepareFilterQuery(filter);
 178
 16179        using var context = _dbProvider.CreateDbContext();
 16180        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 16181    }
 182
 183    /// <inheritdoc />
 184    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 185    {
 0186        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 187    }
 188
 189    /// <inheritdoc />
 190    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 191    {
 0192        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 193    }
 194
 195    /// <inheritdoc />
 196    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 197    {
 0198        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 199    }
 200
 201    /// <inheritdoc />
 202    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 203    {
 0204        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 205    }
 206
 207    /// <inheritdoc />
 208    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 209    {
 0210        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 211    }
 212
 213    /// <inheritdoc />
 214    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 215    {
 0216        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 217    }
 218
 219    /// <inheritdoc />
 220    public IReadOnlyList<string> GetStudioNames()
 221    {
 16222        return GetItemValueNames(_getStudiosValueTypes, [], []);
 223    }
 224
 225    /// <inheritdoc />
 226    public IReadOnlyList<string> GetAllArtistNames()
 227    {
 16228        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 229    }
 230
 231    /// <inheritdoc />
 232    public IReadOnlyList<string> GetMusicGenreNames()
 233    {
 16234        return GetItemValueNames(
 16235            _getGenreValueTypes,
 16236            _itemTypeLookup.MusicGenreTypes,
 16237            []);
 238    }
 239
 240    /// <inheritdoc />
 241    public IReadOnlyList<string> GetGenreNames()
 242    {
 16243        return GetItemValueNames(
 16244            _getGenreValueTypes,
 16245            [],
 16246            _itemTypeLookup.MusicGenreTypes);
 247    }
 248
 249    /// <inheritdoc />
 250    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
 251    {
 1252        ArgumentNullException.ThrowIfNull(filter);
 1253        if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0))
 254        {
 1255            var returnList = GetItemList(filter);
 1256            return new QueryResult<BaseItemDto>(
 1257                filter.StartIndex,
 1258                returnList.Count,
 1259                returnList);
 260        }
 261
 0262        PrepareFilterQuery(filter);
 0263        var result = new QueryResult<BaseItemDto>();
 264
 0265        using var context = _dbProvider.CreateDbContext();
 266
 0267        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 268
 0269        dbQuery = TranslateQuery(dbQuery, context, filter);
 0270        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 271
 0272        if (filter.EnableTotalRecordCount)
 273        {
 0274            result.TotalRecordCount = dbQuery.Count();
 275        }
 276
 0277        dbQuery = ApplyQueryPaging(dbQuery, filter);
 0278        dbQuery = ApplyNavigations(dbQuery, filter);
 279
 0280        result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDe
 0281        result.StartIndex = filter.StartIndex ?? 0;
 0282        return result;
 0283    }
 284
 285    /// <inheritdoc />
 286    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
 287    {
 320288        ArgumentNullException.ThrowIfNull(filter);
 320289        PrepareFilterQuery(filter);
 290
 320291        using var context = _dbProvider.CreateDbContext();
 319292        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 293
 319294        dbQuery = TranslateQuery(dbQuery, context, filter);
 295
 319296        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 319297        dbQuery = ApplyQueryPaging(dbQuery, filter);
 298
 319299        var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
 319300        if (hasRandomSort)
 301        {
 62302            var orderedIds = dbQuery.Select(e => e.Id).ToList();
 62303            if (orderedIds.Count == 0)
 304            {
 62305                return Array.Empty<BaseItemDto>();
 306            }
 307
 0308            var itemsById = ApplyNavigations(context.BaseItems.Where(e => orderedIds.Contains(e.Id)), filter)
 0309                .AsEnumerable()
 0310                .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
 0311                .Where(dto => dto is not null)
 0312                .ToDictionary(i => i!.Id);
 313
 0314            return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!;
 315        }
 316
 257317        dbQuery = ApplyNavigations(dbQuery, filter);
 318
 257319        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializ
 319320    }
 321
 322    /// <inheritdoc/>
 323    public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 324    {
 0325        ArgumentNullException.ThrowIfNull(filter);
 0326        PrepareFilterQuery(filter);
 327
 328        // Early exit if collection type is not tvshows or music
 0329        if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music)
 330        {
 0331            return Array.Empty<BaseItem>();
 332        }
 333
 0334        using var context = _dbProvider.CreateDbContext();
 335
 336        // Subquery to group by SeriesNames/Album and get the max Date Created for each group.
 0337        var subquery = PrepareItemQuery(context, filter);
 0338        subquery = TranslateQuery(subquery, context, filter);
 0339        var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album)
 0340            .Select(g => new
 0341            {
 0342                Key = g.Key,
 0343                MaxDateCreated = g.Max(a => a.DateCreated)
 0344            })
 0345            .OrderByDescending(g => g.MaxDateCreated)
 0346            .Select(g => g);
 347
 0348        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 349        {
 0350            subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
 351        }
 352
 0353        filter.Limit = null;
 354
 0355        var mainquery = PrepareItemQuery(context, filter);
 0356        mainquery = TranslateQuery(mainquery, context, filter);
 0357        mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
 0358        mainquery = ApplyGroupingFilter(context, mainquery, filter);
 0359        mainquery = ApplyQueryPaging(mainquery, filter);
 360
 0361        mainquery = ApplyNavigations(mainquery, filter);
 362
 0363        return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserial
 0364    }
 365
 366    /// <inheritdoc />
 367    public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
 368    {
 0369        ArgumentNullException.ThrowIfNull(filter);
 0370        ArgumentNullException.ThrowIfNull(filter.User);
 371
 0372        using var context = _dbProvider.CreateDbContext();
 373
 0374        var query = context.BaseItems
 0375            .AsNoTracking()
 0376            .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
 0377            .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
 0378            .Join(
 0379                context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
 0380                i => new { UserId = filter.User.Id, ItemId = i.Id },
 0381                u => new { UserId = u.UserId, ItemId = u.ItemId },
 0382                (entity, data) => new { Item = entity, UserData = data })
 0383            .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
 0384            .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
 0385            .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
 0386            .OrderByDescending(g => g.LastPlayedDate)
 0387            .Select(g => g.Key!);
 388
 0389        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 390        {
 0391            query = query.Take(filter.Limit.Value);
 392        }
 393
 0394        return query.ToArray();
 0395    }
 396
 397    private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery
 398    {
 399        // This whole block is needed to filter duplicate entries on request
 400        // for the time being it cannot be used because it would destroy the ordering
 401        // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions b
 402        // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
 403
 335404        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 335405        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 406        {
 0407            var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(
 0408            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 409        }
 335410        else if (enableGroupByPresentationUniqueKey)
 411        {
 1412            var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!
 1413            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 414        }
 334415        else if (filter.GroupBySeriesPresentationUniqueKey)
 416        {
 0417            var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e
 0418            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 419        }
 420        else
 421        {
 334422            dbQuery = dbQuery.Distinct();
 423        }
 424
 335425        dbQuery = ApplyOrder(dbQuery, filter, context);
 426
 335427        return dbQuery;
 428    }
 429
 430    private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 431    {
 273432        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 433        {
 0434            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 435        }
 436
 273437        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 438        {
 272439            dbQuery = dbQuery.Include(e => e.Provider);
 440        }
 441
 273442        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 443        {
 272444            dbQuery = dbQuery.Include(e => e.LockedFields);
 445        }
 446
 273447        if (filter.DtoOptions.EnableUserData)
 448        {
 273449            dbQuery = dbQuery.Include(e => e.UserData);
 450        }
 451
 273452        if (filter.DtoOptions.EnableImages)
 453        {
 273454            dbQuery = dbQuery.Include(e => e.Images);
 455        }
 456
 273457        return dbQuery;
 458    }
 459
 460    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 461    {
 335462        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 463        {
 0464            dbQuery = dbQuery.Skip(filter.StartIndex.Value);
 465        }
 466
 335467        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 468        {
 103469            dbQuery = dbQuery.Take(filter.Limit.Value);
 470        }
 471
 335472        return dbQuery;
 473    }
 474
 475    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 476    {
 16477        dbQuery = TranslateQuery(dbQuery, context, filter);
 16478        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 16479        dbQuery = ApplyQueryPaging(dbQuery, filter);
 16480        dbQuery = ApplyNavigations(dbQuery, filter);
 16481        return dbQuery;
 482    }
 483
 484    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 485    {
 405486        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 405487        dbQuery = dbQuery.AsSingleQuery();
 488
 405489        return dbQuery;
 490    }
 491
 492    /// <inheritdoc/>
 493    public int GetCount(InternalItemsQuery filter)
 494    {
 0495        ArgumentNullException.ThrowIfNull(filter);
 496        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0497        PrepareFilterQuery(filter);
 498
 0499        using var context = _dbProvider.CreateDbContext();
 0500        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 501
 0502        return dbQuery.Count();
 0503    }
 504
 505    /// <inheritdoc />
 506    public ItemCounts GetItemCounts(InternalItemsQuery filter)
 507    {
 0508        ArgumentNullException.ThrowIfNull(filter);
 509        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0510        PrepareFilterQuery(filter);
 511
 0512        using var context = _dbProvider.CreateDbContext();
 0513        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 514
 0515        var counts = dbQuery
 0516            .GroupBy(x => x.Type)
 0517            .Select(x => new { x.Key, Count = x.Count() })
 0518            .ToArray();
 519
 0520        var lookup = _itemTypeLookup.BaseItemKindNames;
 0521        var result = new ItemCounts();
 0522        foreach (var count in counts)
 523        {
 0524            if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
 525            {
 0526                result.AlbumCount = count.Count;
 527            }
 0528            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
 529            {
 0530                result.ArtistCount = count.Count;
 531            }
 0532            else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
 533            {
 0534                result.EpisodeCount = count.Count;
 535            }
 0536            else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
 537            {
 0538                result.MovieCount = count.Count;
 539            }
 0540            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
 541            {
 0542                result.MusicVideoCount = count.Count;
 543            }
 0544            else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
 545            {
 0546                result.ProgramCount = count.Count;
 547            }
 0548            else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
 549            {
 0550                result.SeriesCount = count.Count;
 551            }
 0552            else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
 553            {
 0554                result.SongCount = count.Count;
 555            }
 0556            else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
 557            {
 0558                result.TrailerCount = count.Count;
 559            }
 560        }
 561
 0562        return result;
 0563    }
 564
 565#pragma warning disable CA1307 // Specify StringComparison for clarity
 566    /// <summary>
 567    /// Gets the type.
 568    /// </summary>
 569    /// <param name="typeName">Name of the type.</param>
 570    /// <returns>Type.</returns>
 571    /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
 572    private static Type? GetType(string typeName)
 573    {
 143574        ArgumentException.ThrowIfNullOrEmpty(typeName);
 575
 576        // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static 
 577        // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are 
 143578        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 143579            .Select(a => a.GetType(k))
 143580            .FirstOrDefault(t => t is not null));
 581    }
 582
 583    /// <inheritdoc  />
 584    public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default)
 585    {
 586        ArgumentNullException.ThrowIfNull(item);
 587
 588        var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray();
 589
 590        var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 591        await using (context.ConfigureAwait(false))
 592        {
 593            if (!await context.BaseItems
 594                .AnyAsync(bi => bi.Id == item.Id, cancellationToken)
 595                .ConfigureAwait(false))
 596            {
 597                _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
 598                return;
 599            }
 600
 601            await context.BaseItemImageInfos
 602                .Where(e => e.ItemId == item.Id)
 603                .ExecuteDeleteAsync(cancellationToken)
 604                .ConfigureAwait(false);
 605
 606            await context.BaseItemImageInfos
 607                .AddRangeAsync(images, cancellationToken)
 608                .ConfigureAwait(false);
 609
 610            await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 611        }
 612    }
 613
 614    /// <inheritdoc  />
 615    public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 616    {
 111617        UpdateOrInsertItems(items, cancellationToken);
 110618    }
 619
 620    /// <inheritdoc cref="IItemRepository"/>
 621    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 622    {
 111623        ArgumentNullException.ThrowIfNull(items);
 111624        cancellationToken.ThrowIfCancellationRequested();
 625
 110626        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 440627        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
 628        {
 110629            var ancestorIds = item.SupportsAncestors ?
 110630                item.GetAncestorIds().Distinct().ToList() :
 110631                null;
 632
 110633            var topParent = item.GetTopParent();
 634
 110635            var userdataKey = item.GetUserDataKeys();
 110636            var inheritedTags = item.GetInheritedTags();
 637
 110638            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 639        }
 640
 110641        using var context = _dbProvider.CreateDbContext();
 110642        using var transaction = context.Database.BeginTransaction();
 643
 110644        var ids = tuples.Select(f => f.Item.Id).ToArray();
 110645        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 646
 440647        foreach (var item in tuples)
 648        {
 110649            var entity = Map(item.Item);
 650            // TODO: refactor this "inconsistency"
 110651            entity.TopParentId = item.TopParent?.Id;
 652
 110653            if (!existingItems.Any(e => e == entity.Id))
 654            {
 59655                context.BaseItems.Add(entity);
 656            }
 657            else
 658            {
 51659                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 51660                context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 51661                context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 662
 51663                if (entity.Images is { Count: > 0 })
 664                {
 0665                    context.BaseItemImageInfos.AddRange(entity.Images);
 666                }
 667
 51668                if (entity.LockedFields is { Count: > 0 })
 669                {
 0670                    context.BaseItemMetadataFields.AddRange(entity.LockedFields);
 671                }
 672
 51673                context.BaseItems.Attach(entity).State = EntityState.Modified;
 674            }
 675        }
 676
 110677        context.SaveChanges();
 678
 110679        var itemValueMaps = tuples
 110680            .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 110681            .ToArray();
 110682        var allListedItemValues = itemValueMaps
 110683            .SelectMany(f => f.Values)
 110684            .Distinct()
 110685            .ToArray();
 686
 110687        var types = allListedItemValues.Select(e => e.MagicNumber).Distinct().ToArray();
 110688        var values = allListedItemValues.Select(e => e.Value).Distinct().ToArray();
 110689        var allListedItemValuesSet = allListedItemValues.ToHashSet();
 690
 110691        var existingValues = context.ItemValues
 110692            .Where(e => types.Contains(e.Type) && values.Contains(e.Value))
 110693            .AsEnumerable()
 110694            .Where(e => allListedItemValuesSet.Contains((e.Type, e.Value)))
 110695            .ToArray();
 110696        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 110697        {
 110698            CleanValue = GetCleanValue(f.Value),
 110699            ItemValueId = Guid.NewGuid(),
 110700            Type = f.MagicNumber,
 110701            Value = f.Value
 110702        }).ToArray();
 110703        context.ItemValues.AddRange(missingItemValues);
 110704        context.SaveChanges();
 705
 110706        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 110707        var valueMap = itemValueMaps
 110708            .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type =
 110709            .ToArray();
 710
 110711        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 712
 440713        foreach (var item in valueMap)
 714        {
 110715            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 220716            foreach (var itemValue in item.Values)
 717            {
 0718                var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
 0719                if (existingItem is null)
 720                {
 0721                    context.ItemValuesMap.Add(new ItemValueMap()
 0722                    {
 0723                        Item = null!,
 0724                        ItemId = item.Item.Id,
 0725                        ItemValue = null!,
 0726                        ItemValueId = itemValue.ItemValueId
 0727                    });
 728                }
 729                else
 730                {
 731                    // map exists, remove from list so its been handled.
 0732                    itemMappedValues.Remove(existingItem);
 733                }
 734            }
 735
 736            // all still listed values are not in the new list so remove them.
 110737            context.ItemValuesMap.RemoveRange(itemMappedValues);
 738        }
 739
 110740        context.SaveChanges();
 741
 440742        foreach (var item in tuples)
 743        {
 110744            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 745            {
 110746                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 110747                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 274748                foreach (var ancestorId in validAncestorIds)
 749                {
 27750                    var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
 27751                    if (existingAncestorId is null)
 752                    {
 23753                        context.AncestorIds.Add(new AncestorId()
 23754                        {
 23755                            ParentItemId = ancestorId,
 23756                            ItemId = item.Item.Id,
 23757                            Item = null!,
 23758                            ParentItem = null!
 23759                        });
 760                    }
 761                    else
 762                    {
 4763                        existingAncestorIds.Remove(existingAncestorId);
 764                    }
 765                }
 766
 110767                context.AncestorIds.RemoveRange(existingAncestorIds);
 768            }
 769        }
 770
 110771        context.SaveChanges();
 110772        transaction.Commit();
 220773    }
 774
 775    /// <inheritdoc  />
 776    public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
 777    {
 778        ArgumentNullException.ThrowIfNull(item);
 779        cancellationToken.ThrowIfCancellationRequested();
 780
 781        var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 782
 783        await using (dbContext.ConfigureAwait(false))
 784        {
 785            var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
 786            await using (transaction.ConfigureAwait(false))
 787            {
 788                var userKeys = item.GetUserDataKeys().ToArray();
 789                var retentionDate = (DateTime?)null;
 790
 791                await dbContext.UserData
 792                    .Where(e => e.ItemId == PlaceholderId)
 793                    .Where(e => userKeys.Contains(e.CustomDataKey))
 794                    .ExecuteUpdateAsync(
 795                        e => e
 796                            .SetProperty(f => f.ItemId, item.Id)
 797                            .SetProperty(f => f.RetentionDate, retentionDate),
 798                        cancellationToken).ConfigureAwait(false);
 799
 800                // Rehydrate the cached userdata
 801                item.UserData = await dbContext.UserData
 802                    .AsNoTracking()
 803                    .Where(e => e.ItemId == item.Id)
 804                    .ToArrayAsync(cancellationToken)
 805                    .ConfigureAwait(false);
 806
 807                await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
 808            }
 809        }
 810    }
 811
 812    /// <inheritdoc  />
 813    public BaseItemDto? RetrieveItem(Guid id)
 814    {
 86815        if (id.IsEmpty())
 816        {
 0817            throw new ArgumentException("Guid can't be empty", nameof(id));
 818        }
 819
 86820        using var context = _dbProvider.CreateDbContext();
 86821        var dbQuery = PrepareItemQuery(context, new()
 86822        {
 86823            DtoOptions = new()
 86824            {
 86825                EnableImages = true
 86826            }
 86827        });
 86828        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 86829            .Include(e => e.Provider)
 86830            .Include(e => e.LockedFields)
 86831            .Include(e => e.UserData)
 86832            .Include(e => e.Images);
 833
 86834        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 86835        if (item is null)
 836        {
 86837            return null;
 838        }
 839
 0840        return DeserializeBaseItem(item);
 86841    }
 842
 843    /// <summary>
 844    /// Maps a Entity to the DTO.
 845    /// </summary>
 846    /// <param name="entity">The entity.</param>
 847    /// <param name="dto">The dto base instance.</param>
 848    /// <param name="appHost">The Application server Host.</param>
 849    /// <param name="logger">The applogger.</param>
 850    /// <returns>The dto to map.</returns>
 851    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 852    {
 71853        dto.Id = entity.Id;
 71854        dto.ParentId = entity.ParentId.GetValueOrDefault();
 71855        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 71856        dto.EndDate = entity.EndDate;
 71857        dto.CommunityRating = entity.CommunityRating;
 71858        dto.CustomRating = entity.CustomRating;
 71859        dto.IndexNumber = entity.IndexNumber;
 71860        dto.IsLocked = entity.IsLocked;
 71861        dto.Name = entity.Name;
 71862        dto.OfficialRating = entity.OfficialRating;
 71863        dto.Overview = entity.Overview;
 71864        dto.ParentIndexNumber = entity.ParentIndexNumber;
 71865        dto.PremiereDate = entity.PremiereDate;
 71866        dto.ProductionYear = entity.ProductionYear;
 71867        dto.SortName = entity.SortName;
 71868        dto.ForcedSortName = entity.ForcedSortName;
 71869        dto.RunTimeTicks = entity.RunTimeTicks;
 71870        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 71871        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 71872        dto.IsInMixedFolder = entity.IsInMixedFolder;
 71873        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 71874        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 71875        dto.CriticRating = entity.CriticRating;
 71876        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 71877        dto.OriginalTitle = entity.OriginalTitle;
 71878        dto.Album = entity.Album;
 71879        dto.LUFS = entity.LUFS;
 71880        dto.NormalizationGain = entity.NormalizationGain;
 71881        dto.IsVirtualItem = entity.IsVirtualItem;
 71882        dto.ExternalSeriesId = entity.ExternalSeriesId;
 71883        dto.Tagline = entity.Tagline;
 71884        dto.TotalBitrate = entity.TotalBitrate;
 71885        dto.ExternalId = entity.ExternalId;
 71886        dto.Size = entity.Size;
 71887        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 71888        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 71889        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 71890        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 71891        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 71892        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 71893        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 71894        dto.Width = entity.Width.GetValueOrDefault();
 71895        dto.Height = entity.Height.GetValueOrDefault();
 71896        dto.UserData = entity.UserData;
 897
 71898        if (entity.Provider is not null)
 899        {
 70900            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 901        }
 902
 71903        if (entity.ExtraType is not null)
 904        {
 0905            dto.ExtraType = (ExtraType)entity.ExtraType;
 906        }
 907
 71908        if (entity.LockedFields is not null)
 909        {
 70910            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 911        }
 912
 71913        if (entity.Audio is not null)
 914        {
 0915            dto.Audio = (ProgramAudio)entity.Audio;
 916        }
 917
 71918        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 71919        dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 71920        dto.Studios = entity.Studios?.Split('|') ?? [];
 71921        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 922
 71923        if (dto is IHasProgramAttributes hasProgramAttributes)
 924        {
 0925            hasProgramAttributes.IsMovie = entity.IsMovie;
 0926            hasProgramAttributes.IsSeries = entity.IsSeries;
 0927            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0928            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 929        }
 930
 71931        if (dto is LiveTvChannel liveTvChannel)
 932        {
 0933            liveTvChannel.ServiceName = entity.ExternalServiceId;
 934        }
 935
 71936        if (dto is Trailer trailer)
 937        {
 0938            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 939        }
 940
 71941        if (dto is Video video)
 942        {
 1943            video.PrimaryVersionId = entity.PrimaryVersionId;
 944        }
 945
 71946        if (dto is IHasSeries hasSeriesName)
 947        {
 0948            hasSeriesName.SeriesName = entity.SeriesName;
 0949            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0950            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 951        }
 952
 71953        if (dto is Episode episode)
 954        {
 0955            episode.SeasonName = entity.SeasonName;
 0956            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 957        }
 958
 71959        if (dto is IHasArtist hasArtists)
 960        {
 0961            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 962        }
 963
 71964        if (dto is IHasAlbumArtist hasAlbumArtists)
 965        {
 0966            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 967        }
 968
 71969        if (dto is LiveTvProgram program)
 970        {
 0971            program.ShowId = entity.ShowId;
 972        }
 973
 71974        if (entity.Images is not null)
 975        {
 70976            dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
 977        }
 978
 979        // dto.Type = entity.Type;
 980        // dto.Data = entity.Data;
 981        // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
 71982        if (dto is IHasStartDate hasStartDate)
 983        {
 0984            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 985        }
 986
 987        // Fields that are present in the DB but are never actually used
 988        // dto.UnratedType = entity.UnratedType;
 989        // dto.TopParentId = entity.TopParentId;
 990        // dto.CleanName = entity.CleanName;
 991        // dto.UserDataKey = entity.UserDataKey;
 992
 71993        if (dto is Folder folder)
 994        {
 70995            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 996        }
 997
 71998        return dto;
 999    }
 1000
 1001    /// <summary>
 1002    /// Maps a Entity to the DTO.
 1003    /// </summary>
 1004    /// <param name="dto">The entity.</param>
 1005    /// <returns>The dto to map.</returns>
 1006    public BaseItemEntity Map(BaseItemDto dto)
 1007    {
 1201008        var dtoType = dto.GetType();
 1201009        var entity = new BaseItemEntity()
 1201010        {
 1201011            Type = dtoType.ToString(),
 1201012            Id = dto.Id
 1201013        };
 1014
 1201015        if (TypeRequiresDeserialization(dtoType))
 1016        {
 991017            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 1018        }
 1019
 1201020        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 1201021        entity.Path = GetPathToSave(dto.Path);
 1201022        entity.EndDate = dto.EndDate;
 1201023        entity.CommunityRating = dto.CommunityRating;
 1201024        entity.CustomRating = dto.CustomRating;
 1201025        entity.IndexNumber = dto.IndexNumber;
 1201026        entity.IsLocked = dto.IsLocked;
 1201027        entity.Name = dto.Name;
 1201028        entity.CleanName = GetCleanValue(dto.Name);
 1201029        entity.OfficialRating = dto.OfficialRating;
 1201030        entity.Overview = dto.Overview;
 1201031        entity.ParentIndexNumber = dto.ParentIndexNumber;
 1201032        entity.PremiereDate = dto.PremiereDate;
 1201033        entity.ProductionYear = dto.ProductionYear;
 1201034        entity.SortName = dto.SortName;
 1201035        entity.ForcedSortName = dto.ForcedSortName;
 1201036        entity.RunTimeTicks = dto.RunTimeTicks;
 1201037        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 1201038        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 1201039        entity.IsInMixedFolder = dto.IsInMixedFolder;
 1201040        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 1201041        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 1201042        entity.CriticRating = dto.CriticRating;
 1201043        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 1201044        entity.OriginalTitle = dto.OriginalTitle;
 1201045        entity.Album = dto.Album;
 1201046        entity.LUFS = dto.LUFS;
 1201047        entity.NormalizationGain = dto.NormalizationGain;
 1201048        entity.IsVirtualItem = dto.IsVirtualItem;
 1201049        entity.ExternalSeriesId = dto.ExternalSeriesId;
 1201050        entity.Tagline = dto.Tagline;
 1201051        entity.TotalBitrate = dto.TotalBitrate;
 1201052        entity.ExternalId = dto.ExternalId;
 1201053        entity.Size = dto.Size;
 1201054        entity.Genres = string.Join('|', dto.Genres.Distinct(StringComparer.OrdinalIgnoreCase));
 1201055        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 1201056        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 1201057        entity.ChannelId = dto.ChannelId;
 1201058        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 1201059        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 1201060        entity.OwnerId = dto.OwnerId.ToString();
 1201061        entity.Width = dto.Width;
 1201062        entity.Height = dto.Height;
 1201063        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 1201064        {
 1201065            Item = entity,
 1201066            ProviderId = e.Key,
 1201067            ProviderValue = e.Value
 1201068        }).ToList();
 1069
 1201070        if (dto.Audio.HasValue)
 1071        {
 01072            entity.Audio = (ProgramAudioEntity)dto.Audio;
 1073        }
 1074
 1201075        if (dto.ExtraType.HasValue)
 1076        {
 01077            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 1078        }
 1079
 1201080        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 1201081        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Wher
 1201082        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios.Distinct(StringComparer.OrdinalIgnoreCas
 1201083        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags.Distinct(StringComparer.OrdinalIgnoreCase)) : nul
 1201084        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 1201085            .Select(e => new BaseItemMetadataField()
 1201086            {
 1201087                Id = (int)e,
 1201088                Item = entity,
 1201089                ItemId = entity.Id
 1201090            })
 1201091            .ToArray() : null;
 1092
 1201093        if (dto is IHasProgramAttributes hasProgramAttributes)
 1094        {
 01095            entity.IsMovie = hasProgramAttributes.IsMovie;
 01096            entity.IsSeries = hasProgramAttributes.IsSeries;
 01097            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 01098            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 1099        }
 1100
 1201101        if (dto is LiveTvChannel liveTvChannel)
 1102        {
 01103            entity.ExternalServiceId = liveTvChannel.ServiceName;
 1104        }
 1105
 1201106        if (dto is Video video)
 1107        {
 01108            entity.PrimaryVersionId = video.PrimaryVersionId;
 1109        }
 1110
 1201111        if (dto is IHasSeries hasSeriesName)
 1112        {
 01113            entity.SeriesName = hasSeriesName.SeriesName;
 01114            entity.SeriesId = hasSeriesName.SeriesId;
 01115            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 1116        }
 1117
 1201118        if (dto is Episode episode)
 1119        {
 01120            entity.SeasonName = episode.SeasonName;
 01121            entity.SeasonId = episode.SeasonId;
 1122        }
 1123
 1201124        if (dto is IHasArtist hasArtists)
 1125        {
 01126            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists.Distinct(StringCompare
 1127        }
 1128
 1201129        if (dto is IHasAlbumArtist hasAlbumArtists)
 1130        {
 01131            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 1132        }
 1133
 1201134        if (dto is LiveTvProgram program)
 1135        {
 01136            entity.ShowId = program.ShowId;
 1137        }
 1138
 1201139        if (dto.ImageInfos is not null)
 1140        {
 1201141            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 1142        }
 1143
 1201144        if (dto is Trailer trailer)
 1145        {
 01146            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 01147            {
 01148                Id = (int)e,
 01149                Item = entity,
 01150                ItemId = entity.Id
 01151            }).ToArray() ?? [];
 1152        }
 1153
 1154        // dto.Type = entity.Type;
 1155        // dto.Data = entity.Data;
 1201156        entity.MediaType = dto.MediaType.ToString();
 1201157        if (dto is IHasStartDate hasStartDate)
 1158        {
 01159            entity.StartDate = hasStartDate.StartDate;
 1160        }
 1161
 1201162        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 1163
 1164        // Fields that are present in the DB but are never actually used
 1165        // dto.UserDataKey = entity.UserDataKey;
 1166
 1201167        if (dto is Folder folder)
 1168        {
 1201169            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 1201170            entity.IsFolder = folder.IsFolder;
 1171        }
 1172
 1201173        return entity;
 1174    }
 1175
 1176    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 1177    {
 641178        using var context = _dbProvider.CreateDbContext();
 1179
 641180        var query = context.ItemValuesMap
 641181            .AsNoTracking()
 641182            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 641183        if (withItemTypes.Count > 0)
 1184        {
 161185            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 1186        }
 1187
 641188        if (excludeItemTypes.Count > 0)
 1189        {
 161190            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 1191        }
 1192
 1193        // query = query.DistinctBy(e => e.CleanValue);
 641194        return query.Select(e => e.ItemValue)
 641195            .GroupBy(e => e.CleanValue)
 641196            .Select(e => e.First().Value)
 641197            .ToArray();
 641198    }
 1199
 1200    private static bool TypeRequiresDeserialization(Type type)
 1201    {
 1911202        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 1203    }
 1204
 1205    private BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 1206    {
 701207        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 701208        if (_serverConfigurationManager?.Configuration is null)
 1209        {
 01210            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 1211        }
 1212
 701213        var typeToSerialise = GetType(baseItemEntity.Type);
 701214        return BaseItemRepository.DeserializeBaseItem(
 701215            baseItemEntity,
 701216            _logger,
 701217            _appHost,
 701218            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 1219    }
 1220
 1221    /// <summary>
 1222    /// Deserializes a BaseItemEntity and sets all properties.
 1223    /// </summary>
 1224    /// <param name="baseItemEntity">The DB entity.</param>
 1225    /// <param name="logger">Logger.</param>
 1226    /// <param name="appHost">The application server Host.</param>
 1227    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 1228    /// <returns>A mapped BaseItem, or null if the item type is unknown.</returns>
 1229    public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost
 1230    {
 731231        var type = GetType(baseItemEntity.Type);
 731232        if (type is null)
 1233        {
 21234            logger.LogWarning(
 21235                "Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database c
 21236                baseItemEntity.Id,
 21237                baseItemEntity.Type);
 21238            return null;
 1239        }
 1240
 711241        BaseItemDto? dto = null;
 711242        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1243        {
 1244            try
 1245            {
 121246                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 121247            }
 01248            catch (JsonException ex)
 1249            {
 01250                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01251            }
 1252        }
 1253
 711254        if (dto is null)
 1255        {
 591256            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1257        }
 1258
 711259        return Map(baseItemEntity, dto, appHost, logger);
 1260    }
 1261
 1262    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 1263    {
 01264        ArgumentNullException.ThrowIfNull(filter);
 1265
 01266        if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
 1267        {
 01268            filter.EnableTotalRecordCount = false;
 1269        }
 1270
 01271        using var context = _dbProvider.CreateDbContext();
 1272
 01273        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 01274        {
 01275            ExcludeItemTypes = filter.ExcludeItemTypes,
 01276            IncludeItemTypes = filter.IncludeItemTypes,
 01277            MediaTypes = filter.MediaTypes,
 01278            AncestorIds = filter.AncestorIds,
 01279            ItemIds = filter.ItemIds,
 01280            TopParentIds = filter.TopParentIds,
 01281            ParentId = filter.ParentId,
 01282            IsAiring = filter.IsAiring,
 01283            IsMovie = filter.IsMovie,
 01284            IsSports = filter.IsSports,
 01285            IsKids = filter.IsKids,
 01286            IsNews = filter.IsNews,
 01287            IsSeries = filter.IsSeries
 01288        });
 1289
 01290        var itemValuesQuery = context.ItemValues
 01291            .Where(f => itemValueTypes.Contains(f.Type))
 01292            .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
 01293            .Join(
 01294                innerQueryFilter,
 01295                fw => fw.w.ItemId,
 01296                g => g.Id,
 01297                (fw, g) => fw.f.CleanValue);
 1298
 01299        var innerQuery = PrepareItemQuery(context, filter)
 01300            .Where(e => e.Type == returnType)
 01301            .Where(e => itemValuesQuery.Contains(e.CleanName));
 1302
 01303        var outerQueryFilter = new InternalItemsQuery(filter.User)
 01304        {
 01305            IsPlayed = filter.IsPlayed,
 01306            IsFavorite = filter.IsFavorite,
 01307            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 01308            IsLiked = filter.IsLiked,
 01309            IsLocked = filter.IsLocked,
 01310            NameLessThan = filter.NameLessThan,
 01311            NameStartsWith = filter.NameStartsWith,
 01312            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 01313            Tags = filter.Tags,
 01314            OfficialRatings = filter.OfficialRatings,
 01315            StudioIds = filter.StudioIds,
 01316            GenreIds = filter.GenreIds,
 01317            Genres = filter.Genres,
 01318            Years = filter.Years,
 01319            NameContains = filter.NameContains,
 01320            SearchTerm = filter.SearchTerm,
 01321            ExcludeItemIds = filter.ExcludeItemIds
 01322        };
 1323
 01324        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
 01325            .GroupBy(e => e.PresentationUniqueKey)
 01326            .Select(e => e.FirstOrDefault())
 01327            .Select(e => e!.Id);
 1328
 01329        var query = context.BaseItems
 01330            .Include(e => e.TrailerTypes)
 01331            .Include(e => e.Provider)
 01332            .Include(e => e.LockedFields)
 01333            .Include(e => e.Images)
 01334            .AsSingleQuery()
 01335            .Where(e => masterQuery.Contains(e.Id));
 1336
 01337        query = ApplyOrder(query, filter, context);
 1338
 01339        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 01340        if (filter.EnableTotalRecordCount)
 1341        {
 01342            result.TotalRecordCount = query.Count();
 1343        }
 1344
 01345        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 1346        {
 01347            query = query.Skip(filter.StartIndex.Value);
 1348        }
 1349
 01350        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 1351        {
 01352            query = query.Take(filter.Limit.Value);
 1353        }
 1354
 01355        IQueryable<BaseItemEntity>? itemCountQuery = null;
 1356
 01357        if (filter.IncludeItemTypes.Length > 0)
 1358        {
 1359            // if we are to include more then one type, sub query those items beforehand.
 1360
 01361            var typeSubQuery = new InternalItemsQuery(filter.User)
 01362            {
 01363                ExcludeItemTypes = filter.ExcludeItemTypes,
 01364                IncludeItemTypes = filter.IncludeItemTypes,
 01365                MediaTypes = filter.MediaTypes,
 01366                AncestorIds = filter.AncestorIds,
 01367                ExcludeItemIds = filter.ExcludeItemIds,
 01368                ItemIds = filter.ItemIds,
 01369                TopParentIds = filter.TopParentIds,
 01370                ParentId = filter.ParentId,
 01371                IsPlayed = filter.IsPlayed
 01372            };
 1373
 01374            itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 01375                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 1376
 01377            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 01378            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 01379            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 01380            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 01381            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 01382            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 01383            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 1384
 01385            var resultQuery = query.Select(e => new
 01386            {
 01387                item = e,
 01388                // TODO: This is bad refactor!
 01389                itemCount = new ItemCounts()
 01390                {
 01391                    SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
 01392                    EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
 01393                    MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
 01394                    AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
 01395                    ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
 01396                    SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
 01397                    TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
 01398                }
 01399            });
 1400
 01401            result.StartIndex = filter.StartIndex ?? 0;
 01402            result.Items =
 01403            [
 01404                .. resultQuery
 01405                    .AsEnumerable()
 01406                    .Where(e => e is not null)
 01407                    .Select(e => (Item: DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount))
 01408                    .Where(e => e.Item is not null)
 01409                    .Select(e => (e.Item!, e.itemCount))
 01410            ];
 1411        }
 1412        else
 1413        {
 01414            result.StartIndex = filter.StartIndex ?? 0;
 01415            result.Items =
 01416            [
 01417                .. query
 01418                    .AsEnumerable()
 01419                    .Where(e => e is not null)
 01420                    .Select(e => (Item: DeserializeBaseItem(e, filter.SkipDeserialization), ItemCounts: (ItemCounts?)nul
 01421                    .Where(e => e.Item is not null)
 01422                    .Select(e => (e.Item!, e.ItemCounts))
 01423            ];
 1424        }
 1425
 01426        return result;
 01427    }
 1428
 1429    private static void PrepareFilterQuery(InternalItemsQuery query)
 1430    {
 3361431        if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
 1432        {
 01433            query.Limit = query.Limit.Value + 4;
 1434        }
 1435
 3361436        if (query.IsResumable ?? false)
 1437        {
 11438            query.IsVirtualItem = false;
 1439        }
 3361440    }
 1441
 1442    /// <summary>
 1443    /// Gets the clean value for search and sorting purposes.
 1444    /// </summary>
 1445    /// <param name="value">The value to clean.</param>
 1446    /// <returns>The cleaned value.</returns>
 1447    public static string GetCleanValue(string value)
 1448    {
 1231449        if (string.IsNullOrWhiteSpace(value))
 1450        {
 01451            return value;
 1452        }
 1453
 1231454        var noDiacritics = value.RemoveDiacritics();
 1455
 1456        // Build a string where any punctuation or symbol is treated as a separator (space).
 1231457        var sb = new StringBuilder(noDiacritics.Length);
 1231458        var previousWasSpace = false;
 20721459        foreach (var ch in noDiacritics)
 1460        {
 1461            char outCh;
 9131462            if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
 1463            {
 8911464                outCh = ch;
 1465            }
 1466            else
 1467            {
 221468                outCh = ' ';
 1469            }
 1470
 1471            // normalize any whitespace character to a single ASCII space.
 9131472            if (char.IsWhiteSpace(outCh))
 1473            {
 511474                if (!previousWasSpace)
 1475                {
 401476                    sb.Append(' ');
 401477                    previousWasSpace = true;
 1478                }
 1479            }
 1480            else
 1481            {
 8621482                sb.Append(outCh);
 8621483                previousWasSpace = false;
 1484            }
 1485        }
 1486
 1487        // trim leading/trailing spaces that may have been added.
 1231488        var collapsed = sb.ToString().Trim();
 1231489        return collapsed.ToLowerInvariant();
 1490    }
 1491
 1492    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1493    {
 1101494        var list = new List<(ItemValueType, string)>();
 1495
 1101496        if (item is IHasArtist hasArtist)
 1497        {
 01498            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1499        }
 1500
 1101501        if (item is IHasAlbumArtist hasAlbumArtist)
 1502        {
 01503            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1504        }
 1505
 1101506        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 1101507        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 1101508        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1509
 1510        // keywords was 5
 1511
 1101512        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1513
 1514        // Remove all invalid values.
 1101515        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1516
 1101517        return list;
 1518    }
 1519
 1520    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1521    {
 01522        return new BaseItemImageInfo()
 01523        {
 01524            ItemId = baseItemId,
 01525            Id = Guid.NewGuid(),
 01526            Path = e.Path,
 01527            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 01528            DateModified = e.DateModified,
 01529            Height = e.Height,
 01530            Width = e.Width,
 01531            ImageType = (ImageInfoImageType)e.Type,
 01532            Item = null!
 01533        };
 1534    }
 1535
 1536    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 1537    {
 01538        return new ItemImageInfo()
 01539        {
 01540            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 01541            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 01542            DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
 01543            Height = e.Height,
 01544            Width = e.Width,
 01545            Type = (ImageType)e.ImageType
 01546        };
 1547    }
 1548
 1549    private string? GetPathToSave(string path)
 1550    {
 1201551        if (path is null)
 1552        {
 101553            return null;
 1554        }
 1555
 1101556        return _appHost.ReverseVirtualPath(path);
 1557    }
 1558
 1559    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1560    {
 151561        var list = new List<string>();
 1562
 151563        if (IsTypeInQuery(BaseItemKind.Person, query))
 1564        {
 11565            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1566        }
 1567
 151568        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1569        {
 11570            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1571        }
 1572
 151573        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1574        {
 11575            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1576        }
 1577
 151578        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1579        {
 11580            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1581        }
 1582
 151583        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1584        {
 11585            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1586        }
 1587
 151588        return list;
 1589    }
 1590
 1591    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1592    {
 751593        if (query.ExcludeItemTypes.Contains(type))
 1594        {
 01595            return false;
 1596        }
 1597
 751598        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 1599    }
 1600
 1601    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 1602    {
 3351603        if (!query.GroupByPresentationUniqueKey)
 1604        {
 1421605            return false;
 1606        }
 1607
 1931608        if (query.GroupBySeriesPresentationUniqueKey)
 1609        {
 01610            return false;
 1611        }
 1612
 1931613        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 1614        {
 01615            return false;
 1616        }
 1617
 1931618        if (query.User is null)
 1619        {
 1911620            return false;
 1621        }
 1622
 21623        if (query.IncludeItemTypes.Length == 0)
 1624        {
 11625            return true;
 1626        }
 1627
 11628        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 11629            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 11630            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 11631            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 11632            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 11633            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 1634    }
 1635
 1636    private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinD
 1637    {
 3351638        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 3351639        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1640
 3351641        if (hasSearch)
 1642        {
 01643            orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1644        }
 3351645        else if (orderBy.Length == 0)
 1646        {
 2311647            return query.OrderBy(e => e.SortName);
 1648        }
 1649
 1041650        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1651
 1652        // When searching, prioritize by match quality: exact match > prefix match > contains
 1041653        if (hasSearch)
 1654        {
 01655            orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!));
 1656        }
 1657
 1041658        var firstOrdering = orderBy.FirstOrDefault();
 1041659        if (firstOrdering != default)
 1660        {
 1041661            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 1041662            if (orderedQuery is null)
 1663            {
 1664                // No search relevance ordering, start fresh
 1041665                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 1041666                    ? query.OrderBy(expression)
 1041667                    : query.OrderByDescending(expression);
 1668            }
 1669            else
 1670            {
 1671                // Search relevance ordering already applied, chain with ThenBy
 01672                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 01673                    ? orderedQuery.ThenBy(expression)
 01674                    : orderedQuery.ThenByDescending(expression);
 1675            }
 1676
 1041677            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 1678            {
 01679                orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending
 01680                    ? orderedQuery.ThenBy(e => e.Name)
 01681                    : orderedQuery.ThenByDescending(e => e.Name);
 1682            }
 1683        }
 1684
 2901685        foreach (var item in orderBy.Skip(1))
 1686        {
 411687            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 411688            if (item.SortOrder == SortOrder.Ascending)
 1689            {
 411690                orderedQuery = orderedQuery!.ThenBy(expression);
 1691            }
 1692            else
 1693            {
 01694                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1695            }
 1696        }
 1697
 1041698        return orderedQuery ?? query;
 1699    }
 1700
 1701    private IQueryable<BaseItemEntity> TranslateQuery(
 1702        IQueryable<BaseItemEntity> baseQuery,
 1703        JellyfinDbContext context,
 1704        InternalItemsQuery filter)
 1705    {
 1706        const int HDWidth = 1200;
 1707        const int UHDWidth = 3800;
 1708        const int UHDHeight = 2100;
 1709
 3351710        var minWidth = filter.MinWidth;
 3351711        var maxWidth = filter.MaxWidth;
 3351712        var now = DateTime.UtcNow;
 1713
 3351714        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 1715        {
 01716            bool includeSD = false;
 01717            bool includeHD = false;
 01718            bool include4K = false;
 1719
 01720            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 1721            {
 01722                includeSD = true;
 1723            }
 1724
 01725            if (filter.IsHD.HasValue && filter.IsHD.Value)
 1726            {
 01727                includeHD = true;
 1728            }
 1729
 01730            if (filter.Is4K.HasValue && filter.Is4K.Value)
 1731            {
 01732                include4K = true;
 1733            }
 1734
 01735            baseQuery = baseQuery.Where(e =>
 01736                (includeSD && e.Width < HDWidth) ||
 01737                (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
 01738                (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
 1739        }
 1740
 3351741        if (minWidth.HasValue)
 1742        {
 01743            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1744        }
 1745
 3351746        if (filter.MinHeight.HasValue)
 1747        {
 01748            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1749        }
 1750
 3351751        if (maxWidth.HasValue)
 1752        {
 01753            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1754        }
 1755
 3351756        if (filter.MaxHeight.HasValue)
 1757        {
 01758            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1759        }
 1760
 3351761        if (filter.IsLocked.HasValue)
 1762        {
 481763            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1764        }
 1765
 3351766        var tags = filter.Tags.ToList();
 3351767        var excludeTags = filter.ExcludeTags.ToList();
 1768
 3351769        if (filter.IsMovie.HasValue)
 1770        {
 01771            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 01772                && (filter.IncludeItemTypes.Length == 0
 01773                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01774                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 1775
 01776            if (!shouldIncludeAllMovieTypes)
 1777            {
 01778                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 1779            }
 1780        }
 1781
 3351782        if (filter.IsSeries.HasValue)
 1783        {
 01784            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1785        }
 1786
 3351787        if (filter.IsSports.HasValue)
 1788        {
 01789            if (filter.IsSports.Value)
 1790            {
 01791                tags.Add("Sports");
 1792            }
 1793            else
 1794            {
 01795                excludeTags.Add("Sports");
 1796            }
 1797        }
 1798
 3351799        if (filter.IsNews.HasValue)
 1800        {
 01801            if (filter.IsNews.Value)
 1802            {
 01803                tags.Add("News");
 1804            }
 1805            else
 1806            {
 01807                excludeTags.Add("News");
 1808            }
 1809        }
 1810
 3351811        if (filter.IsKids.HasValue)
 1812        {
 01813            if (filter.IsKids.Value)
 1814            {
 01815                tags.Add("Kids");
 1816            }
 1817            else
 1818            {
 01819                excludeTags.Add("Kids");
 1820            }
 1821        }
 1822
 3351823        if (!string.IsNullOrEmpty(filter.SearchTerm))
 1824        {
 01825            var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
 01826            var originalSearchTerm = filter.SearchTerm.ToLower();
 01827            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 1828            {
 01829                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 01830                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 1831            }
 1832            else
 1833            {
 01834                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 1835            }
 1836        }
 1837
 3351838        if (filter.IsFolder.HasValue)
 1839        {
 211840            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1841        }
 1842
 3351843        var includeTypes = filter.IncludeItemTypes;
 1844
 1845        // Only specify excluded types if no included types are specified
 3351846        if (filter.IncludeItemTypes.Length == 0)
 1847        {
 2211848            var excludeTypes = filter.ExcludeItemTypes;
 2211849            if (excludeTypes.Length == 1)
 1850            {
 01851                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 1852                {
 01853                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 1854                }
 1855            }
 2211856            else if (excludeTypes.Length > 1)
 1857            {
 01858                var excludeTypeName = new List<string>();
 01859                foreach (var excludeType in excludeTypes)
 1860                {
 01861                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 1862                    {
 01863                        excludeTypeName.Add(baseItemKindName!);
 1864                    }
 1865                }
 1866
 01867                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 1868            }
 1869        }
 1870        else
 1871        {
 1141872            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 1141873            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1874        }
 1875
 3351876        if (filter.ChannelIds.Count > 0)
 1877        {
 01878            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1879        }
 1880
 3351881        if (!filter.ParentId.IsEmpty())
 1882        {
 1421883            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1884        }
 1885
 3351886        if (!string.IsNullOrWhiteSpace(filter.Path))
 1887        {
 01888            var pathToQuery = GetPathToSave(filter.Path);
 01889            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 1890        }
 1891
 3351892        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1893        {
 01894            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1895        }
 1896
 3351897        if (filter.MinCommunityRating.HasValue)
 1898        {
 01899            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1900        }
 1901
 3351902        if (filter.MinIndexNumber.HasValue)
 1903        {
 01904            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1905        }
 1906
 3351907        if (filter.MinParentAndIndexNumber.HasValue)
 1908        {
 01909            baseQuery = baseQuery
 01910                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1911        }
 1912
 3351913        if (filter.MinDateCreated.HasValue)
 1914        {
 01915            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1916        }
 1917
 3351918        if (filter.MinDateLastSaved.HasValue)
 1919        {
 01920            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1921        }
 1922
 3351923        if (filter.MinDateLastSavedForUser.HasValue)
 1924        {
 01925            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1926        }
 1927
 3351928        if (filter.IndexNumber.HasValue)
 1929        {
 01930            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1931        }
 1932
 3351933        if (filter.ParentIndexNumber.HasValue)
 1934        {
 01935            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1936        }
 1937
 3351938        if (filter.ParentIndexNumberNotEquals.HasValue)
 1939        {
 01940            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1941        }
 1942
 3351943        var minEndDate = filter.MinEndDate;
 3351944        var maxEndDate = filter.MaxEndDate;
 1945
 3351946        if (filter.HasAired.HasValue)
 1947        {
 01948            if (filter.HasAired.Value)
 1949            {
 01950                maxEndDate = DateTime.UtcNow;
 1951            }
 1952            else
 1953            {
 01954                minEndDate = DateTime.UtcNow;
 1955            }
 1956        }
 1957
 3351958        if (minEndDate.HasValue)
 1959        {
 01960            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1961        }
 1962
 3351963        if (maxEndDate.HasValue)
 1964        {
 01965            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1966        }
 1967
 3351968        if (filter.MinStartDate.HasValue)
 1969        {
 01970            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1971        }
 1972
 3351973        if (filter.MaxStartDate.HasValue)
 1974        {
 01975            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1976        }
 1977
 3351978        if (filter.MinPremiereDate.HasValue)
 1979        {
 01980            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1981        }
 1982
 3351983        if (filter.MaxPremiereDate.HasValue)
 1984        {
 01985            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1986        }
 1987
 3351988        if (filter.TrailerTypes.Length > 0)
 1989        {
 01990            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 01991            baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
 1992        }
 1993
 3351994        if (filter.IsAiring.HasValue)
 1995        {
 01996            if (filter.IsAiring.Value)
 1997            {
 01998                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 1999            }
 2000            else
 2001            {
 02002                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 2003            }
 2004        }
 2005
 3352006        if (filter.PersonIds.Length > 0)
 2007        {
 02008            var peopleEntityIds = context.BaseItems
 02009                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 02010                .Join(
 02011                    context.Peoples,
 02012                    b => b.Name,
 02013                    p => p.Name,
 02014                    (b, p) => p.Id);
 2015
 02016            baseQuery = baseQuery
 02017                .Where(e => context.PeopleBaseItemMap
 02018                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 2019        }
 2020
 3352021        if (!string.IsNullOrWhiteSpace(filter.Person))
 2022        {
 02023            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 2024        }
 2025
 3352026        if (!string.IsNullOrWhiteSpace(filter.MinSortName))
 2027        {
 2028            // this does not makes sense.
 2029            // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
 2030            // whereClauses.Add("SortName>=@MinSortName");
 2031            // statement?.TryBind("@MinSortName", query.MinSortName);
 2032        }
 2033
 3352034        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 2035        {
 02036            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 2037        }
 2038
 3352039        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 2040        {
 02041            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 2042        }
 2043
 3352044        if (!string.IsNullOrWhiteSpace(filter.Name))
 2045        {
 32046            if (filter.UseRawName == true)
 2047            {
 02048                baseQuery = baseQuery.Where(e => e.Name == filter.Name);
 2049            }
 2050            else
 2051            {
 32052                var cleanName = GetCleanValue(filter.Name);
 32053                baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 2054            }
 2055        }
 2056
 2057        // These are the same, for now
 3352058        var nameContains = filter.NameContains;
 3352059        if (!string.IsNullOrWhiteSpace(nameContains))
 2060        {
 02061            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 2062            {
 02063                nameContains = $"%{nameContains.Trim('%')}%";
 02064                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 2065            }
 2066            else
 2067            {
 02068                baseQuery = baseQuery.Where(e =>
 02069                                    e.CleanName!.Contains(nameContains)
 02070                                    || e.OriginalTitle!.ToLower().Contains(nameContains!));
 2071            }
 2072        }
 2073
 3352074        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 2075        {
 02076            var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
 02077            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
 2078        }
 2079
 3352080        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 2081        {
 02082            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 02083            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
 2084        }
 2085
 3352086        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 2087        {
 02088            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 02089            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
 2090        }
 2091
 3352092        if (filter.ImageTypes.Length > 0)
 2093        {
 1032094            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 1032095            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 2096        }
 2097
 3352098        if (filter.IsLiked.HasValue)
 2099        {
 02100            baseQuery = baseQuery
 02101                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLike
 2102        }
 2103
 3352104        if (filter.IsFavoriteOrLiked.HasValue)
 2105        {
 02106            baseQuery = baseQuery
 02107                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2108        }
 2109
 3352110        if (filter.IsFavorite.HasValue)
 2111        {
 02112            baseQuery = baseQuery
 02113                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2114        }
 2115
 3352116        if (filter.IsPlayed.HasValue)
 2117        {
 2118            // We should probably figure this out for all folders, but for right now, this is the only place where we ne
 02119            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
 2120            {
 02121                baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
 02122                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
 02123                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
 02124                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
 2125            }
 2126            else
 2127            {
 02128                baseQuery = baseQuery
 02129                    .Select(e => new
 02130                    {
 02131                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).Firs
 02132                        Item = e
 02133                    })
 02134                    .Where(e => e.IsPlayed == filter.IsPlayed)
 02135                    .Select(f => f.Item);
 2136            }
 2137        }
 2138
 3352139        if (filter.IsResumable.HasValue)
 2140        {
 12141            if (filter.IsResumable.Value)
 2142            {
 12143                baseQuery = baseQuery
 12144                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks >
 2145            }
 2146            else
 2147            {
 02148                baseQuery = baseQuery
 02149                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks =
 2150            }
 2151        }
 2152
 3352153        if (filter.ArtistIds.Length > 0)
 2154        {
 02155            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2156        }
 2157
 3352158        if (filter.AlbumArtistIds.Length > 0)
 2159        {
 02160            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 2161        }
 2162
 3352163        if (filter.ContributingArtistIds.Length > 0)
 2164        {
 02165            var contributingNames = context.BaseItems
 02166                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 02167                .Select(b => b.CleanName);
 2168
 02169            baseQuery = baseQuery.Where(e =>
 02170                e.ItemValues!.Any(ivm =>
 02171                    ivm.ItemValue.Type == ItemValueType.Artist &&
 02172                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 02173                &&
 02174                !e.ItemValues!.Any(ivm =>
 02175                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 02176                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 2177        }
 2178
 3352179        if (filter.AlbumIds.Length > 0)
 2180        {
 02181            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 02182            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 2183        }
 2184
 3352185        if (filter.ExcludeArtistIds.Length > 0)
 2186        {
 02187            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2188        }
 2189
 3352190        if (filter.GenreIds.Count > 0)
 2191        {
 02192            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 2193        }
 2194
 3352195        if (filter.Genres.Count > 0)
 2196        {
 02197            var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValue
 02198            baseQuery = baseQuery
 02199                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 2200        }
 2201
 3352202        if (tags.Count > 0)
 2203        {
 02204            var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, stri
 02205            baseQuery = baseQuery
 02206                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 2207        }
 2208
 3352209        if (excludeTags.Count > 0)
 2210        {
 02211            var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMa
 02212            baseQuery = baseQuery
 02213                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 2214        }
 2215
 3352216        if (filter.StudioIds.Length > 0)
 2217        {
 02218            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 2219        }
 2220
 3352221        if (filter.OfficialRatings.Length > 0)
 2222        {
 02223            baseQuery = baseQuery
 02224                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 2225        }
 2226
 3352227        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 3352228        if (filter.MinParentalRating != null)
 2229        {
 02230            var min = filter.MinParentalRating;
 02231            var minScore = min.Score;
 02232            var minSubScore = min.SubScore ?? 0;
 2233
 02234            minParentalRatingFilter = e =>
 02235                e.InheritedParentalRatingValue == null ||
 02236                e.InheritedParentalRatingValue > minScore ||
 02237                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 2238        }
 2239
 3352240        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 3352241        if (filter.MaxParentalRating != null)
 2242        {
 482243            var max = filter.MaxParentalRating;
 482244            var maxScore = max.Score;
 482245            var maxSubScore = max.SubScore ?? 0;
 2246
 482247            maxParentalRatingFilter = e =>
 482248                e.InheritedParentalRatingValue == null ||
 482249                e.InheritedParentalRatingValue < maxScore ||
 482250                (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
 2251        }
 2252
 3352253        if (filter.HasParentalRating ?? false)
 2254        {
 02255            if (minParentalRatingFilter != null)
 2256            {
 02257                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2258            }
 2259
 02260            if (maxParentalRatingFilter != null)
 2261            {
 02262                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2263            }
 2264        }
 3352265        else if (filter.BlockUnratedItems.Length > 0)
 2266        {
 02267            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 02268            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 2269
 02270            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 2271            {
 02272                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 2273            }
 02274            else if (minParentalRatingFilter != null)
 2275            {
 02276                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 2277            }
 02278            else if (maxParentalRatingFilter != null)
 2279            {
 02280                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 2281            }
 2282            else
 2283            {
 02284                baseQuery = baseQuery.Where(unratedItemFilter);
 2285            }
 2286        }
 3352287        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 2288        {
 482289            if (minParentalRatingFilter != null)
 2290            {
 02291                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2292            }
 2293
 482294            if (maxParentalRatingFilter != null)
 2295            {
 482296                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2297            }
 2298        }
 2872299        else if (!filter.HasParentalRating ?? false)
 2300        {
 02301            baseQuery = baseQuery
 02302                .Where(e => e.InheritedParentalRatingValue == null);
 2303        }
 2304
 3352305        if (filter.HasOfficialRating.HasValue)
 2306        {
 02307            if (filter.HasOfficialRating.Value)
 2308            {
 02309                baseQuery = baseQuery
 02310                    .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
 2311            }
 2312            else
 2313            {
 02314                baseQuery = baseQuery
 02315                    .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
 2316            }
 2317        }
 2318
 3352319        if (filter.HasOverview.HasValue)
 2320        {
 02321            if (filter.HasOverview.Value)
 2322            {
 02323                baseQuery = baseQuery
 02324                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 2325            }
 2326            else
 2327            {
 02328                baseQuery = baseQuery
 02329                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 2330            }
 2331        }
 2332
 3352333        if (filter.HasOwnerId.HasValue)
 2334        {
 02335            if (filter.HasOwnerId.Value)
 2336            {
 02337                baseQuery = baseQuery
 02338                    .Where(e => e.OwnerId != null);
 2339            }
 2340            else
 2341            {
 02342                baseQuery = baseQuery
 02343                    .Where(e => e.OwnerId == null);
 2344            }
 2345        }
 2346
 3352347        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 2348        {
 02349            baseQuery = baseQuery
 02350                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filte
 2351        }
 2352
 3352353        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2354        {
 02355            baseQuery = baseQuery
 02356                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2357        }
 2358
 3352359        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2360        {
 02361            baseQuery = baseQuery
 02362                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2363        }
 2364
 3352365        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 2366        {
 02367            baseQuery = baseQuery
 02368                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == fi
 2369        }
 2370
 3352371        if (filter.HasSubtitles.HasValue)
 2372        {
 02373            baseQuery = baseQuery
 02374                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2375        }
 2376
 3352377        if (filter.HasChapterImages.HasValue)
 2378        {
 02379            baseQuery = baseQuery
 02380                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2381        }
 2382
 3352383        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2384        {
 162385            baseQuery = baseQuery
 162386                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 2387        }
 2388
 3352389        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2390        {
 162391            baseQuery = baseQuery
 162392                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2393        }
 2394
 3352395        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2396        {
 162397            baseQuery = baseQuery
 162398                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2399        }
 2400
 3352401        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2402        {
 162403            baseQuery = baseQuery
 162404                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2405        }
 2406
 3352407        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2408        {
 02409            baseQuery = baseQuery
 02410                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2411        }
 2412
 3352413        if (filter.Years.Length > 0)
 2414        {
 02415            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2416        }
 2417
 3352418        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 3352419        if (isVirtualItem.HasValue)
 2420        {
 222421            baseQuery = baseQuery
 222422                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2423        }
 2424
 3352425        if (filter.IsSpecialSeason.HasValue)
 2426        {
 02427            if (filter.IsSpecialSeason.Value)
 2428            {
 02429                baseQuery = baseQuery
 02430                    .Where(e => e.IndexNumber == 0);
 2431            }
 2432            else
 2433            {
 02434                baseQuery = baseQuery
 02435                    .Where(e => e.IndexNumber != 0);
 2436            }
 2437        }
 2438
 3352439        if (filter.IsUnaired.HasValue)
 2440        {
 02441            if (filter.IsUnaired.Value)
 2442            {
 02443                baseQuery = baseQuery
 02444                    .Where(e => e.PremiereDate >= now);
 2445            }
 2446            else
 2447            {
 02448                baseQuery = baseQuery
 02449                    .Where(e => e.PremiereDate < now);
 2450            }
 2451        }
 2452
 3352453        if (filter.MediaTypes.Length > 0)
 2454        {
 212455            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 212456            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 2457        }
 2458
 3352459        if (filter.ItemIds.Length > 0)
 2460        {
 02461            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2462        }
 2463
 3352464        if (filter.ExcludeItemIds.Length > 0)
 2465        {
 02466            baseQuery = baseQuery
 02467                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 2468        }
 2469
 3352470        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 2471        {
 02472            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 02473            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 2474        }
 2475
 3352476        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 2477        {
 2478            // Allow setting a null or empty value to get all items that have the specified provider set.
 02479            var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArra
 02480            if (includeAny.Length > 0)
 2481            {
 02482                baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
 2483            }
 2484
 02485            var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Ke
 02486            if (includeSelected.Length > 0)
 2487            {
 02488                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 2489            }
 2490        }
 2491
 3352492        if (filter.HasImdbId.HasValue)
 2493        {
 02494            baseQuery = filter.HasImdbId.Value
 02495                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().T
 02496                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().T
 2497        }
 2498
 3352499        if (filter.HasTmdbId.HasValue)
 2500        {
 02501            baseQuery = filter.HasTmdbId.Value
 02502                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().T
 02503                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().T
 2504        }
 2505
 3352506        if (filter.HasTvdbId.HasValue)
 2507        {
 02508            baseQuery = filter.HasTvdbId.Value
 02509                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().T
 02510                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().T
 2511        }
 2512
 3352513        var queryTopParentIds = filter.TopParentIds;
 2514
 3352515        if (queryTopParentIds.Length > 0)
 2516        {
 152517            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 152518            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 152519            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2520            {
 02521                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2522            }
 2523            else
 2524            {
 152525                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2526            }
 2527        }
 2528
 3352529        if (filter.AncestorIds.Length > 0)
 2530        {
 422531            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2532        }
 2533
 3352534        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 2535        {
 02536            baseQuery = baseQuery
 02537                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 2538        }
 2539
 3352540        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2541        {
 02542            baseQuery = baseQuery
 02543                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2544        }
 2545
 3352546        if (filter.ExcludeInheritedTags.Length > 0)
 2547        {
 02548            var excludedTags = filter.ExcludeInheritedTags;
 02549            baseQuery = baseQuery.Where(e =>
 02550                !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.Clea
 02551                && (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.
 2552        }
 2553
 3352554        if (filter.IncludeInheritedTags.Length > 0)
 2555        {
 02556            var includeTags = filter.IncludeInheritedTags;
 02557            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 02558            baseQuery = baseQuery.Where(e =>
 02559                e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanV
 02560
 02561                // For seasons and episodes, we also need to check the parent series' tags.
 02562                || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Ty
 02563
 02564                // A playlist should be accessible to its owner regardless of allowed tags
 02565                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 2566        }
 2567
 3352568        if (filter.SeriesStatuses.Length > 0)
 2569        {
 02570            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 02571            baseQuery = baseQuery
 02572                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 2573        }
 2574
 3352575        if (filter.BoxSetLibraryFolders.Length > 0)
 2576        {
 02577            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 02578            baseQuery = baseQuery
 02579                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 2580        }
 2581
 3352582        if (filter.VideoTypes.Length > 0)
 2583        {
 02584            var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
 02585            baseQuery = baseQuery
 02586                .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
 2587        }
 2588
 3352589        if (filter.Is3D.HasValue)
 2590        {
 02591            if (filter.Is3D.Value)
 2592            {
 02593                baseQuery = baseQuery
 02594                    .Where(e => e.Data!.Contains("Video3DFormat"));
 2595            }
 2596            else
 2597            {
 02598                baseQuery = baseQuery
 02599                    .Where(e => !e.Data!.Contains("Video3DFormat"));
 2600            }
 2601        }
 2602
 3352603        if (filter.IsPlaceHolder.HasValue)
 2604        {
 02605            if (filter.IsPlaceHolder.Value)
 2606            {
 02607                baseQuery = baseQuery
 02608                    .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
 2609            }
 2610            else
 2611            {
 02612                baseQuery = baseQuery
 02613                    .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
 2614            }
 2615        }
 2616
 3352617        if (filter.HasSpecialFeature.HasValue)
 2618        {
 02619            if (filter.HasSpecialFeature.Value)
 2620            {
 02621                baseQuery = baseQuery
 02622                    .Where(e => e.ExtraIds != null);
 2623            }
 2624            else
 2625            {
 02626                baseQuery = baseQuery
 02627                    .Where(e => e.ExtraIds == null);
 2628            }
 2629        }
 2630
 3352631        if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
 2632        {
 02633            if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo
 2634            {
 02635                baseQuery = baseQuery
 02636                    .Where(e => e.ExtraIds != null);
 2637            }
 2638            else
 2639            {
 02640                baseQuery = baseQuery
 02641                    .Where(e => e.ExtraIds == null);
 2642            }
 2643        }
 2644
 3352645        return baseQuery;
 2646    }
 2647
 2648    /// <inheritdoc/>
 2649    public async Task<bool> ItemExistsAsync(Guid id)
 2650    {
 2651        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 2652        await using (dbContext.ConfigureAwait(false))
 2653        {
 2654            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 2655        }
 2656    }
 2657
 2658    /// <inheritdoc/>
 2659    public bool GetIsPlayed(User user, Guid id, bool recursive)
 2660    {
 02661        using var dbContext = _dbProvider.CreateDbContext();
 2662
 02663        if (recursive)
 2664        {
 02665            var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
 2666
 02667            return dbContext.BaseItems
 02668                    .Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
 02669                    .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
 2670        }
 2671
 02672        return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.P
 02673    }
 2674
 2675    private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseIte
 2676    {
 12677        var folderStack = new HashSet<Guid>()
 12678            {
 12679                parentId
 12680            };
 12681        var folderList = new HashSet<Guid>()
 12682            {
 12683                parentId
 12684            };
 2685
 22686        while (folderStack.Count != 0)
 2687        {
 12688            var items = folderStack.ToArray();
 12689            folderStack.Clear();
 12690            var query = dbContext.BaseItems
 12691                .WhereOneOrMany(items, e => e.ParentId!.Value);
 2692
 12693            if (filter != null)
 2694            {
 02695                query = query.Where(filter);
 2696            }
 2697
 22698            foreach (var item in query.Select(e => e.Id).ToArray())
 2699            {
 02700                if (folderList.Add(item))
 2701                {
 02702                    folderStack.Add(item);
 2703                }
 2704            }
 2705        }
 2706
 12707        return folderList;
 2708    }
 2709
 2710    /// <inheritdoc/>
 2711    public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
 2712    {
 02713        using var dbContext = _dbProvider.CreateDbContext();
 2714
 02715        var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 02716            .Where(e => artistNames.Contains(e.Name))
 02717            .ToArray();
 2718
 02719        var lookup = artists
 02720            .GroupBy(e => e.Name!)
 02721            .ToDictionary(
 02722                g => g.Key,
 02723                g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
 2724
 02725        var result = new Dictionary<string, MusicArtist[]>(artistNames.Count);
 02726        foreach (var name in artistNames)
 2727        {
 02728            if (lookup.TryGetValue(name, out var artistArray))
 2729            {
 02730                result[name] = artistArray;
 2731            }
 2732        }
 2733
 02734        return result;
 02735    }
 2736}

Methods/Properties

.cctor()
.ctor(Microsoft.EntityFrameworkCore.IDbContextFactory`1<Jellyfin.Database.Implementations.JellyfinDbContext>,MediaBrowser.Controller.IServerApplicationHost,MediaBrowser.Controller.Persistence.IItemTypeLookup,MediaBrowser.Controller.Configuration.IServerConfigurationManager,Microsoft.Extensions.Logging.ILogger`1<Jellyfin.Server.Implementations.Item.BaseItemRepository>)
DeleteItem(System.Collections.Generic.IReadOnlyList`1<System.Guid>)
UpdateInheritedValues()
GetItemIdsList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetAllArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetAlbumArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetStudios(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetGenres(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetMusicGenres(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetStudioNames()
GetAllArtistNames()
GetMusicGenreNames()
GetGenreNames()
GetItems(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetLatestItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Data.Enums.CollectionType)
GetNextUpSeriesKeys(MediaBrowser.Controller.Entities.InternalItemsQuery,System.DateTime)
ApplyGroupingFilter(Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyNavigations(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyQueryPaging(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyQueryFilter(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)
PrepareItemQuery(Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetCount(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemCounts(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetType(System.String)
SaveItems(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,System.Threading.CancellationToken)
UpdateOrInsertItems(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,System.Threading.CancellationToken)
RetrieveItem(System.Guid)
Map(Jellyfin.Database.Implementations.Entities.BaseItemEntity,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.IServerApplicationHost,Microsoft.Extensions.Logging.ILogger)
Map(MediaBrowser.Controller.Entities.BaseItem)
GetItemValueNames(System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Collections.Generic.IReadOnlyList`1<System.String>)
TypeRequiresDeserialization(System.Type)
DeserializeBaseItem(Jellyfin.Database.Implementations.Entities.BaseItemEntity,System.Boolean)
DeserializeBaseItem(Jellyfin.Database.Implementations.Entities.BaseItemEntity,Microsoft.Extensions.Logging.ILogger,MediaBrowser.Controller.IServerApplicationHost,System.Boolean)
GetItemValues(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<Jellyfin.Database.Implementations.Entities.ItemValueType>,System.String)
PrepareFilterQuery(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetCleanValue(System.String)
GetItemValuesToSave(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.List`1<System.String>)
Map(System.Guid,MediaBrowser.Controller.Entities.ItemImageInfo)
Map(Jellyfin.Database.Implementations.Entities.BaseItemImageInfo,MediaBrowser.Controller.IServerApplicationHost)
GetPathToSave(System.String)
GetItemByNameTypesInQuery(MediaBrowser.Controller.Entities.InternalItemsQuery)
IsTypeInQuery(Jellyfin.Data.Enums.BaseItemKind,MediaBrowser.Controller.Entities.InternalItemsQuery)
EnableGroupByPresentationUniqueKey(MediaBrowser.Controller.Entities.InternalItemsQuery)
ApplyOrder(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.JellyfinDbContext)
TranslateQuery(System.Linq.IQueryable`1<Jellyfin.Database.Implementations.Entities.BaseItemEntity>,Jellyfin.Database.Implementations.JellyfinDbContext,MediaBrowser.Controller.Entities.InternalItemsQuery)
GetIsPlayed(Jellyfin.Database.Implementations.Entities.User,System.Guid,System.Boolean)
TraverseHirachyDown(System.Guid,Jellyfin.Database.Implementations.JellyfinDbContext,System.Linq.Expressions.Expression`1<System.Func`2<Jellyfin.Database.Implementations.Entities.BaseItemEntity,System.Boolean>>)
FindArtists(System.Collections.Generic.IReadOnlyList`1<System.String>)