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