# 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: ```lua 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 ```bash 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.. --oneline -- ``` Re-apply the modified-file changes onto the new upstream code rather than reverting upstream's changes to make a patch apply.