docs: add architecture, contributing guide, and trim Dockerfile comment (#4)

* docs: add architecture overview, contributing guide, and GitHub discussion draft

ARCHITECTURE.md covers server layer diagram, subsystems, and runtime info.
CONTRIBUTING.md covers dev setup, build, test, and submission workflow.
GITHUB-DISCUSSION-DRAFT.md drafts the upstream discussion post for the HA fork.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: trim verbose comment in Dockerfile.runtime

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ZoltyMat
2026-03-25 23:37:44 -04:00
committed by mat
parent 13f49305da
commit d46539b718
4 changed files with 450 additions and 2 deletions
+1 -2
View File
@@ -1,7 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Runtime-only image — the .NET publish step runs on the CI host (runner), # Runtime-only image — the .NET publish step runs on the CI host (runner),
# not inside this Dockerfile. This avoids DinD overlay-on-overlay I/O throttling # not inside this Dockerfile.
# which makes `dotnet publish` inside Docker-in-Docker prohibitively slow on k3s.
# ── Web client stage ────────────────────────────────────────────────────────── # ── Web client stage ──────────────────────────────────────────────────────────
# Install jellyfin-web via the official Jellyfin apt repo. # Install jellyfin-web via the official Jellyfin apt repo.
+202
View File
@@ -0,0 +1,202 @@
> **Last updated: 2026-03-04**
# Jellyfin Server Architecture
High-level overview of the Jellyfin server structure, layer responsibilities, and key subsystems.
## Runtime
| Component | Value |
|---|---|
| Framework | .NET 10 / ASP.NET Core 10 |
| Target | `net10.0` |
| Entry point | `Jellyfin.Server` |
| Version | `10.12.0` (see `SharedVersion.cs`) |
---
## Layer Diagram
```
┌───────────────────────────────────────────────────────────┐
│ HTTP Clients │
│ (Jellyfin Web, mobile apps, 3rd-party) │
└────────────────────────┬──────────────────────────────────┘
│ REST / WebSocket
┌────────────────────────▼──────────────────────────────────┐
│ Jellyfin.Api │
│ ASP.NET Core controllers, middleware, auth, Swashbuckle │
└────────────────────────┬──────────────────────────────────┘
│ Interfaces (ILibraryManager, etc.)
┌────────────────────────▼──────────────────────────────────┐
│ MediaBrowser.Controller │
│ Core domain interfaces — no implementation here │
└────────────────────────┬──────────────────────────────────┘
│ Implementations
┌────────────────────────▼──────────────────────────────────┐
│ Emby.Server.Implementations / Jellyfin.Server.Impl │
│ Library manager, item repos, scheduled tasks, HTTP server│
└────────┬───────────────────────────────┬──────────────────┘
│ │
┌────────▼────────┐ ┌────────▼────────┐
│ Jellyfin.Data │ │ MediaBrowser │
│ EF Core DbCtx │ │ MediaEncoding │
│ SQLite via │ │ FFmpeg, HLS, │
│ Microsoft.Data │ │ Trickplay │
│ .Sqlite │ └─────────────────┘
└─────────────────┘
┌────────▼─────────────────────────────────────────────────┐
│ MediaBrowser.Model │
│ Pure DTOs, enums, no logic (shared by all layers) │
└──────────────────────────────────────────────────────────┘
```
---
## Project Responsibilities
### `Jellyfin.Server`
Entry point. Handles:
- CLI argument parsing (`CommandLineParser`)
- Serilog configuration (console, file, Graylog sinks)
- DI container wiring (`ApplicationHost`)
- ASP.NET Core host startup
### `Jellyfin.Api`
All HTTP surface. Handles:
- ASP.NET Core controllers (`Controllers/`)
- Authentication middleware (`Auth/`)
- Swashbuckle/OpenAPI configuration
- Request/response formatting (camelCase + PascalCase JSON)
- WebSocket listeners (`WebSocketListeners/`)
Controllers inherit from `BaseJellyfinApiController` which sets default route, produces JSON, and provides typed `Ok<T>()` helpers.
### `MediaBrowser.Controller`
Core domain interfaces. Key examples:
- `ILibraryManager` — media library operations
- `IMediaEncoder` — FFmpeg wrapper
- `IProviderManager` — metadata provider coordination
- `IUserManager` — user management
- `IPlaybackManager` — playback session tracking
**No implementations live here.** This keeps the domain decoupled from infrastructure.
### `Emby.Server.Implementations`
Primary implementation assembly. Contains:
- `ApplicationHost.cs` — DI wiring and startup
- `Data/` — SQLite queries and EF Core repositories
- `Library/``LibraryManager`, `LibraryMonitor`
- `Images/` — image processing pipeline (SkiaSharp)
- `HttpServer/` — HTTP server wiring
### `Jellyfin.Server.Implementations`
Secondary implementation assembly split from `Emby.Server.Implementations`. Contains newer implementations using EF Core patterns.
### `Jellyfin.Data`
EF Core data models and `DbContext`. Migrations managed here.
### `MediaBrowser.Model`
Pure data-transfer objects (DTOs) and enums. No logic. Consumed by all layers and by external clients. Changes here are API-breaking.
### `MediaBrowser.Providers`
Online metadata providers:
- TMDB (movies, TV)
- MusicBrainz (audio)
- OMDB
- TV Maze, TheTVDB
Uses `IMetadataProvider<T>` interface from `MediaBrowser.Controller`.
### `MediaBrowser.MediaEncoding`
FFmpeg process management, HLS streaming, keyframe extraction, subtitle transcoding, trickplay image generation.
### `Emby.Naming`
Media file path parsing — resolves series/season/episode structure, detects extras, parses video codecs from filenames.
### `MediaBrowser.LocalMetadata` / `MediaBrowser.XbmcMetadata`
Local NFO/XML metadata providers (Kodi-compatible `.nfo` sidecar files).
### `src/Jellyfin.CodeAnalysis`
Custom Roslyn analyzer. Runs only in Debug builds. Enforces project-specific rules.
---
## Key Subsystems
### Authentication
- Session-based API keys (stored in SQLite)
- Quick Connect (pairing flow)
- Auth middleware in `Jellyfin.Api/Auth/`
- Policies defined in `Jellyfin.Api/Constants/Policies.cs`
### Library Scanning
1. `LibraryMonitor` watches filesystem for changes
2. `LibraryManager` resolves paths → `BaseItem` subclasses
3. `Emby.Naming` parses filenames → metadata hints
4. `IProviderManager` fetches remote metadata and saves locally
5. Results persisted to SQLite via EF Core
### Transcoding
1. Client requests a stream via `MediaInfoController` or `DynamicHlsController`
2. `MediaInfoHelper` determines if transcoding is needed (codec matrix)
3. `MediaEncoder` spawns an FFmpeg subprocess with computed arguments
4. HLS segments or direct stream served via `AudioController` / `VideosController`
### Metrics
prometheus-net serves metrics at `/metrics`. Key meters:
- `prometheus-net.AspNetCore` — HTTP request duration/count
- `prometheus-net.DotNetRuntime` — GC, thread pool, JIT metrics
- Custom counters can be added via `Metrics.CreateCounter(...)` in any service
### Logging
Serilog pipeline:
- Console sink (structured)
- File sink (rolling, default `%APPDATA%/jellyfin/logs/`)
- Graylog GELF sink (optional, configured via `logging.json`)
---
## Database
SQLite database at `{DataDir}/data/jellyfin.db`. Accessed via:
- EF Core (`Jellyfin.Data.JellyfinDbContext`) for new data access
- `Microsoft.Data.Sqlite` direct queries for legacy paths
**All EF Core operations must use async methods** (`ToListAsync`, `FirstOrDefaultAsync`, etc.).
---
## Test Layout
```
tests/
Jellyfin.Api.Tests/ Controller + middleware unit tests
Jellyfin.Common.Tests/ MediaBrowser.Common utilities
Jellyfin.Controller.Tests/ Interface contracts and helpers
Jellyfin.MediaEncoding.Tests/ FFmpeg argument building
Jellyfin.Naming.Tests/ File path parsing
Jellyfin.Providers.Tests/ Provider logic
Jellyfin.Server.Integration.Tests/ Full-stack HTTP tests + OpenAPI spec gen
Jellyfin.Server.Tests/ Server startup and DI tests
```
Test stack: xUnit + AutoFixture + Moq + FsCheck. See `.github/instructions/testing.instructions.md`.
+164
View File
@@ -0,0 +1,164 @@
> **Last updated: 2026-03-04**
# Contributing to Jellyfin Server
This guide covers everything you need to develop, build, test, and submit changes to the Jellyfin server.
## Prerequisites
| Tool | Version | Notes |
|---|---|---|
| .NET SDK | 10.0.x | See `global.json``rollForward: latestMinor` |
| Git | any recent | `git clone` with submodules not required |
| FFmpeg | 7.x | Required for transcoding tests; install via devcontainer or manually |
| Docker | optional | For devcontainer workflow |
### macOS (Homebrew)
```bash
brew install dotnet
```
### Linux (Debian/Ubuntu)
```bash
wget https://dot.net/v1/dotnet-install.sh && bash dotnet-install.sh --channel 10.0
```
### Windows
Download the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10) installer.
### DevContainer (recommended for new contributors)
Open the repo in VS Code and accept the "Reopen in Container" prompt. The devcontainer installs:
- .NET 10
- FFmpeg
- All recommended VS Code extensions
---
## Build
```bash
# Build the server entry point
dotnet build Jellyfin.Server/Jellyfin.Server.csproj
# Build the entire solution (all projects)
dotnet build Jellyfin.sln
```
Debug builds activate all code analyzers (StyleCop, BannedApiAnalyzers, IDisposableAnalyzers, MultithreadingAnalyzer). **Expect build failures if your code has missing XML docs or uses banned APIs.**
---
## Run Locally
```bash
dotnet run --project Jellyfin.Server/Jellyfin.Server.csproj \
-- --datadir /tmp/jellyfin-data --webdir /tmp/jellyfin-web --nowebclient
```
The server starts on `http://localhost:8096` by default.
---
## Test
```bash
# Run all tests (cross-platform matrix: Linux, macOS, Windows)
dotnet test Jellyfin.sln --configuration Release --verbosity minimal
# Run a single test project
dotnet test tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
# Run tests matching a name filter
dotnet test Jellyfin.sln --filter "ClassName=MyServiceTests"
# Run with code coverage
dotnet test Jellyfin.sln \
--configuration Release \
--collect:"XPlat Code Coverage" \
--settings tests/coverletArgs.runsettings
```
Coverage output: `merged/Cobertura.xml` (merged by ReportGenerator in CI).
### Regenerate OpenAPI Spec
After adding or changing any API endpoint:
```bash
dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj \
-c Release \
--filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
```
Commit the updated `openapi.json` — the CI diff job will flag unintentional breaking changes.
---
## Code Style
All style rules are enforced by the compiler in Debug builds. Key rules:
- **Nullable enabled** — mark nullable types with `?`, never silence with `null!` without a comment
- **Warnings as errors** — fix every warning; do not suppress with `#pragma warning disable`
- **XML docs** — every `public` type and member must have `/// <summary>`
- **No `Task.Result`** — always `await` instead
- **Central NuGet versions** — versions in `Directory.Packages.props` only, never in `.csproj`
- **File-scoped namespaces** — use `namespace Jellyfin.Example;` (not block-scoped)
See `.github/instructions/csharp.instructions.md` for the full ruleset.
---
## Pull Request Process
1. Fork the repo and create a feature branch from `master`
2. Make your changes; ensure `dotnet build` and `dotnet test` pass locally
3. Fill out the PR template (`.github/pull_request_template.md`):
- **Changes**: 15 sentence summary
- **Issues**: tag with `Fixes #NNN`
4. CI runs automatically:
- `ci-tests.yml` — tests on Linux, macOS, Windows
- `ci-openapi.yml` — OpenAPI diff
- `ci-codeql-analysis.yml` — security scan
5. A maintainer will review and merge
### Title format
Use the imperative mood:
-`Add lyrics endpoint for audio items`
-`Fix null reference in LibraryController`
-`Added lyrics endpoint`
-`Fixed null reference`
---
## Adding a New Package Dependency
1. Add the version to `Directory.Packages.props`:
```xml
<PackageVersion Include="SomePackage" Version="1.2.3" />
```
2. Add the reference to the relevant `.csproj` (no `Version=` attribute):
```xml
<PackageReference Include="SomePackage" />
```
**Never** specify both a version in `Directory.Packages.props` AND in the `.csproj` — that causes `NU1008`.
---
## Project Conventions
See `.github/instructions/` for detailed instructions per concern:
| Topic | File |
|---|---|
| C# style | `csharp.instructions.md` |
| API controllers | `api.instructions.md` |
| Tests | `testing.instructions.md` |
| CI/CD workflows | `ci-cd.instructions.md` |
| Documentation | `docs.instructions.md` |
+83
View File
@@ -0,0 +1,83 @@
# Jellyfin HA transcoding fork: Redis-backed session failover + experimental PostgreSQL provider
I've been working on a fork of Jellyfin focused on one specific problem: making HLS transcoding survive pod restarts in a multi-replica Kubernetes deployment.
## What it does
Right now, Jellyfin assumes transcode state lives in one server process. If that pod dies, active transcodes die with it. This fork adds a small HA layer so transcode ownership can survive a pod restart:
- A new `ITranscodeSessionStore` abstraction for durable transcode session tracking
- A `RedisTranscodeSessionStore` implementation with lease-based ownership
- Atomic pod takeover using a Redis Lua script when a lease expires
- Lease-aware cleanup so one pod does not delete segments another pod still needs
- A `NullTranscodeSessionStore` fallback, so single-instance deployments behave exactly like upstream with no config changes
I also added an experimental PostgreSQL provider for shared-database deployments, since SQLite is not a good fit once multiple replicas are involved.
## What the HA flow looks like
- Pod A starts an HLS transcode and registers the session in Redis
- Pod A renews the lease while it owns the session
- If Pod A dies, the lease expires
- Pod B receives the next request, atomically claims the expired lease, and resumes from the last completed segment on shared storage
- The client sees a short buffer pause instead of a hard failure
## How to run it
There are three practical modes:
### 1. Single instance
No config needed. It falls back to the no-op store automatically.
### 2. Local HA test
Run two Jellyfin instances against:
- the same Redis
- the same shared transcode directory
That is enough to test failover behavior locally.
### 3. Kubernetes / k3s
This is the intended deployment model. You need:
- 2+ Jellyfin replicas
- Redis
- shared RWX storage for transcode output
- shared media storage
- ideally PostgreSQL if you want a proper shared DB setup
The key config is:
```text
Jellyfin:TranscodeStore:RedisConnectionString
Jellyfin:TranscodeStore:LeaseDurationSeconds
```
Repo and write-up:
- Source: https://github.com/ZoltyMat/jellyfin-ha
- Full change summary vs upstream: https://github.com/ZoltyMat/jellyfin-ha/blob/main/docs/FORK-DIFF.md
- Write-up with diagrams and k8s manifests: https://blog.zolty.systems/posts/jellyfin-ha-kubernetes
## What would be required to merge upstream
I do not expect this to be merged as-is without discussion. If there is interest, I think the realistic path is to split it into small pieces:
1. Introduce `ITranscodeSessionStore`, `TranscodeSession`, and `NullTranscodeSessionStore` only
2. Add the DI wiring with no behavior change unless configured
3. Add HLS session registration and lease renewal hooks
4. Add lease-aware cleanup in `DeleteTranscodeFileTask`
5. Add takeover logic in the HLS/session path
6. Discuss whether Redis should be the first supported distributed store, or whether the interface should land before any concrete implementation
7. Treat PostgreSQL as a separate discussion entirely
I think the HA transcode work has a better chance of review if it is separated from the PostgreSQL provider and migration tooling.
## Why I'm posting it
I'm not trying to maintain a permanent hard fork. I built this to see whether Jellyfin could be made to behave well in a replicated environment without rewriting major subsystems. The answer seems to be yes, but it needs maintainers to decide whether this kind of deployment is something upstream wants to support.
If there's interest, I'm happy to break the work into smaller PRs, clean up anything that does not match project direction, and rework the design around maintainer feedback.