2023-09-24 22:31:21 +02:00
|
|
|
{ config, pkgs, lib, ... }:
|
|
|
|
|
|
|
|
let
|
|
|
|
cfg = config.shb.arr;
|
|
|
|
|
2024-01-12 08:22:46 +01:00
|
|
|
contracts = pkgs.callPackage ../contracts {};
|
|
|
|
|
2023-09-24 22:31:21 +02:00
|
|
|
apps = {
|
|
|
|
radarr = {
|
2023-11-15 21:44:47 +01:00
|
|
|
defaultPort = 7001;
|
|
|
|
settingsFormat = formatXML {};
|
|
|
|
moreOptions = {
|
|
|
|
settings = lib.mkOption {
|
2023-12-04 09:33:16 +01:00
|
|
|
description = "Specific options for radarr.";
|
2023-11-15 21:44:47 +01:00
|
|
|
default = {};
|
|
|
|
type = lib.types.submodule {
|
|
|
|
freeformType = apps.radarr.settingsFormat.type;
|
|
|
|
options = {
|
|
|
|
APIKeyFile = lib.mkOption {
|
|
|
|
type = lib.types.path;
|
2023-12-04 09:33:16 +01:00
|
|
|
description = "Path to api key secret file.";
|
2023-11-15 21:44:47 +01:00
|
|
|
};
|
|
|
|
LogLevel = lib.mkOption {
|
|
|
|
type = lib.types.enum ["debug" "info"];
|
2023-12-04 09:33:16 +01:00
|
|
|
description = "Log level.";
|
2023-11-15 21:44:47 +01:00
|
|
|
default = "info";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
2023-09-24 22:31:21 +02:00
|
|
|
};
|
|
|
|
sonarr = {
|
2023-09-26 08:15:36 +02:00
|
|
|
defaultPort = 8989;
|
2023-09-24 22:31:21 +02:00
|
|
|
};
|
|
|
|
bazarr = {
|
|
|
|
defaultPort = 6767;
|
|
|
|
};
|
|
|
|
readarr = {
|
|
|
|
defaultPort = 8787;
|
|
|
|
};
|
|
|
|
lidarr = {
|
|
|
|
defaultPort = 8686;
|
|
|
|
};
|
2023-10-13 07:23:58 +02:00
|
|
|
jackett = {
|
|
|
|
defaultPort = 9117;
|
|
|
|
settingsFormat = pkgs.formats.json {};
|
|
|
|
moreOptions = {
|
|
|
|
settings = lib.mkOption {
|
2023-11-30 21:06:41 +01:00
|
|
|
description = "Specific options for jackett.";
|
2023-11-08 21:27:47 +01:00
|
|
|
default = {};
|
2023-10-13 07:23:58 +02:00
|
|
|
type = lib.types.submodule {
|
|
|
|
freeformType = apps.jackett.settingsFormat.type;
|
|
|
|
options = {
|
|
|
|
APIKeyFile = lib.mkOption {
|
|
|
|
type = lib.types.path;
|
2023-11-30 21:06:41 +01:00
|
|
|
description = "Path to api key secret file.";
|
2023-10-13 07:23:58 +02:00
|
|
|
};
|
|
|
|
FlareSolverrUrl = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
2023-11-30 21:06:41 +01:00
|
|
|
description = "FlareSolverr endpoint.";
|
2023-10-13 07:23:58 +02:00
|
|
|
default = null;
|
|
|
|
};
|
|
|
|
OmdbApiKeyFile = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.path;
|
2023-12-04 09:33:16 +01:00
|
|
|
description = "File containing the Open Movie Database API key.";
|
2023-10-13 07:23:58 +02:00
|
|
|
default = null;
|
|
|
|
};
|
|
|
|
ProxyType = lib.mkOption {
|
|
|
|
type = lib.types.enum [ "-1" "0" "1" "2" ];
|
2023-11-30 21:06:41 +01:00
|
|
|
default = "-1";
|
2023-10-13 07:23:58 +02:00
|
|
|
description = ''
|
|
|
|
-1 = disabled
|
|
|
|
0 = HTTP
|
|
|
|
1 = SOCKS4
|
|
|
|
2 = SOCKS5
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
ProxyUrl = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
2023-12-04 09:33:16 +01:00
|
|
|
description = "URL of the proxy. Ignored if ProxyType is set to -1";
|
2023-10-13 07:23:58 +02:00
|
|
|
default = null;
|
|
|
|
};
|
|
|
|
ProxyPort = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.port;
|
2023-12-04 09:33:16 +01:00
|
|
|
description = "Port of the proxy. Ignored if ProxyType is set to -1";
|
2023-10-13 07:23:58 +02:00
|
|
|
default = null;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
2023-09-24 22:31:21 +02:00
|
|
|
};
|
|
|
|
|
2023-11-15 21:44:47 +01:00
|
|
|
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))
|
|
|
|
'')) {};
|
|
|
|
|
|
|
|
};
|
|
|
|
|
2023-09-24 22:31:21 +02:00
|
|
|
appOption = name: c: lib.nameValuePair name (lib.mkOption {
|
|
|
|
description = "Configuration for ${name}";
|
2023-11-08 21:27:47 +01:00
|
|
|
default = {};
|
2023-09-24 22:31:21 +02:00
|
|
|
type = lib.types.submodule {
|
|
|
|
options = {
|
2023-11-30 21:06:41 +01:00
|
|
|
enable = lib.mkEnableOption name;
|
2023-09-24 22:31:21 +02:00
|
|
|
|
|
|
|
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.";
|
2023-11-30 21:06:41 +01:00
|
|
|
example = "example.com";
|
2023-09-24 22:31:21 +02:00
|
|
|
};
|
|
|
|
|
2024-01-12 08:22:46 +01:00
|
|
|
ssl = lib.mkOption {
|
|
|
|
description = "Path to SSL files";
|
|
|
|
type = lib.types.nullOr contracts.ssl.certs;
|
|
|
|
default = null;
|
|
|
|
};
|
|
|
|
|
2023-09-24 22:31:21 +02:00
|
|
|
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;
|
2023-11-30 21:06:41 +01:00
|
|
|
description = "Directory where ${name} stores data.";
|
2023-09-24 22:31:21 +02:00
|
|
|
default = "/var/lib/${name}";
|
|
|
|
};
|
|
|
|
|
2023-11-30 21:48:57 +01:00
|
|
|
authEndpoint = lib.mkOption {
|
2023-11-08 21:27:47 +01:00
|
|
|
type = lib.types.nullOr lib.types.str;
|
|
|
|
default = null;
|
2023-11-30 21:06:41 +01:00
|
|
|
description = "Endpoint to the SSO provider. Leave null to not have SSO configured.";
|
2023-09-24 22:31:21 +02:00
|
|
|
example = "https://authelia.example.com";
|
|
|
|
};
|
2023-11-17 08:00:53 +01:00
|
|
|
|
|
|
|
backupCfg = lib.mkOption {
|
|
|
|
type = lib.types.anything;
|
|
|
|
description = "Backup configuration for ${name}.";
|
|
|
|
default = {};
|
|
|
|
example = {
|
|
|
|
backend = "restic";
|
|
|
|
repositories = [];
|
|
|
|
};
|
|
|
|
};
|
2023-10-13 07:23:58 +02:00
|
|
|
} // (c.moreOptions or {});
|
2023-09-24 22:31:21 +02:00
|
|
|
};
|
|
|
|
});
|
2023-10-13 07:23:58 +02:00
|
|
|
|
|
|
|
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}
|
|
|
|
'';
|
2023-09-24 22:31:21 +02:00
|
|
|
in
|
|
|
|
{
|
|
|
|
options.shb.arr = lib.listToAttrs (lib.mapAttrsToList appOption apps);
|
|
|
|
|
2023-09-27 05:13:08 +02:00
|
|
|
config = lib.mkMerge ([
|
|
|
|
{
|
|
|
|
services.radarr = lib.mkIf cfg.radarr.enable {
|
|
|
|
enable = true;
|
|
|
|
dataDir = "/var/lib/radarr";
|
|
|
|
};
|
2023-11-08 21:27:47 +01:00
|
|
|
users.users.radarr = lib.mkIf cfg.radarr.enable {
|
2023-09-27 05:13:08 +02:00
|
|
|
extraGroups = [ "media" ];
|
|
|
|
};
|
2023-11-15 21:44:47 +01:00
|
|
|
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;
|
2023-09-24 22:31:21 +02:00
|
|
|
|
2023-09-27 05:13:08 +02:00
|
|
|
# Listens on port 8989
|
|
|
|
services.sonarr = lib.mkIf cfg.sonarr.enable {
|
|
|
|
enable = true;
|
|
|
|
dataDir = "/var/lib/sonarr";
|
|
|
|
};
|
2023-11-08 21:27:47 +01:00
|
|
|
users.users.sonarr = lib.mkIf cfg.sonarr.enable {
|
2023-09-27 05:13:08 +02:00
|
|
|
extraGroups = [ "media" ];
|
|
|
|
};
|
2023-09-26 08:15:36 +02:00
|
|
|
|
2023-09-27 05:13:08 +02:00
|
|
|
services.bazarr = lib.mkIf cfg.bazarr.enable {
|
|
|
|
enable = true;
|
|
|
|
listenPort = cfg.bazarr.port;
|
|
|
|
};
|
2023-11-08 21:27:47 +01:00
|
|
|
users.users.bazarr = lib.mkIf cfg.bazarr.enable {
|
|
|
|
extraGroups = [ "media" ];
|
|
|
|
};
|
2023-09-24 22:31:21 +02:00
|
|
|
|
2023-09-27 05:13:08 +02:00
|
|
|
# Listens on port 8787
|
|
|
|
services.readarr = lib.mkIf cfg.readarr.enable {
|
|
|
|
enable = true;
|
|
|
|
dataDir = "/var/lib/readarr";
|
|
|
|
};
|
2023-11-08 21:27:47 +01:00
|
|
|
users.users.readarr = lib.mkIf cfg.readarr.enable {
|
2023-09-30 08:19:39 +02:00
|
|
|
extraGroups = [ "media" ];
|
|
|
|
};
|
2023-09-24 22:31:21 +02:00
|
|
|
|
2023-09-27 05:13:08 +02:00
|
|
|
# Listens on port 8686
|
|
|
|
services.lidarr = lib.mkIf cfg.lidarr.enable {
|
|
|
|
enable = true;
|
|
|
|
dataDir = "/var/lib/lidarr";
|
|
|
|
};
|
2023-11-08 21:27:47 +01:00
|
|
|
users.users.lidarr = lib.mkIf cfg.lidarr.enable {
|
2023-09-30 08:19:39 +02:00
|
|
|
extraGroups = [ "media" ];
|
|
|
|
};
|
2023-09-24 22:31:21 +02:00
|
|
|
|
2023-10-13 07:23:58 +02:00
|
|
|
# Listens on port 9117
|
|
|
|
services.jackett = lib.mkIf cfg.jackett.enable {
|
|
|
|
enable = true;
|
|
|
|
dataDir = "/var/lib/jackett";
|
|
|
|
};
|
2023-11-08 21:27:47 +01:00
|
|
|
shb.arr.jackett.settings = lib.mkIf cfg.jackett.enable {
|
2023-10-13 07:23:58 +02:00
|
|
|
Port = config.shb.arr.jackett.port;
|
|
|
|
AllowExternal = "false";
|
|
|
|
UpdateDisabled = "true";
|
|
|
|
};
|
2023-11-08 21:27:47 +01:00
|
|
|
users.users.jackett = lib.mkIf cfg.jackett.enable {
|
2023-10-13 07:23:58 +02:00
|
|
|
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;
|
2023-11-08 21:27:47 +01:00
|
|
|
|
|
|
|
t = template (apps.jackett.settingsFormat.generate "jackett.json" templatedSettings) "${config.services.jackett.dataDir}/ServerConfig.json" (
|
2023-10-13 07:23:58 +02:00
|
|
|
lib.optionalAttrs (!(isNull s.APIKeyFile)) {
|
|
|
|
"%APIKEY%" = "$(cat ${s.APIKeyFile})";
|
|
|
|
} // lib.optionalAttrs (!(isNull s.OmdbApiKeyFile)) {
|
|
|
|
"%OMDBAPIKEY%" = "$(cat ${s.OmdbApiKeyFile})";
|
|
|
|
}
|
|
|
|
);
|
2023-11-08 21:27:47 +01:00
|
|
|
in
|
|
|
|
lib.mkIf cfg.jackett.enable t;
|
2023-10-13 07:23:58 +02:00
|
|
|
|
2023-09-27 05:13:08 +02:00
|
|
|
shb.nginx.autheliaProtect =
|
|
|
|
let
|
|
|
|
appProtectConfig = name: _defaults:
|
|
|
|
let
|
|
|
|
c = cfg.${name};
|
|
|
|
in
|
2023-11-30 21:48:57 +01:00
|
|
|
lib.mkIf (c.authEndpoint != null) {
|
2024-01-12 08:22:46 +01:00
|
|
|
inherit (c) subdomain domain authEndpoint ssl;
|
2023-09-27 05:13:08 +02:00
|
|
|
upstream = "http://127.0.0.1:${toString c.port}";
|
|
|
|
autheliaRules = [
|
|
|
|
{
|
|
|
|
domain = "${c.subdomain}.${c.domain}";
|
|
|
|
policy = "bypass";
|
|
|
|
resources = [
|
|
|
|
"^/api.*"
|
2023-10-13 07:38:38 +02:00
|
|
|
] ++ lib.optionals (name == "jackett") [
|
|
|
|
"^/dl.*"
|
2023-09-27 05:13:08 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
{
|
|
|
|
domain = "${c.subdomain}.${c.domain}";
|
|
|
|
policy = "two_factor";
|
|
|
|
subject = ["group:arr_user"];
|
|
|
|
}
|
|
|
|
];
|
2023-09-24 22:31:21 +02:00
|
|
|
};
|
2023-09-27 05:13:08 +02:00
|
|
|
in
|
|
|
|
lib.mapAttrsToList appProtectConfig apps;
|
|
|
|
|
|
|
|
shb.backup.instances =
|
|
|
|
let
|
2023-11-17 08:48:19 +01:00
|
|
|
backupConfig = name: _defaults: lib.mkIf (cfg.${name}.backupCfg.enable or false) ({
|
|
|
|
${name} = (
|
|
|
|
cfg.${name}.backupCfg
|
|
|
|
// {
|
|
|
|
sourceDirectories = [
|
|
|
|
config.shb.arr.${name}.dataDir
|
|
|
|
];
|
|
|
|
excludePatterns = [".db-shm" ".db-wal" ".mono"];
|
|
|
|
});
|
|
|
|
});
|
2023-09-27 05:13:08 +02:00
|
|
|
in
|
|
|
|
lib.mkMerge (lib.mapAttrsToList backupConfig apps);
|
|
|
|
}
|
2023-11-08 21:27:47 +01:00
|
|
|
] ++ map (name: lib.mkIf cfg.${name}.enable {
|
2023-09-27 05:13:08 +02:00
|
|
|
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));
|
2023-09-24 22:31:21 +02:00
|
|
|
}
|