Files
T
dtourolleandClaude Opus 5 390146e8d4
🏗️ Build Plugin / build (push) Successful in 1m43s
🧪 Test Plugin / test (push) Successful in 34s
🚀 Release Plugin / build-and-release (push) Successful in 51s
Nightly Build / nightly-build (push) Failing after 50s
Clean up stale Continue Watching entries
Livestreams and ended broadcasts accumulated in every user's resume row
because Jellyfin saves a playback position for channel items regardless
of whether the position means anything. A livestream has nothing to
return to, so the entry stayed pinned forever.

Two parts:

- PlaybackResumeGuard, an IHostedService on ISessionManager.PlaybackStopped.
  Jellyfin saves the resume point before raising the event, so the guard
  zeroes it afterwards for livestreams. Stops new entries at the source.
- ResumeCleanupService plus a daily 4 AM task (after the 3 AM expiration
  check) for the existing backlog. Clears livestreams, resume points older
  than N days, positions under N seconds and playback past N% of runtime,
  each threshold configurable.

Only plugin-owned items are touched, matched by the SRF provider ID with a
fallback to the owning channel ID so recordings are covered too. Clearing
sets PlaybackPositionTicks to 0 and leaves Played false: nothing is deleted
and the item stays unwatched.

Config page gains the thresholds plus "Clean Up Stale Entries Now" and
"Clear All SRF Play Entries" buttons, backed by MaintenanceController
under RequiresElevation.

Also folds the duplicated urn.Contains("livestream") check in
MediaSourceFactory and RecordingService into UrnHelper.IsLivestreamUrn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:55:41 +02:00

215 lines
7.4 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 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; }
}