Duncan Tourolle 7c76402a4a
Some checks failed
🏗️ Build Plugin / build (push) Failing after 9s
🧪 Test Plugin / test (push) Successful in 1m28s
🚀 Release Plugin / build-and-release (push) Failing after 5s
Clean up dead code, consolidate duplication, fix redundancies
Remove 9 dead methods, 6 unused constants, and redundant
ReaderWriterLockSlim from MetadataCache. Consolidate repeated
patterns into HasChapters, IsPlayable, and ToLowerString helpers.
Extract shared API methods in SRFApiClient. Move variant manifest
rewriting from controller to StreamProxyService. Make Auto quality
distinct from HD. Update README architecture section.
2026-02-28 11:34:45 +01:00

206 lines
7.8 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.HasChapters)
{
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;
}
}