MetadataService.cs 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304
  1. #nullable disable
  2. #pragma warning disable CS1591
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Net.Http;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using Jellyfin.Extensions;
  10. using MediaBrowser.Controller.Configuration;
  11. using MediaBrowser.Controller.Entities;
  12. using MediaBrowser.Controller.Entities.Audio;
  13. using MediaBrowser.Controller.IO;
  14. using MediaBrowser.Controller.Library;
  15. using MediaBrowser.Controller.Persistence;
  16. using MediaBrowser.Controller.Providers;
  17. using MediaBrowser.Model.Configuration;
  18. using MediaBrowser.Model.Entities;
  19. using MediaBrowser.Model.IO;
  20. using MediaBrowser.Model.Providers;
  21. using Microsoft.Extensions.Logging;
  22. namespace MediaBrowser.Providers.Manager
  23. {
  24. public abstract class MetadataService<TItemType, TIdType> : IMetadataService
  25. where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
  26. where TIdType : ItemLookupInfo, new()
  27. {
  28. protected MetadataService(
  29. IServerConfigurationManager serverConfigurationManager,
  30. ILogger<MetadataService<TItemType, TIdType>> logger,
  31. IProviderManager providerManager,
  32. IFileSystem fileSystem,
  33. ILibraryManager libraryManager,
  34. IExternalDataManager externalDataManager,
  35. IItemRepository itemRepository)
  36. {
  37. ServerConfigurationManager = serverConfigurationManager;
  38. Logger = logger;
  39. ProviderManager = providerManager;
  40. FileSystem = fileSystem;
  41. LibraryManager = libraryManager;
  42. ExternalDataManager = externalDataManager;
  43. ItemRepository = itemRepository;
  44. ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
  45. }
  46. protected ItemImageProvider ImageProvider { get; }
  47. protected IServerConfigurationManager ServerConfigurationManager { get; }
  48. protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; }
  49. protected IProviderManager ProviderManager { get; }
  50. protected IFileSystem FileSystem { get; }
  51. protected ILibraryManager LibraryManager { get; }
  52. protected IExternalDataManager ExternalDataManager { get; }
  53. protected IItemRepository ItemRepository { get; }
  54. protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
  55. protected virtual bool EnableUpdatingGenresFromChildren => false;
  56. protected virtual bool EnableUpdatingStudiosFromChildren => false;
  57. protected virtual bool EnableUpdatingOfficialRatingFromChildren => false;
  58. public virtual int Order => 0;
  59. private FileSystemMetadata TryGetFileSystemMetadata(string path, IDirectoryService directoryService)
  60. {
  61. try
  62. {
  63. return directoryService.GetFileSystemEntry(path);
  64. }
  65. catch (Exception ex)
  66. {
  67. Logger.LogError(ex, "Error getting file {Path}", path);
  68. return null;
  69. }
  70. }
  71. public virtual async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
  72. {
  73. var itemOfType = (TItemType)item;
  74. var updateType = ItemUpdateType.None;
  75. var libraryOptions = LibraryManager.GetLibraryOptions(item);
  76. var isFirstRefresh = item.DateLastRefreshed == DateTime.MinValue;
  77. var hasRefreshedMetadata = true;
  78. var hasRefreshedImages = true;
  79. var requiresRefresh = libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays;
  80. if (!requiresRefresh && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
  81. {
  82. // TODO: If this returns true, should we instead just change metadata refresh mode to Full?
  83. requiresRefresh = item.RequiresRefresh();
  84. if (requiresRefresh)
  85. {
  86. Logger.LogDebug("Refreshing {Type} {Item} because item.RequiresRefresh() returned true", typeof(TItemType).Name, item.Path ?? item.Name);
  87. }
  88. }
  89. if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
  90. {
  91. if (ImageProvider.RemoveImages(item))
  92. {
  93. updateType |= ItemUpdateType.ImageUpdate;
  94. }
  95. }
  96. var localImagesFailed = false;
  97. var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
  98. // Only validate already registered images if we are replacing and saving locally
  99. if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
  100. {
  101. item.ValidateImages();
  102. }
  103. else
  104. {
  105. // Run full image validation and register new local images
  106. try
  107. {
  108. if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
  109. {
  110. updateType |= ItemUpdateType.ImageUpdate;
  111. }
  112. }
  113. catch (Exception ex)
  114. {
  115. localImagesFailed = true;
  116. Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
  117. }
  118. }
  119. var metadataResult = new MetadataResult<TItemType>
  120. {
  121. Item = itemOfType
  122. };
  123. var beforeSaveResult = await BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType)
  124. .ConfigureAwait(false);
  125. updateType |= beforeSaveResult;
  126. updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
  127. // Next run metadata providers
  128. if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
  129. {
  130. var providers = GetProviders(item, libraryOptions, refreshOptions, isFirstRefresh, requiresRefresh)
  131. .ToList();
  132. if (providers.Count > 0 || isFirstRefresh || requiresRefresh)
  133. {
  134. if (item.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata))
  135. {
  136. updateType |= ItemUpdateType.MetadataImport;
  137. }
  138. }
  139. if (providers.Count > 0)
  140. {
  141. var id = itemOfType.GetLookupInfo();
  142. if (refreshOptions.SearchResult is not null)
  143. {
  144. ApplySearchResult(id, refreshOptions.SearchResult);
  145. }
  146. id.IsAutomated = refreshOptions.IsAutomated;
  147. var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
  148. var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false);
  149. updateType |= result.UpdateType;
  150. if (result.Failures > 0)
  151. {
  152. hasRefreshedMetadata = false;
  153. }
  154. }
  155. }
  156. // Next run remote image providers, but only if local image providers didn't throw an exception
  157. if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
  158. {
  159. var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
  160. if (providers.Count > 0)
  161. {
  162. var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false);
  163. updateType |= result.UpdateType;
  164. if (result.Failures > 0)
  165. {
  166. hasRefreshedImages = false;
  167. }
  168. }
  169. }
  170. if (hasRefreshedMetadata && hasRefreshedImages)
  171. {
  172. item.DateLastRefreshed = DateTime.UtcNow;
  173. updateType |= item.OnMetadataChanged();
  174. }
  175. updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
  176. await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
  177. return updateType;
  178. async Task<ItemUpdateType> SaveInternal(BaseItem item, MetadataRefreshOptions refreshOptions, ItemUpdateType updateType, bool isFirstRefresh, bool requiresRefresh, MetadataResult<TItemType> metadataResult, CancellationToken cancellationToken)
  179. {
  180. // Save if changes were made, or it's never been saved before
  181. if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh)
  182. {
  183. if (item.IsFileProtocol)
  184. {
  185. var file = TryGetFileSystemMetadata(item.Path, refreshOptions.DirectoryService);
  186. if (file is not null)
  187. {
  188. item.DateModified = file.LastWriteTimeUtc;
  189. if (!file.IsDirectory)
  190. {
  191. item.Size = file.Length;
  192. }
  193. }
  194. }
  195. // If any of these properties are set then make sure the updateType is not None, just to force everything to save
  196. if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
  197. {
  198. updateType |= ItemUpdateType.MetadataDownload;
  199. }
  200. // Save to database
  201. await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
  202. }
  203. return updateType;
  204. }
  205. }
  206. private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result)
  207. {
  208. // Episode and Season do not support Identify, so the search results are the Series'
  209. switch (lookupInfo)
  210. {
  211. case EpisodeInfo episodeInfo:
  212. episodeInfo.SeriesProviderIds = result.ProviderIds;
  213. episodeInfo.ProviderIds.Clear();
  214. break;
  215. case SeasonInfo seasonInfo:
  216. seasonInfo.SeriesProviderIds = result.ProviderIds;
  217. seasonInfo.ProviderIds.Clear();
  218. break;
  219. default:
  220. lookupInfo.ProviderIds = result.ProviderIds;
  221. lookupInfo.Name = result.Name;
  222. lookupInfo.Year = result.ProductionYear;
  223. break;
  224. }
  225. }
  226. protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
  227. {
  228. await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
  229. if (result.Item.SupportsPeople && result.People is not null)
  230. {
  231. var baseItem = result.Item;
  232. await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false);
  233. }
  234. }
  235. protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
  236. {
  237. item.AfterMetadataRefresh();
  238. return Task.CompletedTask;
  239. }
  240. /// <summary>
  241. /// Before the save.
  242. /// </summary>
  243. /// <param name="item">The item.</param>
  244. /// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param>
  245. /// <param name="currentUpdateType">Type of the current update.</param>
  246. /// <returns>ItemUpdateType.</returns>
  247. private async Task<ItemUpdateType> BeforeSave(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateType)
  248. {
  249. var updateType = BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
  250. updateType |= item.OnMetadataChanged();
  251. if (updateType == ItemUpdateType.None)
  252. {
  253. if (!await ItemRepository.ItemExistsAsync(item.Id).ConfigureAwait(false))
  254. {
  255. return ItemUpdateType.MetadataImport;
  256. }
  257. }
  258. return updateType;
  259. }
  260. protected virtual ItemUpdateType BeforeSaveInternal(TItemType item, bool isFullRefresh, ItemUpdateType updateType)
  261. {
  262. if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType))
  263. {
  264. var children = GetChildrenForMetadataUpdates(item);
  265. updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType);
  266. }
  267. var presentationUniqueKey = item.CreatePresentationUniqueKey();
  268. if (!string.Equals(item.PresentationUniqueKey, presentationUniqueKey, StringComparison.Ordinal))
  269. {
  270. item.PresentationUniqueKey = presentationUniqueKey;
  271. updateType |= ItemUpdateType.MetadataImport;
  272. }
  273. // Cleanup extracted files if source file was modified
  274. var itemPath = item.Path;
  275. if (!string.IsNullOrEmpty(itemPath))
  276. {
  277. var info = FileSystem.GetFileSystemInfo(itemPath);
  278. if (info.Exists && item.HasChanged(info.LastWriteTimeUtc))
  279. {
  280. Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", item.DateModified, info.LastWriteTimeUtc, itemPath);
  281. item.DateModified = info.LastWriteTimeUtc;
  282. if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
  283. {
  284. item.DateCreated = info.CreationTimeUtc;
  285. }
  286. if (item is Video video)
  287. {
  288. Logger.LogInformation("File changed, pruning extracted data: {Path}", item.Path);
  289. ExternalDataManager.DeleteExternalItemDataAsync(video, CancellationToken.None).GetAwaiter().GetResult();
  290. }
  291. updateType |= ItemUpdateType.MetadataImport;
  292. }
  293. }
  294. return updateType;
  295. }
  296. protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateType)
  297. {
  298. if (item is Folder folder)
  299. {
  300. if (!isFullRefresh && currentUpdateType == ItemUpdateType.None)
  301. {
  302. return folder.SupportsDateLastMediaAdded;
  303. }
  304. if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
  305. {
  306. if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosFromChildren || EnableUpdatingOfficialRatingFromChildren)
  307. {
  308. return true;
  309. }
  310. if (folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks)
  311. {
  312. return true;
  313. }
  314. }
  315. }
  316. return false;
  317. }
  318. protected virtual IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
  319. {
  320. if (item is Folder folder)
  321. {
  322. return folder.GetRecursiveChildren();
  323. }
  324. return [];
  325. }
  326. protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
  327. {
  328. var updateType = ItemUpdateType.None;
  329. if (item is Folder folder)
  330. {
  331. if (folder.SupportsDateLastMediaAdded)
  332. {
  333. updateType |= UpdateDateLastMediaAdded(item, children);
  334. }
  335. if ((isFullRefresh || currentUpdateType > ItemUpdateType.None) && folder.SupportsCumulativeRunTimeTicks)
  336. {
  337. updateType |= UpdateCumulativeRunTimeTicks(item, children);
  338. }
  339. }
  340. if (!(isFullRefresh || currentUpdateType > ItemUpdateType.None) || item.IsLocked)
  341. {
  342. return updateType;
  343. }
  344. if (EnableUpdatingPremiereDateFromChildren)
  345. {
  346. updateType |= UpdatePremiereDate(item, children);
  347. }
  348. if (EnableUpdatingGenresFromChildren)
  349. {
  350. updateType |= UpdateGenres(item, children);
  351. }
  352. if (EnableUpdatingStudiosFromChildren)
  353. {
  354. updateType |= UpdateStudios(item, children);
  355. }
  356. if (EnableUpdatingOfficialRatingFromChildren)
  357. {
  358. updateType |= UpdateOfficialRating(item, children);
  359. }
  360. return updateType;
  361. }
  362. private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IReadOnlyList<BaseItem> children)
  363. {
  364. if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks)
  365. {
  366. long ticks = 0;
  367. foreach (var child in children)
  368. {
  369. if (!child.IsFolder)
  370. {
  371. ticks += child.RunTimeTicks ?? 0;
  372. }
  373. }
  374. if (!folder.RunTimeTicks.HasValue || folder.RunTimeTicks.Value != ticks)
  375. {
  376. folder.RunTimeTicks = ticks;
  377. return ItemUpdateType.MetadataImport;
  378. }
  379. }
  380. return ItemUpdateType.None;
  381. }
  382. private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IReadOnlyList<BaseItem> children)
  383. {
  384. var updateType = ItemUpdateType.None;
  385. if (item is Folder folder && folder.SupportsDateLastMediaAdded)
  386. {
  387. var dateLastMediaAdded = DateTime.MinValue;
  388. var any = false;
  389. foreach (var child in children)
  390. {
  391. // Exclude any folders and virtual items since they are only placeholders
  392. if (!child.IsFolder && !child.IsVirtualItem)
  393. {
  394. var childDateCreated = child.DateCreated;
  395. if (childDateCreated > dateLastMediaAdded)
  396. {
  397. dateLastMediaAdded = childDateCreated;
  398. }
  399. any = true;
  400. }
  401. }
  402. if ((!folder.DateLastMediaAdded.HasValue && any) || folder.DateLastMediaAdded != dateLastMediaAdded)
  403. {
  404. folder.DateLastMediaAdded = dateLastMediaAdded;
  405. updateType = ItemUpdateType.MetadataImport;
  406. }
  407. }
  408. return updateType;
  409. }
  410. private ItemUpdateType UpdatePremiereDate(TItemType item, IReadOnlyList<BaseItem> children)
  411. {
  412. var updateType = ItemUpdateType.None;
  413. if (children.Count == 0)
  414. {
  415. return updateType;
  416. }
  417. var date = children.Select(i => i.PremiereDate ?? DateTime.MaxValue).Min();
  418. var originalPremiereDate = item.PremiereDate;
  419. var originalProductionYear = item.ProductionYear;
  420. if (date > DateTime.MinValue && date < DateTime.MaxValue)
  421. {
  422. item.PremiereDate = date;
  423. item.ProductionYear = date.Year;
  424. }
  425. else
  426. {
  427. var year = children.Select(i => i.ProductionYear ?? 0).Min();
  428. if (year > 0)
  429. {
  430. item.ProductionYear = year;
  431. }
  432. }
  433. if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue)
  434. || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
  435. {
  436. updateType |= ItemUpdateType.MetadataEdit;
  437. }
  438. return updateType;
  439. }
  440. private ItemUpdateType UpdateGenres(TItemType item, IReadOnlyList<BaseItem> children)
  441. {
  442. var updateType = ItemUpdateType.None;
  443. if (!item.LockedFields.Contains(MetadataField.Genres))
  444. {
  445. var currentList = item.Genres;
  446. item.Genres = children.SelectMany(i => i.Genres)
  447. .Distinct(StringComparer.OrdinalIgnoreCase)
  448. .ToArray();
  449. if (currentList.Length != item.Genres.Length || !currentList.Order().SequenceEqual(item.Genres.Order(), StringComparer.OrdinalIgnoreCase))
  450. {
  451. updateType |= ItemUpdateType.MetadataEdit;
  452. }
  453. }
  454. return updateType;
  455. }
  456. private ItemUpdateType UpdateStudios(TItemType item, IReadOnlyList<BaseItem> children)
  457. {
  458. var updateType = ItemUpdateType.None;
  459. if (!item.LockedFields.Contains(MetadataField.Studios))
  460. {
  461. var currentList = item.Studios;
  462. item.Studios = children.SelectMany(i => i.Studios)
  463. .Distinct(StringComparer.OrdinalIgnoreCase)
  464. .ToArray();
  465. if (currentList.Length != item.Studios.Length || !currentList.Order().SequenceEqual(item.Studios.Order(), StringComparer.OrdinalIgnoreCase))
  466. {
  467. updateType |= ItemUpdateType.MetadataEdit;
  468. }
  469. }
  470. return updateType;
  471. }
  472. private ItemUpdateType UpdateOfficialRating(TItemType item, IReadOnlyList<BaseItem> children)
  473. {
  474. var updateType = ItemUpdateType.None;
  475. if (!item.LockedFields.Contains(MetadataField.OfficialRating))
  476. {
  477. if (item.UpdateRatingToItems(children))
  478. {
  479. updateType |= ItemUpdateType.MetadataEdit;
  480. }
  481. }
  482. return updateType;
  483. }
  484. /// <summary>
  485. /// Gets the providers.
  486. /// </summary>
  487. /// <param name="item">A media item.</param>
  488. /// <param name="libraryOptions">The LibraryOptions to use.</param>
  489. /// <param name="options">The MetadataRefreshOptions to use.</param>
  490. /// <param name="isFirstRefresh">Specifies first refresh mode.</param>
  491. /// <param name="requiresRefresh">Specifies refresh mode.</param>
  492. /// <returns>IEnumerable{`0}.</returns>
  493. protected IEnumerable<IMetadataProvider> GetProviders(BaseItem item, LibraryOptions libraryOptions, MetadataRefreshOptions options, bool isFirstRefresh, bool requiresRefresh)
  494. {
  495. // Get providers to refresh
  496. var providers = ProviderManager.GetMetadataProviders<TItemType>(item, libraryOptions).ToList();
  497. var metadataRefreshMode = options.MetadataRefreshMode;
  498. // Run all if either of these flags are true
  499. var runAllProviders = options.ReplaceAllMetadata ||
  500. metadataRefreshMode == MetadataRefreshMode.FullRefresh ||
  501. (isFirstRefresh && metadataRefreshMode >= MetadataRefreshMode.Default) ||
  502. (requiresRefresh && metadataRefreshMode >= MetadataRefreshMode.Default);
  503. if (!runAllProviders)
  504. {
  505. var providersWithChanges = providers
  506. .Where(i =>
  507. {
  508. if (i is IHasItemChangeMonitor hasFileChangeMonitor)
  509. {
  510. return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
  511. }
  512. return false;
  513. })
  514. .ToList();
  515. if (providersWithChanges.Count == 0)
  516. {
  517. providers = new List<IMetadataProvider<TItemType>>();
  518. }
  519. else
  520. {
  521. var anyRemoteProvidersChanged = providersWithChanges.OfType<IRemoteMetadataProvider>()
  522. .Any();
  523. var anyLocalProvidersChanged = providersWithChanges.OfType<ILocalMetadataProvider>()
  524. .Any();
  525. var anyLocalPreRefreshProvidersChanged = providersWithChanges.OfType<IPreRefreshProvider>()
  526. .Any();
  527. providers = providers.Where(i =>
  528. {
  529. // If any provider reports a change, always run local ones as well
  530. if (i is ILocalMetadataProvider)
  531. {
  532. return anyRemoteProvidersChanged || anyLocalProvidersChanged || anyLocalPreRefreshProvidersChanged;
  533. }
  534. // If any remote providers changed, run them all so that priorities can be honored
  535. if (i is IRemoteMetadataProvider)
  536. {
  537. if (options.MetadataRefreshMode == MetadataRefreshMode.ValidationOnly)
  538. {
  539. return false;
  540. }
  541. return anyRemoteProvidersChanged;
  542. }
  543. // Run custom refresh providers if they report a change or any remote providers change
  544. return anyRemoteProvidersChanged || providersWithChanges.Contains(i);
  545. }).ToList();
  546. }
  547. }
  548. return providers;
  549. }
  550. protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options)
  551. {
  552. // Get providers to refresh
  553. var providers = allImageProviders.Where(i => i is not ILocalImageProvider);
  554. var dateLastImageRefresh = item.DateLastRefreshed;
  555. // Run all if either of these flags are true
  556. var runAllProviders = options.ImageRefreshMode == MetadataRefreshMode.FullRefresh || dateLastImageRefresh.Date == DateTime.MinValue.Date;
  557. if (!runAllProviders)
  558. {
  559. providers = providers
  560. .Where(i =>
  561. {
  562. if (i is IHasItemChangeMonitor hasFileChangeMonitor)
  563. {
  564. return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
  565. }
  566. return false;
  567. });
  568. }
  569. return providers;
  570. }
  571. public bool CanRefresh(BaseItem item)
  572. {
  573. return item is TItemType;
  574. }
  575. public bool CanRefreshPrimary(Type type)
  576. {
  577. return type == typeof(TItemType);
  578. }
  579. protected virtual async Task<RefreshResult> RefreshWithProviders(
  580. MetadataResult<TItemType> metadata,
  581. TIdType id,
  582. MetadataRefreshOptions options,
  583. ICollection<IMetadataProvider> providers,
  584. ItemImageProvider imageService,
  585. bool isSavingMetadata,
  586. CancellationToken cancellationToken)
  587. {
  588. var refreshResult = new RefreshResult
  589. {
  590. UpdateType = ItemUpdateType.None
  591. };
  592. var item = metadata.Item;
  593. var customProviders = providers.OfType<ICustomMetadataProvider<TItemType>>().ToList();
  594. var logName = !item.IsFileProtocol ? item.Name ?? item.Path : item.Path ?? item.Name;
  595. foreach (var provider in customProviders.Where(i => i is IPreRefreshProvider))
  596. {
  597. await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
  598. }
  599. if (item.IsLocked)
  600. {
  601. return refreshResult;
  602. }
  603. var temp = new MetadataResult<TItemType>
  604. {
  605. Item = CreateNew()
  606. };
  607. temp.Item.Path = item.Path;
  608. temp.Item.Id = item.Id;
  609. temp.Item.ParentIndexNumber = item.ParentIndexNumber;
  610. temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
  611. temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
  612. var foundImageTypes = new List<ImageType>();
  613. // Do not execute local providers if we are identifying or replacing with local metadata saving enabled
  614. if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
  615. {
  616. foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
  617. {
  618. var providerName = provider.GetType().Name;
  619. Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
  620. var itemInfo = new ItemInfo(item);
  621. try
  622. {
  623. var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
  624. if (localItem.HasMetadata)
  625. {
  626. foreach (var remoteImage in localItem.RemoteImages)
  627. {
  628. try
  629. {
  630. if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
  631. && !options.IsReplacingImage(remoteImage.Type))
  632. {
  633. continue;
  634. }
  635. await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
  636. refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
  637. // remember imagetype that has just been downloaded
  638. foundImageTypes.Add(remoteImage.Type);
  639. }
  640. catch (HttpRequestException ex)
  641. {
  642. Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
  643. }
  644. }
  645. if (foundImageTypes.Count > 0)
  646. {
  647. imageService.UpdateReplaceImages(options, foundImageTypes);
  648. }
  649. if (imageService.MergeImages(item, localItem.Images, options))
  650. {
  651. refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
  652. }
  653. MergeData(localItem, temp, [], false, true);
  654. refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
  655. break;
  656. }
  657. Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
  658. }
  659. catch (OperationCanceledException)
  660. {
  661. throw;
  662. }
  663. catch (Exception ex)
  664. {
  665. Logger.LogError(ex, "Error in {Provider}", provider.Name);
  666. // If a local provider fails, consider that a failure
  667. refreshResult.ErrorMessage = ex.Message;
  668. }
  669. }
  670. }
  671. var isLocalLocked = temp.Item.IsLocked;
  672. if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly))
  673. {
  674. var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
  675. .ConfigureAwait(false);
  676. refreshResult.UpdateType |= remoteResult.UpdateType;
  677. refreshResult.ErrorMessage = remoteResult.ErrorMessage;
  678. refreshResult.Failures += remoteResult.Failures;
  679. }
  680. if (providers.Any(i => i is not ICustomMetadataProvider))
  681. {
  682. if (refreshResult.UpdateType > ItemUpdateType.None)
  683. {
  684. if (!options.RemoveOldMetadata)
  685. {
  686. // Add existing metadata to provider result if it does not exist there
  687. MergeData(metadata, temp, [], false, false);
  688. }
  689. if (isLocalLocked)
  690. {
  691. MergeData(temp, metadata, item.LockedFields, true, true);
  692. }
  693. else
  694. {
  695. var shouldReplace = (options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly && options.ReplaceAllMetadata)
  696. // Case for Scan for new and updated files
  697. || (options.MetadataRefreshMode == MetadataRefreshMode.Default && !options.ReplaceAllMetadata);
  698. MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
  699. }
  700. }
  701. }
  702. foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
  703. {
  704. await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
  705. }
  706. return refreshResult;
  707. }
  708. private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken)
  709. {
  710. Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
  711. try
  712. {
  713. refreshResult.UpdateType |= await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false);
  714. }
  715. catch (OperationCanceledException)
  716. {
  717. throw;
  718. }
  719. catch (Exception ex)
  720. {
  721. refreshResult.ErrorMessage = ex.Message;
  722. Logger.LogError(ex, "Error in {Provider}", provider.Name);
  723. }
  724. }
  725. protected virtual TItemType CreateNew()
  726. {
  727. return new TItemType();
  728. }
  729. private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool replaceData, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken)
  730. {
  731. var refreshResult = new RefreshResult();
  732. if (id is not null)
  733. {
  734. MergeNewData(temp.Item, id);
  735. }
  736. foreach (var provider in providers)
  737. {
  738. var providerName = provider.GetType().Name;
  739. Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
  740. try
  741. {
  742. var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
  743. if (result.HasMetadata)
  744. {
  745. result.Provider = provider.Name;
  746. MergeData(result, temp, [], replaceData, false);
  747. MergeNewData(temp.Item, id);
  748. refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
  749. }
  750. else
  751. {
  752. Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
  753. }
  754. }
  755. catch (OperationCanceledException)
  756. {
  757. throw;
  758. }
  759. catch (Exception ex)
  760. {
  761. refreshResult.Failures++;
  762. refreshResult.ErrorMessage = ex.Message;
  763. Logger.LogError(ex, "Error in {Provider}", provider.Name);
  764. }
  765. }
  766. return refreshResult;
  767. }
  768. private void MergeNewData(TItemType source, TIdType lookupInfo)
  769. {
  770. // Copy new provider id's that may have been obtained
  771. foreach (var providerId in source.ProviderIds)
  772. {
  773. var key = providerId.Key;
  774. // Don't replace existing Id's.
  775. lookupInfo.ProviderIds.TryAdd(key, providerId.Value);
  776. }
  777. }
  778. private bool HasChanged(BaseItem item, IHasItemChangeMonitor changeMonitor, IDirectoryService directoryService)
  779. {
  780. try
  781. {
  782. var hasChanged = changeMonitor.HasChanged(item, directoryService);
  783. if (hasChanged)
  784. {
  785. Logger.LogDebug("{Monitor} reports change to {Item}", changeMonitor.GetType().Name, item.Path ?? item.Name);
  786. }
  787. return hasChanged;
  788. }
  789. catch (Exception ex)
  790. {
  791. Logger.LogError(ex, "Error in {Monitor}.HasChanged", changeMonitor.GetType().Name);
  792. return false;
  793. }
  794. }
  795. /// <summary>
  796. /// Merges metadata from source into target.
  797. /// </summary>
  798. /// <param name="source">The source for new metadata.</param>
  799. /// <param name="target">The target to insert new metadata into.</param>
  800. /// <param name="lockedFields">The fields that are locked and should not be updated.</param>
  801. /// <param name="replaceData"><c>true</c> if existing data should be replaced.</param>
  802. /// <param name="mergeMetadataSettings"><c>true</c> if the metadata settings in target should be updated to match source.</param>
  803. /// <exception cref="ArgumentException">Thrown if source or target are null.</exception>
  804. protected virtual void MergeData(
  805. MetadataResult<TItemType> source,
  806. MetadataResult<TItemType> target,
  807. MetadataField[] lockedFields,
  808. bool replaceData,
  809. bool mergeMetadataSettings)
  810. {
  811. MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
  812. }
  813. internal static void MergeBaseItemData(
  814. MetadataResult<TItemType> sourceResult,
  815. MetadataResult<TItemType> targetResult,
  816. MetadataField[] lockedFields,
  817. bool replaceData,
  818. bool mergeMetadataSettings)
  819. {
  820. var source = sourceResult.Item;
  821. var target = targetResult.Item;
  822. ArgumentNullException.ThrowIfNull(sourceResult);
  823. ArgumentNullException.ThrowIfNull(targetResult);
  824. if (!lockedFields.Contains(MetadataField.Name))
  825. {
  826. if (replaceData || string.IsNullOrEmpty(target.Name))
  827. {
  828. // Safeguard against incoming data having an empty name
  829. if (!string.IsNullOrWhiteSpace(source.Name))
  830. {
  831. target.Name = source.Name;
  832. }
  833. }
  834. }
  835. if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
  836. {
  837. target.OriginalTitle = source.OriginalTitle;
  838. }
  839. if (replaceData || !target.CommunityRating.HasValue)
  840. {
  841. target.CommunityRating = source.CommunityRating;
  842. }
  843. if (replaceData || !target.EndDate.HasValue)
  844. {
  845. target.EndDate = source.EndDate;
  846. }
  847. if (!lockedFields.Contains(MetadataField.Genres))
  848. {
  849. if (replaceData || target.Genres.Length == 0)
  850. {
  851. target.Genres = source.Genres;
  852. }
  853. }
  854. if (replaceData || !target.IndexNumber.HasValue)
  855. {
  856. target.IndexNumber = source.IndexNumber;
  857. }
  858. if (!lockedFields.Contains(MetadataField.OfficialRating))
  859. {
  860. if (replaceData || string.IsNullOrEmpty(target.OfficialRating))
  861. {
  862. target.OfficialRating = source.OfficialRating;
  863. }
  864. }
  865. if (replaceData || string.IsNullOrEmpty(target.CustomRating))
  866. {
  867. target.CustomRating = source.CustomRating;
  868. }
  869. if (replaceData || string.IsNullOrEmpty(target.Tagline))
  870. {
  871. target.Tagline = source.Tagline;
  872. }
  873. if (!lockedFields.Contains(MetadataField.Overview))
  874. {
  875. if (replaceData || string.IsNullOrEmpty(target.Overview))
  876. {
  877. target.Overview = source.Overview;
  878. }
  879. }
  880. if (replaceData || !target.ParentIndexNumber.HasValue)
  881. {
  882. target.ParentIndexNumber = source.ParentIndexNumber;
  883. }
  884. if (!lockedFields.Contains(MetadataField.Cast))
  885. {
  886. if (replaceData || targetResult.People is null || targetResult.People.Count == 0)
  887. {
  888. targetResult.People = sourceResult.People;
  889. }
  890. else if (sourceResult.People is not null && sourceResult.People.Count > 0)
  891. {
  892. MergePeople(sourceResult.People, targetResult.People);
  893. }
  894. }
  895. if (replaceData || !target.PremiereDate.HasValue)
  896. {
  897. target.PremiereDate = source.PremiereDate;
  898. }
  899. if (replaceData || !target.ProductionYear.HasValue)
  900. {
  901. target.ProductionYear = source.ProductionYear;
  902. }
  903. if (!lockedFields.Contains(MetadataField.Runtime))
  904. {
  905. if (replaceData || !target.RunTimeTicks.HasValue)
  906. {
  907. if (target is not Audio && target is not Video)
  908. {
  909. target.RunTimeTicks = source.RunTimeTicks;
  910. }
  911. }
  912. }
  913. if (!lockedFields.Contains(MetadataField.Studios))
  914. {
  915. if (replaceData || target.Studios.Length == 0)
  916. {
  917. target.Studios = source.Studios;
  918. }
  919. else
  920. {
  921. target.Studios = target.Studios.Concat(source.Studios).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  922. }
  923. }
  924. if (!lockedFields.Contains(MetadataField.Tags))
  925. {
  926. if (replaceData || target.Tags.Length == 0)
  927. {
  928. target.Tags = source.Tags;
  929. }
  930. else
  931. {
  932. target.Tags = target.Tags.Concat(source.Tags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  933. }
  934. }
  935. if (!lockedFields.Contains(MetadataField.ProductionLocations))
  936. {
  937. if (replaceData || target.ProductionLocations.Length == 0)
  938. {
  939. target.ProductionLocations = source.ProductionLocations;
  940. }
  941. else
  942. {
  943. target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  944. }
  945. }
  946. foreach (var id in source.ProviderIds)
  947. {
  948. var key = id.Key;
  949. // Don't replace existing Id's.
  950. if (replaceData)
  951. {
  952. target.ProviderIds[key] = id.Value;
  953. }
  954. else
  955. {
  956. target.ProviderIds.TryAdd(key, id.Value);
  957. }
  958. }
  959. if (replaceData || !target.CriticRating.HasValue)
  960. {
  961. target.CriticRating = source.CriticRating;
  962. }
  963. if (replaceData || target.RemoteTrailers.Count == 0)
  964. {
  965. target.RemoteTrailers = source.RemoteTrailers;
  966. }
  967. else
  968. {
  969. target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArray();
  970. }
  971. MergeAlbumArtist(source, target, replaceData);
  972. MergeVideoInfo(source, target, replaceData);
  973. MergeDisplayOrder(source, target, replaceData);
  974. if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
  975. {
  976. var forcedSortName = source.ForcedSortName;
  977. if (!string.IsNullOrEmpty(forcedSortName))
  978. {
  979. target.ForcedSortName = forcedSortName;
  980. }
  981. }
  982. if (mergeMetadataSettings)
  983. {
  984. if (replaceData || !target.IsLocked)
  985. {
  986. target.IsLocked = target.IsLocked || source.IsLocked;
  987. }
  988. if (target.LockedFields.Length == 0)
  989. {
  990. target.LockedFields = source.LockedFields;
  991. }
  992. else
  993. {
  994. target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
  995. }
  996. if (source.DateCreated != DateTime.MinValue)
  997. {
  998. target.DateCreated = source.DateCreated;
  999. }
  1000. if (replaceData || source.DateModified != DateTime.MinValue)
  1001. {
  1002. target.DateModified = source.DateModified;
  1003. }
  1004. if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
  1005. {
  1006. target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
  1007. }
  1008. if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
  1009. {
  1010. target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
  1011. }
  1012. }
  1013. }
  1014. private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
  1015. {
  1016. var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
  1017. var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
  1018. foreach (var name in targetByName.Select(g => g.Key))
  1019. {
  1020. var targetPeople = targetByName[name].ToArray();
  1021. var sourcePeople = sourceByName[name].ToArray();
  1022. if (sourcePeople.Length == 0)
  1023. {
  1024. continue;
  1025. }
  1026. for (int i = 0; i < targetPeople.Length; i++)
  1027. {
  1028. var person = targetPeople[i];
  1029. var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
  1030. foreach (var providerId in personInSource.ProviderIds)
  1031. {
  1032. person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
  1033. }
  1034. if (string.IsNullOrWhiteSpace(person.ImageUrl))
  1035. {
  1036. person.ImageUrl = personInSource.ImageUrl;
  1037. }
  1038. if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
  1039. {
  1040. person.Role = personInSource.Role;
  1041. }
  1042. if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
  1043. {
  1044. person.SortOrder = personInSource.SortOrder;
  1045. }
  1046. }
  1047. }
  1048. }
  1049. private static void MergeDisplayOrder(BaseItem source, BaseItem target, bool replaceData)
  1050. {
  1051. if (source is IHasDisplayOrder sourceHasDisplayOrder
  1052. && target is IHasDisplayOrder targetHasDisplayOrder)
  1053. {
  1054. if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
  1055. {
  1056. var displayOrder = sourceHasDisplayOrder.DisplayOrder;
  1057. if (!string.IsNullOrWhiteSpace(displayOrder))
  1058. {
  1059. targetHasDisplayOrder.DisplayOrder = displayOrder;
  1060. }
  1061. }
  1062. }
  1063. }
  1064. private static void MergeAlbumArtist(BaseItem source, BaseItem target, bool replaceData)
  1065. {
  1066. if (source is IHasAlbumArtist sourceHasAlbumArtist
  1067. && target is IHasAlbumArtist targetHasAlbumArtist)
  1068. {
  1069. if (replaceData || targetHasAlbumArtist.AlbumArtists.Count == 0)
  1070. {
  1071. targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
  1072. }
  1073. else if (sourceHasAlbumArtist.AlbumArtists.Count > 0)
  1074. {
  1075. targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
  1076. }
  1077. }
  1078. }
  1079. private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
  1080. {
  1081. if (source is Video sourceCast && target is Video targetCast)
  1082. {
  1083. if (sourceCast.Video3DFormat.HasValue && (replaceData || !targetCast.Video3DFormat.HasValue))
  1084. {
  1085. targetCast.Video3DFormat = sourceCast.Video3DFormat;
  1086. }
  1087. }
  1088. }
  1089. }
  1090. }