{ config, pkgs, lib, ... }: let cfg = config.shb.arr; apps = { radarr = { defaultPort = 7001; settingsFormat = formatXML {}; moreOptions = { settings = lib.mkOption { default = {}; type = lib.types.submodule { freeformType = apps.radarr.settingsFormat.type; options = { APIKeyFile = lib.mkOption { type = lib.types.path; }; LogLevel = lib.mkOption { type = lib.types.enum ["debug" "info"]; default = "info"; }; }; }; }; }; }; sonarr = { defaultPort = 8989; }; bazarr = { defaultPort = 6767; }; readarr = { defaultPort = 8787; }; lidarr = { defaultPort = 8686; }; jackett = { defaultPort = 9117; settingsFormat = pkgs.formats.json {}; moreOptions = { settings = lib.mkOption { default = {}; type = lib.types.submodule { freeformType = apps.jackett.settingsFormat.type; options = { APIKeyFile = lib.mkOption { type = lib.types.path; }; FlareSolverrUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; }; OmdbApiKeyFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; }; ProxyType = lib.mkOption { type = lib.types.enum [ "-1" "0" "1" "2" ]; default = "0"; description = '' -1 = disabled 0 = HTTP 1 = SOCKS4 2 = SOCKS5 ''; }; ProxyUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; }; ProxyPort = lib.mkOption { type = lib.types.nullOr lib.types.port; default = null; }; }; }; }; }; }; }; formatXML = {}: { type = with lib.types; let valueType = nullOr (oneOf [ bool int float str path (attrsOf valueType) (listOf valueType) ]) // { description = "XML value"; }; in valueType; generate = name: value: pkgs.callPackage ({ runCommand, python3 }: runCommand name { value = builtins.toJSON {Config = value;}; passAsFile = [ "value" ]; } (pkgs.writers.writePython3 "dict2xml" { libraries = with python3.pkgs; [ python dict2xml ]; } '' import os import json from dict2xml import dict2xml with open(os.environ["valuePath"]) as f: content = json.loads(f.read()) if content is None: print("Could not parse env var valuePath as json") os.exit(2) with open(os.environ["out"], "w") as out: out.write(dict2xml(content)) '')) {}; }; appOption = name: c: lib.nameValuePair name (lib.mkOption { description = "Configuration for ${name}"; default = {}; type = lib.types.submodule { options = { enable = lib.mkEnableOption "selfhostblocks.${name}"; subdomain = lib.mkOption { type = lib.types.str; description = "Subdomain under which ${name} will be served."; example = name; }; domain = lib.mkOption { type = lib.types.str; description = "Domain under which ${name} will be served."; example = "mydomain.com"; }; port = lib.mkOption { type = lib.types.port; description = "Port on which ${name} listens to incoming requests."; default = c.defaultPort; }; dataDir = lib.mkOption { type = lib.types.str; description = "Directory where state of ${name} is stored."; default = "/var/lib/${name}"; }; oidcEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "OIDC endpoint for SSO"; example = "https://authelia.example.com"; }; backupCfg = lib.mkOption { type = lib.types.anything; description = "Backup configuration for ${name}."; default = {}; example = { backend = "restic"; repositories = []; }; }; } // (c.moreOptions or {}); }; }); template = file: newPath: replacements: let templatePath = newPath + ".template"; sedPatterns = lib.strings.concatStringsSep " " (lib.attrsets.mapAttrsToList (from: to: "-e \"s|${from}|${to}|\"") replacements); in '' ln -fs ${file} ${templatePath} rm ${newPath} || : sed ${sedPatterns} ${templatePath} > ${newPath} ''; in { options.shb.arr = lib.listToAttrs (lib.mapAttrsToList appOption apps); config = lib.mkMerge ([ { services.radarr = lib.mkIf cfg.radarr.enable { enable = true; dataDir = "/var/lib/radarr"; }; users.users.radarr = lib.mkIf cfg.radarr.enable { extraGroups = [ "media" ]; }; shb.arr.radarr.settings = lib.mkIf cfg.radarr.enable { Port = config.shb.arr.radarr.port; BindAddress = "127.0.0.1"; UrlBase = ""; EnableSsl = "false"; AuthenticationMethod = "External"; AuthenticationRequired = "Enabled"; }; systemd.services.radarr.preStart = let s = cfg.radarr.settings; templatedfileSettings = lib.optionalAttrs (!(isNull s.APIKeyFile)) { ApiKey = "%APIKEY%"; }; templatedSettings = (removeAttrs s [ "APIKeyFile" ]) // templatedfileSettings; t = template (apps.radarr.settingsFormat.generate " config.xml" templatedSettings) "${config.services.radarr.dataDir}/config.xml" ( lib.optionalAttrs (!(isNull s.APIKeyFile)) { "%APIKEY%" = "$(cat ${s.APIKeyFile})"; } ); in lib.mkIf cfg.radarr.enable t; # Listens on port 8989 services.sonarr = lib.mkIf cfg.sonarr.enable { enable = true; dataDir = "/var/lib/sonarr"; }; users.users.sonarr = lib.mkIf cfg.sonarr.enable { extraGroups = [ "media" ]; }; services.bazarr = lib.mkIf cfg.bazarr.enable { enable = true; listenPort = cfg.bazarr.port; }; users.users.bazarr = lib.mkIf cfg.bazarr.enable { extraGroups = [ "media" ]; }; # Listens on port 8787 services.readarr = lib.mkIf cfg.readarr.enable { enable = true; dataDir = "/var/lib/readarr"; }; users.users.readarr = lib.mkIf cfg.readarr.enable { extraGroups = [ "media" ]; }; # Listens on port 8686 services.lidarr = lib.mkIf cfg.lidarr.enable { enable = true; dataDir = "/var/lib/lidarr"; }; users.users.lidarr = lib.mkIf cfg.lidarr.enable { extraGroups = [ "media" ]; }; # Listens on port 9117 services.jackett = lib.mkIf cfg.jackett.enable { enable = true; dataDir = "/var/lib/jackett"; }; shb.arr.jackett.settings = lib.mkIf cfg.jackett.enable { Port = config.shb.arr.jackett.port; AllowExternal = "false"; UpdateDisabled = "true"; }; users.users.jackett = lib.mkIf cfg.jackett.enable { extraGroups = [ "media" ]; }; systemd.services.jackett.preStart = let s = cfg.jackett.settings; templatedfileSettings = lib.optionalAttrs (!(isNull s.APIKeyFile)) { APIKey = "%APIKEY%"; } // lib.optionalAttrs (!(isNull s.OmdbApiKeyFile)) { OmdbApiKey = "%OMDBAPIKEY%"; }; templatedSettings = (removeAttrs s [ "APIKeyFile" "OmdbApiKeyFile" ]) // templatedfileSettings; t = template (apps.jackett.settingsFormat.generate "jackett.json" templatedSettings) "${config.services.jackett.dataDir}/ServerConfig.json" ( lib.optionalAttrs (!(isNull s.APIKeyFile)) { "%APIKEY%" = "$(cat ${s.APIKeyFile})"; } // lib.optionalAttrs (!(isNull s.OmdbApiKeyFile)) { "%OMDBAPIKEY%" = "$(cat ${s.OmdbApiKeyFile})"; } ); in lib.mkIf cfg.jackett.enable t; shb.nginx.autheliaProtect = let appProtectConfig = name: _defaults: let c = cfg.${name}; in lib.mkIf (c.oidcEndpoint != null) { inherit (c) subdomain domain oidcEndpoint; upstream = "http://127.0.0.1:${toString c.port}"; autheliaRules = [ { domain = "${c.subdomain}.${c.domain}"; policy = "bypass"; resources = [ "^/api.*" ] ++ lib.optionals (name == "jackett") [ "^/dl.*" ]; } { domain = "${c.subdomain}.${c.domain}"; policy = "two_factor"; subject = ["group:arr_user"]; } ]; }; in lib.mapAttrsToList appProtectConfig apps; shb.backup.instances = let backupConfig = name: _defaults: lib.mkIf (cfg.${name}.backupCfg != {}) { ${name} = { sourceDirectories = [ config.shb.arr.${name}.dataDir ]; excludePatterns = [".db-shm" ".db-wal" ".mono"]; }; }; in lib.mkMerge (lib.mapAttrsToList backupConfig apps); } ] ++ map (name: lib.mkIf cfg.${name}.enable { systemd.tmpfiles.rules = lib.mkIf (lib.hasAttr "dataDir" config.services.${name}) [ "d '${config.services.${name}.dataDir}' 0750 ${config.services.${name}.user} ${config.services.${name}.group} - -" ]; users.groups.${name} = { members = [ "backup" ]; }; systemd.services.${name}.serviceConfig = { # Setup permissions needed for backups, as the backup user is member of the jellyfin group. UMask = lib.mkForce "0027"; StateDirectoryMode = lib.mkForce "0750"; }; }) (lib.attrNames apps)); }