A recording of a live event crashed partway through: when its short
Akamai token expired, the proxy's stale-alias check swapped the
recording's mapping to the most recently registered deferred stream,
of any content. Segment requests were then built against the wrong CDN
path, returned 403, and ffmpeg exited, splitting the recording into
several files.
Proxy:
- Never swap URN-backed livestreams; they refresh themselves from the
API. Only consider swap candidates with the same URN.
Recording lifecycle:
- Complete a recording once its stream has been gone from the API for
5 minutes (broadcasts can end before ValidTo).
- Fail a recording still waiting for its stream after ValidTo (or 12h
after ValidFrom) instead of retrying forever.
- Wait for a DVR window whose start= is still in the future instead of
starting ffmpeg against a 400/404.
- Keep the original start time across restarts; don't mark a recording
failed on shutdown cancellation.
Growing .ts recordings:
- ffmpeg writes MPEG-TS (-c copy -copyts) to stdout and the plugin
appends it to one file per recording, so restarts continue the same
file and timeline.
- The Recordings folder lists any recording with a file on disk, not
only completed ones.
- In-progress recordings are exposed as infinite streams reading through
a new endpoint that follows the growing file (same pattern as
Jellyfin's DVR), so they can be watched from the start like a
livestream buffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The single net8.0 build compiled against Jellyfin 10.9.11 still loads on
10.11 and 12, so channels and browsing keep working, but 10.11 changed
APIs in ways that only fail at runtime. A binary audit of the DLL against
the real server assemblies found 9 broken references on both 10.11 and
12 (none on 10.9/10.10):
- TaskTriggerInfo.Type became an enum. GetDefaultTriggers throws, so
Jellyfin silently gives every SRF task a 24h fallback trigger. The
recording scheduler therefore ran about once a day and scheduled
recordings never started.
- ILibraryManager.GetItemList changed its return type: the expiration
check crashed on every run.
- Jellyfin.Data.Entities.User moved (IUserManager.Users, GetUserData,
SaveUserData, PlaybackProgressEventArgs.Users): resume cleanup returned
500 and the playback-stop guard threw on every stop.
Jellyfin 12 also rejects the legacy X-Emby-Token header with an empty
401, which the recordings UI surfaced as "Unexpected end of JSON input".
The pages now send Authorization: MediaBrowser Token="...", verified on
10.9, 10.11 and 12.
The plugin now multi-targets net8.0/net9.0/net10.0 against Jellyfin
10.9.11/10.11.0/12.0.0 (the oldest package of each line, so each DLL
loads on every patch release of it), with JELLYFIN_10_11_OR_GREATER and
JELLYFIN_12_OR_GREATER for the differences. Trigger construction moves
into Utilities/TaskTriggers so the version switch lives in one place.
.gitea/scripts/build-plugins.sh builds one zip per generation. jprm has
no targetAbi flag, so it rewrites build.yaml per flavour and restores
it. Jellyfin installs the highest version whose targetAbi it satisfies,
so the flavour code goes into the last version segment:
release v1.2.0 -> 1.2.0.9 / 1.2.0.11 / 1.2.0.12
nightly -> 1.0.<date>.<run>09 / 11 / 12
The PR, nightly and release workflows use the script and publish one
manifest entry per zip. Verified end to end against a three-entry
manifest: 10.11 installs the .11 build, 12.1 installs the .12 build.
The existing .NET 10 builder image builds all three unchanged.
Supporting changes: the test project multi-targets too, because jprm
publishes the whole solution for one framework at a time, and it drops
its Microsoft.Extensions.Logging 8.0.1 pin, which is a NU1605 downgrade
under the newer Jellyfin packages. CA1873 (new in the .NET 10 analyzers)
is silenced beside CA1848. The release manifest step checks out
origin/master like the nightly does since 7e1973a.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
Remove 9 dead methods, 6 unused constants, and redundant
ReaderWriterLockSlim from MetadataCache. Consolidate repeated
patterns into HasChapters, IsPlayable, and ToLowerString helpers.
Extract shared API methods in SRFApiClient. Move variant manifest
rewriting from controller to StreamProxyService. Make Auto quality
distinct from HD. Update README architecture section.