using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.SRFPlay.Api; using Jellyfin.Plugin.SRFPlay.Services; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; 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 ILoggerFactory _loggerFactory; private readonly MetadataCache _metadataCache; private readonly StreamUrlResolver _streamResolver; /// /// Initializes a new instance of the class. /// /// The logger factory. /// The metadata cache. /// The stream URL resolver. public SRFMediaProvider( ILoggerFactory loggerFactory, MetadataCache metadataCache, StreamUrlResolver streamResolver) { _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); _metadataCache = metadataCache; _streamResolver = streamResolver; } /// /// 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 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 Task.FromResult>(sources); } _logger.LogDebug("Getting media sources for URN: {Urn}", urn); var config = Plugin.Instance?.Configuration; if (config == null) { return Task.FromResult>(sources); } // Try cache first var mediaComposition = _metadataCache.GetMediaComposition(urn, config.CacheDurationMinutes); // If not in cache, fetch from API if (mediaComposition == null) { using var apiClient = new SRFApiClient(_loggerFactory); mediaComposition = apiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).GetAwaiter().GetResult(); if (mediaComposition != null) { _metadataCache.SetMediaComposition(urn, mediaComposition); } } if (mediaComposition?.ChapterList == null || mediaComposition.ChapterList.Count == 0) { _logger.LogWarning("No chapters found for URN: {Urn}", urn); return Task.FromResult>(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 Task.FromResult>(sources); } // Check if content has playable streams if (!_streamResolver.HasPlayableContent(chapter)) { _logger.LogWarning("No playable content found for URN: {Urn}", urn); return Task.FromResult>(sources); } // Get stream URL based on quality preference var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference); if (string.IsNullOrEmpty(streamUrl)) { _logger.LogWarning("Could not resolve stream URL for URN: {Urn}", urn); return Task.FromResult>(sources); } // Create media source var mediaSource = new MediaSourceInfo { Id = urn, Name = chapter.Title, Path = streamUrl, Protocol = MediaProtocol.Http, Container = "m3u8", SupportsDirectStream = true, SupportsDirectPlay = true, SupportsTranscoding = true, IsRemote = true, Type = MediaSourceType.Default, RunTimeTicks = chapter.Duration > 0 ? TimeSpan.FromMilliseconds(chapter.Duration).Ticks : null, VideoType = VideoType.VideoFile, IsInfiniteStream = false, RequiresOpening = false, RequiresClosing = false, SupportsProbing = true }; // Add video stream info mediaSource.MediaStreams = new List { new MediaStream { Type = MediaStreamType.Video, Codec = "h264", IsInterlaced = false, IsDefault = true } }; sources.Add(mediaSource); _logger.LogInformation("Resolved stream URL for {Title}: {Url}", chapter.Title, streamUrl); } catch (Exception ex) { _logger.LogError(ex, "Error getting media sources for item: {Name}", item.Name); } return Task.FromResult>(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) { // Not needed for HTTP streams return Task.FromResult(null); } /// public Task OpenMediaSource(string openToken, List currentLiveStreams, CancellationToken cancellationToken) { // Not needed for static HTTP streams throw new NotImplementedException(); } }