< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Guide.GuideManager
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Guide/GuideManager.cs
Line coverage
5%
Covered lines: 10
Uncovered lines: 173
Coverable lines: 183
Total lines: 761
Line coverage: 5.4%
Branch coverage
0%
Covered branches: 0
Total branches: 89
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%11100%
GetGuideInfo()100%210%
GetGuideDays()0%620%
CleanDatabase(...)0%7280%
GetProgram(...)0%3192560%
UpdateImages(...)100%210%
UpdateImage(...)0%552230%

File(s)

/srv/git/jellyfin/src/Jellyfin.LiveTv/Guide/GuideManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Threading;
 5using System.Threading.Tasks;
 6using Jellyfin.Data.Enums;
 7using Jellyfin.Extensions;
 8using Jellyfin.LiveTv.Configuration;
 9using MediaBrowser.Common.Configuration;
 10using MediaBrowser.Controller.Dto;
 11using MediaBrowser.Controller.Entities;
 12using MediaBrowser.Controller.Library;
 13using MediaBrowser.Controller.LiveTv;
 14using MediaBrowser.Controller.Persistence;
 15using MediaBrowser.Controller.Providers;
 16using MediaBrowser.Model.Entities;
 17using MediaBrowser.Model.IO;
 18using MediaBrowser.Model.LiveTv;
 19using Microsoft.Extensions.Logging;
 20
 21namespace Jellyfin.LiveTv.Guide;
 22
 23/// <inheritdoc />
 24public class GuideManager : IGuideManager
 25{
 26    private const int MaxGuideDays = 14;
 27    private const string EtagKey = "ProgramEtag";
 28    private const string ExternalServiceTag = "ExternalServiceId";
 29
 030    private static readonly ParallelOptions _cacheParallelOptions = new() { MaxDegreeOfParallelism = Math.Min(Environmen
 31
 32    private readonly ILogger<GuideManager> _logger;
 33    private readonly IConfigurationManager _config;
 34    private readonly IFileSystem _fileSystem;
 35    private readonly IItemRepository _itemRepo;
 36    private readonly ILibraryManager _libraryManager;
 37    private readonly ILiveTvManager _liveTvManager;
 38    private readonly ITunerHostManager _tunerHostManager;
 39    private readonly IRecordingsManager _recordingsManager;
 40    private readonly LiveTvDtoService _tvDtoService;
 41
 42    /// <summary>
 43    /// Amount of days images are pre-cached from external sources.
 44    /// </summary>
 45    public const int MaxCacheDays = 2;
 46
 47    /// <summary>
 48    /// Initializes a new instance of the <see cref="GuideManager"/> class.
 49    /// </summary>
 50    /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
 51    /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
 52    /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
 53    /// <param name="itemRepo">The <see cref="IItemRepository"/>.</param>
 54    /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
 55    /// <param name="liveTvManager">The <see cref="ILiveTvManager"/>.</param>
 56    /// <param name="tunerHostManager">The <see cref="ITunerHostManager"/>.</param>
 57    /// <param name="recordingsManager">The <see cref="IRecordingsManager"/>.</param>
 58    /// <param name="tvDtoService">The <see cref="LiveTvDtoService"/>.</param>
 59    public GuideManager(
 60        ILogger<GuideManager> logger,
 61        IConfigurationManager config,
 62        IFileSystem fileSystem,
 63        IItemRepository itemRepo,
 64        ILibraryManager libraryManager,
 65        ILiveTvManager liveTvManager,
 66        ITunerHostManager tunerHostManager,
 67        IRecordingsManager recordingsManager,
 68        LiveTvDtoService tvDtoService)
 69    {
 2170        _logger = logger;
 2171        _config = config;
 2172        _fileSystem = fileSystem;
 2173        _itemRepo = itemRepo;
 2174        _libraryManager = libraryManager;
 2175        _liveTvManager = liveTvManager;
 2176        _tunerHostManager = tunerHostManager;
 2177        _recordingsManager = recordingsManager;
 2178        _tvDtoService = tvDtoService;
 2179    }
 80
 81    /// <inheritdoc />
 82    public GuideInfo GetGuideInfo()
 83    {
 084        var startDate = DateTime.UtcNow;
 085        var endDate = startDate.AddDays(GetGuideDays());
 86
 087        return new GuideInfo
 088        {
 089            StartDate = startDate,
 090            EndDate = endDate
 091        };
 92    }
 93
 94    /// <inheritdoc />
 95    public async Task RefreshGuide(IProgress<double> progress, CancellationToken cancellationToken)
 96    {
 97        ArgumentNullException.ThrowIfNull(progress);
 98
 99        await _recordingsManager.CreateRecordingFolders().ConfigureAwait(false);
 100
 101        await _tunerHostManager.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
 102
 103        var numComplete = 0;
 104        double progressPerService = _liveTvManager.Services.Count == 0
 105            ? 0
 106            : 1.0 / _liveTvManager.Services.Count;
 107
 108        var newChannelIdList = new List<Guid>();
 109        var newProgramIdList = new List<Guid>();
 110
 111        var cleanDatabase = true;
 112
 113        foreach (var service in _liveTvManager.Services)
 114        {
 115            cancellationToken.ThrowIfCancellationRequested();
 116
 117            _logger.LogDebug("Refreshing guide from {Name}", service.Name);
 118
 119            try
 120            {
 121                var innerProgress = new Progress<double>(p => progress.Report(p * progressPerService));
 122
 123                var idList = await RefreshChannelsInternal(service, innerProgress, cancellationToken).ConfigureAwait(fal
 124
 125                newChannelIdList.AddRange(idList.Item1);
 126                newProgramIdList.AddRange(idList.Item2);
 127            }
 128            catch (OperationCanceledException)
 129            {
 130                throw;
 131            }
 132            catch (Exception ex)
 133            {
 134                cleanDatabase = false;
 135                _logger.LogError(ex, "Error refreshing channels for service");
 136            }
 137
 138            numComplete++;
 139            double percent = numComplete;
 140            percent /= _liveTvManager.Services.Count;
 141
 142            progress.Report(100 * percent);
 143        }
 144
 145        if (cleanDatabase)
 146        {
 147            CleanDatabase(newChannelIdList.ToArray(), [BaseItemKind.LiveTvChannel], progress, cancellationToken);
 148            CleanDatabase(newProgramIdList.ToArray(), [BaseItemKind.LiveTvProgram], progress, cancellationToken);
 149        }
 150
 151        var coreService = _liveTvManager.Services.OfType<DefaultLiveTvService>().FirstOrDefault();
 152        if (coreService is not null)
 153        {
 154            await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
 155            await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false);
 156        }
 157
 158        progress.Report(100);
 159    }
 160
 161    private double GetGuideDays()
 162    {
 0163        var config = _config.GetLiveTvConfiguration();
 164
 0165        return config.GuideDays.HasValue
 0166            ? Math.Clamp(config.GuideDays.Value, 1, MaxGuideDays)
 0167            : 7;
 168    }
 169
 170    private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, IProgress<double> 
 171    {
 172        progress.Report(10);
 173
 174        var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false))
 175            .Select(i => new Tuple<string, ChannelInfo>(service.Name, i))
 176            .ToList();
 177
 178        var list = new List<LiveTvChannel>();
 179
 180        var numComplete = 0;
 181        var parentFolder = _liveTvManager.GetInternalLiveTvFolder(cancellationToken);
 182
 183        foreach (var channelInfo in allChannelsList)
 184        {
 185            cancellationToken.ThrowIfCancellationRequested();
 186
 187            try
 188            {
 189                var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).Confi
 190
 191                list.Add(item);
 192            }
 193            catch (OperationCanceledException)
 194            {
 195                throw;
 196            }
 197            catch (Exception ex)
 198            {
 199                _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name);
 200            }
 201
 202            numComplete++;
 203            double percent = numComplete;
 204            percent /= allChannelsList.Count;
 205
 206            progress.Report((5 * percent) + 10);
 207        }
 208
 209        progress.Report(15);
 210
 211        numComplete = 0;
 212        var programIds = new List<Guid>();
 213        var channels = new List<Guid>();
 214
 215        var guideDays = GetGuideDays();
 216
 217        _logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
 218
 219        var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
 220        foreach (var currentChannel in list)
 221        {
 222            cancellationToken.ThrowIfCancellationRequested();
 223            channels.Add(currentChannel.Id);
 224
 225            try
 226            {
 227                var start = DateTime.UtcNow.AddHours(-1);
 228                var end = start.AddDays(guideDays);
 229
 230                var isMovie = false;
 231                var isSports = false;
 232                var isNews = false;
 233                var isKids = false;
 234                var isSeries = false;
 235
 236                var channelPrograms = (await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellatio
 237
 238                var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
 239                {
 240                    IncludeItemTypes = [BaseItemKind.LiveTvProgram],
 241                    ChannelIds = [currentChannel.Id],
 242                    DtoOptions = new DtoOptions(true)
 243                }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
 244
 245                var newPrograms = new List<LiveTvProgram>();
 246                var updatedPrograms = new List<LiveTvProgram>();
 247
 248                foreach (var program in channelPrograms)
 249                {
 250                    var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
 251                    var id = programItem.Id;
 252                    if (isNew)
 253                    {
 254                        newPrograms.Add(programItem);
 255                    }
 256                    else if (isUpdated)
 257                    {
 258                        updatedPrograms.Add(programItem);
 259                    }
 260
 261                    programIds.Add(programItem.Id);
 262
 263                    isMovie |= program.IsMovie;
 264                    isSeries |= program.IsSeries;
 265                    isSports |= program.IsSports;
 266                    isNews |= program.IsNews;
 267                    isKids |= program.IsKids;
 268                }
 269
 270                _logger.LogDebug(
 271                    "Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
 272                    currentChannel.Name,
 273                    newPrograms.Count,
 274                    updatedPrograms.Count);
 275
 276                if (newPrograms.Count > 0)
 277                {
 278                    _libraryManager.CreateItems(newPrograms, currentChannel, cancellationToken);
 279
 280                    await PreCacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
 281                }
 282
 283                if (updatedPrograms.Count > 0)
 284                {
 285                    await _libraryManager.UpdateItemsAsync(
 286                        updatedPrograms,
 287                        currentChannel,
 288                        ItemUpdateType.MetadataImport,
 289                        cancellationToken).ConfigureAwait(false);
 290
 291                    await PreCacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
 292                }
 293
 294                currentChannel.IsMovie = isMovie;
 295                currentChannel.IsNews = isNews;
 296                currentChannel.IsSports = isSports;
 297                currentChannel.IsSeries = isSeries;
 298
 299                if (isKids)
 300                {
 301                    currentChannel.AddTag("Kids");
 302                }
 303
 304                await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).Configure
 305                await currentChannel.RefreshMetadata(
 306                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 307                    {
 308                        ForceSave = true
 309                    },
 310                    cancellationToken).ConfigureAwait(false);
 311            }
 312            catch (OperationCanceledException)
 313            {
 314                throw;
 315            }
 316            catch (Exception ex)
 317            {
 318                _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name);
 319            }
 320
 321            numComplete++;
 322            double percent = numComplete / (double)allChannelsList.Count;
 323
 324            progress.Report((85 * percent) + 15);
 325        }
 326
 327        progress.Report(100);
 328        return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
 329    }
 330
 331    private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, Cancellation
 332    {
 0333        var list = _itemRepo.GetItemIdsList(new InternalItemsQuery
 0334        {
 0335            IncludeItemTypes = validTypes,
 0336            DtoOptions = new DtoOptions(false)
 0337        });
 338
 0339        var numComplete = 0;
 340
 0341        foreach (var itemId in list)
 342        {
 0343            cancellationToken.ThrowIfCancellationRequested();
 344
 0345            if (itemId.IsEmpty())
 346            {
 347                // Somehow some invalid data got into the db. It probably predates the boundary checking
 348                continue;
 349            }
 350
 0351            if (!currentIdList.Contains(itemId))
 352            {
 0353                var item = _libraryManager.GetItemById(itemId);
 354
 0355                if (item is not null)
 356                {
 0357                    _libraryManager.DeleteItem(
 0358                        item,
 0359                        new DeleteOptions
 0360                        {
 0361                            DeleteFileLocation = false,
 0362                            DeleteFromExternalProvider = false
 0363                        },
 0364                        false);
 365                }
 366            }
 367
 0368            numComplete++;
 0369            double percent = numComplete / (double)list.Count;
 370
 0371            progress.Report(100 * percent);
 372        }
 0373    }
 374
 375    private async Task<LiveTvChannel> GetChannel(
 376        ChannelInfo channelInfo,
 377        string serviceName,
 378        BaseItem parentFolder,
 379        CancellationToken cancellationToken)
 380    {
 381        var parentFolderId = parentFolder.Id;
 382        var isNew = false;
 383        var forceUpdate = false;
 384
 385        var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id);
 386
 387        if (_libraryManager.GetItemById(id) is not LiveTvChannel item)
 388        {
 389            item = new LiveTvChannel
 390            {
 391                Name = channelInfo.Name,
 392                Id = id,
 393                DateCreated = DateTime.UtcNow
 394            };
 395
 396            isNew = true;
 397        }
 398
 399        if (channelInfo.Tags is not null)
 400        {
 401            if (!channelInfo.Tags.SequenceEqual(item.Tags, StringComparer.OrdinalIgnoreCase))
 402            {
 403                isNew = true;
 404            }
 405
 406            item.Tags = channelInfo.Tags;
 407        }
 408
 409        if (!item.ParentId.Equals(parentFolderId))
 410        {
 411            isNew = true;
 412        }
 413
 414        item.ParentId = parentFolderId;
 415
 416        item.ChannelType = channelInfo.ChannelType;
 417        item.ServiceName = serviceName;
 418
 419        if (!string.Equals(item.GetProviderId(ExternalServiceTag), serviceName, StringComparison.OrdinalIgnoreCase))
 420        {
 421            forceUpdate = true;
 422        }
 423
 424        item.SetProviderId(ExternalServiceTag, serviceName);
 425
 426        if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal))
 427        {
 428            forceUpdate = true;
 429        }
 430
 431        item.ExternalId = channelInfo.Id;
 432
 433        if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal))
 434        {
 435            forceUpdate = true;
 436        }
 437
 438        item.Number = channelInfo.Number;
 439
 440        if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal))
 441        {
 442            forceUpdate = true;
 443        }
 444
 445        item.Name = channelInfo.Name;
 446
 447        if (!item.HasImage(ImageType.Primary))
 448        {
 449            if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath))
 450            {
 451                item.SetImagePath(ImageType.Primary, channelInfo.ImagePath);
 452                forceUpdate = true;
 453            }
 454            else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl))
 455            {
 456                item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl);
 457                forceUpdate = true;
 458            }
 459        }
 460
 461        if (isNew)
 462        {
 463            _libraryManager.CreateItem(item, parentFolder);
 464        }
 465        else if (forceUpdate)
 466        {
 467            await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).
 468        }
 469
 470        return item;
 471    }
 472
 473    private (LiveTvProgram Item, bool IsNew, bool IsUpdated) GetProgram(
 474        ProgramInfo info,
 475        Dictionary<Guid, LiveTvProgram> allExistingPrograms,
 476        LiveTvChannel channel)
 477    {
 0478        var id = _tvDtoService.GetInternalProgramId(info.Id);
 479
 0480        var isNew = false;
 0481        var forceUpdate = false;
 482
 0483        if (!allExistingPrograms.TryGetValue(id, out var item))
 484        {
 0485            isNew = true;
 0486            item = new LiveTvProgram
 0487            {
 0488                Name = info.Name,
 0489                Id = id,
 0490                DateCreated = DateTime.UtcNow,
 0491                DateModified = DateTime.UtcNow
 0492            };
 493
 0494            item.TrySetProviderId(EtagKey, info.Etag);
 495        }
 496
 0497        if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase))
 498        {
 0499            item.ShowId = info.ShowId;
 0500            forceUpdate = true;
 501        }
 502
 0503        var channelId = channel.Id;
 0504        if (!item.ParentId.Equals(channelId))
 505        {
 0506            item.ParentId = channel.Id;
 0507            forceUpdate = true;
 508        }
 509
 0510        item.Audio = info.Audio;
 0511        item.ChannelId = channelId;
 0512        item.CommunityRating = info.CommunityRating;
 0513        item.EpisodeTitle = info.EpisodeTitle;
 0514        item.ExternalId = info.Id;
 515
 0516        var seriesId = info.SeriesId;
 0517        if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ord
 518        {
 0519            item.ExternalSeriesId = seriesId;
 0520            forceUpdate = true;
 521        }
 522
 0523        var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
 0524        if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
 525        {
 0526            item.SeriesName = info.Name;
 527        }
 528
 0529        var tags = new List<string>();
 0530        if (info.IsLive)
 531        {
 0532            tags.Add("Live");
 533        }
 534
 0535        if (info.IsPremiere)
 536        {
 0537            tags.Add("Premiere");
 538        }
 539
 0540        if (info.IsNews)
 541        {
 0542            tags.Add("News");
 543        }
 544
 0545        if (info.IsSports)
 546        {
 0547            tags.Add("Sports");
 548        }
 549
 0550        if (info.IsKids)
 551        {
 0552            tags.Add("Kids");
 553        }
 554
 0555        if (info.IsRepeat)
 556        {
 0557            tags.Add("Repeat");
 558        }
 559
 0560        if (info.IsMovie)
 561        {
 0562            tags.Add("Movie");
 563        }
 564
 0565        if (isSeries)
 566        {
 0567            tags.Add("Series");
 568        }
 569
 0570        item.Tags = tags.ToArray();
 0571        item.Genres = info.Genres.ToArray();
 572
 0573        if (info.IsHD ?? false)
 574        {
 0575            item.Width = 1280;
 0576            item.Height = 720;
 577        }
 578
 0579        item.IsMovie = info.IsMovie;
 0580        item.IsRepeat = info.IsRepeat;
 0581        if (item.IsSeries != isSeries)
 582        {
 0583            item.IsSeries = isSeries;
 0584            forceUpdate = true;
 585        }
 586
 0587        item.Name = info.Name;
 0588        item.OfficialRating = info.OfficialRating;
 0589        item.Overview = info.Overview;
 0590        item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
 0591        foreach (var providerId in info.SeriesProviderIds)
 592        {
 0593            info.ProviderIds["Series" + providerId.Key] = providerId.Value;
 594        }
 595
 0596        item.ProviderIds = info.ProviderIds;
 0597        if (item.StartDate != info.StartDate)
 598        {
 0599            item.StartDate = info.StartDate;
 0600            forceUpdate = true;
 601        }
 602
 0603        if (item.EndDate != info.EndDate)
 604        {
 0605            item.EndDate = info.EndDate;
 0606            forceUpdate = true;
 607        }
 608
 0609        item.ProductionYear = info.ProductionYear;
 0610        if (!isSeries || info.IsRepeat)
 611        {
 0612            item.PremiereDate = info.OriginalAirDate;
 613        }
 614
 0615        item.IndexNumber = info.EpisodeNumber;
 0616        item.ParentIndexNumber = info.SeasonNumber;
 617
 0618        forceUpdate |= UpdateImages(item, info);
 619
 0620        if (isNew)
 621        {
 0622            item.OnMetadataChanged();
 623
 0624            return (item, true, false);
 625        }
 626
 0627        var isUpdated = forceUpdate;
 0628        var etag = info.Etag;
 0629        if (string.IsNullOrWhiteSpace(etag))
 630        {
 0631            isUpdated = true;
 632        }
 0633        else if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
 634        {
 0635            item.SetProviderId(EtagKey, etag);
 0636            isUpdated = true;
 637        }
 638
 0639        if (isUpdated)
 640        {
 0641            item.OnMetadataChanged();
 642
 0643            return (item, false, true);
 644        }
 645
 0646        return (item, false, false);
 647    }
 648
 649    private static bool UpdateImages(BaseItem item, ProgramInfo info)
 650    {
 0651        var updated = false;
 652
 653        // Primary
 0654        updated |= UpdateImage(ImageType.Primary, item, info);
 655
 656        // Thumbnail
 0657        updated |= UpdateImage(ImageType.Thumb, item, info);
 658
 659        // Logo
 0660        updated |= UpdateImage(ImageType.Logo, item, info);
 661
 662        // Backdrop
 0663        updated |= UpdateImage(ImageType.Backdrop, item, info);
 664
 0665        return updated;
 666    }
 667
 668    private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
 669    {
 0670        var image = item.GetImages(imageType).FirstOrDefault();
 0671        var currentImagePath = image?.Path;
 0672        var newImagePath = imageType switch
 0673        {
 0674            ImageType.Primary => info.ImagePath,
 0675            _ => null
 0676        };
 0677        var newImageUrl = imageType switch
 0678        {
 0679            ImageType.Backdrop => info.BackdropImageUrl,
 0680            ImageType.Logo => info.LogoImageUrl,
 0681            ImageType.Primary => info.ImageUrl,
 0682            ImageType.Thumb => info.ThumbImageUrl,
 0683            _ => null
 0684        };
 685
 0686        var sameImage = (currentImagePath?.Equals(newImageUrl, StringComparison.OrdinalIgnoreCase) ?? false)
 0687                                || (currentImagePath?.Equals(newImagePath, StringComparison.OrdinalIgnoreCase) ?? false)
 0688        if (sameImage)
 689        {
 0690            return false;
 691        }
 692
 0693        if (!string.IsNullOrWhiteSpace(newImagePath))
 694        {
 0695            item.SetImage(
 0696                new ItemImageInfo
 0697                {
 0698                    Path = newImagePath,
 0699                    Type = imageType
 0700                },
 0701                0);
 702
 0703            return true;
 704        }
 705
 0706        if (!string.IsNullOrWhiteSpace(newImageUrl))
 707        {
 0708            item.SetImage(
 0709                new ItemImageInfo
 0710                {
 0711                    Path = newImageUrl,
 0712                    Type = imageType
 0713                },
 0714                0);
 715
 0716            return true;
 717        }
 718
 0719        item.RemoveImage(image);
 720
 0721        return false;
 722    }
 723
 724    private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
 725    {
 726        await Parallel.ForEachAsync(
 727            programs
 728                .Where(p => p.EndDate.HasValue && p.EndDate.Value < maxCacheDate)
 729                .DistinctBy(p => p.Id),
 730            _cacheParallelOptions,
 731            async (program, cancellationToken) =>
 732            {
 733                for (var i = 0; i < program.ImageInfos.Length; i++)
 734                {
 735                    if (cancellationToken.IsCancellationRequested)
 736                    {
 737                        return;
 738                    }
 739
 740                    var imageInfo = program.ImageInfos[i];
 741                    if (!imageInfo.IsLocalFile)
 742                    {
 743                        _logger.LogDebug("Caching image locally: {Url}", imageInfo.Path);
 744                        try
 745                        {
 746                            program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
 747                                    program,
 748                                    imageInfo,
 749                                    imageIndex: 0,
 750                                    removeOnFailure: false)
 751                                .ConfigureAwait(false);
 752                        }
 753                        catch (Exception ex)
 754                        {
 755                            _logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
 756                        }
 757                    }
 758                }
 759            }).ConfigureAwait(false);
 760    }
 761}