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; /// /// Provides images for SRF Play content. /// public class SRFImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; private readonly IMediaCompositionFetcher _compositionFetcher; /// /// Initializes a new instance of the class. /// /// The HTTP client factory. /// The logger factory. /// The media composition fetcher. public SRFImageProvider( IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, IMediaCompositionFetcher compositionFetcher) { _httpClientFactory = httpClientFactory; _logger = loggerFactory.CreateLogger(); _compositionFetcher = compositionFetcher; } /// public string Name => "SRF Play"; /// public int Order => 0; /// 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; } /// public IEnumerable GetSupportedImages(BaseItem item) { return new List { ImageType.Primary, ImageType.Backdrop, ImageType.Thumb }; } /// public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { var list = new List(); 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; } /// public async Task 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; } }