180 lines
7.3 KiB
C#
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");
|
|
}
|
|
}
|