ProviderManager.cs 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Net;
  8. using System.Net.Http;
  9. using System.Net.Mime;
  10. using System.Runtime.ExceptionServices;
  11. using System.Threading;
  12. using System.Threading.Tasks;
  13. using AsyncKeyedLock;
  14. using Jellyfin.Data.Enums;
  15. using Jellyfin.Data.Events;
  16. using Jellyfin.Extensions;
  17. using MediaBrowser.Common.Net;
  18. using MediaBrowser.Controller;
  19. using MediaBrowser.Controller.BaseItemManager;
  20. using MediaBrowser.Controller.Configuration;
  21. using MediaBrowser.Controller.Dto;
  22. using MediaBrowser.Controller.Entities;
  23. using MediaBrowser.Controller.Entities.Audio;
  24. using MediaBrowser.Controller.Entities.Movies;
  25. using MediaBrowser.Controller.Library;
  26. using MediaBrowser.Controller.Lyrics;
  27. using MediaBrowser.Controller.Providers;
  28. using MediaBrowser.Controller.Subtitles;
  29. using MediaBrowser.Model.Configuration;
  30. using MediaBrowser.Model.Entities;
  31. using MediaBrowser.Model.Extensions;
  32. using MediaBrowser.Model.IO;
  33. using MediaBrowser.Model.Net;
  34. using MediaBrowser.Model.Providers;
  35. using Microsoft.Extensions.Caching.Memory;
  36. using Microsoft.Extensions.Logging;
  37. using Book = MediaBrowser.Controller.Entities.Book;
  38. using Episode = MediaBrowser.Controller.Entities.TV.Episode;
  39. using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
  40. using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
  41. using Season = MediaBrowser.Controller.Entities.TV.Season;
  42. using Series = MediaBrowser.Controller.Entities.TV.Series;
  43. namespace MediaBrowser.Providers.Manager
  44. {
  45. /// <summary>
  46. /// Class ProviderManager.
  47. /// </summary>
  48. public class ProviderManager : IProviderManager, IDisposable
  49. {
  50. private readonly Lock _refreshQueueLock = new();
  51. private readonly ILogger<ProviderManager> _logger;
  52. private readonly IHttpClientFactory _httpClientFactory;
  53. private readonly ILibraryMonitor _libraryMonitor;
  54. private readonly IFileSystem _fileSystem;
  55. private readonly IServerApplicationPaths _appPaths;
  56. private readonly ILibraryManager _libraryManager;
  57. private readonly ISubtitleManager _subtitleManager;
  58. private readonly ILyricManager _lyricManager;
  59. private readonly IServerConfigurationManager _configurationManager;
  60. private readonly IBaseItemManager _baseItemManager;
  61. private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new();
  62. private readonly CancellationTokenSource _disposeCancellationTokenSource = new();
  63. private readonly PriorityQueue<(Guid ItemId, MetadataRefreshOptions RefreshOptions), RefreshPriority> _refreshQueue = new();
  64. private readonly IMemoryCache _memoryCache;
  65. private readonly IMediaSegmentManager _mediaSegmentManager;
  66. private readonly AsyncKeyedLocker<string> _imageSaveLock = new(o =>
  67. {
  68. o.PoolSize = 20;
  69. o.PoolInitialFill = 1;
  70. });
  71. private IImageProvider[] _imageProviders = [];
  72. private IMetadataService[] _metadataServices = [];
  73. private IMetadataProvider[] _metadataProviders = [];
  74. private IMetadataSaver[] _savers = [];
  75. private IExternalId[] _externalIds = [];
  76. private IExternalUrlProvider[] _externalUrlProviders = [];
  77. private bool _isProcessingRefreshQueue;
  78. private bool _disposed;
  79. /// <summary>
  80. /// Initializes a new instance of the <see cref="ProviderManager"/> class.
  81. /// </summary>
  82. /// <param name="httpClientFactory">The Http client factory.</param>
  83. /// <param name="subtitleManager">The subtitle manager.</param>
  84. /// <param name="configurationManager">The configuration manager.</param>
  85. /// <param name="libraryMonitor">The library monitor.</param>
  86. /// <param name="logger">The logger.</param>
  87. /// <param name="fileSystem">The filesystem.</param>
  88. /// <param name="appPaths">The server application paths.</param>
  89. /// <param name="libraryManager">The library manager.</param>
  90. /// <param name="baseItemManager">The BaseItem manager.</param>
  91. /// <param name="lyricManager">The lyric manager.</param>
  92. /// <param name="memoryCache">The memory cache.</param>
  93. /// <param name="mediaSegmentManager">The media segment manager.</param>
  94. public ProviderManager(
  95. IHttpClientFactory httpClientFactory,
  96. ISubtitleManager subtitleManager,
  97. IServerConfigurationManager configurationManager,
  98. ILibraryMonitor libraryMonitor,
  99. ILogger<ProviderManager> logger,
  100. IFileSystem fileSystem,
  101. IServerApplicationPaths appPaths,
  102. ILibraryManager libraryManager,
  103. IBaseItemManager baseItemManager,
  104. ILyricManager lyricManager,
  105. IMemoryCache memoryCache,
  106. IMediaSegmentManager mediaSegmentManager)
  107. {
  108. _logger = logger;
  109. _httpClientFactory = httpClientFactory;
  110. _configurationManager = configurationManager;
  111. _libraryMonitor = libraryMonitor;
  112. _fileSystem = fileSystem;
  113. _appPaths = appPaths;
  114. _libraryManager = libraryManager;
  115. _subtitleManager = subtitleManager;
  116. _baseItemManager = baseItemManager;
  117. _lyricManager = lyricManager;
  118. _memoryCache = memoryCache;
  119. _mediaSegmentManager = mediaSegmentManager;
  120. }
  121. /// <inheritdoc/>
  122. public event EventHandler<GenericEventArgs<BaseItem>>? RefreshStarted;
  123. /// <inheritdoc/>
  124. public event EventHandler<GenericEventArgs<BaseItem>>? RefreshCompleted;
  125. /// <inheritdoc/>
  126. public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>>? RefreshProgress;
  127. /// <inheritdoc/>
  128. public void AddParts(
  129. IEnumerable<IImageProvider> imageProviders,
  130. IEnumerable<IMetadataService> metadataServices,
  131. IEnumerable<IMetadataProvider> metadataProviders,
  132. IEnumerable<IMetadataSaver> metadataSavers,
  133. IEnumerable<IExternalId> externalIds,
  134. IEnumerable<IExternalUrlProvider> externalUrlProviders)
  135. {
  136. _imageProviders = imageProviders.ToArray();
  137. _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray();
  138. _metadataProviders = metadataProviders.ToArray();
  139. _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray();
  140. _externalUrlProviders = externalUrlProviders.OrderBy(i => i.Name).ToArray();
  141. _savers = metadataSavers.ToArray();
  142. }
  143. /// <inheritdoc/>
  144. public Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
  145. {
  146. var type = item.GetType();
  147. var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type))
  148. ?? _metadataServices.FirstOrDefault(current => current.CanRefresh(item));
  149. if (service is null)
  150. {
  151. _logger.LogError("Unable to find a metadata service for item of type {TypeName}", type.Name);
  152. return Task.FromResult(ItemUpdateType.None);
  153. }
  154. return service.RefreshMetadata(item, options, cancellationToken);
  155. }
  156. /// <inheritdoc/>
  157. public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken)
  158. {
  159. using (await _imageSaveLock.LockAsync(url, cancellationToken).ConfigureAwait(false))
  160. {
  161. if (_memoryCache.TryGetValue(url, out (string ContentType, byte[] ImageContents)? cachedValue)
  162. && cachedValue is not null)
  163. {
  164. var imageContents = cachedValue.Value.ImageContents;
  165. var cacheStream = new MemoryStream(imageContents, 0, imageContents.Length, false);
  166. await using (cacheStream.ConfigureAwait(false))
  167. {
  168. await SaveImage(
  169. item,
  170. cacheStream,
  171. cachedValue.Value.ContentType,
  172. type,
  173. imageIndex,
  174. cancellationToken).ConfigureAwait(false);
  175. return;
  176. }
  177. }
  178. var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
  179. using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
  180. response.EnsureSuccessStatusCode();
  181. var contentType = response.Content.Headers.ContentType?.MediaType;
  182. // Workaround for tvheadend channel icons
  183. // TODO: Isolate this hack into the tvh plugin
  184. if (string.IsNullOrEmpty(contentType))
  185. {
  186. // Special case for imagecache
  187. if (url.Contains("/imagecache/", StringComparison.OrdinalIgnoreCase))
  188. {
  189. contentType = MediaTypeNames.Image.Png;
  190. }
  191. else
  192. {
  193. // Deduce content type from file extension
  194. contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
  195. }
  196. // Throw if we still can't determine the content type
  197. if (string.IsNullOrEmpty(contentType))
  198. {
  199. throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode);
  200. }
  201. }
  202. // TVDb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
  203. if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
  204. {
  205. throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound);
  206. }
  207. // some iptv/epg providers don't correctly report media type, extract from url if no extension found
  208. if (string.IsNullOrWhiteSpace(MimeTypes.ToExtension(contentType)))
  209. {
  210. // Strip query parameters from url to get actual path.
  211. contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
  212. }
  213. if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
  214. {
  215. throw new HttpRequestException($"Request returned {contentType} instead of an image type", null, HttpStatusCode.NotFound);
  216. }
  217. var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
  218. var stream = new MemoryStream(responseBytes, 0, responseBytes.Length, false);
  219. await using (stream.ConfigureAwait(false))
  220. {
  221. _memoryCache.Set(url, (contentType, responseBytes), TimeSpan.FromSeconds(10));
  222. await SaveImage(
  223. item,
  224. stream,
  225. contentType,
  226. type,
  227. imageIndex,
  228. cancellationToken).ConfigureAwait(false);
  229. }
  230. }
  231. }
  232. /// <inheritdoc/>
  233. public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
  234. {
  235. return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken);
  236. }
  237. /// <inheritdoc/>
  238. public async Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
  239. {
  240. if (string.IsNullOrWhiteSpace(source))
  241. {
  242. throw new ArgumentNullException(nameof(source));
  243. }
  244. try
  245. {
  246. var fileStream = AsyncFile.OpenRead(source);
  247. await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken).ConfigureAwait(false);
  248. }
  249. finally
  250. {
  251. try
  252. {
  253. File.Delete(source);
  254. }
  255. catch (Exception ex)
  256. {
  257. _logger.LogError(ex, "Source file {Source} not found or in use, skip removing", source);
  258. }
  259. }
  260. }
  261. /// <inheritdoc/>
  262. public Task SaveImage(Stream source, string mimeType, string path)
  263. {
  264. return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
  265. .SaveImage(source, path);
  266. }
  267. /// <inheritdoc/>
  268. public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken)
  269. {
  270. var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders);
  271. if (!string.IsNullOrEmpty(query.ProviderName))
  272. {
  273. var providerName = query.ProviderName;
  274. providers = providers.Where(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
  275. }
  276. if (query.ImageType is not null)
  277. {
  278. providers = providers.Where(i => i.GetSupportedImages(item).Contains(query.ImageType.Value));
  279. }
  280. var preferredLanguage = item.GetPreferredMetadataLanguage();
  281. var tasks = providers.Select(i => GetImages(item, i, preferredLanguage, query.IncludeAllLanguages, cancellationToken, query.ImageType));
  282. var results = await Task.WhenAll(tasks).ConfigureAwait(false);
  283. return results.SelectMany(i => i);
  284. }
  285. /// <summary>
  286. /// Gets the images.
  287. /// </summary>
  288. /// <param name="item">The item.</param>
  289. /// <param name="provider">The provider.</param>
  290. /// <param name="preferredLanguage">The preferred language.</param>
  291. /// <param name="includeAllLanguages">Whether to include all languages in results.</param>
  292. /// <param name="cancellationToken">The cancellation token.</param>
  293. /// <param name="type">The type.</param>
  294. /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
  295. private async Task<IEnumerable<RemoteImageInfo>> GetImages(
  296. BaseItem item,
  297. IRemoteImageProvider provider,
  298. string preferredLanguage,
  299. bool includeAllLanguages,
  300. CancellationToken cancellationToken,
  301. ImageType? type = null)
  302. {
  303. bool hasPreferredLanguage = !string.IsNullOrWhiteSpace(preferredLanguage);
  304. try
  305. {
  306. var result = await provider.GetImages(item, cancellationToken).ConfigureAwait(false);
  307. if (type.HasValue)
  308. {
  309. result = result.Where(i => i.Type == type.Value);
  310. }
  311. if (!includeAllLanguages && hasPreferredLanguage)
  312. {
  313. // Filter out languages that do not match the preferred languages.
  314. //
  315. // TODO: should exception case of "en" (English) eventually be removed?
  316. result = result.Where(i => string.IsNullOrWhiteSpace(i.Language) ||
  317. string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase) ||
  318. string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase));
  319. }
  320. return result.OrderByLanguageDescending(preferredLanguage);
  321. }
  322. catch (OperationCanceledException)
  323. {
  324. return Enumerable.Empty<RemoteImageInfo>();
  325. }
  326. catch (Exception ex)
  327. {
  328. _logger.LogError(ex, "{ProviderName} failed in GetImageInfos for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
  329. return Enumerable.Empty<RemoteImageInfo>();
  330. }
  331. }
  332. /// <inheritdoc/>
  333. public IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item)
  334. {
  335. return GetRemoteImageProviders(item, true).Select(i => new ImageProviderInfo(i.Name, i.GetSupportedImages(item).ToArray()));
  336. }
  337. private IEnumerable<IRemoteImageProvider> GetRemoteImageProviders(BaseItem item, bool includeDisabled)
  338. {
  339. var options = GetMetadataOptions(item);
  340. var libraryOptions = _libraryManager.GetLibraryOptions(item);
  341. return GetImageProvidersInternal(
  342. item,
  343. libraryOptions,
  344. options,
  345. new ImageRefreshOptions(new DirectoryService(_fileSystem)),
  346. includeDisabled).OfType<IRemoteImageProvider>();
  347. }
  348. /// <inheritdoc/>
  349. public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions)
  350. {
  351. return GetImageProvidersInternal(item, _libraryManager.GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false);
  352. }
  353. private IEnumerable<IImageProvider> GetImageProvidersInternal(BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, ImageRefreshOptions refreshOptions, bool includeDisabled)
  354. {
  355. var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
  356. var fetcherOrder = typeOptions?.ImageFetcherOrder ?? options.ImageFetcherOrder;
  357. return _imageProviders.Where(i => CanRefreshImages(i, item, typeOptions, refreshOptions, includeDisabled))
  358. .OrderBy(i => GetConfiguredOrder(fetcherOrder, i.Name))
  359. .ThenBy(GetDefaultOrder);
  360. }
  361. private bool CanRefreshImages(
  362. IImageProvider provider,
  363. BaseItem item,
  364. TypeOptions? libraryTypeOptions,
  365. ImageRefreshOptions refreshOptions,
  366. bool includeDisabled)
  367. {
  368. try
  369. {
  370. if (!provider.Supports(item))
  371. {
  372. return false;
  373. }
  374. }
  375. catch (Exception ex)
  376. {
  377. _logger.LogError(ex, "{ProviderName} failed in Supports for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
  378. return false;
  379. }
  380. if (includeDisabled || provider is ILocalImageProvider)
  381. {
  382. return true;
  383. }
  384. if (item.IsLocked && refreshOptions.ImageRefreshMode != MetadataRefreshMode.FullRefresh)
  385. {
  386. return false;
  387. }
  388. return _baseItemManager.IsImageFetcherEnabled(item, libraryTypeOptions, provider.Name);
  389. }
  390. /// <inheritdoc />
  391. public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
  392. where T : BaseItem
  393. {
  394. var globalMetadataOptions = GetMetadataOptions(item);
  395. return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
  396. }
  397. /// <inheritdoc />
  398. public IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions)
  399. {
  400. return _savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, false));
  401. }
  402. private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
  403. where T : BaseItem
  404. {
  405. var localMetadataReaderOrder = libraryOptions.LocalMetadataReaderOrder ?? globalMetadataOptions.LocalMetadataReaderOrder;
  406. var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
  407. var metadataFetcherOrder = typeOptions?.MetadataFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder;
  408. return _metadataProviders.OfType<IMetadataProvider<T>>()
  409. .Where(i => CanRefreshMetadata(i, item, typeOptions, includeDisabled, forceEnableInternetMetadata))
  410. .OrderBy(i =>
  411. // local and remote providers will be interleaved in the final order
  412. // only relative order within a type matters: consumers of the list filter to one or the other
  413. i switch
  414. {
  415. ILocalMetadataProvider => GetConfiguredOrder(localMetadataReaderOrder, i.Name),
  416. IRemoteMetadataProvider => GetConfiguredOrder(metadataFetcherOrder, i.Name),
  417. // Default to end
  418. _ => int.MaxValue
  419. })
  420. .ThenBy(GetDefaultOrder);
  421. }
  422. private bool CanRefreshMetadata(
  423. IMetadataProvider provider,
  424. BaseItem item,
  425. TypeOptions? libraryTypeOptions,
  426. bool includeDisabled,
  427. bool forceEnableInternetMetadata)
  428. {
  429. if (!item.SupportsLocalMetadata && provider is ILocalMetadataProvider)
  430. {
  431. return false;
  432. }
  433. if (includeDisabled)
  434. {
  435. return true;
  436. }
  437. // If locked only allow local providers
  438. if (item.IsLocked && provider is not ILocalMetadataProvider && provider is not IForcedProvider)
  439. {
  440. return false;
  441. }
  442. if (forceEnableInternetMetadata || provider is not IRemoteMetadataProvider)
  443. {
  444. return true;
  445. }
  446. return _baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, provider.Name);
  447. }
  448. private static int GetConfiguredOrder(string[] order, string providerName)
  449. {
  450. var index = Array.IndexOf(order, providerName);
  451. if (index != -1)
  452. {
  453. return index;
  454. }
  455. // default to end
  456. return int.MaxValue;
  457. }
  458. private static int GetDefaultOrder(object provider)
  459. {
  460. if (provider is IHasOrder hasOrder)
  461. {
  462. return hasOrder.Order;
  463. }
  464. // after items that want to be first (~0) but before items that want to be last (~100)
  465. return 50;
  466. }
  467. /// <inheritdoc/>
  468. public MetadataPluginSummary[] GetAllMetadataPlugins()
  469. {
  470. return new[]
  471. {
  472. GetPluginSummary<Movie>(),
  473. GetPluginSummary<BoxSet>(),
  474. GetPluginSummary<Book>(),
  475. GetPluginSummary<Series>(),
  476. GetPluginSummary<Season>(),
  477. GetPluginSummary<Episode>(),
  478. GetPluginSummary<MusicAlbum>(),
  479. GetPluginSummary<MusicArtist>(),
  480. GetPluginSummary<Audio>(),
  481. GetPluginSummary<AudioBook>(),
  482. GetPluginSummary<Studio>(),
  483. GetPluginSummary<MusicVideo>(),
  484. GetPluginSummary<Video>()
  485. };
  486. }
  487. private MetadataPluginSummary GetPluginSummary<T>()
  488. where T : BaseItem, new()
  489. {
  490. // Give it a dummy path just so that it looks like a file system item
  491. var dummy = new T
  492. {
  493. Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
  494. ParentId = Guid.NewGuid()
  495. };
  496. var options = GetMetadataOptions(dummy);
  497. var summary = new MetadataPluginSummary
  498. {
  499. ItemType = typeof(T).Name
  500. };
  501. var libraryOptions = new LibraryOptions();
  502. var imageProviders = GetImageProvidersInternal(
  503. dummy,
  504. libraryOptions,
  505. options,
  506. new ImageRefreshOptions(new DirectoryService(_fileSystem)),
  507. true).ToList();
  508. var pluginList = summary.Plugins.ToList();
  509. AddMetadataPlugins(pluginList, dummy, libraryOptions, options);
  510. AddImagePlugins(pluginList, imageProviders);
  511. // Subtitle fetchers
  512. var subtitleProviders = _subtitleManager.GetSupportedProviders(dummy);
  513. pluginList.AddRange(subtitleProviders.Select(i => new MetadataPlugin
  514. {
  515. Name = i.Name,
  516. Type = MetadataPluginType.SubtitleFetcher
  517. }));
  518. // Lyric fetchers
  519. var lyricProviders = _lyricManager.GetSupportedProviders(dummy);
  520. pluginList.AddRange(lyricProviders.Select(i => new MetadataPlugin
  521. {
  522. Name = i.Name,
  523. Type = MetadataPluginType.LyricFetcher
  524. }));
  525. // Media segment providers
  526. var mediaSegmentProviders = _mediaSegmentManager.GetSupportedProviders(dummy);
  527. pluginList.AddRange(mediaSegmentProviders.Select(i => new MetadataPlugin
  528. {
  529. Name = i.Name,
  530. Type = MetadataPluginType.MediaSegmentProvider
  531. }));
  532. summary.Plugins = pluginList.ToArray();
  533. var supportedImageTypes = imageProviders.OfType<IRemoteImageProvider>()
  534. .SelectMany(i => i.GetSupportedImages(dummy))
  535. .ToList();
  536. supportedImageTypes.AddRange(imageProviders.OfType<IDynamicImageProvider>()
  537. .SelectMany(i => i.GetSupportedImages(dummy)));
  538. summary.SupportedImageTypes = supportedImageTypes.Distinct().ToArray();
  539. return summary;
  540. }
  541. private void AddMetadataPlugins<T>(List<MetadataPlugin> list, T item, LibraryOptions libraryOptions, MetadataOptions options)
  542. where T : BaseItem
  543. {
  544. var providers = GetMetadataProvidersInternal<T>(item, libraryOptions, options, true, true).ToList();
  545. // Locals
  546. list.AddRange(providers.Where(i => i is ILocalMetadataProvider).Select(i => new MetadataPlugin
  547. {
  548. Name = i.Name,
  549. Type = MetadataPluginType.LocalMetadataProvider
  550. }));
  551. // Fetchers
  552. list.AddRange(providers.Where(i => i is IRemoteMetadataProvider).Select(i => new MetadataPlugin
  553. {
  554. Name = i.Name,
  555. Type = MetadataPluginType.MetadataFetcher
  556. }));
  557. // Savers
  558. list.AddRange(_savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, true)).OrderBy(i => i.Name).Select(i => new MetadataPlugin
  559. {
  560. Name = i.Name,
  561. Type = MetadataPluginType.MetadataSaver
  562. }));
  563. }
  564. private void AddImagePlugins(List<MetadataPlugin> list, List<IImageProvider> imageProviders)
  565. {
  566. // Locals
  567. list.AddRange(imageProviders.Where(i => i is ILocalImageProvider).Select(i => new MetadataPlugin
  568. {
  569. Name = i.Name,
  570. Type = MetadataPluginType.LocalImageProvider
  571. }));
  572. // Fetchers
  573. list.AddRange(imageProviders.Where(i => i is IDynamicImageProvider || (i is IRemoteImageProvider)).Select(i => new MetadataPlugin
  574. {
  575. Name = i.Name,
  576. Type = MetadataPluginType.ImageFetcher
  577. }));
  578. }
  579. /// <inheritdoc/>
  580. public MetadataOptions GetMetadataOptions(BaseItem item)
  581. => _configurationManager.GetMetadataOptionsForType(item.GetType().Name) ?? new MetadataOptions();
  582. /// <inheritdoc/>
  583. public Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType)
  584. => SaveMetadataAsync(item, updateType, _savers);
  585. /// <inheritdoc/>
  586. public Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
  587. => SaveMetadataAsync(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparison.OrdinalIgnoreCase)));
  588. /// <summary>
  589. /// Saves the metadata.
  590. /// </summary>
  591. /// <param name="item">The item.</param>
  592. /// <param name="updateType">Type of the update.</param>
  593. /// <param name="savers">The savers.</param>
  594. private async Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
  595. {
  596. var libraryOptions = _libraryManager.GetLibraryOptions(item);
  597. foreach (var saver in savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false)))
  598. {
  599. _logger.LogDebug("Saving {Item} to {Saver}", item.Path ?? item.Name, saver.Name);
  600. if (saver is IMetadataFileSaver fileSaver)
  601. {
  602. string path;
  603. try
  604. {
  605. path = fileSaver.GetSavePath(item);
  606. }
  607. catch (Exception ex)
  608. {
  609. _logger.LogError(ex, "Error in {Saver} GetSavePath", saver.Name);
  610. continue;
  611. }
  612. try
  613. {
  614. _libraryMonitor.ReportFileSystemChangeBeginning(path);
  615. await saver.SaveAsync(item, CancellationToken.None).ConfigureAwait(false);
  616. }
  617. catch (Exception ex)
  618. {
  619. _logger.LogError(ex, "Error in metadata saver");
  620. }
  621. finally
  622. {
  623. _libraryMonitor.ReportFileSystemChangeComplete(path, false);
  624. }
  625. }
  626. else
  627. {
  628. try
  629. {
  630. await saver.SaveAsync(item, CancellationToken.None).ConfigureAwait(false);
  631. }
  632. catch (Exception ex)
  633. {
  634. _logger.LogError(ex, "Error in metadata saver");
  635. }
  636. }
  637. }
  638. }
  639. /// <summary>
  640. /// Determines whether [is saver enabled for item] [the specified saver].
  641. /// </summary>
  642. private bool IsSaverEnabledForItem(IMetadataSaver saver, BaseItem item, LibraryOptions libraryOptions, ItemUpdateType updateType, bool includeDisabled)
  643. {
  644. var options = GetMetadataOptions(item);
  645. try
  646. {
  647. if (!saver.IsEnabledFor(item, updateType))
  648. {
  649. return false;
  650. }
  651. if (!includeDisabled)
  652. {
  653. if (libraryOptions.MetadataSavers is null)
  654. {
  655. if (options.DisabledMetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase))
  656. {
  657. return false;
  658. }
  659. if (!item.IsSaveLocalMetadataEnabled())
  660. {
  661. if (updateType >= ItemUpdateType.MetadataEdit)
  662. {
  663. // Manual edit occurred
  664. // Even if save local is off, save locally anyway if the metadata file already exists
  665. if (saver is not IMetadataFileSaver fileSaver || !File.Exists(fileSaver.GetSavePath(item)))
  666. {
  667. return false;
  668. }
  669. }
  670. else
  671. {
  672. // Manual edit did not occur
  673. // Since local metadata saving is disabled, consider it disabled
  674. return false;
  675. }
  676. }
  677. }
  678. else
  679. {
  680. if (!libraryOptions.MetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase))
  681. {
  682. return false;
  683. }
  684. }
  685. }
  686. return true;
  687. }
  688. catch (Exception ex)
  689. {
  690. _logger.LogError(ex, "Error in {Saver}.IsEnabledFor", saver.Name);
  691. return false;
  692. }
  693. }
  694. /// <inheritdoc/>
  695. public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, CancellationToken cancellationToken)
  696. where TItemType : BaseItem, new()
  697. where TLookupType : ItemLookupInfo
  698. {
  699. BaseItem? referenceItem = null;
  700. if (!searchInfo.ItemId.IsEmpty())
  701. {
  702. referenceItem = _libraryManager.GetItemById(searchInfo.ItemId);
  703. }
  704. return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken);
  705. }
  706. private async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem? referenceItem, CancellationToken cancellationToken)
  707. where TItemType : BaseItem, new()
  708. where TLookupType : ItemLookupInfo
  709. {
  710. LibraryOptions libraryOptions;
  711. if (referenceItem is null)
  712. {
  713. // Give it a dummy path just so that it looks like a file system item
  714. var dummy = new TItemType
  715. {
  716. Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
  717. ParentId = Guid.NewGuid()
  718. };
  719. dummy.SetParent(new Folder());
  720. referenceItem = dummy;
  721. libraryOptions = new LibraryOptions();
  722. }
  723. else
  724. {
  725. libraryOptions = _libraryManager.GetLibraryOptions(referenceItem);
  726. }
  727. var options = GetMetadataOptions(referenceItem);
  728. var providers = GetMetadataProvidersInternal<TItemType>(referenceItem, libraryOptions, options, searchInfo.IncludeDisabledProviders, false)
  729. .OfType<IRemoteSearchProvider<TLookupType>>();
  730. if (!string.IsNullOrEmpty(searchInfo.SearchProviderName))
  731. {
  732. providers = providers.Where(i => string.Equals(i.Name, searchInfo.SearchProviderName, StringComparison.OrdinalIgnoreCase));
  733. }
  734. if (string.IsNullOrWhiteSpace(searchInfo.SearchInfo.MetadataLanguage))
  735. {
  736. searchInfo.SearchInfo.MetadataLanguage = _configurationManager.Configuration.PreferredMetadataLanguage;
  737. }
  738. if (string.IsNullOrWhiteSpace(searchInfo.SearchInfo.MetadataCountryCode))
  739. {
  740. searchInfo.SearchInfo.MetadataCountryCode = _configurationManager.Configuration.MetadataCountryCode;
  741. }
  742. var resultList = new List<RemoteSearchResult>();
  743. foreach (var provider in providers)
  744. {
  745. try
  746. {
  747. var results = await provider.GetSearchResults(searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false);
  748. foreach (var result in results)
  749. {
  750. result.SearchProviderName = provider.Name;
  751. var existingMatch = resultList.FirstOrDefault(i => i.ProviderIds.Any(p => string.Equals(result.GetProviderId(p.Key), p.Value, StringComparison.OrdinalIgnoreCase)));
  752. if (existingMatch is null)
  753. {
  754. resultList.Add(result);
  755. }
  756. else
  757. {
  758. foreach (var providerId in result.ProviderIds)
  759. {
  760. existingMatch.ProviderIds.TryAdd(providerId.Key, providerId.Value);
  761. }
  762. if (string.IsNullOrWhiteSpace(existingMatch.ImageUrl))
  763. {
  764. existingMatch.ImageUrl = result.ImageUrl;
  765. }
  766. }
  767. }
  768. }
  769. #pragma warning disable CA1031 // do not catch general exception types
  770. catch (Exception ex)
  771. #pragma warning restore CA1031 // do not catch general exception types
  772. {
  773. _logger.LogError(ex, "Provider {ProviderName} failed to retrieve search results", provider.Name);
  774. }
  775. }
  776. return resultList;
  777. }
  778. private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
  779. {
  780. return _externalIds.Where(i =>
  781. {
  782. try
  783. {
  784. return i.Supports(item);
  785. }
  786. catch (Exception ex)
  787. {
  788. _logger.LogError(ex, "Error in {Type}.Supports", i.GetType().Name);
  789. return false;
  790. }
  791. });
  792. }
  793. /// <inheritdoc/>
  794. public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
  795. {
  796. #pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
  797. var legacyExternalIdUrls = GetExternalIds(item)
  798. .Select(i =>
  799. {
  800. var urlFormatString = i.UrlFormatString;
  801. if (string.IsNullOrEmpty(urlFormatString)
  802. || !item.TryGetProviderId(i.Key, out var providerId))
  803. {
  804. return null;
  805. }
  806. return new ExternalUrl
  807. {
  808. Name = i.ProviderName,
  809. Url = string.Format(
  810. CultureInfo.InvariantCulture,
  811. urlFormatString,
  812. providerId)
  813. };
  814. })
  815. .OfType<ExternalUrl>();
  816. #pragma warning restore CS0618 // Type or member is obsolete
  817. var externalUrls = _externalUrlProviders
  818. .SelectMany(p => p
  819. .GetExternalUrls(item)
  820. .Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl }));
  821. return legacyExternalIdUrls.Concat(externalUrls).OrderBy(u => u.Name);
  822. }
  823. /// <inheritdoc/>
  824. public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item)
  825. {
  826. return GetExternalIds(item)
  827. .Select(i => new ExternalIdInfo(
  828. name: i.ProviderName,
  829. key: i.Key,
  830. type: i.Type,
  831. #pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
  832. urlFormatString: i.UrlFormatString));
  833. #pragma warning restore CS0618 // Type or member is obsolete
  834. }
  835. /// <inheritdoc/>
  836. public HashSet<Guid> GetRefreshQueue()
  837. {
  838. lock (_refreshQueueLock)
  839. {
  840. return _refreshQueue.UnorderedItems.Select(x => x.Element.ItemId).ToHashSet();
  841. }
  842. }
  843. /// <inheritdoc/>
  844. public void OnRefreshStart(BaseItem item)
  845. {
  846. _logger.LogDebug("OnRefreshStart {Item:N}", item.Id);
  847. _activeRefreshes[item.Id] = 0;
  848. try
  849. {
  850. RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
  851. }
  852. catch (Exception ex)
  853. {
  854. // EventHandlers should never propagate exceptions, but we have little control over plugins...
  855. _logger.LogError(ex, "Invoking {RefreshEvent} event handlers failed", nameof(RefreshStarted));
  856. }
  857. }
  858. /// <inheritdoc/>
  859. public void OnRefreshComplete(BaseItem item)
  860. {
  861. _logger.LogDebug("OnRefreshComplete {Item:N}", item.Id);
  862. _activeRefreshes.TryRemove(item.Id, out _);
  863. try
  864. {
  865. RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
  866. }
  867. catch (Exception ex)
  868. {
  869. // EventHandlers should never propagate exceptions, but we have little control over plugins...
  870. _logger.LogError(ex, "Invoking {RefreshEvent} event handlers failed", nameof(RefreshCompleted));
  871. }
  872. }
  873. /// <inheritdoc/>
  874. public double? GetRefreshProgress(Guid id)
  875. {
  876. if (_activeRefreshes.TryGetValue(id, out double value))
  877. {
  878. return value;
  879. }
  880. return null;
  881. }
  882. /// <inheritdoc/>
  883. public void OnRefreshProgress(BaseItem item, double progress)
  884. {
  885. var id = item.Id;
  886. _logger.LogDebug("OnRefreshProgress {Id:N} {Progress}", id, progress);
  887. if (!_activeRefreshes.TryGetValue(id, out var current)
  888. || progress <= current
  889. || !_activeRefreshes.TryUpdate(id, progress, current))
  890. {
  891. // Item isn't currently refreshing, or update was received out-of-order, so don't trigger event.
  892. return;
  893. }
  894. try
  895. {
  896. RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
  897. }
  898. catch (Exception ex)
  899. {
  900. // EventHandlers should never propagate exceptions, but we have little control over plugins...
  901. _logger.LogError(ex, "Invoking {RefreshEvent} event handlers failed", nameof(RefreshProgress));
  902. }
  903. }
  904. /// <inheritdoc/>
  905. public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
  906. {
  907. if (itemId.IsEmpty())
  908. {
  909. throw new ArgumentException("Guid can't be empty", nameof(itemId));
  910. }
  911. if (_disposed)
  912. {
  913. return;
  914. }
  915. _refreshQueue.Enqueue((itemId, options), priority);
  916. lock (_refreshQueueLock)
  917. {
  918. if (!_isProcessingRefreshQueue)
  919. {
  920. _isProcessingRefreshQueue = true;
  921. Task.Run(StartProcessingRefreshQueue);
  922. }
  923. }
  924. }
  925. private async Task StartProcessingRefreshQueue()
  926. {
  927. var libraryManager = _libraryManager;
  928. if (_disposed)
  929. {
  930. return;
  931. }
  932. var cancellationToken = _disposeCancellationTokenSource.Token;
  933. while (_refreshQueue.TryDequeue(out var refreshItem, out _))
  934. {
  935. if (_disposed)
  936. {
  937. return;
  938. }
  939. try
  940. {
  941. var item = libraryManager.GetItemById(refreshItem.ItemId);
  942. if (item is null)
  943. {
  944. continue;
  945. }
  946. var task = item is MusicArtist artist
  947. ? RefreshArtist(artist, refreshItem.RefreshOptions, cancellationToken)
  948. : RefreshItem(item, refreshItem.RefreshOptions, cancellationToken);
  949. await task.ConfigureAwait(false);
  950. }
  951. catch (OperationCanceledException)
  952. {
  953. break;
  954. }
  955. catch (Exception ex)
  956. {
  957. _logger.LogError(ex, "Error refreshing item");
  958. }
  959. }
  960. lock (_refreshQueueLock)
  961. {
  962. _isProcessingRefreshQueue = false;
  963. }
  964. }
  965. private async Task RefreshItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
  966. {
  967. await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
  968. // Collection folders don't validate their children so we'll have to simulate that here
  969. switch (item)
  970. {
  971. case CollectionFolder collectionFolder:
  972. await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
  973. break;
  974. case Folder folder:
  975. await folder.ValidateChildren(new Progress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
  976. break;
  977. }
  978. }
  979. private async Task RefreshCollectionFolderChildren(MetadataRefreshOptions options, CollectionFolder collectionFolder, CancellationToken cancellationToken)
  980. {
  981. foreach (var child in collectionFolder.GetPhysicalFolders())
  982. {
  983. await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
  984. await child.ValidateChildren(new Progress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
  985. }
  986. }
  987. private async Task RefreshArtist(MusicArtist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
  988. {
  989. var albums = _libraryManager
  990. .GetItemList(new InternalItemsQuery
  991. {
  992. IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
  993. ArtistIds = new[] { item.Id },
  994. DtoOptions = new DtoOptions(false)
  995. {
  996. EnableImages = false
  997. }
  998. })
  999. .OfType<MusicAlbum>();
  1000. var musicArtists = albums
  1001. .Select(i => i.MusicArtist)
  1002. .Where(i => i is not null)
  1003. .Distinct();
  1004. var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), options, true, false, cancellationToken));
  1005. await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false);
  1006. try
  1007. {
  1008. await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
  1009. }
  1010. catch (Exception ex)
  1011. {
  1012. _logger.LogError(ex, "Error refreshing library");
  1013. }
  1014. }
  1015. /// <inheritdoc/>
  1016. public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
  1017. {
  1018. return RefreshItem(item, options, cancellationToken);
  1019. }
  1020. /// <inheritdoc/>
  1021. public void Dispose()
  1022. {
  1023. Dispose(true);
  1024. GC.SuppressFinalize(this);
  1025. }
  1026. /// <summary>
  1027. /// Releases unmanaged and optionally managed resources.
  1028. /// </summary>
  1029. /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  1030. protected virtual void Dispose(bool disposing)
  1031. {
  1032. if (_disposed)
  1033. {
  1034. return;
  1035. }
  1036. if (disposing)
  1037. {
  1038. if (!_disposeCancellationTokenSource.IsCancellationRequested)
  1039. {
  1040. _disposeCancellationTokenSource.Cancel();
  1041. }
  1042. _disposeCancellationTokenSource.Dispose();
  1043. _imageSaveLock.Dispose();
  1044. }
  1045. _disposed = true;
  1046. }
  1047. }
  1048. }