ソースを参照

Backport pull request #13227 from jellyfin/release-10.10.z

Fix EPG image caching

Original-merge: b9881b8bdf650a39cbf8f0f98d9a970266fec90a

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
Shadowghost 4 ヶ月 前
コミット
c44006c20d

+ 109 - 82
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;
 
+    /// <summary>
+    /// Amount of days images are pre-cached from external sources.
+    /// </summary>
+    public const int MaxCacheDays = 2;
+
     /// <summary>
     /// Initializes a new instance of the <see cref="GuideManager"/> class.
     /// </summary>
@@ -204,14 +210,14 @@ public class GuideManager : IGuideManager
         progress.Report(15);
 
         numComplete = 0;
-        var programs = new List<Guid>();
+        var programs = new List<LiveTvProgram>();
         var channels = new List<Guid>();
 
         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<LiveTvProgram>().ToDictionary(i => i.Id);
 
-                var newPrograms = new List<LiveTvProgram>();
-                var updatedPrograms = new List<BaseItem>();
+                var newPrograms = new List<Guid>();
+                var updatedPrograms = new List<Guid>();
 
                 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.CreateOrUpdateItems(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<Guid>, List<Guid>>(channels, programs);
+        var programIds = programs.Select(p => p.Id).ToList();
+        return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
     }
 
     private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> 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<BaseItem> 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<BaseItem> 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);
                         }
                     }
                 }

+ 15 - 16
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<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
+        private readonly ConcurrentDictionary<string, NameValuePair> _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<ProgramInfo>();
+                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<RequestScheduleForChannelDto>()
                 {
-                    new RequestScheduleForChannelDto()
+                    new()
                     {
                         StationId = channelId,
                         Date = dates
@@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
             var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
             if (dailySchedules is null)
             {
-                return Array.Empty<ProgramInfo>();
+                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<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
-                    .ConfigureAwait(false);
+            var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
             if (programDetails is null)
             {
-                return Array.Empty<ProgramInfo>();
+                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<ProgramInfo>();
             foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
             {
-                // _logger.LogDebug("Processing Schedule for station 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<ShowImagesDto>();
+                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<ShowImagesDto>();
+                return [];
             }
         }