SRG sends times with an offset (e.g. 13:55+02:00). They are stored as DateTime, which deserialisation converts to server-local time, and two places then wrote that straight into text: the "[dd.MM HH:mm]" prefix on upcoming livestream names and the timestamp in recording file names (DateTime.Now). On a server running in UTC both were two hours behind Swiss time, e.g. a qualifying starting at 13:55 was listed as 11:55. A new "Display Time Zone" setting (IANA id, e.g. Europe/Zurich) now decides the zone for both; empty keeps the server's zone, and an unknown id falls back to it rather than breaking the listing. Utilities/DisplayTime does the conversion. Also fixes PremiereDate on upcoming livestreams, which was set to that server-local value although Jellyfin expects UTC. It was only correct on UTC servers; it is now converted, like the other channel items. Verified on simulated UTC and Europe/Paris servers: the same instant gives 11:55 / 13:55 / 07:55 for server zone / Europe/Zurich / America/New_York, and PremiereDate is 11:55 UTC on both. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
223 lines
7.9 KiB
C#
223 lines
7.9 KiB
C#
using MediaBrowser.Model.Plugins;
|
|
|
|
namespace Jellyfin.Plugin.SRFPlay.Configuration;
|
|
|
|
/// <summary>
|
|
/// Business unit options for SRF content.
|
|
/// </summary>
|
|
public enum BusinessUnit
|
|
{
|
|
/// <summary>
|
|
/// SRF (Swiss Radio and Television - German).
|
|
/// </summary>
|
|
SRF,
|
|
|
|
/// <summary>
|
|
/// RTS (Radio Télévision Suisse - French).
|
|
/// </summary>
|
|
RTS,
|
|
|
|
/// <summary>
|
|
/// RSI (Radiotelevisione svizzera - Italian).
|
|
/// </summary>
|
|
RSI,
|
|
|
|
/// <summary>
|
|
/// RTR (Radiotelevisiun Svizra Rumantscha - Romansh).
|
|
/// </summary>
|
|
RTR,
|
|
|
|
/// <summary>
|
|
/// SWI (Swiss World International).
|
|
/// </summary>
|
|
SWI
|
|
}
|
|
|
|
/// <summary>
|
|
/// Quality preference for video streams.
|
|
/// </summary>
|
|
public enum QualityPreference
|
|
{
|
|
/// <summary>
|
|
/// Automatic quality selection.
|
|
/// </summary>
|
|
Auto,
|
|
|
|
/// <summary>
|
|
/// Standard definition.
|
|
/// </summary>
|
|
SD,
|
|
|
|
/// <summary>
|
|
/// High definition.
|
|
/// </summary>
|
|
HD
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plugin configuration.
|
|
/// </summary>
|
|
public class PluginConfiguration : BasePluginConfiguration
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
|
/// </summary>
|
|
public PluginConfiguration()
|
|
{
|
|
// Set default options
|
|
BusinessUnit = BusinessUnit.SRF;
|
|
QualityPreference = QualityPreference.Auto;
|
|
ContentRefreshIntervalHours = 6;
|
|
ExpirationCheckIntervalHours = 24;
|
|
CacheDurationMinutes = 60;
|
|
EnableLatestContent = true;
|
|
EnableTrendingContent = true;
|
|
EnableCategoryFolders = true;
|
|
EnabledTopics = new System.Collections.Generic.List<string>();
|
|
GenerateTitleCards = true;
|
|
LiveStartSegmentsBack = 3;
|
|
CleanUpResumePoints = true;
|
|
ClearLiveStreamResumePoints = true;
|
|
ResumePointMaxAgeDays = 30;
|
|
ResumePointMinPositionSeconds = 60;
|
|
ResumePointCompletedPercent = 92;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the legacy single business unit. Retained only for backwards compatibility
|
|
/// with older configs; every unit now has its own always-on channel.
|
|
/// </summary>
|
|
public BusinessUnit BusinessUnit { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the preferred video quality.
|
|
/// </summary>
|
|
public QualityPreference QualityPreference { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the content refresh interval in hours.
|
|
/// </summary>
|
|
public int ContentRefreshIntervalHours { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the expiration check interval in hours.
|
|
/// </summary>
|
|
public int ExpirationCheckIntervalHours { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the metadata cache duration in minutes.
|
|
/// </summary>
|
|
public int CacheDurationMinutes { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to enable latest content discovery.
|
|
/// </summary>
|
|
public bool EnableLatestContent { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to enable trending content discovery.
|
|
/// </summary>
|
|
public bool EnableTrendingContent { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to use a proxy for API requests.
|
|
/// </summary>
|
|
public bool UseProxy { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the proxy server address (e.g., http://proxy.example.com:8080).
|
|
/// </summary>
|
|
public string ProxyAddress { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the proxy username (optional).
|
|
/// </summary>
|
|
public string ProxyUsername { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the proxy password (optional).
|
|
/// </summary>
|
|
public string ProxyPassword { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to enable category/topic folders in the channel.
|
|
/// </summary>
|
|
public bool EnableCategoryFolders { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the list of enabled topic IDs. If empty, all topics are shown.
|
|
/// </summary>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Required for configuration serialization")]
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Configuration DTO")]
|
|
public System.Collections.Generic.List<string> EnabledTopics { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the public/external server URL for remote clients (e.g., https://jellyfin.example.com:8920).
|
|
/// If not set, the plugin will use Jellyfin's GetSmartApiUrl() which may return local addresses.
|
|
/// This is important for Android and other remote clients to access streams.
|
|
/// </summary>
|
|
public string PublicServerUrl { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to generate title card images with the content title.
|
|
/// When enabled, generates custom thumbnails instead of using SRF-provided images.
|
|
/// </summary>
|
|
public bool GenerateTitleCards { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the output directory for sport livestream recordings.
|
|
/// </summary>
|
|
public string RecordingOutputPath { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public string DisplayTimeZone { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets or sets how many segments back from the live edge livestream playback should start.
|
|
/// Injected as <c>#EXT-X-START:TIME-OFFSET=-(N * targetDuration)</c> into the live media
|
|
/// playlist. This keeps the start point out of the volatile live edge, which on Android TV
|
|
/// (ExoPlayer) otherwise causes stalling/jumping until a manual skip. RFC 8216 requires the
|
|
/// offset to stay at least 3 target durations from the edge, so values below 3 are clamped.
|
|
/// Set to 0 to disable injection entirely.
|
|
/// </summary>
|
|
public int LiveStartSegmentsBack { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the "Clean Up SRF Play Continue Watching" task
|
|
/// removes stale resume points. When false the task inspects nothing and clears nothing.
|
|
/// </summary>
|
|
public bool CleanUpResumePoints { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether resume points for livestreams are cleared.
|
|
/// A livestream has no meaningful resume position, so stopping one otherwise leaves it
|
|
/// pinned in "Continue Watching" forever. When enabled the position is also cleared
|
|
/// immediately on playback stop, not just by the scheduled task.
|
|
/// </summary>
|
|
public bool ClearLiveStreamResumePoints { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the age in days after which an untouched resume point is cleared.
|
|
/// Measured from the last time the item was played. Set to 0 to disable the age rule.
|
|
/// </summary>
|
|
public int ResumePointMaxAgeDays { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the minimum resume position in seconds. Anything at or below this is
|
|
/// treated as an accidental start rather than something worth resuming.
|
|
/// Set to 0 to disable the rule.
|
|
/// </summary>
|
|
public int ResumePointMinPositionSeconds { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the percentage of runtime at or beyond which playback counts as finished.
|
|
/// Only applied to items with a known runtime. Set to 0 to disable the rule.
|
|
/// </summary>
|
|
public int ResumePointCompletedPercent { get; set; }
|
|
}
|