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 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) { 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/*"); var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); // Fix Content-Type if it's binary/octet-stream - SRF CDN returns wrong content type // Jellyfin needs correct Content-Type to process images if (response.IsSuccessStatusCode && response.Content.Headers.ContentType?.MediaType == "binary/octet-stream") { // Determine correct content type from URL extension var contentType = GetContentTypeFromUrl(url); if (!string.IsNullOrEmpty(contentType)) { _logger.LogDebug("Fixing Content-Type from binary/octet-stream to {ContentType} for {Url}", contentType, url); response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); } } return response; } /// /// Determines the correct content type based on URL file extension. /// private static string? GetContentTypeFromUrl(string url) { if (string.IsNullOrEmpty(url)) { return null; } // Get the file extension from the URL (ignore query string) var uri = new Uri(url); var path = uri.AbsolutePath.ToLowerInvariant(); if (path.EndsWith(".jpg", StringComparison.Ordinal) || path.EndsWith(".jpeg", StringComparison.Ordinal)) { return "image/jpeg"; } if (path.EndsWith(".png", StringComparison.Ordinal)) { return "image/png"; } if (path.EndsWith(".gif", StringComparison.Ordinal)) { return "image/gif"; } if (path.EndsWith(".webp", StringComparison.Ordinal)) { return "image/webp"; } // Default to JPEG for SRF images (most common) return "image/jpeg"; } }