{ config, pkgs, lib, shb, ... }: let cfg = config.shb.arr; apps = { radarr = { settingsFormat = shb.formatXML { enclosingRoot = "Config"; }; moreOptions = { settings = lib.mkOption { description = "Specific options for radarr."; default = { }; type = lib.types.submodule { freeformType = apps.radarr.settingsFormat.type; options = { ApiKey = lib.mkOption { type = shb.secretFileType; description = "Path to api key secret file."; }; LogLevel = lib.mkOption { type = lib.types.enum [ "debug" "info" ]; description = "Log level."; default = "info"; }; Port = lib.mkOption { type = lib.types.port; description = "Port on which radarr listens to incoming requests."; default = 7878; }; AnalyticsEnabled = lib.mkOption { type = lib.types.bool; description = "Wether to send anonymous data or not."; default = false; }; BindAddress = lib.mkOption { type = lib.types.str; internal = true; default = "127.0.0.1"; }; UrlBase = lib.mkOption { type = lib.types.str; internal = true; default = ""; }; EnableSsl = lib.mkOption { type = lib.types.bool; internal = true; default = false; }; AuthenticationMethod = lib.mkOption { type = lib.types.str; internal = true; default = "External"; }; AuthenticationRequired = lib.mkOption { type = lib.types.str; internal = true; default = "Enabled"; }; }; }; }; }; }; sonarr = { settingsFormat = shb.formatXML { enclosingRoot = "Config"; }; moreOptions = { settings = lib.mkOption { description = "Specific options for sonarr."; default = { }; type = lib.types.submodule { freeformType = apps.sonarr.settingsFormat.type; options = { ApiKey = lib.mkOption { type = shb.secretFileType; description = "Path to api key secret file."; }; LogLevel = lib.mkOption { type = lib.types.enum [ "debug" "info" ]; description = "Log level."; default = "info"; }; Port = lib.mkOption { type = lib.types.port; description = "Port on which sonarr listens to incoming requests."; default = 8989; }; BindAddress = lib.mkOption { type = lib.types.str; internal = true; default = "127.0.0.1"; }; UrlBase = lib.mkOption { type = lib.types.str; internal = true; default = ""; }; EnableSsl = lib.mkOption { type = lib.types.bool; internal = true; default = false; }; AuthenticationMethod = lib.mkOption { type = lib.types.str; internal = true; default = "External"; }; AuthenticationRequired = lib.mkOption { type = lib.types.str; internal = true; default = "Enabled"; }; }; }; }; }; }; bazarr = { settingsFormat = shb.formatXML { enclosingRoot = "Config"; }; moreOptions = { settings = lib.mkOption { description = "Specific options for bazarr."; default = { }; type = lib.types.submodule { freeformType = apps.bazarr.settingsFormat.type; options = { LogLevel = lib.mkOption { type = lib.types.enum [ "debug" "info" ]; description = "Log level."; default = "info"; }; ApiKey = lib.mkOption { type = shb.secretFileType; description = "Path to api key secret file."; }; Port = lib.mkOption { type = lib.types.port; description = "Port on which bazarr listens to incoming requests."; default = 6767; readOnly = true; }; }; }; }; }; }; readarr = { settingsFormat = shb.formatXML { enclosingRoot = "Config"; }; moreOptions = { settings = lib.mkOption { description = "Specific options for readarr."; default = { }; type = lib.types.submodule { freeformType = apps.readarr.settingsFormat.type; options = { LogLevel = lib.mkOption { type = lib.types.enum [ "debug" "info" ]; description = "Log level."; default = "info"; }; ApiKey = lib.mkOption { type = shb.secretFileType; description = "Path to api key secret file."; }; Port = lib.mkOption { type = lib.types.port; description = "Port on which readarr listens to incoming requests."; default = 8787; }; }; }; }; }; }; lidarr = { settingsFormat = shb.formatXML { enclosingRoot = "Config"; }; moreOptions = { settings = lib.mkOption { description = "Specific options for lidarr."; default = { }; type = lib.types.submodule { freeformType = apps.lidarr.settingsFormat.type; options = { LogLevel = lib.mkOption { type = lib.types.enum [ "debug" "info" ]; description = "Log level."; default = "info"; }; ApiKey = lib.mkOption { type = shb.secretFileType; description = "Path to api key secret file."; }; Port = lib.mkOption { type = lib.types.port; description = "Port on which lidarr listens to incoming requests."; default = 8686; }; }; }; }; }; }; jackett = { settingsFormat = pkgs.formats.json { }; moreOptions = { settings = lib.mkOption { description = "Specific options for jackett."; default = { }; type = lib.types.submodule { freeformType = apps.jackett.settingsFormat.type; options = { ApiKey = lib.mkOption { type = shb.secretFileType; description = "Path to api key secret file."; }; FlareSolverrUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "FlareSolverr endpoint."; default = null; }; OmdbApiKey = lib.mkOption { type = lib.types.nullOr shb.secretFileType; description = "File containing the Open Movie Database API key."; default = null; }; ProxyType = lib.mkOption { type = lib.types.enum [ "-1" "0" "1" "2" ]; default = "-1"; description = '' -1 = disabled 0 = HTTP 1 = SOCKS4 2 = SOCKS5 ''; }; ProxyUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "URL of the proxy. Ignored if ProxyType is set to -1"; default = null; }; ProxyPort = lib.mkOption { type = lib.types.nullOr lib.types.port; description = "Port of the proxy. Ignored if ProxyType is set to -1"; default = null; }; Port = lib.mkOption { type = lib.types.port; description = "Port on which jackett listens to incoming requests."; default = 9117; readOnly = true; }; AllowExternal = lib.mkOption { type = lib.types.bool; internal = true; default = false; }; UpdateDisabled = lib.mkOption { type = lib.types.bool; internal = true; default = true; }; }; }; }; }; }; }; vhosts = { extraBypassResources ? [ ], }: c: { inherit (c) subdomain domain authEndpoint ssl ; upstream = "http://127.0.0.1:${toString c.settings.Port}"; autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [ { domain = "${c.subdomain}.${c.domain}"; policy = "bypass"; resources = extraBypassResources ++ [ "^/api.*" "^/feed.*" ]; } { domain = "${c.subdomain}.${c.domain}"; policy = "two_factor"; subject = [ "group:${c.ldapUserGroup}" ]; } ]; }; appOption = name: c: lib.nameValuePair name ( lib.mkOption { description = "Configuration for ${name}"; default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption 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 = "example.com"; }; dataDir = lib.mkOption { type = lib.types.str; description = "Directory where ${name} stores data."; default = "/var/lib/${name}"; }; ssl = lib.mkOption { description = "Path to SSL files"; type = lib.types.nullOr shb.contracts.ssl.certs; default = null; }; ldapUserGroup = lib.mkOption { description = '' LDAP group a user must belong to be able to login. Note that all users are admins too. ''; type = lib.types.str; default = "arr_user"; }; authEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Endpoint to the SSO provider. Leave null to not have SSO configured."; example = "https://authelia.example.com"; }; backup = lib.mkOption { description = '' Backup configuration. ''; default = { }; type = lib.types.submodule { options = shb.contracts.backup.mkRequester { user = name; sourceDirectories = [ cfg.${name}.dataDir ]; excludePatterns = [ ".db-shm" ".db-wal" ".mono" ]; }; }; }; dashboard = lib.mkOption { description = '' Dashboard contract consumer ''; default = { }; type = lib.types.submodule { options = shb.contracts.dashboard.mkRequester { externalUrl = "https://${cfg.${name}.subdomain}.${cfg.${name}.domain}"; externalUrlText = "https://\${config.shb.arr.${name}.subdomain}.\${config.shb.arr.${name}.domain}"; internalUrl = "http://127.0.0.1:${toString cfg.${name}.settings.Port}"; }; }; }; } // (c.moreOptions or { }); }; } ); in { imports = [ ../../lib/module.nix ../blocks/nginx.nix ../blocks/lldap.nix ]; options.shb.arr = lib.listToAttrs (lib.mapAttrsToList appOption apps); config = lib.mkMerge [ (lib.mkIf cfg.radarr.enable ( let cfg' = cfg.radarr; isSSOEnabled = !(isNull cfg'.authEndpoint); in { services.nginx.enable = true; services.radarr = { enable = true; dataDir = cfg'.dataDir; }; systemd.services.radarr.preStart = shb.replaceSecrets { userConfig = cfg'.settings // (lib.optionalAttrs isSSOEnabled { AuthenticationRequired = "DisabledForLocalAddresses"; AuthenticationMethod = "External"; }); resultPath = "${cfg'.dataDir}/config.xml"; generator = shb.replaceSecretsFormatAdapter apps.radarr.settingsFormat; }; shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.lldap.ensureGroups = { ${cfg'.ldapUserGroup} = { }; }; } )) (lib.mkIf cfg.sonarr.enable ( let cfg' = cfg.sonarr; isSSOEnabled = !(isNull cfg'.authEndpoint); in { systemd.tmpfiles.rules = [ "d ${cfg'.dataDir} 0700 ${config.services.sonarr.user} ${config.services.sonarr.user}" ]; services.nginx.enable = true; services.sonarr = { enable = true; dataDir = cfg'.dataDir; }; users.users.sonarr = { extraGroups = [ "media" ]; }; systemd.services.sonarr.preStart = shb.replaceSecrets { userConfig = cfg'.settings // (lib.optionalAttrs isSSOEnabled { AuthenticationRequired = "DisabledForLocalAddresses"; AuthenticationMethod = "External"; }); resultPath = "${cfg'.dataDir}/config.xml"; generator = apps.sonarr.settingsFormat.generate; }; shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.lldap.ensureGroups = { ${cfg'.ldapUserGroup} = { }; }; } )) (lib.mkIf cfg.bazarr.enable ( let cfg' = cfg.bazarr; isSSOEnabled = !(isNull cfg'.authEndpoint); in { services.bazarr = { enable = true; dataDir = cfg'.dataDir; listenPort = cfg'.settings.Port; }; users.users.bazarr = { extraGroups = [ "media" ]; }; # This is actually not working. Bazarr uses a config file in dataDir/config/config.yaml # which includes all configuration so we must somehow merge our declarative config with it. # It's doable but will take some time. Help is welcomed. # # systemd.services.bazarr.preStart = shb.replaceSecrets { # userConfig = # cfg'.settings # // (lib.optionalAttrs isSSOEnabled { # AuthenticationRequired = "DisabledForLocalAddresses"; # AuthenticationMethod = "External"; # }); # resultPath = "${cfg'.dataDir}/config.xml"; # generator = apps.bazarr.settingsFormat.generate; # }; shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.lldap.ensureGroups = { ${cfg'.ldapUserGroup} = { }; }; } )) (lib.mkIf cfg.readarr.enable ( let cfg' = cfg.readarr; isSSOEnabled = !(isNull cfg'.authEndpoint); in { services.readarr = { enable = true; dataDir = cfg'.dataDir; }; users.users.readarr = { extraGroups = [ "media" ]; }; systemd.services.readarr.preStart = shb.replaceSecrets { userConfig = cfg'.settings // (lib.optionalAttrs isSSOEnabled { AuthenticationRequired = "DisabledForLocalAddresses"; AuthenticationMethod = "External"; }); resultPath = "${cfg'.dataDir}/config.xml"; generator = apps.readarr.settingsFormat.generate; }; shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.lldap.ensureGroups = { ${cfg'.ldapUserGroup} = { }; }; } )) (lib.mkIf cfg.lidarr.enable ( let cfg' = cfg.lidarr; isSSOEnabled = !(isNull cfg'.authEndpoint); in { services.lidarr = { enable = true; dataDir = cfg'.dataDir; }; users.users.lidarr = { extraGroups = [ "media" ]; }; systemd.services.lidarr.preStart = shb.replaceSecrets { userConfig = cfg'.settings // (lib.optionalAttrs isSSOEnabled { AuthenticationRequired = "DisabledForLocalAddresses"; AuthenticationMethod = "External"; }); resultPath = "${cfg'.dataDir}/config.xml"; generator = apps.lidarr.settingsFormat.generate; }; shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.lldap.ensureGroups = { ${cfg'.ldapUserGroup} = { }; }; } )) (lib.mkIf cfg.jackett.enable ( let cfg' = cfg.jackett; in { services.jackett = { enable = true; dataDir = cfg'.dataDir; }; # TODO: avoid implicitly relying on the media group users.users.jackett = { extraGroups = [ "media" ]; }; systemd.services.jackett.preStart = shb.replaceSecrets { userConfig = shb.renameAttrName cfg'.settings "ApiKey" "APIKey"; resultPath = "${cfg'.dataDir}/ServerConfig.json"; generator = apps.jackett.settingsFormat.generate; }; shb.nginx.vhosts = [ (vhosts { extraBypassResources = [ "^/dl.*" ]; } cfg') ]; shb.lldap.ensureGroups = { ${cfg'.ldapUserGroup} = { }; }; } )) ]; }