Duncan Tourolle ed4cc0990c
All checks were successful
🏗️ Build Plugin / build (push) Successful in 2m47s
🧪 Test Plugin / test (push) Successful in 1m22s
🚀 Release Plugin / build-and-release (push) Successful in 2m39s
mixed refactor
2025-12-06 20:18:43 +01:00

180 lines
7.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SRFPlay.Configuration;
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.SRFPlay.Providers;
/// <summary>
/// Provides media sources (playback URLs) for SRF Play content.
/// </summary>
public class SRFMediaProvider : IMediaSourceProvider
{
private readonly ILogger<SRFMediaProvider> _logger;
private readonly IMediaCompositionFetcher _compositionFetcher;
private readonly IStreamUrlResolver _streamResolver;
private readonly IMediaSourceFactory _mediaSourceFactory;
/// <summary>
/// Initializes a new instance of the <see cref="SRFMediaProvider"/> class.
/// </summary>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="compositionFetcher">The media composition fetcher.</param>
/// <param name="streamResolver">The stream URL resolver.</param>
/// <param name="mediaSourceFactory">The media source factory.</param>
public SRFMediaProvider(
ILoggerFactory loggerFactory,
IMediaCompositionFetcher compositionFetcher,
IStreamUrlResolver streamResolver,
IMediaSourceFactory mediaSourceFactory)
{
_logger = loggerFactory.CreateLogger<SRFMediaProvider>();
_compositionFetcher = compositionFetcher;
_streamResolver = streamResolver;
_mediaSourceFactory = mediaSourceFactory;
}
/// <summary>
/// Gets the provider name.
/// </summary>
public string Name => "SRF Play";
/// <summary>
/// Gets media sources for the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>List of media sources.</returns>
public async Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
{
var sources = new List<MediaSourceInfo>();
try
{
// Check if this is an SRF item
if (!item.ProviderIds.TryGetValue("SRF", out var urn) || string.IsNullOrEmpty(urn))
{
return sources;
}
_logger.LogDebug("GetMediaSources for URN: {Urn}, Item: {ItemName}", urn, item.Name);
// For scheduled livestreams, use shorter cache TTL (5 minutes) so they refresh when they go live
var cacheDuration = urn.Contains("scheduled_livestream", StringComparison.OrdinalIgnoreCase) ? 5 : (int?)null;
var mediaComposition = await _compositionFetcher.GetMediaCompositionAsync(urn, cancellationToken, cacheDuration).ConfigureAwait(false);
if (mediaComposition?.ChapterList == null || mediaComposition.ChapterList.Count == 0)
{
_logger.LogWarning("No chapters found for URN: {Urn}", urn);
return sources;
}
// Get the first chapter (main video)
var chapter = mediaComposition.ChapterList[0];
// Check if content is expired
if (_streamResolver.IsContentExpired(chapter))
{
_logger.LogWarning("Content expired for URN: {Urn}, ValidTo: {ValidTo}", urn, chapter.ValidTo);
return sources;
}
// Check if content has playable streams
if (!_streamResolver.HasPlayableContent(chapter))
{
_logger.LogWarning("No playable content found for URN: {Urn}", urn);
return sources;
}
// Get quality preference from config
var config = Plugin.Instance?.Configuration;
var qualityPref = config?.QualityPreference ?? QualityPreference.HD;
// Use item ID in hex format without dashes
var itemIdStr = item.Id.ToString("N");
// Use factory to create MediaSourceInfo
var mediaSource = await _mediaSourceFactory.CreateMediaSourceAsync(
chapter,
itemIdStr,
urn,
qualityPref,
cancellationToken).ConfigureAwait(false);
// For scheduled livestreams, retry with fresh data if no stream URL
if (mediaSource == null && chapter.Type == "SCHEDULED_LIVESTREAM")
{
_logger.LogDebug("URN {Urn}: Scheduled livestream has no stream URL, fetching fresh data", urn);
// Force fresh fetch with short cache duration
var freshMediaComposition = await _compositionFetcher.GetMediaCompositionAsync(urn, cancellationToken, 1).ConfigureAwait(false);
if (freshMediaComposition?.ChapterList != null && freshMediaComposition.ChapterList.Count > 0)
{
var freshChapter = freshMediaComposition.ChapterList[0];
mediaSource = await _mediaSourceFactory.CreateMediaSourceAsync(
freshChapter,
itemIdStr,
urn,
qualityPref,
cancellationToken).ConfigureAwait(false);
if (mediaSource != null)
{
chapter = freshChapter;
_logger.LogInformation("URN {Urn}: Got fresh stream URL for scheduled livestream", urn);
}
}
}
if (mediaSource == null)
{
_logger.LogWarning("Could not resolve stream URL for URN: {Urn}", urn);
return sources;
}
sources.Add(mediaSource);
_logger.LogDebug(
"MediaSource created for {Title} - Id={Id}, DirectPlay={DirectPlay}, Transcoding={Transcoding}",
chapter.Title,
mediaSource.Id,
mediaSource.SupportsDirectPlay,
mediaSource.SupportsTranscoding);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting media sources for item: {Name}", item.Name);
}
return sources;
}
/// <summary>
/// Gets direct stream provider by unique ID.
/// </summary>
/// <param name="uniqueId">The unique ID.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The direct stream provider.</returns>
public Task<IDirectStreamProvider?> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
_logger.LogInformation("GetDirectStreamProviderByUniqueId called with uniqueId: {UniqueId}", uniqueId);
// Not needed for HTTP streams
return Task.FromResult<IDirectStreamProvider?>(null);
}
/// <inheritdoc />
public Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
// Not used - RequiresOpening is false, proxy handles authentication directly
_logger.LogWarning("OpenMediaSource unexpectedly called with openToken: {OpenToken}", openToken);
throw new NotSupportedException("OpenMediaSource not supported - streams use direct proxy access");
}
}