< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Item.BaseItemRepository
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
Line coverage
51%
Covered lines: 681
Uncovered lines: 631
Coverable lines: 1312
Total lines: 2654
Line coverage: 51.9%
Branch coverage
49%
Covered branches: 369
Total branches: 746
Branch coverage: 49.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 9/14/2025 - 12:09:49 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24529/17/2025 - 12:11:23 AM Line coverage: 51% (631/1236) Branch coverage: 46.8% (330/704) Total lines: 25129/18/2025 - 12:09:59 AM Line coverage: 51% (631/1236) Branch coverage: 46.7% (329/704) Total lines: 25129/19/2025 - 12:11:12 AM Line coverage: 51% (631/1236) Branch coverage: 46.8% (330/704) Total lines: 25129/20/2025 - 12:11:30 AM Line coverage: 51.1% (632/1236) Branch coverage: 47.1% (332/704) Total lines: 25129/23/2025 - 12:11:13 AM Line coverage: 52.3% (658/1258) Branch coverage: 49.1% (352/716) Total lines: 25349/24/2025 - 12:10:54 AM Line coverage: 52.1% (659/1263) Branch coverage: 49.1% (352/716) Total lines: 25419/25/2025 - 12:11:18 AM Line coverage: 51.9% (659/1269) Branch coverage: 49.1% (353/718) Total lines: 255310/2/2025 - 12:11:36 AM Line coverage: 52% (663/1275) Branch coverage: 49.1% (351/714) Total lines: 255710/3/2025 - 12:11:15 AM Line coverage: 51.7% (664/1282) Branch coverage: 48.8% (351/718) Total lines: 257510/9/2025 - 12:11:25 AM Line coverage: 51.5% (664/1288) Branch coverage: 48.8% (351/718) Total lines: 258210/12/2025 - 12:11:07 AM Line coverage: 51.6% (664/1286) Branch coverage: 48.8% (351/718) Total lines: 258210/14/2025 - 12:11:23 AM Line coverage: 51.2% (664/1296) Branch coverage: 48.8% (351/718) Total lines: 259310/28/2025 - 12:11:27 AM Line coverage: 50.9% (666/1306) Branch coverage: 48.9% (356/728) Total lines: 260711/18/2025 - 12:11:25 AM Line coverage: 50.9% (667/1309) Branch coverage: 49% (356/726) Total lines: 260711/29/2025 - 12:11:22 AM Line coverage: 50.9% (667/1309) Branch coverage: 49.1% (357/726) Total lines: 260711/30/2025 - 12:09:51 AM Line coverage: 50.9% (667/1309) Branch coverage: 49% (356/726) Total lines: 260712/4/2025 - 12:11:49 AM Line coverage: 50.9% (668/1312) Branch coverage: 49% (358/730) Total lines: 261212/9/2025 - 12:12:43 AM Line coverage: 51.5% (683/1326) Branch coverage: 49.7% (368/740) Total lines: 265212/10/2025 - 12:13:43 AM Line coverage: 51.5% (681/1322) Branch coverage: 49.3% (369/748) Total lines: 264212/14/2025 - 12:12:01 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/20/2025 - 12:12:47 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.7% (371/746) Total lines: 265412/21/2025 - 12:12:15 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 2654 9/14/2025 - 12:09:49 AM Line coverage: 50.7% (611/1204) Branch coverage: 47.5% (330/694) Total lines: 24529/17/2025 - 12:11:23 AM Line coverage: 51% (631/1236) Branch coverage: 46.8% (330/704) Total lines: 25129/18/2025 - 12:09:59 AM Line coverage: 51% (631/1236) Branch coverage: 46.7% (329/704) Total lines: 25129/19/2025 - 12:11:12 AM Line coverage: 51% (631/1236) Branch coverage: 46.8% (330/704) Total lines: 25129/20/2025 - 12:11:30 AM Line coverage: 51.1% (632/1236) Branch coverage: 47.1% (332/704) Total lines: 25129/23/2025 - 12:11:13 AM Line coverage: 52.3% (658/1258) Branch coverage: 49.1% (352/716) Total lines: 25349/24/2025 - 12:10:54 AM Line coverage: 52.1% (659/1263) Branch coverage: 49.1% (352/716) Total lines: 25419/25/2025 - 12:11:18 AM Line coverage: 51.9% (659/1269) Branch coverage: 49.1% (353/718) Total lines: 255310/2/2025 - 12:11:36 AM Line coverage: 52% (663/1275) Branch coverage: 49.1% (351/714) Total lines: 255710/3/2025 - 12:11:15 AM Line coverage: 51.7% (664/1282) Branch coverage: 48.8% (351/718) Total lines: 257510/9/2025 - 12:11:25 AM Line coverage: 51.5% (664/1288) Branch coverage: 48.8% (351/718) Total lines: 258210/12/2025 - 12:11:07 AM Line coverage: 51.6% (664/1286) Branch coverage: 48.8% (351/718) Total lines: 258210/14/2025 - 12:11:23 AM Line coverage: 51.2% (664/1296) Branch coverage: 48.8% (351/718) Total lines: 259310/28/2025 - 12:11:27 AM Line coverage: 50.9% (666/1306) Branch coverage: 48.9% (356/728) Total lines: 260711/18/2025 - 12:11:25 AM Line coverage: 50.9% (667/1309) Branch coverage: 49% (356/726) Total lines: 260711/29/2025 - 12:11:22 AM Line coverage: 50.9% (667/1309) Branch coverage: 49.1% (357/726) Total lines: 260711/30/2025 - 12:09:51 AM Line coverage: 50.9% (667/1309) Branch coverage: 49% (356/726) Total lines: 260712/4/2025 - 12:11:49 AM Line coverage: 50.9% (668/1312) Branch coverage: 49% (358/730) Total lines: 261212/9/2025 - 12:12:43 AM Line coverage: 51.5% (683/1326) Branch coverage: 49.7% (368/740) Total lines: 265212/10/2025 - 12:13:43 AM Line coverage: 51.5% (681/1322) Branch coverage: 49.3% (369/748) Total lines: 264212/14/2025 - 12:12:01 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 265412/20/2025 - 12:12:47 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.7% (371/746) Total lines: 265412/21/2025 - 12:12:15 AM Line coverage: 51.9% (681/1312) Branch coverage: 49.4% (369/746) Total lines: 2654

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
DeleteItem(...)50%6697.56%
UpdateInheritedValues()100%11100%
GetItemIdsList(...)100%11100%
GetAllArtists(...)100%210%
GetArtists(...)100%210%
GetAlbumArtists(...)100%210%
GetStudios(...)100%210%
GetGenres(...)100%210%
GetMusicGenres(...)100%210%
GetStudioNames()100%11100%
GetAllArtistNames()100%11100%
GetMusicGenreNames()100%11100%
GetGenreNames()100%11100%
GetItems(...)50%27833.33%
GetItemList(...)100%11100%
GetLatestItemList(...)0%7280%
GetNextUpSeriesKeys(...)0%2040%
ApplyGroupingFilter(...)75%10869.23%
ApplyNavigations(...)100%22100%
ApplyQueryPaging(...)75%9880%
ApplyQueryFilter(...)100%11100%
PrepareItemQuery(...)100%11100%
GetCount(...)100%210%
GetItemCounts(...)0%420200%
GetType(...)100%11100%
SaveItems(...)100%11100%
UpdateOrInsertItems(...)83.33%383688.34%
RetrieveItem(...)50%4490%
Map(...)50%1428279.31%
Map(...)54.83%956279.46%
GetItemValueNames(...)100%44100%
TypeRequiresDeserialization(...)100%11100%
DeserializeBaseItem(...)50%101088.88%
DeserializeBaseItem(...)83.33%151272.72%
GetItemValues(...)0%272160%
PrepareFilterQuery(...)87.5%9880%
GetCleanValue(...)91.66%121294.11%
GetItemValuesToSave(...)50%4481.81%
Map(...)0%620%
Map(...)0%7280%
GetPathToSave(...)50%2266.66%
GetItemByNameTypesInQuery(...)100%1010100%
IsTypeInQuery(...)75%5466.66%
EnableGroupByPresentationUniqueKey(...)65%212087.5%
ApplyOrder(...)57.69%332678.26%
TranslateQuery(...)44.57%3108335036.93%
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    {
 3195        _dbProvider = dbProvider;
 3196        _appHost = appHost;
 3197        _itemTypeLookup = itemTypeLookup;
 3198        _serverConfigurationManager = serverConfigurationManager;
 3199        _logger = logger;
 31100    }
 101
 102    /// <inheritdoc />
 103    public void DeleteItem(params IReadOnlyList<Guid> ids)
 104    {
 2105        if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
 106        {
 0107            throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
 108        }
 109
 2110        using var context = _dbProvider.CreateDbContext();
 2111        using var transaction = context.Database.BeginTransaction();
 112
 2113        var date = (DateTime?)DateTime.UtcNow;
 114
 2115        var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
 116
 117        // Remove any UserData entries for the placeholder item that would conflict with the UserData
 118        // being detached from the item being deleted. This is necessary because, during an update,
 119        // UserData may be reattached to a new entry, but some entries can be left behind.
 120        // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
 2121        context.UserData
 2122            .Join(
 2123                context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
 2124                placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
 2125                userData => new { userData.UserId, userData.CustomDataKey },
 2126                (placeholder, userData) => placeholder)
 2127            .Where(e => e.ItemId == PlaceholderId)
 2128            .ExecuteDelete();
 129
 130        // Detach all user watch data
 2131        context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
 2132            .ExecuteUpdate(e => e
 2133                .SetProperty(f => f.RetentionDate, date)
 2134                .SetProperty(f => f.ItemId, PlaceholderId));
 135
 2136        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2137        context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
 2138        context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2139        context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2140        context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2141        context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2142        context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2143        context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
 2144        context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2145        context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2146        context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2147        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
 2148        context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2149        context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2150        context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2151        context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2152        var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distin
 2153        context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2154        context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
 2155        context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
 2156        context.SaveChanges();
 2157        transaction.Commit();
 4158    }
 159
 160    /// <inheritdoc />
 161    public void UpdateInheritedValues()
 162    {
 15163        using var context = _dbProvider.CreateDbContext();
 15164        using var transaction = context.Database.BeginTransaction();
 165
 15166        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
 167        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
 15168        context.SaveChanges();
 169
 15170        transaction.Commit();
 30171    }
 172
 173    /// <inheritdoc />
 174    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
 175    {
 16176        ArgumentNullException.ThrowIfNull(filter);
 16177        PrepareFilterQuery(filter);
 178
 16179        using var context = _dbProvider.CreateDbContext();
 16180        return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context
 16181    }
 182
 183    /// <inheritdoc />
 184    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
 185    {
 0186        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 187    }
 188
 189    /// <inheritdoc />
 190    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
 191    {
 0192        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
 193    }
 194
 195    /// <inheritdoc />
 196    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
 197    {
 0198        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArti
 199    }
 200
 201    /// <inheritdoc />
 202    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
 203    {
 0204        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
 205    }
 206
 207    /// <inheritdoc />
 208    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
 209    {
 0210        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
 211    }
 212
 213    /// <inheritdoc />
 214    public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
 215    {
 0216        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
 217    }
 218
 219    /// <inheritdoc />
 220    public IReadOnlyList<string> GetStudioNames()
 221    {
 16222        return GetItemValueNames(_getStudiosValueTypes, [], []);
 223    }
 224
 225    /// <inheritdoc />
 226    public IReadOnlyList<string> GetAllArtistNames()
 227    {
 16228        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
 229    }
 230
 231    /// <inheritdoc />
 232    public IReadOnlyList<string> GetMusicGenreNames()
 233    {
 16234        return GetItemValueNames(
 16235            _getGenreValueTypes,
 16236            _itemTypeLookup.MusicGenreTypes,
 16237            []);
 238    }
 239
 240    /// <inheritdoc />
 241    public IReadOnlyList<string> GetGenreNames()
 242    {
 16243        return GetItemValueNames(
 16244            _getGenreValueTypes,
 16245            [],
 16246            _itemTypeLookup.MusicGenreTypes);
 247    }
 248
 249    /// <inheritdoc />
 250    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
 251    {
 1252        ArgumentNullException.ThrowIfNull(filter);
 1253        if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0))
 254        {
 1255            var returnList = GetItemList(filter);
 1256            return new QueryResult<BaseItemDto>(
 1257                filter.StartIndex,
 1258                returnList.Count,
 1259                returnList);
 260        }
 261
 0262        PrepareFilterQuery(filter);
 0263        var result = new QueryResult<BaseItemDto>();
 264
 0265        using var context = _dbProvider.CreateDbContext();
 266
 0267        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 268
 0269        dbQuery = TranslateQuery(dbQuery, context, filter);
 0270        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 271
 0272        if (filter.EnableTotalRecordCount)
 273        {
 0274            result.TotalRecordCount = dbQuery.Count();
 275        }
 276
 0277        dbQuery = ApplyQueryPaging(dbQuery, filter);
 0278        dbQuery = ApplyNavigations(dbQuery, filter);
 279
 0280        result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDe
 0281        result.StartIndex = filter.StartIndex ?? 0;
 0282        return result;
 0283    }
 284
 285    /// <inheritdoc />
 286    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
 287    {
 318288        ArgumentNullException.ThrowIfNull(filter);
 318289        PrepareFilterQuery(filter);
 290
 318291        using var context = _dbProvider.CreateDbContext();
 318292        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
 293
 318294        dbQuery = TranslateQuery(dbQuery, context, filter);
 295
 318296        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 318297        dbQuery = ApplyQueryPaging(dbQuery, filter);
 318298        dbQuery = ApplyNavigations(dbQuery, filter);
 299
 318300        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserializ
 318301    }
 302
 303    /// <inheritdoc/>
 304    public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
 305    {
 0306        ArgumentNullException.ThrowIfNull(filter);
 0307        PrepareFilterQuery(filter);
 308
 309        // Early exit if collection type is not tvshows or music
 0310        if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music)
 311        {
 0312            return Array.Empty<BaseItem>();
 313        }
 314
 0315        using var context = _dbProvider.CreateDbContext();
 316
 317        // Subquery to group by SeriesNames/Album and get the max Date Created for each group.
 0318        var subquery = PrepareItemQuery(context, filter);
 0319        subquery = TranslateQuery(subquery, context, filter);
 0320        var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album)
 0321            .Select(g => new
 0322            {
 0323                Key = g.Key,
 0324                MaxDateCreated = g.Max(a => a.DateCreated)
 0325            })
 0326            .OrderByDescending(g => g.MaxDateCreated)
 0327            .Select(g => g);
 328
 0329        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 330        {
 0331            subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
 332        }
 333
 0334        filter.Limit = null;
 335
 0336        var mainquery = PrepareItemQuery(context, filter);
 0337        mainquery = TranslateQuery(mainquery, context, filter);
 0338        mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
 0339        mainquery = ApplyGroupingFilter(context, mainquery, filter);
 0340        mainquery = ApplyQueryPaging(mainquery, filter);
 341
 0342        mainquery = ApplyNavigations(mainquery, filter);
 343
 0344        return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserial
 0345    }
 346
 347    /// <inheritdoc />
 348    public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
 349    {
 0350        ArgumentNullException.ThrowIfNull(filter);
 0351        ArgumentNullException.ThrowIfNull(filter.User);
 352
 0353        using var context = _dbProvider.CreateDbContext();
 354
 0355        var query = context.BaseItems
 0356            .AsNoTracking()
 0357            .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
 0358            .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
 0359            .Join(
 0360                context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
 0361                i => new { UserId = filter.User.Id, ItemId = i.Id },
 0362                u => new { UserId = u.UserId, ItemId = u.ItemId },
 0363                (entity, data) => new { Item = entity, UserData = data })
 0364            .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
 0365            .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
 0366            .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
 0367            .OrderByDescending(g => g.LastPlayedDate)
 0368            .Select(g => g.Key!);
 369
 0370        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 371        {
 0372            query = query.Take(filter.Limit.Value);
 373        }
 374
 0375        return query.ToArray();
 0376    }
 377
 378    private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery
 379    {
 380        // This whole block is needed to filter duplicate entries on request
 381        // for the time being it cannot be used because it would destroy the ordering
 382        // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions b
 383        // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
 384
 334385        var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
 334386        if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
 387        {
 0388            var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(
 0389            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 390        }
 334391        else if (enableGroupByPresentationUniqueKey)
 392        {
 1393            var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!
 1394            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 395        }
 333396        else if (filter.GroupBySeriesPresentationUniqueKey)
 397        {
 0398            var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e
 0399            dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
 400        }
 401        else
 402        {
 333403            dbQuery = dbQuery.Distinct();
 404        }
 405
 334406        dbQuery = ApplyOrder(dbQuery, filter, context);
 407
 334408        return dbQuery;
 409    }
 410
 411    private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery fi
 412    {
 334413        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 334414           .Include(e => e.Provider)
 334415           .Include(e => e.LockedFields)
 334416           .Include(e => e.UserData);
 417
 334418        if (filter.DtoOptions.EnableImages)
 419        {
 334420            dbQuery = dbQuery.Include(e => e.Images);
 421        }
 422
 334423        return dbQuery;
 424    }
 425
 426    private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
 427    {
 334428        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 429        {
 0430            dbQuery = dbQuery.Skip(filter.StartIndex.Value);
 431        }
 432
 334433        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 434        {
 102435            dbQuery = dbQuery.Take(filter.Limit.Value);
 436        }
 437
 334438        return dbQuery;
 439    }
 440
 441    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, I
 442    {
 16443        dbQuery = TranslateQuery(dbQuery, context, filter);
 16444        dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
 16445        dbQuery = ApplyQueryPaging(dbQuery, filter);
 16446        dbQuery = ApplyNavigations(dbQuery, filter);
 16447        return dbQuery;
 448    }
 449
 450    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
 451    {
 404452        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
 404453        dbQuery = dbQuery.AsSingleQuery();
 454
 404455        return dbQuery;
 456    }
 457
 458    /// <inheritdoc/>
 459    public int GetCount(InternalItemsQuery filter)
 460    {
 0461        ArgumentNullException.ThrowIfNull(filter);
 462        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0463        PrepareFilterQuery(filter);
 464
 0465        using var context = _dbProvider.CreateDbContext();
 0466        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 467
 0468        return dbQuery.Count();
 0469    }
 470
 471    /// <inheritdoc />
 472    public ItemCounts GetItemCounts(InternalItemsQuery filter)
 473    {
 0474        ArgumentNullException.ThrowIfNull(filter);
 475        // Hack for right now since we currently don't support filtering out these duplicates within a query
 0476        PrepareFilterQuery(filter);
 477
 0478        using var context = _dbProvider.CreateDbContext();
 0479        var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 480
 0481        var counts = dbQuery
 0482            .GroupBy(x => x.Type)
 0483            .Select(x => new { x.Key, Count = x.Count() })
 0484            .ToArray();
 485
 0486        var lookup = _itemTypeLookup.BaseItemKindNames;
 0487        var result = new ItemCounts();
 0488        foreach (var count in counts)
 489        {
 0490            if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
 491            {
 0492                result.AlbumCount = count.Count;
 493            }
 0494            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
 495            {
 0496                result.ArtistCount = count.Count;
 497            }
 0498            else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
 499            {
 0500                result.EpisodeCount = count.Count;
 501            }
 0502            else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
 503            {
 0504                result.MovieCount = count.Count;
 505            }
 0506            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
 507            {
 0508                result.MusicVideoCount = count.Count;
 509            }
 0510            else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
 511            {
 0512                result.ProgramCount = count.Count;
 513            }
 0514            else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
 515            {
 0516                result.SeriesCount = count.Count;
 517            }
 0518            else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
 519            {
 0520                result.SongCount = count.Count;
 521            }
 0522            else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
 523            {
 0524                result.TrailerCount = count.Count;
 525            }
 526        }
 527
 0528        return result;
 0529    }
 530
 531#pragma warning disable CA1307 // Specify StringComparison for clarity
 532    /// <summary>
 533    /// Gets the type.
 534    /// </summary>
 535    /// <param name="typeName">Name of the type.</param>
 536    /// <returns>Type.</returns>
 537    /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
 538    private static Type? GetType(string typeName)
 539    {
 140540        ArgumentException.ThrowIfNullOrEmpty(typeName);
 541
 542        // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static 
 543        // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are 
 140544        return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
 140545            .Select(a => a.GetType(k))
 140546            .FirstOrDefault(t => t is not null));
 547    }
 548
 549    /// <inheritdoc  />
 550    public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default)
 551    {
 552        ArgumentNullException.ThrowIfNull(item);
 553
 554        var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray();
 555
 556        var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 557        await using (context.ConfigureAwait(false))
 558        {
 559            if (!await context.BaseItems
 560                .AnyAsync(bi => bi.Id == item.Id, cancellationToken)
 561                .ConfigureAwait(false))
 562            {
 563                _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
 564                return;
 565            }
 566
 567            await context.BaseItemImageInfos
 568                .Where(e => e.ItemId == item.Id)
 569                .ExecuteDeleteAsync(cancellationToken)
 570                .ConfigureAwait(false);
 571
 572            await context.BaseItemImageInfos
 573                .AddRangeAsync(images, cancellationToken)
 574                .ConfigureAwait(false);
 575
 576            await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 577        }
 578    }
 579
 580    /// <inheritdoc  />
 581    public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 582    {
 111583        UpdateOrInsertItems(items, cancellationToken);
 110584    }
 585
 586    /// <inheritdoc cref="IItemRepository"/>
 587    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
 588    {
 111589        ArgumentNullException.ThrowIfNull(items);
 111590        cancellationToken.ThrowIfCancellationRequested();
 591
 110592        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> Use
 440593        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
 594        {
 110595            var ancestorIds = item.SupportsAncestors ?
 110596                item.GetAncestorIds().Distinct().ToList() :
 110597                null;
 598
 110599            var topParent = item.GetTopParent();
 600
 110601            var userdataKey = item.GetUserDataKeys();
 110602            var inheritedTags = item.GetInheritedTags();
 603
 110604            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
 605        }
 606
 110607        using var context = _dbProvider.CreateDbContext();
 110608        using var transaction = context.Database.BeginTransaction();
 609
 110610        var ids = tuples.Select(f => f.Item.Id).ToArray();
 110611        var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
 110612        var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
 613
 440614        foreach (var item in tuples)
 615        {
 110616            var entity = Map(item.Item);
 617            // TODO: refactor this "inconsistency"
 110618            entity.TopParentId = item.TopParent?.Id;
 619
 110620            if (!existingItems.Any(e => e == entity.Id))
 621            {
 59622                context.BaseItems.Add(entity);
 623            }
 624            else
 625            {
 51626                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 51627                context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 51628                context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
 629
 51630                if (entity.Images is { Count: > 0 })
 631                {
 0632                    context.BaseItemImageInfos.AddRange(entity.Images);
 633                }
 634
 51635                if (entity.LockedFields is { Count: > 0 })
 636                {
 0637                    context.BaseItemMetadataFields.AddRange(entity.LockedFields);
 638                }
 639
 51640                context.BaseItems.Attach(entity).State = EntityState.Modified;
 641            }
 642        }
 643
 110644        context.SaveChanges();
 645
 338646        foreach (var item in newItems)
 647        {
 648            // reattach old userData entries
 59649            var userKeys = item.UserDataKey.ToArray();
 59650            var retentionDate = (DateTime?)null;
 59651            context.UserData
 59652                .Where(e => e.ItemId == PlaceholderId)
 59653                .Where(e => userKeys.Contains(e.CustomDataKey))
 59654                .ExecuteUpdate(e => e
 59655                    .SetProperty(f => f.ItemId, item.Item.Id)
 59656                    .SetProperty(f => f.RetentionDate, retentionDate));
 657        }
 658
 110659        var itemValueMaps = tuples
 110660            .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
 110661            .ToArray();
 110662        var allListedItemValues = itemValueMaps
 110663            .SelectMany(f => f.Values)
 110664            .Distinct()
 110665            .ToArray();
 110666        var existingValues = context.ItemValues
 110667            .Select(e => new
 110668            {
 110669                item = e,
 110670                Key = e.Type + "+" + e.Value
 110671            })
 110672            .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
 110673            .Select(e => e.item)
 110674            .ToArray();
 110675        var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).S
 110676        {
 110677            CleanValue = GetCleanValue(f.Value),
 110678            ItemValueId = Guid.NewGuid(),
 110679            Type = f.MagicNumber,
 110680            Value = f.Value
 110681        }).ToArray();
 110682        context.ItemValues.AddRange(missingItemValues);
 110683        context.SaveChanges();
 684
 110685        var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
 110686        var valueMap = itemValueMaps
 110687            .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type =
 110688            .ToArray();
 689
 110690        var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
 691
 440692        foreach (var item in valueMap)
 693        {
 110694            var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
 220695            foreach (var itemValue in item.Values)
 696            {
 0697                var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
 0698                if (existingItem is null)
 699                {
 0700                    context.ItemValuesMap.Add(new ItemValueMap()
 0701                    {
 0702                        Item = null!,
 0703                        ItemId = item.Item.Id,
 0704                        ItemValue = null!,
 0705                        ItemValueId = itemValue.ItemValueId
 0706                    });
 707                }
 708                else
 709                {
 710                    // map exists, remove from list so its been handled.
 0711                    itemMappedValues.Remove(existingItem);
 712                }
 713            }
 714
 715            // all still listed values are not in the new list so remove them.
 110716            context.ItemValuesMap.RemoveRange(itemMappedValues);
 717        }
 718
 110719        context.SaveChanges();
 720
 440721        foreach (var item in tuples)
 722        {
 110723            if (item.Item.SupportsAncestors && item.AncestorIds != null)
 724            {
 110725                var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
 110726                var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).T
 274727                foreach (var ancestorId in validAncestorIds)
 728                {
 27729                    var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
 27730                    if (existingAncestorId is null)
 731                    {
 23732                        context.AncestorIds.Add(new AncestorId()
 23733                        {
 23734                            ParentItemId = ancestorId,
 23735                            ItemId = item.Item.Id,
 23736                            Item = null!,
 23737                            ParentItem = null!
 23738                        });
 739                    }
 740                    else
 741                    {
 4742                        existingAncestorIds.Remove(existingAncestorId);
 743                    }
 744                }
 745
 110746                context.AncestorIds.RemoveRange(existingAncestorIds);
 747            }
 748        }
 749
 110750        context.SaveChanges();
 110751        transaction.Commit();
 220752    }
 753
 754    /// <inheritdoc  />
 755    public BaseItemDto? RetrieveItem(Guid id)
 756    {
 86757        if (id.IsEmpty())
 758        {
 0759            throw new ArgumentException("Guid can't be empty", nameof(id));
 760        }
 761
 86762        using var context = _dbProvider.CreateDbContext();
 86763        var dbQuery = PrepareItemQuery(context, new()
 86764        {
 86765            DtoOptions = new()
 86766            {
 86767                EnableImages = true
 86768            }
 86769        });
 86770        dbQuery = dbQuery.Include(e => e.TrailerTypes)
 86771            .Include(e => e.Provider)
 86772            .Include(e => e.LockedFields)
 86773            .Include(e => e.UserData)
 86774            .Include(e => e.Images);
 775
 86776        var item = dbQuery.FirstOrDefault(e => e.Id == id);
 86777        if (item is null)
 778        {
 86779            return null;
 780        }
 781
 0782        return DeserializeBaseItem(item);
 86783    }
 784
 785    /// <summary>
 786    /// Maps a Entity to the DTO.
 787    /// </summary>
 788    /// <param name="entity">The entity.</param>
 789    /// <param name="dto">The dto base instance.</param>
 790    /// <param name="appHost">The Application server Host.</param>
 791    /// <param name="logger">The applogger.</param>
 792    /// <returns>The dto to map.</returns>
 793    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logge
 794    {
 70795        dto.Id = entity.Id;
 70796        dto.ParentId = entity.ParentId.GetValueOrDefault();
 70797        dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
 70798        dto.EndDate = entity.EndDate;
 70799        dto.CommunityRating = entity.CommunityRating;
 70800        dto.CustomRating = entity.CustomRating;
 70801        dto.IndexNumber = entity.IndexNumber;
 70802        dto.IsLocked = entity.IsLocked;
 70803        dto.Name = entity.Name;
 70804        dto.OfficialRating = entity.OfficialRating;
 70805        dto.Overview = entity.Overview;
 70806        dto.ParentIndexNumber = entity.ParentIndexNumber;
 70807        dto.PremiereDate = entity.PremiereDate;
 70808        dto.ProductionYear = entity.ProductionYear;
 70809        dto.SortName = entity.SortName;
 70810        dto.ForcedSortName = entity.ForcedSortName;
 70811        dto.RunTimeTicks = entity.RunTimeTicks;
 70812        dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
 70813        dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
 70814        dto.IsInMixedFolder = entity.IsInMixedFolder;
 70815        dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
 70816        dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
 70817        dto.CriticRating = entity.CriticRating;
 70818        dto.PresentationUniqueKey = entity.PresentationUniqueKey;
 70819        dto.OriginalTitle = entity.OriginalTitle;
 70820        dto.Album = entity.Album;
 70821        dto.LUFS = entity.LUFS;
 70822        dto.NormalizationGain = entity.NormalizationGain;
 70823        dto.IsVirtualItem = entity.IsVirtualItem;
 70824        dto.ExternalSeriesId = entity.ExternalSeriesId;
 70825        dto.Tagline = entity.Tagline;
 70826        dto.TotalBitrate = entity.TotalBitrate;
 70827        dto.ExternalId = entity.ExternalId;
 70828        dto.Size = entity.Size;
 70829        dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
 70830        dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 70831        dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 70832        dto.ChannelId = entity.ChannelId ?? Guid.Empty;
 70833        dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 70834        dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
 70835        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ow
 70836        dto.Width = entity.Width.GetValueOrDefault();
 70837        dto.Height = entity.Height.GetValueOrDefault();
 70838        dto.UserData = entity.UserData;
 839
 70840        if (entity.Provider is not null)
 841        {
 70842            dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
 843        }
 844
 70845        if (entity.ExtraType is not null)
 846        {
 0847            dto.ExtraType = (ExtraType)entity.ExtraType;
 848        }
 849
 70850        if (entity.LockedFields is not null)
 851        {
 70852            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
 853        }
 854
 70855        if (entity.Audio is not null)
 856        {
 0857            dto.Audio = (ProgramAudio)entity.Audio;
 858        }
 859
 70860        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Par
 70861        dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
 70862        dto.Studios = entity.Studios?.Split('|') ?? [];
 70863        dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
 864
 70865        if (dto is IHasProgramAttributes hasProgramAttributes)
 866        {
 0867            hasProgramAttributes.IsMovie = entity.IsMovie;
 0868            hasProgramAttributes.IsSeries = entity.IsSeries;
 0869            hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
 0870            hasProgramAttributes.IsRepeat = entity.IsRepeat;
 871        }
 872
 70873        if (dto is LiveTvChannel liveTvChannel)
 874        {
 0875            liveTvChannel.ServiceName = entity.ExternalServiceId;
 876        }
 877
 70878        if (dto is Trailer trailer)
 879        {
 0880            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
 881        }
 882
 70883        if (dto is Video video)
 884        {
 0885            video.PrimaryVersionId = entity.PrimaryVersionId;
 886        }
 887
 70888        if (dto is IHasSeries hasSeriesName)
 889        {
 0890            hasSeriesName.SeriesName = entity.SeriesName;
 0891            hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
 0892            hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
 893        }
 894
 70895        if (dto is Episode episode)
 896        {
 0897            episode.SeasonName = entity.SeasonName;
 0898            episode.SeasonId = entity.SeasonId.GetValueOrDefault();
 899        }
 900
 70901        if (dto is IHasArtist hasArtists)
 902        {
 0903            hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 904        }
 905
 70906        if (dto is IHasAlbumArtist hasAlbumArtists)
 907        {
 0908            hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
 909        }
 910
 70911        if (dto is LiveTvProgram program)
 912        {
 0913            program.ShowId = entity.ShowId;
 914        }
 915
 70916        if (entity.Images is not null)
 917        {
 70918            dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
 919        }
 920
 921        // dto.Type = entity.Type;
 922        // dto.Data = entity.Data;
 923        // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
 70924        if (dto is IHasStartDate hasStartDate)
 925        {
 0926            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
 927        }
 928
 929        // Fields that are present in the DB but are never actually used
 930        // dto.UnratedType = entity.UnratedType;
 931        // dto.TopParentId = entity.TopParentId;
 932        // dto.CleanName = entity.CleanName;
 933        // dto.UserDataKey = entity.UserDataKey;
 934
 70935        if (dto is Folder folder)
 936        {
 70937            folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKin
 938        }
 939
 70940        return dto;
 941    }
 942
 943    /// <summary>
 944    /// Maps a Entity to the DTO.
 945    /// </summary>
 946    /// <param name="dto">The entity.</param>
 947    /// <returns>The dto to map.</returns>
 948    public BaseItemEntity Map(BaseItemDto dto)
 949    {
 120950        var dtoType = dto.GetType();
 120951        var entity = new BaseItemEntity()
 120952        {
 120953            Type = dtoType.ToString(),
 120954            Id = dto.Id
 120955        };
 956
 120957        if (TypeRequiresDeserialization(dtoType))
 958        {
 99959            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
 960        }
 961
 120962        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
 120963        entity.Path = GetPathToSave(dto.Path);
 120964        entity.EndDate = dto.EndDate;
 120965        entity.CommunityRating = dto.CommunityRating;
 120966        entity.CustomRating = dto.CustomRating;
 120967        entity.IndexNumber = dto.IndexNumber;
 120968        entity.IsLocked = dto.IsLocked;
 120969        entity.Name = dto.Name;
 120970        entity.CleanName = GetCleanValue(dto.Name);
 120971        entity.OfficialRating = dto.OfficialRating;
 120972        entity.Overview = dto.Overview;
 120973        entity.ParentIndexNumber = dto.ParentIndexNumber;
 120974        entity.PremiereDate = dto.PremiereDate;
 120975        entity.ProductionYear = dto.ProductionYear;
 120976        entity.SortName = dto.SortName;
 120977        entity.ForcedSortName = dto.ForcedSortName;
 120978        entity.RunTimeTicks = dto.RunTimeTicks;
 120979        entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
 120980        entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
 120981        entity.IsInMixedFolder = dto.IsInMixedFolder;
 120982        entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
 120983        entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
 120984        entity.CriticRating = dto.CriticRating;
 120985        entity.PresentationUniqueKey = dto.PresentationUniqueKey;
 120986        entity.OriginalTitle = dto.OriginalTitle;
 120987        entity.Album = dto.Album;
 120988        entity.LUFS = dto.LUFS;
 120989        entity.NormalizationGain = dto.NormalizationGain;
 120990        entity.IsVirtualItem = dto.IsVirtualItem;
 120991        entity.ExternalSeriesId = dto.ExternalSeriesId;
 120992        entity.Tagline = dto.Tagline;
 120993        entity.TotalBitrate = dto.TotalBitrate;
 120994        entity.ExternalId = dto.ExternalId;
 120995        entity.Size = dto.Size;
 120996        entity.Genres = string.Join('|', dto.Genres);
 120997        entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
 120998        entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
 120999        entity.ChannelId = dto.ChannelId;
 1201000        entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
 1201001        entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
 1201002        entity.OwnerId = dto.OwnerId.ToString();
 1201003        entity.Width = dto.Width;
 1201004        entity.Height = dto.Height;
 1201005        entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
 1201006        {
 1201007            Item = entity,
 1201008            ProviderId = e.Key,
 1201009            ProviderValue = e.Value
 1201010        }).ToList();
 1011
 1201012        if (dto.Audio.HasValue)
 1013        {
 01014            entity.Audio = (ProgramAudioEntity)dto.Audio;
 1015        }
 1016
 1201017        if (dto.ExtraType.HasValue)
 1018        {
 01019            entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
 1020        }
 1021
 1201022        entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
 1201023        entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : n
 1201024        entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
 1201025        entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
 1201026        entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
 1201027            .Select(e => new BaseItemMetadataField()
 1201028            {
 1201029                Id = (int)e,
 1201030                Item = entity,
 1201031                ItemId = entity.Id
 1201032            })
 1201033            .ToArray() : null;
 1034
 1201035        if (dto is IHasProgramAttributes hasProgramAttributes)
 1036        {
 01037            entity.IsMovie = hasProgramAttributes.IsMovie;
 01038            entity.IsSeries = hasProgramAttributes.IsSeries;
 01039            entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
 01040            entity.IsRepeat = hasProgramAttributes.IsRepeat;
 1041        }
 1042
 1201043        if (dto is LiveTvChannel liveTvChannel)
 1044        {
 01045            entity.ExternalServiceId = liveTvChannel.ServiceName;
 1046        }
 1047
 1201048        if (dto is Video video)
 1049        {
 01050            entity.PrimaryVersionId = video.PrimaryVersionId;
 1051        }
 1052
 1201053        if (dto is IHasSeries hasSeriesName)
 1054        {
 01055            entity.SeriesName = hasSeriesName.SeriesName;
 01056            entity.SeriesId = hasSeriesName.SeriesId;
 01057            entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
 1058        }
 1059
 1201060        if (dto is Episode episode)
 1061        {
 01062            entity.SeasonName = episode.SeasonName;
 01063            entity.SeasonId = episode.SeasonId;
 1064        }
 1065
 1201066        if (dto is IHasArtist hasArtists)
 1067        {
 01068            entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
 1069        }
 1070
 1201071        if (dto is IHasAlbumArtist hasAlbumArtists)
 1072        {
 01073            entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtis
 1074        }
 1075
 1201076        if (dto is LiveTvProgram program)
 1077        {
 01078            entity.ShowId = program.ShowId;
 1079        }
 1080
 1201081        if (dto.ImageInfos is not null)
 1082        {
 1201083            entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
 1084        }
 1085
 1201086        if (dto is Trailer trailer)
 1087        {
 01088            entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
 01089            {
 01090                Id = (int)e,
 01091                Item = entity,
 01092                ItemId = entity.Id
 01093            }).ToArray() ?? [];
 1094        }
 1095
 1096        // dto.Type = entity.Type;
 1097        // dto.Data = entity.Data;
 1201098        entity.MediaType = dto.MediaType.ToString();
 1201099        if (dto is IHasStartDate hasStartDate)
 1100        {
 01101            entity.StartDate = hasStartDate.StartDate;
 1102        }
 1103
 1201104        entity.UnratedType = dto.GetBlockUnratedType().ToString();
 1105
 1106        // Fields that are present in the DB but are never actually used
 1107        // dto.UserDataKey = entity.UserDataKey;
 1108
 1201109        if (dto is Folder folder)
 1110        {
 1201111            entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdde
 1201112            entity.IsFolder = folder.IsFolder;
 1113        }
 1114
 1201115        return entity;
 1116    }
 1117
 1118    private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes,
 1119    {
 641120        using var context = _dbProvider.CreateDbContext();
 1121
 641122        var query = context.ItemValuesMap
 641123            .AsNoTracking()
 641124            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
 641125        if (withItemTypes.Count > 0)
 1126        {
 161127            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
 1128        }
 1129
 641130        if (excludeItemTypes.Count > 0)
 1131        {
 161132            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
 1133        }
 1134
 1135        // query = query.DistinctBy(e => e.CleanValue);
 641136        return query.Select(e => e.ItemValue)
 641137            .GroupBy(e => e.CleanValue)
 641138            .Select(e => e.First().Value)
 641139            .ToArray();
 641140    }
 1141
 1142    private static bool TypeRequiresDeserialization(Type type)
 1143    {
 1901144        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
 1145    }
 1146
 1147    private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
 1148    {
 701149        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
 701150        if (_serverConfigurationManager?.Configuration is null)
 1151        {
 01152            throw new InvalidOperationException("Server Configuration manager or configuration is null");
 1153        }
 1154
 701155        var typeToSerialise = GetType(baseItemEntity.Type);
 701156        return BaseItemRepository.DeserializeBaseItem(
 701157            baseItemEntity,
 701158            _logger,
 701159            _appHost,
 701160            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeT
 1161    }
 1162
 1163    /// <summary>
 1164    /// Deserializes a BaseItemEntity and sets all properties.
 1165    /// </summary>
 1166    /// <param name="baseItemEntity">The DB entity.</param>
 1167    /// <param name="logger">Logger.</param>
 1168    /// <param name="appHost">The application server Host.</param>
 1169    /// <param name="skipDeserialization">If only mapping should be processed.</param>
 1170    /// <returns>A mapped BaseItem.</returns>
 1171    /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
 1172    public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost?
 1173    {
 701174        var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.
 701175        BaseItemDto? dto = null;
 701176        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
 1177        {
 1178            try
 1179            {
 111180                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
 111181            }
 01182            catch (JsonException ex)
 1183            {
 01184                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
 01185            }
 1186        }
 1187
 701188        if (dto is null)
 1189        {
 591190            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deseriali
 1191        }
 1192
 701193        return Map(baseItemEntity, dto, appHost, logger);
 1194    }
 1195
 1196    private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyLi
 1197    {
 01198        ArgumentNullException.ThrowIfNull(filter);
 1199
 01200        if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
 1201        {
 01202            filter.EnableTotalRecordCount = false;
 1203        }
 1204
 01205        using var context = _dbProvider.CreateDbContext();
 1206
 01207        var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context,
 01208        {
 01209            ExcludeItemTypes = filter.ExcludeItemTypes,
 01210            IncludeItemTypes = filter.IncludeItemTypes,
 01211            MediaTypes = filter.MediaTypes,
 01212            AncestorIds = filter.AncestorIds,
 01213            ItemIds = filter.ItemIds,
 01214            TopParentIds = filter.TopParentIds,
 01215            ParentId = filter.ParentId,
 01216            IsAiring = filter.IsAiring,
 01217            IsMovie = filter.IsMovie,
 01218            IsSports = filter.IsSports,
 01219            IsKids = filter.IsKids,
 01220            IsNews = filter.IsNews,
 01221            IsSeries = filter.IsSeries
 01222        });
 1223
 01224        var itemValuesQuery = context.ItemValues
 01225            .Where(f => itemValueTypes.Contains(f.Type))
 01226            .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
 01227            .Join(
 01228                innerQueryFilter,
 01229                fw => fw.w.ItemId,
 01230                g => g.Id,
 01231                (fw, g) => fw.f.CleanValue);
 1232
 01233        var innerQuery = PrepareItemQuery(context, filter)
 01234            .Where(e => e.Type == returnType)
 01235            .Where(e => itemValuesQuery.Contains(e.CleanName));
 1236
 01237        var outerQueryFilter = new InternalItemsQuery(filter.User)
 01238        {
 01239            IsPlayed = filter.IsPlayed,
 01240            IsFavorite = filter.IsFavorite,
 01241            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
 01242            IsLiked = filter.IsLiked,
 01243            IsLocked = filter.IsLocked,
 01244            NameLessThan = filter.NameLessThan,
 01245            NameStartsWith = filter.NameStartsWith,
 01246            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
 01247            Tags = filter.Tags,
 01248            OfficialRatings = filter.OfficialRatings,
 01249            StudioIds = filter.StudioIds,
 01250            GenreIds = filter.GenreIds,
 01251            Genres = filter.Genres,
 01252            Years = filter.Years,
 01253            NameContains = filter.NameContains,
 01254            SearchTerm = filter.SearchTerm,
 01255            ExcludeItemIds = filter.ExcludeItemIds
 01256        };
 1257
 01258        var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
 01259            .GroupBy(e => e.PresentationUniqueKey)
 01260            .Select(e => e.FirstOrDefault())
 01261            .Select(e => e!.Id);
 1262
 01263        var query = context.BaseItems
 01264            .Include(e => e.TrailerTypes)
 01265            .Include(e => e.Provider)
 01266            .Include(e => e.LockedFields)
 01267            .Include(e => e.Images)
 01268            .AsSingleQuery()
 01269            .Where(e => masterQuery.Contains(e.Id));
 1270
 01271        query = ApplyOrder(query, filter, context);
 1272
 01273        var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
 01274        if (filter.EnableTotalRecordCount)
 1275        {
 01276            result.TotalRecordCount = query.Count();
 1277        }
 1278
 01279        if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
 1280        {
 01281            query = query.Skip(filter.StartIndex.Value);
 1282        }
 1283
 01284        if (filter.Limit.HasValue && filter.Limit.Value > 0)
 1285        {
 01286            query = query.Take(filter.Limit.Value);
 1287        }
 1288
 01289        IQueryable<BaseItemEntity>? itemCountQuery = null;
 1290
 01291        if (filter.IncludeItemTypes.Length > 0)
 1292        {
 1293            // if we are to include more then one type, sub query those items beforehand.
 1294
 01295            var typeSubQuery = new InternalItemsQuery(filter.User)
 01296            {
 01297                ExcludeItemTypes = filter.ExcludeItemTypes,
 01298                IncludeItemTypes = filter.IncludeItemTypes,
 01299                MediaTypes = filter.MediaTypes,
 01300                AncestorIds = filter.AncestorIds,
 01301                ExcludeItemIds = filter.ExcludeItemIds,
 01302                ItemIds = filter.ItemIds,
 01303                TopParentIds = filter.TopParentIds,
 01304                ParentId = filter.ParentId,
 01305                IsPlayed = filter.IsPlayed
 01306            };
 1307
 01308            itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderI
 01309                .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
 1310
 01311            var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
 01312            var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
 01313            var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
 01314            var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
 01315            var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
 01316            var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
 01317            var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
 1318
 01319            var resultQuery = query.Select(e => new
 01320            {
 01321                item = e,
 01322                // TODO: This is bad refactor!
 01323                itemCount = new ItemCounts()
 01324                {
 01325                    SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
 01326                    EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
 01327                    MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
 01328                    AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
 01329                    ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
 01330                    SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
 01331                    TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
 01332                }
 01333            });
 1334
 01335            result.StartIndex = filter.StartIndex ?? 0;
 01336            result.Items =
 01337            [
 01338                .. resultQuery
 01339                    .AsEnumerable()
 01340                    .Where(e => e is not null)
 01341                    .Select(e =>
 01342                    {
 01343                        return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
 01344                    })
 01345            ];
 1346        }
 1347        else
 1348        {
 01349            result.StartIndex = filter.StartIndex ?? 0;
 01350            result.Items =
 01351            [
 01352                .. query
 01353                    .AsEnumerable()
 01354                    .Where(e => e is not null)
 01355                    .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
 01356                    {
 01357                        return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
 01358                    })
 01359            ];
 1360        }
 1361
 01362        return result;
 01363    }
 1364
 1365    private static void PrepareFilterQuery(InternalItemsQuery query)
 1366    {
 3341367        if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
 1368        {
 01369            query.Limit = query.Limit.Value + 4;
 1370        }
 1371
 3341372        if (query.IsResumable ?? false)
 1373        {
 11374            query.IsVirtualItem = false;
 1375        }
 3341376    }
 1377
 1378    /// <summary>
 1379    /// Gets the clean value for search and sorting purposes.
 1380    /// </summary>
 1381    /// <param name="value">The value to clean.</param>
 1382    /// <returns>The cleaned value.</returns>
 1383    public static string GetCleanValue(string value)
 1384    {
 1231385        if (string.IsNullOrWhiteSpace(value))
 1386        {
 01387            return value;
 1388        }
 1389
 1231390        var noDiacritics = value.RemoveDiacritics();
 1391
 1392        // Build a string where any punctuation or symbol is treated as a separator (space).
 1231393        var sb = new StringBuilder(noDiacritics.Length);
 1231394        var previousWasSpace = false;
 20541395        foreach (var ch in noDiacritics)
 1396        {
 1397            char outCh;
 9041398            if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
 1399            {
 8821400                outCh = ch;
 1401            }
 1402            else
 1403            {
 221404                outCh = ' ';
 1405            }
 1406
 1407            // normalize any whitespace character to a single ASCII space.
 9041408            if (char.IsWhiteSpace(outCh))
 1409            {
 501410                if (!previousWasSpace)
 1411                {
 391412                    sb.Append(' ');
 391413                    previousWasSpace = true;
 1414                }
 1415            }
 1416            else
 1417            {
 8541418                sb.Append(outCh);
 8541419                previousWasSpace = false;
 1420            }
 1421        }
 1422
 1423        // trim leading/trailing spaces that may have been added.
 1231424        var collapsed = sb.ToString().Trim();
 1231425        return collapsed.ToLowerInvariant();
 1426    }
 1427
 1428    private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inherited
 1429    {
 1101430        var list = new List<(ItemValueType, string)>();
 1431
 1101432        if (item is IHasArtist hasArtist)
 1433        {
 01434            list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
 1435        }
 1436
 1101437        if (item is IHasAlbumArtist hasAlbumArtist)
 1438        {
 01439            list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
 1440        }
 1441
 1101442        list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
 1101443        list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
 1101444        list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
 1445
 1446        // keywords was 5
 1447
 1101448        list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
 1449
 1450        // Remove all invalid values.
 1101451        list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
 1452
 1101453        return list;
 1454    }
 1455
 1456    private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
 1457    {
 01458        return new BaseItemImageInfo()
 01459        {
 01460            ItemId = baseItemId,
 01461            Id = Guid.NewGuid(),
 01462            Path = e.Path,
 01463            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
 01464            DateModified = e.DateModified,
 01465            Height = e.Height,
 01466            Width = e.Width,
 01467            ImageType = (ImageInfoImageType)e.Type,
 01468            Item = null!
 01469        };
 1470    }
 1471
 1472    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
 1473    {
 01474        return new ItemImageInfo()
 01475        {
 01476            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
 01477            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
 01478            DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
 01479            Height = e.Height,
 01480            Width = e.Width,
 01481            Type = (ImageType)e.ImageType
 01482        };
 1483    }
 1484
 1485    private string? GetPathToSave(string path)
 1486    {
 1201487        if (path is null)
 1488        {
 101489            return null;
 1490        }
 1491
 1101492        return _appHost.ReverseVirtualPath(path);
 1493    }
 1494
 1495    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
 1496    {
 121497        var list = new List<string>();
 1498
 121499        if (IsTypeInQuery(BaseItemKind.Person, query))
 1500        {
 11501            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
 1502        }
 1503
 121504        if (IsTypeInQuery(BaseItemKind.Genre, query))
 1505        {
 11506            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
 1507        }
 1508
 121509        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
 1510        {
 11511            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
 1512        }
 1513
 121514        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
 1515        {
 11516            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
 1517        }
 1518
 121519        if (IsTypeInQuery(BaseItemKind.Studio, query))
 1520        {
 11521            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
 1522        }
 1523
 121524        return list;
 1525    }
 1526
 1527    private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
 1528    {
 601529        if (query.ExcludeItemTypes.Contains(type))
 1530        {
 01531            return false;
 1532        }
 1533
 601534        return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
 1535    }
 1536
 1537    private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
 1538    {
 3341539        if (!query.GroupByPresentationUniqueKey)
 1540        {
 1421541            return false;
 1542        }
 1543
 1921544        if (query.GroupBySeriesPresentationUniqueKey)
 1545        {
 01546            return false;
 1547        }
 1548
 1921549        if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
 1550        {
 01551            return false;
 1552        }
 1553
 1921554        if (query.User is null)
 1555        {
 1901556            return false;
 1557        }
 1558
 21559        if (query.IncludeItemTypes.Length == 0)
 1560        {
 11561            return true;
 1562        }
 1563
 11564        return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
 11565            || query.IncludeItemTypes.Contains(BaseItemKind.Video)
 11566            || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
 11567            || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
 11568            || query.IncludeItemTypes.Contains(BaseItemKind.Series)
 11569            || query.IncludeItemTypes.Contains(BaseItemKind.Season);
 1570    }
 1571
 1572    private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinD
 1573    {
 3341574        var orderBy = filter.OrderBy;
 3341575        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
 1576
 3341577        if (hasSearch)
 1578        {
 01579            orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
 1580        }
 3341581        else if (orderBy.Count == 0)
 1582        {
 2311583            return query.OrderBy(e => e.SortName);
 1584        }
 1585
 1031586        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
 1587
 1031588        var firstOrdering = orderBy.FirstOrDefault();
 1031589        if (firstOrdering != default)
 1590        {
 1031591            var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
 1031592            if (firstOrdering.SortOrder == SortOrder.Ascending)
 1593            {
 1021594                orderedQuery = query.OrderBy(expression);
 1595            }
 1596            else
 1597            {
 11598                orderedQuery = query.OrderByDescending(expression);
 1599            }
 1600
 1031601            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
 1602            {
 01603                if (firstOrdering.SortOrder is SortOrder.Ascending)
 1604                {
 01605                    orderedQuery = orderedQuery.ThenBy(e => e.Name);
 1606                }
 1607                else
 1608                {
 01609                    orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
 1610                }
 1611            }
 1612        }
 1613
 2921614        foreach (var item in orderBy.Skip(1))
 1615        {
 431616            var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
 431617            if (item.SortOrder == SortOrder.Ascending)
 1618            {
 431619                orderedQuery = orderedQuery!.ThenBy(expression);
 1620            }
 1621            else
 1622            {
 01623                orderedQuery = orderedQuery!.ThenByDescending(expression);
 1624            }
 1625        }
 1626
 1031627        return orderedQuery ?? query;
 1628    }
 1629
 1630    private IQueryable<BaseItemEntity> TranslateQuery(
 1631        IQueryable<BaseItemEntity> baseQuery,
 1632        JellyfinDbContext context,
 1633        InternalItemsQuery filter)
 1634    {
 1635        const int HDWidth = 1200;
 1636        const int UHDWidth = 3800;
 1637        const int UHDHeight = 2100;
 1638
 3341639        var minWidth = filter.MinWidth;
 3341640        var maxWidth = filter.MaxWidth;
 3341641        var now = DateTime.UtcNow;
 1642
 3341643        if (filter.IsHD.HasValue || filter.Is4K.HasValue)
 1644        {
 01645            bool includeSD = false;
 01646            bool includeHD = false;
 01647            bool include4K = false;
 1648
 01649            if (filter.IsHD.HasValue && !filter.IsHD.Value)
 1650            {
 01651                includeSD = true;
 1652            }
 1653
 01654            if (filter.IsHD.HasValue && filter.IsHD.Value)
 1655            {
 01656                includeHD = true;
 1657            }
 1658
 01659            if (filter.Is4K.HasValue && filter.Is4K.Value)
 1660            {
 01661                include4K = true;
 1662            }
 1663
 01664            baseQuery = baseQuery.Where(e =>
 01665                (includeSD && e.Width < HDWidth) ||
 01666                (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
 01667                (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
 1668        }
 1669
 3341670        if (minWidth.HasValue)
 1671        {
 01672            baseQuery = baseQuery.Where(e => e.Width >= minWidth);
 1673        }
 1674
 3341675        if (filter.MinHeight.HasValue)
 1676        {
 01677            baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
 1678        }
 1679
 3341680        if (maxWidth.HasValue)
 1681        {
 01682            baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
 1683        }
 1684
 3341685        if (filter.MaxHeight.HasValue)
 1686        {
 01687            baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
 1688        }
 1689
 3341690        if (filter.IsLocked.HasValue)
 1691        {
 481692            baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
 1693        }
 1694
 3341695        var tags = filter.Tags.ToList();
 3341696        var excludeTags = filter.ExcludeTags.ToList();
 1697
 3341698        if (filter.IsMovie.HasValue)
 1699        {
 01700            var shouldIncludeAllMovieTypes = filter.IsMovie.Value
 01701                && (filter.IncludeItemTypes.Length == 0
 01702                    || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
 01703                    || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
 1704
 01705            if (!shouldIncludeAllMovieTypes)
 1706            {
 01707                baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
 1708            }
 1709        }
 1710
 3341711        if (filter.IsSeries.HasValue)
 1712        {
 01713            baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
 1714        }
 1715
 3341716        if (filter.IsSports.HasValue)
 1717        {
 01718            if (filter.IsSports.Value)
 1719            {
 01720                tags.Add("Sports");
 1721            }
 1722            else
 1723            {
 01724                excludeTags.Add("Sports");
 1725            }
 1726        }
 1727
 3341728        if (filter.IsNews.HasValue)
 1729        {
 01730            if (filter.IsNews.Value)
 1731            {
 01732                tags.Add("News");
 1733            }
 1734            else
 1735            {
 01736                excludeTags.Add("News");
 1737            }
 1738        }
 1739
 3341740        if (filter.IsKids.HasValue)
 1741        {
 01742            if (filter.IsKids.Value)
 1743            {
 01744                tags.Add("Kids");
 1745            }
 1746            else
 1747            {
 01748                excludeTags.Add("Kids");
 1749            }
 1750        }
 1751
 3341752        if (!string.IsNullOrEmpty(filter.SearchTerm))
 1753        {
 01754            var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
 01755            var originalSearchTerm = filter.SearchTerm.ToLower();
 01756            if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
 1757            {
 01758                cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
 01759                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle 
 1760            }
 1761            else
 1762            {
 01763                baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null &&
 1764            }
 1765        }
 1766
 3341767        if (filter.IsFolder.HasValue)
 1768        {
 211769            baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
 1770        }
 1771
 3341772        var includeTypes = filter.IncludeItemTypes;
 1773
 1774        // Only specify excluded types if no included types are specified
 3341775        if (filter.IncludeItemTypes.Length == 0)
 1776        {
 2231777            var excludeTypes = filter.ExcludeItemTypes;
 2231778            if (excludeTypes.Length == 1)
 1779            {
 01780                if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
 1781                {
 01782                    baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
 1783                }
 1784            }
 2231785            else if (excludeTypes.Length > 1)
 1786            {
 01787                var excludeTypeName = new List<string>();
 01788                foreach (var excludeType in excludeTypes)
 1789                {
 01790                    if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
 1791                    {
 01792                        excludeTypeName.Add(baseItemKindName!);
 1793                    }
 1794                }
 1795
 01796                baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
 1797            }
 1798        }
 1799        else
 1800        {
 1111801            string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e =>
 1111802            baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
 1803        }
 1804
 3341805        if (filter.ChannelIds.Count > 0)
 1806        {
 01807            baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
 1808        }
 1809
 3341810        if (!filter.ParentId.IsEmpty())
 1811        {
 1421812            baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
 1813        }
 1814
 3341815        if (!string.IsNullOrWhiteSpace(filter.Path))
 1816        {
 01817            var pathToQuery = GetPathToSave(filter.Path);
 01818            baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
 1819        }
 1820
 3341821        if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
 1822        {
 01823            baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
 1824        }
 1825
 3341826        if (filter.MinCommunityRating.HasValue)
 1827        {
 01828            baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
 1829        }
 1830
 3341831        if (filter.MinIndexNumber.HasValue)
 1832        {
 01833            baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
 1834        }
 1835
 3341836        if (filter.MinParentAndIndexNumber.HasValue)
 1837        {
 01838            baseQuery = baseQuery
 01839                .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNum
 1840        }
 1841
 3341842        if (filter.MinDateCreated.HasValue)
 1843        {
 01844            baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
 1845        }
 1846
 3341847        if (filter.MinDateLastSaved.HasValue)
 1848        {
 01849            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value
 1850        }
 1851
 3341852        if (filter.MinDateLastSavedForUser.HasValue)
 1853        {
 01854            baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUse
 1855        }
 1856
 3341857        if (filter.IndexNumber.HasValue)
 1858        {
 01859            baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
 1860        }
 1861
 3341862        if (filter.ParentIndexNumber.HasValue)
 1863        {
 01864            baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
 1865        }
 1866
 3341867        if (filter.ParentIndexNumberNotEquals.HasValue)
 1868        {
 01869            baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentI
 1870        }
 1871
 3341872        var minEndDate = filter.MinEndDate;
 3341873        var maxEndDate = filter.MaxEndDate;
 1874
 3341875        if (filter.HasAired.HasValue)
 1876        {
 01877            if (filter.HasAired.Value)
 1878            {
 01879                maxEndDate = DateTime.UtcNow;
 1880            }
 1881            else
 1882            {
 01883                minEndDate = DateTime.UtcNow;
 1884            }
 1885        }
 1886
 3341887        if (minEndDate.HasValue)
 1888        {
 01889            baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
 1890        }
 1891
 3341892        if (maxEndDate.HasValue)
 1893        {
 01894            baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
 1895        }
 1896
 3341897        if (filter.MinStartDate.HasValue)
 1898        {
 01899            baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
 1900        }
 1901
 3341902        if (filter.MaxStartDate.HasValue)
 1903        {
 01904            baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
 1905        }
 1906
 3341907        if (filter.MinPremiereDate.HasValue)
 1908        {
 01909            baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
 1910        }
 1911
 3341912        if (filter.MaxPremiereDate.HasValue)
 1913        {
 01914            baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
 1915        }
 1916
 3341917        if (filter.TrailerTypes.Length > 0)
 1918        {
 01919            var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
 01920            baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
 1921        }
 1922
 3341923        if (filter.IsAiring.HasValue)
 1924        {
 01925            if (filter.IsAiring.Value)
 1926            {
 01927                baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
 1928            }
 1929            else
 1930            {
 01931                baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
 1932            }
 1933        }
 1934
 3341935        if (filter.PersonIds.Length > 0)
 1936        {
 01937            var peopleEntityIds = context.BaseItems
 01938                .WhereOneOrMany(filter.PersonIds, b => b.Id)
 01939                .Join(
 01940                    context.Peoples,
 01941                    b => b.Name,
 01942                    p => p.Name,
 01943                    (b, p) => p.Id);
 1944
 01945            baseQuery = baseQuery
 01946                .Where(e => context.PeopleBaseItemMap
 01947                    .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
 1948        }
 1949
 3341950        if (!string.IsNullOrWhiteSpace(filter.Person))
 1951        {
 01952            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
 1953        }
 1954
 3341955        if (!string.IsNullOrWhiteSpace(filter.MinSortName))
 1956        {
 1957            // this does not makes sense.
 1958            // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
 1959            // whereClauses.Add("SortName>=@MinSortName");
 1960            // statement?.TryBind("@MinSortName", query.MinSortName);
 1961        }
 1962
 3341963        if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
 1964        {
 01965            baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
 1966        }
 1967
 3341968        if (!string.IsNullOrWhiteSpace(filter.ExternalId))
 1969        {
 01970            baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
 1971        }
 1972
 3341973        if (!string.IsNullOrWhiteSpace(filter.Name))
 1974        {
 31975            var cleanName = GetCleanValue(filter.Name);
 31976            baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
 1977        }
 1978
 1979        // These are the same, for now
 3341980        var nameContains = filter.NameContains;
 3341981        if (!string.IsNullOrWhiteSpace(nameContains))
 1982        {
 01983            if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
 1984            {
 01985                nameContains = $"%{nameContains.Trim('%')}%";
 01986                baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.Ori
 1987            }
 1988            else
 1989            {
 01990                baseQuery = baseQuery.Where(e =>
 01991                                    e.CleanName!.Contains(nameContains)
 01992                                    || e.OriginalTitle!.ToLower().Contains(nameContains!));
 1993            }
 1994        }
 1995
 3341996        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
 1997        {
 01998            var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
 01999            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
 2000        }
 2001
 3342002        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
 2003        {
 02004            var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
 02005            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
 2006        }
 2007
 3342008        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
 2009        {
 02010            var lessThanLower = filter.NameLessThan.ToLowerInvariant();
 02011            baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
 2012        }
 2013
 3342014        if (filter.ImageTypes.Length > 0)
 2015        {
 1022016            var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
 1022017            baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
 2018        }
 2019
 3342020        if (filter.IsLiked.HasValue)
 2021        {
 02022            baseQuery = baseQuery
 02023                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLike
 2024        }
 2025
 3342026        if (filter.IsFavoriteOrLiked.HasValue)
 2027        {
 02028            baseQuery = baseQuery
 02029                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2030        }
 2031
 3342032        if (filter.IsFavorite.HasValue)
 2033        {
 02034            baseQuery = baseQuery
 02035                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorit
 2036        }
 2037
 3342038        if (filter.IsPlayed.HasValue)
 2039        {
 2040            // We should probably figure this out for all folders, but for right now, this is the only place where we ne
 02041            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
 2042            {
 02043                baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
 02044                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
 02045                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
 02046                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
 2047            }
 2048            else
 2049            {
 02050                baseQuery = baseQuery
 02051                    .Select(e => new
 02052                    {
 02053                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).Firs
 02054                        Item = e
 02055                    })
 02056                    .Where(e => e.IsPlayed == filter.IsPlayed)
 02057                    .Select(f => f.Item);
 2058            }
 2059        }
 2060
 3342061        if (filter.IsResumable.HasValue)
 2062        {
 12063            if (filter.IsResumable.Value)
 2064            {
 12065                baseQuery = baseQuery
 12066                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks >
 2067            }
 2068            else
 2069            {
 02070                baseQuery = baseQuery
 02071                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks =
 2072            }
 2073        }
 2074
 3342075        if (filter.ArtistIds.Length > 0)
 2076        {
 02077            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2078        }
 2079
 3342080        if (filter.AlbumArtistIds.Length > 0)
 2081        {
 02082            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
 2083        }
 2084
 3342085        if (filter.ContributingArtistIds.Length > 0)
 2086        {
 02087            var contributingNames = context.BaseItems
 02088                .Where(b => filter.ContributingArtistIds.Contains(b.Id))
 02089                .Select(b => b.CleanName);
 2090
 02091            baseQuery = baseQuery.Where(e =>
 02092                e.ItemValues!.Any(ivm =>
 02093                    ivm.ItemValue.Type == ItemValueType.Artist &&
 02094                    contributingNames.Contains(ivm.ItemValue.CleanValue))
 02095                &&
 02096                !e.ItemValues!.Any(ivm =>
 02097                    ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
 02098                    contributingNames.Contains(ivm.ItemValue.CleanValue)));
 2099        }
 2100
 3342101        if (filter.AlbumIds.Length > 0)
 2102        {
 02103            var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
 02104            baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
 2105        }
 2106
 3342107        if (filter.ExcludeArtistIds.Length > 0)
 2108        {
 02109            baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumAr
 2110        }
 2111
 3342112        if (filter.GenreIds.Count > 0)
 2113        {
 02114            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
 2115        }
 2116
 3342117        if (filter.Genres.Count > 0)
 2118        {
 02119            var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValue
 02120            baseQuery = baseQuery
 02121                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(clea
 2122        }
 2123
 3342124        if (tags.Count > 0)
 2125        {
 02126            var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, stri
 02127            baseQuery = baseQuery
 02128                    .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clean
 2129        }
 2130
 3342131        if (excludeTags.Count > 0)
 2132        {
 02133            var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMa
 02134            baseQuery = baseQuery
 02135                    .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(clea
 2136        }
 2137
 3342138        if (filter.StudioIds.Length > 0)
 2139        {
 02140            baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
 2141        }
 2142
 3342143        if (filter.OfficialRatings.Length > 0)
 2144        {
 02145            baseQuery = baseQuery
 02146                   .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
 2147        }
 2148
 3342149        Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
 3342150        if (filter.MinParentalRating != null)
 2151        {
 02152            var min = filter.MinParentalRating;
 02153            var minScore = min.Score;
 02154            var minSubScore = min.SubScore ?? 0;
 2155
 02156            minParentalRatingFilter = e =>
 02157                e.InheritedParentalRatingValue == null ||
 02158                e.InheritedParentalRatingValue > minScore ||
 02159                (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
 2160        }
 2161
 3342162        Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
 3342163        if (filter.MaxParentalRating != null)
 2164        {
 482165            var max = filter.MaxParentalRating;
 482166            var maxScore = max.Score;
 482167            var maxSubScore = max.SubScore ?? 0;
 2168
 482169            maxParentalRatingFilter = e =>
 482170                e.InheritedParentalRatingValue == null ||
 482171                e.InheritedParentalRatingValue < maxScore ||
 482172                (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
 2173        }
 2174
 3342175        if (filter.HasParentalRating ?? false)
 2176        {
 02177            if (minParentalRatingFilter != null)
 2178            {
 02179                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2180            }
 2181
 02182            if (maxParentalRatingFilter != null)
 2183            {
 02184                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2185            }
 2186        }
 3342187        else if (filter.BlockUnratedItems.Length > 0)
 2188        {
 02189            var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
 02190            Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !u
 2191
 02192            if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
 2193            {
 02194                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)))
 2195            }
 02196            else if (minParentalRatingFilter != null)
 2197            {
 02198                baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
 2199            }
 02200            else if (maxParentalRatingFilter != null)
 2201            {
 02202                baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
 2203            }
 2204            else
 2205            {
 02206                baseQuery = baseQuery.Where(unratedItemFilter);
 2207            }
 2208        }
 3342209        else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
 2210        {
 482211            if (minParentalRatingFilter != null)
 2212            {
 02213                baseQuery = baseQuery.Where(minParentalRatingFilter);
 2214            }
 2215
 482216            if (maxParentalRatingFilter != null)
 2217            {
 482218                baseQuery = baseQuery.Where(maxParentalRatingFilter);
 2219            }
 2220        }
 2862221        else if (!filter.HasParentalRating ?? false)
 2222        {
 02223            baseQuery = baseQuery
 02224                .Where(e => e.InheritedParentalRatingValue == null);
 2225        }
 2226
 3342227        if (filter.HasOfficialRating.HasValue)
 2228        {
 02229            if (filter.HasOfficialRating.Value)
 2230            {
 02231                baseQuery = baseQuery
 02232                    .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
 2233            }
 2234            else
 2235            {
 02236                baseQuery = baseQuery
 02237                    .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
 2238            }
 2239        }
 2240
 3342241        if (filter.HasOverview.HasValue)
 2242        {
 02243            if (filter.HasOverview.Value)
 2244            {
 02245                baseQuery = baseQuery
 02246                    .Where(e => e.Overview != null && e.Overview != string.Empty);
 2247            }
 2248            else
 2249            {
 02250                baseQuery = baseQuery
 02251                    .Where(e => e.Overview == null || e.Overview == string.Empty);
 2252            }
 2253        }
 2254
 3342255        if (filter.HasOwnerId.HasValue)
 2256        {
 02257            if (filter.HasOwnerId.Value)
 2258            {
 02259                baseQuery = baseQuery
 02260                    .Where(e => e.OwnerId != null);
 2261            }
 2262            else
 2263            {
 02264                baseQuery = baseQuery
 02265                    .Where(e => e.OwnerId == null);
 2266            }
 2267        }
 2268
 3342269        if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
 2270        {
 02271            baseQuery = baseQuery
 02272                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filte
 2273        }
 2274
 3342275        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
 2276        {
 02277            baseQuery = baseQuery
 02278                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal &&
 2279        }
 2280
 3342281        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
 2282        {
 02283            baseQuery = baseQuery
 02284                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && 
 2285        }
 2286
 3342287        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
 2288        {
 02289            baseQuery = baseQuery
 02290                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == fi
 2291        }
 2292
 3342293        if (filter.HasSubtitles.HasValue)
 2294        {
 02295            baseQuery = baseQuery
 02296                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtit
 2297        }
 2298
 3342299        if (filter.HasChapterImages.HasValue)
 2300        {
 02301            baseQuery = baseQuery
 02302                .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
 2303        }
 2304
 3342305        if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
 2306        {
 162307            baseQuery = baseQuery
 162308                .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any
 2309        }
 2310
 3342311        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
 2312        {
 162313            baseQuery = baseQuery
 162314                    .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Val
 2315        }
 2316
 3342317        if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
 2318        {
 162319            baseQuery = baseQuery
 162320                    .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value 
 2321        }
 2322
 3342323        if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
 2324        {
 162325            baseQuery = baseQuery
 162326                    .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value ==
 2327        }
 2328
 3342329        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
 2330        {
 02331            baseQuery = baseQuery
 02332                .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
 2333        }
 2334
 3342335        if (filter.Years.Length > 0)
 2336        {
 02337            baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
 2338        }
 2339
 3342340        var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
 3342341        if (isVirtualItem.HasValue)
 2342        {
 222343            baseQuery = baseQuery
 222344                .Where(e => e.IsVirtualItem == isVirtualItem.Value);
 2345        }
 2346
 3342347        if (filter.IsSpecialSeason.HasValue)
 2348        {
 02349            if (filter.IsSpecialSeason.Value)
 2350            {
 02351                baseQuery = baseQuery
 02352                    .Where(e => e.IndexNumber == 0);
 2353            }
 2354            else
 2355            {
 02356                baseQuery = baseQuery
 02357                    .Where(e => e.IndexNumber != 0);
 2358            }
 2359        }
 2360
 3342361        if (filter.IsUnaired.HasValue)
 2362        {
 02363            if (filter.IsUnaired.Value)
 2364            {
 02365                baseQuery = baseQuery
 02366                    .Where(e => e.PremiereDate >= now);
 2367            }
 2368            else
 2369            {
 02370                baseQuery = baseQuery
 02371                    .Where(e => e.PremiereDate < now);
 2372            }
 2373        }
 2374
 3342375        if (filter.MediaTypes.Length > 0)
 2376        {
 212377            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
 212378            baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
 2379        }
 2380
 3342381        if (filter.ItemIds.Length > 0)
 2382        {
 02383            baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
 2384        }
 2385
 3342386        if (filter.ExcludeItemIds.Length > 0)
 2387        {
 02388            baseQuery = baseQuery
 02389                .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
 2390        }
 2391
 3342392        if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
 2393        {
 02394            var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
 02395            baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !ex
 2396        }
 2397
 3342398        if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
 2399        {
 2400            // Allow setting a null or empty value to get all items that have the specified provider set.
 02401            var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArra
 02402            if (includeAny.Length > 0)
 2403            {
 02404                baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
 2405            }
 2406
 02407            var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Ke
 02408            if (includeSelected.Length > 0)
 2409            {
 02410                baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f =>
 2411            }
 2412        }
 2413
 3342414        if (filter.HasImdbId.HasValue)
 2415        {
 02416            baseQuery = filter.HasImdbId.Value
 02417                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().T
 02418                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().T
 2419        }
 2420
 3342421        if (filter.HasTmdbId.HasValue)
 2422        {
 02423            baseQuery = filter.HasTmdbId.Value
 02424                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().T
 02425                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().T
 2426        }
 2427
 3342428        if (filter.HasTvdbId.HasValue)
 2429        {
 02430            baseQuery = filter.HasTvdbId.Value
 02431                ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().T
 02432                : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().T
 2433        }
 2434
 3342435        var queryTopParentIds = filter.TopParentIds;
 2436
 3342437        if (queryTopParentIds.Length > 0)
 2438        {
 122439            var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
 122440            var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
 122441            if (enableItemsByName && includedItemByNameTypes.Count > 0)
 2442            {
 02443                baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => 
 2444            }
 2445            else
 2446            {
 122447                baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
 2448            }
 2449        }
 2450
 3342451        if (filter.AncestorIds.Length > 0)
 2452        {
 442453            baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
 2454        }
 2455
 3342456        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
 2457        {
 02458            baseQuery = baseQuery
 02459                .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUn
 2460        }
 2461
 3342462        if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
 2463        {
 02464            baseQuery = baseQuery
 02465                .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
 2466        }
 2467
 3342468        if (filter.ExcludeInheritedTags.Length > 0)
 2469        {
 02470            baseQuery = baseQuery.Where(e =>
 02471                !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f
 02472                && (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
 02473                !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags &
 2474        }
 2475
 3342476        if (filter.IncludeInheritedTags.Length > 0)
 2477        {
 2478            // For seasons and episodes, we also need to check the parent series' tags.
 02479            if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
 2480            {
 02481                baseQuery = baseQuery.Where(e =>
 02482                    e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contain
 02483                    || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValu
 2484            }
 2485
 2486            // A playlist should be accessible to its owner regardless of allowed tags.
 02487            else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
 2488            {
 02489                baseQuery = baseQuery.Where(e =>
 02490                    e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contain
 02491                    || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
 2492                // d        ^^ this is stupid it hate this.
 2493            }
 2494            else
 2495            {
 02496                baseQuery = baseQuery.Where(e =>
 02497                    e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contain
 2498            }
 2499        }
 2500
 3342501        if (filter.SeriesStatuses.Length > 0)
 2502        {
 02503            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
 02504            baseQuery = baseQuery
 02505                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
 2506        }
 2507
 3342508        if (filter.BoxSetLibraryFolders.Length > 0)
 2509        {
 02510            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).T
 02511            baseQuery = baseQuery
 02512                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
 2513        }
 2514
 3342515        if (filter.VideoTypes.Length > 0)
 2516        {
 02517            var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
 02518            baseQuery = baseQuery
 02519                .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
 2520        }
 2521
 3342522        if (filter.Is3D.HasValue)
 2523        {
 02524            if (filter.Is3D.Value)
 2525            {
 02526                baseQuery = baseQuery
 02527                    .Where(e => e.Data!.Contains("Video3DFormat"));
 2528            }
 2529            else
 2530            {
 02531                baseQuery = baseQuery
 02532                    .Where(e => !e.Data!.Contains("Video3DFormat"));
 2533            }
 2534        }
 2535
 3342536        if (filter.IsPlaceHolder.HasValue)
 2537        {
 02538            if (filter.IsPlaceHolder.Value)
 2539            {
 02540                baseQuery = baseQuery
 02541                    .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
 2542            }
 2543            else
 2544            {
 02545                baseQuery = baseQuery
 02546                    .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
 2547            }
 2548        }
 2549
 3342550        if (filter.HasSpecialFeature.HasValue)
 2551        {
 02552            if (filter.HasSpecialFeature.Value)
 2553            {
 02554                baseQuery = baseQuery
 02555                    .Where(e => e.ExtraIds != null);
 2556            }
 2557            else
 2558            {
 02559                baseQuery = baseQuery
 02560                    .Where(e => e.ExtraIds == null);
 2561            }
 2562        }
 2563
 3342564        if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
 2565        {
 02566            if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo
 2567            {
 02568                baseQuery = baseQuery
 02569                    .Where(e => e.ExtraIds != null);
 2570            }
 2571            else
 2572            {
 02573                baseQuery = baseQuery
 02574                    .Where(e => e.ExtraIds == null);
 2575            }
 2576        }
 2577
 3342578        return baseQuery;
 2579    }
 2580
 2581    /// <inheritdoc/>
 2582    public async Task<bool> ItemExistsAsync(Guid id)
 2583    {
 2584        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 2585        await using (dbContext.ConfigureAwait(false))
 2586        {
 2587            return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
 2588        }
 2589    }
 2590
 2591    /// <inheritdoc/>
 2592    public bool GetIsPlayed(User user, Guid id, bool recursive)
 2593    {
 02594        using var dbContext = _dbProvider.CreateDbContext();
 2595
 02596        if (recursive)
 2597        {
 02598            var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
 2599
 02600            return dbContext.BaseItems
 02601                    .Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
 02602                    .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
 2603        }
 2604
 02605        return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.P
 02606    }
 2607
 2608    private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseIte
 2609    {
 22610        var folderStack = new HashSet<Guid>()
 22611            {
 22612                parentId
 22613            };
 22614        var folderList = new HashSet<Guid>()
 22615            {
 22616                parentId
 22617            };
 2618
 42619        while (folderStack.Count != 0)
 2620        {
 22621            var items = folderStack.ToArray();
 22622            folderStack.Clear();
 22623            var query = dbContext.BaseItems
 22624                .WhereOneOrMany(items, e => e.ParentId!.Value);
 2625
 22626            if (filter != null)
 2627            {
 02628                query = query.Where(filter);
 2629            }
 2630
 42631            foreach (var item in query.Select(e => e.Id).ToArray())
 2632            {
 02633                if (folderList.Add(item))
 2634                {
 02635                    folderStack.Add(item);
 2636                }
 2637            }
 2638        }
 2639
 22640        return folderList;
 2641    }
 2642
 2643    /// <inheritdoc/>
 2644    public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
 2645    {
 02646        using var dbContext = _dbProvider.CreateDbContext();
 2647
 02648        var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtis
 02649            .Where(e => artistNames.Contains(e.Name))
 02650            .ToArray();
 2651
 02652        return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<Mu
 02653    }
 2654}

Methods/Properties

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