Files
homey/modules/services/jellyfin.nix
T
2026-08-01 14:59:43 +03:00

168 lines
6.2 KiB
Nix

{ config, lib, pkgs, homeyConfig, ... }:
# Jellyfin — media server.
#
# Auth model: Jellyfin-native (no Authelia SSO — Jellyfin intercepts the
# forward_auth flow in ways that break SSO). LDAP plugin can be installed
# manually via the UI; the XML config is pre-seeded on first start so the
# plugin settings are pre-populated when the user opens the config page.
#
# Manual step after first deploy:
# 1. Dashboard → Plugins → Catalog → search "LDAP Authentication" → Install
# 2. Restart Jellyfin (Dashboard → Administration → Restart)
# 3. Dashboard → Plugins → LDAP Authentication → verify fields → Save
#
# Volume layout:
# <dataDir>/jellyfin/config/ → /config
# <dataDir>/media/movies/ → /data/movies
# <dataDir>/media/tvshows/ → /data/tvshows
# <dataDir>/media/general/ → /data/general
# <dataDir>/media/complete/ → /data/complete
let
cfg = config.homey.jellyfin;
dataDir = config.homey.storage.mountPoint;
domain = homeyConfig.domain;
ldapBaseDn = lib.concatStringsSep ","
(map (p: "dc=${p}") (lib.splitString "." domain));
ldapConfigDir = "${dataDir}/jellyfin/config/plugins/configurations";
ldapConfigXml = "${ldapConfigDir}/LDAP-Auth.xml";
in
{
options.homey.jellyfin = {
enable = lib.mkEnableOption "Jellyfin media server";
image = lib.mkOption {
type = lib.types.str;
default = "docker.io/jellyfin/jellyfin:latest";
};
port = lib.mkOption {
type = lib.types.port;
default = 8096;
};
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."openldap/ro_password" = { owner = "root"; };
# -----------------------------------------------------------------------
# Container
# -----------------------------------------------------------------------
virtualisation.oci-containers.containers.jellyfin = {
image = cfg.image;
ports = [ "127.0.0.1:${toString cfg.port}:8096" ];
environment = {
JELLYFIN_PublishedServerUrl = "https://media.${domain}";
PUID = "1000";
PGID = "1000";
};
volumes = [
"${dataDir}/jellyfin/config:/config"
"${dataDir}/media/movies:/data/movies:ro"
"${dataDir}/media/tvshows:/data/tvshows:ro"
"${dataDir}/media/general:/data/general:ro"
"${dataDir}/media/complete:/data/complete:ro"
];
extraOptions = [ "--network=homey" ];
};
# -----------------------------------------------------------------------
# ExecStartPre: pre-seed LDAP plugin config on first start only
# -----------------------------------------------------------------------
systemd.services."podman-jellyfin" = {
serviceConfig.ExecStartPre = [
(pkgs.writeShellScript "jellyfin-seed-ldap-config" ''
set -euo pipefail
# Only write on first start; preserve any UI edits on subsequent starts.
if [ -f "${ldapConfigXml}" ]; then
exit 0
fi
mkdir -p "${ldapConfigDir}"
RO_PASSWORD=$(cat ${config.sops.secrets."openldap/ro_password".path})
cat > "${ldapConfigXml}" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<LdapServer>openldap</LdapServer>
<LdapPort>389</LdapPort>
<UseSsl>false</UseSsl>
<UseStartTls>false</UseStartTls>
<SkipSslVerify>false</SkipSslVerify>
<LdapBindUser>cn=readonly,${ldapBaseDn}</LdapBindUser>
<LdapBindPassword>$RO_PASSWORD</LdapBindPassword>
<LdapBaseDn>ou=users,${ldapBaseDn}</LdapBaseDn>
<LdapSearchFilter></LdapSearchFilter>
<LdapSearchAttributes>uid,cn,mail,displayName</LdapSearchAttributes>
<LdapUsernameAttribute>uid</LdapUsernameAttribute>
<LdapPasswordAttribute>userPassword</LdapPasswordAttribute>
<EnableAllFolders>true</EnableAllFolders>
<EnabledFolders />
<AdminBaseDn>cn=admins,ou=groups,${ldapBaseDn}</AdminBaseDn>
<AdminFilter></AdminFilter>
<AllowPassChange>false</AllowPassChange>
<CreateUsersFromLdap>true</CreateUsersFromLdap>
<EnableCaseSensitiveUsername>false</EnableCaseSensitiveUsername>
<LdapClientCertPath></LdapClientCertPath>
<LdapClientKeyPath></LdapClientKeyPath>
<LdapRootCaPath></LdapRootCaPath>
<PasswordResetUrl></PasswordResetUrl>
</PluginConfiguration>
EOF
chmod 600 "${ldapConfigXml}"
'')
];
after = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ];
requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ];
};
# -----------------------------------------------------------------------
# Authelia access control — bypass; Jellyfin has its own login UI and
# does not work correctly behind SSO forward_auth.
# -----------------------------------------------------------------------
homey.authelia.accessControlRules = [{
priority = 60;
domain = [ "media.${domain}" ];
policy = "bypass";
}];
# -----------------------------------------------------------------------
# Caddy virtual host — no forward_auth; Jellyfin manages its own auth
# -----------------------------------------------------------------------
homey.caddy.virtualHosts = [{
subdomain = "media";
port = cfg.port;
auth = false;
}];
# -----------------------------------------------------------------------
# Storage directories
# -----------------------------------------------------------------------
homey.storage.extraDirs = [
{ path = "jellyfin"; }
{ path = "jellyfin/config"; }
];
# -----------------------------------------------------------------------
# Backup
# -----------------------------------------------------------------------
homey.backup.extraPaths = [ "${dataDir}/jellyfin" ];
# -----------------------------------------------------------------------
# Uptime Kuma monitor
# -----------------------------------------------------------------------
homey.monitoring.monitors = [{
name = "Jellyfin";
url = "https://media.${domain}";
interval = 60;
}];
};
}