141 lines
6.9 KiB
Markdown
141 lines
6.9 KiB
Markdown
# 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..<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.
|