arr-init: add module for API-based configuration
This commit is contained in:
218
modules/arr-init.nix
Normal file
218
modules/arr-init.nix
Normal file
@@ -0,0 +1,218 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.arrInit;
|
||||
|
||||
downloadClientModule = lib.types.submodule {
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Display name of the download client (e.g. \"qBittorrent\").";
|
||||
example = "qBittorrent";
|
||||
};
|
||||
|
||||
implementation = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Implementation identifier for the Servarr API.";
|
||||
example = "QBittorrent";
|
||||
};
|
||||
|
||||
configContract = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Config contract identifier for the Servarr API.";
|
||||
example = "QBittorrentSettings";
|
||||
};
|
||||
|
||||
protocol = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"torrent"
|
||||
"usenet"
|
||||
];
|
||||
default = "torrent";
|
||||
description = "Download protocol type.";
|
||||
};
|
||||
|
||||
fields = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything;
|
||||
default = { };
|
||||
description = ''
|
||||
Flat key/value pairs for the download client configuration.
|
||||
These are converted to the API's [{name, value}] array format.
|
||||
'';
|
||||
example = {
|
||||
host = "192.168.15.1";
|
||||
port = 6011;
|
||||
useSsl = false;
|
||||
tvCategory = "tvshows";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
instanceModule = lib.types.submodule {
|
||||
options = {
|
||||
enable = lib.mkEnableOption "Servarr application API initialization";
|
||||
|
||||
serviceName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Name of the systemd service this init depends on.";
|
||||
example = "sonarr";
|
||||
};
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Path to the application data directory containing config.xml.";
|
||||
example = "/var/lib/sonarr";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
description = "API port of the Servarr application.";
|
||||
example = 8989;
|
||||
};
|
||||
|
||||
apiVersion = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "v3";
|
||||
description = "API version string used in the base URL.";
|
||||
};
|
||||
|
||||
downloadClients = lib.mkOption {
|
||||
type = lib.types.listOf downloadClientModule;
|
||||
default = [ ];
|
||||
description = "List of download clients to configure via the API.";
|
||||
};
|
||||
|
||||
rootFolders = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "List of root folder paths to configure via the API.";
|
||||
example = [
|
||||
"/media/tv"
|
||||
"/media/movies"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
curl = "${pkgs.curl}/bin/curl";
|
||||
jq = "${pkgs.jq}/bin/jq";
|
||||
grep = "${pkgs.gnugrep}/bin/grep";
|
||||
|
||||
mkDownloadClientPayload =
|
||||
dc:
|
||||
builtins.toJSON {
|
||||
enable = true;
|
||||
protocol = dc.protocol;
|
||||
priority = 1;
|
||||
name = dc.name;
|
||||
implementation = dc.implementation;
|
||||
configContract = dc.configContract;
|
||||
fields = lib.mapAttrsToList (n: v: {
|
||||
name = n;
|
||||
value = v;
|
||||
}) dc.fields;
|
||||
tags = [ ];
|
||||
};
|
||||
|
||||
mkDownloadClientSection = dc: ''
|
||||
# Download client: ${dc.name}
|
||||
echo "Checking download client '${dc.name}'..."
|
||||
EXISTING_DC=$(${curl} -sf "$BASE_URL/downloadclient" -H "X-Api-Key: $API_KEY")
|
||||
if echo "$EXISTING_DC" | ${jq} -e --arg name ${lib.escapeShellArg dc.name} '.[] | select(.name == $name)' > /dev/null 2>&1; then
|
||||
echo "Download client '${dc.name}' already exists, skipping"
|
||||
else
|
||||
echo "Adding download client '${dc.name}'..."
|
||||
${curl} -sf -X POST "$BASE_URL/downloadclient?forceSave=true" \
|
||||
-H "X-Api-Key: $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d ${lib.escapeShellArg (mkDownloadClientPayload dc)}
|
||||
echo "Download client '${dc.name}' added"
|
||||
fi
|
||||
'';
|
||||
|
||||
mkRootFolderSection = path: ''
|
||||
# Root folder: ${path}
|
||||
echo "Checking root folder '${path}'..."
|
||||
EXISTING_RF=$(${curl} -sf "$BASE_URL/rootfolder" -H "X-Api-Key: $API_KEY")
|
||||
if echo "$EXISTING_RF" | ${jq} -e --arg path ${lib.escapeShellArg path} '.[] | select(.path == $path)' > /dev/null 2>&1; then
|
||||
echo "Root folder '${path}' already exists, skipping"
|
||||
else
|
||||
echo "Adding root folder '${path}'..."
|
||||
${curl} -sf -X POST "$BASE_URL/rootfolder" \
|
||||
-H "X-Api-Key: $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d ${lib.escapeShellArg (builtins.toJSON { inherit path; })}
|
||||
echo "Root folder '${path}' added"
|
||||
fi
|
||||
'';
|
||||
|
||||
mkInitScript =
|
||||
name: inst:
|
||||
pkgs.writeShellScript "${name}-init" ''
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_XML="${inst.dataDir}/config.xml"
|
||||
|
||||
if [ ! -f "$CONFIG_XML" ]; then
|
||||
echo "Config file $CONFIG_XML not found, skipping ${name} init"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
API_KEY=$(${grep} -oP '(?<=<ApiKey>)[^<]+' "$CONFIG_XML")
|
||||
BASE_URL="http://localhost:${builtins.toString inst.port}/api/${inst.apiVersion}"
|
||||
|
||||
# Wait for API to become available
|
||||
echo "Waiting for ${name} API..."
|
||||
for i in $(seq 1 90); do
|
||||
if ${curl} -sf "$BASE_URL/system/status" -H "X-Api-Key: $API_KEY" > /dev/null 2>&1; then
|
||||
echo "${name} API is ready"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 90 ]; then
|
||||
echo "${name} API not available after 90 seconds" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
${lib.concatMapStringsSep "\n" mkDownloadClientSection inst.downloadClients}
|
||||
${lib.concatMapStringsSep "\n" mkRootFolderSection inst.rootFolders}
|
||||
|
||||
echo "${name} init complete"
|
||||
'';
|
||||
|
||||
enabledInstances = lib.filterAttrs (_: inst: inst.enable) cfg;
|
||||
in
|
||||
{
|
||||
options.services.arrInit = lib.mkOption {
|
||||
type = lib.types.attrsOf instanceModule;
|
||||
default = { };
|
||||
description = ''
|
||||
Attribute set of Servarr application instances to initialize via their APIs.
|
||||
Each instance generates a systemd oneshot service that idempotently configures
|
||||
download clients and root folders.
|
||||
'';
|
||||
};
|
||||
|
||||
config = lib.mkIf (enabledInstances != { }) {
|
||||
systemd.services = lib.mapAttrs' (
|
||||
name: inst:
|
||||
lib.nameValuePair "${inst.serviceName}-init" {
|
||||
description = "Initialize ${name} API connections";
|
||||
after = [ "${inst.serviceName}.service" ];
|
||||
requires = [ "${inst.serviceName}.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = "${mkInitScript name inst}";
|
||||
};
|
||||
}
|
||||
) enabledInstances;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user