# 101 — `jellyfin`

Media server: serves the movies / TV / anime / music libraries from `/mnt/media_local` to LAN clients. Hardware transcoding via AMD Radeon 680M + RX 7600 VAAPI. Authentik SSO via OIDC.

## At a glance
- **Host:** **strong** (migrated from hubris 2026-07-05)
- **IP:** `192.168.8.246`
- **Privilege:** privileged (recreated on strong as priv)
- **Resources:** 4 cores / 8 GiB RAM / 1 GiB swap / 16 GiB rootfs
- **GPU:** `/dev/dri/renderD128` + `/dev/dri/card0` (AMD Radeon 680M iGPU + RX 7600 dGPU) passed via `dev0` / `dev1` in LXC config
- **Mounts:** `/mnt/media_local` ↔ `/mnt/library`
- **Public hostname:** [`media.hubris.network`](../infrastructure/dns.md) → [caddy](121-caddy.md) → `:8096`
- **Version:** Jellyfin 10.11.11 (apt package, Ubuntu 24.04 noble repo)
- **FFmpeg:** jellyfin-ffmpeg7 7.1.4

## Service / port map

| Service  | Listen | Notes |
| -------- | ------ | ----- |
| jellyfin | `:8096` | HTTP (caddy terminates TLS) |

## Hardware acceleration (VAAPI)

GPU is passed through to the LXC via `dev0: /dev/dri/renderD128,gid=993` and
`dev1: /dev/dri/card0,gid=44` in
`/etc/pve/lxc/101.conf` (strong). The `jellyfin` user is in the `render` (GID 993) and
`video` groups inside the container.

| GPU | Model | Role |
|-----|-------|------|
| Radeon 680M | iGPU (AMD Ryzen 7 PRO 6850U) | Primary VAAPI encoder/decoder |
| RX 7600 | dGPU (add-in) | Secondary transcode, HEVC/AV1 encoding |

Encoding settings (`/etc/jellyfin/encoding.xml`):
- `HardwareAccelerationType`: `vaapi`
- `VaapiDevice`: `/dev/dri/renderD128`
- `EnableHardwareEncoding`: `true`
- `AllowHevcEncoding`: `true`
- `AllowAv1Encoding`: `true`
- `EnableTonemapping`: `true`
- `HardwareDecodingCodecs`: h264, hevc, vc1, vp9, av1
- `EnableThrottling`: `true`
- `EnableSegmentDeletion`: `true`

Trickplay (`/etc/jellyfin/system.xml`):
- `EnableHwAcceleration`: `true`
- `EnableHwEncoding`: `true`

## Authentik SSO (OIDC)

Jellyfin uses the [SSO-Auth plugin](https://github.com/9p4/jellyfin-plugin-sso)
v4.0.0.4 for Authentik OIDC login. No Caddy forward-auth gate — the SSO plugin
handles auth directly via OIDC redirect flow.

### Architecture

```
User → media.hubris.network → Caddy (TLS, no forward-auth) → Jellyfin :8096
                                                                ↓
                                                    Login page with "Sign in with Authentik" button
                                                                ↓ (click)
                                                    /sso/OID/start/Authentik
                                                                ↓ (302 redirect)
                                                    auth.hubris.network OIDC
                                                                ↓ (login)
                                                    /sso/OID/redirect/Authentik?code=...&state=...
                                                                ↓
                                                    Jellyfin SSO plugin validates token → logged in
```

### Components

1. **SSO-Auth plugin** — installed at `/var/lib/jellyfin/plugins/SSO-Auth_4.0.0.4/`
   - Config: `/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml`
   - Provider name: `Authentik`
   - OIDC endpoint: `https://auth.hubris.network/application/o/jellyfin/`
   - `SchemeOverride`: `https` (required — without it, plugin generates
     `http://` redirect URIs that Authentik rejects)
   - `EnableAuthorization`: `false` (prevents plugin from overwriting admin
     permissions on each SSO login — see
     [jellyfin-sso-plugin](../../../devops/homelab-authentik-admin/references/jellyfin-sso-plugin.md))
   - `OidScopes`: `["email"]` (openid+profile added by default by the plugin;
     must be non-null or `OidChallenge()` throws `ArgumentNullException`)

2. **Authentik OIDC provider** — `Provider for Jellyfin` (PK 6)
   - Client ID: `vt61t5Y2ZVtN6l3QjitkBvwUJjFKvSyl4TDBXcJx`
   - Redirect URI: `https://media.hubris.network/sso/OID/redirect/Authentik`
   - Application slug: `jellyfin`

3. **SSO button injection** — `/usr/share/jellyfin/web/sso-inject.js`
   - Injected via `<script defer src="sso-inject.js?v=3">` in `index.html`
   - Polls for `.readOnlyContent` div on the login page, adds "Sign in with
     Authentik" button linking to `/sso/OID/start/Authentik`
   - Cache-busted with `?v=N` parameter (bump on changes)
   - **Lost on apt upgrade** — re-inject the script tag and copy the JS file
     after `apt-get upgrade jellyfin*`

4. **Caddy config** — no forward-auth gate for media.hubris.network:
   ```caddy
   media.hubris.network {
       tls { dns ionos {env.IONOS_AUTH_API_TOKEN} }
       reverse_proxy 192.168.8.206:8096
   }
   ```

### Known issues / pitfalls

- **`OidScopes` must be non-null** — if the field is missing from the plugin
  config XML, `OidChallenge()` throws `System.ArgumentNullException`. Always
  include `OidScopes` in the provider config (even if empty array).
- **`SchemeOverride: "https"` is required** — without it, the plugin generates
  `http://` redirect URIs (from the internal HTTP listener). Authentik rejects
  them with "Redirect URI Error".
- **SSO button JS is not served by the plugin** — the `__plugin/SSO-Auth.js`
  endpoint returns 404 on Jellyfin 10.11.x when the plugin is installed
  manually (not via Jellyfin's plugin manager). The `sso-inject.js` workaround
  in `index.html` is the fallback.
- **No Caddy forward-auth gate** — the SSO plugin's OIDC redirect flow is
  incompatible with Caddy's `import authentik` forward-auth. If both are
  enabled, the forward-auth intercepts the OIDC callback and breaks the flow.
  Use one or the other, not both. SSO plugin (OIDC redirect) is preferred.
- **API key for setup** — a temp API key can be inserted directly into the
  `ApiKeys` SQLite table for automated configuration:
  ```sql
  INSERT INTO ApiKeys VALUES (1, '2026-07-04', '2026-07-04', 'setup', 'jf-setup-key-...');
  ```

## Permissions
Member of the [media GID 10000](../infrastructure/media-permissions.md) standard. Service user `jellyfin` is in the `media` group inside the container; `/mnt/media_local` on strong's ludo-lvm is owned `root:media` with mode `2775`.

## Related
- [Caddy reverse proxy](121-caddy.md)
- [Media permissions](../infrastructure/media-permissions.md)
- [arriman](122-arriman.md) — \*arr stack writes the libraries jellyfin reads
- [DNS split-horizon](../infrastructure/dns.md)
- [Authentik admin](../../../devops/homelab-authentik-admin/SKILL.md) — OIDC provider creation, SSO plugin config

## Changelog

### 2026-07-06 — wiki: IP, host, GPU, mount path updated for strong migration
Updated At-a-glance: IP 206→246, host hubris→strong, mount /mnt/library→/mnt/media_local, GPU Radeon 760M→680M+RX7600, privilege unpriv→priv. Permissions section updated. Changelog entry in 122-arriman.md updated similarly.

### 2026-07-04 — VAAPI hardware acceleration + Authentik SSO + resource bump
- Upgraded Jellyfin 10.11.8 → 10.11.11 (purge + reinstall to fix DB migration bug)
- Enabled VAAPI hardware acceleration (Radeon 760M): h264/hevc/vc1/vp9/av1 decode + encode
- Bumped resources: 2→4 cores, 4→8 GiB RAM, 512→1024 MiB swap
- Enabled trickplay HW acceleration + throttling + segment deletion
- Installed SSO-Auth plugin v4.0.0.4 with Authentik OIDC provider
- Configured `SchemeOverride: https`, `OidScopes: ["email"]`, `EnableAuthorization: false`
- Injected SSO button via `sso-inject.js` in web `index.html`
- Removed Caddy forward-auth gate (incompatible with SSO plugin OIDC flow)
- **Database was wiped** during cache relocation attempt — no LVM snapshot
  existed. All watch states, user accounts, and library configs lost.
  Libraries re-added via setup wizard.

### 2026-04-28 — wiki entry created
Initial documentation. No config changes.

### 2026-04-20 — joined the `media` GID 10000 standard
Idmap block applied; in-container `media` group at GID 10000 mapped to host GID 10000. See [media permissions](../infrastructure/media-permissions.md). Config backup: `/root/101.conf.bak.*`.
