Files
jellyfin-ha-src/docs/FORK-DIFF.md
unkin-agent b77702c851
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
merge: resolve CoreAppHost conflict with main, drop dead redisConnectionString local
2026-09-13 14:48:26 +10:00

6.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
Emby.Server.Implementations/MediaEncoding/TranscodeStoreConnectivityProbe.cs Startup ping; an unreachable configured store is logged at Error instead of failing open silently
Jellyfin.Server/Extensions/TranscodeStoreServiceCollectionExtensions.cs Store selection, logged at Information so the active store is visible at startup
Jellyfin.Server/Extensions/ConfigurationBuilderExtensions.cs Reads bare Jellyfin__* environment variables into the Jellyfin:* configuration root

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 (defaults to on when Redis is configured), 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.

Gating is on whenever Jellyfin:TranscodeStore:RedisConnectionString is set, because that is only set for a multi-instance deployment. Set Jellyfin:ScanLeader:Enabled=false to opt out. Startup logs which way it went:

Scan-leader gating is active: timer-driven library tasks run only on the instance holding the Redis scan-leader lease.
Scan-leader gating is off: timer-driven library tasks run on every instance.

Enabled=true with no Redis connection string logs a warning, because gating cannot run.

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.