< 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: 666
Uncovered lines: 640
Coverable lines: 1306
Total lines: 2607
Line coverage: 50.9%
Branch coverage
48%
Covered branches: 356
Total branches: 728
Branch coverage: 48.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 7/22/2025 - 12:11:20 AM Line coverage: 52.3% (611/1167) Branch coverage: 48.8% (328/672) Total lines: 23817/23/2025 - 12:11:37 AM Line coverage: 52.3% (611/1167) Branch coverage: 48.9% (329/672) Total lines: 23817/24/2025 - 12:11:34 AM Line coverage: 52.3% (611/1167) Branch coverage: 49.1% (330/672) Total lines: 23817/26/2025 - 12:11:20 AM Line coverage: 52.3% (611/1167) Branch coverage: 48.8% (328/672) Total lines: 23817/27/2025 - 12:09:49 AM Line coverage: 52.3% (611/1167) Branch coverage: 49.1% (330/672) Total lines: 23817/28/2025 - 12:11:17 AM Line coverage: 52.3% (611/1167) Branch coverage: 48.8% (328/672) Total lines: 23817/29/2025 - 12:11:08 AM Line coverage: 52.4% (611/1166) Branch coverage: 49.1% (330/672) Total lines: 23808/2/2025 - 12:11:30 AM Line coverage: 52.4% (611/1166) Branch coverage: 48.8% (328/672) Total lines: 23808/3/2025 - 12:10:08 AM Line coverage: 52.4% (611/1166) Branch coverage: 49.1% (330/672) Total lines: 23808/8/2025 - 12:11:52 AM Line coverage: 52.2% (611/1169) Branch coverage: 48.9% (330/674) Total lines: 23878/11/2025 - 12:11:38 AM Line coverage: 52.2% (611/1169) Branch coverage: 49.1% (331/674) Total lines: 23878/12/2025 - 12:11:01 AM Line coverage: 52% (611/1173) Branch coverage: 48.9% (330/674) Total lines: 23928/13/2025 - 12:11:40 AM Line coverage: 52% (611/1173) Branch coverage: 48.8% (329/674) Total lines: 23928/14/2025 - 12:11:05 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24528/16/2025 - 12:10:44 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.2% (328/694) Total lines: 24528/17/2025 - 12:10:48 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: 2607 7/22/2025 - 12:11:20 AM Line coverage: 52.3% (611/1167) Branch coverage: 48.8% (328/672) Total lines: 23817/23/2025 - 12:11:37 AM Line coverage: 52.3% (611/1167) Branch coverage: 48.9% (329/672) Total lines: 23817/24/2025 - 12:11:34 AM Line coverage: 52.3% (611/1167) Branch coverage: 49.1% (330/672) Total lines: 23817/26/2025 - 12:11:20 AM Line coverage: 52.3% (611/1167) Branch coverage: 48.8% (328/672) Total lines: 23817/27/2025 - 12:09:49 AM Line coverage: 52.3% (611/1167) Branch coverage: 49.1% (330/672) Total lines: 23817/28/2025 - 12:11:17 AM Line coverage: 52.3% (611/1167) Branch coverage: 48.8% (328/672) Total lines: 23817/29/2025 - 12:11:08 AM Line coverage: 52.4% (611/1166) Branch coverage: 49.1% (330/672) Total lines: 23808/2/2025 - 12:11:30 AM Line coverage: 52.4% (611/1166) Branch coverage: 48.8% (328/672) Total lines: 23808/3/2025 - 12:10:08 AM Line coverage: 52.4% (611/1166) Branch coverage: 49.1% (330/672) Total lines: 23808/8/2025 - 12:11:52 AM Line coverage: 52.2% (611/1169) Branch coverage: 48.9% (330/674) Total lines: 23878/11/2025 - 12:11:38 AM Line coverage: 52.2% (611/1169) Branch coverage: 49.1% (331/674) Total lines: 23878/12/2025 - 12:11:01 AM Line coverage: 52% (611/1173) Branch coverage: 48.9% (330/674) Total lines: 23928/13/2025 - 12:11:40 AM Line coverage: 52% (611/1173) Branch coverage: 48.8% (329/674) Total lines: 23928/14/2025 - 12:11:05 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24528/16/2025 - 12:10:44 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.2% (328/694) Total lines: 24528/17/2025 - 12:10:48 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: 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%26835%
GetItemList(...)100%11100%
GetLatestItemList(...)0%4260%
GetNextUpSeriesKeys(...)0%620%
ApplyGroupingFilter(...)75%9871.42%
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.6%3109835237.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    {
 16163        using var context = _dbProvider.CreateDbContext();
 16164        using var transaction = context.Database.BeginTransaction();
 165
 16166        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 167        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 16168        context.SaveChanges();
 169
 16170        transaction.Commit();
 32171    }
 172
 173    /// <inheritdoc />
 174    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 175    {
 17176        ArgumentNullException.ThrowIfNull(filter);
 17177        PrepareFilterQuery(filter);
 178
 17179        using var context = _dbProvider.CreateDbContext();
 17180        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 17181    }
 182
 183    /// <inheritdoc />
 184    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 185    {
 0186        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 187    }
 188
 189    /// <inheritdoc />
 190    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 191    {
 0192        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 193    }
 194
 195    /// <inheritdoc />
 196    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 197    {
 0198        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 199    }
 200
 201    /// <inheritdoc />
 202    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 203    {
 0204        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 205    }
 206
 207    /// <inheritdoc />
 208    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 209    {
 0210        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 211    }
 212
 213    /// <inheritdoc />
 214    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 215    {
 0216        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 217    }
 218
 219    /// <inheritdoc />
 220    public IReadOnlyList<string> GetStudioNames()
 221    {
 17222        return GetItemValueNames(_getStudiosValueTypes, [], []);
 223    }
 224
 225    /// <inheritdoc />
 226    public IReadOnlyList<string> GetAllArtistNames()
 227    {
 17228        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 229    }
 230
 231    /// <inheritdoc />
 232    public IReadOnlyList<string> GetMusicGenreNames()
 233    {
 17234        return GetItemValueNames(
 17235            _getGenreValueTypes,
 17236            _itemTypeLookup.MusicGenreTypes,
 17237            []);
 238    }
 239
 240    /// <inheritdoc />
 241    public IReadOnlyList<string> GetGenreNames()
 242    {
 17243        return GetItemValueNames(
 17244            _getGenreValueTypes,
 17245            [],
 17246            _itemTypeLookup.MusicGenreTypes);
 247    }
 248
 249    /// <inheritdoc />
 250    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
 251    {
 1252        ArgumentNullException.ThrowIfNull(filter);
 1253        if (!filter.EnableTotalRecordCount || (!filter.Limit.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);
 278
 0279        result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDe
 0280        result.StartIndex = filter.StartIndex ?? 0;
 0281        return result;
 0282    }
 283
 284    /// <inheritdoc />
 285    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
 286    {
 307287        ArgumentNullException.ThrowIfNull(filter);
 307288        PrepareFilterQuery(filter);
 289
 307290        using var context = _dbProvider.CreateDbContext();
 307291        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 292
 307293        dbQuery = TranslateQuery(dbQuery, context, filter);
 294
 307295        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 307296        dbQuery = ApplyQueryPaging(dbQuery, filter);
 297
 307298        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializ
 307299    }
 300
 301    /// <inheritdoc/>
 302    public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 303    {
 0304        ArgumentNullException.ThrowIfNull(filter);
 0305        PrepareFilterQuery(filter);
 306
 307        // Early exit if collection type is not tvshows or music
 0308        if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music)
 309        {
 0310            return Array.Empty<BaseItem>();
 311        }
 312
 0313        using var context = _dbProvider.CreateDbContext();
 314
 315        // Subquery to group by SeriesNames/Album and get the max Date Created for each group.
 0316        var subquery = PrepareItemQuery(context, filter);
 0317        subquery = TranslateQuery(subquery, context, filter);
 0318        var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album)
 0319            .Select(g => new
 0320            {
 0321                Key = g.Key,
 0322                MaxDateCreated = g.Max(a => a.DateCreated)
 0323            })
 0324            .OrderByDescending(g => g.MaxDateCreated)
 0325            .Select(g => g);
 326
 0327        if (filter.Limit.HasValue)
 328        {
 0329            subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
 330        }
 331
 0332        filter.Limit = null;
 333
 0334        var mainquery = PrepareItemQuery(context, filter);
 0335        mainquery = TranslateQuery(mainquery, context, filter);
 0336        mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
 0337        mainquery = ApplyGroupingFilter(context, mainquery, filter);
 0338        mainquery = ApplyQueryPaging(mainquery, filter);
 339
 0340        return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserial
 0341    }
 342
 343    /// <inheritdoc />
 344    public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
 345    {
 0346        ArgumentNullException.ThrowIfNull(filter);
 0347        ArgumentNullException.ThrowIfNull(filter.User);
 348
 0349        using var context = _dbProvider.CreateDbContext();
 350
 0351        var query = context.BaseItems
 0352            .AsNoTracking()
 0353            .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
 0354            .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
 0355            .Join(
 0356                context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
 0357                i => new { UserId = filter.User.Id, ItemId = i.Id },
 0358                u => new { UserId = u.UserId, ItemId = u.ItemId },
 0359                (entity, data) => new { Item = entity, UserData = data })
 0360            .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
 0361            .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
 0362            .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
 0363            .OrderByDescending(g => g.LastPlayedDate)
 0364            .Select(g => g.Key!);
 365
 0366        if (filter.Limit.HasValue)
 367        {
 0368            query = query.Take(filter.Limit.Value);
 369        }
 370
 0371        return query.ToArray();
 0372    }
 373
 374    private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery
 375    {
 376        // This whole block is needed to filter duplicate entries on request
 377        // for the time being it cannot be used because it would destroy the ordering
 378        // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions b
 379        // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
 380
 324381        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 324382        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 383        {
 0384            var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(
 0385            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 386        }
 324387        else if (enableGroupByPresentationUniqueKey)
 388        {
 1389            var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!
 1390            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 391        }
 323392        else if (filter.GroupBySeriesPresentationUniqueKey)
 393        {
 0394            var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e
 0395            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 396        }
 397        else
 398        {
 323399            dbQuery = dbQuery.Distinct();
 400        }
 401
 324402        dbQuery = ApplyOrder(dbQuery, filter);
 403
 324404        dbQuery = ApplyNavigations(dbQuery, filter);
 405
 324406        return dbQuery;
 407    }
 408
 409    private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 410    {
 324411        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 324412           .Include(e => e.Provider)
 324413           .Include(e => e.LockedFields)
 324414           .Include(e => e.UserData);
 415
 324416        if (filter.DtoOptions.EnableImages)
 417        {
 324418            dbQuery = dbQuery.Include(e => e.Images);
 419        }
 420
 324421        return dbQuery;
 422    }
 423
 424    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 425    {
 324426        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 427        {
 106428            var offset = filter.StartIndex ?? 0;
 429
 106430            if (offset > 0)
 431            {
 0432                dbQuery = dbQuery.Skip(offset);
 433            }
 434
 106435            if (filter.Limit.HasValue)
 436            {
 106437                dbQuery = dbQuery.Take(filter.Limit.Value);
 438            }
 439        }
 440
 324441        return dbQuery;
 442    }
 443
 444    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 445    {
 17446        dbQuery = TranslateQuery(dbQuery, context, filter);
 17447        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 17448        dbQuery = ApplyQueryPaging(dbQuery, filter);
 17449        return dbQuery;
 450    }
 451
 452    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 453    {
 393454        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 393455        dbQuery = dbQuery.AsSingleQuery();
 456
 393457        return dbQuery;
 458    }
 459
 460    /// <inheritdoc/>
 461    public int GetCount(InternalItemsQuery filter)
 462    {
 0463        ArgumentNullException.ThrowIfNull(filter);
 464        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0465        PrepareFilterQuery(filter);
 466
 0467        using var context = _dbProvider.CreateDbContext();
 0468        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 469
 0470        return dbQuery.Count();
 0471    }
 472
 473    /// <inheritdoc />
 474    public ItemCounts GetItemCounts(InternalItemsQuery filter)
 475    {
 0476        ArgumentNullException.ThrowIfNull(filter);
 477        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0478        PrepareFilterQuery(filter);
 479
 0480        using var context = _dbProvider.CreateDbContext();
 0481        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 482
 0483        var counts = dbQuery
 0484            .GroupBy(x => x.Type)
 0485            .Select(x => new { x.Key, Count = x.Count() })
 0486            .ToArray();
 487
 0488        var lookup = _itemTypeLookup.BaseItemKindNames;
 0489        var result = new ItemCounts();
 0490        foreach (var count in counts)
 491        {
 0492            if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
 493            {
 0494                result.AlbumCount = count.Count;
 495            }
 0496            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
 497            {
 0498                result.ArtistCount = count.Count;
 499            }
 0500            else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
 501            {
 0502                result.EpisodeCount = count.Count;
 503            }
 0504            else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
 505            {
 0506                result.MovieCount = count.Count;
 507            }
 0508            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
 509            {
 0510                result.MusicVideoCount = count.Count;
 511            }
 0512            else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
 513            {
 0514                result.ProgramCount = count.Count;
 515            }
 0516            else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
 517            {
 0518                result.SeriesCount = count.Count;
 519            }
 0520            else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
 521            {
 0522                result.SongCount = count.Count;
 523            }
 0524            else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
 525            {
 0526                result.TrailerCount = count.Count;
 527            }
 528        }
 529
 0530        return result;
 0531    }
 532
 533#pragma warning disable CA1307 // Specify StringComparison for clarity
 534    /// <summary>
 535    /// Gets the type.
 536    /// </summary>
 537    /// <param name="typeName">Name of the type.</param>
 538    /// <returns>Type.</returns>
 539    /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
 540    private static Type? GetType(string typeName)
 541    {
 104542        ArgumentException.ThrowIfNullOrEmpty(typeName);
 543
 544        // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static 
 545        // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are 
 104546        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 104547            .Select(a => a.GetType(k))
 104548            .FirstOrDefault(t => t is not null));
 549    }
 550
 551    /// <inheritdoc  />
 552    public void SaveImages(BaseItemDto item)
 553    {
 0554        ArgumentNullException.ThrowIfNull(item);
 555
 0556        var images = item.ImageInfos.Select(e => Map(item.Id, e));
 0557        using var context = _dbProvider.CreateDbContext();
 558
 0559        if (!context.BaseItems.Any(bi => bi.Id == item.Id))
 560        {
 0561            _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
 0562            return;
 563        }
 564
 0565        context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
 0566        context.BaseItemImageInfos.AddRange(images);
 0567        context.SaveChanges();
 0568    }
 569
 570    /// <inheritdoc  />
 571    public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 572    {
 112573        UpdateOrInsertItems(items, cancellationToken);
 112574    }
 575
 576    /// <inheritdoc cref="IItemRepository"/>
 577    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 578    {
 112579        ArgumentNullException.ThrowIfNull(items);
 112580        cancellationToken.ThrowIfCancellationRequested();
 581
 112582        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 448583        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
 584        {
 112585            var ancestorIds = item.SupportsAncestors ?
 112586                item.GetAncestorIds().Distinct().ToList() :
 112587                null;
 588
 112589            var topParent = item.GetTopParent();
 590
 112591            var userdataKey = item.GetUserDataKeys();
 112592            var inheritedTags = item.GetInheritedTags();
 593
 112594            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 595        }
 596
 112597        using var context = _dbProvider.CreateDbContext();
 112598        using var transaction = context.Database.BeginTransaction();
 599
 112600        var ids = tuples.Select(f => f.Item.Id).ToArray();
 112601        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 112602        var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
 603
 448604        foreach (var item in tuples)
 605        {
 112606            var entity = Map(item.Item);
 607            // TODO: refactor this "inconsistency"
 112608            entity.TopParentId = item.TopParent?.Id;
 609
 112610            if (!existingItems.Any(e => e == entity.Id))
 611            {
 59612                context.BaseItems.Add(entity);
 613            }
 614            else
 615            {
 53616                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 53617                context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 618
 53619                if (entity.Images is { Count: > 0 })
 620                {
 0621                    context.BaseItemImageInfos.AddRange(entity.Images);
 622                }
 623
 53624                context.BaseItems.Attach(entity).State = EntityState.Modified;
 625            }
 626        }
 627
 112628        context.SaveChanges();
 629
 342630        foreach (var item in newItems)
 631        {
 632            // reattach old userData entries
 59633            var userKeys = item.UserDataKey.ToArray();
 59634            var retentionDate = (DateTime?)null;
 59635            context.UserData
 59636                .Where(e => e.ItemId == PlaceholderId)
 59637                .Where(e => userKeys.Contains(e.CustomDataKey))
 59638                .ExecuteUpdate(e => e
 59639                    .SetProperty(f => f.ItemId, item.Item.Id)
 59640                    .SetProperty(f => f.RetentionDate, retentionDate));
 641        }
 642
 112643        var itemValueMaps = tuples
 112644            .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 112645            .ToArray();
 112646        var allListedItemValues = itemValueMaps
 112647            .SelectMany(f => f.Values)
 112648            .Distinct()
 112649            .ToArray();
 112650        var existingValues = context.ItemValues
 112651            .Select(e => new
 112652            {
 112653                item = e,
 112654                Key = e.Type + "+" + e.Value
 112655            })
 112656            .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
 112657            .Select(e => e.item)
 112658            .ToArray();
 112659        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 112660        {
 112661            CleanValue = GetCleanValue(f.Value),
 112662            ItemValueId = Guid.NewGuid(),
 112663            Type = f.MagicNumber,
 112664            Value = f.Value
 112665        }).ToArray();
 112666        context.ItemValues.AddRange(missingItemValues);
 112667        context.SaveChanges();
 668
 112669        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 112670        var valueMap = itemValueMaps
 112671            .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type =
 112672            .ToArray();
 673
 112674        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 675
 448676        foreach (var item in valueMap)
 677        {
 112678            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 224679            foreach (var itemValue in item.Values)
 680            {
 0681                var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
 0682                if (existingItem is null)
 683                {
 0684                    context.ItemValuesMap.Add(new ItemValueMap()
 0685                    {
 0686                        Item = null!,
 0687                        ItemId = item.Item.Id,
 0688                        ItemValue = null!,
 0689                        ItemValueId = itemValue.ItemValueId
 0690                    });
 691                }
 692                else
 693                {
 694                    // map exists, remove from list so its been handled.
 0695                    itemMappedValues.Remove(existingItem);
 696                }
 697            }
 698
 699            // all still listed values are not in the new list so remove them.
 112700            context.ItemValuesMap.RemoveRange(itemMappedValues);
 701        }
 702
 112703        context.SaveChanges();
 704
 448705        foreach (var item in tuples)
 706        {
 112707            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 708            {
 112709                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 112710                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 278711                foreach (var ancestorId in validAncestorIds)
 712                {
 27713                    var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
 27714                    if (existingAncestorId is null)
 715                    {
 23716                        context.AncestorIds.Add(new AncestorId()
 23717                        {
 23718                            ParentItemId = ancestorId,
 23719                            ItemId = item.Item.Id,
 23720                            Item = null!,
 23721                            ParentItem = null!
 23722                        });
 723                    }
 724                    else
 725                    {
 4726                        existingAncestorIds.Remove(existingAncestorId);
 727                    }
 728                }
 729
 112730                context.AncestorIds.RemoveRange(existingAncestorIds);
 731            }
 732        }
 733
 112734        context.SaveChanges();
 112735        transaction.Commit();
 224736    }
 737
 738    /// <inheritdoc  />
 739    public BaseItemDto? RetrieveItem(Guid id)
 740    {
 86741        if (id.IsEmpty())
 742        {
 0743            throw new ArgumentException("Guid can't be empty", nameof(id));
 744        }
 745
 86746        using var context = _dbProvider.CreateDbContext();
 86747        var dbQuery = PrepareItemQuery(context, new()
 86748        {
 86749            DtoOptions = new()
 86750            {
 86751                EnableImages = true
 86752            }
 86753        });
 86754        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 86755            .Include(e => e.Provider)
 86756            .Include(e => e.LockedFields)
 86757            .Include(e => e.UserData)
 86758            .Include(e => e.Images);
 759
 86760        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 86761        if (item is null)
 762        {
 86763            return null;
 764        }
 765
 0766        return DeserializeBaseItem(item);
 86767    }
 768
 769    /// <summary>
 770    /// Maps a Entity to the DTO.
 771    /// </summary>
 772    /// <param name="entity">The entity.</param>
 773    /// <param name="dto">The dto base instance.</param>
 774    /// <param name="appHost">The Application server Host.</param>
 775    /// <param name="logger">The applogger.</param>
 776    /// <returns>The dto to map.</returns>
 777    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 778    {
 52779        dto.Id = entity.Id;
 52780        dto.ParentId = entity.ParentId.GetValueOrDefault();
 52781        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 52782        dto.EndDate = entity.EndDate;
 52783        dto.CommunityRating = entity.CommunityRating;
 52784        dto.CustomRating = entity.CustomRating;
 52785        dto.IndexNumber = entity.IndexNumber;
 52786        dto.IsLocked = entity.IsLocked;
 52787        dto.Name = entity.Name;
 52788        dto.OfficialRating = entity.OfficialRating;
 52789        dto.Overview = entity.Overview;
 52790        dto.ParentIndexNumber = entity.ParentIndexNumber;
 52791        dto.PremiereDate = entity.PremiereDate;
 52792        dto.ProductionYear = entity.ProductionYear;
 52793        dto.SortName = entity.SortName;
 52794        dto.ForcedSortName = entity.ForcedSortName;
 52795        dto.RunTimeTicks = entity.RunTimeTicks;
 52796        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 52797        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 52798        dto.IsInMixedFolder = entity.IsInMixedFolder;
 52799        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 52800        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 52801        dto.CriticRating = entity.CriticRating;
 52802        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 52803        dto.OriginalTitle = entity.OriginalTitle;
 52804        dto.Album = entity.Album;
 52805        dto.LUFS = entity.LUFS;
 52806        dto.NormalizationGain = entity.NormalizationGain;
 52807        dto.IsVirtualItem = entity.IsVirtualItem;
 52808        dto.ExternalSeriesId = entity.ExternalSeriesId;
 52809        dto.Tagline = entity.Tagline;
 52810        dto.TotalBitrate = entity.TotalBitrate;
 52811        dto.ExternalId = entity.ExternalId;
 52812        dto.Size = entity.Size;
 52813        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 52814        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 52815        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 52816        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 52817        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 52818        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 52819        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 52820        dto.Width = entity.Width.GetValueOrDefault();
 52821        dto.Height = entity.Height.GetValueOrDefault();
 52822        dto.UserData = entity.UserData;
 823
 52824        if (entity.Provider is not null)
 825        {
 52826            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 827        }
 828
 52829        if (entity.ExtraType is not null)
 830        {
 0831            dto.ExtraType = (ExtraType)entity.ExtraType;
 832        }
 833
 52834        if (entity.LockedFields is not null)
 835        {
 52836            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 837        }
 838
 52839        if (entity.Audio is not null)
 840        {
 0841            dto.Audio = (ProgramAudio)entity.Audio;
 842        }
 843
 52844        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 52845        dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
 52846        dto.Studios = entity.Studios?.Split('|') ?? [];
 52847        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 848
 52849        if (dto is IHasProgramAttributes hasProgramAttributes)
 850        {
 0851            hasProgramAttributes.IsMovie = entity.IsMovie;
 0852            hasProgramAttributes.IsSeries = entity.IsSeries;
 0853            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0854            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 855        }
 856
 52857        if (dto is LiveTvChannel liveTvChannel)
 858        {
 0859            liveTvChannel.ServiceName = entity.ExternalServiceId;
 860        }
 861
 52862        if (dto is Trailer trailer)
 863        {
 0864            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 865        }
 866
 52867        if (dto is Video video)
 868        {
 0869            video.PrimaryVersionId = entity.PrimaryVersionId;
 870        }
 871
 52872        if (dto is IHasSeries hasSeriesName)
 873        {
 0874            hasSeriesName.SeriesName = entity.SeriesName;
 0875            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0876            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 877        }
 878
 52879        if (dto is Episode episode)
 880        {
 0881            episode.SeasonName = entity.SeasonName;
 0882            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 883        }
 884
 52885        if (dto is IHasArtist hasArtists)
 886        {
 0887            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 888        }
 889
 52890        if (dto is IHasAlbumArtist hasAlbumArtists)
 891        {
 0892            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 893        }
 894
 52895        if (dto is LiveTvProgram program)
 896        {
 0897            program.ShowId = entity.ShowId;
 898        }
 899
 52900        if (entity.Images is not null)
 901        {
 52902            dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
 903        }
 904
 905        // dto.Type = entity.Type;
 906        // dto.Data = entity.Data;
 907        // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
 52908        if (dto is IHasStartDate hasStartDate)
 909        {
 0910            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 911        }
 912
 913        // Fields that are present in the DB but are never actually used
 914        // dto.UnratedType = entity.UnratedType;
 915        // dto.TopParentId = entity.TopParentId;
 916        // dto.CleanName = entity.CleanName;
 917        // dto.UserDataKey = entity.UserDataKey;
 918
 52919        if (dto is Folder folder)
 920        {
 52921            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 922        }
 923
 52924        return dto;
 925    }
 926
 927    /// <summary>
 928    /// Maps a Entity to the DTO.
 929    /// </summary>
 930    /// <param name="dto">The entity.</param>
 931    /// <returns>The dto to map.</returns>
 932    public BaseItemEntity Map(BaseItemDto dto)
 933    {
 112934        var dtoType = dto.GetType();
 112935        var entity = new BaseItemEntity()
 112936        {
 112937            Type = dtoType.ToString(),
 112938            Id = dto.Id
 112939        };
 940
 112941        if (TypeRequiresDeserialization(dtoType))
 942        {
 91943            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 944        }
 945
 112946        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 112947        entity.Path = GetPathToSave(dto.Path);
 112948        entity.EndDate = dto.EndDate;
 112949        entity.CommunityRating = dto.CommunityRating;
 112950        entity.CustomRating = dto.CustomRating;
 112951        entity.IndexNumber = dto.IndexNumber;
 112952        entity.IsLocked = dto.IsLocked;
 112953        entity.Name = dto.Name;
 112954        entity.CleanName = GetCleanValue(dto.Name);
 112955        entity.OfficialRating = dto.OfficialRating;
 112956        entity.Overview = dto.Overview;
 112957        entity.ParentIndexNumber = dto.ParentIndexNumber;
 112958        entity.PremiereDate = dto.PremiereDate;
 112959        entity.ProductionYear = dto.ProductionYear;
 112960        entity.SortName = dto.SortName;
 112961        entity.ForcedSortName = dto.ForcedSortName;
 112962        entity.RunTimeTicks = dto.RunTimeTicks;
 112963        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 112964        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 112965        entity.IsInMixedFolder = dto.IsInMixedFolder;
 112966        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 112967        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 112968        entity.CriticRating = dto.CriticRating;
 112969        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 112970        entity.OriginalTitle = dto.OriginalTitle;
 112971        entity.Album = dto.Album;
 112972        entity.LUFS = dto.LUFS;
 112973        entity.NormalizationGain = dto.NormalizationGain;
 112974        entity.IsVirtualItem = dto.IsVirtualItem;
 112975        entity.ExternalSeriesId = dto.ExternalSeriesId;
 112976        entity.Tagline = dto.Tagline;
 112977        entity.TotalBitrate = dto.TotalBitrate;
 112978        entity.ExternalId = dto.ExternalId;
 112979        entity.Size = dto.Size;
 112980        entity.Genres = string.Join('|', dto.Genres);
 112981        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 112982        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 112983        entity.ChannelId = dto.ChannelId;
 112984        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 112985        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 112986        entity.OwnerId = dto.OwnerId.ToString();
 112987        entity.Width = dto.Width;
 112988        entity.Height = dto.Height;
 112989        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 112990        {
 112991            Item = entity,
 112992            ProviderId = e.Key,
 112993            ProviderValue = e.Value
 112994        }).ToList();
 995
 112996        if (dto.Audio.HasValue)
 997        {
 0998            entity.Audio = (ProgramAudioEntity)dto.Audio;
 999        }
 1000
 1121001        if (dto.ExtraType.HasValue)
 1002        {
 01003            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 1004        }
 1005
 1121006        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 1121007        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : n
 1121008        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
 1121009        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
 1121010        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 1121011            .Select(e => new BaseItemMetadataField()
 1121012            {
 1121013                Id = (int)e,
 1121014                Item = entity,
 1121015                ItemId = entity.Id
 1121016            })
 1121017            .ToArray() : null;
 1018
 1121019        if (dto is IHasProgramAttributes hasProgramAttributes)
 1020        {
 01021            entity.IsMovie = hasProgramAttributes.IsMovie;
 01022            entity.IsSeries = hasProgramAttributes.IsSeries;
 01023            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 01024            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 1025        }
 1026
 1121027        if (dto is LiveTvChannel liveTvChannel)
 1028        {
 01029            entity.ExternalServiceId = liveTvChannel.ServiceName;
 1030        }
 1031
 1121032        if (dto is Video video)
 1033        {
 01034            entity.PrimaryVersionId = video.PrimaryVersionId;
 1035        }
 1036
 1121037        if (dto is IHasSeries hasSeriesName)
 1038        {
 01039            entity.SeriesName = hasSeriesName.SeriesName;
 01040            entity.SeriesId = hasSeriesName.SeriesId;
 01041            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 1042        }
 1043
 1121044        if (dto is Episode episode)
 1045        {
 01046            entity.SeasonName = episode.SeasonName;
 01047            entity.SeasonId = episode.SeasonId;
 1048        }
 1049
 1121050        if (dto is IHasArtist hasArtists)
 1051        {
 01052            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
 1053        }
 1054
 1121055        if (dto is IHasAlbumArtist hasAlbumArtists)
 1056        {
 01057            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 1058        }
 1059
 1121060        if (dto is LiveTvProgram program)
 1061        {
 01062            entity.ShowId = program.ShowId;
 1063        }
 1064
 1121065        if (dto.ImageInfos is not null)
 1066        {
 1121067            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 1068        }
 1069
 1121070        if (dto is Trailer trailer)
 1071        {
 01072            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 01073            {
 01074                Id = (int)e,
 01075                Item = entity,
 01076                ItemId = entity.Id
 01077            }).ToArray() ?? [];
 1078        }
 1079
 1080        // dto.Type = entity.Type;
 1081        // dto.Data = entity.Data;
 1121082        entity.MediaType = dto.MediaType.ToString();
 1121083        if (dto is IHasStartDate hasStartDate)
 1084        {
 01085            entity.StartDate = hasStartDate.StartDate;
 1086        }
 1087
 1121088        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 1089
 1090        // Fields that are present in the DB but are never actually used
 1091        // dto.UserDataKey = entity.UserDataKey;
 1092
 1121093        if (dto is Folder folder)
 1094        {
 1121095            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 1121096            entity.IsFolder = folder.IsFolder;
 1097        }
 1098
 1121099        return entity;
 1100    }
 1101
 1102    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 1103    {
 681104        using var context = _dbProvider.CreateDbContext();
 1105
 681106        var query = context.ItemValuesMap
 681107            .AsNoTracking()
 681108            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 681109        if (withItemTypes.Count > 0)
 1110        {
 171111            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 1112        }
 1113
 681114        if (excludeItemTypes.Count > 0)
 1115        {
 171116            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 1117        }
 1118
 1119        // query = query.DistinctBy(e => e.CleanValue);
 681120        return query.Select(e => e.ItemValue)
 681121            .GroupBy(e => e.CleanValue)
 681122            .Select(e => e.First().Value)
 681123            .ToArray();
 681124    }
 1125
 1126    private static bool TypeRequiresDeserialization(Type type)
 1127    {
 1641128        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 1129    }
 1130
 1131    private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 1132    {
 521133        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 521134        if (_serverConfigurationManager?.Configuration is null)
 1135        {
 01136            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 1137        }
 1138
 521139        var typeToSerialise = GetType(baseItemEntity.Type);
 521140        return BaseItemRepository.DeserializeBaseItem(
 521141            baseItemEntity,
 521142            _logger,
 521143            _appHost,
 521144            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 1145    }
 1146
 1147    /// <summary>
 1148    /// Deserializes a BaseItemEntity and sets all properties.
 1149    /// </summary>
 1150    /// <param name="baseItemEntity">The DB entity.</param>
 1151    /// <param name="logger">Logger.</param>
 1152    /// <param name="appHost">The application server Host.</param>
 1153    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 1154    /// <returns>A mapped BaseItem.</returns>
 1155    /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
 1156    public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost?
 1157    {
 521158        var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.
 521159        BaseItemDto? dto = null;
 521160        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1161        {
 1162            try
 1163            {
 121164                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 121165            }
 01166            catch (JsonException ex)
 1167            {
 01168                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01169            }
 1170        }
 1171
 521172        if (dto is null)
 1173        {
 401174            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1175        }
 1176
 521177        return Map(baseItemEntity, dto, appHost, logger);
 1178    }
 1179
 1180    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 1181    {
 01182        ArgumentNullException.ThrowIfNull(filter);
 1183
 01184        if (!filter.Limit.HasValue)
 1185        {
 01186            filter.EnableTotalRecordCount = false;
 1187        }
 1188
 01189        using var context = _dbProvider.CreateDbContext();
 1190
 01191        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 01192        {
 01193            ExcludeItemTypes = filter.ExcludeItemTypes,
 01194            IncludeItemTypes = filter.IncludeItemTypes,
 01195            MediaTypes = filter.MediaTypes,
 01196            AncestorIds = filter.AncestorIds,
 01197            ItemIds = filter.ItemIds,
 01198            TopParentIds = filter.TopParentIds,
 01199            ParentId = filter.ParentId,
 01200            IsAiring = filter.IsAiring,
 01201            IsMovie = filter.IsMovie,
 01202            IsSports = filter.IsSports,
 01203            IsKids = filter.IsKids,
 01204            IsNews = filter.IsNews,
 01205            IsSeries = filter.IsSeries
 01206        });
 1207
 01208        var itemValuesQuery = context.ItemValues
 01209            .Where(f => itemValueTypes.Contains(f.Type))
 01210            .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
 01211            .Join(
 01212                innerQueryFilter,
 01213                fw => fw.w.ItemId,
 01214                g => g.Id,
 01215                (fw, g) => fw.f.CleanValue);
 1216
 01217        var innerQuery = PrepareItemQuery(context, filter)
 01218            .Where(e => e.Type == returnType)
 01219            .Where(e => itemValuesQuery.Contains(e.CleanName));
 1220
 01221        var outerQueryFilter = new InternalItemsQuery(filter.User)
 01222        {
 01223            IsPlayed = filter.IsPlayed,
 01224            IsFavorite = filter.IsFavorite,
 01225            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 01226            IsLiked = filter.IsLiked,
 01227            IsLocked = filter.IsLocked,
 01228            NameLessThan = filter.NameLessThan,
 01229            NameStartsWith = filter.NameStartsWith,
 01230            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 01231            Tags = filter.Tags,
 01232            OfficialRatings = filter.OfficialRatings,
 01233            StudioIds = filter.StudioIds,
 01234            GenreIds = filter.GenreIds,
 01235            Genres = filter.Genres,
 01236            Years = filter.Years,
 01237            NameContains = filter.NameContains,
 01238            SearchTerm = filter.SearchTerm,
 01239            ExcludeItemIds = filter.ExcludeItemIds
 01240        };
 1241
 01242        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
 01243            .GroupBy(e => e.PresentationUniqueKey)
 01244            .Select(e => e.FirstOrDefault())
 01245            .Select(e => e!.Id);
 1246
 01247        var query = context.BaseItems
 01248            .Include(e => e.TrailerTypes)
 01249            .Include(e => e.Provider)
 01250            .Include(e => e.LockedFields)
 01251            .Include(e => e.Images)
 01252            .AsSingleQuery()
 01253            .Where(e => masterQuery.Contains(e.Id));
 1254
 01255        query = ApplyOrder(query, filter);
 1256
 01257        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 01258        if (filter.EnableTotalRecordCount)
 1259        {
 01260            result.TotalRecordCount = query.Count();
 1261        }
 1262
 01263        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
 1264        {
 01265            var offset = filter.StartIndex ?? 0;
 1266
 01267            if (offset > 0)
 1268            {
 01269                query = query.Skip(offset);
 1270            }
 1271
 01272            if (filter.Limit.HasValue)
 1273            {
 01274                query = query.Take(filter.Limit.Value);
 1275            }
 1276        }
 1277
 01278        IQueryable<BaseItemEntity>? itemCountQuery = null;
 1279
 01280        if (filter.IncludeItemTypes.Length > 0)
 1281        {
 1282            // if we are to include more then one type, sub query those items beforehand.
 1283
 01284            var typeSubQuery = new InternalItemsQuery(filter.User)
 01285            {
 01286                ExcludeItemTypes = filter.ExcludeItemTypes,
 01287                IncludeItemTypes = filter.IncludeItemTypes,
 01288                MediaTypes = filter.MediaTypes,
 01289                AncestorIds = filter.AncestorIds,
 01290                ExcludeItemIds = filter.ExcludeItemIds,
 01291                ItemIds = filter.ItemIds,
 01292                TopParentIds = filter.TopParentIds,
 01293                ParentId = filter.ParentId,
 01294                IsPlayed = filter.IsPlayed
 01295            };
 1296
 01297            itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 01298                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 1299
 01300            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 01301            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 01302            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 01303            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 01304            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 01305            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 01306            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 1307
 01308            var resultQuery = query.Select(e => new
 01309            {
 01310                item = e,
 01311                // TODO: This is bad refactor!
 01312                itemCount = new ItemCounts()
 01313                {
 01314                    SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
 01315                    EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
 01316                    MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
 01317                    AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
 01318                    ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
 01319                    SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
 01320                    TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
 01321                }
 01322            });
 1323
 01324            result.StartIndex = filter.StartIndex ?? 0;
 01325            result.Items =
 01326            [
 01327                .. resultQuery
 01328                    .AsEnumerable()
 01329                    .Where(e => e is not null)
 01330                    .Select(e =>
 01331                    {
 01332                        return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
 01333                    })
 01334            ];
 1335        }
 1336        else
 1337        {
 01338            result.StartIndex = filter.StartIndex ?? 0;
 01339            result.Items =
 01340            [
 01341                .. query
 01342                    .AsEnumerable()
 01343                    .Where(e => e is not null)
 01344                    .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
 01345                    {
 01346                        return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
 01347                    })
 01348            ];
 1349        }
 1350
 01351        return result;
 01352    }
 1353
 1354    private static void PrepareFilterQuery(InternalItemsQuery query)
 1355    {
 3241356        if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
 1357        {
 01358            query.Limit = query.Limit.Value + 4;
 1359        }
 1360
 3241361        if (query.IsResumable ?? false)
 1362        {
 11363            query.IsVirtualItem = false;
 1364        }
 3241365    }
 1366
 1367    private string GetCleanValue(string value)
 1368    {
 1151369        if (string.IsNullOrWhiteSpace(value))
 1370        {
 01371            return value;
 1372        }
 1373
 1151374        return value.RemoveDiacritics().ToLowerInvariant();
 1375    }
 1376
 1377    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1378    {
 1121379        var list = new List<(ItemValueType, string)>();
 1380
 1121381        if (item is IHasArtist hasArtist)
 1382        {
 01383            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1384        }
 1385
 1121386        if (item is IHasAlbumArtist hasAlbumArtist)
 1387        {
 01388            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1389        }
 1390
 1121391        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 1121392        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 1121393        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1394
 1395        // keywords was 5
 1396
 1121397        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1398
 1399        // Remove all invalid values.
 1121400        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1401
 1121402        return list;
 1403    }
 1404
 1405    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1406    {
 01407        return new BaseItemImageInfo()
 01408        {
 01409            ItemId = baseItemId,
 01410            Id = Guid.NewGuid(),
 01411            Path = e.Path,
 01412            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 01413            DateModified = e.DateModified,
 01414            Height = e.Height,
 01415            Width = e.Width,
 01416            ImageType = (ImageInfoImageType)e.Type,
 01417            Item = null!
 01418        };
 1419    }
 1420
 1421    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 1422    {
 01423        return new ItemImageInfo()
 01424        {
 01425            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 01426            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 01427            DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
 01428            Height = e.Height,
 01429            Width = e.Width,
 01430            Type = (ImageType)e.ImageType
 01431        };
 1432    }
 1433
 1434    private string? GetPathToSave(string path)
 1435    {
 1121436        if (path is null)
 1437        {
 01438            return null;
 1439        }
 1440
 1121441        return _appHost.ReverseVirtualPath(path);
 1442    }
 1443
 1444    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1445    {
 131446        var list = new List<string>();
 1447
 131448        if (IsTypeInQuery(BaseItemKind.Person, query))
 1449        {
 11450            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1451        }
 1452
 131453        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1454        {
 11455            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1456        }
 1457
 131458        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1459        {
 11460            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1461        }
 1462
 131463        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1464        {
 11465            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1466        }
 1467
 131468        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1469        {
 11470            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1471        }
 1472
 131473        return list;
 1474    }
 1475
 1476    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1477    {
 651478        if (query.ExcludeItemTypes.Contains(type))
 1479        {
 01480            return false;
 1481        }
 1482
 651483        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 1484    }
 1485
 1486    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 1487    {
 3241488        if (!query.GroupByPresentationUniqueKey)
 1489        {
 1241490            return false;
 1491        }
 1492
 2001493        if (query.GroupBySeriesPresentationUniqueKey)
 1494        {
 01495            return false;
 1496        }
 1497
 2001498        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 1499        {
 01500            return false;
 1501        }
 1502
 2001503        if (query.User is null)
 1504        {
 1981505            return false;
 1506        }
 1507
 21508        if (query.IncludeItemTypes.Length == 0)
 1509        {
 11510            return true;
 1511        }
 1512
 11513        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 11514            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 11515            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 11516            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 11517            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 11518            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 1519    }
 1520
 1521    private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter)
 1522    {
 3241523        var orderBy = filter.OrderBy;
 3241524        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1525
 3241526        if (hasSearch)
 1527        {
 01528            orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1529        }
 3241530        else if (orderBy.Count == 0)
 1531        {
 2171532            return query.OrderBy(e => e.SortName);
 1533        }
 1534
 1071535        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1536
 1071537        var firstOrdering = orderBy.FirstOrDefault();
 1071538        if (firstOrdering != default)
 1539        {
 1071540            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
 1071541            if (firstOrdering.SortOrder == SortOrder.Ascending)
 1542            {
 1061543                orderedQuery = query.OrderBy(expression);
 1544            }
 1545            else
 1546            {
 11547                orderedQuery = query.OrderByDescending(expression);
 1548            }
 1549
 1071550            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 1551            {
 01552                if (firstOrdering.SortOrder is SortOrder.Ascending)
 1553                {
 01554                    orderedQuery = orderedQuery.ThenBy(e => e.Name);
 1555                }
 1556                else
 1557                {
 01558                    orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
 1559                }
 1560            }
 1561        }
 1562
 3001563        foreach (var item in orderBy.Skip(1))
 1564        {
 431565            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter);
 431566            if (item.SortOrder == SortOrder.Ascending)
 1567            {
 431568                orderedQuery = orderedQuery!.ThenBy(expression);
 1569            }
 1570            else
 1571            {
 01572                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1573            }
 1574        }
 1575
 1071576        return orderedQuery ?? query;
 1577    }
 1578
 1579    private IQueryable<BaseItemEntity> TranslateQuery(
 1580        IQueryable<BaseItemEntity> baseQuery,
 1581        JellyfinDbContext context,
 1582        InternalItemsQuery filter)
 1583    {
 1584        const int HDWidth = 1200;
 1585        const int UHDWidth = 3800;
 1586        const int UHDHeight = 2100;
 1587
 3241588        var minWidth = filter.MinWidth;
 3241589        var maxWidth = filter.MaxWidth;
 3241590        var now = DateTime.UtcNow;
 1591
 3241592        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 1593        {
 01594            bool includeSD = false;
 01595            bool includeHD = false;
 01596            bool include4K = false;
 1597
 01598            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 1599            {
 01600                includeSD = true;
 1601            }
 1602
 01603            if (filter.IsHD.HasValue && filter.IsHD.Value)
 1604            {
 01605                includeHD = true;
 1606            }
 1607
 01608            if (filter.Is4K.HasValue && filter.Is4K.Value)
 1609            {
 01610                include4K = true;
 1611            }
 1612
 01613            baseQuery = baseQuery.Where(e =>
 01614                (includeSD && e.Width < HDWidth) ||
 01615                (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
 01616                (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
 1617        }
 1618
 3241619        if (minWidth.HasValue)
 1620        {
 01621            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1622        }
 1623
 3241624        if (filter.MinHeight.HasValue)
 1625        {
 01626            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1627        }
 1628
 3241629        if (maxWidth.HasValue)
 1630        {
 01631            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1632        }
 1633
 3241634        if (filter.MaxHeight.HasValue)
 1635        {
 01636            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1637        }
 1638
 3241639        if (filter.IsLocked.HasValue)
 1640        {
 511641            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1642        }
 1643
 3241644        var tags = filter.Tags.ToList();
 3241645        var excludeTags = filter.ExcludeTags.ToList();
 1646
 3241647        if (filter.IsMovie == true)
 1648        {
 01649            if (filter.IncludeItemTypes.Length == 0
 01650                || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01651                || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
 1652            {
 01653                baseQuery = baseQuery.Where(e => e.IsMovie);
 1654            }
 1655        }
 3241656        else if (filter.IsMovie.HasValue)
 1657        {
 01658            baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
 1659        }
 1660
 3241661        if (filter.IsSeries.HasValue)
 1662        {
 01663            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1664        }
 1665
 3241666        if (filter.IsSports.HasValue)
 1667        {
 01668            if (filter.IsSports.Value)
 1669            {
 01670                tags.Add("Sports");
 1671            }
 1672            else
 1673            {
 01674                excludeTags.Add("Sports");
 1675            }
 1676        }
 1677
 3241678        if (filter.IsNews.HasValue)
 1679        {
 01680            if (filter.IsNews.Value)
 1681            {
 01682                tags.Add("News");
 1683            }
 1684            else
 1685            {
 01686                excludeTags.Add("News");
 1687            }
 1688        }
 1689
 3241690        if (filter.IsKids.HasValue)
 1691        {
 01692            if (filter.IsKids.Value)
 1693            {
 01694                tags.Add("Kids");
 1695            }
 1696            else
 1697            {
 01698                excludeTags.Add("Kids");
 1699            }
 1700        }
 1701
 3241702        if (!string.IsNullOrEmpty(filter.SearchTerm))
 1703        {
 01704            var searchTerm = filter.SearchTerm.ToLower();
 01705            if (SearchWildcardTerms.Any(f => searchTerm.Contains(f)))
 1706            {
 01707                searchTerm = $"%{searchTerm.Trim('%')}%";
 01708                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!.ToLower(), searchTerm) || (e.OriginalTit
 1709            }
 1710            else
 1711            {
 01712                baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null
 1713            }
 1714        }
 1715
 3241716        if (filter.IsFolder.HasValue)
 1717        {
 211718            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1719        }
 1720
 3241721        var includeTypes = filter.IncludeItemTypes;
 1722
 1723        // Only specify excluded types if no included types are specified
 3241724        if (filter.IncludeItemTypes.Length == 0)
 1725        {
 2061726            var excludeTypes = filter.ExcludeItemTypes;
 2061727            if (excludeTypes.Length == 1)
 1728            {
 01729                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 1730                {
 01731                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 1732                }
 1733            }
 2061734            else if (excludeTypes.Length > 1)
 1735            {
 01736                var excludeTypeName = new List<string>();
 01737                foreach (var excludeType in excludeTypes)
 1738                {
 01739                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 1740                    {
 01741                        excludeTypeName.Add(baseItemKindName!);
 1742                    }
 1743                }
 1744
 01745                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 1746            }
 1747        }
 1748        else
 1749        {
 1181750            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 1181751            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1752        }
 1753
 3241754        if (filter.ChannelIds.Count > 0)
 1755        {
 01756            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1757        }
 1758
 3241759        if (!filter.ParentId.IsEmpty())
 1760        {
 1241761            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1762        }
 1763
 3241764        if (!string.IsNullOrWhiteSpace(filter.Path))
 1765        {
 01766            var pathToQuery = GetPathToSave(filter.Path);
 01767            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 1768        }
 1769
 3241770        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1771        {
 01772            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1773        }
 1774
 3241775        if (filter.MinCommunityRating.HasValue)
 1776        {
 01777            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1778        }
 1779
 3241780        if (filter.MinIndexNumber.HasValue)
 1781        {
 01782            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1783        }
 1784
 3241785        if (filter.MinParentAndIndexNumber.HasValue)
 1786        {
 01787            baseQuery = baseQuery
 01788                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1789        }
 1790
 3241791        if (filter.MinDateCreated.HasValue)
 1792        {
 01793            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1794        }
 1795
 3241796        if (filter.MinDateLastSaved.HasValue)
 1797        {
 01798            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1799        }
 1800
 3241801        if (filter.MinDateLastSavedForUser.HasValue)
 1802        {
 01803            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1804        }
 1805
 3241806        if (filter.IndexNumber.HasValue)
 1807        {
 01808            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1809        }
 1810
 3241811        if (filter.ParentIndexNumber.HasValue)
 1812        {
 01813            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1814        }
 1815
 3241816        if (filter.ParentIndexNumberNotEquals.HasValue)
 1817        {
 01818            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1819        }
 1820
 3241821        var minEndDate = filter.MinEndDate;
 3241822        var maxEndDate = filter.MaxEndDate;
 1823
 3241824        if (filter.HasAired.HasValue)
 1825        {
 01826            if (filter.HasAired.Value)
 1827            {
 01828                maxEndDate = DateTime.UtcNow;
 1829            }
 1830            else
 1831            {
 01832                minEndDate = DateTime.UtcNow;
 1833            }
 1834        }
 1835
 3241836        if (minEndDate.HasValue)
 1837        {
 01838            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1839        }
 1840
 3241841        if (maxEndDate.HasValue)
 1842        {
 01843            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1844        }
 1845
 3241846        if (filter.MinStartDate.HasValue)
 1847        {
 01848            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1849        }
 1850
 3241851        if (filter.MaxStartDate.HasValue)
 1852        {
 01853            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1854        }
 1855
 3241856        if (filter.MinPremiereDate.HasValue)
 1857        {
 01858            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1859        }
 1860
 3241861        if (filter.MaxPremiereDate.HasValue)
 1862        {
 01863            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1864        }
 1865
 3241866        if (filter.TrailerTypes.Length > 0)
 1867        {
 01868            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 01869            baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
 1870        }
 1871
 3241872        if (filter.IsAiring.HasValue)
 1873        {
 01874            if (filter.IsAiring.Value)
 1875            {
 01876                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 1877            }
 1878            else
 1879            {
 01880                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 1881            }
 1882        }
 1883
 3241884        if (filter.PersonIds.Length > 0)
 1885        {
 01886            var peopleEntityIds = context.BaseItems
 01887                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 01888                .Join(
 01889                    context.Peoples,
 01890                    b => b.Name,
 01891                    p => p.Name,
 01892                    (b, p) => p.Id);
 1893
 01894            baseQuery = baseQuery
 01895                .Where(e => context.PeopleBaseItemMap
 01896                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 1897        }
 1898
 3241899        if (!string.IsNullOrWhiteSpace(filter.Person))
 1900        {
 01901            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 1902        }
 1903
 3241904        if (!string.IsNullOrWhiteSpace(filter.MinSortName))
 1905        {
 1906            // this does not makes sense.
 1907            // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
 1908            // whereClauses.Add("SortName>=@MinSortName");
 1909            // statement?.TryBind("@MinSortName", query.MinSortName);
 1910        }
 1911
 3241912        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 1913        {
 01914            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 1915        }
 1916
 3241917        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 1918        {
 01919            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 1920        }
 1921
 3241922        if (!string.IsNullOrWhiteSpace(filter.Name))
 1923        {
 31924            var cleanName = GetCleanValue(filter.Name);
 31925            baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 1926        }
 1927
 1928        // These are the same, for now
 3241929        var nameContains = filter.NameContains;
 3241930        if (!string.IsNullOrWhiteSpace(nameContains))
 1931        {
 01932            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 1933            {
 01934                nameContains = $"%{nameContains.Trim('%')}%";
 01935                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 1936            }
 1937            else
 1938            {
 01939                baseQuery = baseQuery.Where(e =>
 01940                                    e.CleanName!.Contains(nameContains)
 01941                                    || e.OriginalTitle!.ToLower().Contains(nameContains!));
 1942            }
 1943        }
 1944
 3241945        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 1946        {
 01947            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith));
 1948        }
 1949
 3241950        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 1951        {
 1952            // i hate this
 01953            baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!
 1954        }
 1955
 3241956        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 1957        {
 1958            // i hate this
 01959            baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDef
 1960        }
 1961
 3241962        if (filter.ImageTypes.Length > 0)
 1963        {
 1061964            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 1061965            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 1966        }
 1967
 3241968        if (filter.IsLiked.HasValue)
 1969        {
 01970            baseQuery = baseQuery
 01971                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLike
 1972        }
 1973
 3241974        if (filter.IsFavoriteOrLiked.HasValue)
 1975        {
 01976            baseQuery = baseQuery
 01977                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 1978        }
 1979
 3241980        if (filter.IsFavorite.HasValue)
 1981        {
 01982            baseQuery = baseQuery
 01983                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 1984        }
 1985
 3241986        if (filter.IsPlayed.HasValue)
 1987        {
 1988            // We should probably figure this out for all folders, but for right now, this is the only place where we ne
 01989            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
 1990            {
 01991                baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
 01992                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
 01993                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
 01994                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
 1995            }
 1996            else
 1997            {
 01998                baseQuery = baseQuery
 01999                    .Select(e => new
 02000                    {
 02001                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).Firs
 02002                        Item = e
 02003                    })
 02004                    .Where(e => e.IsPlayed == filter.IsPlayed)
 02005                    .Select(f => f.Item);
 2006            }
 2007        }
 2008
 3242009        if (filter.IsResumable.HasValue)
 2010        {
 12011            if (filter.IsResumable.Value)
 2012            {
 12013                baseQuery = baseQuery
 12014                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks >
 2015            }
 2016            else
 2017            {
 02018                baseQuery = baseQuery
 02019                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks =
 2020            }
 2021        }
 2022
 3242023        if (filter.ArtistIds.Length > 0)
 2024        {
 02025            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2026        }
 2027
 3242028        if (filter.AlbumArtistIds.Length > 0)
 2029        {
 02030            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 2031        }
 2032
 3242033        if (filter.ContributingArtistIds.Length > 0)
 2034        {
 02035            var contributingNames = context.BaseItems
 02036                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 02037                .Select(b => b.CleanName);
 2038
 02039            baseQuery = baseQuery.Where(e =>
 02040                e.ItemValues!.Any(ivm =>
 02041                    ivm.ItemValue.Type == ItemValueType.Artist &&
 02042                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 02043                &&
 02044                !e.ItemValues!.Any(ivm =>
 02045                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 02046                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 2047        }
 2048
 3242049        if (filter.AlbumIds.Length > 0)
 2050        {
 02051            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 02052            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 2053        }
 2054
 3242055        if (filter.ExcludeArtistIds.Length > 0)
 2056        {
 02057            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2058        }
 2059
 3242060        if (filter.GenreIds.Count > 0)
 2061        {
 02062            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 2063        }
 2064
 3242065        if (filter.Genres.Count > 0)
 2066        {
 02067            var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValue
 02068            baseQuery = baseQuery
 02069                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 2070        }
 2071
 3242072        if (tags.Count > 0)
 2073        {
 02074            var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, stri
 02075            baseQuery = baseQuery
 02076                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 2077        }
 2078
 3242079        if (excludeTags.Count > 0)
 2080        {
 02081            var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMa
 02082            baseQuery = baseQuery
 02083                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 2084        }
 2085
 3242086        if (filter.StudioIds.Length > 0)
 2087        {
 02088            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 2089        }
 2090
 3242091        if (filter.OfficialRatings.Length > 0)
 2092        {
 02093            baseQuery = baseQuery
 02094                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 2095        }
 2096
 3242097        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 3242098        if (filter.MinParentalRating != null)
 2099        {
 02100            var min = filter.MinParentalRating;
 02101            var minScore = min.Score;
 02102            var minSubScore = min.SubScore ?? 0;
 2103
 02104            minParentalRatingFilter = e =>
 02105                e.InheritedParentalRatingValue == null ||
 02106                e.InheritedParentalRatingValue > minScore ||
 02107                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 2108        }
 2109
 3242110        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 3242111        if (filter.MaxParentalRating != null)
 2112        {
 512113            var max = filter.MaxParentalRating;
 512114            var maxScore = max.Score;
 512115            var maxSubScore = max.SubScore ?? 0;
 2116
 512117            maxParentalRatingFilter = e =>
 512118                e.InheritedParentalRatingValue == null ||
 512119                e.InheritedParentalRatingValue < maxScore ||
 512120                (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
 2121        }
 2122
 3242123        if (filter.HasParentalRating ?? false)
 2124        {
 02125            if (minParentalRatingFilter != null)
 2126            {
 02127                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2128            }
 2129
 02130            if (maxParentalRatingFilter != null)
 2131            {
 02132                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2133            }
 2134        }
 3242135        else if (filter.BlockUnratedItems.Length > 0)
 2136        {
 02137            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 02138            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 2139
 02140            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 2141            {
 02142                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 2143            }
 02144            else if (minParentalRatingFilter != null)
 2145            {
 02146                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 2147            }
 02148            else if (maxParentalRatingFilter != null)
 2149            {
 02150                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 2151            }
 2152            else
 2153            {
 02154                baseQuery = baseQuery.Where(unratedItemFilter);
 2155            }
 2156        }
 3242157        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 2158        {
 512159            if (minParentalRatingFilter != null)
 2160            {
 02161                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2162            }
 2163
 512164            if (maxParentalRatingFilter != null)
 2165            {
 512166                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2167            }
 2168        }
 2732169        else if (!filter.HasParentalRating ?? false)
 2170        {
 02171            baseQuery = baseQuery
 02172                .Where(e => e.InheritedParentalRatingValue == null);
 2173        }
 2174
 3242175        if (filter.HasOfficialRating.HasValue)
 2176        {
 02177            if (filter.HasOfficialRating.Value)
 2178            {
 02179                baseQuery = baseQuery
 02180                    .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
 2181            }
 2182            else
 2183            {
 02184                baseQuery = baseQuery
 02185                    .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
 2186            }
 2187        }
 2188
 3242189        if (filter.HasOverview.HasValue)
 2190        {
 02191            if (filter.HasOverview.Value)
 2192            {
 02193                baseQuery = baseQuery
 02194                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 2195            }
 2196            else
 2197            {
 02198                baseQuery = baseQuery
 02199                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 2200            }
 2201        }
 2202
 3242203        if (filter.HasOwnerId.HasValue)
 2204        {
 02205            if (filter.HasOwnerId.Value)
 2206            {
 02207                baseQuery = baseQuery
 02208                    .Where(e => e.OwnerId != null);
 2209            }
 2210            else
 2211            {
 02212                baseQuery = baseQuery
 02213                    .Where(e => e.OwnerId == null);
 2214            }
 2215        }
 2216
 3242217        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 2218        {
 02219            baseQuery = baseQuery
 02220                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filte
 2221        }
 2222
 3242223        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2224        {
 02225            baseQuery = baseQuery
 02226                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2227        }
 2228
 3242229        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2230        {
 02231            baseQuery = baseQuery
 02232                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2233        }
 2234
 3242235        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 2236        {
 02237            baseQuery = baseQuery
 02238                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == fi
 2239        }
 2240
 3242241        if (filter.HasSubtitles.HasValue)
 2242        {
 02243            baseQuery = baseQuery
 02244                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2245        }
 2246
 3242247        if (filter.HasChapterImages.HasValue)
 2248        {
 02249            baseQuery = baseQuery
 02250                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2251        }
 2252
 3242253        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2254        {
 172255            baseQuery = baseQuery
 172256                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 2257        }
 2258
 3242259        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2260        {
 172261            baseQuery = baseQuery
 172262                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2263        }
 2264
 3242265        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2266        {
 172267            baseQuery = baseQuery
 172268                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2269        }
 2270
 3242271        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2272        {
 172273            baseQuery = baseQuery
 172274                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2275        }
 2276
 3242277        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2278        {
 02279            baseQuery = baseQuery
 02280                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2281        }
 2282
 3242283        if (filter.Years.Length > 0)
 2284        {
 02285            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2286        }
 2287
 3242288        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 3242289        if (isVirtualItem.HasValue)
 2290        {
 222291            baseQuery = baseQuery
 222292                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2293        }
 2294
 3242295        if (filter.IsSpecialSeason.HasValue)
 2296        {
 02297            if (filter.IsSpecialSeason.Value)
 2298            {
 02299                baseQuery = baseQuery
 02300                    .Where(e => e.IndexNumber == 0);
 2301            }
 2302            else
 2303            {
 02304                baseQuery = baseQuery
 02305                    .Where(e => e.IndexNumber != 0);
 2306            }
 2307        }
 2308
 3242309        if (filter.IsUnaired.HasValue)
 2310        {
 02311            if (filter.IsUnaired.Value)
 2312            {
 02313                baseQuery = baseQuery
 02314                    .Where(e => e.PremiereDate >= now);
 2315            }
 2316            else
 2317            {
 02318                baseQuery = baseQuery
 02319                    .Where(e => e.PremiereDate < now);
 2320            }
 2321        }
 2322
 3242323        if (filter.MediaTypes.Length > 0)
 2324        {
 212325            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 212326            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 2327        }
 2328
 3242329        if (filter.ItemIds.Length > 0)
 2330        {
 02331            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2332        }
 2333
 3242334        if (filter.ExcludeItemIds.Length > 0)
 2335        {
 02336            baseQuery = baseQuery
 02337                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 2338        }
 2339
 3242340        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 2341        {
 02342            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 02343            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 2344        }
 2345
 3242346        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 2347        {
 2348            // Allow setting a null or empty value to get all items that have the specified provider set.
 02349            var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArra
 02350            if (includeAny.Length > 0)
 2351            {
 02352                baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
 2353            }
 2354
 02355            var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Ke
 02356            if (includeSelected.Length > 0)
 2357            {
 02358                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 2359            }
 2360        }
 2361
 3242362        if (filter.HasImdbId.HasValue)
 2363        {
 02364            baseQuery = filter.HasImdbId.Value
 02365                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().T
 02366                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().T
 2367        }
 2368
 3242369        if (filter.HasTmdbId.HasValue)
 2370        {
 02371            baseQuery = filter.HasTmdbId.Value
 02372                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().T
 02373                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().T
 2374        }
 2375
 3242376        if (filter.HasTvdbId.HasValue)
 2377        {
 02378            baseQuery = filter.HasTvdbId.Value
 02379                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().T
 02380                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().T
 2381        }
 2382
 3242383        var queryTopParentIds = filter.TopParentIds;
 2384
 3242385        if (queryTopParentIds.Length > 0)
 2386        {
 132387            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 132388            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 132389            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2390            {
 02391                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2392            }
 2393            else
 2394            {
 132395                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2396            }
 2397        }
 2398
 3242399        if (filter.AncestorIds.Length > 0)
 2400        {
 442401            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2402        }
 2403
 3242404        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 2405        {
 02406            baseQuery = baseQuery
 02407                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 2408        }
 2409
 3242410        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2411        {
 02412            baseQuery = baseQuery
 02413                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2414        }
 2415
 3242416        if (filter.ExcludeInheritedTags.Length > 0)
 2417        {
 02418            baseQuery = baseQuery
 02419                .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Typ
 02420                .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
 2421        }
 2422
 3242423        if (filter.IncludeInheritedTags.Length > 0)
 2424        {
 2425            // Episodes do not store inherit tags from their parents in the database, and the tag may be still required 
 2426            // In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s 
 02427            if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
 2428            {
 02429                baseQuery = baseQuery
 02430                    .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.
 02431                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
 02432                        ||
 02433                        (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.Item
 02434                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
 2435            }
 2436
 2437            // A playlist should be accessible to its owner regardless of allowed tags.
 02438            else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
 2439            {
 02440                baseQuery = baseQuery
 02441                    .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.
 02442                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
 02443                        || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
 2444                // d        ^^ this is stupid it hate this.
 2445            }
 2446            else
 2447            {
 02448                baseQuery = baseQuery
 02449                    .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.
 02450                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
 2451            }
 2452        }
 2453
 3242454        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
 3242461        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
 3242468        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
 3242475        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
 3242489        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
 3242503        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
 3242517        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
 3242531        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)
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>)