BaseItemRepository.cs 101 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676
  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 comparisons
  6. using System;
  7. using System.Collections.Concurrent;
  8. using System.Collections.Generic;
  9. using System.Globalization;
  10. using System.Linq;
  11. using System.Linq.Expressions;
  12. using System.Reflection;
  13. using System.Text;
  14. using System.Text.Json;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. using Jellyfin.Data.Enums;
  18. using Jellyfin.Database.Implementations;
  19. using Jellyfin.Database.Implementations.Entities;
  20. using Jellyfin.Database.Implementations.Enums;
  21. using Jellyfin.Extensions;
  22. using Jellyfin.Extensions.Json;
  23. using Jellyfin.Server.Implementations.Extensions;
  24. using MediaBrowser.Common;
  25. using MediaBrowser.Controller;
  26. using MediaBrowser.Controller.Channels;
  27. using MediaBrowser.Controller.Configuration;
  28. using MediaBrowser.Controller.Entities;
  29. using MediaBrowser.Controller.Entities.Audio;
  30. using MediaBrowser.Controller.Entities.TV;
  31. using MediaBrowser.Controller.LiveTv;
  32. using MediaBrowser.Controller.Persistence;
  33. using MediaBrowser.Model.Dto;
  34. using MediaBrowser.Model.Entities;
  35. using MediaBrowser.Model.LiveTv;
  36. using MediaBrowser.Model.Querying;
  37. using Microsoft.EntityFrameworkCore;
  38. using Microsoft.Extensions.Logging;
  39. using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
  40. using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
  41. namespace Jellyfin.Server.Implementations.Item;
  42. /*
  43. All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null-forgiving operator "!".
  44. This is done as the code isn't actually executed client side, but only the expressions are interpret and the compiler cannot know that.
  45. This is your only warning/message regarding this topic.
  46. */
  47. /// <summary>
  48. /// Handles all storage logic for BaseItems.
  49. /// </summary>
  50. public sealed class BaseItemRepository
  51. : IItemRepository
  52. {
  53. /// <summary>
  54. /// Gets the placeholder id for UserData detached items.
  55. /// </summary>
  56. public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
  57. /// <summary>
  58. /// This holds all the types in the running assemblies
  59. /// so that we can de-serialize properly when we don't have strong types.
  60. /// </summary>
  61. private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
  62. private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
  63. private readonly IServerApplicationHost _appHost;
  64. private readonly IItemTypeLookup _itemTypeLookup;
  65. private readonly IServerConfigurationManager _serverConfigurationManager;
  66. private readonly ILogger<BaseItemRepository> _logger;
  67. private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType.AlbumArtist];
  68. private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
  69. private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
  70. private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
  71. private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
  72. private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
  73. /// <summary>
  74. /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
  75. /// </summary>
  76. /// <param name="dbProvider">The db factory.</param>
  77. /// <param name="appHost">The Application host.</param>
  78. /// <param name="itemTypeLookup">The static type lookup.</param>
  79. /// <param name="serverConfigurationManager">The server Configuration manager.</param>
  80. /// <param name="logger">System logger.</param>
  81. public BaseItemRepository(
  82. IDbContextFactory<JellyfinDbContext> dbProvider,
  83. IServerApplicationHost appHost,
  84. IItemTypeLookup itemTypeLookup,
  85. IServerConfigurationManager serverConfigurationManager,
  86. ILogger<BaseItemRepository> logger)
  87. {
  88. _dbProvider = dbProvider;
  89. _appHost = appHost;
  90. _itemTypeLookup = itemTypeLookup;
  91. _serverConfigurationManager = serverConfigurationManager;
  92. _logger = logger;
  93. }
  94. /// <inheritdoc />
  95. public void DeleteItem(params IReadOnlyList<Guid> ids)
  96. {
  97. if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
  98. {
  99. throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
  100. }
  101. using var context = _dbProvider.CreateDbContext();
  102. using var transaction = context.Database.BeginTransaction();
  103. var date = (DateTime?)DateTime.UtcNow;
  104. var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
  105. // Remove any UserData entries for the placeholder item that would conflict with the UserData
  106. // being detached from the item being deleted. This is necessary because, during an update,
  107. // UserData may be reattached to a new entry, but some entries can be left behind.
  108. // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
  109. context.UserData
  110. .Join(
  111. context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
  112. placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
  113. userData => new { userData.UserId, userData.CustomDataKey },
  114. (placeholder, userData) => placeholder)
  115. .Where(e => e.ItemId == PlaceholderId)
  116. .ExecuteDelete();
  117. // Detach all user watch data
  118. context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
  119. .ExecuteUpdate(e => e
  120. .SetProperty(f => f.RetentionDate, date)
  121. .SetProperty(f => f.ItemId, PlaceholderId));
  122. context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  123. context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
  124. context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  125. context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  126. context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  127. context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  128. context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  129. context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
  130. context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  131. context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  132. context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  133. context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
  134. context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  135. context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  136. context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  137. context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  138. var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distinct().ToArray();
  139. context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  140. context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
  141. context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
  142. context.SaveChanges();
  143. transaction.Commit();
  144. }
  145. /// <inheritdoc />
  146. public void UpdateInheritedValues()
  147. {
  148. using var context = _dbProvider.CreateDbContext();
  149. using var transaction = context.Database.BeginTransaction();
  150. context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
  151. // ItemValue Inheritance is now correctly mapped via AncestorId on demand
  152. context.SaveChanges();
  153. transaction.Commit();
  154. }
  155. /// <inheritdoc />
  156. public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
  157. {
  158. ArgumentNullException.ThrowIfNull(filter);
  159. PrepareFilterQuery(filter);
  160. using var context = _dbProvider.CreateDbContext();
  161. return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, filter).Select(e => e.Id).ToArray();
  162. }
  163. /// <inheritdoc />
  164. public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
  165. {
  166. return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
  167. }
  168. /// <inheritdoc />
  169. public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
  170. {
  171. return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
  172. }
  173. /// <inheritdoc />
  174. public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
  175. {
  176. return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
  177. }
  178. /// <inheritdoc />
  179. public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
  180. {
  181. return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
  182. }
  183. /// <inheritdoc />
  184. public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
  185. {
  186. return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
  187. }
  188. /// <inheritdoc />
  189. public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
  190. {
  191. return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
  192. }
  193. /// <inheritdoc />
  194. public IReadOnlyList<string> GetStudioNames()
  195. {
  196. return GetItemValueNames(_getStudiosValueTypes, [], []);
  197. }
  198. /// <inheritdoc />
  199. public IReadOnlyList<string> GetAllArtistNames()
  200. {
  201. return GetItemValueNames(_getAllArtistsValueTypes, [], []);
  202. }
  203. /// <inheritdoc />
  204. public IReadOnlyList<string> GetMusicGenreNames()
  205. {
  206. return GetItemValueNames(
  207. _getGenreValueTypes,
  208. _itemTypeLookup.MusicGenreTypes,
  209. []);
  210. }
  211. /// <inheritdoc />
  212. public IReadOnlyList<string> GetGenreNames()
  213. {
  214. return GetItemValueNames(
  215. _getGenreValueTypes,
  216. [],
  217. _itemTypeLookup.MusicGenreTypes);
  218. }
  219. /// <inheritdoc />
  220. public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
  221. {
  222. ArgumentNullException.ThrowIfNull(filter);
  223. if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0))
  224. {
  225. var returnList = GetItemList(filter);
  226. return new QueryResult<BaseItemDto>(
  227. filter.StartIndex,
  228. returnList.Count,
  229. returnList);
  230. }
  231. PrepareFilterQuery(filter);
  232. var result = new QueryResult<BaseItemDto>();
  233. using var context = _dbProvider.CreateDbContext();
  234. IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
  235. dbQuery = TranslateQuery(dbQuery, context, filter);
  236. dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
  237. if (filter.EnableTotalRecordCount)
  238. {
  239. result.TotalRecordCount = dbQuery.Count();
  240. }
  241. dbQuery = ApplyQueryPaging(dbQuery, filter);
  242. dbQuery = ApplyNavigations(dbQuery, filter);
  243. result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
  244. result.StartIndex = filter.StartIndex ?? 0;
  245. return result;
  246. }
  247. /// <inheritdoc />
  248. public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
  249. {
  250. ArgumentNullException.ThrowIfNull(filter);
  251. PrepareFilterQuery(filter);
  252. using var context = _dbProvider.CreateDbContext();
  253. IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
  254. dbQuery = TranslateQuery(dbQuery, context, filter);
  255. dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
  256. dbQuery = ApplyQueryPaging(dbQuery, filter);
  257. dbQuery = ApplyNavigations(dbQuery, filter);
  258. return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
  259. }
  260. /// <inheritdoc/>
  261. public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
  262. {
  263. ArgumentNullException.ThrowIfNull(filter);
  264. PrepareFilterQuery(filter);
  265. // Early exit if collection type is not tvshows or music
  266. if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music)
  267. {
  268. return Array.Empty<BaseItem>();
  269. }
  270. using var context = _dbProvider.CreateDbContext();
  271. // Subquery to group by SeriesNames/Album and get the max Date Created for each group.
  272. var subquery = PrepareItemQuery(context, filter);
  273. subquery = TranslateQuery(subquery, context, filter);
  274. var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album)
  275. .Select(g => new
  276. {
  277. Key = g.Key,
  278. MaxDateCreated = g.Max(a => a.DateCreated)
  279. })
  280. .OrderByDescending(g => g.MaxDateCreated)
  281. .Select(g => g);
  282. if (filter.Limit.HasValue && filter.Limit.Value > 0)
  283. {
  284. subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
  285. }
  286. filter.Limit = null;
  287. var mainquery = PrepareItemQuery(context, filter);
  288. mainquery = TranslateQuery(mainquery, context, filter);
  289. mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
  290. mainquery = ApplyGroupingFilter(context, mainquery, filter);
  291. mainquery = ApplyQueryPaging(mainquery, filter);
  292. mainquery = ApplyNavigations(mainquery, filter);
  293. return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
  294. }
  295. /// <inheritdoc />
  296. public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
  297. {
  298. ArgumentNullException.ThrowIfNull(filter);
  299. ArgumentNullException.ThrowIfNull(filter.User);
  300. using var context = _dbProvider.CreateDbContext();
  301. var query = context.BaseItems
  302. .AsNoTracking()
  303. .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
  304. .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
  305. .Join(
  306. context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
  307. i => new { UserId = filter.User.Id, ItemId = i.Id },
  308. u => new { UserId = u.UserId, ItemId = u.ItemId },
  309. (entity, data) => new { Item = entity, UserData = data })
  310. .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
  311. .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
  312. .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
  313. .OrderByDescending(g => g.LastPlayedDate)
  314. .Select(g => g.Key!);
  315. if (filter.Limit.HasValue && filter.Limit.Value > 0)
  316. {
  317. query = query.Take(filter.Limit.Value);
  318. }
  319. return query.ToArray();
  320. }
  321. private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
  322. {
  323. // This whole block is needed to filter duplicate entries on request
  324. // for the time being it cannot be used because it would destroy the ordering
  325. // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but
  326. // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
  327. var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
  328. if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
  329. {
  330. var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.FirstOrDefault()).Select(e => e!.Id);
  331. dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
  332. }
  333. else if (enableGroupByPresentationUniqueKey)
  334. {
  335. var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id);
  336. dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
  337. }
  338. else if (filter.GroupBySeriesPresentationUniqueKey)
  339. {
  340. var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id);
  341. dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
  342. }
  343. else
  344. {
  345. dbQuery = dbQuery.Distinct();
  346. }
  347. dbQuery = ApplyOrder(dbQuery, filter, context);
  348. return dbQuery;
  349. }
  350. private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
  351. {
  352. if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
  353. {
  354. dbQuery = dbQuery.Include(e => e.TrailerTypes);
  355. }
  356. if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
  357. {
  358. dbQuery = dbQuery.Include(e => e.Provider);
  359. }
  360. if (filter.DtoOptions.ContainsField(ItemFields.Settings))
  361. {
  362. dbQuery = dbQuery.Include(e => e.LockedFields);
  363. }
  364. if (filter.DtoOptions.EnableUserData)
  365. {
  366. dbQuery = dbQuery.Include(e => e.UserData);
  367. }
  368. if (filter.DtoOptions.EnableImages)
  369. {
  370. dbQuery = dbQuery.Include(e => e.Images);
  371. }
  372. return dbQuery;
  373. }
  374. private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
  375. {
  376. if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
  377. {
  378. dbQuery = dbQuery.Skip(filter.StartIndex.Value);
  379. }
  380. if (filter.Limit.HasValue && filter.Limit.Value > 0)
  381. {
  382. dbQuery = dbQuery.Take(filter.Limit.Value);
  383. }
  384. return dbQuery;
  385. }
  386. private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
  387. {
  388. dbQuery = TranslateQuery(dbQuery, context, filter);
  389. dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
  390. dbQuery = ApplyQueryPaging(dbQuery, filter);
  391. dbQuery = ApplyNavigations(dbQuery, filter);
  392. return dbQuery;
  393. }
  394. private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
  395. {
  396. IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
  397. dbQuery = dbQuery.AsSingleQuery();
  398. return dbQuery;
  399. }
  400. /// <inheritdoc/>
  401. public int GetCount(InternalItemsQuery filter)
  402. {
  403. ArgumentNullException.ThrowIfNull(filter);
  404. // Hack for right now since we currently don't support filtering out these duplicates within a query
  405. PrepareFilterQuery(filter);
  406. using var context = _dbProvider.CreateDbContext();
  407. var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
  408. return dbQuery.Count();
  409. }
  410. /// <inheritdoc />
  411. public ItemCounts GetItemCounts(InternalItemsQuery filter)
  412. {
  413. ArgumentNullException.ThrowIfNull(filter);
  414. // Hack for right now since we currently don't support filtering out these duplicates within a query
  415. PrepareFilterQuery(filter);
  416. using var context = _dbProvider.CreateDbContext();
  417. var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
  418. var counts = dbQuery
  419. .GroupBy(x => x.Type)
  420. .Select(x => new { x.Key, Count = x.Count() })
  421. .ToArray();
  422. var lookup = _itemTypeLookup.BaseItemKindNames;
  423. var result = new ItemCounts();
  424. foreach (var count in counts)
  425. {
  426. if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
  427. {
  428. result.AlbumCount = count.Count;
  429. }
  430. else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
  431. {
  432. result.ArtistCount = count.Count;
  433. }
  434. else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
  435. {
  436. result.EpisodeCount = count.Count;
  437. }
  438. else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
  439. {
  440. result.MovieCount = count.Count;
  441. }
  442. else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
  443. {
  444. result.MusicVideoCount = count.Count;
  445. }
  446. else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
  447. {
  448. result.ProgramCount = count.Count;
  449. }
  450. else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
  451. {
  452. result.SeriesCount = count.Count;
  453. }
  454. else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
  455. {
  456. result.SongCount = count.Count;
  457. }
  458. else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
  459. {
  460. result.TrailerCount = count.Count;
  461. }
  462. }
  463. return result;
  464. }
  465. #pragma warning disable CA1307 // Specify StringComparison for clarity
  466. /// <summary>
  467. /// Gets the type.
  468. /// </summary>
  469. /// <param name="typeName">Name of the type.</param>
  470. /// <returns>Type.</returns>
  471. /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
  472. private static Type? GetType(string typeName)
  473. {
  474. ArgumentException.ThrowIfNullOrEmpty(typeName);
  475. // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static variable and be loaded eagerly.
  476. // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are first called, before or after plugins are loaded
  477. return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
  478. .Select(a => a.GetType(k))
  479. .FirstOrDefault(t => t is not null));
  480. }
  481. /// <inheritdoc />
  482. public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default)
  483. {
  484. ArgumentNullException.ThrowIfNull(item);
  485. var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray();
  486. var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
  487. await using (context.ConfigureAwait(false))
  488. {
  489. if (!await context.BaseItems
  490. .AnyAsync(bi => bi.Id == item.Id, cancellationToken)
  491. .ConfigureAwait(false))
  492. {
  493. _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
  494. return;
  495. }
  496. await context.BaseItemImageInfos
  497. .Where(e => e.ItemId == item.Id)
  498. .ExecuteDeleteAsync(cancellationToken)
  499. .ConfigureAwait(false);
  500. await context.BaseItemImageInfos
  501. .AddRangeAsync(images, cancellationToken)
  502. .ConfigureAwait(false);
  503. await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
  504. }
  505. }
  506. /// <inheritdoc />
  507. public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
  508. {
  509. UpdateOrInsertItems(items, cancellationToken);
  510. }
  511. /// <inheritdoc cref="IItemRepository"/>
  512. public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
  513. {
  514. ArgumentNullException.ThrowIfNull(items);
  515. cancellationToken.ThrowIfCancellationRequested();
  516. var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> UserDataKey, List<string> InheritedTags)>();
  517. foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
  518. {
  519. var ancestorIds = item.SupportsAncestors ?
  520. item.GetAncestorIds().Distinct().ToList() :
  521. null;
  522. var topParent = item.GetTopParent();
  523. var userdataKey = item.GetUserDataKeys();
  524. var inheritedTags = item.GetInheritedTags();
  525. tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
  526. }
  527. using var context = _dbProvider.CreateDbContext();
  528. using var transaction = context.Database.BeginTransaction();
  529. var ids = tuples.Select(f => f.Item.Id).ToArray();
  530. var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
  531. var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
  532. foreach (var item in tuples)
  533. {
  534. var entity = Map(item.Item);
  535. // TODO: refactor this "inconsistency"
  536. entity.TopParentId = item.TopParent?.Id;
  537. if (!existingItems.Any(e => e == entity.Id))
  538. {
  539. context.BaseItems.Add(entity);
  540. }
  541. else
  542. {
  543. context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
  544. context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
  545. context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
  546. if (entity.Images is { Count: > 0 })
  547. {
  548. context.BaseItemImageInfos.AddRange(entity.Images);
  549. }
  550. if (entity.LockedFields is { Count: > 0 })
  551. {
  552. context.BaseItemMetadataFields.AddRange(entity.LockedFields);
  553. }
  554. context.BaseItems.Attach(entity).State = EntityState.Modified;
  555. }
  556. }
  557. context.SaveChanges();
  558. foreach (var item in newItems)
  559. {
  560. // reattach old userData entries
  561. var userKeys = item.UserDataKey.ToArray();
  562. var retentionDate = (DateTime?)null;
  563. context.UserData
  564. .Where(e => e.ItemId == PlaceholderId)
  565. .Where(e => userKeys.Contains(e.CustomDataKey))
  566. .ExecuteUpdate(e => e
  567. .SetProperty(f => f.ItemId, item.Item.Id)
  568. .SetProperty(f => f.RetentionDate, retentionDate));
  569. }
  570. var itemValueMaps = tuples
  571. .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
  572. .ToArray();
  573. var allListedItemValues = itemValueMaps
  574. .SelectMany(f => f.Values)
  575. .Distinct()
  576. .ToArray();
  577. var existingValues = context.ItemValues
  578. .Select(e => new
  579. {
  580. item = e,
  581. Key = e.Type + "+" + e.Value
  582. })
  583. .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
  584. .Select(e => e.item)
  585. .ToArray();
  586. var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue()
  587. {
  588. CleanValue = GetCleanValue(f.Value),
  589. ItemValueId = Guid.NewGuid(),
  590. Type = f.MagicNumber,
  591. Value = f.Value
  592. }).ToArray();
  593. context.ItemValues.AddRange(missingItemValues);
  594. context.SaveChanges();
  595. var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
  596. var valueMap = itemValueMaps
  597. .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).DistinctBy(e => e.ItemValueId).ToArray()))
  598. .ToArray();
  599. var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
  600. foreach (var item in valueMap)
  601. {
  602. var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
  603. foreach (var itemValue in item.Values)
  604. {
  605. var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
  606. if (existingItem is null)
  607. {
  608. context.ItemValuesMap.Add(new ItemValueMap()
  609. {
  610. Item = null!,
  611. ItemId = item.Item.Id,
  612. ItemValue = null!,
  613. ItemValueId = itemValue.ItemValueId
  614. });
  615. }
  616. else
  617. {
  618. // map exists, remove from list so its been handled.
  619. itemMappedValues.Remove(existingItem);
  620. }
  621. }
  622. // all still listed values are not in the new list so remove them.
  623. context.ItemValuesMap.RemoveRange(itemMappedValues);
  624. }
  625. context.SaveChanges();
  626. foreach (var item in tuples)
  627. {
  628. if (item.Item.SupportsAncestors && item.AncestorIds != null)
  629. {
  630. var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
  631. var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).ToArray();
  632. foreach (var ancestorId in validAncestorIds)
  633. {
  634. var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
  635. if (existingAncestorId is null)
  636. {
  637. context.AncestorIds.Add(new AncestorId()
  638. {
  639. ParentItemId = ancestorId,
  640. ItemId = item.Item.Id,
  641. Item = null!,
  642. ParentItem = null!
  643. });
  644. }
  645. else
  646. {
  647. existingAncestorIds.Remove(existingAncestorId);
  648. }
  649. }
  650. context.AncestorIds.RemoveRange(existingAncestorIds);
  651. }
  652. }
  653. context.SaveChanges();
  654. transaction.Commit();
  655. }
  656. /// <inheritdoc />
  657. public BaseItemDto? RetrieveItem(Guid id)
  658. {
  659. if (id.IsEmpty())
  660. {
  661. throw new ArgumentException("Guid can't be empty", nameof(id));
  662. }
  663. using var context = _dbProvider.CreateDbContext();
  664. var dbQuery = PrepareItemQuery(context, new()
  665. {
  666. DtoOptions = new()
  667. {
  668. EnableImages = true
  669. }
  670. });
  671. dbQuery = dbQuery.Include(e => e.TrailerTypes)
  672. .Include(e => e.Provider)
  673. .Include(e => e.LockedFields)
  674. .Include(e => e.UserData)
  675. .Include(e => e.Images);
  676. var item = dbQuery.FirstOrDefault(e => e.Id == id);
  677. if (item is null)
  678. {
  679. return null;
  680. }
  681. return DeserializeBaseItem(item);
  682. }
  683. /// <summary>
  684. /// Maps a Entity to the DTO.
  685. /// </summary>
  686. /// <param name="entity">The entity.</param>
  687. /// <param name="dto">The dto base instance.</param>
  688. /// <param name="appHost">The Application server Host.</param>
  689. /// <param name="logger">The applogger.</param>
  690. /// <returns>The dto to map.</returns>
  691. public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logger)
  692. {
  693. dto.Id = entity.Id;
  694. dto.ParentId = entity.ParentId.GetValueOrDefault();
  695. dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
  696. dto.EndDate = entity.EndDate;
  697. dto.CommunityRating = entity.CommunityRating;
  698. dto.CustomRating = entity.CustomRating;
  699. dto.IndexNumber = entity.IndexNumber;
  700. dto.IsLocked = entity.IsLocked;
  701. dto.Name = entity.Name;
  702. dto.OfficialRating = entity.OfficialRating;
  703. dto.Overview = entity.Overview;
  704. dto.ParentIndexNumber = entity.ParentIndexNumber;
  705. dto.PremiereDate = entity.PremiereDate;
  706. dto.ProductionYear = entity.ProductionYear;
  707. dto.SortName = entity.SortName;
  708. dto.ForcedSortName = entity.ForcedSortName;
  709. dto.RunTimeTicks = entity.RunTimeTicks;
  710. dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
  711. dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
  712. dto.IsInMixedFolder = entity.IsInMixedFolder;
  713. dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
  714. dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
  715. dto.CriticRating = entity.CriticRating;
  716. dto.PresentationUniqueKey = entity.PresentationUniqueKey;
  717. dto.OriginalTitle = entity.OriginalTitle;
  718. dto.Album = entity.Album;
  719. dto.LUFS = entity.LUFS;
  720. dto.NormalizationGain = entity.NormalizationGain;
  721. dto.IsVirtualItem = entity.IsVirtualItem;
  722. dto.ExternalSeriesId = entity.ExternalSeriesId;
  723. dto.Tagline = entity.Tagline;
  724. dto.TotalBitrate = entity.TotalBitrate;
  725. dto.ExternalId = entity.ExternalId;
  726. dto.Size = entity.Size;
  727. dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
  728. dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
  729. dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
  730. dto.ChannelId = entity.ChannelId ?? Guid.Empty;
  731. dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
  732. dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
  733. dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
  734. dto.Width = entity.Width.GetValueOrDefault();
  735. dto.Height = entity.Height.GetValueOrDefault();
  736. dto.UserData = entity.UserData;
  737. if (entity.Provider is not null)
  738. {
  739. dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
  740. }
  741. if (entity.ExtraType is not null)
  742. {
  743. dto.ExtraType = (ExtraType)entity.ExtraType;
  744. }
  745. if (entity.LockedFields is not null)
  746. {
  747. dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
  748. }
  749. if (entity.Audio is not null)
  750. {
  751. dto.Audio = (ProgramAudio)entity.Audio;
  752. }
  753. dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
  754. dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
  755. dto.Studios = entity.Studios?.Split('|') ?? [];
  756. dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
  757. if (dto is IHasProgramAttributes hasProgramAttributes)
  758. {
  759. hasProgramAttributes.IsMovie = entity.IsMovie;
  760. hasProgramAttributes.IsSeries = entity.IsSeries;
  761. hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
  762. hasProgramAttributes.IsRepeat = entity.IsRepeat;
  763. }
  764. if (dto is LiveTvChannel liveTvChannel)
  765. {
  766. liveTvChannel.ServiceName = entity.ExternalServiceId;
  767. }
  768. if (dto is Trailer trailer)
  769. {
  770. trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
  771. }
  772. if (dto is Video video)
  773. {
  774. video.PrimaryVersionId = entity.PrimaryVersionId;
  775. }
  776. if (dto is IHasSeries hasSeriesName)
  777. {
  778. hasSeriesName.SeriesName = entity.SeriesName;
  779. hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
  780. hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
  781. }
  782. if (dto is Episode episode)
  783. {
  784. episode.SeasonName = entity.SeasonName;
  785. episode.SeasonId = entity.SeasonId.GetValueOrDefault();
  786. }
  787. if (dto is IHasArtist hasArtists)
  788. {
  789. hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
  790. }
  791. if (dto is IHasAlbumArtist hasAlbumArtists)
  792. {
  793. hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
  794. }
  795. if (dto is LiveTvProgram program)
  796. {
  797. program.ShowId = entity.ShowId;
  798. }
  799. if (entity.Images is not null)
  800. {
  801. dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
  802. }
  803. // dto.Type = entity.Type;
  804. // dto.Data = entity.Data;
  805. // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
  806. if (dto is IHasStartDate hasStartDate)
  807. {
  808. hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
  809. }
  810. // Fields that are present in the DB but are never actually used
  811. // dto.UnratedType = entity.UnratedType;
  812. // dto.TopParentId = entity.TopParentId;
  813. // dto.CleanName = entity.CleanName;
  814. // dto.UserDataKey = entity.UserDataKey;
  815. if (dto is Folder folder)
  816. {
  817. folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
  818. }
  819. return dto;
  820. }
  821. /// <summary>
  822. /// Maps a Entity to the DTO.
  823. /// </summary>
  824. /// <param name="dto">The entity.</param>
  825. /// <returns>The dto to map.</returns>
  826. public BaseItemEntity Map(BaseItemDto dto)
  827. {
  828. var dtoType = dto.GetType();
  829. var entity = new BaseItemEntity()
  830. {
  831. Type = dtoType.ToString(),
  832. Id = dto.Id
  833. };
  834. if (TypeRequiresDeserialization(dtoType))
  835. {
  836. entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
  837. }
  838. entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
  839. entity.Path = GetPathToSave(dto.Path);
  840. entity.EndDate = dto.EndDate;
  841. entity.CommunityRating = dto.CommunityRating;
  842. entity.CustomRating = dto.CustomRating;
  843. entity.IndexNumber = dto.IndexNumber;
  844. entity.IsLocked = dto.IsLocked;
  845. entity.Name = dto.Name;
  846. entity.CleanName = GetCleanValue(dto.Name);
  847. entity.OfficialRating = dto.OfficialRating;
  848. entity.Overview = dto.Overview;
  849. entity.ParentIndexNumber = dto.ParentIndexNumber;
  850. entity.PremiereDate = dto.PremiereDate;
  851. entity.ProductionYear = dto.ProductionYear;
  852. entity.SortName = dto.SortName;
  853. entity.ForcedSortName = dto.ForcedSortName;
  854. entity.RunTimeTicks = dto.RunTimeTicks;
  855. entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
  856. entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
  857. entity.IsInMixedFolder = dto.IsInMixedFolder;
  858. entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
  859. entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
  860. entity.CriticRating = dto.CriticRating;
  861. entity.PresentationUniqueKey = dto.PresentationUniqueKey;
  862. entity.OriginalTitle = dto.OriginalTitle;
  863. entity.Album = dto.Album;
  864. entity.LUFS = dto.LUFS;
  865. entity.NormalizationGain = dto.NormalizationGain;
  866. entity.IsVirtualItem = dto.IsVirtualItem;
  867. entity.ExternalSeriesId = dto.ExternalSeriesId;
  868. entity.Tagline = dto.Tagline;
  869. entity.TotalBitrate = dto.TotalBitrate;
  870. entity.ExternalId = dto.ExternalId;
  871. entity.Size = dto.Size;
  872. entity.Genres = string.Join('|', dto.Genres);
  873. entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
  874. entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
  875. entity.ChannelId = dto.ChannelId;
  876. entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
  877. entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
  878. entity.OwnerId = dto.OwnerId.ToString();
  879. entity.Width = dto.Width;
  880. entity.Height = dto.Height;
  881. entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
  882. {
  883. Item = entity,
  884. ProviderId = e.Key,
  885. ProviderValue = e.Value
  886. }).ToList();
  887. if (dto.Audio.HasValue)
  888. {
  889. entity.Audio = (ProgramAudioEntity)dto.Audio;
  890. }
  891. if (dto.ExtraType.HasValue)
  892. {
  893. entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
  894. }
  895. entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
  896. entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null;
  897. entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
  898. entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
  899. entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
  900. .Select(e => new BaseItemMetadataField()
  901. {
  902. Id = (int)e,
  903. Item = entity,
  904. ItemId = entity.Id
  905. })
  906. .ToArray() : null;
  907. if (dto is IHasProgramAttributes hasProgramAttributes)
  908. {
  909. entity.IsMovie = hasProgramAttributes.IsMovie;
  910. entity.IsSeries = hasProgramAttributes.IsSeries;
  911. entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
  912. entity.IsRepeat = hasProgramAttributes.IsRepeat;
  913. }
  914. if (dto is LiveTvChannel liveTvChannel)
  915. {
  916. entity.ExternalServiceId = liveTvChannel.ServiceName;
  917. }
  918. if (dto is Video video)
  919. {
  920. entity.PrimaryVersionId = video.PrimaryVersionId;
  921. }
  922. if (dto is IHasSeries hasSeriesName)
  923. {
  924. entity.SeriesName = hasSeriesName.SeriesName;
  925. entity.SeriesId = hasSeriesName.SeriesId;
  926. entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
  927. }
  928. if (dto is Episode episode)
  929. {
  930. entity.SeasonName = episode.SeasonName;
  931. entity.SeasonId = episode.SeasonId;
  932. }
  933. if (dto is IHasArtist hasArtists)
  934. {
  935. entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
  936. }
  937. if (dto is IHasAlbumArtist hasAlbumArtists)
  938. {
  939. entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null;
  940. }
  941. if (dto is LiveTvProgram program)
  942. {
  943. entity.ShowId = program.ShowId;
  944. }
  945. if (dto.ImageInfos is not null)
  946. {
  947. entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
  948. }
  949. if (dto is Trailer trailer)
  950. {
  951. entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
  952. {
  953. Id = (int)e,
  954. Item = entity,
  955. ItemId = entity.Id
  956. }).ToArray() ?? [];
  957. }
  958. // dto.Type = entity.Type;
  959. // dto.Data = entity.Data;
  960. entity.MediaType = dto.MediaType.ToString();
  961. if (dto is IHasStartDate hasStartDate)
  962. {
  963. entity.StartDate = hasStartDate.StartDate;
  964. }
  965. entity.UnratedType = dto.GetBlockUnratedType().ToString();
  966. // Fields that are present in the DB but are never actually used
  967. // dto.UserDataKey = entity.UserDataKey;
  968. if (dto is Folder folder)
  969. {
  970. entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdded;
  971. entity.IsFolder = folder.IsFolder;
  972. }
  973. return entity;
  974. }
  975. private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
  976. {
  977. using var context = _dbProvider.CreateDbContext();
  978. var query = context.ItemValuesMap
  979. .AsNoTracking()
  980. .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
  981. if (withItemTypes.Count > 0)
  982. {
  983. query = query.Where(e => withItemTypes.Contains(e.Item.Type));
  984. }
  985. if (excludeItemTypes.Count > 0)
  986. {
  987. query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
  988. }
  989. // query = query.DistinctBy(e => e.CleanValue);
  990. return query.Select(e => e.ItemValue)
  991. .GroupBy(e => e.CleanValue)
  992. .Select(e => e.First().Value)
  993. .ToArray();
  994. }
  995. private static bool TypeRequiresDeserialization(Type type)
  996. {
  997. return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
  998. }
  999. private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
  1000. {
  1001. ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
  1002. if (_serverConfigurationManager?.Configuration is null)
  1003. {
  1004. throw new InvalidOperationException("Server Configuration manager or configuration is null");
  1005. }
  1006. var typeToSerialise = GetType(baseItemEntity.Type);
  1007. return BaseItemRepository.DeserializeBaseItem(
  1008. baseItemEntity,
  1009. _logger,
  1010. _appHost,
  1011. skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeToSerialise == typeof(Channel) || typeToSerialise == typeof(UserRootFolder))));
  1012. }
  1013. /// <summary>
  1014. /// Deserializes a BaseItemEntity and sets all properties.
  1015. /// </summary>
  1016. /// <param name="baseItemEntity">The DB entity.</param>
  1017. /// <param name="logger">Logger.</param>
  1018. /// <param name="appHost">The application server Host.</param>
  1019. /// <param name="skipDeserialization">If only mapping should be processed.</param>
  1020. /// <returns>A mapped BaseItem.</returns>
  1021. /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
  1022. public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
  1023. {
  1024. var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
  1025. BaseItemDto? dto = null;
  1026. if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
  1027. {
  1028. try
  1029. {
  1030. dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
  1031. }
  1032. catch (JsonException ex)
  1033. {
  1034. logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
  1035. }
  1036. }
  1037. if (dto is null)
  1038. {
  1039. dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
  1040. }
  1041. return Map(baseItemEntity, dto, appHost, logger);
  1042. }
  1043. private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList<ItemValueType> itemValueTypes, string returnType)
  1044. {
  1045. ArgumentNullException.ThrowIfNull(filter);
  1046. if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
  1047. {
  1048. filter.EnableTotalRecordCount = false;
  1049. }
  1050. using var context = _dbProvider.CreateDbContext();
  1051. var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context, new InternalItemsQuery(filter.User)
  1052. {
  1053. ExcludeItemTypes = filter.ExcludeItemTypes,
  1054. IncludeItemTypes = filter.IncludeItemTypes,
  1055. MediaTypes = filter.MediaTypes,
  1056. AncestorIds = filter.AncestorIds,
  1057. ItemIds = filter.ItemIds,
  1058. TopParentIds = filter.TopParentIds,
  1059. ParentId = filter.ParentId,
  1060. IsAiring = filter.IsAiring,
  1061. IsMovie = filter.IsMovie,
  1062. IsSports = filter.IsSports,
  1063. IsKids = filter.IsKids,
  1064. IsNews = filter.IsNews,
  1065. IsSeries = filter.IsSeries
  1066. });
  1067. var itemValuesQuery = context.ItemValues
  1068. .Where(f => itemValueTypes.Contains(f.Type))
  1069. .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
  1070. .Join(
  1071. innerQueryFilter,
  1072. fw => fw.w.ItemId,
  1073. g => g.Id,
  1074. (fw, g) => fw.f.CleanValue);
  1075. var innerQuery = PrepareItemQuery(context, filter)
  1076. .Where(e => e.Type == returnType)
  1077. .Where(e => itemValuesQuery.Contains(e.CleanName));
  1078. var outerQueryFilter = new InternalItemsQuery(filter.User)
  1079. {
  1080. IsPlayed = filter.IsPlayed,
  1081. IsFavorite = filter.IsFavorite,
  1082. IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
  1083. IsLiked = filter.IsLiked,
  1084. IsLocked = filter.IsLocked,
  1085. NameLessThan = filter.NameLessThan,
  1086. NameStartsWith = filter.NameStartsWith,
  1087. NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
  1088. Tags = filter.Tags,
  1089. OfficialRatings = filter.OfficialRatings,
  1090. StudioIds = filter.StudioIds,
  1091. GenreIds = filter.GenreIds,
  1092. Genres = filter.Genres,
  1093. Years = filter.Years,
  1094. NameContains = filter.NameContains,
  1095. SearchTerm = filter.SearchTerm,
  1096. ExcludeItemIds = filter.ExcludeItemIds
  1097. };
  1098. var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
  1099. .GroupBy(e => e.PresentationUniqueKey)
  1100. .Select(e => e.FirstOrDefault())
  1101. .Select(e => e!.Id);
  1102. var query = context.BaseItems
  1103. .Include(e => e.TrailerTypes)
  1104. .Include(e => e.Provider)
  1105. .Include(e => e.LockedFields)
  1106. .Include(e => e.Images)
  1107. .AsSingleQuery()
  1108. .Where(e => masterQuery.Contains(e.Id));
  1109. query = ApplyOrder(query, filter, context);
  1110. var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
  1111. if (filter.EnableTotalRecordCount)
  1112. {
  1113. result.TotalRecordCount = query.Count();
  1114. }
  1115. if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
  1116. {
  1117. query = query.Skip(filter.StartIndex.Value);
  1118. }
  1119. if (filter.Limit.HasValue && filter.Limit.Value > 0)
  1120. {
  1121. query = query.Take(filter.Limit.Value);
  1122. }
  1123. IQueryable<BaseItemEntity>? itemCountQuery = null;
  1124. if (filter.IncludeItemTypes.Length > 0)
  1125. {
  1126. // if we are to include more then one type, sub query those items beforehand.
  1127. var typeSubQuery = new InternalItemsQuery(filter.User)
  1128. {
  1129. ExcludeItemTypes = filter.ExcludeItemTypes,
  1130. IncludeItemTypes = filter.IncludeItemTypes,
  1131. MediaTypes = filter.MediaTypes,
  1132. AncestorIds = filter.AncestorIds,
  1133. ExcludeItemIds = filter.ExcludeItemIds,
  1134. ItemIds = filter.ItemIds,
  1135. TopParentIds = filter.TopParentIds,
  1136. ParentId = filter.ParentId,
  1137. IsPlayed = filter.IsPlayed
  1138. };
  1139. itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery)
  1140. .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
  1141. var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
  1142. var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
  1143. var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
  1144. var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
  1145. var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
  1146. var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
  1147. var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
  1148. var resultQuery = query.Select(e => new
  1149. {
  1150. item = e,
  1151. // TODO: This is bad refactor!
  1152. itemCount = new ItemCounts()
  1153. {
  1154. SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
  1155. EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
  1156. MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
  1157. AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
  1158. ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
  1159. SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
  1160. TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
  1161. }
  1162. });
  1163. result.StartIndex = filter.StartIndex ?? 0;
  1164. result.Items =
  1165. [
  1166. .. resultQuery
  1167. .AsEnumerable()
  1168. .Where(e => e is not null)
  1169. .Select(e =>
  1170. {
  1171. return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
  1172. })
  1173. ];
  1174. }
  1175. else
  1176. {
  1177. result.StartIndex = filter.StartIndex ?? 0;
  1178. result.Items =
  1179. [
  1180. .. query
  1181. .AsEnumerable()
  1182. .Where(e => e is not null)
  1183. .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
  1184. {
  1185. return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
  1186. })
  1187. ];
  1188. }
  1189. return result;
  1190. }
  1191. private static void PrepareFilterQuery(InternalItemsQuery query)
  1192. {
  1193. if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
  1194. {
  1195. query.Limit = query.Limit.Value + 4;
  1196. }
  1197. if (query.IsResumable ?? false)
  1198. {
  1199. query.IsVirtualItem = false;
  1200. }
  1201. }
  1202. /// <summary>
  1203. /// Gets the clean value for search and sorting purposes.
  1204. /// </summary>
  1205. /// <param name="value">The value to clean.</param>
  1206. /// <returns>The cleaned value.</returns>
  1207. public static string GetCleanValue(string value)
  1208. {
  1209. if (string.IsNullOrWhiteSpace(value))
  1210. {
  1211. return value;
  1212. }
  1213. var noDiacritics = value.RemoveDiacritics();
  1214. // Build a string where any punctuation or symbol is treated as a separator (space).
  1215. var sb = new StringBuilder(noDiacritics.Length);
  1216. var previousWasSpace = false;
  1217. foreach (var ch in noDiacritics)
  1218. {
  1219. char outCh;
  1220. if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
  1221. {
  1222. outCh = ch;
  1223. }
  1224. else
  1225. {
  1226. outCh = ' ';
  1227. }
  1228. // normalize any whitespace character to a single ASCII space.
  1229. if (char.IsWhiteSpace(outCh))
  1230. {
  1231. if (!previousWasSpace)
  1232. {
  1233. sb.Append(' ');
  1234. previousWasSpace = true;
  1235. }
  1236. }
  1237. else
  1238. {
  1239. sb.Append(outCh);
  1240. previousWasSpace = false;
  1241. }
  1242. }
  1243. // trim leading/trailing spaces that may have been added.
  1244. var collapsed = sb.ToString().Trim();
  1245. return collapsed.ToLowerInvariant();
  1246. }
  1247. private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
  1248. {
  1249. var list = new List<(ItemValueType, string)>();
  1250. if (item is IHasArtist hasArtist)
  1251. {
  1252. list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
  1253. }
  1254. if (item is IHasAlbumArtist hasAlbumArtist)
  1255. {
  1256. list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
  1257. }
  1258. list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
  1259. list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
  1260. list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
  1261. // keywords was 5
  1262. list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
  1263. // Remove all invalid values.
  1264. list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
  1265. return list;
  1266. }
  1267. private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
  1268. {
  1269. return new BaseItemImageInfo()
  1270. {
  1271. ItemId = baseItemId,
  1272. Id = Guid.NewGuid(),
  1273. Path = e.Path,
  1274. Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
  1275. DateModified = e.DateModified,
  1276. Height = e.Height,
  1277. Width = e.Width,
  1278. ImageType = (ImageInfoImageType)e.Type,
  1279. Item = null!
  1280. };
  1281. }
  1282. private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
  1283. {
  1284. return new ItemImageInfo()
  1285. {
  1286. Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
  1287. BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
  1288. DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
  1289. Height = e.Height,
  1290. Width = e.Width,
  1291. Type = (ImageType)e.ImageType
  1292. };
  1293. }
  1294. private string? GetPathToSave(string path)
  1295. {
  1296. if (path is null)
  1297. {
  1298. return null;
  1299. }
  1300. return _appHost.ReverseVirtualPath(path);
  1301. }
  1302. private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
  1303. {
  1304. var list = new List<string>();
  1305. if (IsTypeInQuery(BaseItemKind.Person, query))
  1306. {
  1307. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
  1308. }
  1309. if (IsTypeInQuery(BaseItemKind.Genre, query))
  1310. {
  1311. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
  1312. }
  1313. if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
  1314. {
  1315. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
  1316. }
  1317. if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
  1318. {
  1319. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
  1320. }
  1321. if (IsTypeInQuery(BaseItemKind.Studio, query))
  1322. {
  1323. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
  1324. }
  1325. return list;
  1326. }
  1327. private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
  1328. {
  1329. if (query.ExcludeItemTypes.Contains(type))
  1330. {
  1331. return false;
  1332. }
  1333. return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
  1334. }
  1335. private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
  1336. {
  1337. if (!query.GroupByPresentationUniqueKey)
  1338. {
  1339. return false;
  1340. }
  1341. if (query.GroupBySeriesPresentationUniqueKey)
  1342. {
  1343. return false;
  1344. }
  1345. if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
  1346. {
  1347. return false;
  1348. }
  1349. if (query.User is null)
  1350. {
  1351. return false;
  1352. }
  1353. if (query.IncludeItemTypes.Length == 0)
  1354. {
  1355. return true;
  1356. }
  1357. return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
  1358. || query.IncludeItemTypes.Contains(BaseItemKind.Video)
  1359. || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
  1360. || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
  1361. || query.IncludeItemTypes.Contains(BaseItemKind.Series)
  1362. || query.IncludeItemTypes.Contains(BaseItemKind.Season);
  1363. }
  1364. private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDbContext context)
  1365. {
  1366. var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
  1367. var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
  1368. if (hasSearch)
  1369. {
  1370. orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
  1371. }
  1372. else if (orderBy.Length == 0)
  1373. {
  1374. return query.OrderBy(e => e.SortName);
  1375. }
  1376. IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
  1377. var firstOrdering = orderBy.FirstOrDefault();
  1378. if (firstOrdering != default)
  1379. {
  1380. var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
  1381. if (firstOrdering.SortOrder == SortOrder.Ascending)
  1382. {
  1383. orderedQuery = query.OrderBy(expression);
  1384. }
  1385. else
  1386. {
  1387. orderedQuery = query.OrderByDescending(expression);
  1388. }
  1389. if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
  1390. {
  1391. if (firstOrdering.SortOrder is SortOrder.Ascending)
  1392. {
  1393. orderedQuery = orderedQuery.ThenBy(e => e.Name);
  1394. }
  1395. else
  1396. {
  1397. orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
  1398. }
  1399. }
  1400. }
  1401. foreach (var item in orderBy.Skip(1))
  1402. {
  1403. var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
  1404. if (item.SortOrder == SortOrder.Ascending)
  1405. {
  1406. orderedQuery = orderedQuery!.ThenBy(expression);
  1407. }
  1408. else
  1409. {
  1410. orderedQuery = orderedQuery!.ThenByDescending(expression);
  1411. }
  1412. }
  1413. return orderedQuery ?? query;
  1414. }
  1415. private IQueryable<BaseItemEntity> TranslateQuery(
  1416. IQueryable<BaseItemEntity> baseQuery,
  1417. JellyfinDbContext context,
  1418. InternalItemsQuery filter)
  1419. {
  1420. const int HDWidth = 1200;
  1421. const int UHDWidth = 3800;
  1422. const int UHDHeight = 2100;
  1423. var minWidth = filter.MinWidth;
  1424. var maxWidth = filter.MaxWidth;
  1425. var now = DateTime.UtcNow;
  1426. if (filter.IsHD.HasValue || filter.Is4K.HasValue)
  1427. {
  1428. bool includeSD = false;
  1429. bool includeHD = false;
  1430. bool include4K = false;
  1431. if (filter.IsHD.HasValue && !filter.IsHD.Value)
  1432. {
  1433. includeSD = true;
  1434. }
  1435. if (filter.IsHD.HasValue && filter.IsHD.Value)
  1436. {
  1437. includeHD = true;
  1438. }
  1439. if (filter.Is4K.HasValue && filter.Is4K.Value)
  1440. {
  1441. include4K = true;
  1442. }
  1443. baseQuery = baseQuery.Where(e =>
  1444. (includeSD && e.Width < HDWidth) ||
  1445. (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
  1446. (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
  1447. }
  1448. if (minWidth.HasValue)
  1449. {
  1450. baseQuery = baseQuery.Where(e => e.Width >= minWidth);
  1451. }
  1452. if (filter.MinHeight.HasValue)
  1453. {
  1454. baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
  1455. }
  1456. if (maxWidth.HasValue)
  1457. {
  1458. baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
  1459. }
  1460. if (filter.MaxHeight.HasValue)
  1461. {
  1462. baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
  1463. }
  1464. if (filter.IsLocked.HasValue)
  1465. {
  1466. baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
  1467. }
  1468. var tags = filter.Tags.ToList();
  1469. var excludeTags = filter.ExcludeTags.ToList();
  1470. if (filter.IsMovie.HasValue)
  1471. {
  1472. var shouldIncludeAllMovieTypes = filter.IsMovie.Value
  1473. && (filter.IncludeItemTypes.Length == 0
  1474. || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
  1475. || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
  1476. if (!shouldIncludeAllMovieTypes)
  1477. {
  1478. baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
  1479. }
  1480. }
  1481. if (filter.IsSeries.HasValue)
  1482. {
  1483. baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
  1484. }
  1485. if (filter.IsSports.HasValue)
  1486. {
  1487. if (filter.IsSports.Value)
  1488. {
  1489. tags.Add("Sports");
  1490. }
  1491. else
  1492. {
  1493. excludeTags.Add("Sports");
  1494. }
  1495. }
  1496. if (filter.IsNews.HasValue)
  1497. {
  1498. if (filter.IsNews.Value)
  1499. {
  1500. tags.Add("News");
  1501. }
  1502. else
  1503. {
  1504. excludeTags.Add("News");
  1505. }
  1506. }
  1507. if (filter.IsKids.HasValue)
  1508. {
  1509. if (filter.IsKids.Value)
  1510. {
  1511. tags.Add("Kids");
  1512. }
  1513. else
  1514. {
  1515. excludeTags.Add("Kids");
  1516. }
  1517. }
  1518. if (!string.IsNullOrEmpty(filter.SearchTerm))
  1519. {
  1520. var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
  1521. var originalSearchTerm = filter.SearchTerm.ToLower();
  1522. if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
  1523. {
  1524. cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
  1525. baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), originalSearchTerm)));
  1526. }
  1527. else
  1528. {
  1529. baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(originalSearchTerm)));
  1530. }
  1531. }
  1532. if (filter.IsFolder.HasValue)
  1533. {
  1534. baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
  1535. }
  1536. var includeTypes = filter.IncludeItemTypes;
  1537. // Only specify excluded types if no included types are specified
  1538. if (filter.IncludeItemTypes.Length == 0)
  1539. {
  1540. var excludeTypes = filter.ExcludeItemTypes;
  1541. if (excludeTypes.Length == 1)
  1542. {
  1543. if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
  1544. {
  1545. baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
  1546. }
  1547. }
  1548. else if (excludeTypes.Length > 1)
  1549. {
  1550. var excludeTypeName = new List<string>();
  1551. foreach (var excludeType in excludeTypes)
  1552. {
  1553. if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
  1554. {
  1555. excludeTypeName.Add(baseItemKindName!);
  1556. }
  1557. }
  1558. baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
  1559. }
  1560. }
  1561. else
  1562. {
  1563. string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e => e != null).ToArray()!;
  1564. baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
  1565. }
  1566. if (filter.ChannelIds.Count > 0)
  1567. {
  1568. baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
  1569. }
  1570. if (!filter.ParentId.IsEmpty())
  1571. {
  1572. baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
  1573. }
  1574. if (!string.IsNullOrWhiteSpace(filter.Path))
  1575. {
  1576. var pathToQuery = GetPathToSave(filter.Path);
  1577. baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
  1578. }
  1579. if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
  1580. {
  1581. baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
  1582. }
  1583. if (filter.MinCommunityRating.HasValue)
  1584. {
  1585. baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
  1586. }
  1587. if (filter.MinIndexNumber.HasValue)
  1588. {
  1589. baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
  1590. }
  1591. if (filter.MinParentAndIndexNumber.HasValue)
  1592. {
  1593. baseQuery = baseQuery
  1594. .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= filter.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > filter.MinParentAndIndexNumber.Value.ParentIndexNumber);
  1595. }
  1596. if (filter.MinDateCreated.HasValue)
  1597. {
  1598. baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
  1599. }
  1600. if (filter.MinDateLastSaved.HasValue)
  1601. {
  1602. baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value);
  1603. }
  1604. if (filter.MinDateLastSavedForUser.HasValue)
  1605. {
  1606. baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUser.Value);
  1607. }
  1608. if (filter.IndexNumber.HasValue)
  1609. {
  1610. baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
  1611. }
  1612. if (filter.ParentIndexNumber.HasValue)
  1613. {
  1614. baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
  1615. }
  1616. if (filter.ParentIndexNumberNotEquals.HasValue)
  1617. {
  1618. baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null);
  1619. }
  1620. var minEndDate = filter.MinEndDate;
  1621. var maxEndDate = filter.MaxEndDate;
  1622. if (filter.HasAired.HasValue)
  1623. {
  1624. if (filter.HasAired.Value)
  1625. {
  1626. maxEndDate = DateTime.UtcNow;
  1627. }
  1628. else
  1629. {
  1630. minEndDate = DateTime.UtcNow;
  1631. }
  1632. }
  1633. if (minEndDate.HasValue)
  1634. {
  1635. baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
  1636. }
  1637. if (maxEndDate.HasValue)
  1638. {
  1639. baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
  1640. }
  1641. if (filter.MinStartDate.HasValue)
  1642. {
  1643. baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
  1644. }
  1645. if (filter.MaxStartDate.HasValue)
  1646. {
  1647. baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
  1648. }
  1649. if (filter.MinPremiereDate.HasValue)
  1650. {
  1651. baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
  1652. }
  1653. if (filter.MaxPremiereDate.HasValue)
  1654. {
  1655. baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
  1656. }
  1657. if (filter.TrailerTypes.Length > 0)
  1658. {
  1659. var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
  1660. baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
  1661. }
  1662. if (filter.IsAiring.HasValue)
  1663. {
  1664. if (filter.IsAiring.Value)
  1665. {
  1666. baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
  1667. }
  1668. else
  1669. {
  1670. baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
  1671. }
  1672. }
  1673. if (filter.PersonIds.Length > 0)
  1674. {
  1675. var peopleEntityIds = context.BaseItems
  1676. .WhereOneOrMany(filter.PersonIds, b => b.Id)
  1677. .Join(
  1678. context.Peoples,
  1679. b => b.Name,
  1680. p => p.Name,
  1681. (b, p) => p.Id);
  1682. baseQuery = baseQuery
  1683. .Where(e => context.PeopleBaseItemMap
  1684. .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
  1685. }
  1686. if (!string.IsNullOrWhiteSpace(filter.Person))
  1687. {
  1688. baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
  1689. }
  1690. if (!string.IsNullOrWhiteSpace(filter.MinSortName))
  1691. {
  1692. // this does not makes sense.
  1693. // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
  1694. // whereClauses.Add("SortName>=@MinSortName");
  1695. // statement?.TryBind("@MinSortName", query.MinSortName);
  1696. }
  1697. if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
  1698. {
  1699. baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
  1700. }
  1701. if (!string.IsNullOrWhiteSpace(filter.ExternalId))
  1702. {
  1703. baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
  1704. }
  1705. if (!string.IsNullOrWhiteSpace(filter.Name))
  1706. {
  1707. if (filter.UseRawName == true)
  1708. {
  1709. baseQuery = baseQuery.Where(e => e.Name == filter.Name);
  1710. }
  1711. else
  1712. {
  1713. var cleanName = GetCleanValue(filter.Name);
  1714. baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
  1715. }
  1716. }
  1717. // These are the same, for now
  1718. var nameContains = filter.NameContains;
  1719. if (!string.IsNullOrWhiteSpace(nameContains))
  1720. {
  1721. if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
  1722. {
  1723. nameContains = $"%{nameContains.Trim('%')}%";
  1724. baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.OriginalTitle, nameContains));
  1725. }
  1726. else
  1727. {
  1728. baseQuery = baseQuery.Where(e =>
  1729. e.CleanName!.Contains(nameContains)
  1730. || e.OriginalTitle!.ToLower().Contains(nameContains!));
  1731. }
  1732. }
  1733. if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
  1734. {
  1735. var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
  1736. baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
  1737. }
  1738. if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
  1739. {
  1740. var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
  1741. baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
  1742. }
  1743. if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
  1744. {
  1745. var lessThanLower = filter.NameLessThan.ToLowerInvariant();
  1746. baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
  1747. }
  1748. if (filter.ImageTypes.Length > 0)
  1749. {
  1750. var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
  1751. baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
  1752. }
  1753. if (filter.IsLiked.HasValue)
  1754. {
  1755. baseQuery = baseQuery
  1756. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLikeValue);
  1757. }
  1758. if (filter.IsFavoriteOrLiked.HasValue)
  1759. {
  1760. baseQuery = baseQuery
  1761. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavoriteOrLiked);
  1762. }
  1763. if (filter.IsFavorite.HasValue)
  1764. {
  1765. baseQuery = baseQuery
  1766. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorite);
  1767. }
  1768. if (filter.IsPlayed.HasValue)
  1769. {
  1770. // We should probably figure this out for all folders, but for right now, this is the only place where we need it
  1771. if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
  1772. {
  1773. baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
  1774. .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
  1775. .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
  1776. .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
  1777. }
  1778. else
  1779. {
  1780. baseQuery = baseQuery
  1781. .Select(e => new
  1782. {
  1783. IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).FirstOrDefault() ?? false,
  1784. Item = e
  1785. })
  1786. .Where(e => e.IsPlayed == filter.IsPlayed)
  1787. .Select(f => f.Item);
  1788. }
  1789. }
  1790. if (filter.IsResumable.HasValue)
  1791. {
  1792. if (filter.IsResumable.Value)
  1793. {
  1794. baseQuery = baseQuery
  1795. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks > 0);
  1796. }
  1797. else
  1798. {
  1799. baseQuery = baseQuery
  1800. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks == 0);
  1801. }
  1802. }
  1803. if (filter.ArtistIds.Length > 0)
  1804. {
  1805. baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ArtistIds);
  1806. }
  1807. if (filter.AlbumArtistIds.Length > 0)
  1808. {
  1809. baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
  1810. }
  1811. if (filter.ContributingArtistIds.Length > 0)
  1812. {
  1813. var contributingNames = context.BaseItems
  1814. .Where(b => filter.ContributingArtistIds.Contains(b.Id))
  1815. .Select(b => b.CleanName);
  1816. baseQuery = baseQuery.Where(e =>
  1817. e.ItemValues!.Any(ivm =>
  1818. ivm.ItemValue.Type == ItemValueType.Artist &&
  1819. contributingNames.Contains(ivm.ItemValue.CleanValue))
  1820. &&
  1821. !e.ItemValues!.Any(ivm =>
  1822. ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
  1823. contributingNames.Contains(ivm.ItemValue.CleanValue)));
  1824. }
  1825. if (filter.AlbumIds.Length > 0)
  1826. {
  1827. var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
  1828. baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
  1829. }
  1830. if (filter.ExcludeArtistIds.Length > 0)
  1831. {
  1832. baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ExcludeArtistIds, true);
  1833. }
  1834. if (filter.GenreIds.Count > 0)
  1835. {
  1836. baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
  1837. }
  1838. if (filter.Genres.Count > 0)
  1839. {
  1840. var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
  1841. baseQuery = baseQuery
  1842. .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(cleanGenres));
  1843. }
  1844. if (tags.Count > 0)
  1845. {
  1846. var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
  1847. baseQuery = baseQuery
  1848. .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(cleanValues));
  1849. }
  1850. if (excludeTags.Count > 0)
  1851. {
  1852. var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
  1853. baseQuery = baseQuery
  1854. .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(cleanValues));
  1855. }
  1856. if (filter.StudioIds.Length > 0)
  1857. {
  1858. baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
  1859. }
  1860. if (filter.OfficialRatings.Length > 0)
  1861. {
  1862. baseQuery = baseQuery
  1863. .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
  1864. }
  1865. Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
  1866. if (filter.MinParentalRating != null)
  1867. {
  1868. var min = filter.MinParentalRating;
  1869. var minScore = min.Score;
  1870. var minSubScore = min.SubScore ?? 0;
  1871. minParentalRatingFilter = e =>
  1872. e.InheritedParentalRatingValue == null ||
  1873. e.InheritedParentalRatingValue > minScore ||
  1874. (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
  1875. }
  1876. Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
  1877. if (filter.MaxParentalRating != null)
  1878. {
  1879. var max = filter.MaxParentalRating;
  1880. var maxScore = max.Score;
  1881. var maxSubScore = max.SubScore ?? 0;
  1882. maxParentalRatingFilter = e =>
  1883. e.InheritedParentalRatingValue == null ||
  1884. e.InheritedParentalRatingValue < maxScore ||
  1885. (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
  1886. }
  1887. if (filter.HasParentalRating ?? false)
  1888. {
  1889. if (minParentalRatingFilter != null)
  1890. {
  1891. baseQuery = baseQuery.Where(minParentalRatingFilter);
  1892. }
  1893. if (maxParentalRatingFilter != null)
  1894. {
  1895. baseQuery = baseQuery.Where(maxParentalRatingFilter);
  1896. }
  1897. }
  1898. else if (filter.BlockUnratedItems.Length > 0)
  1899. {
  1900. var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
  1901. Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType);
  1902. if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
  1903. {
  1904. baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)));
  1905. }
  1906. else if (minParentalRatingFilter != null)
  1907. {
  1908. baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
  1909. }
  1910. else if (maxParentalRatingFilter != null)
  1911. {
  1912. baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
  1913. }
  1914. else
  1915. {
  1916. baseQuery = baseQuery.Where(unratedItemFilter);
  1917. }
  1918. }
  1919. else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
  1920. {
  1921. if (minParentalRatingFilter != null)
  1922. {
  1923. baseQuery = baseQuery.Where(minParentalRatingFilter);
  1924. }
  1925. if (maxParentalRatingFilter != null)
  1926. {
  1927. baseQuery = baseQuery.Where(maxParentalRatingFilter);
  1928. }
  1929. }
  1930. else if (!filter.HasParentalRating ?? false)
  1931. {
  1932. baseQuery = baseQuery
  1933. .Where(e => e.InheritedParentalRatingValue == null);
  1934. }
  1935. if (filter.HasOfficialRating.HasValue)
  1936. {
  1937. if (filter.HasOfficialRating.Value)
  1938. {
  1939. baseQuery = baseQuery
  1940. .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
  1941. }
  1942. else
  1943. {
  1944. baseQuery = baseQuery
  1945. .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
  1946. }
  1947. }
  1948. if (filter.HasOverview.HasValue)
  1949. {
  1950. if (filter.HasOverview.Value)
  1951. {
  1952. baseQuery = baseQuery
  1953. .Where(e => e.Overview != null && e.Overview != string.Empty);
  1954. }
  1955. else
  1956. {
  1957. baseQuery = baseQuery
  1958. .Where(e => e.Overview == null || e.Overview == string.Empty);
  1959. }
  1960. }
  1961. if (filter.HasOwnerId.HasValue)
  1962. {
  1963. if (filter.HasOwnerId.Value)
  1964. {
  1965. baseQuery = baseQuery
  1966. .Where(e => e.OwnerId != null);
  1967. }
  1968. else
  1969. {
  1970. baseQuery = baseQuery
  1971. .Where(e => e.OwnerId == null);
  1972. }
  1973. }
  1974. if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
  1975. {
  1976. baseQuery = baseQuery
  1977. .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filter.HasNoAudioTrackWithLanguage));
  1978. }
  1979. if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
  1980. {
  1981. baseQuery = baseQuery
  1982. .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal && f.Language == filter.HasNoInternalSubtitleTrackWithLanguage));
  1983. }
  1984. if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
  1985. {
  1986. baseQuery = baseQuery
  1987. .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && f.Language == filter.HasNoExternalSubtitleTrackWithLanguage));
  1988. }
  1989. if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
  1990. {
  1991. baseQuery = baseQuery
  1992. .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == filter.HasNoSubtitleTrackWithLanguage));
  1993. }
  1994. if (filter.HasSubtitles.HasValue)
  1995. {
  1996. baseQuery = baseQuery
  1997. .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtitles.Value);
  1998. }
  1999. if (filter.HasChapterImages.HasValue)
  2000. {
  2001. baseQuery = baseQuery
  2002. .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
  2003. }
  2004. if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
  2005. {
  2006. baseQuery = baseQuery
  2007. .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any(f => f.Id == e.ParentId.Value));
  2008. }
  2009. if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
  2010. {
  2011. baseQuery = baseQuery
  2012. .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name));
  2013. }
  2014. if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
  2015. {
  2016. baseQuery = baseQuery
  2017. .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name));
  2018. }
  2019. if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
  2020. {
  2021. baseQuery = baseQuery
  2022. .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name));
  2023. }
  2024. if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
  2025. {
  2026. baseQuery = baseQuery
  2027. .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
  2028. }
  2029. if (filter.Years.Length > 0)
  2030. {
  2031. baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
  2032. }
  2033. var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
  2034. if (isVirtualItem.HasValue)
  2035. {
  2036. baseQuery = baseQuery
  2037. .Where(e => e.IsVirtualItem == isVirtualItem.Value);
  2038. }
  2039. if (filter.IsSpecialSeason.HasValue)
  2040. {
  2041. if (filter.IsSpecialSeason.Value)
  2042. {
  2043. baseQuery = baseQuery
  2044. .Where(e => e.IndexNumber == 0);
  2045. }
  2046. else
  2047. {
  2048. baseQuery = baseQuery
  2049. .Where(e => e.IndexNumber != 0);
  2050. }
  2051. }
  2052. if (filter.IsUnaired.HasValue)
  2053. {
  2054. if (filter.IsUnaired.Value)
  2055. {
  2056. baseQuery = baseQuery
  2057. .Where(e => e.PremiereDate >= now);
  2058. }
  2059. else
  2060. {
  2061. baseQuery = baseQuery
  2062. .Where(e => e.PremiereDate < now);
  2063. }
  2064. }
  2065. if (filter.MediaTypes.Length > 0)
  2066. {
  2067. var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
  2068. baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
  2069. }
  2070. if (filter.ItemIds.Length > 0)
  2071. {
  2072. baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
  2073. }
  2074. if (filter.ExcludeItemIds.Length > 0)
  2075. {
  2076. baseQuery = baseQuery
  2077. .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
  2078. }
  2079. if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
  2080. {
  2081. var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
  2082. baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !exclude.Contains(f)));
  2083. }
  2084. if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
  2085. {
  2086. // Allow setting a null or empty value to get all items that have the specified provider set.
  2087. var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArray();
  2088. if (includeAny.Length > 0)
  2089. {
  2090. baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
  2091. }
  2092. var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Key}:{e.Value}").ToArray();
  2093. if (includeSelected.Length > 0)
  2094. {
  2095. baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => includeSelected.Contains(f)));
  2096. }
  2097. }
  2098. if (filter.HasImdbId.HasValue)
  2099. {
  2100. baseQuery = filter.HasImdbId.Value
  2101. ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().ToLower()))
  2102. : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().ToLower()));
  2103. }
  2104. if (filter.HasTmdbId.HasValue)
  2105. {
  2106. baseQuery = filter.HasTmdbId.Value
  2107. ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().ToLower()))
  2108. : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().ToLower()));
  2109. }
  2110. if (filter.HasTvdbId.HasValue)
  2111. {
  2112. baseQuery = filter.HasTvdbId.Value
  2113. ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().ToLower()))
  2114. : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().ToLower()));
  2115. }
  2116. var queryTopParentIds = filter.TopParentIds;
  2117. if (queryTopParentIds.Length > 0)
  2118. {
  2119. var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
  2120. var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
  2121. if (enableItemsByName && includedItemByNameTypes.Count > 0)
  2122. {
  2123. baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w == e.TopParentId!.Value));
  2124. }
  2125. else
  2126. {
  2127. baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
  2128. }
  2129. }
  2130. if (filter.AncestorIds.Length > 0)
  2131. {
  2132. baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
  2133. }
  2134. if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
  2135. {
  2136. baseQuery = baseQuery
  2137. .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id)));
  2138. }
  2139. if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
  2140. {
  2141. baseQuery = baseQuery
  2142. .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
  2143. }
  2144. if (filter.ExcludeInheritedTags.Length > 0)
  2145. {
  2146. baseQuery = baseQuery.Where(e =>
  2147. !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))
  2148. && (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
  2149. !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))));
  2150. }
  2151. if (filter.IncludeInheritedTags.Length > 0)
  2152. {
  2153. // For seasons and episodes, we also need to check the parent series' tags.
  2154. if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
  2155. {
  2156. baseQuery = baseQuery.Where(e =>
  2157. e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
  2158. || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
  2159. }
  2160. // A playlist should be accessible to its owner regardless of allowed tags.
  2161. else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
  2162. {
  2163. baseQuery = baseQuery.Where(e =>
  2164. e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
  2165. || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
  2166. // d ^^ this is stupid it hate this.
  2167. }
  2168. else
  2169. {
  2170. baseQuery = baseQuery.Where(e =>
  2171. e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
  2172. }
  2173. }
  2174. if (filter.SeriesStatuses.Length > 0)
  2175. {
  2176. var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
  2177. baseQuery = baseQuery
  2178. .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
  2179. }
  2180. if (filter.BoxSetLibraryFolders.Length > 0)
  2181. {
  2182. var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).ToArray();
  2183. baseQuery = baseQuery
  2184. .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
  2185. }
  2186. if (filter.VideoTypes.Length > 0)
  2187. {
  2188. var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
  2189. baseQuery = baseQuery
  2190. .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
  2191. }
  2192. if (filter.Is3D.HasValue)
  2193. {
  2194. if (filter.Is3D.Value)
  2195. {
  2196. baseQuery = baseQuery
  2197. .Where(e => e.Data!.Contains("Video3DFormat"));
  2198. }
  2199. else
  2200. {
  2201. baseQuery = baseQuery
  2202. .Where(e => !e.Data!.Contains("Video3DFormat"));
  2203. }
  2204. }
  2205. if (filter.IsPlaceHolder.HasValue)
  2206. {
  2207. if (filter.IsPlaceHolder.Value)
  2208. {
  2209. baseQuery = baseQuery
  2210. .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
  2211. }
  2212. else
  2213. {
  2214. baseQuery = baseQuery
  2215. .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
  2216. }
  2217. }
  2218. if (filter.HasSpecialFeature.HasValue)
  2219. {
  2220. if (filter.HasSpecialFeature.Value)
  2221. {
  2222. baseQuery = baseQuery
  2223. .Where(e => e.ExtraIds != null);
  2224. }
  2225. else
  2226. {
  2227. baseQuery = baseQuery
  2228. .Where(e => e.ExtraIds == null);
  2229. }
  2230. }
  2231. if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
  2232. {
  2233. if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo.GetValueOrDefault())
  2234. {
  2235. baseQuery = baseQuery
  2236. .Where(e => e.ExtraIds != null);
  2237. }
  2238. else
  2239. {
  2240. baseQuery = baseQuery
  2241. .Where(e => e.ExtraIds == null);
  2242. }
  2243. }
  2244. return baseQuery;
  2245. }
  2246. /// <inheritdoc/>
  2247. public async Task<bool> ItemExistsAsync(Guid id)
  2248. {
  2249. var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
  2250. await using (dbContext.ConfigureAwait(false))
  2251. {
  2252. return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
  2253. }
  2254. }
  2255. /// <inheritdoc/>
  2256. public bool GetIsPlayed(User user, Guid id, bool recursive)
  2257. {
  2258. using var dbContext = _dbProvider.CreateDbContext();
  2259. if (recursive)
  2260. {
  2261. var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
  2262. return dbContext.BaseItems
  2263. .Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
  2264. .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
  2265. }
  2266. return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
  2267. }
  2268. private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseItemEntity, bool>>? filter = null)
  2269. {
  2270. var folderStack = new HashSet<Guid>()
  2271. {
  2272. parentId
  2273. };
  2274. var folderList = new HashSet<Guid>()
  2275. {
  2276. parentId
  2277. };
  2278. while (folderStack.Count != 0)
  2279. {
  2280. var items = folderStack.ToArray();
  2281. folderStack.Clear();
  2282. var query = dbContext.BaseItems
  2283. .WhereOneOrMany(items, e => e.ParentId!.Value);
  2284. if (filter != null)
  2285. {
  2286. query = query.Where(filter);
  2287. }
  2288. foreach (var item in query.Select(e => e.Id).ToArray())
  2289. {
  2290. if (folderList.Add(item))
  2291. {
  2292. folderStack.Add(item);
  2293. }
  2294. }
  2295. }
  2296. return folderList;
  2297. }
  2298. /// <inheritdoc/>
  2299. public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
  2300. {
  2301. using var dbContext = _dbProvider.CreateDbContext();
  2302. var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!)
  2303. .Where(e => artistNames.Contains(e.Name))
  2304. .ToArray();
  2305. return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray());
  2306. }
  2307. }