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"); } }