ProviderManager.cs 48 KB

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