jellyfin: Prefer fMP4-HLS Media Container for all users
All checks were successful
Build and Deploy / deploy (push) Successful in 2m41s

This commit is contained in:
2026-04-16 01:11:59 -04:00
parent 55fda4b5ee
commit 735603deb8
5 changed files with 241 additions and 0 deletions

View File

@@ -2,5 +2,6 @@
imports = [
./jellyfin.nix
./jellyfin-qbittorrent-monitor.nix
./jellyfin-set-defaults.nix
];
}

View File

@@ -0,0 +1,47 @@
{
pkgs,
config,
service_configs,
lib,
...
}:
lib.mkIf config.services.jellyfin.enable {
systemd.services."jellyfin-set-defaults" = {
description = "Enforce default Jellyfin user preferences (fMP4-HLS)";
after = [ "jellyfin.service" ];
requires = [ "jellyfin.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.python3}/bin/python ${./jellyfin-set-defaults.py}";
# Security hardening
DynamicUser = true;
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
LoadCredential = "jellyfin-api-key:${config.age.secrets.jellyfin-api-key.path}";
};
environment = {
JELLYFIN_URL = "http://127.0.0.1:${toString service_configs.ports.private.jellyfin.port}";
};
};
# Run at boot and daily to catch newly created users
systemd.timers."jellyfin-set-defaults" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = "1d";
};
};
}

View File

@@ -0,0 +1,97 @@
"""Enforce default Jellyfin user preferences via the API.
Iterates all users and sets preferFmp4HlsContainer in their
DisplayPreferences if not already enabled. Idempotent.
"""
import json
import os
import sys
import time
import urllib.request
from pathlib import Path
JELLYFIN_URL = os.environ.get("JELLYFIN_URL", "http://127.0.0.1:8096")
def get_api_key():
cred_dir = os.environ["CREDENTIALS_DIRECTORY"]
return Path(cred_dir, "jellyfin-api-key").read_text().strip()
def api(method, path, data=None):
url = f"{JELLYFIN_URL}{path}"
sep = "&" if "?" in url else "?"
url += f"{sep}api_key={get_api_key()}"
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(url, data=body, method=method)
if body:
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as resp:
if resp.status == 204:
return None
return json.loads(resp.read())
def wait_healthy(timeout=120):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
req = urllib.request.Request(f"{JELLYFIN_URL}/health")
with urllib.request.urlopen(req, timeout=5) as resp:
if resp.read().decode().strip() == "Healthy":
return
except Exception:
pass
time.sleep(2)
print("Jellyfin did not become healthy in time", file=sys.stderr)
sys.exit(1)
# Preferences to enforce: (key, desired_value)
DEFAULTS = [
("preferFmp4HlsContainer", "true"),
]
def main():
wait_healthy()
users = api("GET", "/Users")
if not users:
print("No users found, nothing to do")
return
for user in users:
user_id = user["Id"]
name = user["Name"]
prefs = api(
"GET",
f"/DisplayPreferences/usersettings?userId={user_id}&client=emby",
)
custom = prefs.get("CustomPrefs", {})
changed = False
for key, value in DEFAULTS:
if custom.get(key) != value:
custom[key] = value
changed = True
if not changed:
print(f"{name}: already up to date")
continue
prefs["CustomPrefs"] = custom
api(
"POST",
f"/DisplayPreferences/usersettings?userId={user_id}&client=emby",
prefs,
)
print(f"{name}: updated preferences")
if __name__ == "__main__":
main()