@@ -142,7 +155,12 @@
.addEventListener('pageshow', function() {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SRFPlayConfig.pluginUniqueId).then(function (config) {
- document.querySelector('#BusinessUnit').value = config.BusinessUnit;
+ var enabled = (config.EnabledBusinessUnits && config.EnabledBusinessUnits.length)
+ ? config.EnabledBusinessUnits
+ : [config.BusinessUnit]; // migrate legacy single value
+ ['SRF','RTS','RSI','RTR','SWI'].forEach(function(bu) {
+ document.querySelector('#bu' + bu).checked = enabled.indexOf(bu) !== -1;
+ });
document.querySelector('#QualityPreference').value = config.QualityPreference;
document.querySelector('#ContentRefreshIntervalHours').value = config.ContentRefreshIntervalHours;
document.querySelector('#ExpirationCheckIntervalHours').value = config.ExpirationCheckIntervalHours;
@@ -168,7 +186,11 @@
.addEventListener('submit', function(e) {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SRFPlayConfig.pluginUniqueId).then(function (config) {
- config.BusinessUnit = document.querySelector('#BusinessUnit').value;
+ config.EnabledBusinessUnits = ['SRF','RTS','RSI','RTR','SWI'].filter(function(bu) {
+ return document.querySelector('#bu' + bu).checked;
+ });
+ // Keep legacy field in sync with the first enabled unit for backwards compat.
+ config.BusinessUnit = config.EnabledBusinessUnits[0] || 'SRF';
config.QualityPreference = document.querySelector('#QualityPreference').value;
config.ContentRefreshIntervalHours = parseInt(document.querySelector('#ContentRefreshIntervalHours').value);
config.ExpirationCheckIntervalHours = parseInt(document.querySelector('#ExpirationCheckIntervalHours').value);
diff --git a/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj b/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj
index 5c74e22..c824c13 100644
--- a/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj
+++ b/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj
@@ -33,6 +33,11 @@
+
+
+
+
+
diff --git a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs
index 84678ca..4d9ef92 100644
--- a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs
+++ b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
+using Jellyfin.Plugin.SRFPlay.Utilities;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -63,20 +64,25 @@ public class ContentRefreshTask : IScheduledTask
return;
}
- // Refresh latest content
- if (config.EnableLatestContent)
+ // Refresh content for every enabled business unit (one channel per unit).
+ var units = config.ResolveEnabledUnits();
+ foreach (var unit in units)
{
- _logger.LogInformation("Refreshing latest content");
- progress?.Report(25);
- await _contentRefreshService.RefreshLatestContentAsync(cancellationToken).ConfigureAwait(false);
- }
+ var businessUnit = unit.ToLowerString();
- // Refresh trending content
- if (config.EnableTrendingContent)
- {
- _logger.LogInformation("Refreshing trending content");
- progress?.Report(75);
- await _contentRefreshService.RefreshTrendingContentAsync(cancellationToken).ConfigureAwait(false);
+ if (config.EnableLatestContent)
+ {
+ _logger.LogInformation("Refreshing latest content for {BusinessUnit}", businessUnit);
+ progress?.Report(25);
+ await _contentRefreshService.RefreshLatestContentAsync(businessUnit, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (config.EnableTrendingContent)
+ {
+ _logger.LogInformation("Refreshing trending content for {BusinessUnit}", businessUnit);
+ progress?.Report(75);
+ await _contentRefreshService.RefreshTrendingContentAsync(businessUnit, cancellationToken).ConfigureAwait(false);
+ }
}
progress?.Report(100);
diff --git a/Jellyfin.Plugin.SRFPlay/ServiceRegistrator.cs b/Jellyfin.Plugin.SRFPlay/ServiceRegistrator.cs
index 0ef6988..d862cb8 100644
--- a/Jellyfin.Plugin.SRFPlay/ServiceRegistrator.cs
+++ b/Jellyfin.Plugin.SRFPlay/ServiceRegistrator.cs
@@ -49,7 +49,13 @@ public class ServiceRegistrator : IPluginServiceRegistrator
serviceCollection.AddSingleton
();
serviceCollection.AddSingleton();
- // Register channel - must register as IChannel interface for Jellyfin to discover it
- serviceCollection.AddSingleton();
+ // Register one channel (tile) per SRG business unit. Each must be registered as IChannel
+ // for Jellyfin to discover it. A unit that is not in EnabledBusinessUnits returns no
+ // content (its tile appears empty) - see SrgChannelBase.
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
}
}
diff --git a/Jellyfin.Plugin.SRFPlay/Services/ContentRefreshService.cs b/Jellyfin.Plugin.SRFPlay/Services/ContentRefreshService.cs
index 60194af..0b4a303 100644
--- a/Jellyfin.Plugin.SRFPlay/Services/ContentRefreshService.cs
+++ b/Jellyfin.Plugin.SRFPlay/Services/ContentRefreshService.cs
@@ -34,9 +34,10 @@ public class ContentRefreshService : IContentRefreshService
///
/// Refreshes latest content from SRF API using Play v3.
///
+ /// The business unit to fetch content for (e.g. "srf", "rts").
/// The cancellation token.
/// List of URNs for new content.
- public async Task> RefreshLatestContentAsync(CancellationToken cancellationToken)
+ public async Task> RefreshLatestContentAsync(string businessUnit, CancellationToken cancellationToken)
{
var config = Plugin.Instance?.Configuration;
if (config == null || !config.EnableLatestContent)
@@ -46,7 +47,7 @@ public class ContentRefreshService : IContentRefreshService
}
return await FetchVideosFromShowsAsync(
- config.BusinessUnit.ToLowerString(),
+ businessUnit,
minEpisodeCount: 0,
maxShows: 20,
videosPerShow: 1,
@@ -58,9 +59,10 @@ public class ContentRefreshService : IContentRefreshService
/// Refreshes trending content from SRF API using Play v3.
/// Gets videos from shows with the most episodes.
///
+ /// The business unit to fetch content for (e.g. "srf", "rts").
/// The cancellation token.
/// List of URNs for trending content.
- public async Task> RefreshTrendingContentAsync(CancellationToken cancellationToken)
+ public async Task> RefreshTrendingContentAsync(string businessUnit, CancellationToken cancellationToken)
{
var config = Plugin.Instance?.Configuration;
if (config == null || !config.EnableTrendingContent)
@@ -70,7 +72,7 @@ public class ContentRefreshService : IContentRefreshService
}
return await FetchVideosFromShowsAsync(
- config.BusinessUnit.ToLowerString(),
+ businessUnit,
minEpisodeCount: 10,
maxShows: 15,
videosPerShow: 2,
diff --git a/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IContentRefreshService.cs b/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IContentRefreshService.cs
index b0fd5c6..acaa66c 100644
--- a/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IContentRefreshService.cs
+++ b/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IContentRefreshService.cs
@@ -12,14 +12,16 @@ public interface IContentRefreshService
///
/// Refreshes latest content from SRF API using Play v3.
///
+ /// The business unit to fetch content for (e.g. "srf", "rts").
/// The cancellation token.
/// List of URNs for new content.
- Task> RefreshLatestContentAsync(CancellationToken cancellationToken);
+ Task> RefreshLatestContentAsync(string businessUnit, CancellationToken cancellationToken);
///
/// Refreshes trending content from SRF API using Play v3.
///
+ /// The business unit to fetch content for (e.g. "srf", "rts").
/// The cancellation token.
/// List of URNs for trending content.
- Task> RefreshTrendingContentAsync(CancellationToken cancellationToken);
+ Task> RefreshTrendingContentAsync(string businessUnit, CancellationToken cancellationToken);
}
diff --git a/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs b/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs
index e8f1037..2b14467 100644
--- a/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs
+++ b/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs
@@ -148,18 +148,25 @@ public class RecordingService : IRecordingService, IDisposable
public async Task> GetUpcomingScheduleAsync(CancellationToken cancellationToken)
{
var config = Plugin.Instance?.Configuration;
- var businessUnit = (config?.BusinessUnit ?? Configuration.BusinessUnit.SRF).ToString().ToLowerInvariant();
+ var units = config?.ResolveEnabledUnits() ?? new[] { Configuration.BusinessUnit.SRF };
using var apiClient = _apiClientFactory.CreateClient();
- var livestreams = await apiClient.GetScheduledLivestreamsAsync(businessUnit, "SPORT", cancellationToken).ConfigureAwait(false);
- if (livestreams == null)
+ // Aggregate sport livestreams across every enabled business unit so the recordings
+ // page shows events from all enabled languages.
+ var all = new List();
+ foreach (var unit in units)
{
- return Array.Empty();
+ var businessUnit = unit.ToString().ToLowerInvariant();
+ var livestreams = await apiClient.GetScheduledLivestreamsAsync(businessUnit, "SPORT", cancellationToken).ConfigureAwait(false);
+ if (livestreams != null)
+ {
+ all.AddRange(livestreams);
+ }
}
// Filter to only future/current livestreams that aren't blocked
- return livestreams
+ return all
.Where(ls => ls.Blocked != true && (ls.ValidTo == null || ls.ValidTo.Value.ToUniversalTime() > DateTime.UtcNow))
.OrderBy(ls => ls.ValidFrom)
.ToList();
@@ -178,9 +185,11 @@ public class RecordingService : IRecordingService, IDisposable
return existing;
}
- // Fetch metadata for the URN
+ // Fetch metadata for the URN. The unit is encoded in the URN itself, so use that to
+ // query the right schedule regardless of which units are enabled.
var config = Plugin.Instance?.Configuration;
- var businessUnit = (config?.BusinessUnit ?? Configuration.BusinessUnit.SRF).ToString().ToLowerInvariant();
+ var businessUnit = ParseBusinessUnitFromUrn(urn)
+ ?? (config?.BusinessUnit ?? Configuration.BusinessUnit.SRF).ToString().ToLowerInvariant();
using var apiClient = _apiClientFactory.CreateClient();
var livestreams = await apiClient.GetScheduledLivestreamsAsync(businessUnit, "SPORT", cancellationToken).ConfigureAwait(false);
@@ -190,6 +199,7 @@ public class RecordingService : IRecordingService, IDisposable
{
Id = Guid.NewGuid().ToString("N"),
Urn = urn,
+ BusinessUnit = ParseBusinessUnitFromUrn(urn) ?? businessUnit,
Title = program?.Title ?? urn,
Description = program?.Lead ?? program?.Description,
ImageUrl = program?.ImageUrl,
@@ -206,6 +216,24 @@ public class RecordingService : IRecordingService, IDisposable
return entry;
}
+ ///
+ /// Extracts the business unit from an SRF URN of the form "urn:<bu>:<type>:...".
+ ///
+ /// The URN.
+ /// The lowercase business unit, or null if it cannot be determined.
+ private static string? ParseBusinessUnitFromUrn(string urn)
+ {
+ if (string.IsNullOrEmpty(urn))
+ {
+ return null;
+ }
+
+ var parts = urn.Split(':');
+ return parts.Length >= 2 && !string.IsNullOrEmpty(parts[1])
+ ? parts[1].ToLowerInvariant()
+ : null;
+ }
+
///
public bool CancelRecording(string recordingId)
{
diff --git a/assests/units/rsi-logo.png b/assests/units/rsi-logo.png
new file mode 100644
index 0000000..b7d3e8e
Binary files /dev/null and b/assests/units/rsi-logo.png differ
diff --git a/assests/units/rtr-logo.png b/assests/units/rtr-logo.png
new file mode 100644
index 0000000..e9c6a99
Binary files /dev/null and b/assests/units/rtr-logo.png differ
diff --git a/assests/units/rts-logo.png b/assests/units/rts-logo.png
new file mode 100644
index 0000000..34070f4
Binary files /dev/null and b/assests/units/rts-logo.png differ
diff --git a/assests/units/srf-logo.png b/assests/units/srf-logo.png
new file mode 100644
index 0000000..86d4162
Binary files /dev/null and b/assests/units/srf-logo.png differ
diff --git a/assests/units/swi-logo.png b/assests/units/swi-logo.png
new file mode 100644
index 0000000..6f98cd2
Binary files /dev/null and b/assests/units/swi-logo.png differ