diff --git a/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs b/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs
index 3c19588..2d84f2b 100644
--- a/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs
+++ b/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs
@@ -489,18 +489,21 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
var overview = chapter.Description ?? chapter.Lead;
// Get image URL - prefer chapter image, fall back to show image if available
- var imageUrl = chapter.ImageUrl;
- if (string.IsNullOrEmpty(imageUrl) && mediaComposition.Show != null)
+ var originalImageUrl = chapter.ImageUrl;
+ if (string.IsNullOrEmpty(originalImageUrl) && mediaComposition.Show != null)
{
- imageUrl = mediaComposition.Show.ImageUrl;
+ originalImageUrl = mediaComposition.Show.ImageUrl;
_logger.LogDebug("URN {Urn}: Using show image as fallback", urn);
}
- if (string.IsNullOrEmpty(imageUrl))
+ if (string.IsNullOrEmpty(originalImageUrl))
{
_logger.LogWarning("URN {Urn}: No image URL available for '{Title}'", urn, chapter.Title);
}
+ // Proxy image URL to fix Content-Type headers from SRF CDN
+ var imageUrl = CreateProxiedImageUrl(originalImageUrl, serverUrl);
+
// Use ValidFrom for premiere date if this is a scheduled livestream, otherwise use Date
var premiereDate = chapter.Type == "SCHEDULED_LIVESTREAM" ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime();
@@ -617,6 +620,24 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
}
#pragma warning restore CA5351
+ ///
+ /// Creates a proxy URL for an image to fix Content-Type headers from SRF CDN.
+ ///
+ /// The original SRF image URL.
+ /// The server base URL.
+ /// The proxied image URL, or null if original is null.
+ private static string? CreateProxiedImageUrl(string? originalUrl, string serverUrl)
+ {
+ if (string.IsNullOrEmpty(originalUrl))
+ {
+ return null;
+ }
+
+ // Encode the original URL as base64 for safe transport
+ var encodedUrl = Convert.ToBase64String(Encoding.UTF8.GetBytes(originalUrl));
+ return $"{serverUrl}/Plugins/SRFPlay/Proxy/Image?url={encodedUrl}";
+ }
+
///
public bool IsEnabledFor(string userId)
{
diff --git a/Jellyfin.Plugin.SRFPlay/Controllers/StreamProxyController.cs b/Jellyfin.Plugin.SRFPlay/Controllers/StreamProxyController.cs
index e52c986..84b01d5 100644
--- a/Jellyfin.Plugin.SRFPlay/Controllers/StreamProxyController.cs
+++ b/Jellyfin.Plugin.SRFPlay/Controllers/StreamProxyController.cs
@@ -1,8 +1,11 @@
using System;
using System.Globalization;
+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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -19,16 +22,22 @@ public class StreamProxyController : ControllerBase
{
private readonly ILogger _logger;
private readonly IStreamProxyService _proxyService;
+ private readonly IHttpClientFactory _httpClientFactory;
///
/// Initializes a new instance of the class.
///
/// The logger.
/// The proxy service.
- public StreamProxyController(ILogger logger, IStreamProxyService proxyService)
+ /// The HTTP client factory.
+ public StreamProxyController(
+ ILogger logger,
+ IStreamProxyService proxyService,
+ IHttpClientFactory httpClientFactory)
{
_logger = logger;
_proxyService = proxyService;
+ _httpClientFactory = httpClientFactory;
}
///
@@ -316,4 +325,117 @@ public class StreamProxyController : ControllerBase
return "application/octet-stream";
}
+
+ ///
+ /// Proxies image requests from SRF CDN, fixing Content-Type headers.
+ ///
+ /// The original image URL (base64 encoded).
+ /// Cancellation token.
+ /// The image data with correct Content-Type.
+ [HttpGet("Image")]
+ [AllowAnonymous]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetImage(
+ [FromQuery] string url,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(url))
+ {
+ return BadRequest("URL parameter is required");
+ }
+
+ string decodedUrl;
+ try
+ {
+ // Decode base64 URL
+ var bytes = Convert.FromBase64String(url);
+ decodedUrl = System.Text.Encoding.UTF8.GetString(bytes);
+ }
+ catch (FormatException)
+ {
+ _logger.LogWarning("Invalid base64 URL parameter: {Url}", url);
+ return BadRequest("Invalid URL encoding");
+ }
+
+ _logger.LogDebug("Proxying image from: {Url}", decodedUrl);
+
+ try
+ {
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
+
+ // Create request with proper headers
+ var request = new HttpRequestMessage(HttpMethod.Get, new Uri(decodedUrl));
+ request.Headers.UserAgent.ParseAdd(
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
+ request.Headers.Accept.ParseAdd("image/jpeg, image/png, image/webp, image/*;q=0.8");
+
+ var response = await httpClient.SendAsync(
+ request,
+ HttpCompletionOption.ResponseHeadersRead,
+ cancellationToken).ConfigureAwait(false);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogWarning(
+ "Image fetch failed with status {StatusCode} for {Url}",
+ response.StatusCode,
+ decodedUrl);
+ return NotFound();
+ }
+
+ var imageData = await response.Content.ReadAsByteArrayAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ // Determine correct content type
+ var contentType = GetImageContentType(decodedUrl);
+
+ _logger.LogDebug(
+ "Returning proxied image ({Length} bytes, {ContentType})",
+ imageData.Length,
+ contentType);
+
+ return File(imageData, contentType);
+ }
+ catch (HttpRequestException ex)
+ {
+ _logger.LogError(ex, "Error fetching image from {Url}", decodedUrl);
+ return StatusCode(StatusCodes.Status502BadGateway);
+ }
+ }
+
+ ///
+ /// Gets the content type for an image based on URL or file extension.
+ ///
+ /// The image URL.
+ /// The MIME content type.
+ private static string GetImageContentType(string url)
+ {
+ if (string.IsNullOrEmpty(url))
+ {
+ return "image/jpeg";
+ }
+
+ var uri = new Uri(url);
+ var path = uri.AbsolutePath.ToLowerInvariant();
+
+ 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";
+ }
}
diff --git a/Jellyfin.Plugin.SRFPlay/Providers/SRFImageProvider.cs b/Jellyfin.Plugin.SRFPlay/Providers/SRFImageProvider.cs
index 59f7b04..c4d176b 100644
--- a/Jellyfin.Plugin.SRFPlay/Providers/SRFImageProvider.cs
+++ b/Jellyfin.Plugin.SRFPlay/Providers/SRFImageProvider.cs
@@ -156,26 +156,46 @@ public class SRFImageProvider : IRemoteImageProvider, IHasOrder
///
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/*");
+ request.Headers.Accept.ParseAdd("image/jpeg, image/png, image/webp, image/*;q=0.8");
var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
- // Fix Content-Type if it's binary/octet-stream - SRF CDN returns wrong content type
+ 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 &&
- response.Content.Headers.ContentType?.MediaType == "binary/octet-stream")
+ if (response.IsSuccessStatusCode)
{
- // Determine correct content type from URL extension
- var contentType = GetContentTypeFromUrl(url);
- if (!string.IsNullOrEmpty(contentType))
+ var needsContentTypeFix = string.IsNullOrEmpty(originalContentType) ||
+ originalContentType == "binary/octet-stream" ||
+ originalContentType == "application/octet-stream" ||
+ !originalContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
+
+ if (needsContentTypeFix)
{
- _logger.LogDebug("Fixing Content-Type from binary/octet-stream to {ContentType} for {Url}", contentType, url);
- response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
+ // Determine correct content type from URL extension or default to JPEG
+ var contentType = GetContentTypeFromUrl(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);
+ }
}
}