< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Item.BaseItemRepository
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
Line coverage
50%
Covered lines: 667
Uncovered lines: 642
Coverable lines: 1309
Total lines: 2607
Line coverage: 50.9%
Branch coverage
49%
Covered branches: 356
Total branches: 726
Branch coverage: 49%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/19/2025 - 12:10:00 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24528/25/2025 - 12:11:25 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.4% (329/694) Total lines: 24528/26/2025 - 12:09:43 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24528/31/2025 - 12:09:44 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.4% (329/694) Total lines: 24529/1/2025 - 12:10:05 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.2% (328/694) Total lines: 24529/2/2025 - 12:10:53 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24529/4/2025 - 12:11:04 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.4% (329/694) Total lines: 24529/5/2025 - 12:11:19 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.2% (328/694) Total lines: 24529/6/2025 - 12:11:20 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24529/7/2025 - 12:11:13 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.2% (328/694) Total lines: 24529/8/2025 - 12:11:25 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24529/17/2025 - 12:11:23 AM Line coverage: 51% (631/1236) Branch coverage: 46.8% (330/704) Total lines: 25129/18/2025 - 12:09:59 AM Line coverage: 51% (631/1236) Branch coverage: 46.7% (329/704) Total lines: 25129/19/2025 - 12:11:12 AM Line coverage: 51% (631/1236) Branch coverage: 46.8% (330/704) Total lines: 25129/20/2025 - 12:11:30 AM Line coverage: 51.1% (632/1236) Branch coverage: 47.1% (332/704) Total lines: 25129/23/2025 - 12:11:13 AM Line coverage: 52.3% (658/1258) Branch coverage: 49.1% (352/716) Total lines: 25349/24/2025 - 12:10:54 AM Line coverage: 52.1% (659/1263) Branch coverage: 49.1% (352/716) Total lines: 25419/25/2025 - 12:11:18 AM Line coverage: 51.9% (659/1269) Branch coverage: 49.1% (353/718) Total lines: 255310/2/2025 - 12:11:36 AM Line coverage: 52% (663/1275) Branch coverage: 49.1% (351/714) Total lines: 255710/3/2025 - 12:11:15 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: 2607 8/19/2025 - 12:10:00 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24528/25/2025 - 12:11:25 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.4% (329/694) Total lines: 24528/26/2025 - 12:09:43 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24528/31/2025 - 12:09:44 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.4% (329/694) Total lines: 24529/1/2025 - 12:10:05 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.2% (328/694) Total lines: 24529/2/2025 - 12:10:53 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24529/4/2025 - 12:11:04 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.4% (329/694) Total lines: 24529/5/2025 - 12:11:19 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.2% (328/694) Total lines: 24529/6/2025 - 12:11:20 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24529/7/2025 - 12:11:13 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.2% (328/694) Total lines: 24529/8/2025 - 12:11:25 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24529/17/2025 - 12:11:23 AM Line coverage: 51% (631/1236) Branch coverage: 46.8% (330/704) Total lines: 25129/18/2025 - 12:09:59 AM Line coverage: 51% (631/1236) Branch coverage: 46.7% (329/704) Total lines: 25129/19/2025 - 12:11:12 AM Line coverage: 51% (631/1236) Branch coverage: 46.8% (330/704) Total lines: 25129/20/2025 - 12:11:30 AM Line coverage: 51.1% (632/1236) Branch coverage: 47.1% (332/704) Total lines: 25129/23/2025 - 12:11:13 AM Line coverage: 52.3% (658/1258) Branch coverage: 49.1% (352/716) Total lines: 25349/24/2025 - 12:10:54 AM Line coverage: 52.1% (659/1263) Branch coverage: 49.1% (352/716) Total lines: 25419/25/2025 - 12:11:18 AM Line coverage: 51.9% (659/1269) Branch coverage: 49.1% (353/718) Total lines: 255310/2/2025 - 12:11:36 AM Line coverage: 52% (663/1275) Branch coverage: 49.1% (351/714) Total lines: 255710/3/2025 - 12:11:15 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: 2607

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%4260%
GetNextUpSeriesKeys(...)0%620%
ApplyGroupingFilter(...)75%10869.23%
ApplyNavigations(...)100%22100%
ApplyQueryPaging(...)87.5%8885.71%
ApplyQueryFilter(...)100%11100%
PrepareItemQuery(...)100%11100%
GetCount(...)100%210%
GetItemCounts(...)0%420200%
GetType(...)100%11100%
SaveImages(...)0%620%
SaveItems(...)100%11100%
UpdateOrInsertItems(...)84.37%333289%
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%151272.72%
GetItemValues(...)0%210140%
PrepareFilterQuery(...)83.33%6680%
GetCleanValue(...)50%2266.66%
GetItemValuesToSave(...)50%4481.81%
Map(...)0%620%
Map(...)0%7280%
GetPathToSave(...)50%2266.66%
GetItemByNameTypesInQuery(...)100%1010100%
IsTypeInQuery(...)75%5466.66%
EnableGroupByPresentationUniqueKey(...)65%212087.5%
ApplyOrder(...)57.69%332678.26%
TranslateQuery(...)44.85%3074835037.16%
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>
 160    public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
 61
 62    /// <summary>
 63    /// This holds all the types in the running assemblies
 64    /// so that we can de-serialize properly when we don't have strong types.
 65    /// </summary>
 166    private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
 67    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 68    private readonly IServerApplicationHost _appHost;
 69    private readonly IItemTypeLookup _itemTypeLookup;
 70    private readonly IServerConfigurationManager _serverConfigurationManager;
 71    private readonly ILogger<BaseItemRepository> _logger;
 72
 173    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType
 174    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
 175    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
 176    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
 177    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
 178    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    {
 2195        _dbProvider = dbProvider;
 2196        _appHost = appHost;
 2197        _itemTypeLookup = itemTypeLookup;
 2198        _serverConfigurationManager = serverConfigurationManager;
 2199        _logger = logger;
 21100    }
 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    {
 14163        using var context = _dbProvider.CreateDbContext();
 14164        using var transaction = context.Database.BeginTransaction();
 165
 14166        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 167        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 14168        context.SaveChanges();
 169
 14170        transaction.Commit();
 28171    }
 172
 173    /// <inheritdoc />
 174    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 175    {
 16176        ArgumentNullException.ThrowIfNull(filter);
 16177        PrepareFilterQuery(filter);
 178
 16179        using var context = _dbProvider.CreateDbContext();
 16180        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 16181    }
 182
 183    /// <inheritdoc />
 184    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 185    {
 0186        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 187    }
 188
 189    /// <inheritdoc />
 190    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 191    {
 0192        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 193    }
 194
 195    /// <inheritdoc />
 196    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 197    {
 0198        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 199    }
 200
 201    /// <inheritdoc />
 202    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 203    {
 0204        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 205    }
 206
 207    /// <inheritdoc />
 208    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 209    {
 0210        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 211    }
 212
 213    /// <inheritdoc />
 214    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 215    {
 0216        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 217    }
 218
 219    /// <inheritdoc />
 220    public IReadOnlyList<string> GetStudioNames()
 221    {
 16222        return GetItemValueNames(_getStudiosValueTypes, [], []);
 223    }
 224
 225    /// <inheritdoc />
 226    public IReadOnlyList<string> GetAllArtistNames()
 227    {
 16228        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 229    }
 230
 231    /// <inheritdoc />
 232    public IReadOnlyList<string> GetMusicGenreNames()
 233    {
 16234        return GetItemValueNames(
 16235            _getGenreValueTypes,
 16236            _itemTypeLookup.MusicGenreTypes,
 16237            []);
 238    }
 239
 240    /// <inheritdoc />
 241    public IReadOnlyList<string> GetGenreNames()
 242    {
 16243        return GetItemValueNames(
 16244            _getGenreValueTypes,
 16245            [],
 16246            _itemTypeLookup.MusicGenreTypes);
 247    }
 248
 249    /// <inheritdoc />
 250    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
 251    {
 1252        ArgumentNullException.ThrowIfNull(filter);
 1253        if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (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    {
 321288        ArgumentNullException.ThrowIfNull(filter);
 321289        PrepareFilterQuery(filter);
 290
 321291        using var context = _dbProvider.CreateDbContext();
 321292        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 293
 321294        dbQuery = TranslateQuery(dbQuery, context, filter);
 295
 321296        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 321297        dbQuery = ApplyQueryPaging(dbQuery, filter);
 321298        dbQuery = ApplyNavigations(dbQuery, filter);
 299
 321300        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializ
 321301    }
 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)
 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)
 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
 337385        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 337386        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        }
 337391        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        }
 336396        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        {
 336403            dbQuery = dbQuery.Distinct();
 404        }
 405
 337406        dbQuery = ApplyOrder(dbQuery, filter, context);
 407
 337408        return dbQuery;
 409    }
 410
 411    private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 412    {
 337413        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 337414           .Include(e => e.Provider)
 337415           .Include(e => e.LockedFields)
 337416           .Include(e => e.UserData);
 417
 337418        if (filter.DtoOptions.EnableImages)
 419        {
 337420            dbQuery = dbQuery.Include(e => e.Images);
 421        }
 422
 337423        return dbQuery;
 424    }
 425
 426    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 427    {
 337428        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 429        {
 103430            var offset = filter.StartIndex ?? 0;
 431
 103432            if (offset > 0)
 433            {
 0434                dbQuery = dbQuery.Skip(offset);
 435            }
 436
 103437            if (filter.Limit.HasValue)
 438            {
 103439                dbQuery = dbQuery.Take(filter.Limit.Value);
 440            }
 441        }
 442
 337443        return dbQuery;
 444    }
 445
 446    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 447    {
 16448        dbQuery = TranslateQuery(dbQuery, context, filter);
 16449        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 16450        dbQuery = ApplyQueryPaging(dbQuery, filter);
 16451        dbQuery = ApplyNavigations(dbQuery, filter);
 16452        return dbQuery;
 453    }
 454
 455    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 456    {
 407457        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 407458        dbQuery = dbQuery.AsSingleQuery();
 459
 407460        return dbQuery;
 461    }
 462
 463    /// <inheritdoc/>
 464    public int GetCount(InternalItemsQuery filter)
 465    {
 0466        ArgumentNullException.ThrowIfNull(filter);
 467        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0468        PrepareFilterQuery(filter);
 469
 0470        using var context = _dbProvider.CreateDbContext();
 0471        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 472
 0473        return dbQuery.Count();
 0474    }
 475
 476    /// <inheritdoc />
 477    public ItemCounts GetItemCounts(InternalItemsQuery filter)
 478    {
 0479        ArgumentNullException.ThrowIfNull(filter);
 480        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0481        PrepareFilterQuery(filter);
 482
 0483        using var context = _dbProvider.CreateDbContext();
 0484        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 485
 0486        var counts = dbQuery
 0487            .GroupBy(x => x.Type)
 0488            .Select(x => new { x.Key, Count = x.Count() })
 0489            .ToArray();
 490
 0491        var lookup = _itemTypeLookup.BaseItemKindNames;
 0492        var result = new ItemCounts();
 0493        foreach (var count in counts)
 494        {
 0495            if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
 496            {
 0497                result.AlbumCount = count.Count;
 498            }
 0499            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
 500            {
 0501                result.ArtistCount = count.Count;
 502            }
 0503            else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
 504            {
 0505                result.EpisodeCount = count.Count;
 506            }
 0507            else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
 508            {
 0509                result.MovieCount = count.Count;
 510            }
 0511            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
 512            {
 0513                result.MusicVideoCount = count.Count;
 514            }
 0515            else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
 516            {
 0517                result.ProgramCount = count.Count;
 518            }
 0519            else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
 520            {
 0521                result.SeriesCount = count.Count;
 522            }
 0523            else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
 524            {
 0525                result.SongCount = count.Count;
 526            }
 0527            else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
 528            {
 0529                result.TrailerCount = count.Count;
 530            }
 531        }
 532
 0533        return result;
 0534    }
 535
 536#pragma warning disable CA1307 // Specify StringComparison for clarity
 537    /// <summary>
 538    /// Gets the type.
 539    /// </summary>
 540    /// <param name="typeName">Name of the type.</param>
 541    /// <returns>Type.</returns>
 542    /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
 543    private static Type? GetType(string typeName)
 544    {
 142545        ArgumentException.ThrowIfNullOrEmpty(typeName);
 546
 547        // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static 
 548        // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are 
 142549        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 142550            .Select(a => a.GetType(k))
 142551            .FirstOrDefault(t => t is not null));
 552    }
 553
 554    /// <inheritdoc  />
 555    public void SaveImages(BaseItemDto item)
 556    {
 0557        ArgumentNullException.ThrowIfNull(item);
 558
 0559        var images = item.ImageInfos.Select(e => Map(item.Id, e));
 0560        using var context = _dbProvider.CreateDbContext();
 561
 0562        if (!context.BaseItems.Any(bi => bi.Id == item.Id))
 563        {
 0564            _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
 0565            return;
 566        }
 567
 0568        context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
 0569        context.BaseItemImageInfos.AddRange(images);
 0570        context.SaveChanges();
 0571    }
 572
 573    /// <inheritdoc  />
 574    public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 575    {
 80576        UpdateOrInsertItems(items, cancellationToken);
 79577    }
 578
 579    /// <inheritdoc cref="IItemRepository"/>
 580    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 581    {
 80582        ArgumentNullException.ThrowIfNull(items);
 80583        cancellationToken.ThrowIfCancellationRequested();
 584
 79585        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 316586        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
 587        {
 79588            var ancestorIds = item.SupportsAncestors ?
 79589                item.GetAncestorIds().Distinct().ToList() :
 79590                null;
 591
 79592            var topParent = item.GetTopParent();
 593
 79594            var userdataKey = item.GetUserDataKeys();
 79595            var inheritedTags = item.GetInheritedTags();
 596
 79597            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 598        }
 599
 79600        using var context = _dbProvider.CreateDbContext();
 79601        using var transaction = context.Database.BeginTransaction();
 602
 79603        var ids = tuples.Select(f => f.Item.Id).ToArray();
 79604        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 79605        var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
 606
 316607        foreach (var item in tuples)
 608        {
 79609            var entity = Map(item.Item);
 610            // TODO: refactor this "inconsistency"
 79611            entity.TopParentId = item.TopParent?.Id;
 612
 79613            if (!existingItems.Any(e => e == entity.Id))
 614            {
 59615                context.BaseItems.Add(entity);
 616            }
 617            else
 618            {
 20619                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 20620                context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 621
 20622                if (entity.Images is { Count: > 0 })
 623                {
 0624                    context.BaseItemImageInfos.AddRange(entity.Images);
 625                }
 626
 20627                context.BaseItems.Attach(entity).State = EntityState.Modified;
 628            }
 629        }
 630
 79631        context.SaveChanges();
 632
 276633        foreach (var item in newItems)
 634        {
 635            // reattach old userData entries
 59636            var userKeys = item.UserDataKey.ToArray();
 59637            var retentionDate = (DateTime?)null;
 59638            context.UserData
 59639                .Where(e => e.ItemId == PlaceholderId)
 59640                .Where(e => userKeys.Contains(e.CustomDataKey))
 59641                .ExecuteUpdate(e => e
 59642                    .SetProperty(f => f.ItemId, item.Item.Id)
 59643                    .SetProperty(f => f.RetentionDate, retentionDate));
 644        }
 645
 79646        var itemValueMaps = tuples
 79647            .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 79648            .ToArray();
 79649        var allListedItemValues = itemValueMaps
 79650            .SelectMany(f => f.Values)
 79651            .Distinct()
 79652            .ToArray();
 79653        var existingValues = context.ItemValues
 79654            .Select(e => new
 79655            {
 79656                item = e,
 79657                Key = e.Type + "+" + e.Value
 79658            })
 79659            .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
 79660            .Select(e => e.item)
 79661            .ToArray();
 79662        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 79663        {
 79664            CleanValue = GetCleanValue(f.Value),
 79665            ItemValueId = Guid.NewGuid(),
 79666            Type = f.MagicNumber,
 79667            Value = f.Value
 79668        }).ToArray();
 79669        context.ItemValues.AddRange(missingItemValues);
 79670        context.SaveChanges();
 671
 79672        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 79673        var valueMap = itemValueMaps
 79674            .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type =
 79675            .ToArray();
 676
 79677        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 678
 316679        foreach (var item in valueMap)
 680        {
 79681            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 158682            foreach (var itemValue in item.Values)
 683            {
 0684                var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
 0685                if (existingItem is null)
 686                {
 0687                    context.ItemValuesMap.Add(new ItemValueMap()
 0688                    {
 0689                        Item = null!,
 0690                        ItemId = item.Item.Id,
 0691                        ItemValue = null!,
 0692                        ItemValueId = itemValue.ItemValueId
 0693                    });
 694                }
 695                else
 696                {
 697                    // map exists, remove from list so its been handled.
 0698                    itemMappedValues.Remove(existingItem);
 699                }
 700            }
 701
 702            // all still listed values are not in the new list so remove them.
 79703            context.ItemValuesMap.RemoveRange(itemMappedValues);
 704        }
 705
 79706        context.SaveChanges();
 707
 316708        foreach (var item in tuples)
 709        {
 79710            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 711            {
 79712                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 79713                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 208714                foreach (var ancestorId in validAncestorIds)
 715                {
 25716                    var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
 25717                    if (existingAncestorId is null)
 718                    {
 23719                        context.AncestorIds.Add(new AncestorId()
 23720                        {
 23721                            ParentItemId = ancestorId,
 23722                            ItemId = item.Item.Id,
 23723                            Item = null!,
 23724                            ParentItem = null!
 23725                        });
 726                    }
 727                    else
 728                    {
 2729                        existingAncestorIds.Remove(existingAncestorId);
 730                    }
 731                }
 732
 79733                context.AncestorIds.RemoveRange(existingAncestorIds);
 734            }
 735        }
 736
 79737        context.SaveChanges();
 79738        transaction.Commit();
 158739    }
 740
 741    /// <inheritdoc  />
 742    public BaseItemDto? RetrieveItem(Guid id)
 743    {
 86744        if (id.IsEmpty())
 745        {
 0746            throw new ArgumentException("Guid can't be empty", nameof(id));
 747        }
 748
 86749        using var context = _dbProvider.CreateDbContext();
 86750        var dbQuery = PrepareItemQuery(context, new()
 86751        {
 86752            DtoOptions = new()
 86753            {
 86754                EnableImages = true
 86755            }
 86756        });
 86757        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 86758            .Include(e => e.Provider)
 86759            .Include(e => e.LockedFields)
 86760            .Include(e => e.UserData)
 86761            .Include(e => e.Images);
 762
 86763        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 86764        if (item is null)
 765        {
 86766            return null;
 767        }
 768
 0769        return DeserializeBaseItem(item);
 86770    }
 771
 772    /// <summary>
 773    /// Maps a Entity to the DTO.
 774    /// </summary>
 775    /// <param name="entity">The entity.</param>
 776    /// <param name="dto">The dto base instance.</param>
 777    /// <param name="appHost">The Application server Host.</param>
 778    /// <param name="logger">The applogger.</param>
 779    /// <returns>The dto to map.</returns>
 780    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 781    {
 71782        dto.Id = entity.Id;
 71783        dto.ParentId = entity.ParentId.GetValueOrDefault();
 71784        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 71785        dto.EndDate = entity.EndDate;
 71786        dto.CommunityRating = entity.CommunityRating;
 71787        dto.CustomRating = entity.CustomRating;
 71788        dto.IndexNumber = entity.IndexNumber;
 71789        dto.IsLocked = entity.IsLocked;
 71790        dto.Name = entity.Name;
 71791        dto.OfficialRating = entity.OfficialRating;
 71792        dto.Overview = entity.Overview;
 71793        dto.ParentIndexNumber = entity.ParentIndexNumber;
 71794        dto.PremiereDate = entity.PremiereDate;
 71795        dto.ProductionYear = entity.ProductionYear;
 71796        dto.SortName = entity.SortName;
 71797        dto.ForcedSortName = entity.ForcedSortName;
 71798        dto.RunTimeTicks = entity.RunTimeTicks;
 71799        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 71800        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 71801        dto.IsInMixedFolder = entity.IsInMixedFolder;
 71802        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 71803        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 71804        dto.CriticRating = entity.CriticRating;
 71805        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 71806        dto.OriginalTitle = entity.OriginalTitle;
 71807        dto.Album = entity.Album;
 71808        dto.LUFS = entity.LUFS;
 71809        dto.NormalizationGain = entity.NormalizationGain;
 71810        dto.IsVirtualItem = entity.IsVirtualItem;
 71811        dto.ExternalSeriesId = entity.ExternalSeriesId;
 71812        dto.Tagline = entity.Tagline;
 71813        dto.TotalBitrate = entity.TotalBitrate;
 71814        dto.ExternalId = entity.ExternalId;
 71815        dto.Size = entity.Size;
 71816        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 71817        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 71818        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 71819        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 71820        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 71821        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 71822        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 71823        dto.Width = entity.Width.GetValueOrDefault();
 71824        dto.Height = entity.Height.GetValueOrDefault();
 71825        dto.UserData = entity.UserData;
 826
 71827        if (entity.Provider is not null)
 828        {
 71829            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 830        }
 831
 71832        if (entity.ExtraType is not null)
 833        {
 0834            dto.ExtraType = (ExtraType)entity.ExtraType;
 835        }
 836
 71837        if (entity.LockedFields is not null)
 838        {
 71839            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 840        }
 841
 71842        if (entity.Audio is not null)
 843        {
 0844            dto.Audio = (ProgramAudio)entity.Audio;
 845        }
 846
 71847        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 71848        dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
 71849        dto.Studios = entity.Studios?.Split('|') ?? [];
 71850        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 851
 71852        if (dto is IHasProgramAttributes hasProgramAttributes)
 853        {
 0854            hasProgramAttributes.IsMovie = entity.IsMovie;
 0855            hasProgramAttributes.IsSeries = entity.IsSeries;
 0856            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0857            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 858        }
 859
 71860        if (dto is LiveTvChannel liveTvChannel)
 861        {
 0862            liveTvChannel.ServiceName = entity.ExternalServiceId;
 863        }
 864
 71865        if (dto is Trailer trailer)
 866        {
 0867            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 868        }
 869
 71870        if (dto is Video video)
 871        {
 0872            video.PrimaryVersionId = entity.PrimaryVersionId;
 873        }
 874
 71875        if (dto is IHasSeries hasSeriesName)
 876        {
 0877            hasSeriesName.SeriesName = entity.SeriesName;
 0878            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0879            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 880        }
 881
 71882        if (dto is Episode episode)
 883        {
 0884            episode.SeasonName = entity.SeasonName;
 0885            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 886        }
 887
 71888        if (dto is IHasArtist hasArtists)
 889        {
 0890            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 891        }
 892
 71893        if (dto is IHasAlbumArtist hasAlbumArtists)
 894        {
 0895            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 896        }
 897
 71898        if (dto is LiveTvProgram program)
 899        {
 0900            program.ShowId = entity.ShowId;
 901        }
 902
 71903        if (entity.Images is not null)
 904        {
 71905            dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
 906        }
 907
 908        // dto.Type = entity.Type;
 909        // dto.Data = entity.Data;
 910        // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
 71911        if (dto is IHasStartDate hasStartDate)
 912        {
 0913            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 914        }
 915
 916        // Fields that are present in the DB but are never actually used
 917        // dto.UnratedType = entity.UnratedType;
 918        // dto.TopParentId = entity.TopParentId;
 919        // dto.CleanName = entity.CleanName;
 920        // dto.UserDataKey = entity.UserDataKey;
 921
 71922        if (dto is Folder folder)
 923        {
 71924            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 925        }
 926
 71927        return dto;
 928    }
 929
 930    /// <summary>
 931    /// Maps a Entity to the DTO.
 932    /// </summary>
 933    /// <param name="dto">The entity.</param>
 934    /// <returns>The dto to map.</returns>
 935    public BaseItemEntity Map(BaseItemDto dto)
 936    {
 79937        var dtoType = dto.GetType();
 79938        var entity = new BaseItemEntity()
 79939        {
 79940            Type = dtoType.ToString(),
 79941            Id = dto.Id
 79942        };
 943
 79944        if (TypeRequiresDeserialization(dtoType))
 945        {
 58946            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 947        }
 948
 79949        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 79950        entity.Path = GetPathToSave(dto.Path);
 79951        entity.EndDate = dto.EndDate;
 79952        entity.CommunityRating = dto.CommunityRating;
 79953        entity.CustomRating = dto.CustomRating;
 79954        entity.IndexNumber = dto.IndexNumber;
 79955        entity.IsLocked = dto.IsLocked;
 79956        entity.Name = dto.Name;
 79957        entity.CleanName = GetCleanValue(dto.Name);
 79958        entity.OfficialRating = dto.OfficialRating;
 79959        entity.Overview = dto.Overview;
 79960        entity.ParentIndexNumber = dto.ParentIndexNumber;
 79961        entity.PremiereDate = dto.PremiereDate;
 79962        entity.ProductionYear = dto.ProductionYear;
 79963        entity.SortName = dto.SortName;
 79964        entity.ForcedSortName = dto.ForcedSortName;
 79965        entity.RunTimeTicks = dto.RunTimeTicks;
 79966        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 79967        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 79968        entity.IsInMixedFolder = dto.IsInMixedFolder;
 79969        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 79970        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 79971        entity.CriticRating = dto.CriticRating;
 79972        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 79973        entity.OriginalTitle = dto.OriginalTitle;
 79974        entity.Album = dto.Album;
 79975        entity.LUFS = dto.LUFS;
 79976        entity.NormalizationGain = dto.NormalizationGain;
 79977        entity.IsVirtualItem = dto.IsVirtualItem;
 79978        entity.ExternalSeriesId = dto.ExternalSeriesId;
 79979        entity.Tagline = dto.Tagline;
 79980        entity.TotalBitrate = dto.TotalBitrate;
 79981        entity.ExternalId = dto.ExternalId;
 79982        entity.Size = dto.Size;
 79983        entity.Genres = string.Join('|', dto.Genres);
 79984        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 79985        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 79986        entity.ChannelId = dto.ChannelId;
 79987        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 79988        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 79989        entity.OwnerId = dto.OwnerId.ToString();
 79990        entity.Width = dto.Width;
 79991        entity.Height = dto.Height;
 79992        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 79993        {
 79994            Item = entity,
 79995            ProviderId = e.Key,
 79996            ProviderValue = e.Value
 79997        }).ToList();
 998
 79999        if (dto.Audio.HasValue)
 1000        {
 01001            entity.Audio = (ProgramAudioEntity)dto.Audio;
 1002        }
 1003
 791004        if (dto.ExtraType.HasValue)
 1005        {
 01006            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 1007        }
 1008
 791009        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 791010        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : n
 791011        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
 791012        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
 791013        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 791014            .Select(e => new BaseItemMetadataField()
 791015            {
 791016                Id = (int)e,
 791017                Item = entity,
 791018                ItemId = entity.Id
 791019            })
 791020            .ToArray() : null;
 1021
 791022        if (dto is IHasProgramAttributes hasProgramAttributes)
 1023        {
 01024            entity.IsMovie = hasProgramAttributes.IsMovie;
 01025            entity.IsSeries = hasProgramAttributes.IsSeries;
 01026            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 01027            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 1028        }
 1029
 791030        if (dto is LiveTvChannel liveTvChannel)
 1031        {
 01032            entity.ExternalServiceId = liveTvChannel.ServiceName;
 1033        }
 1034
 791035        if (dto is Video video)
 1036        {
 01037            entity.PrimaryVersionId = video.PrimaryVersionId;
 1038        }
 1039
 791040        if (dto is IHasSeries hasSeriesName)
 1041        {
 01042            entity.SeriesName = hasSeriesName.SeriesName;
 01043            entity.SeriesId = hasSeriesName.SeriesId;
 01044            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 1045        }
 1046
 791047        if (dto is Episode episode)
 1048        {
 01049            entity.SeasonName = episode.SeasonName;
 01050            entity.SeasonId = episode.SeasonId;
 1051        }
 1052
 791053        if (dto is IHasArtist hasArtists)
 1054        {
 01055            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
 1056        }
 1057
 791058        if (dto is IHasAlbumArtist hasAlbumArtists)
 1059        {
 01060            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 1061        }
 1062
 791063        if (dto is LiveTvProgram program)
 1064        {
 01065            entity.ShowId = program.ShowId;
 1066        }
 1067
 791068        if (dto.ImageInfos is not null)
 1069        {
 791070            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 1071        }
 1072
 791073        if (dto is Trailer trailer)
 1074        {
 01075            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 01076            {
 01077                Id = (int)e,
 01078                Item = entity,
 01079                ItemId = entity.Id
 01080            }).ToArray() ?? [];
 1081        }
 1082
 1083        // dto.Type = entity.Type;
 1084        // dto.Data = entity.Data;
 791085        entity.MediaType = dto.MediaType.ToString();
 791086        if (dto is IHasStartDate hasStartDate)
 1087        {
 01088            entity.StartDate = hasStartDate.StartDate;
 1089        }
 1090
 791091        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 1092
 1093        // Fields that are present in the DB but are never actually used
 1094        // dto.UserDataKey = entity.UserDataKey;
 1095
 791096        if (dto is Folder folder)
 1097        {
 791098            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 791099            entity.IsFolder = folder.IsFolder;
 1100        }
 1101
 791102        return entity;
 1103    }
 1104
 1105    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 1106    {
 641107        using var context = _dbProvider.CreateDbContext();
 1108
 641109        var query = context.ItemValuesMap
 641110            .AsNoTracking()
 641111            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 641112        if (withItemTypes.Count > 0)
 1113        {
 161114            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 1115        }
 1116
 641117        if (excludeItemTypes.Count > 0)
 1118        {
 161119            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 1120        }
 1121
 1122        // query = query.DistinctBy(e => e.CleanValue);
 641123        return query.Select(e => e.ItemValue)
 641124            .GroupBy(e => e.CleanValue)
 641125            .Select(e => e.First().Value)
 641126            .ToArray();
 641127    }
 1128
 1129    private static bool TypeRequiresDeserialization(Type type)
 1130    {
 1501131        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 1132    }
 1133
 1134    private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 1135    {
 711136        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 711137        if (_serverConfigurationManager?.Configuration is null)
 1138        {
 01139            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 1140        }
 1141
 711142        var typeToSerialise = GetType(baseItemEntity.Type);
 711143        return BaseItemRepository.DeserializeBaseItem(
 711144            baseItemEntity,
 711145            _logger,
 711146            _appHost,
 711147            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 1148    }
 1149
 1150    /// <summary>
 1151    /// Deserializes a BaseItemEntity and sets all properties.
 1152    /// </summary>
 1153    /// <param name="baseItemEntity">The DB entity.</param>
 1154    /// <param name="logger">Logger.</param>
 1155    /// <param name="appHost">The application server Host.</param>
 1156    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 1157    /// <returns>A mapped BaseItem.</returns>
 1158    /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
 1159    public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost?
 1160    {
 711161        var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.
 711162        BaseItemDto? dto = null;
 711163        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1164        {
 1165            try
 1166            {
 121167                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 121168            }
 01169            catch (JsonException ex)
 1170            {
 01171                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01172            }
 1173        }
 1174
 711175        if (dto is null)
 1176        {
 591177            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1178        }
 1179
 711180        return Map(baseItemEntity, dto, appHost, logger);
 1181    }
 1182
 1183    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 1184    {
 01185        ArgumentNullException.ThrowIfNull(filter);
 1186
 01187        if (!filter.Limit.HasValue)
 1188        {
 01189            filter.EnableTotalRecordCount = false;
 1190        }
 1191
 01192        using var context = _dbProvider.CreateDbContext();
 1193
 01194        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 01195        {
 01196            ExcludeItemTypes = filter.ExcludeItemTypes,
 01197            IncludeItemTypes = filter.IncludeItemTypes,
 01198            MediaTypes = filter.MediaTypes,
 01199            AncestorIds = filter.AncestorIds,
 01200            ItemIds = filter.ItemIds,
 01201            TopParentIds = filter.TopParentIds,
 01202            ParentId = filter.ParentId,
 01203            IsAiring = filter.IsAiring,
 01204            IsMovie = filter.IsMovie,
 01205            IsSports = filter.IsSports,
 01206            IsKids = filter.IsKids,
 01207            IsNews = filter.IsNews,
 01208            IsSeries = filter.IsSeries
 01209        });
 1210
 01211        var itemValuesQuery = context.ItemValues
 01212            .Where(f => itemValueTypes.Contains(f.Type))
 01213            .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
 01214            .Join(
 01215                innerQueryFilter,
 01216                fw => fw.w.ItemId,
 01217                g => g.Id,
 01218                (fw, g) => fw.f.CleanValue);
 1219
 01220        var innerQuery = PrepareItemQuery(context, filter)
 01221            .Where(e => e.Type == returnType)
 01222            .Where(e => itemValuesQuery.Contains(e.CleanName));
 1223
 01224        var outerQueryFilter = new InternalItemsQuery(filter.User)
 01225        {
 01226            IsPlayed = filter.IsPlayed,
 01227            IsFavorite = filter.IsFavorite,
 01228            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 01229            IsLiked = filter.IsLiked,
 01230            IsLocked = filter.IsLocked,
 01231            NameLessThan = filter.NameLessThan,
 01232            NameStartsWith = filter.NameStartsWith,
 01233            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 01234            Tags = filter.Tags,
 01235            OfficialRatings = filter.OfficialRatings,
 01236            StudioIds = filter.StudioIds,
 01237            GenreIds = filter.GenreIds,
 01238            Genres = filter.Genres,
 01239            Years = filter.Years,
 01240            NameContains = filter.NameContains,
 01241            SearchTerm = filter.SearchTerm,
 01242            ExcludeItemIds = filter.ExcludeItemIds
 01243        };
 1244
 01245        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
 01246            .GroupBy(e => e.PresentationUniqueKey)
 01247            .Select(e => e.FirstOrDefault())
 01248            .Select(e => e!.Id);
 1249
 01250        var query = context.BaseItems
 01251            .Include(e => e.TrailerTypes)
 01252            .Include(e => e.Provider)
 01253            .Include(e => e.LockedFields)
 01254            .Include(e => e.Images)
 01255            .AsSingleQuery()
 01256            .Where(e => masterQuery.Contains(e.Id));
 1257
 01258        query = ApplyOrder(query, filter, context);
 1259
 01260        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 01261        if (filter.EnableTotalRecordCount)
 1262        {
 01263            result.TotalRecordCount = query.Count();
 1264        }
 1265
 01266        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 1267        {
 01268            var offset = filter.StartIndex ?? 0;
 1269
 01270            if (offset > 0)
 1271            {
 01272                query = query.Skip(offset);
 1273            }
 1274
 01275            if (filter.Limit.HasValue)
 1276            {
 01277                query = query.Take(filter.Limit.Value);
 1278            }
 1279        }
 1280
 01281        IQueryable<BaseItemEntity>? itemCountQuery = null;
 1282
 01283        if (filter.IncludeItemTypes.Length > 0)
 1284        {
 1285            // if we are to include more then one type, sub query those items beforehand.
 1286
 01287            var typeSubQuery = new InternalItemsQuery(filter.User)
 01288            {
 01289                ExcludeItemTypes = filter.ExcludeItemTypes,
 01290                IncludeItemTypes = filter.IncludeItemTypes,
 01291                MediaTypes = filter.MediaTypes,
 01292                AncestorIds = filter.AncestorIds,
 01293                ExcludeItemIds = filter.ExcludeItemIds,
 01294                ItemIds = filter.ItemIds,
 01295                TopParentIds = filter.TopParentIds,
 01296                ParentId = filter.ParentId,
 01297                IsPlayed = filter.IsPlayed
 01298            };
 1299
 01300            itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 01301                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 1302
 01303            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 01304            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 01305            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 01306            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 01307            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 01308            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 01309            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 1310
 01311            var resultQuery = query.Select(e => new
 01312            {
 01313                item = e,
 01314                // TODO: This is bad refactor!
 01315                itemCount = new ItemCounts()
 01316                {
 01317                    SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
 01318                    EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
 01319                    MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
 01320                    AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
 01321                    ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
 01322                    SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
 01323                    TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
 01324                }
 01325            });
 1326
 01327            result.StartIndex = filter.StartIndex ?? 0;
 01328            result.Items =
 01329            [
 01330                .. resultQuery
 01331                    .AsEnumerable()
 01332                    .Where(e => e is not null)
 01333                    .Select(e =>
 01334                    {
 01335                        return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
 01336                    })
 01337            ];
 1338        }
 1339        else
 1340        {
 01341            result.StartIndex = filter.StartIndex ?? 0;
 01342            result.Items =
 01343            [
 01344                .. query
 01345                    .AsEnumerable()
 01346                    .Where(e => e is not null)
 01347                    .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
 01348                    {
 01349                        return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
 01350                    })
 01351            ];
 1352        }
 1353
 01354        return result;
 01355    }
 1356
 1357    private static void PrepareFilterQuery(InternalItemsQuery query)
 1358    {
 3371359        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 1360        {
 01361            query.Limit = query.Limit.Value + 4;
 1362        }
 1363
 3371364        if (query.IsResumable ?? false)
 1365        {
 11366            query.IsVirtualItem = false;
 1367        }
 3371368    }
 1369
 1370    private string GetCleanValue(string value)
 1371    {
 821372        if (string.IsNullOrWhiteSpace(value))
 1373        {
 01374            return value;
 1375        }
 1376
 821377        return value.RemoveDiacritics().ToLowerInvariant();
 1378    }
 1379
 1380    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1381    {
 791382        var list = new List<(ItemValueType, string)>();
 1383
 791384        if (item is IHasArtist hasArtist)
 1385        {
 01386            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1387        }
 1388
 791389        if (item is IHasAlbumArtist hasAlbumArtist)
 1390        {
 01391            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1392        }
 1393
 791394        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 791395        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 791396        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1397
 1398        // keywords was 5
 1399
 791400        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1401
 1402        // Remove all invalid values.
 791403        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1404
 791405        return list;
 1406    }
 1407
 1408    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1409    {
 01410        return new BaseItemImageInfo()
 01411        {
 01412            ItemId = baseItemId,
 01413            Id = Guid.NewGuid(),
 01414            Path = e.Path,
 01415            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 01416            DateModified = e.DateModified,
 01417            Height = e.Height,
 01418            Width = e.Width,
 01419            ImageType = (ImageInfoImageType)e.Type,
 01420            Item = null!
 01421        };
 1422    }
 1423
 1424    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 1425    {
 01426        return new ItemImageInfo()
 01427        {
 01428            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 01429            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 01430            DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
 01431            Height = e.Height,
 01432            Width = e.Width,
 01433            Type = (ImageType)e.ImageType
 01434        };
 1435    }
 1436
 1437    private string? GetPathToSave(string path)
 1438    {
 791439        if (path is null)
 1440        {
 01441            return null;
 1442        }
 1443
 791444        return _appHost.ReverseVirtualPath(path);
 1445    }
 1446
 1447    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1448    {
 121449        var list = new List<string>();
 1450
 121451        if (IsTypeInQuery(BaseItemKind.Person, query))
 1452        {
 11453            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1454        }
 1455
 121456        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1457        {
 11458            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1459        }
 1460
 121461        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1462        {
 11463            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1464        }
 1465
 121466        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1467        {
 11468            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1469        }
 1470
 121471        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1472        {
 11473            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1474        }
 1475
 121476        return list;
 1477    }
 1478
 1479    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1480    {
 601481        if (query.ExcludeItemTypes.Contains(type))
 1482        {
 01483            return false;
 1484        }
 1485
 601486        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 1487    }
 1488
 1489    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 1490    {
 3371491        if (!query.GroupByPresentationUniqueKey)
 1492        {
 1441493            return false;
 1494        }
 1495
 1931496        if (query.GroupBySeriesPresentationUniqueKey)
 1497        {
 01498            return false;
 1499        }
 1500
 1931501        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 1502        {
 01503            return false;
 1504        }
 1505
 1931506        if (query.User is null)
 1507        {
 1911508            return false;
 1509        }
 1510
 21511        if (query.IncludeItemTypes.Length == 0)
 1512        {
 11513            return true;
 1514        }
 1515
 11516        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 11517            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 11518            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 11519            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 11520            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 11521            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 1522    }
 1523
 1524    private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinD
 1525    {
 3371526        var orderBy = filter.OrderBy;
 3371527        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1528
 3371529        if (hasSearch)
 1530        {
 01531            orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1532        }
 3371533        else if (orderBy.Count == 0)
 1534        {
 2331535            return query.OrderBy(e => e.SortName);
 1536        }
 1537
 1041538        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1539
 1041540        var firstOrdering = orderBy.FirstOrDefault();
 1041541        if (firstOrdering != default)
 1542        {
 1041543            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 1041544            if (firstOrdering.SortOrder == SortOrder.Ascending)
 1545            {
 1031546                orderedQuery = query.OrderBy(expression);
 1547            }
 1548            else
 1549            {
 11550                orderedQuery = query.OrderByDescending(expression);
 1551            }
 1552
 1041553            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 1554            {
 01555                if (firstOrdering.SortOrder is SortOrder.Ascending)
 1556                {
 01557                    orderedQuery = orderedQuery.ThenBy(e => e.Name);
 1558                }
 1559                else
 1560                {
 01561                    orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
 1562                }
 1563            }
 1564        }
 1565
 2961566        foreach (var item in orderBy.Skip(1))
 1567        {
 441568            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 441569            if (item.SortOrder == SortOrder.Ascending)
 1570            {
 441571                orderedQuery = orderedQuery!.ThenBy(expression);
 1572            }
 1573            else
 1574            {
 01575                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1576            }
 1577        }
 1578
 1041579        return orderedQuery ?? query;
 1580    }
 1581
 1582    private IQueryable<BaseItemEntity> TranslateQuery(
 1583        IQueryable<BaseItemEntity> baseQuery,
 1584        JellyfinDbContext context,
 1585        InternalItemsQuery filter)
 1586    {
 1587        const int HDWidth = 1200;
 1588        const int UHDWidth = 3800;
 1589        const int UHDHeight = 2100;
 1590
 3371591        var minWidth = filter.MinWidth;
 3371592        var maxWidth = filter.MaxWidth;
 3371593        var now = DateTime.UtcNow;
 1594
 3371595        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 1596        {
 01597            bool includeSD = false;
 01598            bool includeHD = false;
 01599            bool include4K = false;
 1600
 01601            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 1602            {
 01603                includeSD = true;
 1604            }
 1605
 01606            if (filter.IsHD.HasValue && filter.IsHD.Value)
 1607            {
 01608                includeHD = true;
 1609            }
 1610
 01611            if (filter.Is4K.HasValue && filter.Is4K.Value)
 1612            {
 01613                include4K = true;
 1614            }
 1615
 01616            baseQuery = baseQuery.Where(e =>
 01617                (includeSD && e.Width < HDWidth) ||
 01618                (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
 01619                (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
 1620        }
 1621
 3371622        if (minWidth.HasValue)
 1623        {
 01624            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1625        }
 1626
 3371627        if (filter.MinHeight.HasValue)
 1628        {
 01629            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1630        }
 1631
 3371632        if (maxWidth.HasValue)
 1633        {
 01634            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1635        }
 1636
 3371637        if (filter.MaxHeight.HasValue)
 1638        {
 01639            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1640        }
 1641
 3371642        if (filter.IsLocked.HasValue)
 1643        {
 481644            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1645        }
 1646
 3371647        var tags = filter.Tags.ToList();
 3371648        var excludeTags = filter.ExcludeTags.ToList();
 1649
 3371650        if (filter.IsMovie == true)
 1651        {
 01652            if (filter.IncludeItemTypes.Length == 0
 01653                || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01654                || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 1655            {
 01656                baseQuery = baseQuery.Where(e => e.IsMovie);
 1657            }
 1658        }
 3371659        else if (filter.IsMovie.HasValue)
 1660        {
 01661            baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
 1662        }
 1663
 3371664        if (filter.IsSeries.HasValue)
 1665        {
 01666            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1667        }
 1668
 3371669        if (filter.IsSports.HasValue)
 1670        {
 01671            if (filter.IsSports.Value)
 1672            {
 01673                tags.Add("Sports");
 1674            }
 1675            else
 1676            {
 01677                excludeTags.Add("Sports");
 1678            }
 1679        }
 1680
 3371681        if (filter.IsNews.HasValue)
 1682        {
 01683            if (filter.IsNews.Value)
 1684            {
 01685                tags.Add("News");
 1686            }
 1687            else
 1688            {
 01689                excludeTags.Add("News");
 1690            }
 1691        }
 1692
 3371693        if (filter.IsKids.HasValue)
 1694        {
 01695            if (filter.IsKids.Value)
 1696            {
 01697                tags.Add("Kids");
 1698            }
 1699            else
 1700            {
 01701                excludeTags.Add("Kids");
 1702            }
 1703        }
 1704
 3371705        if (!string.IsNullOrEmpty(filter.SearchTerm))
 1706        {
 01707            var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
 01708            var originalSearchTerm = filter.SearchTerm.ToLower();
 01709            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 1710            {
 01711                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 01712                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 1713            }
 1714            else
 1715            {
 01716                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 1717            }
 1718        }
 1719
 3371720        if (filter.IsFolder.HasValue)
 1721        {
 211722            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1723        }
 1724
 3371725        var includeTypes = filter.IncludeItemTypes;
 1726
 1727        // Only specify excluded types if no included types are specified
 3371728        if (filter.IncludeItemTypes.Length == 0)
 1729        {
 2261730            var excludeTypes = filter.ExcludeItemTypes;
 2261731            if (excludeTypes.Length == 1)
 1732            {
 01733                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 1734                {
 01735                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 1736                }
 1737            }
 2261738            else if (excludeTypes.Length > 1)
 1739            {
 01740                var excludeTypeName = new List<string>();
 01741                foreach (var excludeType in excludeTypes)
 1742                {
 01743                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 1744                    {
 01745                        excludeTypeName.Add(baseItemKindName!);
 1746                    }
 1747                }
 1748
 01749                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 1750            }
 1751        }
 1752        else
 1753        {
 1111754            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 1111755            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1756        }
 1757
 3371758        if (filter.ChannelIds.Count > 0)
 1759        {
 01760            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1761        }
 1762
 3371763        if (!filter.ParentId.IsEmpty())
 1764        {
 1441765            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1766        }
 1767
 3371768        if (!string.IsNullOrWhiteSpace(filter.Path))
 1769        {
 01770            var pathToQuery = GetPathToSave(filter.Path);
 01771            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 1772        }
 1773
 3371774        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1775        {
 01776            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1777        }
 1778
 3371779        if (filter.MinCommunityRating.HasValue)
 1780        {
 01781            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1782        }
 1783
 3371784        if (filter.MinIndexNumber.HasValue)
 1785        {
 01786            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1787        }
 1788
 3371789        if (filter.MinParentAndIndexNumber.HasValue)
 1790        {
 01791            baseQuery = baseQuery
 01792                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1793        }
 1794
 3371795        if (filter.MinDateCreated.HasValue)
 1796        {
 01797            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1798        }
 1799
 3371800        if (filter.MinDateLastSaved.HasValue)
 1801        {
 01802            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1803        }
 1804
 3371805        if (filter.MinDateLastSavedForUser.HasValue)
 1806        {
 01807            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1808        }
 1809
 3371810        if (filter.IndexNumber.HasValue)
 1811        {
 01812            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1813        }
 1814
 3371815        if (filter.ParentIndexNumber.HasValue)
 1816        {
 01817            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1818        }
 1819
 3371820        if (filter.ParentIndexNumberNotEquals.HasValue)
 1821        {
 01822            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1823        }
 1824
 3371825        var minEndDate = filter.MinEndDate;
 3371826        var maxEndDate = filter.MaxEndDate;
 1827
 3371828        if (filter.HasAired.HasValue)
 1829        {
 01830            if (filter.HasAired.Value)
 1831            {
 01832                maxEndDate = DateTime.UtcNow;
 1833            }
 1834            else
 1835            {
 01836                minEndDate = DateTime.UtcNow;
 1837            }
 1838        }
 1839
 3371840        if (minEndDate.HasValue)
 1841        {
 01842            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1843        }
 1844
 3371845        if (maxEndDate.HasValue)
 1846        {
 01847            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1848        }
 1849
 3371850        if (filter.MinStartDate.HasValue)
 1851        {
 01852            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1853        }
 1854
 3371855        if (filter.MaxStartDate.HasValue)
 1856        {
 01857            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1858        }
 1859
 3371860        if (filter.MinPremiereDate.HasValue)
 1861        {
 01862            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1863        }
 1864
 3371865        if (filter.MaxPremiereDate.HasValue)
 1866        {
 01867            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1868        }
 1869
 3371870        if (filter.TrailerTypes.Length > 0)
 1871        {
 01872            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 01873            baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
 1874        }
 1875
 3371876        if (filter.IsAiring.HasValue)
 1877        {
 01878            if (filter.IsAiring.Value)
 1879            {
 01880                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 1881            }
 1882            else
 1883            {
 01884                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 1885            }
 1886        }
 1887
 3371888        if (filter.PersonIds.Length > 0)
 1889        {
 01890            var peopleEntityIds = context.BaseItems
 01891                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 01892                .Join(
 01893                    context.Peoples,
 01894                    b => b.Name,
 01895                    p => p.Name,
 01896                    (b, p) => p.Id);
 1897
 01898            baseQuery = baseQuery
 01899                .Where(e => context.PeopleBaseItemMap
 01900                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 1901        }
 1902
 3371903        if (!string.IsNullOrWhiteSpace(filter.Person))
 1904        {
 01905            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 1906        }
 1907
 3371908        if (!string.IsNullOrWhiteSpace(filter.MinSortName))
 1909        {
 1910            // this does not makes sense.
 1911            // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
 1912            // whereClauses.Add("SortName>=@MinSortName");
 1913            // statement?.TryBind("@MinSortName", query.MinSortName);
 1914        }
 1915
 3371916        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 1917        {
 01918            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 1919        }
 1920
 3371921        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 1922        {
 01923            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 1924        }
 1925
 3371926        if (!string.IsNullOrWhiteSpace(filter.Name))
 1927        {
 31928            var cleanName = GetCleanValue(filter.Name);
 31929            baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 1930        }
 1931
 1932        // These are the same, for now
 3371933        var nameContains = filter.NameContains;
 3371934        if (!string.IsNullOrWhiteSpace(nameContains))
 1935        {
 01936            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 1937            {
 01938                nameContains = $"%{nameContains.Trim('%')}%";
 01939                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 1940            }
 1941            else
 1942            {
 01943                baseQuery = baseQuery.Where(e =>
 01944                                    e.CleanName!.Contains(nameContains)
 01945                                    || e.OriginalTitle!.ToLower().Contains(nameContains!));
 1946            }
 1947        }
 1948
 3371949        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 1950        {
 01951            var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
 01952            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
 1953        }
 1954
 3371955        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 1956        {
 01957            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 01958            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
 1959        }
 1960
 3371961        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 1962        {
 01963            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 01964            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
 1965        }
 1966
 3371967        if (filter.ImageTypes.Length > 0)
 1968        {
 1031969            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 1031970            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 1971        }
 1972
 3371973        if (filter.IsLiked.HasValue)
 1974        {
 01975            baseQuery = baseQuery
 01976                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLike
 1977        }
 1978
 3371979        if (filter.IsFavoriteOrLiked.HasValue)
 1980        {
 01981            baseQuery = baseQuery
 01982                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 1983        }
 1984
 3371985        if (filter.IsFavorite.HasValue)
 1986        {
 01987            baseQuery = baseQuery
 01988                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 1989        }
 1990
 3371991        if (filter.IsPlayed.HasValue)
 1992        {
 1993            // We should probably figure this out for all folders, but for right now, this is the only place where we ne
 01994            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
 1995            {
 01996                baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
 01997                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
 01998                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
 01999                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
 2000            }
 2001            else
 2002            {
 02003                baseQuery = baseQuery
 02004                    .Select(e => new
 02005                    {
 02006                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).Firs
 02007                        Item = e
 02008                    })
 02009                    .Where(e => e.IsPlayed == filter.IsPlayed)
 02010                    .Select(f => f.Item);
 2011            }
 2012        }
 2013
 3372014        if (filter.IsResumable.HasValue)
 2015        {
 12016            if (filter.IsResumable.Value)
 2017            {
 12018                baseQuery = baseQuery
 12019                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks >
 2020            }
 2021            else
 2022            {
 02023                baseQuery = baseQuery
 02024                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks =
 2025            }
 2026        }
 2027
 3372028        if (filter.ArtistIds.Length > 0)
 2029        {
 02030            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2031        }
 2032
 3372033        if (filter.AlbumArtistIds.Length > 0)
 2034        {
 02035            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 2036        }
 2037
 3372038        if (filter.ContributingArtistIds.Length > 0)
 2039        {
 02040            var contributingNames = context.BaseItems
 02041                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 02042                .Select(b => b.CleanName);
 2043
 02044            baseQuery = baseQuery.Where(e =>
 02045                e.ItemValues!.Any(ivm =>
 02046                    ivm.ItemValue.Type == ItemValueType.Artist &&
 02047                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 02048                &&
 02049                !e.ItemValues!.Any(ivm =>
 02050                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 02051                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 2052        }
 2053
 3372054        if (filter.AlbumIds.Length > 0)
 2055        {
 02056            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 02057            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 2058        }
 2059
 3372060        if (filter.ExcludeArtistIds.Length > 0)
 2061        {
 02062            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2063        }
 2064
 3372065        if (filter.GenreIds.Count > 0)
 2066        {
 02067            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 2068        }
 2069
 3372070        if (filter.Genres.Count > 0)
 2071        {
 02072            var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValue
 02073            baseQuery = baseQuery
 02074                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 2075        }
 2076
 3372077        if (tags.Count > 0)
 2078        {
 02079            var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, stri
 02080            baseQuery = baseQuery
 02081                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 2082        }
 2083
 3372084        if (excludeTags.Count > 0)
 2085        {
 02086            var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMa
 02087            baseQuery = baseQuery
 02088                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 2089        }
 2090
 3372091        if (filter.StudioIds.Length > 0)
 2092        {
 02093            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 2094        }
 2095
 3372096        if (filter.OfficialRatings.Length > 0)
 2097        {
 02098            baseQuery = baseQuery
 02099                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 2100        }
 2101
 3372102        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 3372103        if (filter.MinParentalRating != null)
 2104        {
 02105            var min = filter.MinParentalRating;
 02106            var minScore = min.Score;
 02107            var minSubScore = min.SubScore ?? 0;
 2108
 02109            minParentalRatingFilter = e =>
 02110                e.InheritedParentalRatingValue == null ||
 02111                e.InheritedParentalRatingValue > minScore ||
 02112                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 2113        }
 2114
 3372115        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 3372116        if (filter.MaxParentalRating != null)
 2117        {
 482118            var max = filter.MaxParentalRating;
 482119            var maxScore = max.Score;
 482120            var maxSubScore = max.SubScore ?? 0;
 2121
 482122            maxParentalRatingFilter = e =>
 482123                e.InheritedParentalRatingValue == null ||
 482124                e.InheritedParentalRatingValue < maxScore ||
 482125                (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
 2126        }
 2127
 3372128        if (filter.HasParentalRating ?? false)
 2129        {
 02130            if (minParentalRatingFilter != null)
 2131            {
 02132                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2133            }
 2134
 02135            if (maxParentalRatingFilter != null)
 2136            {
 02137                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2138            }
 2139        }
 3372140        else if (filter.BlockUnratedItems.Length > 0)
 2141        {
 02142            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 02143            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 2144
 02145            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 2146            {
 02147                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 2148            }
 02149            else if (minParentalRatingFilter != null)
 2150            {
 02151                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 2152            }
 02153            else if (maxParentalRatingFilter != null)
 2154            {
 02155                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 2156            }
 2157            else
 2158            {
 02159                baseQuery = baseQuery.Where(unratedItemFilter);
 2160            }
 2161        }
 3372162        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 2163        {
 482164            if (minParentalRatingFilter != null)
 2165            {
 02166                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2167            }
 2168
 482169            if (maxParentalRatingFilter != null)
 2170            {
 482171                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2172            }
 2173        }
 2892174        else if (!filter.HasParentalRating ?? false)
 2175        {
 02176            baseQuery = baseQuery
 02177                .Where(e => e.InheritedParentalRatingValue == null);
 2178        }
 2179
 3372180        if (filter.HasOfficialRating.HasValue)
 2181        {
 02182            if (filter.HasOfficialRating.Value)
 2183            {
 02184                baseQuery = baseQuery
 02185                    .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
 2186            }
 2187            else
 2188            {
 02189                baseQuery = baseQuery
 02190                    .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
 2191            }
 2192        }
 2193
 3372194        if (filter.HasOverview.HasValue)
 2195        {
 02196            if (filter.HasOverview.Value)
 2197            {
 02198                baseQuery = baseQuery
 02199                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 2200            }
 2201            else
 2202            {
 02203                baseQuery = baseQuery
 02204                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 2205            }
 2206        }
 2207
 3372208        if (filter.HasOwnerId.HasValue)
 2209        {
 02210            if (filter.HasOwnerId.Value)
 2211            {
 02212                baseQuery = baseQuery
 02213                    .Where(e => e.OwnerId != null);
 2214            }
 2215            else
 2216            {
 02217                baseQuery = baseQuery
 02218                    .Where(e => e.OwnerId == null);
 2219            }
 2220        }
 2221
 3372222        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 2223        {
 02224            baseQuery = baseQuery
 02225                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filte
 2226        }
 2227
 3372228        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2229        {
 02230            baseQuery = baseQuery
 02231                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2232        }
 2233
 3372234        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2235        {
 02236            baseQuery = baseQuery
 02237                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2238        }
 2239
 3372240        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 2241        {
 02242            baseQuery = baseQuery
 02243                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == fi
 2244        }
 2245
 3372246        if (filter.HasSubtitles.HasValue)
 2247        {
 02248            baseQuery = baseQuery
 02249                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2250        }
 2251
 3372252        if (filter.HasChapterImages.HasValue)
 2253        {
 02254            baseQuery = baseQuery
 02255                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2256        }
 2257
 3372258        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2259        {
 162260            baseQuery = baseQuery
 162261                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 2262        }
 2263
 3372264        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2265        {
 162266            baseQuery = baseQuery
 162267                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2268        }
 2269
 3372270        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2271        {
 162272            baseQuery = baseQuery
 162273                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2274        }
 2275
 3372276        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2277        {
 162278            baseQuery = baseQuery
 162279                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2280        }
 2281
 3372282        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2283        {
 02284            baseQuery = baseQuery
 02285                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2286        }
 2287
 3372288        if (filter.Years.Length > 0)
 2289        {
 02290            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2291        }
 2292
 3372293        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 3372294        if (isVirtualItem.HasValue)
 2295        {
 222296            baseQuery = baseQuery
 222297                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2298        }
 2299
 3372300        if (filter.IsSpecialSeason.HasValue)
 2301        {
 02302            if (filter.IsSpecialSeason.Value)
 2303            {
 02304                baseQuery = baseQuery
 02305                    .Where(e => e.IndexNumber == 0);
 2306            }
 2307            else
 2308            {
 02309                baseQuery = baseQuery
 02310                    .Where(e => e.IndexNumber != 0);
 2311            }
 2312        }
 2313
 3372314        if (filter.IsUnaired.HasValue)
 2315        {
 02316            if (filter.IsUnaired.Value)
 2317            {
 02318                baseQuery = baseQuery
 02319                    .Where(e => e.PremiereDate >= now);
 2320            }
 2321            else
 2322            {
 02323                baseQuery = baseQuery
 02324                    .Where(e => e.PremiereDate < now);
 2325            }
 2326        }
 2327
 3372328        if (filter.MediaTypes.Length > 0)
 2329        {
 212330            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 212331            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 2332        }
 2333
 3372334        if (filter.ItemIds.Length > 0)
 2335        {
 02336            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2337        }
 2338
 3372339        if (filter.ExcludeItemIds.Length > 0)
 2340        {
 02341            baseQuery = baseQuery
 02342                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 2343        }
 2344
 3372345        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 2346        {
 02347            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 02348            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 2349        }
 2350
 3372351        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 2352        {
 2353            // Allow setting a null or empty value to get all items that have the specified provider set.
 02354            var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArra
 02355            if (includeAny.Length > 0)
 2356            {
 02357                baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
 2358            }
 2359
 02360            var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Ke
 02361            if (includeSelected.Length > 0)
 2362            {
 02363                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 2364            }
 2365        }
 2366
 3372367        if (filter.HasImdbId.HasValue)
 2368        {
 02369            baseQuery = filter.HasImdbId.Value
 02370                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().T
 02371                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().T
 2372        }
 2373
 3372374        if (filter.HasTmdbId.HasValue)
 2375        {
 02376            baseQuery = filter.HasTmdbId.Value
 02377                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().T
 02378                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().T
 2379        }
 2380
 3372381        if (filter.HasTvdbId.HasValue)
 2382        {
 02383            baseQuery = filter.HasTvdbId.Value
 02384                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().T
 02385                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().T
 2386        }
 2387
 3372388        var queryTopParentIds = filter.TopParentIds;
 2389
 3372390        if (queryTopParentIds.Length > 0)
 2391        {
 122392            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 122393            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 122394            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2395            {
 02396                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2397            }
 2398            else
 2399            {
 122400                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2401            }
 2402        }
 2403
 3372404        if (filter.AncestorIds.Length > 0)
 2405        {
 452406            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2407        }
 2408
 3372409        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 2410        {
 02411            baseQuery = baseQuery
 02412                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 2413        }
 2414
 3372415        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2416        {
 02417            baseQuery = baseQuery
 02418                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2419        }
 2420
 3372421        if (filter.ExcludeInheritedTags.Length > 0)
 2422        {
 02423            baseQuery = baseQuery.Where(e =>
 02424                !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f
 02425                && (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
 02426                !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags &
 2427        }
 2428
 3372429        if (filter.IncludeInheritedTags.Length > 0)
 2430        {
 2431            // For seasons and episodes, we also need to check the parent series' tags.
 02432            if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
 2433            {
 02434                baseQuery = baseQuery.Where(e =>
 02435                    e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contain
 02436                    || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValu
 2437            }
 2438
 2439            // A playlist should be accessible to its owner regardless of allowed tags.
 02440            else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
 2441            {
 02442                baseQuery = baseQuery.Where(e =>
 02443                    e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contain
 02444                    || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
 2445                // d        ^^ this is stupid it hate this.
 2446            }
 2447            else
 2448            {
 02449                baseQuery = baseQuery.Where(e =>
 02450                    e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contain
 2451            }
 2452        }
 2453
 3372454        if (filter.SeriesStatuses.Length > 0)
 2455        {
 02456            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 02457            baseQuery = baseQuery
 02458                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 2459        }
 2460
 3372461        if (filter.BoxSetLibraryFolders.Length > 0)
 2462        {
 02463            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 02464            baseQuery = baseQuery
 02465                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 2466        }
 2467
 3372468        if (filter.VideoTypes.Length > 0)
 2469        {
 02470            var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
 02471            baseQuery = baseQuery
 02472                .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
 2473        }
 2474
 3372475        if (filter.Is3D.HasValue)
 2476        {
 02477            if (filter.Is3D.Value)
 2478            {
 02479                baseQuery = baseQuery
 02480                    .Where(e => e.Data!.Contains("Video3DFormat"));
 2481            }
 2482            else
 2483            {
 02484                baseQuery = baseQuery
 02485                    .Where(e => !e.Data!.Contains("Video3DFormat"));
 2486            }
 2487        }
 2488
 3372489        if (filter.IsPlaceHolder.HasValue)
 2490        {
 02491            if (filter.IsPlaceHolder.Value)
 2492            {
 02493                baseQuery = baseQuery
 02494                    .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
 2495            }
 2496            else
 2497            {
 02498                baseQuery = baseQuery
 02499                    .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
 2500            }
 2501        }
 2502
 3372503        if (filter.HasSpecialFeature.HasValue)
 2504        {
 02505            if (filter.HasSpecialFeature.Value)
 2506            {
 02507                baseQuery = baseQuery
 02508                    .Where(e => e.ExtraIds != null);
 2509            }
 2510            else
 2511            {
 02512                baseQuery = baseQuery
 02513                    .Where(e => e.ExtraIds == null);
 2514            }
 2515        }
 2516
 3372517        if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
 2518        {
 02519            if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo
 2520            {
 02521                baseQuery = baseQuery
 02522                    .Where(e => e.ExtraIds != null);
 2523            }
 2524            else
 2525            {
 02526                baseQuery = baseQuery
 02527                    .Where(e => e.ExtraIds == null);
 2528            }
 2529        }
 2530
 3372531        return baseQuery;
 2532    }
 2533
 2534    /// <inheritdoc/>
 2535    public async Task<bool> ItemExistsAsync(Guid id)
 2536    {
 2537        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 2538        await using (dbContext.ConfigureAwait(false))
 2539        {
 2540            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 2541        }
 2542    }
 2543
 2544    /// <inheritdoc/>
 2545    public bool GetIsPlayed(User user, Guid id, bool recursive)
 2546    {
 02547        using var dbContext = _dbProvider.CreateDbContext();
 2548
 02549        if (recursive)
 2550        {
 02551            var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
 2552
 02553            return dbContext.BaseItems
 02554                    .Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
 02555                    .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
 2556        }
 2557
 02558        return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.P
 02559    }
 2560
 2561    private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseIte
 2562    {
 22563        var folderStack = new HashSet<Guid>()
 22564            {
 22565                parentId
 22566            };
 22567        var folderList = new HashSet<Guid>()
 22568            {
 22569                parentId
 22570            };
 2571
 42572        while (folderStack.Count != 0)
 2573        {
 22574            var items = folderStack.ToArray();
 22575            folderStack.Clear();
 22576            var query = dbContext.BaseItems
 22577                .WhereOneOrMany(items, e => e.ParentId!.Value);
 2578
 22579            if (filter != null)
 2580            {
 02581                query = query.Where(filter);
 2582            }
 2583
 42584            foreach (var item in query.Select(e => e.Id).ToArray())
 2585            {
 02586                if (folderList.Add(item))
 2587                {
 02588                    folderStack.Add(item);
 2589                }
 2590            }
 2591        }
 2592
 22593        return folderList;
 2594    }
 2595
 2596    /// <inheritdoc/>
 2597    public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
 2598    {
 02599        using var dbContext = _dbProvider.CreateDbContext();
 2600
 02601        var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 02602            .Where(e => artistNames.Contains(e.Name))
 02603            .ToArray();
 2604
 02605        return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<Mu
 02606    }
 2607}

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)
SaveImages(MediaBrowser.Controller.Entities.BaseItem)
SaveItems(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,System.Threading.CancellationToken)
UpdateOrInsertItems(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,System.Threading.CancellationToken)
RetrieveItem(System.Guid)
Map(Jellyfin.Database.Implementations.Entities.BaseItemEntity,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.IServerApplicationHost,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>)