ItemImageProvider.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. #nullable disable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Net.Http;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. using MediaBrowser.Controller.Entities;
  11. using MediaBrowser.Controller.Entities.Audio;
  12. using MediaBrowser.Controller.Library;
  13. using MediaBrowser.Controller.LiveTv;
  14. using MediaBrowser.Controller.Providers;
  15. using MediaBrowser.Model.Configuration;
  16. using MediaBrowser.Model.Drawing;
  17. using MediaBrowser.Model.Entities;
  18. using MediaBrowser.Model.IO;
  19. using MediaBrowser.Model.MediaInfo;
  20. using MediaBrowser.Model.Net;
  21. using MediaBrowser.Model.Providers;
  22. using Microsoft.Extensions.Logging;
  23. namespace MediaBrowser.Providers.Manager
  24. {
  25. /// <summary>
  26. /// Utilities for managing images attached to items.
  27. /// </summary>
  28. public class ItemImageProvider
  29. {
  30. private readonly ILogger _logger;
  31. private readonly IProviderManager _providerManager;
  32. private readonly IFileSystem _fileSystem;
  33. private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
  34. /// <summary>
  35. /// Image types that are only one per item.
  36. /// </summary>
  37. private static readonly ImageType[] _singularImages =
  38. {
  39. ImageType.Primary,
  40. ImageType.Art,
  41. ImageType.Banner,
  42. ImageType.Box,
  43. ImageType.BoxRear,
  44. ImageType.Disc,
  45. ImageType.Logo,
  46. ImageType.Menu,
  47. ImageType.Thumb
  48. };
  49. /// <summary>
  50. /// Initializes a new instance of the <see cref="ItemImageProvider"/> class.
  51. /// </summary>
  52. /// <param name="logger">The logger.</param>
  53. /// <param name="providerManager">The provider manager for interacting with provider image references.</param>
  54. /// <param name="fileSystem">The filesystem.</param>
  55. public ItemImageProvider(ILogger logger, IProviderManager providerManager, IFileSystem fileSystem)
  56. {
  57. _logger = logger;
  58. _providerManager = providerManager;
  59. _fileSystem = fileSystem;
  60. }
  61. /// <summary>
  62. /// Removes all existing images from the provided item.
  63. /// </summary>
  64. /// <param name="item">The <see cref="BaseItem"/> to remove images from.</param>
  65. /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
  66. public bool RemoveImages(BaseItem item)
  67. {
  68. var singular = new List<ItemImageInfo>();
  69. for (var i = 0; i < _singularImages.Length; i++)
  70. {
  71. var currentImage = item.GetImageInfo(_singularImages[i], 0);
  72. if (currentImage is not null)
  73. {
  74. singular.Add(currentImage);
  75. }
  76. }
  77. singular.AddRange(item.GetImages(ImageType.Backdrop));
  78. PruneImages(item, singular);
  79. return singular.Count > 0;
  80. }
  81. /// <summary>
  82. /// Verifies existing images have valid paths and adds any new local images provided.
  83. /// </summary>
  84. /// <param name="item">The <see cref="BaseItem"/> to validate images for.</param>
  85. /// <param name="providers">The providers to use, must include <see cref="ILocalImageProvider"/>(s) for local scanning.</param>
  86. /// <param name="refreshOptions">The refresh options.</param>
  87. /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
  88. public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
  89. {
  90. var hasChanges = false;
  91. IDirectoryService directoryService = refreshOptions?.DirectoryService;
  92. if (item is not Photo)
  93. {
  94. var images = providers.OfType<ILocalImageProvider>()
  95. .SelectMany(i => i.GetImages(item, directoryService))
  96. .ToList();
  97. if (MergeImages(item, images, refreshOptions))
  98. {
  99. hasChanges = true;
  100. }
  101. }
  102. return hasChanges;
  103. }
  104. /// <summary>
  105. /// Refreshes from the providers according to the given options.
  106. /// </summary>
  107. /// <param name="item">The <see cref="BaseItem"/> to gather images for.</param>
  108. /// <param name="libraryOptions">The library options.</param>
  109. /// <param name="providers">The providers to query for images.</param>
  110. /// <param name="refreshOptions">The refresh options.</param>
  111. /// <param name="cancellationToken">The cancellation token.</param>
  112. /// <returns>The refresh result.</returns>
  113. public async Task<RefreshResult> RefreshImages(
  114. BaseItem item,
  115. LibraryOptions libraryOptions,
  116. IEnumerable<IImageProvider> providers,
  117. ImageRefreshOptions refreshOptions,
  118. CancellationToken cancellationToken)
  119. {
  120. var oldBackdropImages = Array.Empty<ItemImageInfo>();
  121. if (refreshOptions.IsReplacingImage(ImageType.Backdrop))
  122. {
  123. oldBackdropImages = item.GetImages(ImageType.Backdrop).ToArray();
  124. }
  125. var result = new RefreshResult { UpdateType = ItemUpdateType.None };
  126. var typeName = item.GetType().Name;
  127. var typeOptions = libraryOptions.GetTypeOptions(typeName) ?? new TypeOptions { Type = typeName };
  128. // track library limits, adding buffer to allow lazy replacing of current images
  129. var backdropLimit = typeOptions.GetLimit(ImageType.Backdrop) + oldBackdropImages.Length;
  130. var downloadedImages = new List<ImageType>();
  131. foreach (var provider in providers)
  132. {
  133. if (provider is IRemoteImageProvider remoteProvider)
  134. {
  135. await RefreshFromProvider(item, remoteProvider, refreshOptions, typeOptions, backdropLimit, downloadedImages, result, cancellationToken).ConfigureAwait(false);
  136. continue;
  137. }
  138. if (provider is IDynamicImageProvider dynamicImageProvider)
  139. {
  140. await RefreshFromProvider(item, dynamicImageProvider, refreshOptions, typeOptions, downloadedImages, result, cancellationToken).ConfigureAwait(false);
  141. }
  142. }
  143. // only delete existing multi-images if new ones were added
  144. if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
  145. {
  146. PruneImages(item, oldBackdropImages);
  147. }
  148. return result;
  149. }
  150. /// <summary>
  151. /// Refreshes from a dynamic provider.
  152. /// </summary>
  153. private async Task RefreshFromProvider(
  154. BaseItem item,
  155. IDynamicImageProvider provider,
  156. ImageRefreshOptions refreshOptions,
  157. TypeOptions savedOptions,
  158. List<ImageType> downloadedImages,
  159. RefreshResult result,
  160. CancellationToken cancellationToken)
  161. {
  162. try
  163. {
  164. var images = provider.GetSupportedImages(item);
  165. foreach (var imageType in images)
  166. {
  167. if (!savedOptions.IsEnabled(imageType))
  168. {
  169. continue;
  170. }
  171. if (!item.HasImage(imageType) || (refreshOptions.IsReplacingImage(imageType) && !downloadedImages.Contains(imageType)))
  172. {
  173. _logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, item.Path ?? item.Name);
  174. var response = await provider.GetImage(item, imageType, cancellationToken).ConfigureAwait(false);
  175. if (response.HasImage)
  176. {
  177. if (string.IsNullOrEmpty(response.Path))
  178. {
  179. var mimeType = response.Format.GetMimeType();
  180. await _providerManager.SaveImage(item, response.Stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
  181. }
  182. else
  183. {
  184. if (response.Protocol == MediaProtocol.Http)
  185. {
  186. _logger.LogDebug("Setting image url into item {Item}", item.Id);
  187. var index = item.AllowsMultipleImages(imageType) ? item.GetImages(imageType).Count() : 0;
  188. item.SetImage(
  189. new ItemImageInfo
  190. {
  191. Path = response.Path,
  192. Type = imageType
  193. },
  194. index);
  195. }
  196. else
  197. {
  198. var mimeType = MimeTypes.GetMimeType(response.Path);
  199. var stream = AsyncFile.OpenRead(response.Path);
  200. await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
  201. }
  202. }
  203. downloadedImages.Add(imageType);
  204. result.UpdateType |= ItemUpdateType.ImageUpdate;
  205. }
  206. }
  207. }
  208. }
  209. catch (OperationCanceledException)
  210. {
  211. throw;
  212. }
  213. catch (Exception ex)
  214. {
  215. result.ErrorMessage = ex.Message;
  216. _logger.LogError(ex, "Error in {Provider}", provider.Name);
  217. }
  218. }
  219. /// <summary>
  220. /// Refreshes from a remote provider.
  221. /// </summary>
  222. /// <param name="item">The item.</param>
  223. /// <param name="provider">The provider.</param>
  224. /// <param name="refreshOptions">The refresh options.</param>
  225. /// <param name="savedOptions">The saved options.</param>
  226. /// <param name="backdropLimit">The backdrop limit.</param>
  227. /// <param name="downloadedImages">The downloaded images.</param>
  228. /// <param name="result">The result.</param>
  229. /// <param name="cancellationToken">The cancellation token.</param>
  230. /// <returns>Task.</returns>
  231. private async Task RefreshFromProvider(
  232. BaseItem item,
  233. IRemoteImageProvider provider,
  234. ImageRefreshOptions refreshOptions,
  235. TypeOptions savedOptions,
  236. int backdropLimit,
  237. List<ImageType> downloadedImages,
  238. RefreshResult result,
  239. CancellationToken cancellationToken)
  240. {
  241. try
  242. {
  243. if (!item.SupportsRemoteImageDownloading)
  244. {
  245. return;
  246. }
  247. if (!refreshOptions.ReplaceAllImages &&
  248. refreshOptions.ReplaceImages.Count == 0 &&
  249. ContainsImages(item, provider.GetSupportedImages(item).ToList(), savedOptions, backdropLimit))
  250. {
  251. return;
  252. }
  253. _logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, item.Path ?? item.Name);
  254. var images = await _providerManager.GetAvailableRemoteImages(
  255. item,
  256. new RemoteImageQuery(provider.Name)
  257. {
  258. IncludeAllLanguages = true,
  259. IncludeDisabledProviders = false,
  260. },
  261. cancellationToken).ConfigureAwait(false);
  262. var list = images.ToList();
  263. int minWidth;
  264. foreach (var imageType in _singularImages)
  265. {
  266. if (!savedOptions.IsEnabled(imageType))
  267. {
  268. continue;
  269. }
  270. if (!item.HasImage(imageType) || (refreshOptions.IsReplacingImage(imageType) && !downloadedImages.Contains(imageType)))
  271. {
  272. minWidth = savedOptions.GetMinWidth(imageType);
  273. var downloaded = await DownloadImage(item, provider, result, list, minWidth, imageType, cancellationToken).ConfigureAwait(false);
  274. if (downloaded)
  275. {
  276. downloadedImages.Add(imageType);
  277. }
  278. }
  279. }
  280. minWidth = savedOptions.GetMinWidth(ImageType.Backdrop);
  281. var listWithNoLangFirst = list.OrderByDescending(i => string.IsNullOrEmpty(i.Language));
  282. await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, listWithNoLangFirst, minWidth, cancellationToken).ConfigureAwait(false);
  283. }
  284. catch (OperationCanceledException)
  285. {
  286. throw;
  287. }
  288. catch (Exception ex)
  289. {
  290. result.ErrorMessage = ex.Message;
  291. _logger.LogError(ex, "Error in {Provider}", provider.Name);
  292. }
  293. }
  294. /// <summary>
  295. /// Determines if an item already contains the given images.
  296. /// </summary>
  297. /// <param name="item">The item.</param>
  298. /// <param name="images">The images.</param>
  299. /// <param name="savedOptions">The saved options.</param>
  300. /// <param name="backdropLimit">The backdrop limit.</param>
  301. /// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns>
  302. private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit)
  303. {
  304. // Using .Any causes the creation of a DisplayClass aka. variable capture
  305. for (var i = 0; i < _singularImages.Length; i++)
  306. {
  307. var type = _singularImages[i];
  308. if (images.Contains(type) && !item.HasImage(type) && savedOptions.GetLimit(type) > 0)
  309. {
  310. return false;
  311. }
  312. }
  313. if (images.Contains(ImageType.Backdrop) && item.GetImages(ImageType.Backdrop).Count() < backdropLimit)
  314. {
  315. return false;
  316. }
  317. return true;
  318. }
  319. private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
  320. {
  321. for (var i = 0; i < images.Count; i++)
  322. {
  323. var image = images[i];
  324. if (image.IsLocalFile)
  325. {
  326. try
  327. {
  328. _fileSystem.DeleteFile(image.Path);
  329. }
  330. catch (FileNotFoundException)
  331. {
  332. // nothing to do, already gone
  333. }
  334. catch (UnauthorizedAccessException ex)
  335. {
  336. _logger.LogWarning(ex, "Unable to delete {Image}", image.Path);
  337. }
  338. }
  339. }
  340. item.RemoveImages(images);
  341. }
  342. /// <summary>
  343. /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary.
  344. /// </summary>
  345. /// <param name="refreshOptions">The refresh options.</param>
  346. /// <param name="dontReplaceImages">List of imageTypes to remove from ReplaceImages.</param>
  347. public void UpdateReplaceImages(ImageRefreshOptions refreshOptions, ICollection<ImageType> dontReplaceImages)
  348. {
  349. if (refreshOptions is not null)
  350. {
  351. if (refreshOptions.ReplaceAllImages)
  352. {
  353. refreshOptions.ReplaceAllImages = false;
  354. refreshOptions.ReplaceImages = AllImageTypes.ToList();
  355. }
  356. refreshOptions.ReplaceImages = refreshOptions.ReplaceImages.Except(dontReplaceImages).ToList();
  357. }
  358. }
  359. /// <summary>
  360. /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary.
  361. /// </summary>
  362. /// <param name="item">The <see cref="BaseItem"/> to modify.</param>
  363. /// <param name="images">The new images to place in <c>item</c>.</param>
  364. /// <param name="refreshOptions">The refresh options.</param>
  365. /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
  366. public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images, ImageRefreshOptions refreshOptions)
  367. {
  368. var changed = item.ValidateImages();
  369. var foundImageTypes = new List<ImageType>();
  370. for (var i = 0; i < _singularImages.Length; i++)
  371. {
  372. var type = _singularImages[i];
  373. var image = GetFirstLocalImageInfoByType(images, type);
  374. if (image is not null)
  375. {
  376. var currentImage = item.GetImageInfo(type, 0);
  377. // if image file is stored with media, don't replace that later
  378. if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase))
  379. {
  380. foundImageTypes.Add(type);
  381. }
  382. if (currentImage is null || !string.Equals(currentImage.Path, image.FileInfo.FullName, StringComparison.OrdinalIgnoreCase))
  383. {
  384. item.SetImagePath(type, image.FileInfo);
  385. changed = true;
  386. }
  387. else
  388. {
  389. var newDateModified = _fileSystem.GetLastWriteTimeUtc(image.FileInfo);
  390. // If date changed then we need to reset saved image dimensions
  391. if (currentImage.DateModified != newDateModified && (currentImage.Width > 0 || currentImage.Height > 0))
  392. {
  393. currentImage.Width = 0;
  394. currentImage.Height = 0;
  395. changed = true;
  396. }
  397. currentImage.DateModified = newDateModified;
  398. }
  399. }
  400. }
  401. if (UpdateMultiImages(item, images, ImageType.Backdrop))
  402. {
  403. changed = true;
  404. foundImageTypes.Add(ImageType.Backdrop);
  405. }
  406. if (foundImageTypes.Count > 0)
  407. {
  408. UpdateReplaceImages(refreshOptions, foundImageTypes);
  409. }
  410. return changed;
  411. }
  412. private static LocalImageInfo GetFirstLocalImageInfoByType(IReadOnlyList<LocalImageInfo> images, ImageType type)
  413. {
  414. var len = images.Count;
  415. for (var i = 0; i < len; i++)
  416. {
  417. var image = images[i];
  418. if (image.Type == type)
  419. {
  420. return image;
  421. }
  422. }
  423. return null;
  424. }
  425. private bool UpdateMultiImages(BaseItem item, IReadOnlyList<LocalImageInfo> images, ImageType type)
  426. {
  427. var changed = false;
  428. var newImageFileInfos = images
  429. .Where(i => i.Type == type)
  430. .Select(i => i.FileInfo)
  431. .ToList();
  432. if (item.AddImages(type, newImageFileInfos))
  433. {
  434. changed = true;
  435. }
  436. return changed;
  437. }
  438. private async Task<bool> DownloadImage(
  439. BaseItem item,
  440. IRemoteImageProvider provider,
  441. RefreshResult result,
  442. IEnumerable<RemoteImageInfo> images,
  443. int minWidth,
  444. ImageType type,
  445. CancellationToken cancellationToken)
  446. {
  447. var eligibleImages = images
  448. .Where(i => i.Type == type && (i.Width is null || i.Width >= minWidth))
  449. .ToList();
  450. if (EnableImageStub(item) && eligibleImages.Count > 0)
  451. {
  452. SaveImageStub(item, type, eligibleImages.Select(i => i.Url));
  453. result.UpdateType |= ItemUpdateType.ImageUpdate;
  454. return true;
  455. }
  456. foreach (var image in eligibleImages)
  457. {
  458. var url = image.Url;
  459. try
  460. {
  461. using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
  462. // Sometimes providers send back bad urls. Just move to the next image
  463. if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
  464. {
  465. _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
  466. continue;
  467. }
  468. if (!response.IsSuccessStatusCode)
  469. {
  470. _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
  471. break;
  472. }
  473. var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
  474. await using (stream.ConfigureAwait(false))
  475. {
  476. await _providerManager.SaveImage(
  477. item,
  478. stream,
  479. response.Content.Headers.ContentType?.MediaType,
  480. type,
  481. null,
  482. cancellationToken).ConfigureAwait(false);
  483. }
  484. result.UpdateType |= ItemUpdateType.ImageUpdate;
  485. return true;
  486. }
  487. catch (HttpRequestException)
  488. {
  489. break;
  490. }
  491. }
  492. return false;
  493. }
  494. private bool EnableImageStub(BaseItem item)
  495. {
  496. if (item is LiveTvProgram)
  497. {
  498. return true;
  499. }
  500. if (!item.IsFileProtocol)
  501. {
  502. return true;
  503. }
  504. if (item is IItemByName and not MusicArtist)
  505. {
  506. var hasDualAccess = item as IHasDualAccess;
  507. if (hasDualAccess is null || hasDualAccess.IsAccessedByName)
  508. {
  509. return true;
  510. }
  511. }
  512. // We always want to use prefetched images
  513. return false;
  514. }
  515. private void SaveImageStub(BaseItem item, ImageType imageType, IEnumerable<string> urls)
  516. {
  517. var newIndex = item.AllowsMultipleImages(imageType) ? item.GetImages(imageType).Count() : 0;
  518. SaveImageStub(item, imageType, urls, newIndex);
  519. }
  520. private void SaveImageStub(BaseItem item, ImageType imageType, IEnumerable<string> urls, int newIndex)
  521. {
  522. var path = string.Join('|', urls.Take(1));
  523. item.SetImage(
  524. new ItemImageInfo
  525. {
  526. Path = path,
  527. Type = imageType
  528. },
  529. newIndex);
  530. }
  531. private async Task DownloadMultiImages(BaseItem item, ImageType imageType, ImageRefreshOptions refreshOptions, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken)
  532. {
  533. foreach (var image in images.Where(i => i.Type == imageType))
  534. {
  535. if (item.GetImages(imageType).Count() >= limit)
  536. {
  537. break;
  538. }
  539. if (image.Width.HasValue && image.Width.Value < minWidth)
  540. {
  541. continue;
  542. }
  543. var url = image.Url;
  544. if (EnableImageStub(item))
  545. {
  546. SaveImageStub(item, imageType, new[] { url });
  547. result.UpdateType |= ItemUpdateType.ImageUpdate;
  548. continue;
  549. }
  550. try
  551. {
  552. using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
  553. // Sometimes providers send back bad urls. Just move to the next image
  554. if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
  555. {
  556. _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
  557. continue;
  558. }
  559. if (!response.IsSuccessStatusCode)
  560. {
  561. _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
  562. break;
  563. }
  564. // If there's already an image of the same file size, skip it unless doing a full refresh
  565. if (response.Content.Headers.ContentLength.HasValue && !refreshOptions.IsReplacingImage(imageType))
  566. {
  567. try
  568. {
  569. if (item.GetImages(imageType).Any(i => _fileSystem.GetFileInfo(i.Path).Length == response.Content.Headers.ContentLength.Value))
  570. {
  571. response.Content.Dispose();
  572. continue;
  573. }
  574. }
  575. catch (IOException ex)
  576. {
  577. _logger.LogError(ex, "Error examining images");
  578. }
  579. }
  580. var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
  581. await using (stream.ConfigureAwait(false))
  582. {
  583. await _providerManager.SaveImage(
  584. item,
  585. stream,
  586. response.Content.Headers.ContentType?.MediaType,
  587. imageType,
  588. null,
  589. cancellationToken).ConfigureAwait(false);
  590. }
  591. result.UpdateType |= ItemUpdateType.ImageUpdate;
  592. }
  593. catch (HttpRequestException)
  594. {
  595. break;
  596. }
  597. }
  598. }
  599. }
  600. }