diff --git a/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs b/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs index 23bb333..5e63e77 100644 --- a/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs +++ b/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs @@ -61,7 +61,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey public string Description => "Swiss Radio and Television video-on-demand content"; /// - public string DataVersion => "1.0"; + public string DataVersion => "1.1"; /// public string HomePageUrl => "https://www.srf.ch/play"; @@ -433,36 +433,34 @@ public class SRFPlayChannel : IChannel, IHasCacheKey // Generate deterministic GUID from 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); - // For scheduled livestreams that haven't started, streamUrl might be null - var isUpcomingLivestream = chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl); - - if (!string.IsNullOrEmpty(streamUrl)) + // Skip scheduled livestreams that haven't started yet (no stream URL available) + if (chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl)) { - // Authenticate the stream URL (required for all SRF streams, especially livestreams) - streamUrl = await _streamResolver.GetAuthenticatedStreamUrlAsync(streamUrl, cancellationToken).ConfigureAwait(false); - } - 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}", + _logger.LogDebug( + "URN {Urn}: Skipping upcoming livestream '{Title}' - stream not yet available (starts at {ValidFrom})", urn, chapter.Title, chapter.ValidFrom); + continue; } - // Build overview with event time for upcoming livestreams - var overview = chapter.Description ?? chapter.Lead; - if (isUpcomingLivestream && chapter.ValidFrom != null) + // Skip items without a valid stream URL + if (string.IsNullOrEmpty(streamUrl)) { - var eventStart = chapter.ValidFrom.Value.ToString("dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture); - overview = $"[Upcoming Event - Starts at {eventStart}]\n\n{overview}"; + _logger.LogWarning( + "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 var imageUrl = chapter.ImageUrl; 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); } - // Use ValidFrom for premiere date if this is an upcoming livestream, otherwise use Date - var premiereDate = isUpcomingLivestream ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime(); + // Use ValidFrom for premiere date if this is a scheduled livestream, otherwise use Date + 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 { Id = itemId, @@ -502,14 +501,14 @@ public class SRFPlayChannel : IChannel, IHasCacheKey { Id = itemId, Name = chapter.Title, - Path = streamUrl, + Path = streamUrl, // Unauthenticated URL - placeholder only Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http, Container = "m3u8", - SupportsDirectStream = true, - SupportsDirectPlay = true, - SupportsTranscoding = true, + SupportsDirectPlay = false, // Disable direct play - requires auth from provider + SupportsDirectStream = false, // Disable direct stream - requires auth from provider + SupportsTranscoding = false, // Force use of IMediaSourceProvider IsRemote = true, - Type = MediaBrowser.Model.Dto.MediaSourceType.Default, + Type = MediaBrowser.Model.Dto.MediaSourceType.Placeholder, // Mark as placeholder VideoType = VideoType.VideoFile } } diff --git a/Jellyfin.Plugin.SRFPlay/Providers/SRFMediaProvider.cs b/Jellyfin.Plugin.SRFPlay/Providers/SRFMediaProvider.cs index 8d45270..51b497b 100644 --- a/Jellyfin.Plugin.SRFPlay/Providers/SRFMediaProvider.cs +++ b/Jellyfin.Plugin.SRFPlay/Providers/SRFMediaProvider.cs @@ -119,37 +119,11 @@ public class SRFMediaProvider : IMediaSourceProvider // Get stream URL based on quality preference var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference); - // Check if this is an upcoming livestream that hasn't started yet - var isUpcomingLivestream = chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl); - - if (string.IsNullOrEmpty(streamUrl) && !isUpcomingLivestream) + // For scheduled livestreams, always fetch fresh data to ensure stream URL is current + if (chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl)) { - _logger.LogWarning("Could not resolve stream URL for URN: {Urn}", urn); - return Task.FromResult>(sources); - } + _logger.LogDebug("URN {Urn}: Scheduled livestream has no stream URL, fetching fresh data", urn); - // 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>(sources); - } - } - - // Event should be live now - re-fetch media composition without cache using var freshApiClient = new SRFApiClient(_loggerFactory); var freshMediaComposition = freshApiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).GetAwaiter().GetResult(); @@ -163,19 +137,15 @@ public class SRFMediaProvider : IMediaSourceProvider // Update cache with fresh data _metadataCache.SetMediaComposition(urn, freshMediaComposition); chapter = freshChapter; - _logger.LogInformation("URN {Urn}: Livestream is now live, got fresh stream URL", urn); - } - else - { - _logger.LogWarning("URN {Urn}: Livestream should be live but still no stream URL available", urn); - return Task.FromResult>(sources); + _logger.LogInformation("URN {Urn}: Got fresh stream URL for scheduled livestream", urn); } } - else - { - _logger.LogWarning("URN {Urn}: Failed to fetch fresh media composition for livestream", urn); - return Task.FromResult>(sources); - } + } + + if (string.IsNullOrEmpty(streamUrl)) + { + _logger.LogWarning("Could not resolve stream URL for URN: {Urn}", urn); + return Task.FromResult>(sources); } // Authenticate the stream URL (required for all SRF streams, especially livestreams) @@ -188,13 +158,13 @@ public class SRFMediaProvider : IMediaSourceProvider // Create media source var mediaSource = new MediaSourceInfo { - Id = urn, + Id = item.Id.ToString(), // Use item GUID, not URN string (required for transcoding) Name = chapter.Title, Path = streamUrl, Protocol = MediaProtocol.Http, Container = "m3u8", SupportsDirectStream = true, - SupportsDirectPlay = true, + SupportsDirectPlay = false, // Disabled: auth tokens don't carry over to HLS segments in browser SupportsTranscoding = true, IsRemote = true, Type = MediaSourceType.Default, diff --git a/Jellyfin.Plugin.SRFPlay/Services/StreamUrlResolver.cs b/Jellyfin.Plugin.SRFPlay/Services/StreamUrlResolver.cs index ffc9da3..a29e5a5 100644 --- a/Jellyfin.Plugin.SRFPlay/Services/StreamUrlResolver.cs +++ b/Jellyfin.Plugin.SRFPlay/Services/StreamUrlResolver.cs @@ -144,23 +144,6 @@ public class StreamUrlResolver : IDisposable /// True if playable content is available. 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) { return false;