tokens refresh on media start
All checks were successful
🏗️ Build Plugin / build (push) Successful in 2m19s
🧪 Test Plugin / test (push) Successful in 1m8s
🚀 Release Plugin / build-and-release (push) Successful in 2m18s

This commit is contained in:
Duncan Tourolle 2025-11-15 17:51:14 +01:00
parent d48b515898
commit 8e86db100a
3 changed files with 38 additions and 86 deletions

View File

@ -61,7 +61,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
public string Description => "Swiss Radio and Television video-on-demand content"; public string Description => "Swiss Radio and Television video-on-demand content";
/// <inheritdoc /> /// <inheritdoc />
public string DataVersion => "1.0"; public string DataVersion => "1.1";
/// <inheritdoc /> /// <inheritdoc />
public string HomePageUrl => "https://www.srf.ch/play"; public string HomePageUrl => "https://www.srf.ch/play";
@ -433,36 +433,34 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
// Generate deterministic GUID from URN // Generate deterministic GUID from URN
var itemId = UrnToGuid(urn); var itemId = UrnToGuid(urn);
// Get stream URL and authenticate it // Get stream URL (unauthenticated - token will be added at playback time by SRFMediaProvider)
var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference); var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference);
// For scheduled livestreams that haven't started, streamUrl might be null // Skip scheduled livestreams that haven't started yet (no stream URL available)
var isUpcomingLivestream = chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl); if (chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl))
if (!string.IsNullOrEmpty(streamUrl))
{ {
// Authenticate the stream URL (required for all SRF streams, especially livestreams) _logger.LogDebug(
streamUrl = await _streamResolver.GetAuthenticatedStreamUrlAsync(streamUrl, cancellationToken).ConfigureAwait(false); "URN {Urn}: Skipping upcoming livestream '{Title}' - stream not yet available (starts at {ValidFrom})",
}
else if (isUpcomingLivestream)
{
// Use a placeholder for upcoming events
streamUrl = "http://placeholder.local/upcoming.m3u8";
_logger.LogInformation(
"URN {Urn}: Upcoming livestream '{Title}' - stream will be available at {ValidFrom}",
urn, urn,
chapter.Title, chapter.Title,
chapter.ValidFrom); chapter.ValidFrom);
continue;
} }
// Build overview with event time for upcoming livestreams // Skip items without a valid stream URL
var overview = chapter.Description ?? chapter.Lead; if (string.IsNullOrEmpty(streamUrl))
if (isUpcomingLivestream && chapter.ValidFrom != null)
{ {
var eventStart = chapter.ValidFrom.Value.ToString("dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture); _logger.LogWarning(
overview = $"[Upcoming Event - Starts at {eventStart}]\n\n{overview}"; "URN {Urn}: Skipping '{Title}' - no valid stream URL available",
urn,
chapter.Title);
noStreamCount++;
continue;
} }
// Build overview
var overview = chapter.Description ?? chapter.Lead;
// Get image URL - prefer chapter image, fall back to show image if available // Get image URL - prefer chapter image, fall back to show image if available
var imageUrl = chapter.ImageUrl; var imageUrl = chapter.ImageUrl;
if (string.IsNullOrEmpty(imageUrl) && mediaComposition.Show != null) if (string.IsNullOrEmpty(imageUrl) && mediaComposition.Show != null)
@ -476,9 +474,10 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
_logger.LogWarning("URN {Urn}: No image URL available for '{Title}'", urn, chapter.Title); _logger.LogWarning("URN {Urn}: No image URL available for '{Title}'", urn, chapter.Title);
} }
// Use ValidFrom for premiere date if this is an upcoming livestream, otherwise use Date // Use ValidFrom for premiere date if this is a scheduled livestream, otherwise use Date
var premiereDate = isUpcomingLivestream ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime(); var premiereDate = chapter.Type == "SCHEDULED_LIVESTREAM" ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime();
// Store unauthenticated URL as placeholder - SRFMediaProvider will provide authenticated version at playback
var item = new ChannelItemInfo var item = new ChannelItemInfo
{ {
Id = itemId, Id = itemId,
@ -502,14 +501,14 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
{ {
Id = itemId, Id = itemId,
Name = chapter.Title, Name = chapter.Title,
Path = streamUrl, Path = streamUrl, // Unauthenticated URL - placeholder only
Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http, Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http,
Container = "m3u8", Container = "m3u8",
SupportsDirectStream = true, SupportsDirectPlay = false, // Disable direct play - requires auth from provider
SupportsDirectPlay = true, SupportsDirectStream = false, // Disable direct stream - requires auth from provider
SupportsTranscoding = true, SupportsTranscoding = false, // Force use of IMediaSourceProvider
IsRemote = true, IsRemote = true,
Type = MediaBrowser.Model.Dto.MediaSourceType.Default, Type = MediaBrowser.Model.Dto.MediaSourceType.Placeholder, // Mark as placeholder
VideoType = VideoType.VideoFile VideoType = VideoType.VideoFile
} }
} }

View File

@ -119,37 +119,11 @@ public class SRFMediaProvider : IMediaSourceProvider
// Get stream URL based on quality preference // Get stream URL based on quality preference
var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference); var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference);
// Check if this is an upcoming livestream that hasn't started yet // For scheduled livestreams, always fetch fresh data to ensure stream URL is current
var isUpcomingLivestream = chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl); if (chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl))
if (string.IsNullOrEmpty(streamUrl) && !isUpcomingLivestream)
{ {
_logger.LogWarning("Could not resolve stream URL for URN: {Urn}", urn); _logger.LogDebug("URN {Urn}: Scheduled livestream has no stream URL, fetching fresh data", urn);
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources);
}
// For upcoming livestreams, check if the event has started
if (isUpcomingLivestream)
{
if (chapter.ValidFrom != null)
{
var eventStart = chapter.ValidFrom.Value.ToUniversalTime();
var now = DateTime.UtcNow;
if (eventStart > now)
{
_logger.LogInformation(
"URN {Urn}: Livestream '{Title}' hasn't started yet (starts at {ValidFrom}). User should refresh when live.",
urn,
chapter.Title,
chapter.ValidFrom);
// Return empty sources - event not yet playable
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources);
}
}
// Event should be live now - re-fetch media composition without cache
using var freshApiClient = new SRFApiClient(_loggerFactory); using var freshApiClient = new SRFApiClient(_loggerFactory);
var freshMediaComposition = freshApiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).GetAwaiter().GetResult(); var freshMediaComposition = freshApiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).GetAwaiter().GetResult();
@ -163,19 +137,15 @@ public class SRFMediaProvider : IMediaSourceProvider
// Update cache with fresh data // Update cache with fresh data
_metadataCache.SetMediaComposition(urn, freshMediaComposition); _metadataCache.SetMediaComposition(urn, freshMediaComposition);
chapter = freshChapter; chapter = freshChapter;
_logger.LogInformation("URN {Urn}: Livestream is now live, got fresh stream URL", urn); _logger.LogInformation("URN {Urn}: Got fresh stream URL for scheduled livestream", urn);
}
else
{
_logger.LogWarning("URN {Urn}: Livestream should be live but still no stream URL available", urn);
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources);
} }
} }
else }
{
_logger.LogWarning("URN {Urn}: Failed to fetch fresh media composition for livestream", urn); if (string.IsNullOrEmpty(streamUrl))
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources); {
} _logger.LogWarning("Could not resolve stream URL for URN: {Urn}", urn);
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources);
} }
// Authenticate the stream URL (required for all SRF streams, especially livestreams) // Authenticate the stream URL (required for all SRF streams, especially livestreams)
@ -188,13 +158,13 @@ public class SRFMediaProvider : IMediaSourceProvider
// Create media source // Create media source
var mediaSource = new MediaSourceInfo var mediaSource = new MediaSourceInfo
{ {
Id = urn, Id = item.Id.ToString(), // Use item GUID, not URN string (required for transcoding)
Name = chapter.Title, Name = chapter.Title,
Path = streamUrl, Path = streamUrl,
Protocol = MediaProtocol.Http, Protocol = MediaProtocol.Http,
Container = "m3u8", Container = "m3u8",
SupportsDirectStream = true, SupportsDirectStream = true,
SupportsDirectPlay = true, SupportsDirectPlay = false, // Disabled: auth tokens don't carry over to HLS segments in browser
SupportsTranscoding = true, SupportsTranscoding = true,
IsRemote = true, IsRemote = true,
Type = MediaSourceType.Default, Type = MediaSourceType.Default,

View File

@ -144,23 +144,6 @@ public class StreamUrlResolver : IDisposable
/// <returns>True if playable content is available.</returns> /// <returns>True if playable content is available.</returns>
public bool HasPlayableContent(Chapter chapter) public bool HasPlayableContent(Chapter chapter)
{ {
// For scheduled livestreams that haven't started yet, resources won't exist
// but we still want to show them. The stream will become available when the event starts.
if (chapter?.Type == "SCHEDULED_LIVESTREAM")
{
var now = DateTime.UtcNow;
var hasStarted = chapter.ValidFrom == null || chapter.ValidFrom.Value.ToUniversalTime() <= now;
if (!hasStarted)
{
_logger.LogInformation(
"Scheduled livestream '{Title}' hasn't started yet (starts at {ValidFrom}), will be playable when live",
chapter.Title,
chapter.ValidFrom);
return true; // Show it, stream will be available when event starts
}
}
if (chapter?.ResourceList == null || chapter.ResourceList.Count == 0) if (chapter?.ResourceList == null || chapter.ResourceList.Count == 0)
{ {
return false; return false;