< Summary - Jellyfin

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

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

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

Metrics

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

File(s)

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2// Do not enforce that because EFCore cannot deal with cultures well.
 3#pragma warning disable CA1304 // Specify CultureInfo
 4#pragma warning disable CA1311 // Specify a culture or use an invariant version
 5#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 6
 7using System;
 8using System.Collections.Concurrent;
 9using System.Collections.Generic;
 10using System.Globalization;
 11using System.Linq;
 12using System.Linq.Expressions;
 13using System.Reflection;
 14using System.Text;
 15using System.Text.Json;
 16using System.Threading;
 17using System.Threading.Tasks;
 18using Jellyfin.Data.Enums;
 19using Jellyfin.Database.Implementations;
 20using Jellyfin.Database.Implementations.Entities;
 21using Jellyfin.Database.Implementations.Enums;
 22using Jellyfin.Extensions;
 23using Jellyfin.Extensions.Json;
 24using Jellyfin.Server.Implementations.Extensions;
 25using MediaBrowser.Common;
 26using MediaBrowser.Controller;
 27using MediaBrowser.Controller.Channels;
 28using MediaBrowser.Controller.Configuration;
 29using MediaBrowser.Controller.Entities;
 30using MediaBrowser.Controller.Entities.Audio;
 31using MediaBrowser.Controller.Entities.TV;
 32using MediaBrowser.Controller.LiveTv;
 33using MediaBrowser.Controller.Persistence;
 34using MediaBrowser.Model.Dto;
 35using MediaBrowser.Model.Entities;
 36using MediaBrowser.Model.LiveTv;
 37using MediaBrowser.Model.Querying;
 38using Microsoft.EntityFrameworkCore;
 39using Microsoft.Extensions.Logging;
 40using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
 41using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
 42
 43namespace Jellyfin.Server.Implementations.Item;
 44
 45/*
 46    All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null
 47    This is done as the code isn't actually executed client side, but only the expressions are interpret and the compile
 48    This is your only warning/message regarding this topic.
 49*/
 50
 51/// <summary>
 52/// Handles all storage logic for BaseItems.
 53/// </summary>
 54public sealed class BaseItemRepository
 55    : IItemRepository
 56{
 57    /// <summary>
 58    /// Gets the placeholder id for UserData detached items.
 59    /// </summary>
 260    public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
 61
 62    /// <summary>
 63    /// This holds all the types in the running assemblies
 64    /// so that we can de-serialize properly when we don't have strong types.
 65    /// </summary>
 266    private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
 67    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 68    private readonly IServerApplicationHost _appHost;
 69    private readonly IItemTypeLookup _itemTypeLookup;
 70    private readonly IServerConfigurationManager _serverConfigurationManager;
 71    private readonly ILogger<BaseItemRepository> _logger;
 72
 273    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 274    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 275    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 276    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 277    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 278    private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
 79
 80    /// <summary>
 81    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
 82    /// </summary>
 83    /// <param name="dbProvider">The db factory.</param>
 84    /// <param name="appHost">The Application host.</param>
 85    /// <param name="itemTypeLookup">The static type lookup.</param>
 86    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
 87    /// <param name="logger">System logger.</param>
 88    public BaseItemRepository(
 89        IDbContextFactory<JellyfinDbContext> dbProvider,
 90        IServerApplicationHost appHost,
 91        IItemTypeLookup itemTypeLookup,
 92        IServerConfigurationManager serverConfigurationManager,
 93        ILogger<BaseItemRepository> logger)
 94    {
 3195        _dbProvider = dbProvider;
 3196        _appHost = appHost;
 3197        _itemTypeLookup = itemTypeLookup;
 3198        _serverConfigurationManager = serverConfigurationManager;
 3199        _logger = logger;
 31100    }
 101
 102    /// <inheritdoc />
 103    public void DeleteItem(params IReadOnlyList<Guid> ids)
 104    {
 1105        if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
 106        {
 0107            throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
 108        }
 109
 1110        using var context = _dbProvider.CreateDbContext();
 1111        using var transaction = context.Database.BeginTransaction();
 112
 1113        var date = (DateTime?)DateTime.UtcNow;
 114
 1115        var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
 116
 117        // Remove any UserData entries for the placeholder item that would conflict with the UserData
 118        // being detached from the item being deleted. This is necessary because, during an update,
 119        // UserData may be reattached to a new entry, but some entries can be left behind.
 120        // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
 1121        context.UserData
 1122            .Join(
 1123                context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
 1124                placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
 1125                userData => new { userData.UserId, userData.CustomDataKey },
 1126                (placeholder, userData) => placeholder)
 1127            .Where(e => e.ItemId == PlaceholderId)
 1128            .ExecuteDelete();
 129
 130        // Detach all user watch data
 1131        context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
 1132            .ExecuteUpdate(e => e
 1133                .SetProperty(f => f.RetentionDate, date)
 1134                .SetProperty(f => f.ItemId, PlaceholderId));
 135
 1136        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1137        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
 1138        context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1139        context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1140        context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1141        context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1142        context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1143        context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
 1144        context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1145        context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1146        context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1147        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
 1148        context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1149        context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1150        context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1151        context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1152        var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distin
 1153        context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1154        context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
 1155        context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 1156        context.SaveChanges();
 1157        transaction.Commit();
 2158    }
 159
 160    /// <inheritdoc />
 161    public void UpdateInheritedValues()
 162    {
 16163        using var context = _dbProvider.CreateDbContext();
 16164        using var transaction = context.Database.BeginTransaction();
 165
 16166        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 167        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 16168        context.SaveChanges();
 169
 16170        transaction.Commit();
 32171    }
 172
 173    /// <inheritdoc />
 174    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 175    {
 17176        ArgumentNullException.ThrowIfNull(filter);
 17177        PrepareFilterQuery(filter);
 178
 17179        using var context = _dbProvider.CreateDbContext();
 17180        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 17181    }
 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    {
 17222        return GetItemValueNames(_getStudiosValueTypes, [], []);
 223    }
 224
 225    /// <inheritdoc />
 226    public IReadOnlyList<string> GetAllArtistNames()
 227    {
 17228        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 229    }
 230
 231    /// <inheritdoc />
 232    public IReadOnlyList<string> GetMusicGenreNames()
 233    {
 17234        return GetItemValueNames(
 17235            _getGenreValueTypes,
 17236            _itemTypeLookup.MusicGenreTypes,
 17237            []);
 238    }
 239
 240    /// <inheritdoc />
 241    public IReadOnlyList<string> GetGenreNames()
 242    {
 17243        return GetItemValueNames(
 17244            _getGenreValueTypes,
 17245            [],
 17246            _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    {
 335288        ArgumentNullException.ThrowIfNull(filter);
 335289        PrepareFilterQuery(filter);
 290
 335291        using var context = _dbProvider.CreateDbContext();
 335292        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 293
 335294        dbQuery = TranslateQuery(dbQuery, context, filter);
 295
 335296        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 335297        dbQuery = ApplyQueryPaging(dbQuery, filter);
 298
 335299        var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
 335300        if (hasRandomSort)
 301        {
 66302            var orderedIds = dbQuery.Select(e => e.Id).ToList();
 66303            if (orderedIds.Count == 0)
 304            {
 66305                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
 269317        dbQuery = ApplyNavigations(dbQuery, filter);
 318
 269319        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializ
 335320    }
 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
 352404        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 352405        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        }
 352410        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        }
 351415        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        {
 351422            dbQuery = dbQuery.Distinct();
 423        }
 424
 352425        dbQuery = ApplyOrder(dbQuery, filter, context);
 426
 352427        return dbQuery;
 428    }
 429
 430    private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 431    {
 286432        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 433        {
 0434            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 435        }
 436
 286437        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 438        {
 285439            dbQuery = dbQuery.Include(e => e.Provider);
 440        }
 441
 286442        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 443        {
 285444            dbQuery = dbQuery.Include(e => e.LockedFields);
 445        }
 446
 286447        if (filter.DtoOptions.EnableUserData)
 448        {
 286449            dbQuery = dbQuery.Include(e => e.UserData);
 450        }
 451
 286452        if (filter.DtoOptions.EnableImages)
 453        {
 286454            dbQuery = dbQuery.Include(e => e.Images);
 455        }
 456
 286457        return dbQuery;
 458    }
 459
 460    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 461    {
 352462        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 463        {
 0464            dbQuery = dbQuery.Skip(filter.StartIndex.Value);
 465        }
 466
 352467        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 468        {
 110469            dbQuery = dbQuery.Take(filter.Limit.Value);
 470        }
 471
 352472        return dbQuery;
 473    }
 474
 475    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 476    {
 17477        dbQuery = TranslateQuery(dbQuery, context, filter);
 17478        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 17479        dbQuery = ApplyQueryPaging(dbQuery, filter);
 17480        dbQuery = ApplyNavigations(dbQuery, filter);
 17481        return dbQuery;
 482    }
 483
 484    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 485    {
 421486        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 421487        dbQuery = dbQuery.AsSingleQuery();
 488
 421489        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    {
 153574        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 
 153578        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 153579            .Select(a => a.GetType(k))
 153580            .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    {
 113617        UpdateOrInsertItems(items, cancellationToken);
 112618    }
 619
 620    /// <inheritdoc cref="IItemRepository"/>
 621    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 622    {
 113623        ArgumentNullException.ThrowIfNull(items);
 113624        cancellationToken.ThrowIfCancellationRequested();
 625
 112626        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 448627        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
 628        {
 112629            var ancestorIds = item.SupportsAncestors ?
 112630                item.GetAncestorIds().Distinct().ToList() :
 112631                null;
 632
 112633            var topParent = item.GetTopParent();
 634
 112635            var userdataKey = item.GetUserDataKeys();
 112636            var inheritedTags = item.GetInheritedTags();
 637
 112638            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 639        }
 640
 112641        using var context = _dbProvider.CreateDbContext();
 112642        using var transaction = context.Database.BeginTransaction();
 643
 112644        var ids = tuples.Select(f => f.Item.Id).ToArray();
 112645        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 646
 448647        foreach (var item in tuples)
 648        {
 112649            var entity = Map(item.Item);
 650            // TODO: refactor this "inconsistency"
 112651            entity.TopParentId = item.TopParent?.Id;
 652
 112653            if (!existingItems.Any(e => e == entity.Id))
 654            {
 60655                context.BaseItems.Add(entity);
 656            }
 657            else
 658            {
 52659                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 52660                context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 52661                context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 662
 52663                if (entity.Images is { Count: > 0 })
 664                {
 0665                    context.BaseItemImageInfos.AddRange(entity.Images);
 666                }
 667
 52668                if (entity.LockedFields is { Count: > 0 })
 669                {
 0670                    context.BaseItemMetadataFields.AddRange(entity.LockedFields);
 671                }
 672
 52673                context.BaseItems.Attach(entity).State = EntityState.Modified;
 674            }
 675        }
 676
 112677        context.SaveChanges();
 678
 112679        var itemValueMaps = tuples
 112680            .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 112681            .ToArray();
 112682        var allListedItemValues = itemValueMaps
 112683            .SelectMany(f => f.Values)
 112684            .Distinct()
 112685            .ToArray();
 112686        var existingValues = context.ItemValues
 112687            .Select(e => new
 112688            {
 112689                item = e,
 112690                Key = e.Type + "+" + e.Value
 112691            })
 112692            .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
 112693            .Select(e => e.item)
 112694            .ToArray();
 112695        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 112696        {
 112697            CleanValue = GetCleanValue(f.Value),
 112698            ItemValueId = Guid.NewGuid(),
 112699            Type = f.MagicNumber,
 112700            Value = f.Value
 112701        }).ToArray();
 112702        context.ItemValues.AddRange(missingItemValues);
 112703        context.SaveChanges();
 704
 112705        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 112706        var valueMap = itemValueMaps
 112707            .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type =
 112708            .ToArray();
 709
 112710        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 711
 448712        foreach (var item in valueMap)
 713        {
 112714            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 224715            foreach (var itemValue in item.Values)
 716            {
 0717                var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
 0718                if (existingItem is null)
 719                {
 0720                    context.ItemValuesMap.Add(new ItemValueMap()
 0721                    {
 0722                        Item = null!,
 0723                        ItemId = item.Item.Id,
 0724                        ItemValue = null!,
 0725                        ItemValueId = itemValue.ItemValueId
 0726                    });
 727                }
 728                else
 729                {
 730                    // map exists, remove from list so its been handled.
 0731                    itemMappedValues.Remove(existingItem);
 732                }
 733            }
 734
 735            // all still listed values are not in the new list so remove them.
 112736            context.ItemValuesMap.RemoveRange(itemMappedValues);
 737        }
 738
 112739        context.SaveChanges();
 740
 448741        foreach (var item in tuples)
 742        {
 112743            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 744            {
 112745                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 112746                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 278747                foreach (var ancestorId in validAncestorIds)
 748                {
 27749                    var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
 27750                    if (existingAncestorId is null)
 751                    {
 23752                        context.AncestorIds.Add(new AncestorId()
 23753                        {
 23754                            ParentItemId = ancestorId,
 23755                            ItemId = item.Item.Id,
 23756                            Item = null!,
 23757                            ParentItem = null!
 23758                        });
 759                    }
 760                    else
 761                    {
 4762                        existingAncestorIds.Remove(existingAncestorId);
 763                    }
 764                }
 765
 112766                context.AncestorIds.RemoveRange(existingAncestorIds);
 767            }
 768        }
 769
 112770        context.SaveChanges();
 112771        transaction.Commit();
 224772    }
 773
 774    /// <inheritdoc  />
 775    public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
 776    {
 777        ArgumentNullException.ThrowIfNull(item);
 778        cancellationToken.ThrowIfCancellationRequested();
 779
 780        var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 781
 782        await using (dbContext.ConfigureAwait(false))
 783        {
 784            var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
 785            await using (transaction.ConfigureAwait(false))
 786            {
 787                var userKeys = item.GetUserDataKeys().ToArray();
 788                var retentionDate = (DateTime?)null;
 789
 790                await dbContext.UserData
 791                    .Where(e => e.ItemId == PlaceholderId)
 792                    .Where(e => userKeys.Contains(e.CustomDataKey))
 793                    .ExecuteUpdateAsync(
 794                        e => e
 795                            .SetProperty(f => f.ItemId, item.Id)
 796                            .SetProperty(f => f.RetentionDate, retentionDate),
 797                        cancellationToken).ConfigureAwait(false);
 798
 799                // Rehydrate the cached userdata
 800                item.UserData = await dbContext.UserData
 801                    .AsNoTracking()
 802                    .Where(e => e.ItemId == item.Id)
 803                    .ToArrayAsync(cancellationToken)
 804                    .ConfigureAwait(false);
 805
 806                await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
 807            }
 808        }
 809    }
 810
 811    /// <inheritdoc  />
 812    public BaseItemDto? RetrieveItem(Guid id)
 813    {
 86814        if (id.IsEmpty())
 815        {
 0816            throw new ArgumentException("Guid can't be empty", nameof(id));
 817        }
 818
 86819        using var context = _dbProvider.CreateDbContext();
 86820        var dbQuery = PrepareItemQuery(context, new()
 86821        {
 86822            DtoOptions = new()
 86823            {
 86824                EnableImages = true
 86825            }
 86826        });
 86827        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 86828            .Include(e => e.Provider)
 86829            .Include(e => e.LockedFields)
 86830            .Include(e => e.UserData)
 86831            .Include(e => e.Images);
 832
 86833        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 86834        if (item is null)
 835        {
 86836            return null;
 837        }
 838
 0839        return DeserializeBaseItem(item);
 86840    }
 841
 842    /// <summary>
 843    /// Maps a Entity to the DTO.
 844    /// </summary>
 845    /// <param name="entity">The entity.</param>
 846    /// <param name="dto">The dto base instance.</param>
 847    /// <param name="appHost">The Application server Host.</param>
 848    /// <param name="logger">The applogger.</param>
 849    /// <returns>The dto to map.</returns>
 850    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 851    {
 76852        dto.Id = entity.Id;
 76853        dto.ParentId = entity.ParentId.GetValueOrDefault();
 76854        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 76855        dto.EndDate = entity.EndDate;
 76856        dto.CommunityRating = entity.CommunityRating;
 76857        dto.CustomRating = entity.CustomRating;
 76858        dto.IndexNumber = entity.IndexNumber;
 76859        dto.IsLocked = entity.IsLocked;
 76860        dto.Name = entity.Name;
 76861        dto.OfficialRating = entity.OfficialRating;
 76862        dto.Overview = entity.Overview;
 76863        dto.ParentIndexNumber = entity.ParentIndexNumber;
 76864        dto.PremiereDate = entity.PremiereDate;
 76865        dto.ProductionYear = entity.ProductionYear;
 76866        dto.SortName = entity.SortName;
 76867        dto.ForcedSortName = entity.ForcedSortName;
 76868        dto.RunTimeTicks = entity.RunTimeTicks;
 76869        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 76870        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 76871        dto.IsInMixedFolder = entity.IsInMixedFolder;
 76872        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 76873        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 76874        dto.CriticRating = entity.CriticRating;
 76875        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 76876        dto.OriginalTitle = entity.OriginalTitle;
 76877        dto.Album = entity.Album;
 76878        dto.LUFS = entity.LUFS;
 76879        dto.NormalizationGain = entity.NormalizationGain;
 76880        dto.IsVirtualItem = entity.IsVirtualItem;
 76881        dto.ExternalSeriesId = entity.ExternalSeriesId;
 76882        dto.Tagline = entity.Tagline;
 76883        dto.TotalBitrate = entity.TotalBitrate;
 76884        dto.ExternalId = entity.ExternalId;
 76885        dto.Size = entity.Size;
 76886        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 76887        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 76888        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 76889        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 76890        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 76891        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 76892        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 76893        dto.Width = entity.Width.GetValueOrDefault();
 76894        dto.Height = entity.Height.GetValueOrDefault();
 76895        dto.UserData = entity.UserData;
 896
 76897        if (entity.Provider is not null)
 898        {
 75899            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 900        }
 901
 76902        if (entity.ExtraType is not null)
 903        {
 0904            dto.ExtraType = (ExtraType)entity.ExtraType;
 905        }
 906
 76907        if (entity.LockedFields is not null)
 908        {
 75909            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 910        }
 911
 76912        if (entity.Audio is not null)
 913        {
 0914            dto.Audio = (ProgramAudio)entity.Audio;
 915        }
 916
 76917        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 76918        dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 76919        dto.Studios = entity.Studios?.Split('|') ?? [];
 76920        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 921
 76922        if (dto is IHasProgramAttributes hasProgramAttributes)
 923        {
 0924            hasProgramAttributes.IsMovie = entity.IsMovie;
 0925            hasProgramAttributes.IsSeries = entity.IsSeries;
 0926            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0927            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 928        }
 929
 76930        if (dto is LiveTvChannel liveTvChannel)
 931        {
 0932            liveTvChannel.ServiceName = entity.ExternalServiceId;
 933        }
 934
 76935        if (dto is Trailer trailer)
 936        {
 0937            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 938        }
 939
 76940        if (dto is Video video)
 941        {
 1942            video.PrimaryVersionId = entity.PrimaryVersionId;
 943        }
 944
 76945        if (dto is IHasSeries hasSeriesName)
 946        {
 0947            hasSeriesName.SeriesName = entity.SeriesName;
 0948            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0949            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 950        }
 951
 76952        if (dto is Episode episode)
 953        {
 0954            episode.SeasonName = entity.SeasonName;
 0955            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 956        }
 957
 76958        if (dto is IHasArtist hasArtists)
 959        {
 0960            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 961        }
 962
 76963        if (dto is IHasAlbumArtist hasAlbumArtists)
 964        {
 0965            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 966        }
 967
 76968        if (dto is LiveTvProgram program)
 969        {
 0970            program.ShowId = entity.ShowId;
 971        }
 972
 76973        if (entity.Images is not null)
 974        {
 75975            dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
 976        }
 977
 978        // dto.Type = entity.Type;
 979        // dto.Data = entity.Data;
 980        // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
 76981        if (dto is IHasStartDate hasStartDate)
 982        {
 0983            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 984        }
 985
 986        // Fields that are present in the DB but are never actually used
 987        // dto.UnratedType = entity.UnratedType;
 988        // dto.TopParentId = entity.TopParentId;
 989        // dto.CleanName = entity.CleanName;
 990        // dto.UserDataKey = entity.UserDataKey;
 991
 76992        if (dto is Folder folder)
 993        {
 75994            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 995        }
 996
 76997        return dto;
 998    }
 999
 1000    /// <summary>
 1001    /// Maps a Entity to the DTO.
 1002    /// </summary>
 1003    /// <param name="dto">The entity.</param>
 1004    /// <returns>The dto to map.</returns>
 1005    public BaseItemEntity Map(BaseItemDto dto)
 1006    {
 1221007        var dtoType = dto.GetType();
 1221008        var entity = new BaseItemEntity()
 1221009        {
 1221010            Type = dtoType.ToString(),
 1221011            Id = dto.Id
 1221012        };
 1013
 1221014        if (TypeRequiresDeserialization(dtoType))
 1015        {
 1011016            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 1017        }
 1018
 1221019        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 1221020        entity.Path = GetPathToSave(dto.Path);
 1221021        entity.EndDate = dto.EndDate;
 1221022        entity.CommunityRating = dto.CommunityRating;
 1221023        entity.CustomRating = dto.CustomRating;
 1221024        entity.IndexNumber = dto.IndexNumber;
 1221025        entity.IsLocked = dto.IsLocked;
 1221026        entity.Name = dto.Name;
 1221027        entity.CleanName = GetCleanValue(dto.Name);
 1221028        entity.OfficialRating = dto.OfficialRating;
 1221029        entity.Overview = dto.Overview;
 1221030        entity.ParentIndexNumber = dto.ParentIndexNumber;
 1221031        entity.PremiereDate = dto.PremiereDate;
 1221032        entity.ProductionYear = dto.ProductionYear;
 1221033        entity.SortName = dto.SortName;
 1221034        entity.ForcedSortName = dto.ForcedSortName;
 1221035        entity.RunTimeTicks = dto.RunTimeTicks;
 1221036        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 1221037        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 1221038        entity.IsInMixedFolder = dto.IsInMixedFolder;
 1221039        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 1221040        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 1221041        entity.CriticRating = dto.CriticRating;
 1221042        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 1221043        entity.OriginalTitle = dto.OriginalTitle;
 1221044        entity.Album = dto.Album;
 1221045        entity.LUFS = dto.LUFS;
 1221046        entity.NormalizationGain = dto.NormalizationGain;
 1221047        entity.IsVirtualItem = dto.IsVirtualItem;
 1221048        entity.ExternalSeriesId = dto.ExternalSeriesId;
 1221049        entity.Tagline = dto.Tagline;
 1221050        entity.TotalBitrate = dto.TotalBitrate;
 1221051        entity.ExternalId = dto.ExternalId;
 1221052        entity.Size = dto.Size;
 1221053        entity.Genres = string.Join('|', dto.Genres);
 1221054        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 1221055        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 1221056        entity.ChannelId = dto.ChannelId;
 1221057        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 1221058        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 1221059        entity.OwnerId = dto.OwnerId.ToString();
 1221060        entity.Width = dto.Width;
 1221061        entity.Height = dto.Height;
 1221062        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 1221063        {
 1221064            Item = entity,
 1221065            ProviderId = e.Key,
 1221066            ProviderValue = e.Value
 1221067        }).ToList();
 1068
 1221069        if (dto.Audio.HasValue)
 1070        {
 01071            entity.Audio = (ProgramAudioEntity)dto.Audio;
 1072        }
 1073
 1221074        if (dto.ExtraType.HasValue)
 1075        {
 01076            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 1077        }
 1078
 1221079        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 1221080        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Wher
 1221081        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
 1221082        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
 1221083        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 1221084            .Select(e => new BaseItemMetadataField()
 1221085            {
 1221086                Id = (int)e,
 1221087                Item = entity,
 1221088                ItemId = entity.Id
 1221089            })
 1221090            .ToArray() : null;
 1091
 1221092        if (dto is IHasProgramAttributes hasProgramAttributes)
 1093        {
 01094            entity.IsMovie = hasProgramAttributes.IsMovie;
 01095            entity.IsSeries = hasProgramAttributes.IsSeries;
 01096            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 01097            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 1098        }
 1099
 1221100        if (dto is LiveTvChannel liveTvChannel)
 1101        {
 01102            entity.ExternalServiceId = liveTvChannel.ServiceName;
 1103        }
 1104
 1221105        if (dto is Video video)
 1106        {
 01107            entity.PrimaryVersionId = video.PrimaryVersionId;
 1108        }
 1109
 1221110        if (dto is IHasSeries hasSeriesName)
 1111        {
 01112            entity.SeriesName = hasSeriesName.SeriesName;
 01113            entity.SeriesId = hasSeriesName.SeriesId;
 01114            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 1115        }
 1116
 1221117        if (dto is Episode episode)
 1118        {
 01119            entity.SeasonName = episode.SeasonName;
 01120            entity.SeasonId = episode.SeasonId;
 1121        }
 1122
 1221123        if (dto is IHasArtist hasArtists)
 1124        {
 01125            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
 1126        }
 1127
 1221128        if (dto is IHasAlbumArtist hasAlbumArtists)
 1129        {
 01130            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 1131        }
 1132
 1221133        if (dto is LiveTvProgram program)
 1134        {
 01135            entity.ShowId = program.ShowId;
 1136        }
 1137
 1221138        if (dto.ImageInfos is not null)
 1139        {
 1221140            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 1141        }
 1142
 1221143        if (dto is Trailer trailer)
 1144        {
 01145            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 01146            {
 01147                Id = (int)e,
 01148                Item = entity,
 01149                ItemId = entity.Id
 01150            }).ToArray() ?? [];
 1151        }
 1152
 1153        // dto.Type = entity.Type;
 1154        // dto.Data = entity.Data;
 1221155        entity.MediaType = dto.MediaType.ToString();
 1221156        if (dto is IHasStartDate hasStartDate)
 1157        {
 01158            entity.StartDate = hasStartDate.StartDate;
 1159        }
 1160
 1221161        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 1162
 1163        // Fields that are present in the DB but are never actually used
 1164        // dto.UserDataKey = entity.UserDataKey;
 1165
 1221166        if (dto is Folder folder)
 1167        {
 1221168            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 1221169            entity.IsFolder = folder.IsFolder;
 1170        }
 1171
 1221172        return entity;
 1173    }
 1174
 1175    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 1176    {
 681177        using var context = _dbProvider.CreateDbContext();
 1178
 681179        var query = context.ItemValuesMap
 681180            .AsNoTracking()
 681181            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 681182        if (withItemTypes.Count > 0)
 1183        {
 171184            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 1185        }
 1186
 681187        if (excludeItemTypes.Count > 0)
 1188        {
 171189            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 1190        }
 1191
 1192        // query = query.DistinctBy(e => e.CleanValue);
 681193        return query.Select(e => e.ItemValue)
 681194            .GroupBy(e => e.CleanValue)
 681195            .Select(e => e.First().Value)
 681196            .ToArray();
 681197    }
 1198
 1199    private static bool TypeRequiresDeserialization(Type type)
 1200    {
 1981201        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 1202    }
 1203
 1204    private BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 1205    {
 751206        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 751207        if (_serverConfigurationManager?.Configuration is null)
 1208        {
 01209            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 1210        }
 1211
 751212        var typeToSerialise = GetType(baseItemEntity.Type);
 751213        return BaseItemRepository.DeserializeBaseItem(
 751214            baseItemEntity,
 751215            _logger,
 751216            _appHost,
 751217            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 1218    }
 1219
 1220    /// <summary>
 1221    /// Deserializes a BaseItemEntity and sets all properties.
 1222    /// </summary>
 1223    /// <param name="baseItemEntity">The DB entity.</param>
 1224    /// <param name="logger">Logger.</param>
 1225    /// <param name="appHost">The application server Host.</param>
 1226    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 1227    /// <returns>A mapped BaseItem, or null if the item type is unknown.</returns>
 1228    public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost
 1229    {
 781230        var type = GetType(baseItemEntity.Type);
 781231        if (type is null)
 1232        {
 21233            logger.LogWarning(
 21234                "Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database c
 21235                baseItemEntity.Id,
 21236                baseItemEntity.Type);
 21237            return null;
 1238        }
 1239
 761240        BaseItemDto? dto = null;
 761241        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1242        {
 1243            try
 1244            {
 141245                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 141246            }
 01247            catch (JsonException ex)
 1248            {
 01249                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01250            }
 1251        }
 1252
 761253        if (dto is null)
 1254        {
 621255            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1256        }
 1257
 761258        return Map(baseItemEntity, dto, appHost, logger);
 1259    }
 1260
 1261    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 1262    {
 01263        ArgumentNullException.ThrowIfNull(filter);
 1264
 01265        if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
 1266        {
 01267            filter.EnableTotalRecordCount = false;
 1268        }
 1269
 01270        using var context = _dbProvider.CreateDbContext();
 1271
 01272        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 01273        {
 01274            ExcludeItemTypes = filter.ExcludeItemTypes,
 01275            IncludeItemTypes = filter.IncludeItemTypes,
 01276            MediaTypes = filter.MediaTypes,
 01277            AncestorIds = filter.AncestorIds,
 01278            ItemIds = filter.ItemIds,
 01279            TopParentIds = filter.TopParentIds,
 01280            ParentId = filter.ParentId,
 01281            IsAiring = filter.IsAiring,
 01282            IsMovie = filter.IsMovie,
 01283            IsSports = filter.IsSports,
 01284            IsKids = filter.IsKids,
 01285            IsNews = filter.IsNews,
 01286            IsSeries = filter.IsSeries
 01287        });
 1288
 01289        var itemValuesQuery = context.ItemValues
 01290            .Where(f => itemValueTypes.Contains(f.Type))
 01291            .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
 01292            .Join(
 01293                innerQueryFilter,
 01294                fw => fw.w.ItemId,
 01295                g => g.Id,
 01296                (fw, g) => fw.f.CleanValue);
 1297
 01298        var innerQuery = PrepareItemQuery(context, filter)
 01299            .Where(e => e.Type == returnType)
 01300            .Where(e => itemValuesQuery.Contains(e.CleanName));
 1301
 01302        var outerQueryFilter = new InternalItemsQuery(filter.User)
 01303        {
 01304            IsPlayed = filter.IsPlayed,
 01305            IsFavorite = filter.IsFavorite,
 01306            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 01307            IsLiked = filter.IsLiked,
 01308            IsLocked = filter.IsLocked,
 01309            NameLessThan = filter.NameLessThan,
 01310            NameStartsWith = filter.NameStartsWith,
 01311            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 01312            Tags = filter.Tags,
 01313            OfficialRatings = filter.OfficialRatings,
 01314            StudioIds = filter.StudioIds,
 01315            GenreIds = filter.GenreIds,
 01316            Genres = filter.Genres,
 01317            Years = filter.Years,
 01318            NameContains = filter.NameContains,
 01319            SearchTerm = filter.SearchTerm,
 01320            ExcludeItemIds = filter.ExcludeItemIds
 01321        };
 1322
 01323        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
 01324            .GroupBy(e => e.PresentationUniqueKey)
 01325            .Select(e => e.FirstOrDefault())
 01326            .Select(e => e!.Id);
 1327
 01328        var query = context.BaseItems
 01329            .Include(e => e.TrailerTypes)
 01330            .Include(e => e.Provider)
 01331            .Include(e => e.LockedFields)
 01332            .Include(e => e.Images)
 01333            .AsSingleQuery()
 01334            .Where(e => masterQuery.Contains(e.Id));
 1335
 01336        query = ApplyOrder(query, filter, context);
 1337
 01338        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 01339        if (filter.EnableTotalRecordCount)
 1340        {
 01341            result.TotalRecordCount = query.Count();
 1342        }
 1343
 01344        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 1345        {
 01346            query = query.Skip(filter.StartIndex.Value);
 1347        }
 1348
 01349        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 1350        {
 01351            query = query.Take(filter.Limit.Value);
 1352        }
 1353
 01354        IQueryable<BaseItemEntity>? itemCountQuery = null;
 1355
 01356        if (filter.IncludeItemTypes.Length > 0)
 1357        {
 1358            // if we are to include more then one type, sub query those items beforehand.
 1359
 01360            var typeSubQuery = new InternalItemsQuery(filter.User)
 01361            {
 01362                ExcludeItemTypes = filter.ExcludeItemTypes,
 01363                IncludeItemTypes = filter.IncludeItemTypes,
 01364                MediaTypes = filter.MediaTypes,
 01365                AncestorIds = filter.AncestorIds,
 01366                ExcludeItemIds = filter.ExcludeItemIds,
 01367                ItemIds = filter.ItemIds,
 01368                TopParentIds = filter.TopParentIds,
 01369                ParentId = filter.ParentId,
 01370                IsPlayed = filter.IsPlayed
 01371            };
 1372
 01373            itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 01374                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 1375
 01376            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 01377            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 01378            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 01379            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 01380            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 01381            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 01382            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 1383
 01384            var resultQuery = query.Select(e => new
 01385            {
 01386                item = e,
 01387                // TODO: This is bad refactor!
 01388                itemCount = new ItemCounts()
 01389                {
 01390                    SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
 01391                    EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
 01392                    MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
 01393                    AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
 01394                    ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
 01395                    SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
 01396                    TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
 01397                }
 01398            });
 1399
 01400            result.StartIndex = filter.StartIndex ?? 0;
 01401            result.Items =
 01402            [
 01403                .. resultQuery
 01404                    .AsEnumerable()
 01405                    .Where(e => e is not null)
 01406                    .Select(e => (Item: DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount))
 01407                    .Where(e => e.Item is not null)
 01408                    .Select(e => (e.Item!, e.itemCount))
 01409            ];
 1410        }
 1411        else
 1412        {
 01413            result.StartIndex = filter.StartIndex ?? 0;
 01414            result.Items =
 01415            [
 01416                .. query
 01417                    .AsEnumerable()
 01418                    .Where(e => e is not null)
 01419                    .Select(e => (Item: DeserializeBaseItem(e, filter.SkipDeserialization), ItemCounts: (ItemCounts?)nul
 01420                    .Where(e => e.Item is not null)
 01421                    .Select(e => (e.Item!, e.ItemCounts))
 01422            ];
 1423        }
 1424
 01425        return result;
 01426    }
 1427
 1428    private static void PrepareFilterQuery(InternalItemsQuery query)
 1429    {
 3521430        if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
 1431        {
 01432            query.Limit = query.Limit.Value + 4;
 1433        }
 1434
 3521435        if (query.IsResumable ?? false)
 1436        {
 11437            query.IsVirtualItem = false;
 1438        }
 3521439    }
 1440
 1441    /// <summary>
 1442    /// Gets the clean value for search and sorting purposes.
 1443    /// </summary>
 1444    /// <param name="value">The value to clean.</param>
 1445    /// <returns>The cleaned value.</returns>
 1446    public static string GetCleanValue(string value)
 1447    {
 1251448        if (string.IsNullOrWhiteSpace(value))
 1449        {
 01450            return value;
 1451        }
 1452
 1251453        var noDiacritics = value.RemoveDiacritics();
 1454
 1455        // Build a string where any punctuation or symbol is treated as a separator (space).
 1251456        var sb = new StringBuilder(noDiacritics.Length);
 1251457        var previousWasSpace = false;
 20981458        foreach (var ch in noDiacritics)
 1459        {
 1460            char outCh;
 9241461            if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
 1462            {
 9021463                outCh = ch;
 1464            }
 1465            else
 1466            {
 221467                outCh = ' ';
 1468            }
 1469
 1470            // normalize any whitespace character to a single ASCII space.
 9241471            if (char.IsWhiteSpace(outCh))
 1472            {
 511473                if (!previousWasSpace)
 1474                {
 401475                    sb.Append(' ');
 401476                    previousWasSpace = true;
 1477                }
 1478            }
 1479            else
 1480            {
 8731481                sb.Append(outCh);
 8731482                previousWasSpace = false;
 1483            }
 1484        }
 1485
 1486        // trim leading/trailing spaces that may have been added.
 1251487        var collapsed = sb.ToString().Trim();
 1251488        return collapsed.ToLowerInvariant();
 1489    }
 1490
 1491    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1492    {
 1121493        var list = new List<(ItemValueType, string)>();
 1494
 1121495        if (item is IHasArtist hasArtist)
 1496        {
 01497            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1498        }
 1499
 1121500        if (item is IHasAlbumArtist hasAlbumArtist)
 1501        {
 01502            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1503        }
 1504
 1121505        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 1121506        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 1121507        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1508
 1509        // keywords was 5
 1510
 1121511        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1512
 1513        // Remove all invalid values.
 1121514        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1515
 1121516        return list;
 1517    }
 1518
 1519    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1520    {
 01521        return new BaseItemImageInfo()
 01522        {
 01523            ItemId = baseItemId,
 01524            Id = Guid.NewGuid(),
 01525            Path = e.Path,
 01526            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 01527            DateModified = e.DateModified,
 01528            Height = e.Height,
 01529            Width = e.Width,
 01530            ImageType = (ImageInfoImageType)e.Type,
 01531            Item = null!
 01532        };
 1533    }
 1534
 1535    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 1536    {
 01537        return new ItemImageInfo()
 01538        {
 01539            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 01540            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 01541            DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
 01542            Height = e.Height,
 01543            Width = e.Width,
 01544            Type = (ImageType)e.ImageType
 01545        };
 1546    }
 1547
 1548    private string? GetPathToSave(string path)
 1549    {
 1221550        if (path is null)
 1551        {
 101552            return null;
 1553        }
 1554
 1121555        return _appHost.ReverseVirtualPath(path);
 1556    }
 1557
 1558    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1559    {
 161560        var list = new List<string>();
 1561
 161562        if (IsTypeInQuery(BaseItemKind.Person, query))
 1563        {
 11564            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1565        }
 1566
 161567        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1568        {
 11569            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1570        }
 1571
 161572        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1573        {
 11574            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1575        }
 1576
 161577        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1578        {
 11579            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1580        }
 1581
 161582        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1583        {
 11584            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1585        }
 1586
 161587        return list;
 1588    }
 1589
 1590    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1591    {
 801592        if (query.ExcludeItemTypes.Contains(type))
 1593        {
 01594            return false;
 1595        }
 1596
 801597        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 1598    }
 1599
 1600    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 1601    {
 3521602        if (!query.GroupByPresentationUniqueKey)
 1603        {
 1481604            return false;
 1605        }
 1606
 2041607        if (query.GroupBySeriesPresentationUniqueKey)
 1608        {
 01609            return false;
 1610        }
 1611
 2041612        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 1613        {
 01614            return false;
 1615        }
 1616
 2041617        if (query.User is null)
 1618        {
 2021619            return false;
 1620        }
 1621
 21622        if (query.IncludeItemTypes.Length == 0)
 1623        {
 11624            return true;
 1625        }
 1626
 11627        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 11628            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 11629            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 11630            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 11631            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 11632            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 1633    }
 1634
 1635    private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinD
 1636    {
 3521637        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 3521638        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1639
 3521640        if (hasSearch)
 1641        {
 01642            orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1643        }
 3521644        else if (orderBy.Length == 0)
 1645        {
 2411646            return query.OrderBy(e => e.SortName);
 1647        }
 1648
 1111649        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1650
 1651        // When searching, prioritize by match quality: exact match > prefix match > contains
 1111652        if (hasSearch)
 1653        {
 01654            orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!));
 1655        }
 1656
 1111657        var firstOrdering = orderBy.FirstOrDefault();
 1111658        if (firstOrdering != default)
 1659        {
 1111660            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 1111661            if (orderedQuery is null)
 1662            {
 1663                // No search relevance ordering, start fresh
 1111664                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 1111665                    ? query.OrderBy(expression)
 1111666                    : query.OrderByDescending(expression);
 1667            }
 1668            else
 1669            {
 1670                // Search relevance ordering already applied, chain with ThenBy
 01671                orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
 01672                    ? orderedQuery.ThenBy(expression)
 01673                    : orderedQuery.ThenByDescending(expression);
 1674            }
 1675
 1111676            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 1677            {
 01678                orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending
 01679                    ? orderedQuery.ThenBy(e => e.Name)
 01680                    : orderedQuery.ThenByDescending(e => e.Name);
 1681            }
 1682        }
 1683
 3101684        foreach (var item in orderBy.Skip(1))
 1685        {
 441686            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 441687            if (item.SortOrder == SortOrder.Ascending)
 1688            {
 441689                orderedQuery = orderedQuery!.ThenBy(expression);
 1690            }
 1691            else
 1692            {
 01693                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1694            }
 1695        }
 1696
 1111697        return orderedQuery ?? query;
 1698    }
 1699
 1700    private IQueryable<BaseItemEntity> TranslateQuery(
 1701        IQueryable<BaseItemEntity> baseQuery,
 1702        JellyfinDbContext context,
 1703        InternalItemsQuery filter)
 1704    {
 1705        const int HDWidth = 1200;
 1706        const int UHDWidth = 3800;
 1707        const int UHDHeight = 2100;
 1708
 3521709        var minWidth = filter.MinWidth;
 3521710        var maxWidth = filter.MaxWidth;
 3521711        var now = DateTime.UtcNow;
 1712
 3521713        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 1714        {
 01715            bool includeSD = false;
 01716            bool includeHD = false;
 01717            bool include4K = false;
 1718
 01719            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 1720            {
 01721                includeSD = true;
 1722            }
 1723
 01724            if (filter.IsHD.HasValue && filter.IsHD.Value)
 1725            {
 01726                includeHD = true;
 1727            }
 1728
 01729            if (filter.Is4K.HasValue && filter.Is4K.Value)
 1730            {
 01731                include4K = true;
 1732            }
 1733
 01734            baseQuery = baseQuery.Where(e =>
 01735                (includeSD && e.Width < HDWidth) ||
 01736                (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
 01737                (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
 1738        }
 1739
 3521740        if (minWidth.HasValue)
 1741        {
 01742            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1743        }
 1744
 3521745        if (filter.MinHeight.HasValue)
 1746        {
 01747            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1748        }
 1749
 3521750        if (maxWidth.HasValue)
 1751        {
 01752            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1753        }
 1754
 3521755        if (filter.MaxHeight.HasValue)
 1756        {
 01757            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1758        }
 1759
 3521760        if (filter.IsLocked.HasValue)
 1761        {
 511762            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1763        }
 1764
 3521765        var tags = filter.Tags.ToList();
 3521766        var excludeTags = filter.ExcludeTags.ToList();
 1767
 3521768        if (filter.IsMovie.HasValue)
 1769        {
 01770            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 01771                && (filter.IncludeItemTypes.Length == 0
 01772                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01773                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 1774
 01775            if (!shouldIncludeAllMovieTypes)
 1776            {
 01777                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 1778            }
 1779        }
 1780
 3521781        if (filter.IsSeries.HasValue)
 1782        {
 01783            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1784        }
 1785
 3521786        if (filter.IsSports.HasValue)
 1787        {
 01788            if (filter.IsSports.Value)
 1789            {
 01790                tags.Add("Sports");
 1791            }
 1792            else
 1793            {
 01794                excludeTags.Add("Sports");
 1795            }
 1796        }
 1797
 3521798        if (filter.IsNews.HasValue)
 1799        {
 01800            if (filter.IsNews.Value)
 1801            {
 01802                tags.Add("News");
 1803            }
 1804            else
 1805            {
 01806                excludeTags.Add("News");
 1807            }
 1808        }
 1809
 3521810        if (filter.IsKids.HasValue)
 1811        {
 01812            if (filter.IsKids.Value)
 1813            {
 01814                tags.Add("Kids");
 1815            }
 1816            else
 1817            {
 01818                excludeTags.Add("Kids");
 1819            }
 1820        }
 1821
 3521822        if (!string.IsNullOrEmpty(filter.SearchTerm))
 1823        {
 01824            var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
 01825            var originalSearchTerm = filter.SearchTerm.ToLower();
 01826            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 1827            {
 01828                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 01829                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 1830            }
 1831            else
 1832            {
 01833                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 1834            }
 1835        }
 1836
 3521837        if (filter.IsFolder.HasValue)
 1838        {
 211839            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1840        }
 1841
 3521842        var includeTypes = filter.IncludeItemTypes;
 1843
 1844        // Only specify excluded types if no included types are specified
 3521845        if (filter.IncludeItemTypes.Length == 0)
 1846        {
 2311847            var excludeTypes = filter.ExcludeItemTypes;
 2311848            if (excludeTypes.Length == 1)
 1849            {
 01850                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 1851                {
 01852                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 1853                }
 1854            }
 2311855            else if (excludeTypes.Length > 1)
 1856            {
 01857                var excludeTypeName = new List<string>();
 01858                foreach (var excludeType in excludeTypes)
 1859                {
 01860                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 1861                    {
 01862                        excludeTypeName.Add(baseItemKindName!);
 1863                    }
 1864                }
 1865
 01866                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 1867            }
 1868        }
 1869        else
 1870        {
 1211871            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 1211872            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1873        }
 1874
 3521875        if (filter.ChannelIds.Count > 0)
 1876        {
 01877            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1878        }
 1879
 3521880        if (!filter.ParentId.IsEmpty())
 1881        {
 1481882            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1883        }
 1884
 3521885        if (!string.IsNullOrWhiteSpace(filter.Path))
 1886        {
 01887            var pathToQuery = GetPathToSave(filter.Path);
 01888            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 1889        }
 1890
 3521891        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1892        {
 01893            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1894        }
 1895
 3521896        if (filter.MinCommunityRating.HasValue)
 1897        {
 01898            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1899        }
 1900
 3521901        if (filter.MinIndexNumber.HasValue)
 1902        {
 01903            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1904        }
 1905
 3521906        if (filter.MinParentAndIndexNumber.HasValue)
 1907        {
 01908            baseQuery = baseQuery
 01909                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1910        }
 1911
 3521912        if (filter.MinDateCreated.HasValue)
 1913        {
 01914            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1915        }
 1916
 3521917        if (filter.MinDateLastSaved.HasValue)
 1918        {
 01919            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1920        }
 1921
 3521922        if (filter.MinDateLastSavedForUser.HasValue)
 1923        {
 01924            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1925        }
 1926
 3521927        if (filter.IndexNumber.HasValue)
 1928        {
 01929            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1930        }
 1931
 3521932        if (filter.ParentIndexNumber.HasValue)
 1933        {
 01934            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1935        }
 1936
 3521937        if (filter.ParentIndexNumberNotEquals.HasValue)
 1938        {
 01939            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1940        }
 1941
 3521942        var minEndDate = filter.MinEndDate;
 3521943        var maxEndDate = filter.MaxEndDate;
 1944
 3521945        if (filter.HasAired.HasValue)
 1946        {
 01947            if (filter.HasAired.Value)
 1948            {
 01949                maxEndDate = DateTime.UtcNow;
 1950            }
 1951            else
 1952            {
 01953                minEndDate = DateTime.UtcNow;
 1954            }
 1955        }
 1956
 3521957        if (minEndDate.HasValue)
 1958        {
 01959            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1960        }
 1961
 3521962        if (maxEndDate.HasValue)
 1963        {
 01964            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1965        }
 1966
 3521967        if (filter.MinStartDate.HasValue)
 1968        {
 01969            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1970        }
 1971
 3521972        if (filter.MaxStartDate.HasValue)
 1973        {
 01974            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1975        }
 1976
 3521977        if (filter.MinPremiereDate.HasValue)
 1978        {
 01979            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1980        }
 1981
 3521982        if (filter.MaxPremiereDate.HasValue)
 1983        {
 01984            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1985        }
 1986
 3521987        if (filter.TrailerTypes.Length > 0)
 1988        {
 01989            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 01990            baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
 1991        }
 1992
 3521993        if (filter.IsAiring.HasValue)
 1994        {
 01995            if (filter.IsAiring.Value)
 1996            {
 01997                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 1998            }
 1999            else
 2000            {
 02001                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 2002            }
 2003        }
 2004
 3522005        if (filter.PersonIds.Length > 0)
 2006        {
 02007            var peopleEntityIds = context.BaseItems
 02008                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 02009                .Join(
 02010                    context.Peoples,
 02011                    b => b.Name,
 02012                    p => p.Name,
 02013                    (b, p) => p.Id);
 2014
 02015            baseQuery = baseQuery
 02016                .Where(e => context.PeopleBaseItemMap
 02017                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 2018        }
 2019
 3522020        if (!string.IsNullOrWhiteSpace(filter.Person))
 2021        {
 02022            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 2023        }
 2024
 3522025        if (!string.IsNullOrWhiteSpace(filter.MinSortName))
 2026        {
 2027            // this does not makes sense.
 2028            // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
 2029            // whereClauses.Add("SortName>=@MinSortName");
 2030            // statement?.TryBind("@MinSortName", query.MinSortName);
 2031        }
 2032
 3522033        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 2034        {
 02035            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 2036        }
 2037
 3522038        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 2039        {
 02040            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 2041        }
 2042
 3522043        if (!string.IsNullOrWhiteSpace(filter.Name))
 2044        {
 32045            if (filter.UseRawName == true)
 2046            {
 02047                baseQuery = baseQuery.Where(e => e.Name == filter.Name);
 2048            }
 2049            else
 2050            {
 32051                var cleanName = GetCleanValue(filter.Name);
 32052                baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 2053            }
 2054        }
 2055
 2056        // These are the same, for now
 3522057        var nameContains = filter.NameContains;
 3522058        if (!string.IsNullOrWhiteSpace(nameContains))
 2059        {
 02060            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 2061            {
 02062                nameContains = $"%{nameContains.Trim('%')}%";
 02063                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 2064            }
 2065            else
 2066            {
 02067                baseQuery = baseQuery.Where(e =>
 02068                                    e.CleanName!.Contains(nameContains)
 02069                                    || e.OriginalTitle!.ToLower().Contains(nameContains!));
 2070            }
 2071        }
 2072
 3522073        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 2074        {
 02075            var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
 02076            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
 2077        }
 2078
 3522079        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 2080        {
 02081            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 02082            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
 2083        }
 2084
 3522085        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 2086        {
 02087            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 02088            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
 2089        }
 2090
 3522091        if (filter.ImageTypes.Length > 0)
 2092        {
 1102093            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 1102094            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 2095        }
 2096
 3522097        if (filter.IsLiked.HasValue)
 2098        {
 02099            baseQuery = baseQuery
 02100                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLike
 2101        }
 2102
 3522103        if (filter.IsFavoriteOrLiked.HasValue)
 2104        {
 02105            baseQuery = baseQuery
 02106                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2107        }
 2108
 3522109        if (filter.IsFavorite.HasValue)
 2110        {
 02111            baseQuery = baseQuery
 02112                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2113        }
 2114
 3522115        if (filter.IsPlayed.HasValue)
 2116        {
 2117            // We should probably figure this out for all folders, but for right now, this is the only place where we ne
 02118            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
 2119            {
 02120                baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
 02121                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
 02122                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
 02123                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
 2124            }
 2125            else
 2126            {
 02127                baseQuery = baseQuery
 02128                    .Select(e => new
 02129                    {
 02130                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).Firs
 02131                        Item = e
 02132                    })
 02133                    .Where(e => e.IsPlayed == filter.IsPlayed)
 02134                    .Select(f => f.Item);
 2135            }
 2136        }
 2137
 3522138        if (filter.IsResumable.HasValue)
 2139        {
 12140            if (filter.IsResumable.Value)
 2141            {
 12142                baseQuery = baseQuery
 12143                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks >
 2144            }
 2145            else
 2146            {
 02147                baseQuery = baseQuery
 02148                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks =
 2149            }
 2150        }
 2151
 3522152        if (filter.ArtistIds.Length > 0)
 2153        {
 02154            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2155        }
 2156
 3522157        if (filter.AlbumArtistIds.Length > 0)
 2158        {
 02159            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 2160        }
 2161
 3522162        if (filter.ContributingArtistIds.Length > 0)
 2163        {
 02164            var contributingNames = context.BaseItems
 02165                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 02166                .Select(b => b.CleanName);
 2167
 02168            baseQuery = baseQuery.Where(e =>
 02169                e.ItemValues!.Any(ivm =>
 02170                    ivm.ItemValue.Type == ItemValueType.Artist &&
 02171                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 02172                &&
 02173                !e.ItemValues!.Any(ivm =>
 02174                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 02175                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 2176        }
 2177
 3522178        if (filter.AlbumIds.Length > 0)
 2179        {
 02180            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 02181            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 2182        }
 2183
 3522184        if (filter.ExcludeArtistIds.Length > 0)
 2185        {
 02186            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2187        }
 2188
 3522189        if (filter.GenreIds.Count > 0)
 2190        {
 02191            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 2192        }
 2193
 3522194        if (filter.Genres.Count > 0)
 2195        {
 02196            var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValue
 02197            baseQuery = baseQuery
 02198                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 2199        }
 2200
 3522201        if (tags.Count > 0)
 2202        {
 02203            var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, stri
 02204            baseQuery = baseQuery
 02205                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 2206        }
 2207
 3522208        if (excludeTags.Count > 0)
 2209        {
 02210            var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMa
 02211            baseQuery = baseQuery
 02212                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 2213        }
 2214
 3522215        if (filter.StudioIds.Length > 0)
 2216        {
 02217            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 2218        }
 2219
 3522220        if (filter.OfficialRatings.Length > 0)
 2221        {
 02222            baseQuery = baseQuery
 02223                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 2224        }
 2225
 3522226        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 3522227        if (filter.MinParentalRating != null)
 2228        {
 02229            var min = filter.MinParentalRating;
 02230            var minScore = min.Score;
 02231            var minSubScore = min.SubScore ?? 0;
 2232
 02233            minParentalRatingFilter = e =>
 02234                e.InheritedParentalRatingValue == null ||
 02235                e.InheritedParentalRatingValue > minScore ||
 02236                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 2237        }
 2238
 3522239        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 3522240        if (filter.MaxParentalRating != null)
 2241        {
 512242            var max = filter.MaxParentalRating;
 512243            var maxScore = max.Score;
 512244            var maxSubScore = max.SubScore ?? 0;
 2245
 512246            maxParentalRatingFilter = e =>
 512247                e.InheritedParentalRatingValue == null ||
 512248                e.InheritedParentalRatingValue < maxScore ||
 512249                (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
 2250        }
 2251
 3522252        if (filter.HasParentalRating ?? false)
 2253        {
 02254            if (minParentalRatingFilter != null)
 2255            {
 02256                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2257            }
 2258
 02259            if (maxParentalRatingFilter != null)
 2260            {
 02261                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2262            }
 2263        }
 3522264        else if (filter.BlockUnratedItems.Length > 0)
 2265        {
 02266            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 02267            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 2268
 02269            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 2270            {
 02271                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 2272            }
 02273            else if (minParentalRatingFilter != null)
 2274            {
 02275                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 2276            }
 02277            else if (maxParentalRatingFilter != null)
 2278            {
 02279                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 2280            }
 2281            else
 2282            {
 02283                baseQuery = baseQuery.Where(unratedItemFilter);
 2284            }
 2285        }
 3522286        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 2287        {
 512288            if (minParentalRatingFilter != null)
 2289            {
 02290                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2291            }
 2292
 512293            if (maxParentalRatingFilter != null)
 2294            {
 512295                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2296            }
 2297        }
 3012298        else if (!filter.HasParentalRating ?? false)
 2299        {
 02300            baseQuery = baseQuery
 02301                .Where(e => e.InheritedParentalRatingValue == null);
 2302        }
 2303
 3522304        if (filter.HasOfficialRating.HasValue)
 2305        {
 02306            if (filter.HasOfficialRating.Value)
 2307            {
 02308                baseQuery = baseQuery
 02309                    .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
 2310            }
 2311            else
 2312            {
 02313                baseQuery = baseQuery
 02314                    .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
 2315            }
 2316        }
 2317
 3522318        if (filter.HasOverview.HasValue)
 2319        {
 02320            if (filter.HasOverview.Value)
 2321            {
 02322                baseQuery = baseQuery
 02323                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 2324            }
 2325            else
 2326            {
 02327                baseQuery = baseQuery
 02328                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 2329            }
 2330        }
 2331
 3522332        if (filter.HasOwnerId.HasValue)
 2333        {
 02334            if (filter.HasOwnerId.Value)
 2335            {
 02336                baseQuery = baseQuery
 02337                    .Where(e => e.OwnerId != null);
 2338            }
 2339            else
 2340            {
 02341                baseQuery = baseQuery
 02342                    .Where(e => e.OwnerId == null);
 2343            }
 2344        }
 2345
 3522346        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 2347        {
 02348            baseQuery = baseQuery
 02349                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filte
 2350        }
 2351
 3522352        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2353        {
 02354            baseQuery = baseQuery
 02355                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2356        }
 2357
 3522358        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2359        {
 02360            baseQuery = baseQuery
 02361                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2362        }
 2363
 3522364        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 2365        {
 02366            baseQuery = baseQuery
 02367                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == fi
 2368        }
 2369
 3522370        if (filter.HasSubtitles.HasValue)
 2371        {
 02372            baseQuery = baseQuery
 02373                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2374        }
 2375
 3522376        if (filter.HasChapterImages.HasValue)
 2377        {
 02378            baseQuery = baseQuery
 02379                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2380        }
 2381
 3522382        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2383        {
 172384            baseQuery = baseQuery
 172385                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 2386        }
 2387
 3522388        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2389        {
 172390            baseQuery = baseQuery
 172391                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2392        }
 2393
 3522394        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2395        {
 172396            baseQuery = baseQuery
 172397                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2398        }
 2399
 3522400        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2401        {
 172402            baseQuery = baseQuery
 172403                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2404        }
 2405
 3522406        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2407        {
 02408            baseQuery = baseQuery
 02409                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2410        }
 2411
 3522412        if (filter.Years.Length > 0)
 2413        {
 02414            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2415        }
 2416
 3522417        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 3522418        if (isVirtualItem.HasValue)
 2419        {
 222420            baseQuery = baseQuery
 222421                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2422        }
 2423
 3522424        if (filter.IsSpecialSeason.HasValue)
 2425        {
 02426            if (filter.IsSpecialSeason.Value)
 2427            {
 02428                baseQuery = baseQuery
 02429                    .Where(e => e.IndexNumber == 0);
 2430            }
 2431            else
 2432            {
 02433                baseQuery = baseQuery
 02434                    .Where(e => e.IndexNumber != 0);
 2435            }
 2436        }
 2437
 3522438        if (filter.IsUnaired.HasValue)
 2439        {
 02440            if (filter.IsUnaired.Value)
 2441            {
 02442                baseQuery = baseQuery
 02443                    .Where(e => e.PremiereDate >= now);
 2444            }
 2445            else
 2446            {
 02447                baseQuery = baseQuery
 02448                    .Where(e => e.PremiereDate < now);
 2449            }
 2450        }
 2451
 3522452        if (filter.MediaTypes.Length > 0)
 2453        {
 212454            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 212455            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 2456        }
 2457
 3522458        if (filter.ItemIds.Length > 0)
 2459        {
 02460            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2461        }
 2462
 3522463        if (filter.ExcludeItemIds.Length > 0)
 2464        {
 02465            baseQuery = baseQuery
 02466                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 2467        }
 2468
 3522469        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 2470        {
 02471            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 02472            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 2473        }
 2474
 3522475        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 2476        {
 2477            // Allow setting a null or empty value to get all items that have the specified provider set.
 02478            var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArra
 02479            if (includeAny.Length > 0)
 2480            {
 02481                baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
 2482            }
 2483
 02484            var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Ke
 02485            if (includeSelected.Length > 0)
 2486            {
 02487                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 2488            }
 2489        }
 2490
 3522491        if (filter.HasImdbId.HasValue)
 2492        {
 02493            baseQuery = filter.HasImdbId.Value
 02494                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().T
 02495                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().T
 2496        }
 2497
 3522498        if (filter.HasTmdbId.HasValue)
 2499        {
 02500            baseQuery = filter.HasTmdbId.Value
 02501                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().T
 02502                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().T
 2503        }
 2504
 3522505        if (filter.HasTvdbId.HasValue)
 2506        {
 02507            baseQuery = filter.HasTvdbId.Value
 02508                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().T
 02509                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().T
 2510        }
 2511
 3522512        var queryTopParentIds = filter.TopParentIds;
 2513
 3522514        if (queryTopParentIds.Length > 0)
 2515        {
 162516            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 162517            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 162518            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2519            {
 02520                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2521            }
 2522            else
 2523            {
 162524                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2525            }
 2526        }
 2527
 3522528        if (filter.AncestorIds.Length > 0)
 2529        {
 452530            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2531        }
 2532
 3522533        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 2534        {
 02535            baseQuery = baseQuery
 02536                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 2537        }
 2538
 3522539        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2540        {
 02541            baseQuery = baseQuery
 02542                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2543        }
 2544
 3522545        if (filter.ExcludeInheritedTags.Length > 0)
 2546        {
 02547            var excludedTags = filter.ExcludeInheritedTags;
 02548            baseQuery = baseQuery.Where(e =>
 02549                !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.Clea
 02550                && (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.
 2551        }
 2552
 3522553        if (filter.IncludeInheritedTags.Length > 0)
 2554        {
 02555            var includeTags = filter.IncludeInheritedTags;
 02556            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 02557            baseQuery = baseQuery.Where(e =>
 02558                e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanV
 02559
 02560                // For seasons and episodes, we also need to check the parent series' tags.
 02561                || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Ty
 02562
 02563                // A playlist should be accessible to its owner regardless of allowed tags
 02564                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 2565        }
 2566
 3522567        if (filter.SeriesStatuses.Length > 0)
 2568        {
 02569            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 02570            baseQuery = baseQuery
 02571                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 2572        }
 2573
 3522574        if (filter.BoxSetLibraryFolders.Length > 0)
 2575        {
 02576            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 02577            baseQuery = baseQuery
 02578                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 2579        }
 2580
 3522581        if (filter.VideoTypes.Length > 0)
 2582        {
 02583            var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
 02584            baseQuery = baseQuery
 02585                .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
 2586        }
 2587
 3522588        if (filter.Is3D.HasValue)
 2589        {
 02590            if (filter.Is3D.Value)
 2591            {
 02592                baseQuery = baseQuery
 02593                    .Where(e => e.Data!.Contains("Video3DFormat"));
 2594            }
 2595            else
 2596            {
 02597                baseQuery = baseQuery
 02598                    .Where(e => !e.Data!.Contains("Video3DFormat"));
 2599            }
 2600        }
 2601
 3522602        if (filter.IsPlaceHolder.HasValue)
 2603        {
 02604            if (filter.IsPlaceHolder.Value)
 2605            {
 02606                baseQuery = baseQuery
 02607                    .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
 2608            }
 2609            else
 2610            {
 02611                baseQuery = baseQuery
 02612                    .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
 2613            }
 2614        }
 2615
 3522616        if (filter.HasSpecialFeature.HasValue)
 2617        {
 02618            if (filter.HasSpecialFeature.Value)
 2619            {
 02620                baseQuery = baseQuery
 02621                    .Where(e => e.ExtraIds != null);
 2622            }
 2623            else
 2624            {
 02625                baseQuery = baseQuery
 02626                    .Where(e => e.ExtraIds == null);
 2627            }
 2628        }
 2629
 3522630        if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
 2631        {
 02632            if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo
 2633            {
 02634                baseQuery = baseQuery
 02635                    .Where(e => e.ExtraIds != null);
 2636            }
 2637            else
 2638            {
 02639                baseQuery = baseQuery
 02640                    .Where(e => e.ExtraIds == null);
 2641            }
 2642        }
 2643
 3522644        return baseQuery;
 2645    }
 2646
 2647    /// <inheritdoc/>
 2648    public async Task<bool> ItemExistsAsync(Guid id)
 2649    {
 2650        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 2651        await using (dbContext.ConfigureAwait(false))
 2652        {
 2653            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 2654        }
 2655    }
 2656
 2657    /// <inheritdoc/>
 2658    public bool GetIsPlayed(User user, Guid id, bool recursive)
 2659    {
 02660        using var dbContext = _dbProvider.CreateDbContext();
 2661
 02662        if (recursive)
 2663        {
 02664            var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
 2665
 02666            return dbContext.BaseItems
 02667                    .Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
 02668                    .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
 2669        }
 2670
 02671        return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.P
 02672    }
 2673
 2674    private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseIte
 2675    {
 12676        var folderStack = new HashSet<Guid>()
 12677            {
 12678                parentId
 12679            };
 12680        var folderList = new HashSet<Guid>()
 12681            {
 12682                parentId
 12683            };
 2684
 22685        while (folderStack.Count != 0)
 2686        {
 12687            var items = folderStack.ToArray();
 12688            folderStack.Clear();
 12689            var query = dbContext.BaseItems
 12690                .WhereOneOrMany(items, e => e.ParentId!.Value);
 2691
 12692            if (filter != null)
 2693            {
 02694                query = query.Where(filter);
 2695            }
 2696
 22697            foreach (var item in query.Select(e => e.Id).ToArray())
 2698            {
 02699                if (folderList.Add(item))
 2700                {
 02701                    folderStack.Add(item);
 2702                }
 2703            }
 2704        }
 2705
 12706        return folderList;
 2707    }
 2708
 2709    /// <inheritdoc/>
 2710    public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
 2711    {
 02712        using var dbContext = _dbProvider.CreateDbContext();
 2713
 02714        var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 02715            .Where(e => artistNames.Contains(e.Name))
 02716            .ToArray();
 2717
 02718        var lookup = artists
 02719            .GroupBy(e => e.Name!)
 02720            .ToDictionary(
 02721                g => g.Key,
 02722                g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
 2723
 02724        var result = new Dictionary<string, MusicArtist[]>(artistNames.Count);
 02725        foreach (var name in artistNames)
 2726        {
 02727            if (lookup.TryGetValue(name, out var artistArray))
 2728            {
 02729                result[name] = artistArray;
 2730            }
 2731        }
 2732
 02733        return result;
 02734    }
 2735}

Methods/Properties

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