245 lines
9.0 KiB
Nix
245 lines
9.0 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
|
|
let
|
|
cfg = config.services.tdarr;
|
|
enabledNodes = lib.filterAttrs (_: nodeCfg: nodeCfg.enable) cfg.nodes;
|
|
nodesEnabled = cfg.enable || (enabledNodes != { });
|
|
serverEnabled = cfg.enable || cfg.server.enable;
|
|
nodeConfigFiles = lib.mapAttrs (
|
|
nodeId: nodeCfg:
|
|
pkgs.writeText "Tdarr_Node_Config_${nodeId}.json" (
|
|
builtins.toJSON { pathTranslators = nodeCfg.pathTranslators; }
|
|
)
|
|
) enabledNodes;
|
|
in
|
|
{
|
|
options.services.tdarr.nodes = lib.mkOption {
|
|
default = { };
|
|
description = "Attribute set of Tdarr processing nodes to run on this machine.";
|
|
type = lib.types.attrsOf (
|
|
lib.types.submodule (
|
|
{ name, ... }:
|
|
{
|
|
options = {
|
|
enable = lib.mkEnableOption "this Tdarr node" // {
|
|
default = true;
|
|
};
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = cfg.package.node;
|
|
defaultText = lib.literalExpression "config.services.tdarr.package.node";
|
|
description = "Package to use for this Tdarr node.";
|
|
};
|
|
|
|
name = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "${config.networking.hostName}-${name}";
|
|
defaultText = lib.literalExpression ''"''${config.networking.hostName}-''${name}"'';
|
|
description = "Display name for this node in the Tdarr web UI.";
|
|
};
|
|
|
|
dataDir = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "${cfg.dataDir}/nodes/${name}";
|
|
defaultText = lib.literalExpression ''"''${config.services.tdarr.dataDir}/nodes/''${name}"'';
|
|
description = "Data directory for this node.";
|
|
};
|
|
|
|
serverURL = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "http://127.0.0.1:${toString cfg.server.serverPort}";
|
|
defaultText = lib.literalExpression ''"http://127.0.0.1:''${toString config.services.tdarr.server.serverPort}"'';
|
|
description = ''
|
|
Full URL of the Tdarr server this node connects to.
|
|
|
|
This is the recommended way to specify the server location.
|
|
When running a local server, the default value is correct.
|
|
'';
|
|
};
|
|
|
|
type = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"mapped"
|
|
"unmapped"
|
|
];
|
|
default = "mapped";
|
|
description = ''
|
|
Node type.
|
|
|
|
- `mapped`: Node accesses files directly from the library paths.
|
|
- `unmapped`: Node receives files over the network API.
|
|
'';
|
|
};
|
|
|
|
priority = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = -1;
|
|
description = ''
|
|
Node priority for job assignment.
|
|
|
|
`-1` means no priority. `0` is the highest priority, `1` is next, and so on.
|
|
'';
|
|
};
|
|
|
|
pollInterval = lib.mkOption {
|
|
type = lib.types.ints.unsigned;
|
|
default = 2000;
|
|
description = "How often the node checks the server for work, in milliseconds.";
|
|
};
|
|
|
|
startPaused = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Whether the node starts in a paused state.";
|
|
};
|
|
|
|
maxLogSizeMB = lib.mkOption {
|
|
type = lib.types.ints.unsigned;
|
|
default = 10;
|
|
description = "Maximum log file size in megabytes.";
|
|
};
|
|
|
|
cronPluginUpdate = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "Cron expression for automatic plugin updates. Empty string disables.";
|
|
};
|
|
|
|
pathTranslators = lib.mkOption {
|
|
type = lib.types.listOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
server = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Server-side path for path translation.";
|
|
};
|
|
node = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Node-side path for path translation.";
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = [ ];
|
|
description = ''
|
|
Path translations between server and node for cross-platform or
|
|
cross-mount-point file access.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
[
|
|
{ server = "/media"; node = "/mnt/media"; }
|
|
{ server = "/cache"; node = "/mnt/cache"; }
|
|
]
|
|
'';
|
|
};
|
|
|
|
workers = {
|
|
transcodeGPU = lib.mkOption {
|
|
type = lib.types.ints.unsigned;
|
|
default = 0;
|
|
description = "Number of GPU transcode workers. Can be overridden in the web UI.";
|
|
};
|
|
transcodeCPU = lib.mkOption {
|
|
type = lib.types.ints.unsigned;
|
|
default = 2;
|
|
description = "Number of CPU transcode workers. Can be overridden in the web UI.";
|
|
};
|
|
healthcheckGPU = lib.mkOption {
|
|
type = lib.types.ints.unsigned;
|
|
default = 0;
|
|
description = "Number of GPU healthcheck workers. Can be overridden in the web UI.";
|
|
};
|
|
healthcheckCPU = lib.mkOption {
|
|
type = lib.types.ints.unsigned;
|
|
default = 1;
|
|
description = "Number of CPU healthcheck workers. Can be overridden in the web UI.";
|
|
};
|
|
};
|
|
|
|
environmentFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = ''
|
|
File containing environment variable overrides for this node,
|
|
in the format accepted by systemd's `EnvironmentFile`.
|
|
|
|
Useful for passing secrets like `apiKey` without putting them
|
|
in the Nix store.
|
|
'';
|
|
example = "/run/secrets/tdarr-node-env";
|
|
};
|
|
};
|
|
}
|
|
)
|
|
);
|
|
};
|
|
|
|
config = lib.mkIf nodesEnabled {
|
|
systemd.tmpfiles.rules = lib.concatMap (nodeId: [
|
|
"d ${cfg.dataDir}/nodes/${nodeId} 0750 ${cfg.user} ${cfg.group} -"
|
|
"d ${cfg.dataDir}/nodes/${nodeId}/configs 0750 ${cfg.user} ${cfg.group} -"
|
|
"d ${cfg.dataDir}/nodes/${nodeId}/logs 0750 ${cfg.user} ${cfg.group} -"
|
|
"L+ ${cfg.dataDir}/nodes/${nodeId}/configs/Tdarr_Node_Config.json - - - - ${nodeConfigFiles.${nodeId}}"
|
|
]) (builtins.attrNames enabledNodes);
|
|
|
|
systemd.services = lib.mapAttrs' (
|
|
nodeId: nodeCfg:
|
|
lib.nameValuePair "tdarr-node-${nodeId}" {
|
|
description = "Tdarr Node - ${nodeCfg.name}";
|
|
after = [ "network.target" ] ++ lib.optionals serverEnabled [ "tdarr-server.service" ];
|
|
wants = lib.optionals serverEnabled [ "tdarr-server.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
environment = {
|
|
nodeName = nodeCfg.name;
|
|
serverURL = nodeCfg.serverURL;
|
|
nodeType = nodeCfg.type;
|
|
priority = toString nodeCfg.priority;
|
|
cronPluginUpdate = nodeCfg.cronPluginUpdate;
|
|
maxLogSizeMB = toString nodeCfg.maxLogSizeMB;
|
|
pollInterval = toString nodeCfg.pollInterval;
|
|
startPaused = lib.boolToString nodeCfg.startPaused;
|
|
transcodegpuWorkers = toString nodeCfg.workers.transcodeGPU;
|
|
transcodecpuWorkers = toString nodeCfg.workers.transcodeCPU;
|
|
healthcheckgpuWorkers = toString nodeCfg.workers.healthcheckGPU;
|
|
healthcheckcpuWorkers = toString nodeCfg.workers.healthcheckCPU;
|
|
rootDataPath = toString nodeCfg.dataDir;
|
|
};
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
ExecStart = lib.getExe nodeCfg.package;
|
|
Restart = "on-failure";
|
|
RestartSec = 5;
|
|
WorkingDirectory = toString nodeCfg.dataDir;
|
|
|
|
# Hardening
|
|
NoNewPrivileges = true;
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
StateDirectory = lib.mkIf (lib.hasPrefix "/var/lib/" (toString nodeCfg.dataDir)) (
|
|
let
|
|
rel = lib.removePrefix "/var/lib/" (toString nodeCfg.dataDir);
|
|
in
|
|
"${rel} ${rel}/configs ${rel}/logs"
|
|
);
|
|
StateDirectoryMode = lib.mkIf (lib.hasPrefix "/var/lib/" (toString nodeCfg.dataDir)) "0750";
|
|
ReadWritePaths = lib.optionals (!lib.hasPrefix "/var/lib/" (toString nodeCfg.dataDir)) [
|
|
(toString nodeCfg.dataDir)
|
|
];
|
|
}
|
|
// lib.optionalAttrs (nodeCfg.environmentFile != null) {
|
|
EnvironmentFile = nodeCfg.environmentFile;
|
|
};
|
|
}
|
|
) enabledNodes;
|
|
};
|
|
}
|