diff --git a/Jellyfin.Plugin.SRFPlay/Channels/SrgChannelBase.cs b/Jellyfin.Plugin.SRFPlay/Channels/SrgChannelBase.cs
index e45cbda..fc6b8a5 100644
--- a/Jellyfin.Plugin.SRFPlay/Channels/SrgChannelBase.cs
+++ b/Jellyfin.Plugin.SRFPlay/Channels/SrgChannelBase.cs
@@ -303,8 +303,8 @@ public abstract class SrgChannelBase : IChannel, IHasCacheKey
if (matchingEvent?.ValidFrom != null)
{
var eventTime = matchingEvent.ValidFrom.Value;
- item.Name = $"[{eventTime:dd.MM HH:mm}] {matchingEvent.Title}";
- item.PremiereDate = eventTime;
+ item.Name = $"[{DisplayTime.From(eventTime):dd.MM HH:mm}] {matchingEvent.Title}";
+ item.PremiereDate = eventTime.ToUniversalTime();
}
}
}
diff --git a/Jellyfin.Plugin.SRFPlay/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.SRFPlay/Configuration/PluginConfiguration.cs
index 138b142..65b749c 100644
--- a/Jellyfin.Plugin.SRFPlay/Configuration/PluginConfiguration.cs
+++ b/Jellyfin.Plugin.SRFPlay/Configuration/PluginConfiguration.cs
@@ -169,6 +169,14 @@ public class PluginConfiguration : BasePluginConfiguration
///
public string RecordingOutputPath { get; set; } = string.Empty;
+ ///
+ /// Gets or sets the time zone used for times the plugin writes as text: the start time in
+ /// upcoming livestream names and the timestamp in recording file names. An IANA id such as
+ /// "Europe/Zurich"; empty uses the server's time zone. Dates Jellyfin shows itself (e.g.
+ /// PremiereDate) are stored as UTC and follow each viewer's own time zone regardless.
+ ///
+ public string DisplayTimeZone { get; set; } = string.Empty;
+
///
/// Gets or sets how many segments back from the live edge livestream playback should start.
/// Injected as #EXT-X-START:TIME-OFFSET=-(N * targetDuration) into the live media
diff --git a/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html b/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html
index 1d56931..1e18ab3 100644
--- a/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html
+++ b/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html
@@ -82,6 +82,11 @@
Directory where sport livestream recordings will be saved (requires ffmpeg)
+
+
+
+
Time zone for start times in upcoming livestream names and for recording file names. Leave empty to use the server's time zone.
+
@@ -200,6 +205,7 @@
document.querySelector('#ProxyPassword').value = config.ProxyPassword || '';
document.querySelector('#PublicServerUrl').value = config.PublicServerUrl || '';
document.querySelector('#RecordingOutputPath').value = config.RecordingOutputPath || '';
+ document.querySelector('#DisplayTimeZone').value = config.DisplayTimeZone || '';
document.querySelector('#LiveStartSegmentsBack').value = config.LiveStartSegmentsBack != null ? config.LiveStartSegmentsBack : 3;
document.querySelector('#CleanUpResumePoints').checked = config.CleanUpResumePoints !== false;
document.querySelector('#ClearLiveStreamResumePoints').checked = config.ClearLiveStreamResumePoints !== false;
@@ -231,6 +237,7 @@
config.ProxyPassword = document.querySelector('#ProxyPassword').value;
config.PublicServerUrl = document.querySelector('#PublicServerUrl').value;
config.RecordingOutputPath = document.querySelector('#RecordingOutputPath').value;
+ config.DisplayTimeZone = document.querySelector('#DisplayTimeZone').value.trim();
config.LiveStartSegmentsBack = parseInt(document.querySelector('#LiveStartSegmentsBack').value) || 0;
config.CleanUpResumePoints = document.querySelector('#CleanUpResumePoints').checked;
config.ClearLiveStreamResumePoints = document.querySelector('#ClearLiveStreamResumePoints').checked;
diff --git a/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs b/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs
index c07427a..8c647f8 100644
--- a/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs
+++ b/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs
@@ -451,7 +451,7 @@ public class RecordingService : IRecordingService, IDisposable
// Build output file path
var safeTitle = SanitizeFileName(entry.Title);
- var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HHmm", CultureInfo.InvariantCulture);
+ var timestamp = DisplayTime.Now().ToString("yyyy-MM-dd_HHmm", CultureInfo.InvariantCulture);
var outputPath = Path.Combine(GetRecordingOutputPath(), $"{safeTitle}_{timestamp}.mkv");
entry.OutputPath = outputPath;
diff --git a/Jellyfin.Plugin.SRFPlay/Utilities/DisplayTime.cs b/Jellyfin.Plugin.SRFPlay/Utilities/DisplayTime.cs
new file mode 100644
index 0000000..b48eab0
--- /dev/null
+++ b/Jellyfin.Plugin.SRFPlay/Utilities/DisplayTime.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace Jellyfin.Plugin.SRFPlay.Utilities;
+
+///
+/// Converts times into the time zone the plugin writes into text (item names, file names).
+/// SRG sends times with an offset, and DateTime deserialisation turns them into server-local
+/// values, so without this the text follows whatever zone the server happens to run in.
+///
+public static class DisplayTime
+{
+ ///
+ /// Converts a time to the configured display time zone (the server's if none is set).
+ ///
+ /// The time. Unspecified values are treated as server-local, like deserialised ones.
+ /// The same instant expressed in the display time zone.
+ public static DateTime From(DateTime value)
+ {
+ return TimeZoneInfo.ConvertTimeFromUtc(value.ToUniversalTime(), GetZone());
+ }
+
+ ///
+ /// Gets the current time in the display time zone.
+ ///
+ /// The current time in the display time zone.
+ public static DateTime Now()
+ {
+ return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, GetZone());
+ }
+
+ private static TimeZoneInfo GetZone()
+ {
+ var id = Plugin.Instance?.Configuration.DisplayTimeZone;
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return TimeZoneInfo.Local;
+ }
+
+ // An unknown id falls back to the server's zone rather than breaking listings.
+ return TimeZoneInfo.TryFindSystemTimeZoneById(id.Trim(), out var zone) ? zone : TimeZoneInfo.Local;
+ }
+}