Files
2026-03-19 15:57:00 +10:00

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;
};
}