JellyFederation is a Jellyfin plugin + companion server for federated media discovery and transfer between Jellyfin instances.
src/JellyFederation.Plugin: Jellyfin plugin (configuration page, startup service, sync, transfer).src/JellyFederation.Server: federation API + SignalR hub.src/JellyFederation.Shared: contracts shared by plugin and server.src/JellyFederation.Web: frontend for server registration and management.
Use ./dev.sh to build/deploy the plugin and run the local Jellyfin + federation stack.
Sensible default refresh (no args):
./dev.shThis runs a full refresh: rebuild plugin+server, restart local federation server, deploy plugin to local and remote Jellyfin, and seed local+remote plugin configs from federation DB.
docker-compose.yml includes:
lgtm(Grafana + OTLP) onhttp://localhost:3000with OTLP gRPC atlocalhost:4317
Jellyfin topology remains:
- local standalone container managed by existing
dev.shcommands (setup/start/deploy/...) - remote production/test Jellyfin at
192.168.2.192managed via./dev.sh deploy-test - local federation server via
dotnet run(./dev.sh server, also started by./dev.sh stack-up)
Start the stack with:
./dev.sh stack-upUseful stack commands:
./dev.sh stack-status
./dev.sh stack-logs server
./dev.sh stack-logs local
./dev.sh stack-logs lgtm
./dev.sh stack-restart
./dev.sh stack-deploy
./dev.sh stack-downFor a media-request test flow:
- Start local observability/server stack with
./dev.sh stack-up. - Ensure local Jellyfin is running (
./dev.sh start) and deploy plugin updates (./dev.sh deploy). - Deploy to the remote Jellyfin (
./dev.sh deploy-test 192.168.2.192 root). - Register local + remote servers in JellyFederation web (
:5264) so each plugin receivesServerId+ApiKey. - Trigger a federation media request and inspect traces/metrics in Grafana (
:3000).
Server (src/JellyFederation.Server/appsettings.json, override with env vars):
Telemetry__ServiceName(default:jellyfederation-server)Telemetry__OtlpEndpoint(default:http://localhost:4317)Telemetry__SamplingRatio(default:1.0)Telemetry__EnableTracing(default:true)Telemetry__EnableMetrics(default:true)Telemetry__EnableLogs(default:true)Telemetry__RedactionEnabled(default:true)
Plugin (PluginConfiguration):
TelemetryServiceName(default:jellyfederation-plugin)TelemetryOtlpEndpoint(default:http://localhost:4317)TelemetrySamplingRatio(default:1.0)EnableTracing/EnableMetrics/EnableLogs/RedactionEnabled
The plugin now advertises transfer transport capabilities during hole-punch readiness:
PreferQuicForLargeFiles(default:true) enables QUIC preference for eligible large files.LargeFileQuicThresholdBytes(default:536870912) controls the size threshold used for QUIC selection.
Server-side mode selection is deterministic per request:
- Select
Quiconly when both peers advertise QUIC support and file size meets threshold. - Otherwise use
ArqUdp. - Record selected mode, selection reason, transfer progress bytes, and failure category in file request state.
- Capture a baseline for
operations.total,operation.duration,timeouts.total,retries.total, andinflight. - Compare the same metric series by
operation,component, andreleasedimensions between releases. - During incidents, pivot from high timeout/error series to traces with matching
correlation_id. - Use
federation.outcome,error.type, and sanitizederror.messagetags to identify the failing stage quickly.
- Core plugin/server workflows now use
OperationOutcome<T>+FailureDescriptorfor expected failures. - Boundary translation happens through:
ErrorContractMapper(HTTP API)SignalRErrorMapper(SignalR payloads)
- External error payloads include stable
code,category, and sanitizedmessagefields.
- Prefer returning
OperationOutcome<T>for expected failures (validation, not-found, conflict, connectivity). - Use stable failure codes (for example:
file_request.invalid_state) and safe messages. - Keep raw exception details in logs/telemetry, not in client-facing contracts.
- At boundaries, map
FailureDescriptorwith mapper services instead of ad-hocNotFound("...")/Conflict("..."). - Use shared composition helpers (
Map,Bind,Match, async variants) to avoid repetitive branching.
See LICENSE.