Files
jellyfin-srfPlay/Jellyfin.Plugin.SRFPlay/Configuration/PluginConfiguration.cs
dtourolleandClaude Opus 5.5 95ce4edf9b
Nightly Build / nightly-build (push) Successful in 1m32s
🧪 Test Plugin / test (push) Successful in 48s
🚀 Release Plugin / build-and-release (push) Successful in 1m18s
🏗️ Build Plugin / build (push) Successful in 12m28s
Show times in a configurable time zone
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>
2026-09-25 19:29:05 -04:00

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; }
}