< 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
52%
Covered lines: 692
Uncovered lines: 630
Coverable lines: 1322
Total lines: 2671
Line coverage: 52.3%
Branch coverage
50%
Covered branches: 383
Total branches: 752
Branch coverage: 50.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/5/2025 - 12:11:27 AM Line coverage: 51.7% (664/1282) Branch coverage: 48.8% (351/718) Total lines: 257510/9/2025 - 12:11:25 AM Line coverage: 51.5% (664/1288) Branch coverage: 48.8% (351/718) Total lines: 258210/12/2025 - 12:11:07 AM Line coverage: 51.6% (664/1286) Branch coverage: 48.8% (351/718) Total lines: 258210/14/2025 - 12:11:23 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: 2671 10/5/2025 - 12:11:27 AM Line coverage: 51.7% (664/1282) Branch coverage: 48.8% (351/718) Total lines: 257510/9/2025 - 12:11:25 AM Line coverage: 51.5% (664/1288) Branch coverage: 48.8% (351/718) Total lines: 258210/12/2025 - 12:11:07 AM Line coverage: 51.6% (664/1286) Branch coverage: 48.8% (351/718) Total lines: 258210/14/2025 - 12:11:23 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: 2671

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(...)100%11100%
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(...)83.33%383688.34%
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(...)62.5%302478.26%
TranslateQuery(...)45.11%3064434836.99%
GetIsPlayed(...)0%620%
TraverseHirachyDown(...)50%8884.21%
FindArtists(...)100%210%

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    {
 2105        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
 2110        using var context = _dbProvider.CreateDbContext();
 2111        using var transaction = context.Database.BeginTransaction();
 112
 2113        var date = (DateTime?)DateTime.UtcNow;
 114
 2115        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.
 2121        context.UserData
 2122            .Join(
 2123                context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
 2124                placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
 2125                userData => new { userData.UserId, userData.CustomDataKey },
 2126                (placeholder, userData) => placeholder)
 2127            .Where(e => e.ItemId == PlaceholderId)
 2128            .ExecuteDelete();
 129
 130        // Detach all user watch data
 2131        context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
 2132            .ExecuteUpdate(e => e
 2133                .SetProperty(f => f.RetentionDate, date)
 2134                .SetProperty(f => f.ItemId, PlaceholderId));
 135
 2136        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2137        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
 2138        context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2139        context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2140        context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2141        context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2142        context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2143        context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
 2144        context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2145        context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2146        context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2147        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
 2148        context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2149        context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2150        context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2151        context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2152        var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distin
 2153        context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2154        context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
 2155        context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2156        context.SaveChanges();
 2157        transaction.Commit();
 4158    }
 159
 160    /// <inheritdoc />
 161    public void UpdateInheritedValues()
 162    {
 15163        using var context = _dbProvider.CreateDbContext();
 15164        using var transaction = context.Database.BeginTransaction();
 165
 15166        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 167        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 15168        context.SaveChanges();
 169
 15170        transaction.Commit();
 30171    }
 172
 173    /// <inheritdoc />
 174    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 175    {
 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    {
 330288        ArgumentNullException.ThrowIfNull(filter);
 330289        PrepareFilterQuery(filter);
 290
 330291        using var context = _dbProvider.CreateDbContext();
 330292        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 293
 330294        dbQuery = TranslateQuery(dbQuery, context, filter);
 295
 330296        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 330297        dbQuery = ApplyQueryPaging(dbQuery, filter);
 330298        dbQuery = ApplyNavigations(dbQuery, filter);
 299
 330300        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializ
 330301    }
 302
 303    /// <inheritdoc/>
 304    public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 305    {
 0306        ArgumentNullException.ThrowIfNull(filter);
 0307        PrepareFilterQuery(filter);
 308
 309        // Early exit if collection type is not tvshows or music
 0310        if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music)
 311        {
 0312            return Array.Empty<BaseItem>();
 313        }
 314
 0315        using var context = _dbProvider.CreateDbContext();
 316
 317        // Subquery to group by SeriesNames/Album and get the max Date Created for each group.
 0318        var subquery = PrepareItemQuery(context, filter);
 0319        subquery = TranslateQuery(subquery, context, filter);
 0320        var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album)
 0321            .Select(g => new
 0322            {
 0323                Key = g.Key,
 0324                MaxDateCreated = g.Max(a => a.DateCreated)
 0325            })
 0326            .OrderByDescending(g => g.MaxDateCreated)
 0327            .Select(g => g);
 328
 0329        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 330        {
 0331            subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
 332        }
 333
 0334        filter.Limit = null;
 335
 0336        var mainquery = PrepareItemQuery(context, filter);
 0337        mainquery = TranslateQuery(mainquery, context, filter);
 0338        mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
 0339        mainquery = ApplyGroupingFilter(context, mainquery, filter);
 0340        mainquery = ApplyQueryPaging(mainquery, filter);
 341
 0342        mainquery = ApplyNavigations(mainquery, filter);
 343
 0344        return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserial
 0345    }
 346
 347    /// <inheritdoc />
 348    public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
 349    {
 0350        ArgumentNullException.ThrowIfNull(filter);
 0351        ArgumentNullException.ThrowIfNull(filter.User);
 352
 0353        using var context = _dbProvider.CreateDbContext();
 354
 0355        var query = context.BaseItems
 0356            .AsNoTracking()
 0357            .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
 0358            .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
 0359            .Join(
 0360                context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
 0361                i => new { UserId = filter.User.Id, ItemId = i.Id },
 0362                u => new { UserId = u.UserId, ItemId = u.ItemId },
 0363                (entity, data) => new { Item = entity, UserData = data })
 0364            .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
 0365            .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
 0366            .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
 0367            .OrderByDescending(g => g.LastPlayedDate)
 0368            .Select(g => g.Key!);
 369
 0370        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 371        {
 0372            query = query.Take(filter.Limit.Value);
 373        }
 374
 0375        return query.ToArray();
 0376    }
 377
 378    private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery
 379    {
 380        // This whole block is needed to filter duplicate entries on request
 381        // for the time being it cannot be used because it would destroy the ordering
 382        // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions b
 383        // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
 384
 347385        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 347386        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 387        {
 0388            var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(
 0389            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 390        }
 347391        else if (enableGroupByPresentationUniqueKey)
 392        {
 1393            var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!
 1394            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 395        }
 346396        else if (filter.GroupBySeriesPresentationUniqueKey)
 397        {
 0398            var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e
 0399            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 400        }
 401        else
 402        {
 346403            dbQuery = dbQuery.Distinct();
 404        }
 405
 347406        dbQuery = ApplyOrder(dbQuery, filter, context);
 407
 347408        return dbQuery;
 409    }
 410
 411    private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 412    {
 347413        if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 414        {
 0415            dbQuery = dbQuery.Include(e => e.TrailerTypes);
 416        }
 417
 347418        if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
 419        {
 282420            dbQuery = dbQuery.Include(e => e.Provider);
 421        }
 422
 347423        if (filter.DtoOptions.ContainsField(ItemFields.Settings))
 424        {
 282425            dbQuery = dbQuery.Include(e => e.LockedFields);
 426        }
 427
 347428        if (filter.DtoOptions.EnableUserData)
 429        {
 347430            dbQuery = dbQuery.Include(e => e.UserData);
 431        }
 432
 347433        if (filter.DtoOptions.EnableImages)
 434        {
 347435            dbQuery = dbQuery.Include(e => e.Images);
 436        }
 437
 347438        return dbQuery;
 439    }
 440
 441    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 442    {
 347443        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 444        {
 0445            dbQuery = dbQuery.Skip(filter.StartIndex.Value);
 446        }
 447
 347448        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 449        {
 107450            dbQuery = dbQuery.Take(filter.Limit.Value);
 451        }
 452
 347453        return dbQuery;
 454    }
 455
 456    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 457    {
 17458        dbQuery = TranslateQuery(dbQuery, context, filter);
 17459        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 17460        dbQuery = ApplyQueryPaging(dbQuery, filter);
 17461        dbQuery = ApplyNavigations(dbQuery, filter);
 17462        return dbQuery;
 463    }
 464
 465    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 466    {
 416467        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 416468        dbQuery = dbQuery.AsSingleQuery();
 469
 416470        return dbQuery;
 471    }
 472
 473    /// <inheritdoc/>
 474    public int GetCount(InternalItemsQuery filter)
 475    {
 0476        ArgumentNullException.ThrowIfNull(filter);
 477        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0478        PrepareFilterQuery(filter);
 479
 0480        using var context = _dbProvider.CreateDbContext();
 0481        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 482
 0483        return dbQuery.Count();
 0484    }
 485
 486    /// <inheritdoc />
 487    public ItemCounts GetItemCounts(InternalItemsQuery filter)
 488    {
 0489        ArgumentNullException.ThrowIfNull(filter);
 490        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0491        PrepareFilterQuery(filter);
 492
 0493        using var context = _dbProvider.CreateDbContext();
 0494        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 495
 0496        var counts = dbQuery
 0497            .GroupBy(x => x.Type)
 0498            .Select(x => new { x.Key, Count = x.Count() })
 0499            .ToArray();
 500
 0501        var lookup = _itemTypeLookup.BaseItemKindNames;
 0502        var result = new ItemCounts();
 0503        foreach (var count in counts)
 504        {
 0505            if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
 506            {
 0507                result.AlbumCount = count.Count;
 508            }
 0509            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
 510            {
 0511                result.ArtistCount = count.Count;
 512            }
 0513            else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
 514            {
 0515                result.EpisodeCount = count.Count;
 516            }
 0517            else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
 518            {
 0519                result.MovieCount = count.Count;
 520            }
 0521            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
 522            {
 0523                result.MusicVideoCount = count.Count;
 524            }
 0525            else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
 526            {
 0527                result.ProgramCount = count.Count;
 528            }
 0529            else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
 530            {
 0531                result.SeriesCount = count.Count;
 532            }
 0533            else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
 534            {
 0535                result.SongCount = count.Count;
 536            }
 0537            else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
 538            {
 0539                result.TrailerCount = count.Count;
 540            }
 541        }
 542
 0543        return result;
 0544    }
 545
 546#pragma warning disable CA1307 // Specify StringComparison for clarity
 547    /// <summary>
 548    /// Gets the type.
 549    /// </summary>
 550    /// <param name="typeName">Name of the type.</param>
 551    /// <returns>Type.</returns>
 552    /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
 553    private static Type? GetType(string typeName)
 554    {
 149555        ArgumentException.ThrowIfNullOrEmpty(typeName);
 556
 557        // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static 
 558        // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are 
 149559        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 149560            .Select(a => a.GetType(k))
 149561            .FirstOrDefault(t => t is not null));
 562    }
 563
 564    /// <inheritdoc  />
 565    public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default)
 566    {
 567        ArgumentNullException.ThrowIfNull(item);
 568
 569        var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray();
 570
 571        var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 572        await using (context.ConfigureAwait(false))
 573        {
 574            if (!await context.BaseItems
 575                .AnyAsync(bi => bi.Id == item.Id, cancellationToken)
 576                .ConfigureAwait(false))
 577            {
 578                _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
 579                return;
 580            }
 581
 582            await context.BaseItemImageInfos
 583                .Where(e => e.ItemId == item.Id)
 584                .ExecuteDeleteAsync(cancellationToken)
 585                .ConfigureAwait(false);
 586
 587            await context.BaseItemImageInfos
 588                .AddRangeAsync(images, cancellationToken)
 589                .ConfigureAwait(false);
 590
 591            await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 592        }
 593    }
 594
 595    /// <inheritdoc  />
 596    public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 597    {
 111598        UpdateOrInsertItems(items, cancellationToken);
 110599    }
 600
 601    /// <inheritdoc cref="IItemRepository"/>
 602    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 603    {
 111604        ArgumentNullException.ThrowIfNull(items);
 111605        cancellationToken.ThrowIfCancellationRequested();
 606
 110607        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 440608        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
 609        {
 110610            var ancestorIds = item.SupportsAncestors ?
 110611                item.GetAncestorIds().Distinct().ToList() :
 110612                null;
 613
 110614            var topParent = item.GetTopParent();
 615
 110616            var userdataKey = item.GetUserDataKeys();
 110617            var inheritedTags = item.GetInheritedTags();
 618
 110619            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 620        }
 621
 110622        using var context = _dbProvider.CreateDbContext();
 110623        using var transaction = context.Database.BeginTransaction();
 624
 110625        var ids = tuples.Select(f => f.Item.Id).ToArray();
 110626        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 110627        var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
 628
 440629        foreach (var item in tuples)
 630        {
 110631            var entity = Map(item.Item);
 632            // TODO: refactor this "inconsistency"
 110633            entity.TopParentId = item.TopParent?.Id;
 634
 110635            if (!existingItems.Any(e => e == entity.Id))
 636            {
 59637                context.BaseItems.Add(entity);
 638            }
 639            else
 640            {
 51641                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 51642                context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 51643                context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 644
 51645                if (entity.Images is { Count: > 0 })
 646                {
 0647                    context.BaseItemImageInfos.AddRange(entity.Images);
 648                }
 649
 51650                if (entity.LockedFields is { Count: > 0 })
 651                {
 0652                    context.BaseItemMetadataFields.AddRange(entity.LockedFields);
 653                }
 654
 51655                context.BaseItems.Attach(entity).State = EntityState.Modified;
 656            }
 657        }
 658
 110659        context.SaveChanges();
 660
 338661        foreach (var item in newItems)
 662        {
 663            // reattach old userData entries
 59664            var userKeys = item.UserDataKey.ToArray();
 59665            var retentionDate = (DateTime?)null;
 59666            context.UserData
 59667                .Where(e => e.ItemId == PlaceholderId)
 59668                .Where(e => userKeys.Contains(e.CustomDataKey))
 59669                .ExecuteUpdate(e => e
 59670                    .SetProperty(f => f.ItemId, item.Item.Id)
 59671                    .SetProperty(f => f.RetentionDate, retentionDate));
 672        }
 673
 110674        var itemValueMaps = tuples
 110675            .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 110676            .ToArray();
 110677        var allListedItemValues = itemValueMaps
 110678            .SelectMany(f => f.Values)
 110679            .Distinct()
 110680            .ToArray();
 110681        var existingValues = context.ItemValues
 110682            .Select(e => new
 110683            {
 110684                item = e,
 110685                Key = e.Type + "+" + e.Value
 110686            })
 110687            .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
 110688            .Select(e => e.item)
 110689            .ToArray();
 110690        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 110691        {
 110692            CleanValue = GetCleanValue(f.Value),
 110693            ItemValueId = Guid.NewGuid(),
 110694            Type = f.MagicNumber,
 110695            Value = f.Value
 110696        }).ToArray();
 110697        context.ItemValues.AddRange(missingItemValues);
 110698        context.SaveChanges();
 699
 110700        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 110701        var valueMap = itemValueMaps
 110702            .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type =
 110703            .ToArray();
 704
 110705        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 706
 440707        foreach (var item in valueMap)
 708        {
 110709            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 220710            foreach (var itemValue in item.Values)
 711            {
 0712                var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
 0713                if (existingItem is null)
 714                {
 0715                    context.ItemValuesMap.Add(new ItemValueMap()
 0716                    {
 0717                        Item = null!,
 0718                        ItemId = item.Item.Id,
 0719                        ItemValue = null!,
 0720                        ItemValueId = itemValue.ItemValueId
 0721                    });
 722                }
 723                else
 724                {
 725                    // map exists, remove from list so its been handled.
 0726                    itemMappedValues.Remove(existingItem);
 727                }
 728            }
 729
 730            // all still listed values are not in the new list so remove them.
 110731            context.ItemValuesMap.RemoveRange(itemMappedValues);
 732        }
 733
 110734        context.SaveChanges();
 735
 440736        foreach (var item in tuples)
 737        {
 110738            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 739            {
 110740                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 110741                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 274742                foreach (var ancestorId in validAncestorIds)
 743                {
 27744                    var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
 27745                    if (existingAncestorId is null)
 746                    {
 23747                        context.AncestorIds.Add(new AncestorId()
 23748                        {
 23749                            ParentItemId = ancestorId,
 23750                            ItemId = item.Item.Id,
 23751                            Item = null!,
 23752                            ParentItem = null!
 23753                        });
 754                    }
 755                    else
 756                    {
 4757                        existingAncestorIds.Remove(existingAncestorId);
 758                    }
 759                }
 760
 110761                context.AncestorIds.RemoveRange(existingAncestorIds);
 762            }
 763        }
 764
 110765        context.SaveChanges();
 110766        transaction.Commit();
 220767    }
 768
 769    /// <inheritdoc  />
 770    public BaseItemDto? RetrieveItem(Guid id)
 771    {
 86772        if (id.IsEmpty())
 773        {
 0774            throw new ArgumentException("Guid can't be empty", nameof(id));
 775        }
 776
 86777        using var context = _dbProvider.CreateDbContext();
 86778        var dbQuery = PrepareItemQuery(context, new()
 86779        {
 86780            DtoOptions = new()
 86781            {
 86782                EnableImages = true
 86783            }
 86784        });
 86785        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 86786            .Include(e => e.Provider)
 86787            .Include(e => e.LockedFields)
 86788            .Include(e => e.UserData)
 86789            .Include(e => e.Images);
 790
 86791        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 86792        if (item is null)
 793        {
 86794            return null;
 795        }
 796
 0797        return DeserializeBaseItem(item);
 86798    }
 799
 800    /// <summary>
 801    /// Maps a Entity to the DTO.
 802    /// </summary>
 803    /// <param name="entity">The entity.</param>
 804    /// <param name="dto">The dto base instance.</param>
 805    /// <param name="appHost">The Application server Host.</param>
 806    /// <param name="logger">The applogger.</param>
 807    /// <returns>The dto to map.</returns>
 808    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 809    {
 74810        dto.Id = entity.Id;
 74811        dto.ParentId = entity.ParentId.GetValueOrDefault();
 74812        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 74813        dto.EndDate = entity.EndDate;
 74814        dto.CommunityRating = entity.CommunityRating;
 74815        dto.CustomRating = entity.CustomRating;
 74816        dto.IndexNumber = entity.IndexNumber;
 74817        dto.IsLocked = entity.IsLocked;
 74818        dto.Name = entity.Name;
 74819        dto.OfficialRating = entity.OfficialRating;
 74820        dto.Overview = entity.Overview;
 74821        dto.ParentIndexNumber = entity.ParentIndexNumber;
 74822        dto.PremiereDate = entity.PremiereDate;
 74823        dto.ProductionYear = entity.ProductionYear;
 74824        dto.SortName = entity.SortName;
 74825        dto.ForcedSortName = entity.ForcedSortName;
 74826        dto.RunTimeTicks = entity.RunTimeTicks;
 74827        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 74828        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 74829        dto.IsInMixedFolder = entity.IsInMixedFolder;
 74830        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 74831        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 74832        dto.CriticRating = entity.CriticRating;
 74833        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 74834        dto.OriginalTitle = entity.OriginalTitle;
 74835        dto.Album = entity.Album;
 74836        dto.LUFS = entity.LUFS;
 74837        dto.NormalizationGain = entity.NormalizationGain;
 74838        dto.IsVirtualItem = entity.IsVirtualItem;
 74839        dto.ExternalSeriesId = entity.ExternalSeriesId;
 74840        dto.Tagline = entity.Tagline;
 74841        dto.TotalBitrate = entity.TotalBitrate;
 74842        dto.ExternalId = entity.ExternalId;
 74843        dto.Size = entity.Size;
 74844        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 74845        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 74846        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 74847        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 74848        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 74849        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 74850        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 74851        dto.Width = entity.Width.GetValueOrDefault();
 74852        dto.Height = entity.Height.GetValueOrDefault();
 74853        dto.UserData = entity.UserData;
 854
 74855        if (entity.Provider is not null)
 856        {
 73857            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 858        }
 859
 74860        if (entity.ExtraType is not null)
 861        {
 0862            dto.ExtraType = (ExtraType)entity.ExtraType;
 863        }
 864
 74865        if (entity.LockedFields is not null)
 866        {
 73867            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 868        }
 869
 74870        if (entity.Audio is not null)
 871        {
 0872            dto.Audio = (ProgramAudio)entity.Audio;
 873        }
 874
 74875        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 74876        dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
 74877        dto.Studios = entity.Studios?.Split('|') ?? [];
 74878        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 879
 74880        if (dto is IHasProgramAttributes hasProgramAttributes)
 881        {
 0882            hasProgramAttributes.IsMovie = entity.IsMovie;
 0883            hasProgramAttributes.IsSeries = entity.IsSeries;
 0884            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0885            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 886        }
 887
 74888        if (dto is LiveTvChannel liveTvChannel)
 889        {
 0890            liveTvChannel.ServiceName = entity.ExternalServiceId;
 891        }
 892
 74893        if (dto is Trailer trailer)
 894        {
 0895            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 896        }
 897
 74898        if (dto is Video video)
 899        {
 1900            video.PrimaryVersionId = entity.PrimaryVersionId;
 901        }
 902
 74903        if (dto is IHasSeries hasSeriesName)
 904        {
 0905            hasSeriesName.SeriesName = entity.SeriesName;
 0906            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0907            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 908        }
 909
 74910        if (dto is Episode episode)
 911        {
 0912            episode.SeasonName = entity.SeasonName;
 0913            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 914        }
 915
 74916        if (dto is IHasArtist hasArtists)
 917        {
 0918            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 919        }
 920
 74921        if (dto is IHasAlbumArtist hasAlbumArtists)
 922        {
 0923            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 924        }
 925
 74926        if (dto is LiveTvProgram program)
 927        {
 0928            program.ShowId = entity.ShowId;
 929        }
 930
 74931        if (entity.Images is not null)
 932        {
 73933            dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
 934        }
 935
 936        // dto.Type = entity.Type;
 937        // dto.Data = entity.Data;
 938        // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
 74939        if (dto is IHasStartDate hasStartDate)
 940        {
 0941            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 942        }
 943
 944        // Fields that are present in the DB but are never actually used
 945        // dto.UnratedType = entity.UnratedType;
 946        // dto.TopParentId = entity.TopParentId;
 947        // dto.CleanName = entity.CleanName;
 948        // dto.UserDataKey = entity.UserDataKey;
 949
 74950        if (dto is Folder folder)
 951        {
 73952            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 953        }
 954
 74955        return dto;
 956    }
 957
 958    /// <summary>
 959    /// Maps a Entity to the DTO.
 960    /// </summary>
 961    /// <param name="dto">The entity.</param>
 962    /// <returns>The dto to map.</returns>
 963    public BaseItemEntity Map(BaseItemDto dto)
 964    {
 120965        var dtoType = dto.GetType();
 120966        var entity = new BaseItemEntity()
 120967        {
 120968            Type = dtoType.ToString(),
 120969            Id = dto.Id
 120970        };
 971
 120972        if (TypeRequiresDeserialization(dtoType))
 973        {
 99974            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 975        }
 976
 120977        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 120978        entity.Path = GetPathToSave(dto.Path);
 120979        entity.EndDate = dto.EndDate;
 120980        entity.CommunityRating = dto.CommunityRating;
 120981        entity.CustomRating = dto.CustomRating;
 120982        entity.IndexNumber = dto.IndexNumber;
 120983        entity.IsLocked = dto.IsLocked;
 120984        entity.Name = dto.Name;
 120985        entity.CleanName = GetCleanValue(dto.Name);
 120986        entity.OfficialRating = dto.OfficialRating;
 120987        entity.Overview = dto.Overview;
 120988        entity.ParentIndexNumber = dto.ParentIndexNumber;
 120989        entity.PremiereDate = dto.PremiereDate;
 120990        entity.ProductionYear = dto.ProductionYear;
 120991        entity.SortName = dto.SortName;
 120992        entity.ForcedSortName = dto.ForcedSortName;
 120993        entity.RunTimeTicks = dto.RunTimeTicks;
 120994        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 120995        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 120996        entity.IsInMixedFolder = dto.IsInMixedFolder;
 120997        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 120998        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 120999        entity.CriticRating = dto.CriticRating;
 1201000        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 1201001        entity.OriginalTitle = dto.OriginalTitle;
 1201002        entity.Album = dto.Album;
 1201003        entity.LUFS = dto.LUFS;
 1201004        entity.NormalizationGain = dto.NormalizationGain;
 1201005        entity.IsVirtualItem = dto.IsVirtualItem;
 1201006        entity.ExternalSeriesId = dto.ExternalSeriesId;
 1201007        entity.Tagline = dto.Tagline;
 1201008        entity.TotalBitrate = dto.TotalBitrate;
 1201009        entity.ExternalId = dto.ExternalId;
 1201010        entity.Size = dto.Size;
 1201011        entity.Genres = string.Join('|', dto.Genres);
 1201012        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 1201013        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 1201014        entity.ChannelId = dto.ChannelId;
 1201015        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 1201016        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 1201017        entity.OwnerId = dto.OwnerId.ToString();
 1201018        entity.Width = dto.Width;
 1201019        entity.Height = dto.Height;
 1201020        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 1201021        {
 1201022            Item = entity,
 1201023            ProviderId = e.Key,
 1201024            ProviderValue = e.Value
 1201025        }).ToList();
 1026
 1201027        if (dto.Audio.HasValue)
 1028        {
 01029            entity.Audio = (ProgramAudioEntity)dto.Audio;
 1030        }
 1031
 1201032        if (dto.ExtraType.HasValue)
 1033        {
 01034            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 1035        }
 1036
 1201037        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 1201038        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : n
 1201039        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
 1201040        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
 1201041        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 1201042            .Select(e => new BaseItemMetadataField()
 1201043            {
 1201044                Id = (int)e,
 1201045                Item = entity,
 1201046                ItemId = entity.Id
 1201047            })
 1201048            .ToArray() : null;
 1049
 1201050        if (dto is IHasProgramAttributes hasProgramAttributes)
 1051        {
 01052            entity.IsMovie = hasProgramAttributes.IsMovie;
 01053            entity.IsSeries = hasProgramAttributes.IsSeries;
 01054            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 01055            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 1056        }
 1057
 1201058        if (dto is LiveTvChannel liveTvChannel)
 1059        {
 01060            entity.ExternalServiceId = liveTvChannel.ServiceName;
 1061        }
 1062
 1201063        if (dto is Video video)
 1064        {
 01065            entity.PrimaryVersionId = video.PrimaryVersionId;
 1066        }
 1067
 1201068        if (dto is IHasSeries hasSeriesName)
 1069        {
 01070            entity.SeriesName = hasSeriesName.SeriesName;
 01071            entity.SeriesId = hasSeriesName.SeriesId;
 01072            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 1073        }
 1074
 1201075        if (dto is Episode episode)
 1076        {
 01077            entity.SeasonName = episode.SeasonName;
 01078            entity.SeasonId = episode.SeasonId;
 1079        }
 1080
 1201081        if (dto is IHasArtist hasArtists)
 1082        {
 01083            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
 1084        }
 1085
 1201086        if (dto is IHasAlbumArtist hasAlbumArtists)
 1087        {
 01088            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 1089        }
 1090
 1201091        if (dto is LiveTvProgram program)
 1092        {
 01093            entity.ShowId = program.ShowId;
 1094        }
 1095
 1201096        if (dto.ImageInfos is not null)
 1097        {
 1201098            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 1099        }
 1100
 1201101        if (dto is Trailer trailer)
 1102        {
 01103            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 01104            {
 01105                Id = (int)e,
 01106                Item = entity,
 01107                ItemId = entity.Id
 01108            }).ToArray() ?? [];
 1109        }
 1110
 1111        // dto.Type = entity.Type;
 1112        // dto.Data = entity.Data;
 1201113        entity.MediaType = dto.MediaType.ToString();
 1201114        if (dto is IHasStartDate hasStartDate)
 1115        {
 01116            entity.StartDate = hasStartDate.StartDate;
 1117        }
 1118
 1201119        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 1120
 1121        // Fields that are present in the DB but are never actually used
 1122        // dto.UserDataKey = entity.UserDataKey;
 1123
 1201124        if (dto is Folder folder)
 1125        {
 1201126            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 1201127            entity.IsFolder = folder.IsFolder;
 1128        }
 1129
 1201130        return entity;
 1131    }
 1132
 1133    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 1134    {
 681135        using var context = _dbProvider.CreateDbContext();
 1136
 681137        var query = context.ItemValuesMap
 681138            .AsNoTracking()
 681139            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 681140        if (withItemTypes.Count > 0)
 1141        {
 171142            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 1143        }
 1144
 681145        if (excludeItemTypes.Count > 0)
 1146        {
 171147            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 1148        }
 1149
 1150        // query = query.DistinctBy(e => e.CleanValue);
 681151        return query.Select(e => e.ItemValue)
 681152            .GroupBy(e => e.CleanValue)
 681153            .Select(e => e.First().Value)
 681154            .ToArray();
 681155    }
 1156
 1157    private static bool TypeRequiresDeserialization(Type type)
 1158    {
 1941159        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 1160    }
 1161
 1162    private BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 1163    {
 731164        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 731165        if (_serverConfigurationManager?.Configuration is null)
 1166        {
 01167            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 1168        }
 1169
 731170        var typeToSerialise = GetType(baseItemEntity.Type);
 731171        return BaseItemRepository.DeserializeBaseItem(
 731172            baseItemEntity,
 731173            _logger,
 731174            _appHost,
 731175            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 1176    }
 1177
 1178    /// <summary>
 1179    /// Deserializes a BaseItemEntity and sets all properties.
 1180    /// </summary>
 1181    /// <param name="baseItemEntity">The DB entity.</param>
 1182    /// <param name="logger">Logger.</param>
 1183    /// <param name="appHost">The application server Host.</param>
 1184    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 1185    /// <returns>A mapped BaseItem, or null if the item type is unknown.</returns>
 1186    public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost
 1187    {
 761188        var type = GetType(baseItemEntity.Type);
 761189        if (type is null)
 1190        {
 21191            logger.LogWarning(
 21192                "Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database c
 21193                baseItemEntity.Id,
 21194                baseItemEntity.Type);
 21195            return null;
 1196        }
 1197
 741198        BaseItemDto? dto = null;
 741199        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1200        {
 1201            try
 1202            {
 121203                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 121204            }
 01205            catch (JsonException ex)
 1206            {
 01207                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01208            }
 1209        }
 1210
 741211        if (dto is null)
 1212        {
 621213            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1214        }
 1215
 741216        return Map(baseItemEntity, dto, appHost, logger);
 1217    }
 1218
 1219    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 1220    {
 01221        ArgumentNullException.ThrowIfNull(filter);
 1222
 01223        if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
 1224        {
 01225            filter.EnableTotalRecordCount = false;
 1226        }
 1227
 01228        using var context = _dbProvider.CreateDbContext();
 1229
 01230        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 01231        {
 01232            ExcludeItemTypes = filter.ExcludeItemTypes,
 01233            IncludeItemTypes = filter.IncludeItemTypes,
 01234            MediaTypes = filter.MediaTypes,
 01235            AncestorIds = filter.AncestorIds,
 01236            ItemIds = filter.ItemIds,
 01237            TopParentIds = filter.TopParentIds,
 01238            ParentId = filter.ParentId,
 01239            IsAiring = filter.IsAiring,
 01240            IsMovie = filter.IsMovie,
 01241            IsSports = filter.IsSports,
 01242            IsKids = filter.IsKids,
 01243            IsNews = filter.IsNews,
 01244            IsSeries = filter.IsSeries
 01245        });
 1246
 01247        var itemValuesQuery = context.ItemValues
 01248            .Where(f => itemValueTypes.Contains(f.Type))
 01249            .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
 01250            .Join(
 01251                innerQueryFilter,
 01252                fw => fw.w.ItemId,
 01253                g => g.Id,
 01254                (fw, g) => fw.f.CleanValue);
 1255
 01256        var innerQuery = PrepareItemQuery(context, filter)
 01257            .Where(e => e.Type == returnType)
 01258            .Where(e => itemValuesQuery.Contains(e.CleanName));
 1259
 01260        var outerQueryFilter = new InternalItemsQuery(filter.User)
 01261        {
 01262            IsPlayed = filter.IsPlayed,
 01263            IsFavorite = filter.IsFavorite,
 01264            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 01265            IsLiked = filter.IsLiked,
 01266            IsLocked = filter.IsLocked,
 01267            NameLessThan = filter.NameLessThan,
 01268            NameStartsWith = filter.NameStartsWith,
 01269            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 01270            Tags = filter.Tags,
 01271            OfficialRatings = filter.OfficialRatings,
 01272            StudioIds = filter.StudioIds,
 01273            GenreIds = filter.GenreIds,
 01274            Genres = filter.Genres,
 01275            Years = filter.Years,
 01276            NameContains = filter.NameContains,
 01277            SearchTerm = filter.SearchTerm,
 01278            ExcludeItemIds = filter.ExcludeItemIds
 01279        };
 1280
 01281        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
 01282            .GroupBy(e => e.PresentationUniqueKey)
 01283            .Select(e => e.FirstOrDefault())
 01284            .Select(e => e!.Id);
 1285
 01286        var query = context.BaseItems
 01287            .Include(e => e.TrailerTypes)
 01288            .Include(e => e.Provider)
 01289            .Include(e => e.LockedFields)
 01290            .Include(e => e.Images)
 01291            .AsSingleQuery()
 01292            .Where(e => masterQuery.Contains(e.Id));
 1293
 01294        query = ApplyOrder(query, filter, context);
 1295
 01296        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 01297        if (filter.EnableTotalRecordCount)
 1298        {
 01299            result.TotalRecordCount = query.Count();
 1300        }
 1301
 01302        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 1303        {
 01304            query = query.Skip(filter.StartIndex.Value);
 1305        }
 1306
 01307        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 1308        {
 01309            query = query.Take(filter.Limit.Value);
 1310        }
 1311
 01312        IQueryable<BaseItemEntity>? itemCountQuery = null;
 1313
 01314        if (filter.IncludeItemTypes.Length > 0)
 1315        {
 1316            // if we are to include more then one type, sub query those items beforehand.
 1317
 01318            var typeSubQuery = new InternalItemsQuery(filter.User)
 01319            {
 01320                ExcludeItemTypes = filter.ExcludeItemTypes,
 01321                IncludeItemTypes = filter.IncludeItemTypes,
 01322                MediaTypes = filter.MediaTypes,
 01323                AncestorIds = filter.AncestorIds,
 01324                ExcludeItemIds = filter.ExcludeItemIds,
 01325                ItemIds = filter.ItemIds,
 01326                TopParentIds = filter.TopParentIds,
 01327                ParentId = filter.ParentId,
 01328                IsPlayed = filter.IsPlayed
 01329            };
 1330
 01331            itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 01332                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 1333
 01334            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 01335            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 01336            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 01337            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 01338            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 01339            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 01340            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 1341
 01342            var resultQuery = query.Select(e => new
 01343            {
 01344                item = e,
 01345                // TODO: This is bad refactor!
 01346                itemCount = new ItemCounts()
 01347                {
 01348                    SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
 01349                    EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
 01350                    MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
 01351                    AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
 01352                    ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
 01353                    SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
 01354                    TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
 01355                }
 01356            });
 1357
 01358            result.StartIndex = filter.StartIndex ?? 0;
 01359            result.Items =
 01360            [
 01361                .. resultQuery
 01362                    .AsEnumerable()
 01363                    .Where(e => e is not null)
 01364                    .Select(e => (Item: DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount))
 01365                    .Where(e => e.Item is not null)
 01366                    .Select(e => (e.Item!, e.itemCount))
 01367            ];
 1368        }
 1369        else
 1370        {
 01371            result.StartIndex = filter.StartIndex ?? 0;
 01372            result.Items =
 01373            [
 01374                .. query
 01375                    .AsEnumerable()
 01376                    .Where(e => e is not null)
 01377                    .Select(e => (Item: DeserializeBaseItem(e, filter.SkipDeserialization), ItemCounts: (ItemCounts?)nul
 01378                    .Where(e => e.Item is not null)
 01379                    .Select(e => (e.Item!, e.ItemCounts))
 01380            ];
 1381        }
 1382
 01383        return result;
 01384    }
 1385
 1386    private static void PrepareFilterQuery(InternalItemsQuery query)
 1387    {
 3471388        if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
 1389        {
 01390            query.Limit = query.Limit.Value + 4;
 1391        }
 1392
 3471393        if (query.IsResumable ?? false)
 1394        {
 11395            query.IsVirtualItem = false;
 1396        }
 3471397    }
 1398
 1399    /// <summary>
 1400    /// Gets the clean value for search and sorting purposes.
 1401    /// </summary>
 1402    /// <param name="value">The value to clean.</param>
 1403    /// <returns>The cleaned value.</returns>
 1404    public static string GetCleanValue(string value)
 1405    {
 1231406        if (string.IsNullOrWhiteSpace(value))
 1407        {
 01408            return value;
 1409        }
 1410
 1231411        var noDiacritics = value.RemoveDiacritics();
 1412
 1413        // Build a string where any punctuation or symbol is treated as a separator (space).
 1231414        var sb = new StringBuilder(noDiacritics.Length);
 1231415        var previousWasSpace = false;
 20721416        foreach (var ch in noDiacritics)
 1417        {
 1418            char outCh;
 9131419            if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
 1420            {
 8911421                outCh = ch;
 1422            }
 1423            else
 1424            {
 221425                outCh = ' ';
 1426            }
 1427
 1428            // normalize any whitespace character to a single ASCII space.
 9131429            if (char.IsWhiteSpace(outCh))
 1430            {
 511431                if (!previousWasSpace)
 1432                {
 401433                    sb.Append(' ');
 401434                    previousWasSpace = true;
 1435                }
 1436            }
 1437            else
 1438            {
 8621439                sb.Append(outCh);
 8621440                previousWasSpace = false;
 1441            }
 1442        }
 1443
 1444        // trim leading/trailing spaces that may have been added.
 1231445        var collapsed = sb.ToString().Trim();
 1231446        return collapsed.ToLowerInvariant();
 1447    }
 1448
 1449    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1450    {
 1101451        var list = new List<(ItemValueType, string)>();
 1452
 1101453        if (item is IHasArtist hasArtist)
 1454        {
 01455            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1456        }
 1457
 1101458        if (item is IHasAlbumArtist hasAlbumArtist)
 1459        {
 01460            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1461        }
 1462
 1101463        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 1101464        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 1101465        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1466
 1467        // keywords was 5
 1468
 1101469        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1470
 1471        // Remove all invalid values.
 1101472        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1473
 1101474        return list;
 1475    }
 1476
 1477    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1478    {
 01479        return new BaseItemImageInfo()
 01480        {
 01481            ItemId = baseItemId,
 01482            Id = Guid.NewGuid(),
 01483            Path = e.Path,
 01484            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 01485            DateModified = e.DateModified,
 01486            Height = e.Height,
 01487            Width = e.Width,
 01488            ImageType = (ImageInfoImageType)e.Type,
 01489            Item = null!
 01490        };
 1491    }
 1492
 1493    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 1494    {
 01495        return new ItemImageInfo()
 01496        {
 01497            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 01498            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 01499            DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
 01500            Height = e.Height,
 01501            Width = e.Width,
 01502            Type = (ImageType)e.ImageType
 01503        };
 1504    }
 1505
 1506    private string? GetPathToSave(string path)
 1507    {
 1201508        if (path is null)
 1509        {
 101510            return null;
 1511        }
 1512
 1101513        return _appHost.ReverseVirtualPath(path);
 1514    }
 1515
 1516    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1517    {
 141518        var list = new List<string>();
 1519
 141520        if (IsTypeInQuery(BaseItemKind.Person, query))
 1521        {
 11522            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1523        }
 1524
 141525        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1526        {
 11527            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1528        }
 1529
 141530        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1531        {
 11532            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1533        }
 1534
 141535        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1536        {
 11537            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1538        }
 1539
 141540        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1541        {
 11542            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1543        }
 1544
 141545        return list;
 1546    }
 1547
 1548    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1549    {
 701550        if (query.ExcludeItemTypes.Contains(type))
 1551        {
 01552            return false;
 1553        }
 1554
 701555        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 1556    }
 1557
 1558    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 1559    {
 3471560        if (!query.GroupByPresentationUniqueKey)
 1561        {
 1461562            return false;
 1563        }
 1564
 2011565        if (query.GroupBySeriesPresentationUniqueKey)
 1566        {
 01567            return false;
 1568        }
 1569
 2011570        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 1571        {
 01572            return false;
 1573        }
 1574
 2011575        if (query.User is null)
 1576        {
 1991577            return false;
 1578        }
 1579
 21580        if (query.IncludeItemTypes.Length == 0)
 1581        {
 11582            return true;
 1583        }
 1584
 11585        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 11586            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 11587            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 11588            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 11589            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 11590            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 1591    }
 1592
 1593    private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinD
 1594    {
 3471595        var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
 3471596        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1597
 3471598        if (hasSearch)
 1599        {
 01600            orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1601        }
 3471602        else if (orderBy.Length == 0)
 1603        {
 2391604            return query.OrderBy(e => e.SortName);
 1605        }
 1606
 1081607        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1608
 1081609        var firstOrdering = orderBy.FirstOrDefault();
 1081610        if (firstOrdering != default)
 1611        {
 1081612            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 1081613            if (firstOrdering.SortOrder == SortOrder.Ascending)
 1614            {
 1071615                orderedQuery = query.OrderBy(expression);
 1616            }
 1617            else
 1618            {
 11619                orderedQuery = query.OrderByDescending(expression);
 1620            }
 1621
 1081622            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 1623            {
 01624                if (firstOrdering.SortOrder is SortOrder.Ascending)
 1625                {
 01626                    orderedQuery = orderedQuery.ThenBy(e => e.Name);
 1627                }
 1628                else
 1629                {
 01630                    orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
 1631                }
 1632            }
 1633        }
 1634
 3021635        foreach (var item in orderBy.Skip(1))
 1636        {
 431637            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 431638            if (item.SortOrder == SortOrder.Ascending)
 1639            {
 431640                orderedQuery = orderedQuery!.ThenBy(expression);
 1641            }
 1642            else
 1643            {
 01644                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1645            }
 1646        }
 1647
 1081648        return orderedQuery ?? query;
 1649    }
 1650
 1651    private IQueryable<BaseItemEntity> TranslateQuery(
 1652        IQueryable<BaseItemEntity> baseQuery,
 1653        JellyfinDbContext context,
 1654        InternalItemsQuery filter)
 1655    {
 1656        const int HDWidth = 1200;
 1657        const int UHDWidth = 3800;
 1658        const int UHDHeight = 2100;
 1659
 3471660        var minWidth = filter.MinWidth;
 3471661        var maxWidth = filter.MaxWidth;
 3471662        var now = DateTime.UtcNow;
 1663
 3471664        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 1665        {
 01666            bool includeSD = false;
 01667            bool includeHD = false;
 01668            bool include4K = false;
 1669
 01670            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 1671            {
 01672                includeSD = true;
 1673            }
 1674
 01675            if (filter.IsHD.HasValue && filter.IsHD.Value)
 1676            {
 01677                includeHD = true;
 1678            }
 1679
 01680            if (filter.Is4K.HasValue && filter.Is4K.Value)
 1681            {
 01682                include4K = true;
 1683            }
 1684
 01685            baseQuery = baseQuery.Where(e =>
 01686                (includeSD && e.Width < HDWidth) ||
 01687                (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
 01688                (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
 1689        }
 1690
 3471691        if (minWidth.HasValue)
 1692        {
 01693            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1694        }
 1695
 3471696        if (filter.MinHeight.HasValue)
 1697        {
 01698            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1699        }
 1700
 3471701        if (maxWidth.HasValue)
 1702        {
 01703            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1704        }
 1705
 3471706        if (filter.MaxHeight.HasValue)
 1707        {
 01708            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1709        }
 1710
 3471711        if (filter.IsLocked.HasValue)
 1712        {
 511713            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1714        }
 1715
 3471716        var tags = filter.Tags.ToList();
 3471717        var excludeTags = filter.ExcludeTags.ToList();
 1718
 3471719        if (filter.IsMovie.HasValue)
 1720        {
 01721            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 01722                && (filter.IncludeItemTypes.Length == 0
 01723                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01724                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 1725
 01726            if (!shouldIncludeAllMovieTypes)
 1727            {
 01728                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 1729            }
 1730        }
 1731
 3471732        if (filter.IsSeries.HasValue)
 1733        {
 01734            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1735        }
 1736
 3471737        if (filter.IsSports.HasValue)
 1738        {
 01739            if (filter.IsSports.Value)
 1740            {
 01741                tags.Add("Sports");
 1742            }
 1743            else
 1744            {
 01745                excludeTags.Add("Sports");
 1746            }
 1747        }
 1748
 3471749        if (filter.IsNews.HasValue)
 1750        {
 01751            if (filter.IsNews.Value)
 1752            {
 01753                tags.Add("News");
 1754            }
 1755            else
 1756            {
 01757                excludeTags.Add("News");
 1758            }
 1759        }
 1760
 3471761        if (filter.IsKids.HasValue)
 1762        {
 01763            if (filter.IsKids.Value)
 1764            {
 01765                tags.Add("Kids");
 1766            }
 1767            else
 1768            {
 01769                excludeTags.Add("Kids");
 1770            }
 1771        }
 1772
 3471773        if (!string.IsNullOrEmpty(filter.SearchTerm))
 1774        {
 01775            var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
 01776            var originalSearchTerm = filter.SearchTerm.ToLower();
 01777            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 1778            {
 01779                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 01780                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 1781            }
 1782            else
 1783            {
 01784                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 1785            }
 1786        }
 1787
 3471788        if (filter.IsFolder.HasValue)
 1789        {
 211790            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1791        }
 1792
 3471793        var includeTypes = filter.IncludeItemTypes;
 1794
 1795        // Only specify excluded types if no included types are specified
 3471796        if (filter.IncludeItemTypes.Length == 0)
 1797        {
 2281798            var excludeTypes = filter.ExcludeItemTypes;
 2281799            if (excludeTypes.Length == 1)
 1800            {
 01801                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 1802                {
 01803                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 1804                }
 1805            }
 2281806            else if (excludeTypes.Length > 1)
 1807            {
 01808                var excludeTypeName = new List<string>();
 01809                foreach (var excludeType in excludeTypes)
 1810                {
 01811                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 1812                    {
 01813                        excludeTypeName.Add(baseItemKindName!);
 1814                    }
 1815                }
 1816
 01817                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 1818            }
 1819        }
 1820        else
 1821        {
 1191822            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 1191823            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1824        }
 1825
 3471826        if (filter.ChannelIds.Count > 0)
 1827        {
 01828            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1829        }
 1830
 3471831        if (!filter.ParentId.IsEmpty())
 1832        {
 1461833            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1834        }
 1835
 3471836        if (!string.IsNullOrWhiteSpace(filter.Path))
 1837        {
 01838            var pathToQuery = GetPathToSave(filter.Path);
 01839            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 1840        }
 1841
 3471842        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1843        {
 01844            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1845        }
 1846
 3471847        if (filter.MinCommunityRating.HasValue)
 1848        {
 01849            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1850        }
 1851
 3471852        if (filter.MinIndexNumber.HasValue)
 1853        {
 01854            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1855        }
 1856
 3471857        if (filter.MinParentAndIndexNumber.HasValue)
 1858        {
 01859            baseQuery = baseQuery
 01860                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1861        }
 1862
 3471863        if (filter.MinDateCreated.HasValue)
 1864        {
 01865            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1866        }
 1867
 3471868        if (filter.MinDateLastSaved.HasValue)
 1869        {
 01870            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1871        }
 1872
 3471873        if (filter.MinDateLastSavedForUser.HasValue)
 1874        {
 01875            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1876        }
 1877
 3471878        if (filter.IndexNumber.HasValue)
 1879        {
 01880            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1881        }
 1882
 3471883        if (filter.ParentIndexNumber.HasValue)
 1884        {
 01885            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1886        }
 1887
 3471888        if (filter.ParentIndexNumberNotEquals.HasValue)
 1889        {
 01890            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1891        }
 1892
 3471893        var minEndDate = filter.MinEndDate;
 3471894        var maxEndDate = filter.MaxEndDate;
 1895
 3471896        if (filter.HasAired.HasValue)
 1897        {
 01898            if (filter.HasAired.Value)
 1899            {
 01900                maxEndDate = DateTime.UtcNow;
 1901            }
 1902            else
 1903            {
 01904                minEndDate = DateTime.UtcNow;
 1905            }
 1906        }
 1907
 3471908        if (minEndDate.HasValue)
 1909        {
 01910            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1911        }
 1912
 3471913        if (maxEndDate.HasValue)
 1914        {
 01915            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1916        }
 1917
 3471918        if (filter.MinStartDate.HasValue)
 1919        {
 01920            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1921        }
 1922
 3471923        if (filter.MaxStartDate.HasValue)
 1924        {
 01925            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1926        }
 1927
 3471928        if (filter.MinPremiereDate.HasValue)
 1929        {
 01930            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1931        }
 1932
 3471933        if (filter.MaxPremiereDate.HasValue)
 1934        {
 01935            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1936        }
 1937
 3471938        if (filter.TrailerTypes.Length > 0)
 1939        {
 01940            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 01941            baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
 1942        }
 1943
 3471944        if (filter.IsAiring.HasValue)
 1945        {
 01946            if (filter.IsAiring.Value)
 1947            {
 01948                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 1949            }
 1950            else
 1951            {
 01952                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 1953            }
 1954        }
 1955
 3471956        if (filter.PersonIds.Length > 0)
 1957        {
 01958            var peopleEntityIds = context.BaseItems
 01959                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 01960                .Join(
 01961                    context.Peoples,
 01962                    b => b.Name,
 01963                    p => p.Name,
 01964                    (b, p) => p.Id);
 1965
 01966            baseQuery = baseQuery
 01967                .Where(e => context.PeopleBaseItemMap
 01968                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 1969        }
 1970
 3471971        if (!string.IsNullOrWhiteSpace(filter.Person))
 1972        {
 01973            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 1974        }
 1975
 3471976        if (!string.IsNullOrWhiteSpace(filter.MinSortName))
 1977        {
 1978            // this does not makes sense.
 1979            // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
 1980            // whereClauses.Add("SortName>=@MinSortName");
 1981            // statement?.TryBind("@MinSortName", query.MinSortName);
 1982        }
 1983
 3471984        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 1985        {
 01986            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 1987        }
 1988
 3471989        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 1990        {
 01991            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 1992        }
 1993
 3471994        if (!string.IsNullOrWhiteSpace(filter.Name))
 1995        {
 31996            if (filter.UseRawName == true)
 1997            {
 01998                baseQuery = baseQuery.Where(e => e.Name == filter.Name);
 1999            }
 2000            else
 2001            {
 32002                var cleanName = GetCleanValue(filter.Name);
 32003                baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 2004            }
 2005        }
 2006
 2007        // These are the same, for now
 3472008        var nameContains = filter.NameContains;
 3472009        if (!string.IsNullOrWhiteSpace(nameContains))
 2010        {
 02011            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 2012            {
 02013                nameContains = $"%{nameContains.Trim('%')}%";
 02014                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 2015            }
 2016            else
 2017            {
 02018                baseQuery = baseQuery.Where(e =>
 02019                                    e.CleanName!.Contains(nameContains)
 02020                                    || e.OriginalTitle!.ToLower().Contains(nameContains!));
 2021            }
 2022        }
 2023
 3472024        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 2025        {
 02026            var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
 02027            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
 2028        }
 2029
 3472030        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 2031        {
 02032            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 02033            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
 2034        }
 2035
 3472036        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 2037        {
 02038            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 02039            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
 2040        }
 2041
 3472042        if (filter.ImageTypes.Length > 0)
 2043        {
 1072044            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 1072045            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 2046        }
 2047
 3472048        if (filter.IsLiked.HasValue)
 2049        {
 02050            baseQuery = baseQuery
 02051                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLike
 2052        }
 2053
 3472054        if (filter.IsFavoriteOrLiked.HasValue)
 2055        {
 02056            baseQuery = baseQuery
 02057                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2058        }
 2059
 3472060        if (filter.IsFavorite.HasValue)
 2061        {
 02062            baseQuery = baseQuery
 02063                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2064        }
 2065
 3472066        if (filter.IsPlayed.HasValue)
 2067        {
 2068            // We should probably figure this out for all folders, but for right now, this is the only place where we ne
 02069            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
 2070            {
 02071                baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
 02072                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
 02073                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
 02074                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
 2075            }
 2076            else
 2077            {
 02078                baseQuery = baseQuery
 02079                    .Select(e => new
 02080                    {
 02081                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).Firs
 02082                        Item = e
 02083                    })
 02084                    .Where(e => e.IsPlayed == filter.IsPlayed)
 02085                    .Select(f => f.Item);
 2086            }
 2087        }
 2088
 3472089        if (filter.IsResumable.HasValue)
 2090        {
 12091            if (filter.IsResumable.Value)
 2092            {
 12093                baseQuery = baseQuery
 12094                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks >
 2095            }
 2096            else
 2097            {
 02098                baseQuery = baseQuery
 02099                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks =
 2100            }
 2101        }
 2102
 3472103        if (filter.ArtistIds.Length > 0)
 2104        {
 02105            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2106        }
 2107
 3472108        if (filter.AlbumArtistIds.Length > 0)
 2109        {
 02110            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 2111        }
 2112
 3472113        if (filter.ContributingArtistIds.Length > 0)
 2114        {
 02115            var contributingNames = context.BaseItems
 02116                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 02117                .Select(b => b.CleanName);
 2118
 02119            baseQuery = baseQuery.Where(e =>
 02120                e.ItemValues!.Any(ivm =>
 02121                    ivm.ItemValue.Type == ItemValueType.Artist &&
 02122                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 02123                &&
 02124                !e.ItemValues!.Any(ivm =>
 02125                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 02126                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 2127        }
 2128
 3472129        if (filter.AlbumIds.Length > 0)
 2130        {
 02131            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 02132            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 2133        }
 2134
 3472135        if (filter.ExcludeArtistIds.Length > 0)
 2136        {
 02137            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2138        }
 2139
 3472140        if (filter.GenreIds.Count > 0)
 2141        {
 02142            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 2143        }
 2144
 3472145        if (filter.Genres.Count > 0)
 2146        {
 02147            var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValue
 02148            baseQuery = baseQuery
 02149                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 2150        }
 2151
 3472152        if (tags.Count > 0)
 2153        {
 02154            var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, stri
 02155            baseQuery = baseQuery
 02156                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 2157        }
 2158
 3472159        if (excludeTags.Count > 0)
 2160        {
 02161            var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMa
 02162            baseQuery = baseQuery
 02163                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 2164        }
 2165
 3472166        if (filter.StudioIds.Length > 0)
 2167        {
 02168            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 2169        }
 2170
 3472171        if (filter.OfficialRatings.Length > 0)
 2172        {
 02173            baseQuery = baseQuery
 02174                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 2175        }
 2176
 3472177        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 3472178        if (filter.MinParentalRating != null)
 2179        {
 02180            var min = filter.MinParentalRating;
 02181            var minScore = min.Score;
 02182            var minSubScore = min.SubScore ?? 0;
 2183
 02184            minParentalRatingFilter = e =>
 02185                e.InheritedParentalRatingValue == null ||
 02186                e.InheritedParentalRatingValue > minScore ||
 02187                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 2188        }
 2189
 3472190        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 3472191        if (filter.MaxParentalRating != null)
 2192        {
 512193            var max = filter.MaxParentalRating;
 512194            var maxScore = max.Score;
 512195            var maxSubScore = max.SubScore ?? 0;
 2196
 512197            maxParentalRatingFilter = e =>
 512198                e.InheritedParentalRatingValue == null ||
 512199                e.InheritedParentalRatingValue < maxScore ||
 512200                (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
 2201        }
 2202
 3472203        if (filter.HasParentalRating ?? false)
 2204        {
 02205            if (minParentalRatingFilter != null)
 2206            {
 02207                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2208            }
 2209
 02210            if (maxParentalRatingFilter != null)
 2211            {
 02212                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2213            }
 2214        }
 3472215        else if (filter.BlockUnratedItems.Length > 0)
 2216        {
 02217            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 02218            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 2219
 02220            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 2221            {
 02222                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 2223            }
 02224            else if (minParentalRatingFilter != null)
 2225            {
 02226                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 2227            }
 02228            else if (maxParentalRatingFilter != null)
 2229            {
 02230                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 2231            }
 2232            else
 2233            {
 02234                baseQuery = baseQuery.Where(unratedItemFilter);
 2235            }
 2236        }
 3472237        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 2238        {
 512239            if (minParentalRatingFilter != null)
 2240            {
 02241                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2242            }
 2243
 512244            if (maxParentalRatingFilter != null)
 2245            {
 512246                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2247            }
 2248        }
 2962249        else if (!filter.HasParentalRating ?? false)
 2250        {
 02251            baseQuery = baseQuery
 02252                .Where(e => e.InheritedParentalRatingValue == null);
 2253        }
 2254
 3472255        if (filter.HasOfficialRating.HasValue)
 2256        {
 02257            if (filter.HasOfficialRating.Value)
 2258            {
 02259                baseQuery = baseQuery
 02260                    .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
 2261            }
 2262            else
 2263            {
 02264                baseQuery = baseQuery
 02265                    .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
 2266            }
 2267        }
 2268
 3472269        if (filter.HasOverview.HasValue)
 2270        {
 02271            if (filter.HasOverview.Value)
 2272            {
 02273                baseQuery = baseQuery
 02274                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 2275            }
 2276            else
 2277            {
 02278                baseQuery = baseQuery
 02279                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 2280            }
 2281        }
 2282
 3472283        if (filter.HasOwnerId.HasValue)
 2284        {
 02285            if (filter.HasOwnerId.Value)
 2286            {
 02287                baseQuery = baseQuery
 02288                    .Where(e => e.OwnerId != null);
 2289            }
 2290            else
 2291            {
 02292                baseQuery = baseQuery
 02293                    .Where(e => e.OwnerId == null);
 2294            }
 2295        }
 2296
 3472297        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 2298        {
 02299            baseQuery = baseQuery
 02300                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filte
 2301        }
 2302
 3472303        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2304        {
 02305            baseQuery = baseQuery
 02306                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2307        }
 2308
 3472309        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2310        {
 02311            baseQuery = baseQuery
 02312                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2313        }
 2314
 3472315        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 2316        {
 02317            baseQuery = baseQuery
 02318                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == fi
 2319        }
 2320
 3472321        if (filter.HasSubtitles.HasValue)
 2322        {
 02323            baseQuery = baseQuery
 02324                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2325        }
 2326
 3472327        if (filter.HasChapterImages.HasValue)
 2328        {
 02329            baseQuery = baseQuery
 02330                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2331        }
 2332
 3472333        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2334        {
 172335            baseQuery = baseQuery
 172336                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 2337        }
 2338
 3472339        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2340        {
 172341            baseQuery = baseQuery
 172342                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2343        }
 2344
 3472345        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2346        {
 172347            baseQuery = baseQuery
 172348                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2349        }
 2350
 3472351        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2352        {
 172353            baseQuery = baseQuery
 172354                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2355        }
 2356
 3472357        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2358        {
 02359            baseQuery = baseQuery
 02360                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2361        }
 2362
 3472363        if (filter.Years.Length > 0)
 2364        {
 02365            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2366        }
 2367
 3472368        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 3472369        if (isVirtualItem.HasValue)
 2370        {
 222371            baseQuery = baseQuery
 222372                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2373        }
 2374
 3472375        if (filter.IsSpecialSeason.HasValue)
 2376        {
 02377            if (filter.IsSpecialSeason.Value)
 2378            {
 02379                baseQuery = baseQuery
 02380                    .Where(e => e.IndexNumber == 0);
 2381            }
 2382            else
 2383            {
 02384                baseQuery = baseQuery
 02385                    .Where(e => e.IndexNumber != 0);
 2386            }
 2387        }
 2388
 3472389        if (filter.IsUnaired.HasValue)
 2390        {
 02391            if (filter.IsUnaired.Value)
 2392            {
 02393                baseQuery = baseQuery
 02394                    .Where(e => e.PremiereDate >= now);
 2395            }
 2396            else
 2397            {
 02398                baseQuery = baseQuery
 02399                    .Where(e => e.PremiereDate < now);
 2400            }
 2401        }
 2402
 3472403        if (filter.MediaTypes.Length > 0)
 2404        {
 212405            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 212406            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 2407        }
 2408
 3472409        if (filter.ItemIds.Length > 0)
 2410        {
 02411            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2412        }
 2413
 3472414        if (filter.ExcludeItemIds.Length > 0)
 2415        {
 02416            baseQuery = baseQuery
 02417                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 2418        }
 2419
 3472420        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 2421        {
 02422            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 02423            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 2424        }
 2425
 3472426        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 2427        {
 2428            // Allow setting a null or empty value to get all items that have the specified provider set.
 02429            var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArra
 02430            if (includeAny.Length > 0)
 2431            {
 02432                baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
 2433            }
 2434
 02435            var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Ke
 02436            if (includeSelected.Length > 0)
 2437            {
 02438                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 2439            }
 2440        }
 2441
 3472442        if (filter.HasImdbId.HasValue)
 2443        {
 02444            baseQuery = filter.HasImdbId.Value
 02445                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().T
 02446                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().T
 2447        }
 2448
 3472449        if (filter.HasTmdbId.HasValue)
 2450        {
 02451            baseQuery = filter.HasTmdbId.Value
 02452                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().T
 02453                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().T
 2454        }
 2455
 3472456        if (filter.HasTvdbId.HasValue)
 2457        {
 02458            baseQuery = filter.HasTvdbId.Value
 02459                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().T
 02460                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().T
 2461        }
 2462
 3472463        var queryTopParentIds = filter.TopParentIds;
 2464
 3472465        if (queryTopParentIds.Length > 0)
 2466        {
 142467            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 142468            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 142469            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2470            {
 02471                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2472            }
 2473            else
 2474            {
 142475                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2476            }
 2477        }
 2478
 3472479        if (filter.AncestorIds.Length > 0)
 2480        {
 442481            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2482        }
 2483
 3472484        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 2485        {
 02486            baseQuery = baseQuery
 02487                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 2488        }
 2489
 3472490        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2491        {
 02492            baseQuery = baseQuery
 02493                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2494        }
 2495
 3472496        if (filter.ExcludeInheritedTags.Length > 0)
 2497        {
 02498            var excludedTags = filter.ExcludeInheritedTags;
 02499            baseQuery = baseQuery.Where(e =>
 02500                !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.Clea
 02501                && (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.
 2502        }
 2503
 3472504        if (filter.IncludeInheritedTags.Length > 0)
 2505        {
 02506            var includeTags = filter.IncludeInheritedTags;
 02507            var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist
 02508            baseQuery = baseQuery.Where(e =>
 02509                e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanV
 02510
 02511                // For seasons and episodes, we also need to check the parent series' tags.
 02512                || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Ty
 02513
 02514                // A playlist should be accessible to its owner regardless of allowed tags
 02515                || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
 2516        }
 2517
 3472518        if (filter.SeriesStatuses.Length > 0)
 2519        {
 02520            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 02521            baseQuery = baseQuery
 02522                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 2523        }
 2524
 3472525        if (filter.BoxSetLibraryFolders.Length > 0)
 2526        {
 02527            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 02528            baseQuery = baseQuery
 02529                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 2530        }
 2531
 3472532        if (filter.VideoTypes.Length > 0)
 2533        {
 02534            var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
 02535            baseQuery = baseQuery
 02536                .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
 2537        }
 2538
 3472539        if (filter.Is3D.HasValue)
 2540        {
 02541            if (filter.Is3D.Value)
 2542            {
 02543                baseQuery = baseQuery
 02544                    .Where(e => e.Data!.Contains("Video3DFormat"));
 2545            }
 2546            else
 2547            {
 02548                baseQuery = baseQuery
 02549                    .Where(e => !e.Data!.Contains("Video3DFormat"));
 2550            }
 2551        }
 2552
 3472553        if (filter.IsPlaceHolder.HasValue)
 2554        {
 02555            if (filter.IsPlaceHolder.Value)
 2556            {
 02557                baseQuery = baseQuery
 02558                    .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
 2559            }
 2560            else
 2561            {
 02562                baseQuery = baseQuery
 02563                    .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
 2564            }
 2565        }
 2566
 3472567        if (filter.HasSpecialFeature.HasValue)
 2568        {
 02569            if (filter.HasSpecialFeature.Value)
 2570            {
 02571                baseQuery = baseQuery
 02572                    .Where(e => e.ExtraIds != null);
 2573            }
 2574            else
 2575            {
 02576                baseQuery = baseQuery
 02577                    .Where(e => e.ExtraIds == null);
 2578            }
 2579        }
 2580
 3472581        if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
 2582        {
 02583            if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo
 2584            {
 02585                baseQuery = baseQuery
 02586                    .Where(e => e.ExtraIds != null);
 2587            }
 2588            else
 2589            {
 02590                baseQuery = baseQuery
 02591                    .Where(e => e.ExtraIds == null);
 2592            }
 2593        }
 2594
 3472595        return baseQuery;
 2596    }
 2597
 2598    /// <inheritdoc/>
 2599    public async Task<bool> ItemExistsAsync(Guid id)
 2600    {
 2601        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 2602        await using (dbContext.ConfigureAwait(false))
 2603        {
 2604            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 2605        }
 2606    }
 2607
 2608    /// <inheritdoc/>
 2609    public bool GetIsPlayed(User user, Guid id, bool recursive)
 2610    {
 02611        using var dbContext = _dbProvider.CreateDbContext();
 2612
 02613        if (recursive)
 2614        {
 02615            var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
 2616
 02617            return dbContext.BaseItems
 02618                    .Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
 02619                    .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
 2620        }
 2621
 02622        return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.P
 02623    }
 2624
 2625    private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseIte
 2626    {
 22627        var folderStack = new HashSet<Guid>()
 22628            {
 22629                parentId
 22630            };
 22631        var folderList = new HashSet<Guid>()
 22632            {
 22633                parentId
 22634            };
 2635
 42636        while (folderStack.Count != 0)
 2637        {
 22638            var items = folderStack.ToArray();
 22639            folderStack.Clear();
 22640            var query = dbContext.BaseItems
 22641                .WhereOneOrMany(items, e => e.ParentId!.Value);
 2642
 22643            if (filter != null)
 2644            {
 02645                query = query.Where(filter);
 2646            }
 2647
 42648            foreach (var item in query.Select(e => e.Id).ToArray())
 2649            {
 02650                if (folderList.Add(item))
 2651                {
 02652                    folderStack.Add(item);
 2653                }
 2654            }
 2655        }
 2656
 22657        return folderList;
 2658    }
 2659
 2660    /// <inheritdoc/>
 2661    public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
 2662    {
 02663        using var dbContext = _dbProvider.CreateDbContext();
 2664
 02665        var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 02666            .Where(e => artistNames.Contains(e.Name))
 02667            .ToArray();
 2668
 02669        return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Where(d
 02670    }
 2671}

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>)