GuideManager.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using Jellyfin.Data.Entities.Libraries;
  7. using Jellyfin.Data.Enums;
  8. using Jellyfin.Extensions;
  9. using Jellyfin.LiveTv.Configuration;
  10. using MediaBrowser.Common.Configuration;
  11. using MediaBrowser.Controller.Dto;
  12. using MediaBrowser.Controller.Entities;
  13. using MediaBrowser.Controller.Library;
  14. using MediaBrowser.Controller.LiveTv;
  15. using MediaBrowser.Controller.Persistence;
  16. using MediaBrowser.Controller.Providers;
  17. using MediaBrowser.Model.Entities;
  18. using MediaBrowser.Model.IO;
  19. using MediaBrowser.Model.LiveTv;
  20. using Microsoft.Extensions.Logging;
  21. namespace Jellyfin.LiveTv.Guide;
  22. /// <inheritdoc />
  23. public class GuideManager : IGuideManager
  24. {
  25. private const int MaxGuideDays = 14;
  26. private const string EtagKey = "ProgramEtag";
  27. private const string ExternalServiceTag = "ExternalServiceId";
  28. private static readonly ParallelOptions _cacheParallelOptions = new() { MaxDegreeOfParallelism = Math.Min(Environment.ProcessorCount, 10) };
  29. private readonly ILogger<GuideManager> _logger;
  30. private readonly IConfigurationManager _config;
  31. private readonly IFileSystem _fileSystem;
  32. private readonly IItemRepository _itemRepo;
  33. private readonly ILibraryManager _libraryManager;
  34. private readonly ILiveTvManager _liveTvManager;
  35. private readonly ITunerHostManager _tunerHostManager;
  36. private readonly IRecordingsManager _recordingsManager;
  37. private readonly LiveTvDtoService _tvDtoService;
  38. /// <summary>
  39. /// Amount of days images are pre-cached from external sources.
  40. /// </summary>
  41. public const int MaxCacheDays = 2;
  42. /// <summary>
  43. /// Initializes a new instance of the <see cref="GuideManager"/> class.
  44. /// </summary>
  45. /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
  46. /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
  47. /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
  48. /// <param name="itemRepo">The <see cref="IItemRepository"/>.</param>
  49. /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
  50. /// <param name="liveTvManager">The <see cref="ILiveTvManager"/>.</param>
  51. /// <param name="tunerHostManager">The <see cref="ITunerHostManager"/>.</param>
  52. /// <param name="recordingsManager">The <see cref="IRecordingsManager"/>.</param>
  53. /// <param name="tvDtoService">The <see cref="LiveTvDtoService"/>.</param>
  54. public GuideManager(
  55. ILogger<GuideManager> logger,
  56. IConfigurationManager config,
  57. IFileSystem fileSystem,
  58. IItemRepository itemRepo,
  59. ILibraryManager libraryManager,
  60. ILiveTvManager liveTvManager,
  61. ITunerHostManager tunerHostManager,
  62. IRecordingsManager recordingsManager,
  63. LiveTvDtoService tvDtoService)
  64. {
  65. _logger = logger;
  66. _config = config;
  67. _fileSystem = fileSystem;
  68. _itemRepo = itemRepo;
  69. _libraryManager = libraryManager;
  70. _liveTvManager = liveTvManager;
  71. _tunerHostManager = tunerHostManager;
  72. _recordingsManager = recordingsManager;
  73. _tvDtoService = tvDtoService;
  74. }
  75. /// <inheritdoc />
  76. public GuideInfo GetGuideInfo()
  77. {
  78. var startDate = DateTime.UtcNow;
  79. var endDate = startDate.AddDays(GetGuideDays());
  80. return new GuideInfo
  81. {
  82. StartDate = startDate,
  83. EndDate = endDate
  84. };
  85. }
  86. /// <inheritdoc />
  87. public async Task RefreshGuide(IProgress<double> progress, CancellationToken cancellationToken)
  88. {
  89. ArgumentNullException.ThrowIfNull(progress);
  90. await _recordingsManager.CreateRecordingFolders().ConfigureAwait(false);
  91. await _tunerHostManager.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
  92. var numComplete = 0;
  93. double progressPerService = _liveTvManager.Services.Count == 0
  94. ? 0
  95. : 1.0 / _liveTvManager.Services.Count;
  96. var newChannelIdList = new List<Guid>();
  97. var newProgramIdList = new List<Guid>();
  98. var cleanDatabase = true;
  99. foreach (var service in _liveTvManager.Services)
  100. {
  101. cancellationToken.ThrowIfCancellationRequested();
  102. _logger.LogDebug("Refreshing guide from {Name}", service.Name);
  103. try
  104. {
  105. var innerProgress = new Progress<double>(p => progress.Report(p * progressPerService));
  106. var idList = await RefreshChannelsInternal(service, innerProgress, cancellationToken).ConfigureAwait(false);
  107. newChannelIdList.AddRange(idList.Item1);
  108. newProgramIdList.AddRange(idList.Item2);
  109. }
  110. catch (OperationCanceledException)
  111. {
  112. throw;
  113. }
  114. catch (Exception ex)
  115. {
  116. cleanDatabase = false;
  117. _logger.LogError(ex, "Error refreshing channels for service");
  118. }
  119. numComplete++;
  120. double percent = numComplete;
  121. percent /= _liveTvManager.Services.Count;
  122. progress.Report(100 * percent);
  123. }
  124. if (cleanDatabase)
  125. {
  126. CleanDatabase(newChannelIdList.ToArray(), [BaseItemKind.LiveTvChannel], progress, cancellationToken);
  127. CleanDatabase(newProgramIdList.ToArray(), [BaseItemKind.LiveTvProgram], progress, cancellationToken);
  128. }
  129. var coreService = _liveTvManager.Services.OfType<DefaultLiveTvService>().FirstOrDefault();
  130. if (coreService is not null)
  131. {
  132. await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
  133. await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false);
  134. }
  135. progress.Report(100);
  136. }
  137. private double GetGuideDays()
  138. {
  139. var config = _config.GetLiveTvConfiguration();
  140. return config.GuideDays.HasValue
  141. ? Math.Clamp(config.GuideDays.Value, 1, MaxGuideDays)
  142. : 7;
  143. }
  144. private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, IProgress<double> progress, CancellationToken cancellationToken)
  145. {
  146. progress.Report(10);
  147. var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false))
  148. .Select(i => new Tuple<string, ChannelInfo>(service.Name, i))
  149. .ToList();
  150. var list = new List<LiveTvChannel>();
  151. var numComplete = 0;
  152. var parentFolder = _liveTvManager.GetInternalLiveTvFolder(cancellationToken);
  153. foreach (var channelInfo in allChannelsList)
  154. {
  155. cancellationToken.ThrowIfCancellationRequested();
  156. try
  157. {
  158. var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false);
  159. list.Add(item);
  160. }
  161. catch (OperationCanceledException)
  162. {
  163. throw;
  164. }
  165. catch (Exception ex)
  166. {
  167. _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name);
  168. }
  169. numComplete++;
  170. double percent = numComplete;
  171. percent /= allChannelsList.Count;
  172. progress.Report((5 * percent) + 10);
  173. }
  174. progress.Report(15);
  175. numComplete = 0;
  176. var programs = new List<LiveTvProgram>();
  177. var channels = new List<Guid>();
  178. var guideDays = GetGuideDays();
  179. _logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
  180. var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
  181. foreach (var currentChannel in list)
  182. {
  183. cancellationToken.ThrowIfCancellationRequested();
  184. channels.Add(currentChannel.Id);
  185. try
  186. {
  187. var start = DateTime.UtcNow.AddHours(-1);
  188. var end = start.AddDays(guideDays);
  189. var isMovie = false;
  190. var isSports = false;
  191. var isNews = false;
  192. var isKids = false;
  193. var isSeries = false;
  194. var channelPrograms = (await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false)).ToList();
  195. var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
  196. {
  197. IncludeItemTypes = [BaseItemKind.LiveTvProgram],
  198. ChannelIds = [currentChannel.Id],
  199. DtoOptions = new DtoOptions(true)
  200. }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
  201. var newPrograms = new List<Guid>();
  202. var updatedPrograms = new List<Guid>();
  203. foreach (var program in channelPrograms)
  204. {
  205. var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
  206. var id = programItem.Id;
  207. if (isNew)
  208. {
  209. newPrograms.Add(id);
  210. }
  211. else if (isUpdated)
  212. {
  213. updatedPrograms.Add(id);
  214. }
  215. programs.Add(programItem);
  216. isMovie |= program.IsMovie;
  217. isSeries |= program.IsSeries;
  218. isSports |= program.IsSports;
  219. isNews |= program.IsNews;
  220. isKids |= program.IsKids;
  221. }
  222. _logger.LogDebug(
  223. "Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
  224. currentChannel.Name,
  225. newPrograms.Count,
  226. updatedPrograms.Count);
  227. if (newPrograms.Count > 0)
  228. {
  229. var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
  230. _libraryManager.CreateOrUpdateItems(newProgramDtos, null, cancellationToken);
  231. }
  232. if (updatedPrograms.Count > 0)
  233. {
  234. var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
  235. await _libraryManager.UpdateItemsAsync(
  236. updatedProgramDtos,
  237. currentChannel,
  238. ItemUpdateType.MetadataImport,
  239. cancellationToken).ConfigureAwait(false);
  240. }
  241. await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
  242. currentChannel.IsMovie = isMovie;
  243. currentChannel.IsNews = isNews;
  244. currentChannel.IsSports = isSports;
  245. currentChannel.IsSeries = isSeries;
  246. if (isKids)
  247. {
  248. currentChannel.AddTag("Kids");
  249. }
  250. await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
  251. await currentChannel.RefreshMetadata(
  252. new MetadataRefreshOptions(new DirectoryService(_fileSystem))
  253. {
  254. ForceSave = true
  255. },
  256. cancellationToken).ConfigureAwait(false);
  257. }
  258. catch (OperationCanceledException)
  259. {
  260. throw;
  261. }
  262. catch (Exception ex)
  263. {
  264. _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name);
  265. }
  266. numComplete++;
  267. double percent = numComplete / (double)allChannelsList.Count;
  268. progress.Report((85 * percent) + 15);
  269. }
  270. progress.Report(100);
  271. var programIds = programs.Select(p => p.Id).ToList();
  272. return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
  273. }
  274. private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
  275. {
  276. var list = _itemRepo.GetItemIdsList(new InternalItemsQuery
  277. {
  278. IncludeItemTypes = validTypes,
  279. DtoOptions = new DtoOptions(false)
  280. });
  281. var numComplete = 0;
  282. foreach (var itemId in list)
  283. {
  284. cancellationToken.ThrowIfCancellationRequested();
  285. if (itemId.IsEmpty())
  286. {
  287. // Somehow some invalid data got into the db. It probably predates the boundary checking
  288. continue;
  289. }
  290. if (!currentIdList.Contains(itemId))
  291. {
  292. var item = _libraryManager.GetItemById(itemId);
  293. if (item is not null)
  294. {
  295. _libraryManager.DeleteItem(
  296. item,
  297. new DeleteOptions
  298. {
  299. DeleteFileLocation = false,
  300. DeleteFromExternalProvider = false
  301. },
  302. false);
  303. }
  304. }
  305. numComplete++;
  306. double percent = numComplete / (double)list.Count;
  307. progress.Report(100 * percent);
  308. }
  309. }
  310. private async Task<LiveTvChannel> GetChannel(
  311. ChannelInfo channelInfo,
  312. string serviceName,
  313. BaseItem parentFolder,
  314. CancellationToken cancellationToken)
  315. {
  316. var parentFolderId = parentFolder.Id;
  317. var isNew = false;
  318. var forceUpdate = false;
  319. var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id);
  320. if (_libraryManager.GetItemById(id) is not LiveTvChannel item)
  321. {
  322. item = new LiveTvChannel
  323. {
  324. Name = channelInfo.Name,
  325. Id = id,
  326. DateCreated = DateTime.UtcNow
  327. };
  328. isNew = true;
  329. }
  330. if (channelInfo.Tags is not null)
  331. {
  332. if (!channelInfo.Tags.SequenceEqual(item.Tags, StringComparer.OrdinalIgnoreCase))
  333. {
  334. isNew = true;
  335. }
  336. item.Tags = channelInfo.Tags;
  337. }
  338. if (!item.ParentId.Equals(parentFolderId))
  339. {
  340. isNew = true;
  341. }
  342. item.ParentId = parentFolderId;
  343. item.ChannelType = channelInfo.ChannelType;
  344. item.ServiceName = serviceName;
  345. if (!string.Equals(item.GetProviderId(ExternalServiceTag), serviceName, StringComparison.OrdinalIgnoreCase))
  346. {
  347. forceUpdate = true;
  348. }
  349. item.SetProviderId(ExternalServiceTag, serviceName);
  350. if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal))
  351. {
  352. forceUpdate = true;
  353. }
  354. item.ExternalId = channelInfo.Id;
  355. if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal))
  356. {
  357. forceUpdate = true;
  358. }
  359. item.Number = channelInfo.Number;
  360. if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal))
  361. {
  362. forceUpdate = true;
  363. }
  364. item.Name = channelInfo.Name;
  365. if (!item.HasImage(ImageType.Primary))
  366. {
  367. if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath))
  368. {
  369. item.SetImagePath(ImageType.Primary, channelInfo.ImagePath);
  370. forceUpdate = true;
  371. }
  372. else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl))
  373. {
  374. item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl);
  375. forceUpdate = true;
  376. }
  377. }
  378. if (isNew)
  379. {
  380. _libraryManager.CreateItem(item, parentFolder);
  381. }
  382. else if (forceUpdate)
  383. {
  384. await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
  385. }
  386. return item;
  387. }
  388. private (LiveTvProgram Item, bool IsNew, bool IsUpdated) GetProgram(
  389. ProgramInfo info,
  390. Dictionary<Guid, LiveTvProgram> allExistingPrograms,
  391. LiveTvChannel channel)
  392. {
  393. var id = _tvDtoService.GetInternalProgramId(info.Id);
  394. var isNew = false;
  395. var forceUpdate = false;
  396. if (!allExistingPrograms.TryGetValue(id, out var item))
  397. {
  398. isNew = true;
  399. item = new LiveTvProgram
  400. {
  401. Name = info.Name,
  402. Id = id,
  403. DateCreated = DateTime.UtcNow,
  404. DateModified = DateTime.UtcNow
  405. };
  406. item.TrySetProviderId(EtagKey, info.Etag);
  407. }
  408. if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase))
  409. {
  410. item.ShowId = info.ShowId;
  411. forceUpdate = true;
  412. }
  413. var seriesId = info.SeriesId;
  414. if (!item.ParentId.Equals(channel.Id))
  415. {
  416. forceUpdate = true;
  417. }
  418. item.ParentId = channel.Id;
  419. item.Audio = info.Audio;
  420. item.ChannelId = channel.Id;
  421. item.CommunityRating ??= info.CommunityRating;
  422. if ((item.CommunityRating ?? 0).Equals(0))
  423. {
  424. item.CommunityRating = null;
  425. }
  426. item.EpisodeTitle = info.EpisodeTitle;
  427. item.ExternalId = info.Id;
  428. if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
  429. {
  430. forceUpdate = true;
  431. }
  432. item.ExternalSeriesId = seriesId;
  433. var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
  434. if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
  435. {
  436. item.SeriesName = info.Name;
  437. }
  438. var tags = new List<string>();
  439. if (info.IsLive)
  440. {
  441. tags.Add("Live");
  442. }
  443. if (info.IsPremiere)
  444. {
  445. tags.Add("Premiere");
  446. }
  447. if (info.IsNews)
  448. {
  449. tags.Add("News");
  450. }
  451. if (info.IsSports)
  452. {
  453. tags.Add("Sports");
  454. }
  455. if (info.IsKids)
  456. {
  457. tags.Add("Kids");
  458. }
  459. if (info.IsRepeat)
  460. {
  461. tags.Add("Repeat");
  462. }
  463. if (info.IsMovie)
  464. {
  465. tags.Add("Movie");
  466. }
  467. if (isSeries)
  468. {
  469. tags.Add("Series");
  470. }
  471. item.Tags = tags.ToArray();
  472. item.Genres = info.Genres.ToArray();
  473. if (info.IsHD ?? false)
  474. {
  475. item.Width = 1280;
  476. item.Height = 720;
  477. }
  478. item.IsMovie = info.IsMovie;
  479. item.IsRepeat = info.IsRepeat;
  480. if (item.IsSeries != isSeries)
  481. {
  482. forceUpdate = true;
  483. }
  484. item.IsSeries = isSeries;
  485. item.Name = info.Name;
  486. item.OfficialRating ??= info.OfficialRating;
  487. item.Overview ??= info.Overview;
  488. item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
  489. item.ProviderIds = info.ProviderIds;
  490. foreach (var providerId in info.SeriesProviderIds)
  491. {
  492. info.ProviderIds["Series" + providerId.Key] = providerId.Value;
  493. }
  494. if (item.StartDate != info.StartDate)
  495. {
  496. forceUpdate = true;
  497. }
  498. item.StartDate = info.StartDate;
  499. if (item.EndDate != info.EndDate)
  500. {
  501. forceUpdate = true;
  502. }
  503. item.EndDate = info.EndDate;
  504. item.ProductionYear = info.ProductionYear;
  505. if (!isSeries || info.IsRepeat)
  506. {
  507. item.PremiereDate = info.OriginalAirDate;
  508. }
  509. item.IndexNumber = info.EpisodeNumber;
  510. item.ParentIndexNumber = info.SeasonNumber;
  511. forceUpdate = forceUpdate || UpdateImages(item, info);
  512. if (isNew)
  513. {
  514. item.OnMetadataChanged();
  515. return (item, isNew, false);
  516. }
  517. var isUpdated = false;
  518. if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
  519. {
  520. isUpdated = true;
  521. }
  522. else
  523. {
  524. var etag = info.Etag;
  525. if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
  526. {
  527. item.SetProviderId(EtagKey, etag);
  528. isUpdated = true;
  529. }
  530. }
  531. if (isUpdated)
  532. {
  533. item.OnMetadataChanged();
  534. }
  535. return (item, isNew, isUpdated);
  536. }
  537. private static bool UpdateImages(BaseItem item, ProgramInfo info)
  538. {
  539. var updated = false;
  540. // Primary
  541. updated |= UpdateImage(ImageType.Primary, item, info);
  542. // Thumbnail
  543. updated |= UpdateImage(ImageType.Thumb, item, info);
  544. // Logo
  545. updated |= UpdateImage(ImageType.Logo, item, info);
  546. // Backdrop
  547. return updated || UpdateImage(ImageType.Backdrop, item, info);
  548. }
  549. private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
  550. {
  551. var image = item.GetImages(imageType).FirstOrDefault();
  552. var currentImagePath = image?.Path;
  553. var newImagePath = imageType switch
  554. {
  555. ImageType.Primary => info.ImagePath,
  556. _ => string.Empty
  557. };
  558. var newImageUrl = imageType switch
  559. {
  560. ImageType.Backdrop => info.BackdropImageUrl,
  561. ImageType.Logo => info.LogoImageUrl,
  562. ImageType.Primary => info.ImageUrl,
  563. ImageType.Thumb => info.ThumbImageUrl,
  564. _ => string.Empty
  565. };
  566. var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false
  567. || newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false;
  568. if (!differentImage)
  569. {
  570. return false;
  571. }
  572. if (!string.IsNullOrWhiteSpace(newImagePath))
  573. {
  574. item.SetImage(
  575. new ItemImageInfo
  576. {
  577. Path = newImagePath,
  578. Type = imageType
  579. },
  580. 0);
  581. return true;
  582. }
  583. if (!string.IsNullOrWhiteSpace(newImageUrl))
  584. {
  585. item.SetImage(
  586. new ItemImageInfo
  587. {
  588. Path = newImageUrl,
  589. Type = imageType
  590. },
  591. 0);
  592. return true;
  593. }
  594. item.RemoveImage(image);
  595. return false;
  596. }
  597. private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
  598. {
  599. await Parallel.ForEachAsync(
  600. programs
  601. .Where(p => p.EndDate.HasValue && p.EndDate.Value < maxCacheDate)
  602. .DistinctBy(p => p.Id),
  603. _cacheParallelOptions,
  604. async (program, cancellationToken) =>
  605. {
  606. for (var i = 0; i < program.ImageInfos.Length; i++)
  607. {
  608. if (cancellationToken.IsCancellationRequested)
  609. {
  610. return;
  611. }
  612. var imageInfo = program.ImageInfos[i];
  613. if (!imageInfo.IsLocalFile)
  614. {
  615. try
  616. {
  617. program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
  618. program,
  619. imageInfo,
  620. imageIndex: 0,
  621. removeOnFailure: false)
  622. .ConfigureAwait(false);
  623. }
  624. catch (Exception ex)
  625. {
  626. _logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
  627. }
  628. }
  629. }
  630. }).ConfigureAwait(false);
  631. }
  632. }