Duncan Tourolle 0548fe7dec
All checks were successful
🏗️ Build Plugin / build (push) Successful in 2m53s
🧪 Test Plugin / test (push) Successful in 1m21s
Use TV guide for livestreams
2025-12-07 17:41:48 +01:00

206 lines
7.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
using Jellyfin.Plugin.SRFPlay.Utilities;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.SRFPlay.Providers;
/// <summary>
/// Provides images for SRF Play content.
/// </summary>
public class SRFImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<SRFImageProvider> _logger;
private readonly IMediaCompositionFetcher _compositionFetcher;
/// <summary>
/// Initializes a new instance of the <see cref="SRFImageProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">The HTTP client factory.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="compositionFetcher">The media composition fetcher.</param>
public SRFImageProvider(
IHttpClientFactory httpClientFactory,
ILoggerFactory loggerFactory,
IMediaCompositionFetcher compositionFetcher)
{
_httpClientFactory = httpClientFactory;
_logger = loggerFactory.CreateLogger<SRFImageProvider>();
_compositionFetcher = compositionFetcher;
}
/// <inheritdoc />
public string Name => "SRF Play";
/// <inheritdoc />
public int Order => 0;
/// <inheritdoc />
public bool Supports(BaseItem item)
{
// Support movies and episodes for now
return item is MediaBrowser.Controller.Entities.Movies.Movie ||
item is MediaBrowser.Controller.Entities.TV.Episode ||
item is MediaBrowser.Controller.Entities.TV.Series;
}
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType>
{
ImageType.Primary,
ImageType.Backdrop,
ImageType.Thumb
};
}
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var list = new List<RemoteImageInfo>();
try
{
// Check if item has SRF URN in provider IDs
if (!item.ProviderIds.TryGetValue("SRF", out var urn) || string.IsNullOrEmpty(urn))
{
_logger.LogDebug("No SRF URN found for item: {ItemName}", item.Name);
return list;
}
_logger.LogDebug("Fetching images for SRF URN: {Urn}", urn);
// Fetch media composition to get image URLs
var mediaComposition = await _compositionFetcher.GetMediaCompositionAsync(urn, cancellationToken).ConfigureAwait(false);
if (mediaComposition == null)
{
_logger.LogWarning("Failed to fetch media composition for URN: {Urn}", urn);
return list;
}
// Extract images from chapters
if (mediaComposition.ChapterList != null && mediaComposition.ChapterList.Count > 0)
{
var chapter = mediaComposition.ChapterList[0];
if (!string.IsNullOrEmpty(chapter.ImageUrl))
{
_logger.LogDebug("URN {Urn}: Adding chapter image: {ImageUrl}", urn, chapter.ImageUrl);
list.Add(new RemoteImageInfo
{
Url = chapter.ImageUrl,
Type = ImageType.Primary,
ProviderName = Name
});
list.Add(new RemoteImageInfo
{
Url = chapter.ImageUrl,
Type = ImageType.Thumb,
ProviderName = Name
});
}
else
{
_logger.LogDebug("URN {Urn}: No chapter image available", urn);
}
}
// Extract images from show
if (mediaComposition.Show != null)
{
if (!string.IsNullOrEmpty(mediaComposition.Show.ImageUrl))
{
_logger.LogDebug("URN {Urn}: Adding show image: {ImageUrl}", urn, mediaComposition.Show.ImageUrl);
list.Add(new RemoteImageInfo
{
Url = mediaComposition.Show.ImageUrl,
Type = ImageType.Primary,
ProviderName = Name
});
}
if (!string.IsNullOrEmpty(mediaComposition.Show.BannerImageUrl))
{
_logger.LogDebug("URN {Urn}: Adding show banner: {BannerUrl}", urn, mediaComposition.Show.BannerImageUrl);
list.Add(new RemoteImageInfo
{
Url = mediaComposition.Show.BannerImageUrl,
Type = ImageType.Backdrop,
ProviderName = Name
});
}
}
_logger.LogInformation("URN {Urn}: Found {Count} images for '{ItemName}'", urn, list.Count, item.Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching images for item: {ItemName}", item.Name);
}
return list;
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
_logger.LogDebug("Fetching image from URL: {Url}", url);
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
// Create request with proper headers - SRF CDN requires User-Agent
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
request.Headers.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
request.Headers.Accept.ParseAdd("image/jpeg, image/png, image/webp, image/*;q=0.8");
var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
var originalContentType = response.Content.Headers.ContentType?.MediaType;
_logger.LogDebug(
"Image response status: {StatusCode}, Content-Type: {ContentType}, Content-Length: {Length}",
response.StatusCode,
originalContentType ?? "null",
response.Content.Headers.ContentLength);
// Fix Content-Type if it's not a proper image type - SRF CDN often returns wrong content type
// Jellyfin needs correct Content-Type to process images
if (response.IsSuccessStatusCode)
{
var needsContentTypeFix = string.IsNullOrEmpty(originalContentType) ||
originalContentType == "binary/octet-stream" ||
originalContentType == "application/octet-stream" ||
!originalContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
if (needsContentTypeFix)
{
// Determine correct content type from URL extension or default to JPEG
var contentType = MimeTypeHelper.GetImageContentType(url);
if (!string.IsNullOrEmpty(contentType))
{
_logger.LogInformation(
"Fixing Content-Type from '{OriginalType}' to '{NewType}' for {Url}",
originalContentType ?? "null",
contentType,
url);
response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
}
}
}
return response;
}
}