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;
///
/// Provides media sources (playback URLs) for SRF Play content.
///
public class SRFMediaProvider : IMediaSourceProvider
{
private readonly ILogger _logger;
private readonly IMediaCompositionFetcher _compositionFetcher;
private readonly IStreamUrlResolver _streamResolver;
private readonly IMediaSourceFactory _mediaSourceFactory;
///
/// Initializes a new instance of the class.
///
/// The logger factory.
/// The media composition fetcher.
/// The stream URL resolver.
/// The media source factory.
public SRFMediaProvider(
ILoggerFactory loggerFactory,
IMediaCompositionFetcher compositionFetcher,
IStreamUrlResolver streamResolver,
IMediaSourceFactory mediaSourceFactory)
{
_logger = loggerFactory.CreateLogger();
_compositionFetcher = compositionFetcher;
_streamResolver = streamResolver;
_mediaSourceFactory = mediaSourceFactory;
}
///
/// Gets the provider name.
///
public string Name => "SRF Play";
///
/// Gets media sources for the specified item.
///
/// The item.
/// The cancellation token.
/// List of media sources.
public async Task> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
{
var sources = new List();
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;
}
///
/// Gets direct stream provider by unique ID.
///
/// The unique ID.
/// The cancellation token.
/// The direct stream provider.
public Task GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
_logger.LogInformation("GetDirectStreamProviderByUniqueId called with uniqueId: {UniqueId}", uniqueId);
// Not needed for HTTP streams
return Task.FromResult(null);
}
///
public Task OpenMediaSource(string openToken, List 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");
}
}