Files
jellyfin-ha-src/docs/FORK-DIFF.md
T
unkin-agent baa16b6586 fix: make transcode leases ownership-checked and cleanup-aware
Cleanup never matched a live session because the controller registered empty
manifest and segment paths, renewal was a read-modify-write that could revert a
takeover, and the takeover script compared an ISO date to a number, so it errored.

- populate the session record's manifest and segment paths from the playlist path
- renew the lease via a Lua compare-and-set on the owning pod
- store the lease expiry as unix milliseconds so the scripts can compare it
- retain the session record past its lease so an orphan can still be taken over
- test the Redis store against a real Redis, including the renew-vs-takeover race
- drop the live stream record nothing ever read back
2026-09-12 10:19:29 +10:00

5.9 KiB

Fork Diff: unkin/jellyfin-ha-src vs jellyfin/jellyfin

Base: v12.0 (6c073e19ddf604b2369c638716164fdab4c952dc) Head: origin/main Summary: 56 files added · 16 upstream files modified · 15 upstream files deleted


What changed and why

This fork adds a high-availability layer so Jellyfin can run as more than one replica against shared storage. The design principle: extend through dependency injection, touch as little upstream code as possible. No core media, library, auth or plugin logic is rewritten.

Bucket Files Lines added
HA contracts and models (MediaBrowser.Controller) 8 ~330
Redis implementations (Emby.Server.Implementations) 2 ~350
PostgreSQL database provider 6 ~4,980
SQLite to PostgreSQL migration tool 6 ~720
Modified upstream files 16 ~420
Tests 14 ~2,790
Helm chart 17 ~1,370
CI and container build 3 ~175

New components

Transcode session store

File Purpose
MediaBrowser.Controller/MediaEncoding/ITranscodeSessionStore.cs DI contract for durable transcode session tracking
MediaBrowser.Controller/MediaEncoding/TranscodeSession.cs Session record: owning pod, lease expiry, manifest and segment paths, last durable segment
MediaBrowser.Controller/MediaEncoding/TranscodeStoreOptions.cs RedisConnectionString, LeaseDurationSeconds and SessionRetentionSeconds
MediaBrowser.Controller/MediaEncoding/NullTranscodeSessionStore.cs No-op store used when no Redis connection is configured
Emby.Server.Implementations/MediaEncoding/RedisTranscodeSessionStore.cs Redis store; sessions under jellyfin:transcode:{playSessionId}, key TTL is the retention window so an orphaned session outlives its lease

Lease takeover and renewal each run as a single Lua script, so concurrent pods cannot both claim an expired lease and a renewal cannot revert a takeover. The expiry is stored as unix milliseconds so the script can compare it:

local raw = redis.call('GET', KEYS[1])
if not raw then return 0 end
local session = cjson.decode(raw)
-- takeover: only an expired lease may be claimed
if tonumber(session['LeaseExpiresUtc']) > tonumber(ARGV[1]) then return 0 end
-- renewal: only the pod that still owns the lease may extend it
-- if session['OwnerPod'] ~= ARGV[2] then return 0 end
session['OwnerPod'] = ARGV[2]
-- update expiry and SET with PX in the same script
return 1

Scan-leader lease

File Purpose
MediaBrowser.Controller/ScheduledTasks/IScanLeaderLease.cs DI contract for the leader lease
MediaBrowser.Controller/ScheduledTasks/ScanLeaderOptions.cs Enabled, LeaseDurationSeconds, GatedTaskKeys
MediaBrowser.Controller/ScheduledTasks/NullScanLeaderLease.cs Always-leader default, preserving single-instance behaviour
Emby.Server.Implementations/ScheduledTasks/RedisScanLeaderLease.cs Redis TTL lease; an unreachable Redis is treated as holding the lease

Gated by default: RefreshLibrary, RefreshPeople, RefreshChapterImages, AudioNormalization, TaskExtractMediaSegments, KeyframeExtraction, CleanupUserDataTask, OptimizeDatabaseTask. Only timer-driven runs are gated; manual and API-triggered runs always execute locally.

PostgreSQL provider

src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/ is an EF Core provider parallel to the SQLite one, selected with DatabaseType: Jellyfin-PostgreSQL. It carries a single initial migration generated against the v12.0 model. SQLite remains the default, so existing deployments are unaffected.

tools/Jellyfin.DbMigrator/ moves an existing jellyfin.db into PostgreSQL, optionally uploading a pre-migration copy of the SQLite file to S3.


Modified upstream files

File Change
Jellyfin.Server/CoreAppHost.cs (+46) Registers the Redis or null transcode store and scan-leader lease from startup config
Jellyfin.Api/Controllers/DynamicHlsController.cs Registers the play session with its manifest and segment paths, renews the lease under this pod's identity, and shortens segments when resuming a stored session
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs (+52/-6) Keeps files belonging to an active session in the store
Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs (+28/-1) Skips timer-driven gated tasks without the leader lease
Emby.Server.Implementations/ScheduledTasks/TaskManager.cs (+12/-2) Passes the lease and options to each worker
Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs (+53) Registers the PostgreSQL provider and a pooled NpgsqlDataSource
Jellyfin.Server/Program.cs (+5) Exposes IServerConfigurationManager to the startup migration container
MediaBrowser.Model/Configuration/EncodingOptions.cs (+16) Adds RecoverySegmentLengthSeconds and RecoverySegmentBufferCount
Directory.Packages.props, Jellyfin.sln, four .csproj files New packages, projects and references
tests/.../SessionManagerTests.cs, tests/.../IdlePlaybackTests.cs Pass the new constructor argument

Deleted upstream files

.github/workflows/* (15 files). The fork is hosted on Gitea and builds on Woodpecker via .woodpecker/ci.yaml.


Rebasing onto a newer upstream release

git remote add upstream https://github.com/jellyfin/jellyfin.git
git fetch upstream --tags

# files this fork owns outright
git diff v12.0...HEAD --name-only --diff-filter=A

# files that need re-porting onto the new release
git diff v12.0...HEAD --name-only --diff-filter=M

# upstream churn on a touched file since this base
git log v12.0..<new-tag> --oneline -- <path>

Re-apply the modified-file changes onto the new upstream code rather than reverting upstream's changes to make a patch apply.