From 9146830546e875cc1bebc341b7df964a221bb528 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 7 Dec 2025 18:18:40 +0100 Subject: [PATCH] remove live-tv as it has issues with URI resolution --- .../LiveTv/SRFLiveTvService.cs | 390 ------------------ Jellyfin.Plugin.SRFPlay/ServiceRegistrator.cs | 5 - .../Services/MediaSourceFactory.cs | 1 + 3 files changed, 1 insertion(+), 395 deletions(-) delete mode 100644 Jellyfin.Plugin.SRFPlay/LiveTv/SRFLiveTvService.cs diff --git a/Jellyfin.Plugin.SRFPlay/LiveTv/SRFLiveTvService.cs b/Jellyfin.Plugin.SRFPlay/LiveTv/SRFLiveTvService.cs deleted file mode 100644 index 69675b0..0000000 --- a/Jellyfin.Plugin.SRFPlay/LiveTv/SRFLiveTvService.cs +++ /dev/null @@ -1,390 +0,0 @@ -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.Api.Models; -using Jellyfin.Plugin.SRFPlay.Configuration; -using Jellyfin.Plugin.SRFPlay.Services.Interfaces; -using Jellyfin.Plugin.SRFPlay.Utilities; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Plugin.SRFPlay.LiveTv; - -/// -/// Live TV service providing virtual channels for SRF scheduled livestreams. -/// -public class SRFLiveTvService : ILiveTvService -{ - private readonly ILogger _logger; - private readonly ISRFApiClientFactory _apiClientFactory; - private readonly IStreamUrlResolver _streamResolver; - private readonly IMediaSourceFactory _mediaSourceFactory; - - // Virtual channel definitions - private static readonly VirtualChannel[] VirtualChannels = new[] - { - new VirtualChannel("srf-sport", "SRF Sport", "SPORT", "Live sports events from SRF"), - new VirtualChannel("srf-events", "SRF Events", "EVENT", "Special events and broadcasts from SRF") - }; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The API client factory. - /// The stream URL resolver. - /// The media source factory. - public SRFLiveTvService( - ILogger logger, - ISRFApiClientFactory apiClientFactory, - IStreamUrlResolver streamResolver, - IMediaSourceFactory mediaSourceFactory) - { - _logger = logger; - _apiClientFactory = apiClientFactory; - _streamResolver = streamResolver; - _mediaSourceFactory = mediaSourceFactory; - } - - /// - public string Name => "SRF Play Live"; - - /// - public string HomePageUrl => "https://www.srf.ch/play"; - - /// - public async Task> GetChannelsAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("GetChannelsAsync called - returning {Count} virtual channels", VirtualChannels.Length); - - var channels = new List(); - - foreach (var vc in VirtualChannels) - { - channels.Add(new ChannelInfo - { - Id = vc.Id, - Name = vc.Name, - Number = vc.Id, // Use ID as channel number - ImageUrl = null, // Could add SRF logo here - ChannelType = ChannelType.TV - }); - } - - return await Task.FromResult(channels).ConfigureAwait(false); - } - - /// - public async Task> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) - { - _logger.LogInformation( - "GetProgramsAsync called for channel {ChannelId} from {Start} to {End}", - channelId, - startDateUtc, - endDateUtc); - - var programs = new List(); - var virtualChannel = VirtualChannels.FirstOrDefault(c => c.Id == channelId); - - if (virtualChannel == null) - { - _logger.LogWarning("Unknown channel ID: {ChannelId}", channelId); - return programs; - } - - try - { - var config = Plugin.Instance?.Configuration; - var businessUnit = config?.BusinessUnit.ToString().ToLowerInvariant() ?? "srf"; - - using var apiClient = _apiClientFactory.CreateClient(); - var scheduledLivestreams = await apiClient.GetScheduledLivestreamsAsync( - businessUnit, - virtualChannel.MediaType, - cancellationToken).ConfigureAwait(false); - - if (scheduledLivestreams == null) - { - _logger.LogWarning("No scheduled livestreams returned for channel {ChannelId}", channelId); - return programs; - } - - // Filter to events within the requested date range - var eventsInRange = scheduledLivestreams - .Where(e => e.Urn != null && - !string.IsNullOrEmpty(e.Title) && - e.ValidFrom != null && - e.ValidFrom.Value.ToUniversalTime() >= startDateUtc.AddHours(-3) && // Include events that started recently - e.ValidFrom.Value.ToUniversalTime() <= endDateUtc) - .OrderBy(e => e.ValidFrom) - .ToList(); - - _logger.LogInformation( - "Found {Count} scheduled events for channel {ChannelId} in date range", - eventsInRange.Count, - channelId); - - foreach (var evt in eventsInRange) - { - if (evt.Urn == null || evt.ValidFrom == null) - { - continue; - } - - var startTime = evt.ValidFrom.Value.ToUniversalTime(); - var endTime = evt.ValidTo?.ToUniversalTime() ?? startTime.AddHours(2); // Default 2h duration - - programs.Add(new ProgramInfo - { - Id = UrnHelper.ToGuid(evt.Urn), - ChannelId = channelId, - Name = evt.Title, - Overview = evt.Description ?? evt.Lead, - StartDate = startTime, - EndDate = endTime, - ImageUrl = evt.ImageUrl, - IsLive = true, - IsPremiere = false, - IsRepeat = false, - // Store URN in external IDs for later retrieval - EpisodeTitle = evt.Urn // Hack: Store URN here for stream lookup - }); - } - - // If no events, add an "Off Air" placeholder - if (programs.Count == 0) - { - programs.Add(new ProgramInfo - { - Id = $"{channelId}-offair-{startDateUtc:yyyyMMdd}", - ChannelId = channelId, - Name = "No Scheduled Events", - Overview = "There are no scheduled livestreams at this time. Check back later for upcoming events.", - StartDate = startDateUtc, - EndDate = endDateUtc, - IsLive = false - }); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching programs for channel {ChannelId}", channelId); - } - - return programs; - } - - /// - public async Task GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) - { - _logger.LogInformation( - "GetChannelStream called for channel {ChannelId}, stream {StreamId}", - channelId, - streamId ?? "(null)"); - - // Find what's currently live on this channel - var now = DateTime.UtcNow; - var programs = await GetProgramsAsync(channelId, now.AddHours(-1), now.AddHours(1), cancellationToken).ConfigureAwait(false); - - var currentProgram = programs - .Where(p => p.StartDate <= now && p.EndDate >= now && !string.IsNullOrEmpty(p.EpisodeTitle)) - .FirstOrDefault(); - - if (currentProgram == null || string.IsNullOrEmpty(currentProgram.EpisodeTitle)) - { - // Find the next upcoming program to show a helpful message - var nextProgram = programs - .Where(p => p.StartDate > now && !string.IsNullOrEmpty(p.EpisodeTitle)) - .OrderBy(p => p.StartDate) - .FirstOrDefault(); - - string message; - if (nextProgram != null) - { - var timeUntil = nextProgram.StartDate - now; - var timeStr = timeUntil.TotalHours >= 1 - ? $"{(int)timeUntil.TotalHours}h {timeUntil.Minutes}m" - : $"{timeUntil.Minutes}m"; - - message = $"No live content right now. Next: \"{nextProgram.Name}\" starts in {timeStr}"; - _logger.LogInformation( - "No live program on channel {ChannelId}. Next program: {NextProgram} at {StartTime}", - channelId, - nextProgram.Name, - nextProgram.StartDate); - } - else - { - message = "No live content available on this channel right now. Check back later for scheduled events."; - _logger.LogWarning("No live or upcoming programs found for channel {ChannelId}", channelId); - } - - throw new InvalidOperationException(message); - } - - var urn = currentProgram.EpisodeTitle; // We stored URN in EpisodeTitle - _logger.LogInformation("Found live program: {Title} (URN: {Urn})", currentProgram.Name, urn); - - // Fetch media composition and create media source - using var apiClient = _apiClientFactory.CreateClient(); - var mediaComposition = await apiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).ConfigureAwait(false); - - if (mediaComposition?.ChapterList == null || mediaComposition.ChapterList.Count == 0) - { - throw new InvalidOperationException($"Could not fetch stream for URN: {urn}"); - } - - var chapter = mediaComposition.ChapterList[0]; - var config = Plugin.Instance?.Configuration ?? new PluginConfiguration(); - - // Use channelId as base for MediaSourceInfo.Id - // This ensures Jellyfin can find the MediaSource during transcoding/playback - // The channelId is Jellyfin's internal ID for our virtual channel - var urnGuid = UrnHelper.ToGuid(urn); - var mediaSourceId = $"{channelId}_{urnGuid}"; - - _logger.LogInformation( - "Creating MediaSource with ID: {MediaSourceId} (channelId={ChannelId}, urnGuid={UrnGuid})", - mediaSourceId, - channelId, - urnGuid); - - var mediaSource = await _mediaSourceFactory.CreateMediaSourceAsync( - chapter, - mediaSourceId, - urn, - config.QualityPreference, - cancellationToken).ConfigureAwait(false); - - if (mediaSource == null) - { - throw new InvalidOperationException($"Could not create media source for URN: {urn}"); - } - - return mediaSource; - } - - /// - public Task> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) - { - _logger.LogDebug("GetChannelStreamMediaSources called for {ChannelId}", channelId); - // Return empty - Jellyfin will call GetChannelStream instead - return Task.FromResult(new List()); - } - - /// - public Task CloseLiveStream(string id, CancellationToken cancellationToken) - { - _logger.LogDebug("CloseLiveStream called for {Id}", id); - return Task.CompletedTask; - } - - /// - public Task ResetTuner(string id, CancellationToken cancellationToken) - { - _logger.LogDebug("ResetTuner called for {Id}", id); - return Task.CompletedTask; - } - - // Timer/Recording methods - Not supported (return empty/throw) - - /// - public Task> GetTimersAsync(CancellationToken cancellationToken) - { - return Task.FromResult(Enumerable.Empty()); - } - - /// - public Task GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo? program = null) - { - return Task.FromResult(new SeriesTimerInfo()); - } - - /// - public Task> GetSeriesTimersAsync(CancellationToken cancellationToken) - { - return Task.FromResult(Enumerable.Empty()); - } - - /// - public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) - { - throw new NotSupportedException("Recording is not supported by SRF Play"); - } - - /// - public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) - { - throw new NotSupportedException("Recording is not supported by SRF Play"); - } - - /// - public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken) - { - throw new NotSupportedException("Recording is not supported by SRF Play"); - } - - /// - public Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) - { - throw new NotSupportedException("Recording is not supported by SRF Play"); - } - - /// - public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken) - { - throw new NotSupportedException("Recording is not supported by SRF Play"); - } - - /// - public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken) - { - throw new NotSupportedException("Recording is not supported by SRF Play"); - } - - /// - /// Represents a virtual channel definition. - /// - private sealed class VirtualChannel - { - /// - /// Initializes a new instance of the class. - /// - /// The channel ID. - /// The channel name. - /// The media type filter for the API. - /// The channel description. - public VirtualChannel(string id, string name, string mediaType, string description) - { - Id = id; - Name = name; - MediaType = mediaType; - Description = description; - } - - /// - /// Gets the channel ID. - /// - public string Id { get; } - - /// - /// Gets the channel name. - /// - public string Name { get; } - - /// - /// Gets the media type filter for the API. - /// - public string MediaType { get; } - - /// - /// Gets the channel description. - /// - public string Description { get; } - } -} diff --git a/Jellyfin.Plugin.SRFPlay/ServiceRegistrator.cs b/Jellyfin.Plugin.SRFPlay/ServiceRegistrator.cs index 56331fa..fad5557 100644 --- a/Jellyfin.Plugin.SRFPlay/ServiceRegistrator.cs +++ b/Jellyfin.Plugin.SRFPlay/ServiceRegistrator.cs @@ -1,13 +1,11 @@ using Jellyfin.Plugin.SRFPlay.Api; using Jellyfin.Plugin.SRFPlay.Channels; -using Jellyfin.Plugin.SRFPlay.LiveTv; using Jellyfin.Plugin.SRFPlay.Providers; using Jellyfin.Plugin.SRFPlay.ScheduledTasks; using Jellyfin.Plugin.SRFPlay.Services; using Jellyfin.Plugin.SRFPlay.Services.Interfaces; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -49,8 +47,5 @@ public class ServiceRegistrator : IPluginServiceRegistrator // Register channel - must register as IChannel interface for Jellyfin to discover it serviceCollection.AddSingleton(); - - // Register Live TV service - provides virtual channels for scheduled livestreams - serviceCollection.AddSingleton(); } } diff --git a/Jellyfin.Plugin.SRFPlay/Services/MediaSourceFactory.cs b/Jellyfin.Plugin.SRFPlay/Services/MediaSourceFactory.cs index ee4dfd4..51a57c8 100644 --- a/Jellyfin.Plugin.SRFPlay/Services/MediaSourceFactory.cs +++ b/Jellyfin.Plugin.SRFPlay/Services/MediaSourceFactory.cs @@ -99,6 +99,7 @@ public class MediaSourceFactory : IMediaSourceFactory RunTimeTicks = chapter.Duration > 0 ? TimeSpan.FromMilliseconds(chapter.Duration).Ticks : null, VideoType = VideoType.VideoFile, IsInfiniteStream = isLiveStream, + // Don't use RequiresOpening - it forces Jellyfin to transcode which breaks token auth RequiresOpening = false, RequiresClosing = false, // Disable probing - we provide stream info directly