Fix EPG image caching (#13227)

pull/13345/head
Tim Eisele 2 months ago committed by GitHub
parent b31f1696f2
commit b9881b8bdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities.Libraries;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.LiveTv.Configuration; using Jellyfin.LiveTv.Configuration;
@ -39,6 +40,11 @@ public class GuideManager : IGuideManager
private readonly IRecordingsManager _recordingsManager; private readonly IRecordingsManager _recordingsManager;
private readonly LiveTvDtoService _tvDtoService; private readonly LiveTvDtoService _tvDtoService;
/// <summary>
/// Amount of days images are pre-cached from external sources.
/// </summary>
public const int MaxCacheDays = 2;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GuideManager"/> class. /// Initializes a new instance of the <see cref="GuideManager"/> class.
/// </summary> /// </summary>
@ -204,14 +210,14 @@ public class GuideManager : IGuideManager
progress.Report(15); progress.Report(15);
numComplete = 0; numComplete = 0;
var programs = new List<Guid>(); var programs = new List<LiveTvProgram>();
var channels = new List<Guid>(); var channels = new List<Guid>();
var guideDays = GetGuideDays(); 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) foreach (var currentChannel in list)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -237,22 +243,23 @@ public class GuideManager : IGuideManager
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id); }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
var newPrograms = new List<LiveTvProgram>(); var newPrograms = new List<Guid>();
var updatedPrograms = new List<BaseItem>(); var updatedPrograms = new List<Guid>();
foreach (var program in channelPrograms) foreach (var program in channelPrograms)
{ {
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel); var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
var id = programItem.Id;
if (isNew) if (isNew)
{ {
newPrograms.Add(programItem); newPrograms.Add(id);
} }
else if (isUpdated) else if (isUpdated)
{ {
updatedPrograms.Add(programItem); updatedPrograms.Add(id);
} }
programs.Add(programItem.Id); programs.Add(programItem);
isMovie |= program.IsMovie; isMovie |= program.IsMovie;
isSeries |= program.IsSeries; isSeries |= program.IsSeries;
@ -261,24 +268,30 @@ public class GuideManager : IGuideManager
isKids |= program.IsKids; 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) if (newPrograms.Count > 0)
{ {
_libraryManager.CreateItems(newPrograms, null, cancellationToken); var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false); _libraryManager.CreateItems(newProgramDtos, null, cancellationToken);
} }
if (updatedPrograms.Count > 0) if (updatedPrograms.Count > 0)
{ {
var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
await _libraryManager.UpdateItemsAsync( await _libraryManager.UpdateItemsAsync(
updatedPrograms, updatedProgramDtos,
currentChannel, currentChannel,
ItemUpdateType.MetadataImport, ItemUpdateType.MetadataImport,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
} }
await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
currentChannel.IsMovie = isMovie; currentChannel.IsMovie = isMovie;
currentChannel.IsNews = isNews; currentChannel.IsNews = isNews;
currentChannel.IsSports = isSports; currentChannel.IsSports = isSports;
@ -313,7 +326,8 @@ public class GuideManager : IGuideManager
} }
progress.Report(100); 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) 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.IndexNumber = info.EpisodeNumber;
item.ParentIndexNumber = info.SeasonNumber; item.ParentIndexNumber = info.SeasonNumber;
if (!item.HasImage(ImageType.Primary)) forceUpdate = forceUpdate || UpdateImages(item, info);
{
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);
}
}
if (!item.HasImage(ImageType.Thumb)) if (isNew)
{
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
{
item.SetImage(
new ItemImageInfo
{
Path = info.ThumbImageUrl,
Type = ImageType.Thumb
},
0);
}
}
if (!item.HasImage(ImageType.Logo))
{ {
if (!string.IsNullOrWhiteSpace(info.LogoImageUrl)) item.OnMetadataChanged();
{
item.SetImage(
new ItemImageInfo
{
Path = info.LogoImageUrl,
Type = ImageType.Logo
},
0);
}
}
if (!item.HasImage(ImageType.Backdrop)) return (item, isNew, false);
{
if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
{
item.SetImage(
new ItemImageInfo
{
Path = info.BackdropImageUrl,
Type = ImageType.Backdrop
},
0);
}
} }
var isUpdated = false; var isUpdated = false;
if (isNew) if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
{
}
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
{ {
isUpdated = true; isUpdated = true;
} }
@ -703,7 +657,7 @@ public class GuideManager : IGuideManager
} }
} }
if (isNew || isUpdated) if (isUpdated)
{ {
item.OnMetadataChanged(); item.OnMetadataChanged();
} }
@ -711,7 +665,80 @@ public class GuideManager : IGuideManager
return (item, isNew, isUpdated); 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( await Parallel.ForEachAsync(
programs programs
@ -741,7 +768,7 @@ public class GuideManager : IGuideManager
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path); _logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
} }
} }
} }

@ -19,6 +19,7 @@ using System.Threading.Tasks;
using AsyncKeyedLock; using AsyncKeyedLock;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using Jellyfin.LiveTv.Guide;
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos; using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly AsyncNonKeyedLocker _tokenLock = new(1); 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 readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private DateTime _lastErrorResponse; private DateTime _lastErrorResponse;
private bool _disposed = false; private bool _disposed = false;
@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings
{ {
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list"); _logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
return Enumerable.Empty<ProgramInfo>(); return [];
} }
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc); var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId); _logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
var requestList = new List<RequestScheduleForChannelDto>() var requestList = new List<RequestScheduleForChannelDto>()
{ {
new RequestScheduleForChannelDto() new()
{ {
StationId = channelId, StationId = channelId,
Date = dates Date = dates
@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false); var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null) if (dailySchedules is null)
{ {
return Array.Empty<ProgramInfo>(); return [];
} }
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); _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(); var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken) var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
.ConfigureAwait(false);
if (programDetails is null) if (programDetails is null)
{ {
return Array.Empty<ProgramInfo>(); return [];
} }
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y); var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
var programIdsWithImages = programDetails var programIdsWithImages = programDetails
.Where(p => p.HasImageArtwork).Select(p => p.ProgramId) .Where(p => p.HasImageArtwork)
.Select(p => p.ProgramId)
.ToList(); .ToList();
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false); var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings
var programsInfo = new List<ProgramInfo>(); var programsInfo = new List<ProgramInfo>();
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs)) 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)) if (string.IsNullOrEmpty(schedule.ProgramId))
{ {
continue; 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]); var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
if (imageIndex > -1) if (imageIndex > -1)
@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings
if (programIds.Count == 0) if (programIds.Count == 0)
{ {
return Array.Empty<ShowImagesDto>(); return [];
} }
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); 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"); _logger.LogError(ex, "Error getting image info from schedules direct");
return Array.Empty<ShowImagesDto>(); return [];
} }
} }

Loading…
Cancel
Save