BaseItemRepository.cs 80 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176
  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.Collections.Immutable;
  10. using System.Globalization;
  11. using System.IO;
  12. using System.Linq;
  13. using System.Linq.Expressions;
  14. using System.Reflection;
  15. using System.Text;
  16. using System.Text.Json;
  17. using System.Threading;
  18. using Jellyfin.Data.Entities;
  19. using Jellyfin.Data.Enums;
  20. using Jellyfin.Extensions;
  21. using Jellyfin.Extensions.Json;
  22. using MediaBrowser.Common;
  23. using MediaBrowser.Controller;
  24. using MediaBrowser.Controller.Channels;
  25. using MediaBrowser.Controller.Configuration;
  26. using MediaBrowser.Controller.Entities;
  27. using MediaBrowser.Controller.Entities.Audio;
  28. using MediaBrowser.Controller.Entities.TV;
  29. using MediaBrowser.Controller.LiveTv;
  30. using MediaBrowser.Controller.Persistence;
  31. using MediaBrowser.Model.Dto;
  32. using MediaBrowser.Model.Entities;
  33. using MediaBrowser.Model.LiveTv;
  34. using MediaBrowser.Model.Querying;
  35. using Microsoft.EntityFrameworkCore;
  36. using Microsoft.Extensions.Logging;
  37. using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
  38. using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity;
  39. namespace Jellyfin.Server.Implementations.Item;
  40. /*
  41. All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null-forgiving operator "!".
  42. This is done as the code isn't actually executed client side, but only the expressions are interpret and the compiler cannot know that.
  43. This is your only warning/message regarding this topic.
  44. */
  45. /// <summary>
  46. /// Handles all storage logic for BaseItems.
  47. /// </summary>
  48. public sealed class BaseItemRepository
  49. : IItemRepository
  50. {
  51. /// <summary>
  52. /// This holds all the types in the running assemblies
  53. /// so that we can de-serialize properly when we don't have strong types.
  54. /// </summary>
  55. private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
  56. private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
  57. private readonly IServerApplicationHost _appHost;
  58. private readonly IItemTypeLookup _itemTypeLookup;
  59. private readonly IServerConfigurationManager _serverConfigurationManager;
  60. private readonly ILogger<BaseItemRepository> _logger;
  61. private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType.AlbumArtist];
  62. private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
  63. private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
  64. private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
  65. private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Studios];
  66. /// <summary>
  67. /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
  68. /// </summary>
  69. /// <param name="dbProvider">The db factory.</param>
  70. /// <param name="appHost">The Application host.</param>
  71. /// <param name="itemTypeLookup">The static type lookup.</param>
  72. /// <param name="serverConfigurationManager">The server Configuration manager.</param>
  73. /// <param name="logger">System logger.</param>
  74. public BaseItemRepository(
  75. IDbContextFactory<JellyfinDbContext> dbProvider,
  76. IServerApplicationHost appHost,
  77. IItemTypeLookup itemTypeLookup,
  78. IServerConfigurationManager serverConfigurationManager,
  79. ILogger<BaseItemRepository> logger)
  80. {
  81. _dbProvider = dbProvider;
  82. _appHost = appHost;
  83. _itemTypeLookup = itemTypeLookup;
  84. _serverConfigurationManager = serverConfigurationManager;
  85. _logger = logger;
  86. }
  87. /// <inheritdoc />
  88. public void DeleteItem(Guid id)
  89. {
  90. if (id.IsEmpty())
  91. {
  92. throw new ArgumentException("Guid can't be empty", nameof(id));
  93. }
  94. using var context = _dbProvider.CreateDbContext();
  95. using var transaction = context.Database.BeginTransaction();
  96. context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
  97. context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
  98. context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
  99. context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
  100. context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
  101. context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
  102. context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
  103. context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
  104. context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
  105. context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
  106. context.SaveChanges();
  107. transaction.Commit();
  108. }
  109. /// <inheritdoc />
  110. public void UpdateInheritedValues()
  111. {
  112. using var context = _dbProvider.CreateDbContext();
  113. using var transaction = context.Database.BeginTransaction();
  114. context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
  115. // ItemValue Inheritance is now correctly mapped via AncestorId on demand
  116. context.SaveChanges();
  117. transaction.Commit();
  118. }
  119. /// <inheritdoc />
  120. public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
  121. {
  122. ArgumentNullException.ThrowIfNull(filter);
  123. PrepareFilterQuery(filter);
  124. using var context = _dbProvider.CreateDbContext();
  125. return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray();
  126. }
  127. /// <inheritdoc />
  128. public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter)
  129. {
  130. return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
  131. }
  132. /// <inheritdoc />
  133. public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter)
  134. {
  135. return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
  136. }
  137. /// <inheritdoc />
  138. public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
  139. {
  140. return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
  141. }
  142. /// <inheritdoc />
  143. public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter)
  144. {
  145. return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
  146. }
  147. /// <inheritdoc />
  148. public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter)
  149. {
  150. return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
  151. }
  152. /// <inheritdoc />
  153. public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
  154. {
  155. return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
  156. }
  157. /// <inheritdoc />
  158. public IReadOnlyList<string> GetStudioNames()
  159. {
  160. return GetItemValueNames(_getStudiosValueTypes, [], []);
  161. }
  162. /// <inheritdoc />
  163. public IReadOnlyList<string> GetAllArtistNames()
  164. {
  165. return GetItemValueNames(_getAllArtistsValueTypes, [], []);
  166. }
  167. /// <inheritdoc />
  168. public IReadOnlyList<string> GetMusicGenreNames()
  169. {
  170. return GetItemValueNames(
  171. _getGenreValueTypes,
  172. _itemTypeLookup.MusicGenreTypes,
  173. []);
  174. }
  175. /// <inheritdoc />
  176. public IReadOnlyList<string> GetGenreNames()
  177. {
  178. return GetItemValueNames(
  179. _getGenreValueTypes,
  180. [],
  181. _itemTypeLookup.MusicGenreTypes);
  182. }
  183. /// <inheritdoc />
  184. public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
  185. {
  186. ArgumentNullException.ThrowIfNull(filter);
  187. if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
  188. {
  189. var returnList = GetItemList(filter);
  190. return new QueryResult<BaseItemDto>(
  191. filter.StartIndex,
  192. returnList.Count,
  193. returnList);
  194. }
  195. PrepareFilterQuery(filter);
  196. var result = new QueryResult<BaseItemDto>();
  197. using var context = _dbProvider.CreateDbContext();
  198. IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
  199. dbQuery = TranslateQuery(dbQuery, context, filter);
  200. if (filter.EnableTotalRecordCount)
  201. {
  202. result.TotalRecordCount = dbQuery.Count();
  203. }
  204. dbQuery = ApplyGroupingFilter(dbQuery, filter);
  205. dbQuery = ApplyQueryPageing(dbQuery, filter);
  206. result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
  207. result.StartIndex = filter.StartIndex ?? 0;
  208. return result;
  209. }
  210. /// <inheritdoc />
  211. public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
  212. {
  213. ArgumentNullException.ThrowIfNull(filter);
  214. PrepareFilterQuery(filter);
  215. using var context = _dbProvider.CreateDbContext();
  216. IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
  217. dbQuery = TranslateQuery(dbQuery, context, filter);
  218. dbQuery = ApplyGroupingFilter(dbQuery, filter);
  219. dbQuery = ApplyQueryPageing(dbQuery, filter);
  220. return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
  221. }
  222. private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
  223. {
  224. // This whole block is needed to filter duplicate entries on request
  225. // for the time beeing it cannot be used because it would destroy the ordering
  226. // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but
  227. // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
  228. // var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
  229. // if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
  230. // {
  231. // dbQuery = ApplyOrder(dbQuery, filter);
  232. // dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.First());
  233. // }
  234. // else if (enableGroupByPresentationUniqueKey)
  235. // {
  236. // dbQuery = ApplyOrder(dbQuery, filter);
  237. // dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First());
  238. // }
  239. // else if (filter.GroupBySeriesPresentationUniqueKey)
  240. // {
  241. // dbQuery = ApplyOrder(dbQuery, filter);
  242. // dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First());
  243. // }
  244. // else
  245. // {
  246. // dbQuery = dbQuery.Distinct();
  247. // dbQuery = ApplyOrder(dbQuery, filter);
  248. // }
  249. dbQuery = dbQuery.Distinct();
  250. dbQuery = ApplyOrder(dbQuery, filter);
  251. return dbQuery;
  252. }
  253. private IQueryable<BaseItemEntity> ApplyQueryPageing(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
  254. {
  255. if (filter.Limit.HasValue || filter.StartIndex.HasValue)
  256. {
  257. var offset = filter.StartIndex ?? 0;
  258. if (offset > 0)
  259. {
  260. dbQuery = dbQuery.Skip(offset);
  261. }
  262. if (filter.Limit.HasValue)
  263. {
  264. dbQuery = dbQuery.Take(filter.Limit.Value);
  265. }
  266. }
  267. return dbQuery;
  268. }
  269. private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
  270. {
  271. dbQuery = TranslateQuery(dbQuery, context, filter);
  272. dbQuery = ApplyOrder(dbQuery, filter);
  273. dbQuery = ApplyGroupingFilter(dbQuery, filter);
  274. dbQuery = ApplyQueryPageing(dbQuery, filter);
  275. return dbQuery;
  276. }
  277. private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
  278. {
  279. IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking().AsSplitQuery()
  280. .Include(e => e.TrailerTypes)
  281. .Include(e => e.Provider)
  282. .Include(e => e.LockedFields);
  283. if (filter.DtoOptions.EnableImages)
  284. {
  285. dbQuery = dbQuery.Include(e => e.Images);
  286. }
  287. return dbQuery;
  288. }
  289. /// <inheritdoc/>
  290. public int GetCount(InternalItemsQuery filter)
  291. {
  292. ArgumentNullException.ThrowIfNull(filter);
  293. // Hack for right now since we currently don't support filtering out these duplicates within a query
  294. PrepareFilterQuery(filter);
  295. using var context = _dbProvider.CreateDbContext();
  296. var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
  297. return dbQuery.Count();
  298. }
  299. #pragma warning disable CA1307 // Specify StringComparison for clarity
  300. /// <summary>
  301. /// Gets the type.
  302. /// </summary>
  303. /// <param name="typeName">Name of the type.</param>
  304. /// <returns>Type.</returns>
  305. /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
  306. private static Type? GetType(string typeName)
  307. {
  308. ArgumentException.ThrowIfNullOrEmpty(typeName);
  309. // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static variable and be loaded eagar.
  310. // 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
  311. return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
  312. .Select(a => a.GetType(k))
  313. .FirstOrDefault(t => t is not null));
  314. }
  315. /// <inheritdoc />
  316. public void SaveImages(BaseItemDto item)
  317. {
  318. ArgumentNullException.ThrowIfNull(item);
  319. var images = item.ImageInfos.Select(e => Map(item.Id, e));
  320. using var context = _dbProvider.CreateDbContext();
  321. using var transaction = context.Database.BeginTransaction();
  322. context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
  323. context.BaseItemImageInfos.AddRange(images);
  324. context.SaveChanges();
  325. transaction.Commit();
  326. }
  327. /// <inheritdoc />
  328. public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
  329. {
  330. UpdateOrInsertItems(items, cancellationToken);
  331. }
  332. /// <inheritdoc cref="IItemRepository"/>
  333. public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
  334. {
  335. ArgumentNullException.ThrowIfNull(items);
  336. cancellationToken.ThrowIfCancellationRequested();
  337. var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> UserDataKey, List<string> InheritedTags)>();
  338. foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()))
  339. {
  340. var ancestorIds = item.SupportsAncestors ?
  341. item.GetAncestorIds().Distinct().ToList() :
  342. null;
  343. var topParent = item.GetTopParent();
  344. var userdataKey = item.GetUserDataKeys();
  345. var inheritedTags = item.GetInheritedTags();
  346. tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
  347. }
  348. var localItemValueCache = new Dictionary<(int MagicNumber, string Value), Guid>();
  349. using var context = _dbProvider.CreateDbContext();
  350. using var transaction = context.Database.BeginTransaction();
  351. foreach (var item in tuples)
  352. {
  353. var entity = Map(item.Item);
  354. // TODO: refactor this "inconsistency"
  355. entity.TopParentId = item.TopParent?.Id;
  356. if (!context.BaseItems.Any(e => e.Id == entity.Id))
  357. {
  358. context.BaseItems.Add(entity);
  359. }
  360. else
  361. {
  362. context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
  363. context.BaseItems.Attach(entity).State = EntityState.Modified;
  364. }
  365. context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete();
  366. if (item.Item.SupportsAncestors && item.AncestorIds != null)
  367. {
  368. foreach (var ancestorId in item.AncestorIds)
  369. {
  370. if (!context.BaseItems.Any(f => f.Id == ancestorId))
  371. {
  372. continue;
  373. }
  374. context.AncestorIds.Add(new AncestorId()
  375. {
  376. ParentItemId = ancestorId,
  377. ItemId = entity.Id,
  378. Item = null!,
  379. ParentItem = null!
  380. });
  381. }
  382. }
  383. // Never save duplicate itemValues as they are now mapped anyway.
  384. var itemValuesToSave = GetItemValuesToSave(item.Item, item.InheritedTags).DistinctBy(e => (GetCleanValue(e.Value), e.MagicNumber));
  385. context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete();
  386. foreach (var itemValue in itemValuesToSave)
  387. {
  388. if (!localItemValueCache.TryGetValue(itemValue, out var refValue))
  389. {
  390. refValue = context.ItemValues
  391. .Where(f => f.CleanValue == GetCleanValue(itemValue.Value) && (int)f.Type == itemValue.MagicNumber)
  392. .Select(e => e.ItemValueId)
  393. .FirstOrDefault();
  394. }
  395. if (refValue.IsEmpty())
  396. {
  397. context.ItemValues.Add(new ItemValue()
  398. {
  399. CleanValue = GetCleanValue(itemValue.Value),
  400. Type = (ItemValueType)itemValue.MagicNumber,
  401. ItemValueId = refValue = Guid.NewGuid(),
  402. Value = itemValue.Value
  403. });
  404. localItemValueCache[itemValue] = refValue;
  405. }
  406. context.ItemValuesMap.Add(new ItemValueMap()
  407. {
  408. Item = null!,
  409. ItemId = entity.Id,
  410. ItemValue = null!,
  411. ItemValueId = refValue
  412. });
  413. }
  414. }
  415. context.SaveChanges();
  416. transaction.Commit();
  417. }
  418. /// <inheritdoc />
  419. public BaseItemDto? RetrieveItem(Guid id)
  420. {
  421. if (id.IsEmpty())
  422. {
  423. throw new ArgumentException("Guid can't be empty", nameof(id));
  424. }
  425. using var context = _dbProvider.CreateDbContext();
  426. var item = PrepareItemQuery(context, new()
  427. {
  428. DtoOptions = new()
  429. {
  430. EnableImages = true
  431. }
  432. }).FirstOrDefault(e => e.Id == id);
  433. if (item is null)
  434. {
  435. return null;
  436. }
  437. return DeserialiseBaseItem(item);
  438. }
  439. /// <summary>
  440. /// Maps a Entity to the DTO.
  441. /// </summary>
  442. /// <param name="entity">The entity.</param>
  443. /// <param name="dto">The dto base instance.</param>
  444. /// <param name="appHost">The Application server Host.</param>
  445. /// <returns>The dto to map.</returns>
  446. public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost)
  447. {
  448. dto.Id = entity.Id;
  449. dto.ParentId = entity.ParentId.GetValueOrDefault();
  450. dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
  451. dto.EndDate = entity.EndDate;
  452. dto.CommunityRating = entity.CommunityRating;
  453. dto.CustomRating = entity.CustomRating;
  454. dto.IndexNumber = entity.IndexNumber;
  455. dto.IsLocked = entity.IsLocked;
  456. dto.Name = entity.Name;
  457. dto.OfficialRating = entity.OfficialRating;
  458. dto.Overview = entity.Overview;
  459. dto.ParentIndexNumber = entity.ParentIndexNumber;
  460. dto.PremiereDate = entity.PremiereDate;
  461. dto.ProductionYear = entity.ProductionYear;
  462. dto.SortName = entity.SortName;
  463. dto.ForcedSortName = entity.ForcedSortName;
  464. dto.RunTimeTicks = entity.RunTimeTicks;
  465. dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
  466. dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
  467. dto.IsInMixedFolder = entity.IsInMixedFolder;
  468. dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
  469. dto.CriticRating = entity.CriticRating;
  470. dto.PresentationUniqueKey = entity.PresentationUniqueKey;
  471. dto.OriginalTitle = entity.OriginalTitle;
  472. dto.Album = entity.Album;
  473. dto.LUFS = entity.LUFS;
  474. dto.NormalizationGain = entity.NormalizationGain;
  475. dto.IsVirtualItem = entity.IsVirtualItem;
  476. dto.ExternalSeriesId = entity.ExternalSeriesId;
  477. dto.Tagline = entity.Tagline;
  478. dto.TotalBitrate = entity.TotalBitrate;
  479. dto.ExternalId = entity.ExternalId;
  480. dto.Size = entity.Size;
  481. dto.Genres = entity.Genres?.Split('|') ?? [];
  482. dto.DateCreated = entity.DateCreated.GetValueOrDefault();
  483. dto.DateModified = entity.DateModified.GetValueOrDefault();
  484. dto.ChannelId = string.IsNullOrWhiteSpace(entity.ChannelId) ? Guid.Empty : (Guid.TryParse(entity.ChannelId, out var channelId) ? channelId : Guid.Empty);
  485. dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault();
  486. dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault();
  487. dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
  488. dto.Width = entity.Width.GetValueOrDefault();
  489. dto.Height = entity.Height.GetValueOrDefault();
  490. if (entity.Provider is not null)
  491. {
  492. dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
  493. }
  494. if (entity.ExtraType is not null)
  495. {
  496. dto.ExtraType = (ExtraType)entity.ExtraType;
  497. }
  498. if (entity.LockedFields is not null)
  499. {
  500. dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
  501. }
  502. if (entity.Audio is not null)
  503. {
  504. dto.Audio = (ProgramAudio)entity.Audio;
  505. }
  506. dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
  507. dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
  508. dto.Studios = entity.Studios?.Split('|') ?? [];
  509. dto.Tags = entity.Tags?.Split('|') ?? [];
  510. if (dto is IHasProgramAttributes hasProgramAttributes)
  511. {
  512. hasProgramAttributes.IsMovie = entity.IsMovie;
  513. hasProgramAttributes.IsSeries = entity.IsSeries;
  514. hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
  515. hasProgramAttributes.IsRepeat = entity.IsRepeat;
  516. }
  517. if (dto is LiveTvChannel liveTvChannel)
  518. {
  519. liveTvChannel.ServiceName = entity.ExternalServiceId;
  520. }
  521. if (dto is Trailer trailer)
  522. {
  523. trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
  524. }
  525. if (dto is Video video)
  526. {
  527. video.PrimaryVersionId = entity.PrimaryVersionId;
  528. }
  529. if (dto is IHasSeries hasSeriesName)
  530. {
  531. hasSeriesName.SeriesName = entity.SeriesName;
  532. hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
  533. hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
  534. }
  535. if (dto is Episode episode)
  536. {
  537. episode.SeasonName = entity.SeasonName;
  538. episode.SeasonId = entity.SeasonId.GetValueOrDefault();
  539. }
  540. if (dto is IHasArtist hasArtists)
  541. {
  542. hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
  543. }
  544. if (dto is IHasAlbumArtist hasAlbumArtists)
  545. {
  546. hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
  547. }
  548. if (dto is LiveTvProgram program)
  549. {
  550. program.ShowId = entity.ShowId;
  551. }
  552. if (entity.Images is not null)
  553. {
  554. dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
  555. }
  556. // dto.Type = entity.Type;
  557. // dto.Data = entity.Data;
  558. // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
  559. if (dto is IHasStartDate hasStartDate)
  560. {
  561. hasStartDate.StartDate = entity.StartDate;
  562. }
  563. // Fields that are present in the DB but are never actually used
  564. // dto.UnratedType = entity.UnratedType;
  565. // dto.TopParentId = entity.TopParentId;
  566. // dto.CleanName = entity.CleanName;
  567. // dto.UserDataKey = entity.UserDataKey;
  568. if (dto is Folder folder)
  569. {
  570. folder.DateLastMediaAdded = entity.DateLastMediaAdded;
  571. }
  572. return dto;
  573. }
  574. /// <summary>
  575. /// Maps a Entity to the DTO.
  576. /// </summary>
  577. /// <param name="dto">The entity.</param>
  578. /// <returns>The dto to map.</returns>
  579. public BaseItemEntity Map(BaseItemDto dto)
  580. {
  581. var dtoType = dto.GetType();
  582. var entity = new BaseItemEntity()
  583. {
  584. Type = dtoType.ToString(),
  585. Id = dto.Id
  586. };
  587. if (TypeRequiresDeserialization(dtoType))
  588. {
  589. entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
  590. }
  591. entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
  592. entity.Path = GetPathToSave(dto.Path);
  593. entity.EndDate = dto.EndDate.GetValueOrDefault();
  594. entity.CommunityRating = dto.CommunityRating;
  595. entity.CustomRating = dto.CustomRating;
  596. entity.IndexNumber = dto.IndexNumber;
  597. entity.IsLocked = dto.IsLocked;
  598. entity.Name = dto.Name;
  599. entity.OfficialRating = dto.OfficialRating;
  600. entity.Overview = dto.Overview;
  601. entity.ParentIndexNumber = dto.ParentIndexNumber;
  602. entity.PremiereDate = dto.PremiereDate;
  603. entity.ProductionYear = dto.ProductionYear;
  604. entity.SortName = dto.SortName;
  605. entity.ForcedSortName = dto.ForcedSortName;
  606. entity.RunTimeTicks = dto.RunTimeTicks;
  607. entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
  608. entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
  609. entity.IsInMixedFolder = dto.IsInMixedFolder;
  610. entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
  611. entity.CriticRating = dto.CriticRating;
  612. entity.PresentationUniqueKey = dto.PresentationUniqueKey;
  613. entity.OriginalTitle = dto.OriginalTitle;
  614. entity.Album = dto.Album;
  615. entity.LUFS = dto.LUFS;
  616. entity.NormalizationGain = dto.NormalizationGain;
  617. entity.IsVirtualItem = dto.IsVirtualItem;
  618. entity.ExternalSeriesId = dto.ExternalSeriesId;
  619. entity.Tagline = dto.Tagline;
  620. entity.TotalBitrate = dto.TotalBitrate;
  621. entity.ExternalId = dto.ExternalId;
  622. entity.Size = dto.Size;
  623. entity.Genres = string.Join('|', dto.Genres);
  624. entity.DateCreated = dto.DateCreated;
  625. entity.DateModified = dto.DateModified;
  626. entity.ChannelId = dto.ChannelId.ToString();
  627. entity.DateLastRefreshed = dto.DateLastRefreshed;
  628. entity.DateLastSaved = dto.DateLastSaved;
  629. entity.OwnerId = dto.OwnerId.ToString();
  630. entity.Width = dto.Width;
  631. entity.Height = dto.Height;
  632. entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
  633. {
  634. Item = entity,
  635. ProviderId = e.Key,
  636. ProviderValue = e.Value
  637. }).ToList();
  638. if (dto.Audio.HasValue)
  639. {
  640. entity.Audio = (ProgramAudioEntity)dto.Audio;
  641. }
  642. if (dto.ExtraType.HasValue)
  643. {
  644. entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
  645. }
  646. entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
  647. entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null;
  648. entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
  649. entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
  650. entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
  651. .Select(e => new BaseItemMetadataField()
  652. {
  653. Id = (int)e,
  654. Item = entity,
  655. ItemId = entity.Id
  656. })
  657. .ToArray() : null;
  658. if (dto is IHasProgramAttributes hasProgramAttributes)
  659. {
  660. entity.IsMovie = hasProgramAttributes.IsMovie;
  661. entity.IsSeries = hasProgramAttributes.IsSeries;
  662. entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
  663. entity.IsRepeat = hasProgramAttributes.IsRepeat;
  664. }
  665. if (dto is LiveTvChannel liveTvChannel)
  666. {
  667. entity.ExternalServiceId = liveTvChannel.ServiceName;
  668. }
  669. if (dto is Video video)
  670. {
  671. entity.PrimaryVersionId = video.PrimaryVersionId;
  672. }
  673. if (dto is IHasSeries hasSeriesName)
  674. {
  675. entity.SeriesName = hasSeriesName.SeriesName;
  676. entity.SeriesId = hasSeriesName.SeriesId;
  677. entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
  678. }
  679. if (dto is Episode episode)
  680. {
  681. entity.SeasonName = episode.SeasonName;
  682. entity.SeasonId = episode.SeasonId;
  683. }
  684. if (dto is IHasArtist hasArtists)
  685. {
  686. entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
  687. }
  688. if (dto is IHasAlbumArtist hasAlbumArtists)
  689. {
  690. entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null;
  691. }
  692. if (dto is LiveTvProgram program)
  693. {
  694. entity.ShowId = program.ShowId;
  695. }
  696. if (dto.ImageInfos is not null)
  697. {
  698. entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
  699. }
  700. if (dto is Trailer trailer)
  701. {
  702. entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
  703. {
  704. Id = (int)e,
  705. Item = entity,
  706. ItemId = entity.Id
  707. }).ToArray() ?? [];
  708. }
  709. // dto.Type = entity.Type;
  710. // dto.Data = entity.Data;
  711. entity.MediaType = dto.MediaType.ToString();
  712. if (dto is IHasStartDate hasStartDate)
  713. {
  714. entity.StartDate = hasStartDate.StartDate;
  715. }
  716. // Fields that are present in the DB but are never actually used
  717. // dto.UnratedType = entity.UnratedType;
  718. // dto.TopParentId = entity.TopParentId;
  719. // dto.CleanName = entity.CleanName;
  720. // dto.UserDataKey = entity.UserDataKey;
  721. if (dto is Folder folder)
  722. {
  723. entity.DateLastMediaAdded = folder.DateLastMediaAdded;
  724. entity.IsFolder = folder.IsFolder;
  725. }
  726. return entity;
  727. }
  728. private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
  729. {
  730. using var context = _dbProvider.CreateDbContext();
  731. var query = context.ItemValuesMap
  732. .AsNoTracking()
  733. .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
  734. if (withItemTypes.Count > 0)
  735. {
  736. query = query.Where(e => withItemTypes.Contains(e.Item.Type));
  737. }
  738. if (excludeItemTypes.Count > 0)
  739. {
  740. query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
  741. }
  742. // query = query.DistinctBy(e => e.CleanValue);
  743. return query.Select(e => e.ItemValue.CleanValue).ToArray();
  744. }
  745. private static bool TypeRequiresDeserialization(Type type)
  746. {
  747. return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
  748. }
  749. private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
  750. {
  751. ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
  752. if (_serverConfigurationManager?.Configuration is null)
  753. {
  754. throw new InvalidOperationException("Server Configuration manager or configuration is null");
  755. }
  756. var typeToSerialise = GetType(baseItemEntity.Type);
  757. return BaseItemRepository.DeserialiseBaseItem(
  758. baseItemEntity,
  759. _logger,
  760. _appHost,
  761. skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeToSerialise == typeof(Channel) || typeToSerialise == typeof(UserRootFolder))));
  762. }
  763. /// <summary>
  764. /// Deserialises a BaseItemEntity and sets all properties.
  765. /// </summary>
  766. /// <param name="baseItemEntity">The DB entity.</param>
  767. /// <param name="logger">Logger.</param>
  768. /// <param name="appHost">The application server Host.</param>
  769. /// <param name="skipDeserialization">If only mapping should be processed.</param>
  770. /// <returns>A mapped BaseItem.</returns>
  771. /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
  772. public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
  773. {
  774. var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type.");
  775. BaseItemDto? dto = null;
  776. if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
  777. {
  778. try
  779. {
  780. dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
  781. }
  782. catch (JsonException ex)
  783. {
  784. logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
  785. }
  786. }
  787. if (dto is null)
  788. {
  789. dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type.");
  790. }
  791. return Map(baseItemEntity, dto, appHost);
  792. }
  793. private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList<ItemValueType> itemValueTypes, string returnType)
  794. {
  795. ArgumentNullException.ThrowIfNull(filter);
  796. if (!filter.Limit.HasValue)
  797. {
  798. filter.EnableTotalRecordCount = false;
  799. }
  800. using var context = _dbProvider.CreateDbContext();
  801. var innerQuery = new InternalItemsQuery(filter.User)
  802. {
  803. ExcludeItemTypes = filter.ExcludeItemTypes,
  804. IncludeItemTypes = filter.IncludeItemTypes,
  805. MediaTypes = filter.MediaTypes,
  806. AncestorIds = filter.AncestorIds,
  807. ItemIds = filter.ItemIds,
  808. TopParentIds = filter.TopParentIds,
  809. ParentId = filter.ParentId,
  810. IsAiring = filter.IsAiring,
  811. IsMovie = filter.IsMovie,
  812. IsSports = filter.IsSports,
  813. IsKids = filter.IsKids,
  814. IsNews = filter.IsNews,
  815. IsSeries = filter.IsSeries
  816. };
  817. var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, innerQuery);
  818. query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type)));
  819. if (filter.OrderBy.Count != 0
  820. || !string.IsNullOrEmpty(filter.SearchTerm))
  821. {
  822. query = ApplyOrder(query, filter);
  823. }
  824. else
  825. {
  826. query = query.OrderBy(e => e.SortName);
  827. }
  828. if (filter.Limit.HasValue || filter.StartIndex.HasValue)
  829. {
  830. var offset = filter.StartIndex ?? 0;
  831. if (offset > 0)
  832. {
  833. query = query.Skip(offset);
  834. }
  835. if (filter.Limit.HasValue)
  836. {
  837. query = query.Take(filter.Limit.Value);
  838. }
  839. }
  840. var result = new QueryResult<(BaseItemDto, ItemCounts)>();
  841. if (filter.EnableTotalRecordCount)
  842. {
  843. result.TotalRecordCount = query.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()).Count();
  844. }
  845. var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
  846. var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
  847. var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
  848. var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
  849. var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
  850. var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
  851. var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
  852. var resultQuery = query.Select(e => new
  853. {
  854. item = e,
  855. // TODO: This is bad refactor!
  856. itemCount = new ItemCounts()
  857. {
  858. SeriesCount = e.ItemValues!.Count(f => f.Item.Type == seriesTypeName),
  859. EpisodeCount = e.ItemValues!.Count(f => f.Item.Type == episodeTypeName),
  860. MovieCount = e.ItemValues!.Count(f => f.Item.Type == movieTypeName),
  861. AlbumCount = e.ItemValues!.Count(f => f.Item.Type == musicAlbumTypeName),
  862. ArtistCount = e.ItemValues!.Count(f => f.Item.Type == musicArtistTypeName),
  863. SongCount = e.ItemValues!.Count(f => f.Item.Type == audioTypeName),
  864. TrailerCount = e.ItemValues!.Count(f => f.Item.Type == trailerTypeName),
  865. }
  866. });
  867. result.StartIndex = filter.StartIndex ?? 0;
  868. result.Items = resultQuery.ToArray().Where(e => e is not null).Select(e =>
  869. {
  870. return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
  871. }).ToArray();
  872. return result;
  873. }
  874. private static void PrepareFilterQuery(InternalItemsQuery query)
  875. {
  876. if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
  877. {
  878. query.Limit = query.Limit.Value + 4;
  879. }
  880. if (query.IsResumable ?? false)
  881. {
  882. query.IsVirtualItem = false;
  883. }
  884. }
  885. private string GetCleanValue(string value)
  886. {
  887. if (string.IsNullOrWhiteSpace(value))
  888. {
  889. return value;
  890. }
  891. return value.RemoveDiacritics().ToLowerInvariant();
  892. }
  893. private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
  894. {
  895. var list = new List<(int, string)>();
  896. if (item is IHasArtist hasArtist)
  897. {
  898. list.AddRange(hasArtist.Artists.Select(i => (0, i)));
  899. }
  900. if (item is IHasAlbumArtist hasAlbumArtist)
  901. {
  902. list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i)));
  903. }
  904. list.AddRange(item.Genres.Select(i => (2, i)));
  905. list.AddRange(item.Studios.Select(i => (3, i)));
  906. list.AddRange(item.Tags.Select(i => (4, i)));
  907. // keywords was 5
  908. list.AddRange(inheritedTags.Select(i => (6, i)));
  909. // Remove all invalid values.
  910. list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
  911. return list;
  912. }
  913. private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
  914. {
  915. return new BaseItemImageInfo()
  916. {
  917. ItemId = baseItemId,
  918. Id = Guid.NewGuid(),
  919. Path = e.Path,
  920. Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
  921. DateModified = e.DateModified,
  922. Height = e.Height,
  923. Width = e.Width,
  924. ImageType = (ImageInfoImageType)e.Type,
  925. Item = null!
  926. };
  927. }
  928. private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
  929. {
  930. return new ItemImageInfo()
  931. {
  932. Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
  933. BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
  934. DateModified = e.DateModified,
  935. Height = e.Height,
  936. Width = e.Width,
  937. Type = (ImageType)e.ImageType
  938. };
  939. }
  940. private string? GetPathToSave(string path)
  941. {
  942. if (path is null)
  943. {
  944. return null;
  945. }
  946. return _appHost.ReverseVirtualPath(path);
  947. }
  948. private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
  949. {
  950. var list = new List<string>();
  951. if (IsTypeInQuery(BaseItemKind.Person, query))
  952. {
  953. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
  954. }
  955. if (IsTypeInQuery(BaseItemKind.Genre, query))
  956. {
  957. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
  958. }
  959. if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
  960. {
  961. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
  962. }
  963. if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
  964. {
  965. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
  966. }
  967. if (IsTypeInQuery(BaseItemKind.Studio, query))
  968. {
  969. list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
  970. }
  971. return list;
  972. }
  973. private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
  974. {
  975. if (query.ExcludeItemTypes.Contains(type))
  976. {
  977. return false;
  978. }
  979. return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
  980. }
  981. private Expression<Func<BaseItemEntity, object>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
  982. {
  983. #pragma warning disable CS8603 // Possible null reference return.
  984. return sortBy switch
  985. {
  986. ItemSortBy.AirTime => e => e.SortName, // TODO
  987. ItemSortBy.Runtime => e => e.RunTimeTicks,
  988. ItemSortBy.Random => e => EF.Functions.Random(),
  989. ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.LastPlayedDate,
  990. ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.PlayCount,
  991. ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.IsFavorite,
  992. ItemSortBy.IsFolder => e => e.IsFolder,
  993. ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
  994. ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
  995. ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded,
  996. ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
  997. ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
  998. ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
  999. ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue,
  1000. // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
  1001. ItemSortBy.SeriesSortName => e => e.SeriesName,
  1002. // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
  1003. ItemSortBy.Album => e => e.Album,
  1004. ItemSortBy.DateCreated => e => e.DateCreated,
  1005. ItemSortBy.PremiereDate => e => e.PremiereDate,
  1006. ItemSortBy.StartDate => e => e.StartDate,
  1007. ItemSortBy.Name => e => e.Name,
  1008. ItemSortBy.CommunityRating => e => e.CommunityRating,
  1009. ItemSortBy.ProductionYear => e => e.ProductionYear,
  1010. ItemSortBy.CriticRating => e => e.CriticRating,
  1011. ItemSortBy.VideoBitRate => e => e.TotalBitrate,
  1012. ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber,
  1013. ItemSortBy.IndexNumber => e => e.IndexNumber,
  1014. _ => e => e.SortName
  1015. };
  1016. #pragma warning restore CS8603 // Possible null reference return.
  1017. }
  1018. private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
  1019. {
  1020. if (!query.GroupByPresentationUniqueKey)
  1021. {
  1022. return false;
  1023. }
  1024. if (query.GroupBySeriesPresentationUniqueKey)
  1025. {
  1026. return false;
  1027. }
  1028. if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
  1029. {
  1030. return false;
  1031. }
  1032. if (query.User is null)
  1033. {
  1034. return false;
  1035. }
  1036. if (query.IncludeItemTypes.Length == 0)
  1037. {
  1038. return true;
  1039. }
  1040. return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
  1041. || query.IncludeItemTypes.Contains(BaseItemKind.Video)
  1042. || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
  1043. || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
  1044. || query.IncludeItemTypes.Contains(BaseItemKind.Series)
  1045. || query.IncludeItemTypes.Contains(BaseItemKind.Season);
  1046. }
  1047. private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter)
  1048. {
  1049. var orderBy = filter.OrderBy;
  1050. var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
  1051. if (hasSearch)
  1052. {
  1053. orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
  1054. }
  1055. else if (orderBy.Count == 0)
  1056. {
  1057. return query;
  1058. }
  1059. IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
  1060. var firstOrdering = orderBy.FirstOrDefault();
  1061. if (firstOrdering != default)
  1062. {
  1063. var expression = MapOrderByField(firstOrdering.OrderBy, filter);
  1064. if (firstOrdering.SortOrder == SortOrder.Ascending)
  1065. {
  1066. orderedQuery = query.OrderBy(expression);
  1067. }
  1068. else
  1069. {
  1070. orderedQuery = query.OrderByDescending(expression);
  1071. }
  1072. if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
  1073. {
  1074. if (firstOrdering.SortOrder is SortOrder.Ascending)
  1075. {
  1076. orderedQuery = orderedQuery.ThenBy(e => e.Name);
  1077. }
  1078. else
  1079. {
  1080. orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
  1081. }
  1082. }
  1083. }
  1084. foreach (var item in orderBy.Skip(1))
  1085. {
  1086. var expression = MapOrderByField(item.OrderBy, filter);
  1087. if (item.SortOrder == SortOrder.Ascending)
  1088. {
  1089. orderedQuery = orderedQuery!.ThenBy(expression);
  1090. }
  1091. else
  1092. {
  1093. orderedQuery = orderedQuery!.ThenByDescending(expression);
  1094. }
  1095. }
  1096. return orderedQuery ?? query;
  1097. }
  1098. private IQueryable<BaseItemEntity> TranslateQuery(
  1099. IQueryable<BaseItemEntity> baseQuery,
  1100. JellyfinDbContext context,
  1101. InternalItemsQuery filter)
  1102. {
  1103. var minWidth = filter.MinWidth;
  1104. var maxWidth = filter.MaxWidth;
  1105. var now = DateTime.UtcNow;
  1106. if (filter.IsHD.HasValue)
  1107. {
  1108. const int Threshold = 1200;
  1109. if (filter.IsHD.Value)
  1110. {
  1111. minWidth = Threshold;
  1112. }
  1113. else
  1114. {
  1115. maxWidth = Threshold - 1;
  1116. }
  1117. }
  1118. if (filter.Is4K.HasValue)
  1119. {
  1120. const int Threshold = 3800;
  1121. if (filter.Is4K.Value)
  1122. {
  1123. minWidth = Threshold;
  1124. }
  1125. else
  1126. {
  1127. maxWidth = Threshold - 1;
  1128. }
  1129. }
  1130. if (minWidth.HasValue)
  1131. {
  1132. baseQuery = baseQuery.Where(e => e.Width >= minWidth);
  1133. }
  1134. if (filter.MinHeight.HasValue)
  1135. {
  1136. baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
  1137. }
  1138. if (maxWidth.HasValue)
  1139. {
  1140. baseQuery = baseQuery.Where(e => e.Width >= maxWidth);
  1141. }
  1142. if (filter.MaxHeight.HasValue)
  1143. {
  1144. baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
  1145. }
  1146. if (filter.IsLocked.HasValue)
  1147. {
  1148. baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
  1149. }
  1150. var tags = filter.Tags.ToList();
  1151. var excludeTags = filter.ExcludeTags.ToList();
  1152. if (filter.IsMovie == true)
  1153. {
  1154. if (filter.IncludeItemTypes.Length == 0
  1155. || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
  1156. || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
  1157. {
  1158. baseQuery = baseQuery.Where(e => e.IsMovie);
  1159. }
  1160. }
  1161. else if (filter.IsMovie.HasValue)
  1162. {
  1163. baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
  1164. }
  1165. if (filter.IsSeries.HasValue)
  1166. {
  1167. baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
  1168. }
  1169. if (filter.IsSports.HasValue)
  1170. {
  1171. if (filter.IsSports.Value)
  1172. {
  1173. tags.Add("Sports");
  1174. }
  1175. else
  1176. {
  1177. excludeTags.Add("Sports");
  1178. }
  1179. }
  1180. if (filter.IsNews.HasValue)
  1181. {
  1182. if (filter.IsNews.Value)
  1183. {
  1184. tags.Add("News");
  1185. }
  1186. else
  1187. {
  1188. excludeTags.Add("News");
  1189. }
  1190. }
  1191. if (filter.IsKids.HasValue)
  1192. {
  1193. if (filter.IsKids.Value)
  1194. {
  1195. tags.Add("Kids");
  1196. }
  1197. else
  1198. {
  1199. excludeTags.Add("Kids");
  1200. }
  1201. }
  1202. if (!string.IsNullOrEmpty(filter.SearchTerm))
  1203. {
  1204. var searchTerm = filter.SearchTerm.ToLower();
  1205. baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm)));
  1206. }
  1207. if (filter.IsFolder.HasValue)
  1208. {
  1209. baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
  1210. }
  1211. var includeTypes = filter.IncludeItemTypes;
  1212. // Only specify excluded types if no included types are specified
  1213. if (filter.IncludeItemTypes.Length == 0)
  1214. {
  1215. var excludeTypes = filter.ExcludeItemTypes;
  1216. if (excludeTypes.Length == 1)
  1217. {
  1218. if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
  1219. {
  1220. baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
  1221. }
  1222. }
  1223. else if (excludeTypes.Length > 1)
  1224. {
  1225. var excludeTypeName = new List<string>();
  1226. foreach (var excludeType in excludeTypes)
  1227. {
  1228. if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
  1229. {
  1230. excludeTypeName.Add(baseItemKindName!);
  1231. }
  1232. }
  1233. baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
  1234. }
  1235. }
  1236. else if (includeTypes.Length == 1)
  1237. {
  1238. if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
  1239. {
  1240. baseQuery = baseQuery.Where(e => e.Type == includeTypeName);
  1241. }
  1242. }
  1243. else if (includeTypes.Length > 1)
  1244. {
  1245. var includeTypeName = new List<string>();
  1246. foreach (var includeType in includeTypes)
  1247. {
  1248. if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeType, out var baseItemKindName))
  1249. {
  1250. includeTypeName.Add(baseItemKindName!);
  1251. }
  1252. }
  1253. baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type));
  1254. }
  1255. if (filter.ChannelIds.Count > 0)
  1256. {
  1257. var channelIds = filter.ChannelIds.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).ToArray();
  1258. baseQuery = baseQuery.Where(e => channelIds.Contains(e.ChannelId));
  1259. }
  1260. if (!filter.ParentId.IsEmpty())
  1261. {
  1262. baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
  1263. }
  1264. if (!string.IsNullOrWhiteSpace(filter.Path))
  1265. {
  1266. baseQuery = baseQuery.Where(e => e.Path == filter.Path);
  1267. }
  1268. if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
  1269. {
  1270. baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
  1271. }
  1272. if (filter.MinCommunityRating.HasValue)
  1273. {
  1274. baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
  1275. }
  1276. if (filter.MinIndexNumber.HasValue)
  1277. {
  1278. baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
  1279. }
  1280. if (filter.MinParentAndIndexNumber.HasValue)
  1281. {
  1282. baseQuery = baseQuery
  1283. .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= filter.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > filter.MinParentAndIndexNumber.Value.ParentIndexNumber);
  1284. }
  1285. if (filter.MinDateCreated.HasValue)
  1286. {
  1287. baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
  1288. }
  1289. if (filter.MinDateLastSaved.HasValue)
  1290. {
  1291. baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value);
  1292. }
  1293. if (filter.MinDateLastSavedForUser.HasValue)
  1294. {
  1295. baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUser.Value);
  1296. }
  1297. if (filter.IndexNumber.HasValue)
  1298. {
  1299. baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
  1300. }
  1301. if (filter.ParentIndexNumber.HasValue)
  1302. {
  1303. baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
  1304. }
  1305. if (filter.ParentIndexNumberNotEquals.HasValue)
  1306. {
  1307. baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null);
  1308. }
  1309. var minEndDate = filter.MinEndDate;
  1310. var maxEndDate = filter.MaxEndDate;
  1311. if (filter.HasAired.HasValue)
  1312. {
  1313. if (filter.HasAired.Value)
  1314. {
  1315. maxEndDate = DateTime.UtcNow;
  1316. }
  1317. else
  1318. {
  1319. minEndDate = DateTime.UtcNow;
  1320. }
  1321. }
  1322. if (minEndDate.HasValue)
  1323. {
  1324. baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
  1325. }
  1326. if (maxEndDate.HasValue)
  1327. {
  1328. baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
  1329. }
  1330. if (filter.MinStartDate.HasValue)
  1331. {
  1332. baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
  1333. }
  1334. if (filter.MaxStartDate.HasValue)
  1335. {
  1336. baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
  1337. }
  1338. if (filter.MinPremiereDate.HasValue)
  1339. {
  1340. baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MinPremiereDate.Value);
  1341. }
  1342. if (filter.MaxPremiereDate.HasValue)
  1343. {
  1344. baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
  1345. }
  1346. if (filter.TrailerTypes.Length > 0)
  1347. {
  1348. var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
  1349. baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
  1350. }
  1351. if (filter.IsAiring.HasValue)
  1352. {
  1353. if (filter.IsAiring.Value)
  1354. {
  1355. baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
  1356. }
  1357. else
  1358. {
  1359. baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
  1360. }
  1361. }
  1362. if (filter.PersonIds.Length > 0)
  1363. {
  1364. baseQuery = baseQuery
  1365. .Where(e =>
  1366. context.PeopleBaseItemMap.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).Any(f => f.Name == w.People.Name))
  1367. .Any(f => f.ItemId == e.Id));
  1368. }
  1369. if (!string.IsNullOrWhiteSpace(filter.Person))
  1370. {
  1371. baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
  1372. }
  1373. if (!string.IsNullOrWhiteSpace(filter.MinSortName))
  1374. {
  1375. // this does not makes sense.
  1376. // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
  1377. // whereClauses.Add("SortName>=@MinSortName");
  1378. // statement?.TryBind("@MinSortName", query.MinSortName);
  1379. }
  1380. if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
  1381. {
  1382. baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
  1383. }
  1384. if (!string.IsNullOrWhiteSpace(filter.ExternalId))
  1385. {
  1386. baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
  1387. }
  1388. if (!string.IsNullOrWhiteSpace(filter.Name))
  1389. {
  1390. var cleanName = GetCleanValue(filter.Name);
  1391. baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
  1392. }
  1393. // These are the same, for now
  1394. var nameContains = filter.NameContains;
  1395. if (!string.IsNullOrWhiteSpace(nameContains))
  1396. {
  1397. baseQuery = baseQuery.Where(e =>
  1398. e.CleanName!.Contains(nameContains)
  1399. || e.OriginalTitle!.ToLower().Contains(nameContains!));
  1400. }
  1401. if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
  1402. {
  1403. baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith) || e.Name!.StartsWith(filter.NameStartsWith));
  1404. }
  1405. if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
  1406. {
  1407. // i hate this
  1408. baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!.FirstOrDefault() > filter.NameStartsWithOrGreater[0]);
  1409. }
  1410. if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
  1411. {
  1412. // i hate this
  1413. baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDefault() < filter.NameLessThan[0]);
  1414. }
  1415. if (filter.ImageTypes.Length > 0)
  1416. {
  1417. var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
  1418. baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
  1419. }
  1420. if (filter.IsLiked.HasValue)
  1421. {
  1422. baseQuery = baseQuery
  1423. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLikeValue);
  1424. }
  1425. if (filter.IsFavoriteOrLiked.HasValue)
  1426. {
  1427. baseQuery = baseQuery
  1428. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavoriteOrLiked);
  1429. }
  1430. if (filter.IsFavorite.HasValue)
  1431. {
  1432. baseQuery = baseQuery
  1433. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorite);
  1434. }
  1435. if (filter.IsPlayed.HasValue)
  1436. {
  1437. // We should probably figure this out for all folders, but for right now, this is the only place where we need it
  1438. if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
  1439. {
  1440. baseQuery = baseQuery.Where(e => context.BaseItems
  1441. .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
  1442. .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
  1443. .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
  1444. }
  1445. else
  1446. {
  1447. baseQuery = baseQuery
  1448. .Select(e => new
  1449. {
  1450. IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).FirstOrDefault() ?? false,
  1451. Item = e
  1452. })
  1453. .Where(e => e.IsPlayed == filter.IsPlayed)
  1454. .Select(f => f.Item);
  1455. }
  1456. }
  1457. if (filter.IsResumable.HasValue)
  1458. {
  1459. if (filter.IsResumable.Value)
  1460. {
  1461. baseQuery = baseQuery
  1462. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks > 0);
  1463. }
  1464. else
  1465. {
  1466. baseQuery = baseQuery
  1467. .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks == 0);
  1468. }
  1469. }
  1470. if (filter.ArtistIds.Length > 0)
  1471. {
  1472. baseQuery = baseQuery
  1473. .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type <= ItemValueType.Artist && filter.ArtistIds.Contains(f.ItemId)));
  1474. }
  1475. if (filter.AlbumArtistIds.Length > 0)
  1476. {
  1477. baseQuery = baseQuery
  1478. .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.AlbumArtistIds.Contains(f.ItemId)));
  1479. }
  1480. if (filter.ContributingArtistIds.Length > 0)
  1481. {
  1482. baseQuery = baseQuery
  1483. .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ContributingArtistIds.Contains(f.ItemId)));
  1484. }
  1485. if (filter.AlbumIds.Length > 0)
  1486. {
  1487. baseQuery = baseQuery.Where(e => context.BaseItems.Where(f => filter.AlbumIds.Contains(f.Id)).Any(f => f.Name == e.Album));
  1488. }
  1489. if (filter.ExcludeArtistIds.Length > 0)
  1490. {
  1491. baseQuery = baseQuery
  1492. .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ExcludeArtistIds.Contains(f.ItemId)));
  1493. }
  1494. if (filter.GenreIds.Count > 0)
  1495. {
  1496. baseQuery = baseQuery
  1497. .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && filter.GenreIds.Contains(f.ItemId)));
  1498. }
  1499. if (filter.Genres.Count > 0)
  1500. {
  1501. var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray();
  1502. baseQuery = baseQuery
  1503. .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && cleanGenres.Contains(f.ItemValue.CleanValue)));
  1504. }
  1505. if (tags.Count > 0)
  1506. {
  1507. var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray();
  1508. baseQuery = baseQuery
  1509. .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue)));
  1510. }
  1511. if (excludeTags.Count > 0)
  1512. {
  1513. var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray();
  1514. baseQuery = baseQuery
  1515. .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue)));
  1516. }
  1517. if (filter.StudioIds.Length > 0)
  1518. {
  1519. baseQuery = baseQuery
  1520. .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Studios && filter.StudioIds.Contains(f.ItemId)));
  1521. }
  1522. if (filter.OfficialRatings.Length > 0)
  1523. {
  1524. baseQuery = baseQuery
  1525. .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
  1526. }
  1527. if (filter.HasParentalRating ?? false)
  1528. {
  1529. if (filter.MinParentalRating.HasValue)
  1530. {
  1531. baseQuery = baseQuery
  1532. .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
  1533. }
  1534. if (filter.MaxParentalRating.HasValue)
  1535. {
  1536. baseQuery = baseQuery
  1537. .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value);
  1538. }
  1539. }
  1540. else if (filter.BlockUnratedItems.Length > 0)
  1541. {
  1542. var unratedItems = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
  1543. if (filter.MinParentalRating.HasValue)
  1544. {
  1545. if (filter.MaxParentalRating.HasValue)
  1546. {
  1547. baseQuery = baseQuery
  1548. .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
  1549. || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating));
  1550. }
  1551. else
  1552. {
  1553. baseQuery = baseQuery
  1554. .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
  1555. || e.InheritedParentalRatingValue >= filter.MinParentalRating);
  1556. }
  1557. }
  1558. else
  1559. {
  1560. baseQuery = baseQuery
  1561. .Where(e => e.InheritedParentalRatingValue != null && !unratedItems.Contains(e.UnratedType));
  1562. }
  1563. }
  1564. else if (filter.MinParentalRating.HasValue)
  1565. {
  1566. if (filter.MaxParentalRating.HasValue)
  1567. {
  1568. baseQuery = baseQuery
  1569. .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value);
  1570. }
  1571. else
  1572. {
  1573. baseQuery = baseQuery
  1574. .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
  1575. }
  1576. }
  1577. else if (filter.MaxParentalRating.HasValue)
  1578. {
  1579. baseQuery = baseQuery
  1580. .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value);
  1581. }
  1582. else if (!filter.HasParentalRating ?? false)
  1583. {
  1584. baseQuery = baseQuery
  1585. .Where(e => e.InheritedParentalRatingValue == null);
  1586. }
  1587. if (filter.HasOfficialRating.HasValue)
  1588. {
  1589. if (filter.HasOfficialRating.Value)
  1590. {
  1591. baseQuery = baseQuery
  1592. .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
  1593. }
  1594. else
  1595. {
  1596. baseQuery = baseQuery
  1597. .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
  1598. }
  1599. }
  1600. if (filter.HasOverview.HasValue)
  1601. {
  1602. if (filter.HasOverview.Value)
  1603. {
  1604. baseQuery = baseQuery
  1605. .Where(e => e.Overview != null && e.Overview != string.Empty);
  1606. }
  1607. else
  1608. {
  1609. baseQuery = baseQuery
  1610. .Where(e => e.Overview == null || e.Overview == string.Empty);
  1611. }
  1612. }
  1613. if (filter.HasOwnerId.HasValue)
  1614. {
  1615. if (filter.HasOwnerId.Value)
  1616. {
  1617. baseQuery = baseQuery
  1618. .Where(e => e.OwnerId != null);
  1619. }
  1620. else
  1621. {
  1622. baseQuery = baseQuery
  1623. .Where(e => e.OwnerId == null);
  1624. }
  1625. }
  1626. if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
  1627. {
  1628. baseQuery = baseQuery
  1629. .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filter.HasNoAudioTrackWithLanguage));
  1630. }
  1631. if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
  1632. {
  1633. baseQuery = baseQuery
  1634. .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal && f.Language == filter.HasNoInternalSubtitleTrackWithLanguage));
  1635. }
  1636. if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
  1637. {
  1638. baseQuery = baseQuery
  1639. .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && f.Language == filter.HasNoExternalSubtitleTrackWithLanguage));
  1640. }
  1641. if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
  1642. {
  1643. baseQuery = baseQuery
  1644. .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == filter.HasNoSubtitleTrackWithLanguage));
  1645. }
  1646. if (filter.HasSubtitles.HasValue)
  1647. {
  1648. baseQuery = baseQuery
  1649. .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtitles.Value);
  1650. }
  1651. if (filter.HasChapterImages.HasValue)
  1652. {
  1653. baseQuery = baseQuery
  1654. .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
  1655. }
  1656. if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
  1657. {
  1658. baseQuery = baseQuery
  1659. .Where(e => e.ParentId.HasValue && !context.BaseItems.Any(f => f.Id == e.ParentId.Value));
  1660. }
  1661. if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
  1662. {
  1663. baseQuery = baseQuery
  1664. .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Artist || f.ItemValue.Type == ItemValueType.AlbumArtist) == 1);
  1665. }
  1666. if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
  1667. {
  1668. baseQuery = baseQuery
  1669. .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Studios) == 1);
  1670. }
  1671. if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
  1672. {
  1673. baseQuery = baseQuery
  1674. .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
  1675. }
  1676. if (filter.Years.Length == 1)
  1677. {
  1678. baseQuery = baseQuery
  1679. .Where(e => e.ProductionYear == filter.Years[0]);
  1680. }
  1681. else if (filter.Years.Length > 1)
  1682. {
  1683. baseQuery = baseQuery
  1684. .Where(e => filter.Years.Any(f => f == e.ProductionYear));
  1685. }
  1686. var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
  1687. if (isVirtualItem.HasValue)
  1688. {
  1689. baseQuery = baseQuery
  1690. .Where(e => e.IsVirtualItem == isVirtualItem.Value);
  1691. }
  1692. if (filter.IsSpecialSeason.HasValue)
  1693. {
  1694. if (filter.IsSpecialSeason.Value)
  1695. {
  1696. baseQuery = baseQuery
  1697. .Where(e => e.IndexNumber == 0);
  1698. }
  1699. else
  1700. {
  1701. baseQuery = baseQuery
  1702. .Where(e => e.IndexNumber != 0);
  1703. }
  1704. }
  1705. if (filter.IsUnaired.HasValue)
  1706. {
  1707. if (filter.IsUnaired.Value)
  1708. {
  1709. baseQuery = baseQuery
  1710. .Where(e => e.PremiereDate >= now);
  1711. }
  1712. else
  1713. {
  1714. baseQuery = baseQuery
  1715. .Where(e => e.PremiereDate < now);
  1716. }
  1717. }
  1718. if (filter.MediaTypes.Length > 0)
  1719. {
  1720. var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
  1721. baseQuery = baseQuery
  1722. .Where(e => mediaTypes.Contains(e.MediaType));
  1723. }
  1724. if (filter.ItemIds.Length > 0)
  1725. {
  1726. baseQuery = baseQuery
  1727. .Where(e => filter.ItemIds.Contains(e.Id));
  1728. }
  1729. if (filter.ExcludeItemIds.Length > 0)
  1730. {
  1731. baseQuery = baseQuery
  1732. .Where(e => !filter.ItemIds.Contains(e.Id));
  1733. }
  1734. if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
  1735. {
  1736. baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
  1737. }
  1738. if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
  1739. {
  1740. baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
  1741. }
  1742. if (filter.HasImdbId.HasValue)
  1743. {
  1744. baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
  1745. }
  1746. if (filter.HasTmdbId.HasValue)
  1747. {
  1748. baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
  1749. }
  1750. if (filter.HasTvdbId.HasValue)
  1751. {
  1752. baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
  1753. }
  1754. var queryTopParentIds = filter.TopParentIds;
  1755. if (queryTopParentIds.Length > 0)
  1756. {
  1757. var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
  1758. var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
  1759. if (enableItemsByName && includedItemByNameTypes.Count > 0)
  1760. {
  1761. baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w == e.TopParentId!.Value));
  1762. }
  1763. else
  1764. {
  1765. baseQuery = baseQuery.Where(e => queryTopParentIds.Contains(e.TopParentId!.Value));
  1766. }
  1767. }
  1768. if (filter.AncestorIds.Length > 0)
  1769. {
  1770. baseQuery = baseQuery.Where(e => e.Children!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
  1771. }
  1772. if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
  1773. {
  1774. baseQuery = baseQuery
  1775. .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.ParentAncestors!.Any(w => w.ItemId == f.Id)));
  1776. }
  1777. if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
  1778. {
  1779. baseQuery = baseQuery
  1780. .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
  1781. }
  1782. if (filter.ExcludeInheritedTags.Length > 0)
  1783. {
  1784. baseQuery = baseQuery
  1785. .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
  1786. .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
  1787. }
  1788. if (filter.IncludeInheritedTags.Length > 0)
  1789. {
  1790. // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
  1791. // In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
  1792. if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
  1793. {
  1794. baseQuery = baseQuery
  1795. .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags)
  1796. .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
  1797. ||
  1798. (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
  1799. .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
  1800. }
  1801. // A playlist should be accessible to its owner regardless of allowed tags.
  1802. else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
  1803. {
  1804. baseQuery = baseQuery
  1805. .Where(e =>
  1806. e.ParentAncestors!
  1807. .Any(f =>
  1808. f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))
  1809. || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
  1810. // d ^^ this is stupid it hate this.
  1811. }
  1812. else
  1813. {
  1814. baseQuery = baseQuery
  1815. .Where(e => e.ParentAncestors!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
  1816. }
  1817. }
  1818. if (filter.SeriesStatuses.Length > 0)
  1819. {
  1820. var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
  1821. baseQuery = baseQuery
  1822. .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
  1823. }
  1824. if (filter.BoxSetLibraryFolders.Length > 0)
  1825. {
  1826. var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).ToArray();
  1827. baseQuery = baseQuery
  1828. .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
  1829. }
  1830. if (filter.VideoTypes.Length > 0)
  1831. {
  1832. var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
  1833. baseQuery = baseQuery
  1834. .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
  1835. }
  1836. if (filter.Is3D.HasValue)
  1837. {
  1838. if (filter.Is3D.Value)
  1839. {
  1840. baseQuery = baseQuery
  1841. .Where(e => e.Data!.Contains("Video3DFormat"));
  1842. }
  1843. else
  1844. {
  1845. baseQuery = baseQuery
  1846. .Where(e => !e.Data!.Contains("Video3DFormat"));
  1847. }
  1848. }
  1849. if (filter.IsPlaceHolder.HasValue)
  1850. {
  1851. if (filter.IsPlaceHolder.Value)
  1852. {
  1853. baseQuery = baseQuery
  1854. .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
  1855. }
  1856. else
  1857. {
  1858. baseQuery = baseQuery
  1859. .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
  1860. }
  1861. }
  1862. if (filter.HasSpecialFeature.HasValue)
  1863. {
  1864. if (filter.HasSpecialFeature.Value)
  1865. {
  1866. baseQuery = baseQuery
  1867. .Where(e => e.ExtraIds != null);
  1868. }
  1869. else
  1870. {
  1871. baseQuery = baseQuery
  1872. .Where(e => e.ExtraIds == null);
  1873. }
  1874. }
  1875. if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
  1876. {
  1877. if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo.GetValueOrDefault())
  1878. {
  1879. baseQuery = baseQuery
  1880. .Where(e => e.ExtraIds != null);
  1881. }
  1882. else
  1883. {
  1884. baseQuery = baseQuery
  1885. .Where(e => e.ExtraIds == null);
  1886. }
  1887. }
  1888. return baseQuery;
  1889. }
  1890. }