From b9881b8bdf650a39cbf8f0f98d9a970266fec90a Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Tue, 31 Dec 2024 17:04:22 +0100 Subject: [PATCH] Fix EPG image caching (#13227) --- src/Jellyfin.LiveTv/Guide/GuideManager.cs | 191 ++++++++++-------- .../Listings/SchedulesDirect.cs | 31 ++- 2 files changed, 124 insertions(+), 98 deletions(-) diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs index f657422a04..05d2ae41de 100644 --- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs +++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities.Libraries; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using Jellyfin.LiveTv.Configuration; @@ -39,6 +40,11 @@ public class GuideManager : IGuideManager private readonly IRecordingsManager _recordingsManager; private readonly LiveTvDtoService _tvDtoService; + /// + /// Amount of days images are pre-cached from external sources. + /// + public const int MaxCacheDays = 2; + /// /// Initializes a new instance of the class. /// @@ -204,14 +210,14 @@ public class GuideManager : IGuideManager progress.Report(15); numComplete = 0; - var programs = new List(); + var programs = new List(); var channels = new List(); var guideDays = GetGuideDays(); - _logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays); + _logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays); - var maxCacheDate = DateTime.UtcNow.AddDays(2); + var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays); foreach (var currentChannel in list) { cancellationToken.ThrowIfCancellationRequested(); @@ -237,22 +243,23 @@ public class GuideManager : IGuideManager DtoOptions = new DtoOptions(true) }).Cast().ToDictionary(i => i.Id); - var newPrograms = new List(); - var updatedPrograms = new List(); + var newPrograms = new List(); + var updatedPrograms = new List(); foreach (var program in channelPrograms) { var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel); + var id = programItem.Id; if (isNew) { - newPrograms.Add(programItem); + newPrograms.Add(id); } else if (isUpdated) { - updatedPrograms.Add(programItem); + updatedPrograms.Add(id); } - programs.Add(programItem.Id); + programs.Add(programItem); isMovie |= program.IsMovie; isSeries |= program.IsSeries; @@ -261,24 +268,30 @@ public class GuideManager : IGuideManager isKids |= program.IsKids; } - _logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count); + _logger.LogDebug( + "Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs", + currentChannel.Name, + newPrograms.Count, + updatedPrograms.Count); if (newPrograms.Count > 0) { - _libraryManager.CreateItems(newPrograms, null, cancellationToken); - await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false); + var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList(); + _libraryManager.CreateItems(newProgramDtos, null, cancellationToken); } if (updatedPrograms.Count > 0) { + var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList(); await _libraryManager.UpdateItemsAsync( - updatedPrograms, + updatedProgramDtos, currentChannel, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); - await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false); } + await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false); + currentChannel.IsMovie = isMovie; currentChannel.IsNews = isNews; currentChannel.IsSports = isSports; @@ -313,7 +326,8 @@ public class GuideManager : IGuideManager } progress.Report(100); - return new Tuple, List>(channels, programs); + var programIds = programs.Select(p => p.Id).ToList(); + return new Tuple, List>(channels, programIds); } private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress progress, CancellationToken cancellationToken) @@ -618,77 +632,17 @@ public class GuideManager : IGuideManager item.IndexNumber = info.EpisodeNumber; item.ParentIndexNumber = info.SeasonNumber; - if (!item.HasImage(ImageType.Primary)) - { - if (!string.IsNullOrWhiteSpace(info.ImagePath)) - { - item.SetImage( - new ItemImageInfo - { - Path = info.ImagePath, - Type = ImageType.Primary - }, - 0); - } - else if (!string.IsNullOrWhiteSpace(info.ImageUrl)) - { - item.SetImage( - new ItemImageInfo - { - Path = info.ImageUrl, - Type = ImageType.Primary - }, - 0); - } - } + forceUpdate = forceUpdate || UpdateImages(item, info); - if (!item.HasImage(ImageType.Thumb)) - { - if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl)) - { - item.SetImage( - new ItemImageInfo - { - Path = info.ThumbImageUrl, - Type = ImageType.Thumb - }, - 0); - } - } - - if (!item.HasImage(ImageType.Logo)) + if (isNew) { - if (!string.IsNullOrWhiteSpace(info.LogoImageUrl)) - { - item.SetImage( - new ItemImageInfo - { - Path = info.LogoImageUrl, - Type = ImageType.Logo - }, - 0); - } - } + item.OnMetadataChanged(); - if (!item.HasImage(ImageType.Backdrop)) - { - if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl)) - { - item.SetImage( - new ItemImageInfo - { - Path = info.BackdropImageUrl, - Type = ImageType.Backdrop - }, - 0); - } + return (item, isNew, false); } var isUpdated = false; - if (isNew) - { - } - else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag)) + if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag)) { isUpdated = true; } @@ -703,7 +657,7 @@ public class GuideManager : IGuideManager } } - if (isNew || isUpdated) + if (isUpdated) { item.OnMetadataChanged(); } @@ -711,7 +665,80 @@ public class GuideManager : IGuideManager return (item, isNew, isUpdated); } - private async Task PrecacheImages(IReadOnlyList programs, DateTime maxCacheDate) + private static bool UpdateImages(BaseItem item, ProgramInfo info) + { + var updated = false; + + // Primary + updated |= UpdateImage(ImageType.Primary, item, info); + + // Thumbnail + updated |= UpdateImage(ImageType.Thumb, item, info); + + // Logo + updated |= UpdateImage(ImageType.Logo, item, info); + + // Backdrop + return updated || UpdateImage(ImageType.Backdrop, item, info); + } + + private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info) + { + var image = item.GetImages(imageType).FirstOrDefault(); + var currentImagePath = image?.Path; + var newImagePath = imageType switch + { + ImageType.Primary => info.ImagePath, + _ => string.Empty + }; + var newImageUrl = imageType switch + { + ImageType.Backdrop => info.BackdropImageUrl, + ImageType.Logo => info.LogoImageUrl, + ImageType.Primary => info.ImageUrl, + ImageType.Thumb => info.ThumbImageUrl, + _ => string.Empty + }; + + var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false + || newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false; + if (!differentImage) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(newImagePath)) + { + item.SetImage( + new ItemImageInfo + { + Path = newImagePath, + Type = imageType + }, + 0); + + return true; + } + + if (!string.IsNullOrWhiteSpace(newImageUrl)) + { + item.SetImage( + new ItemImageInfo + { + Path = newImageUrl, + Type = imageType + }, + 0); + + return true; + } + + item.RemoveImage(image); + + return false; + } + + private async Task PreCacheImages(IReadOnlyList programs, DateTime maxCacheDate) { await Parallel.ForEachAsync( programs @@ -741,7 +768,7 @@ public class GuideManager : IGuideManager } catch (Exception ex) { - _logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path); + _logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path); } } } diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs index c7a57859e8..d6f15906ef 100644 --- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using AsyncKeyedLock; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; +using Jellyfin.LiveTv.Guide; using Jellyfin.LiveTv.Listings.SchedulesDirectDtos; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; @@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings private readonly IHttpClientFactory _httpClientFactory; private readonly AsyncNonKeyedLocker _tokenLock = new(1); - private readonly ConcurrentDictionary _tokens = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _tokens = new(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private DateTime _lastErrorResponse; private bool _disposed = false; @@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings { _logger.LogWarning("SchedulesDirect token is empty, returning empty program list"); - return Enumerable.Empty(); + return []; } var dates = GetScheduleRequestDates(startDateUtc, endDateUtc); @@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings _logger.LogInformation("Channel Station ID is: {ChannelID}", channelId); var requestList = new List() { - new RequestScheduleForChannelDto() + new() { StationId = channelId, Date = dates @@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings var dailySchedules = await Request>(options, true, info, cancellationToken).ConfigureAwait(false); if (dailySchedules is null) { - return Array.Empty(); + return []; } _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); @@ -120,17 +121,17 @@ namespace Jellyfin.LiveTv.Listings var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct(); programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); - var programDetails = await Request>(programRequestOptions, true, info, cancellationToken) - .ConfigureAwait(false); + var programDetails = await Request>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); if (programDetails is null) { - return Array.Empty(); + return []; } var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y); var programIdsWithImages = programDetails - .Where(p => p.HasImageArtwork).Select(p => p.ProgramId) + .Where(p => p.HasImageArtwork) + .Select(p => p.ProgramId) .ToList(); var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false); @@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings var programsInfo = new List(); foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs)) { - // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID + - // " which corresponds to channel " + channelNumber + " and program id " + - // schedule.ProgramId + " which says it has images? " + - // programDict[schedule.ProgramId].hasImageArtwork); - if (string.IsNullOrEmpty(schedule.ProgramId)) { continue; } - if (images is not null) + // Only add images which will be pre-cached until we can implement dynamic token fetching + var endDate = schedule.AirDateTime?.AddSeconds(schedule.Duration); + var willBeCached = endDate.HasValue && endDate.Value < DateTime.UtcNow.AddDays(GuideManager.MaxCacheDays); + if (willBeCached && images is not null) { var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]); if (imageIndex > -1) @@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings if (programIds.Count == 0) { - return Array.Empty(); + return []; } StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); @@ -483,7 +482,7 @@ namespace Jellyfin.LiveTv.Listings { _logger.LogError(ex, "Error getting image info from schedules direct"); - return Array.Empty(); + return []; } }