mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-06-05 21:03:40 +00:00
nixos/tdarr: init module
This commit is contained in:
@@ -76,6 +76,69 @@
|
||||
"module-services-tandoor-recipes-migrating-media-option-disallow-access": [
|
||||
"index.html#module-services-tandoor-recipes-migrating-media-option-disallow-access"
|
||||
],
|
||||
"module-services-tdarr": [
|
||||
"index.html#module-services-tdarr"
|
||||
],
|
||||
"module-services-tdarr-advanced": [
|
||||
"index.html#module-services-tdarr-advanced"
|
||||
],
|
||||
"module-services-tdarr-advanced-datadir": [
|
||||
"index.html#module-services-tdarr-advanced-datadir"
|
||||
],
|
||||
"module-services-tdarr-advanced-node-datadir": [
|
||||
"index.html#module-services-tdarr-advanced-node-datadir"
|
||||
],
|
||||
"module-services-tdarr-advanced-plugins": [
|
||||
"index.html#module-services-tdarr-advanced-plugins"
|
||||
],
|
||||
"module-services-tdarr-authentication": [
|
||||
"index.html#module-services-tdarr-authentication"
|
||||
],
|
||||
"module-services-tdarr-basic-usage": [
|
||||
"index.html#module-services-tdarr-basic-usage"
|
||||
],
|
||||
"module-services-tdarr-distributed": [
|
||||
"index.html#module-services-tdarr-distributed"
|
||||
],
|
||||
"module-services-tdarr-distributed-nodes": [
|
||||
"index.html#module-services-tdarr-distributed-nodes"
|
||||
],
|
||||
"module-services-tdarr-distributed-server": [
|
||||
"index.html#module-services-tdarr-distributed-server"
|
||||
],
|
||||
"module-services-tdarr-networking": [
|
||||
"index.html#module-services-tdarr-networking"
|
||||
],
|
||||
"module-services-tdarr-networking-firewall": [
|
||||
"index.html#module-services-tdarr-networking-firewall"
|
||||
],
|
||||
"module-services-tdarr-networking-ipv6": [
|
||||
"index.html#module-services-tdarr-networking-ipv6"
|
||||
],
|
||||
"module-services-tdarr-networking-ports": [
|
||||
"index.html#module-services-tdarr-networking-ports"
|
||||
],
|
||||
"module-services-tdarr-nodes": [
|
||||
"index.html#module-services-tdarr-nodes"
|
||||
],
|
||||
"module-services-tdarr-nodes-multiple": [
|
||||
"index.html#module-services-tdarr-nodes-multiple"
|
||||
],
|
||||
"module-services-tdarr-nodes-only": [
|
||||
"index.html#module-services-tdarr-nodes-only"
|
||||
],
|
||||
"module-services-tdarr-nodes-path-translators": [
|
||||
"index.html#module-services-tdarr-nodes-path-translators"
|
||||
],
|
||||
"module-services-tdarr-nodes-types": [
|
||||
"index.html#module-services-tdarr-nodes-types"
|
||||
],
|
||||
"module-services-tdarr-nodes-workers": [
|
||||
"index.html#module-services-tdarr-nodes-workers"
|
||||
],
|
||||
"module-services-tdarr-server-only": [
|
||||
"index.html#module-services-tdarr-server-only"
|
||||
],
|
||||
"module-virtualisation-xen": [
|
||||
"index.html#module-virtualisation-xen"
|
||||
],
|
||||
|
||||
@@ -80,6 +80,8 @@
|
||||
|
||||
- [tabbyAPI](https://github.com/theroyallab/tabbyAPI), the official OpenAI compatible API server for Exllama. Available as [services.tabbyapi](#opt-services.tabbyapi.enable).
|
||||
|
||||
- [Tdarr](https://tdarr.io), Audio/Video Library Analytics & Transcode/Remux Automation. Available as [services.tdarr](#opt-services.tdarr.enable)
|
||||
|
||||
## Backward Incompatibilities {#sec-release-26.05-incompatibilities}
|
||||
|
||||
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
|
||||
|
||||
@@ -976,6 +976,7 @@
|
||||
./services/misc/taskchampion-sync-server.nix
|
||||
./services/misc/taskserver
|
||||
./services/misc/tautulli.nix
|
||||
./services/misc/tdarr
|
||||
./services/misc/tee-supplicant
|
||||
./services/misc/tiddlywiki.nix
|
||||
./services/misc/tp-auto-kbbl.nix
|
||||
|
||||
65
nixos/modules/services/misc/tdarr/default.nix
Normal file
65
nixos/modules/services/misc/tdarr/default.nix
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.services.tdarr;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./server.nix
|
||||
./node.nix
|
||||
];
|
||||
|
||||
options.services.tdarr = {
|
||||
enable = lib.mkEnableOption "Tdarr distributed transcoding system" // {
|
||||
description = ''
|
||||
Whether to enable Tdarr. This is a convenience option that enables both
|
||||
the server and all configured nodes. For more granular control, use
|
||||
{option}`services.tdarr.server.enable` and configure nodes individually.
|
||||
'';
|
||||
};
|
||||
|
||||
package = lib.mkPackageOption pkgs "tdarr" { };
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = "/var/lib/tdarr";
|
||||
description = "Base directory for Tdarr data.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "tdarr";
|
||||
description = "User account under which Tdarr runs.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "tdarr";
|
||||
description = "Group under which Tdarr runs.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf (cfg.enable || cfg.server.enable || cfg.nodes != { }) {
|
||||
users.users.tdarr = lib.mkIf (cfg.user == "tdarr") {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = cfg.dataDir;
|
||||
createHome = true;
|
||||
};
|
||||
users.groups.tdarr = lib.mkIf (cfg.group == "tdarr") { };
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -"
|
||||
];
|
||||
};
|
||||
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ mistyttm ];
|
||||
doc = ./tdarr.md;
|
||||
};
|
||||
}
|
||||
244
nixos/modules/services/misc/tdarr/node.nix
Normal file
244
nixos/modules/services/misc/tdarr/node.nix
Normal file
@@ -0,0 +1,244 @@
|
||||
{
|
||||
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;
|
||||
};
|
||||
}
|
||||
151
nixos/modules/services/misc/tdarr/server.nix
Normal file
151
nixos/modules/services/misc/tdarr/server.nix
Normal file
@@ -0,0 +1,151 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.services.tdarr;
|
||||
serverDataDir = "${cfg.dataDir}/server";
|
||||
serverEnabled = cfg.enable || cfg.server.enable;
|
||||
in
|
||||
{
|
||||
options.services.tdarr.server = {
|
||||
enable = lib.mkEnableOption "Tdarr server";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = cfg.package.server;
|
||||
defaultText = lib.literalExpression "config.services.tdarr.package.server";
|
||||
description = "Package to use for the Tdarr server.";
|
||||
};
|
||||
|
||||
serverPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8266;
|
||||
description = "Port for server API communication.";
|
||||
};
|
||||
|
||||
webUIPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8265;
|
||||
description = "Port for the Tdarr web UI.";
|
||||
};
|
||||
|
||||
serverIP = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "0.0.0.0";
|
||||
description = "IP address the server binds to.";
|
||||
};
|
||||
|
||||
serverBindIP = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Whether to bind to the specific IP in {option}`services.tdarr.server.serverIP`.";
|
||||
};
|
||||
|
||||
serverDualStack = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable dual-stack (IPv4/IPv6) networking.
|
||||
|
||||
When enabled, the server binds to `::` if IPv6 is available, accepting both
|
||||
IPv4 and IPv6 connections. Useful in Kubernetes and other modern networking setups.
|
||||
'';
|
||||
};
|
||||
|
||||
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.";
|
||||
example = "0 2 * * *";
|
||||
};
|
||||
|
||||
auth.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable authentication for the Tdarr web UI and API.";
|
||||
};
|
||||
|
||||
environmentFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
File containing environment variable overrides for the server,
|
||||
in the format accepted by systemd's `EnvironmentFile`.
|
||||
|
||||
Useful for setting secrets such as `authSecretKey` or `seededApiKey`
|
||||
without exposing them in the Nix store.
|
||||
|
||||
Example file contents:
|
||||
```
|
||||
authSecretKey=your-secret-key
|
||||
seededApiKey=tapi_your_api_key_here
|
||||
```
|
||||
'';
|
||||
example = "/run/secrets/tdarr-server-env";
|
||||
};
|
||||
|
||||
openFirewall = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Whether to open the firewall for the Tdarr server web UI and API ports.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf serverEnabled {
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${serverDataDir} 0750 ${cfg.user} ${cfg.group} -"
|
||||
"d ${serverDataDir}/configs 0750 ${cfg.user} ${cfg.group} -"
|
||||
];
|
||||
|
||||
systemd.services.tdarr-server = {
|
||||
description = "Tdarr Server";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment = {
|
||||
serverPort = toString cfg.server.serverPort;
|
||||
webUIPort = toString cfg.server.webUIPort;
|
||||
serverIP = cfg.server.serverIP;
|
||||
serverBindIP = lib.boolToString cfg.server.serverBindIP;
|
||||
serverDualStack = lib.boolToString cfg.server.serverDualStack;
|
||||
openBrowser = "false";
|
||||
auth = lib.boolToString cfg.server.auth.enable;
|
||||
maxLogSizeMB = toString cfg.server.maxLogSizeMB;
|
||||
cronPluginUpdate = cfg.server.cronPluginUpdate;
|
||||
rootDataPath = serverDataDir;
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = lib.getExe cfg.server.package;
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
WorkingDirectory = serverDataDir;
|
||||
|
||||
# Hardening
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ReadWritePaths = [ cfg.dataDir ];
|
||||
}
|
||||
// lib.optionalAttrs (cfg.server.environmentFile != null) {
|
||||
EnvironmentFile = cfg.server.environmentFile;
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = lib.mkIf cfg.server.openFirewall [
|
||||
cfg.server.serverPort
|
||||
cfg.server.webUIPort
|
||||
];
|
||||
};
|
||||
}
|
||||
280
nixos/modules/services/misc/tdarr/tdarr.md
Normal file
280
nixos/modules/services/misc/tdarr/tdarr.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Tdarr {#module-services-tdarr}
|
||||
|
||||
*Source:* {file}`modules/services/misc/tdarr`
|
||||
|
||||
*Upstream documentation:* <https://docs.tdarr.io/\>
|
||||
|
||||
[Tdarr](https://tdarr.io) is a distributed transcoding system for automating media library transcoding operations using FFmpeg and HandBrake. It provides a web interface for managing transcoding nodes and configuring media processing pipelines.
|
||||
|
||||
## Basic Usage {#module-services-tdarr-basic-usage}
|
||||
|
||||
A minimal Tdarr setup with a server and one local node:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr = {
|
||||
enable = true;
|
||||
nodes.main = { };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This creates a Tdarr server accessible at `http://localhost:8265` (web UI) with one processing node. The service runs as the `tdarr` user with data stored in `/var/lib/tdarr`.
|
||||
|
||||
::: {.note}
|
||||
The `services.tdarr.enable` option is a convenience that enables both the server and all configured nodes. For finer control, use `services.tdarr.server.enable` and configure nodes independently.
|
||||
:::
|
||||
|
||||
### Server Only {#module-services-tdarr-server-only}
|
||||
|
||||
To run only the Tdarr server without local nodes:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.server.enable = true;
|
||||
}
|
||||
```
|
||||
|
||||
### Nodes Only {#module-services-tdarr-nodes-only}
|
||||
|
||||
To run node(s) connecting to a remote server:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.nodes.worker1 = {
|
||||
serverURL = "http://192.168.1.100:8266";
|
||||
environmentFile = "/run/secrets/tdarr-node-env";
|
||||
# /run/secrets/tdarr-node-env contains:
|
||||
# apiKey=tapi_your_api_key_here
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication {#module-services-tdarr-authentication}
|
||||
|
||||
Authentication should be enabled for any installation accessible beyond localhost. Secrets are passed via environment files to avoid leaking them into the Nix store:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr = {
|
||||
enable = true;
|
||||
server = {
|
||||
auth.enable = true;
|
||||
environmentFile = "/run/secrets/tdarr-server-env";
|
||||
# /run/secrets/tdarr-server-env contains:
|
||||
# authSecretKey=your-secret-key
|
||||
# seededApiKey=tapi_your_api_key_here
|
||||
};
|
||||
nodes.main = {
|
||||
environmentFile = "/run/secrets/tdarr-node-env";
|
||||
# /run/secrets/tdarr-node-env contains:
|
||||
# apiKey=tapi_your_api_key_here
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
::: {.warning}
|
||||
When using unmapped nodes, files in Tdarr's library source and cache folders become accessible through the network API. Authentication is strongly recommended in this configuration.
|
||||
:::
|
||||
|
||||
## Node Configuration {#module-services-tdarr-nodes}
|
||||
|
||||
### Multiple Nodes {#module-services-tdarr-nodes-multiple}
|
||||
|
||||
You can run multiple nodes on the same machine with different configurations:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr = {
|
||||
enable = true;
|
||||
nodes = {
|
||||
cpu-node = {
|
||||
workers = {
|
||||
transcodeCPU = 4;
|
||||
healthcheckCPU = 2;
|
||||
};
|
||||
};
|
||||
gpu-node = {
|
||||
workers = {
|
||||
transcodeGPU = 2;
|
||||
transcodeCPU = 1;
|
||||
healthcheckGPU = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Worker Configuration {#module-services-tdarr-nodes-workers}
|
||||
|
||||
Workers determine how many parallel transcoding and healthcheck operations a node can perform:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.nodes.main = {
|
||||
workers = {
|
||||
transcodeCPU = 4; # default: 2
|
||||
transcodeGPU = 1; # default: 0
|
||||
healthcheckCPU = 2; # default: 1
|
||||
healthcheckGPU = 0; # default: 0
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
::: {.note}
|
||||
GPU workers require appropriate hardware and drivers. Worker counts can also be adjusted at runtime through the Tdarr web UI.
|
||||
:::
|
||||
|
||||
### Node Types {#module-services-tdarr-nodes-types}
|
||||
|
||||
Tdarr supports two node types:
|
||||
|
||||
- **Mapped nodes** (default): Access files directly from the library paths configured in the Tdarr web interface.
|
||||
- **Unmapped nodes**: Receive files over the network, useful for nodes without direct storage access.
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.nodes = {
|
||||
local.type = "mapped";
|
||||
remote = {
|
||||
type = "unmapped";
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Path Translators {#module-services-tdarr-nodes-path-translators}
|
||||
|
||||
Path translators enable cross-mount-point file access by mapping server paths to node paths:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.nodes.remote-node = {
|
||||
pathTranslators = [
|
||||
{
|
||||
server = "/media/videos";
|
||||
node = "/mnt/nfs/videos";
|
||||
}
|
||||
{
|
||||
server = "/media/music";
|
||||
node = "/mnt/nfs/music";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Networking {#module-services-tdarr-networking}
|
||||
|
||||
### Firewall Configuration {#module-services-tdarr-networking-firewall}
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.server = {
|
||||
enable = true;
|
||||
openFirewall = true; # Opens ports 8265 (web UI) and 8266 (server API)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Ports {#module-services-tdarr-networking-ports}
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.server = {
|
||||
enable = true;
|
||||
serverPort = 9266; # default: 8266
|
||||
webUIPort = 9265; # default: 8265
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### IPv6 Support {#module-services-tdarr-networking-ipv6}
|
||||
|
||||
Enable dual-stack networking for IPv4 and IPv6 support:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.server = {
|
||||
enable = true;
|
||||
serverDualStack = true;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Configuration {#module-services-tdarr-advanced}
|
||||
|
||||
### Plugin Updates {#module-services-tdarr-advanced-plugins}
|
||||
|
||||
Configure automatic plugin updates using cron expressions:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.server = {
|
||||
enable = true;
|
||||
cronPluginUpdate = "0 2 * * *"; # Daily at 2 AM
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Data Directory {#module-services-tdarr-advanced-datadir}
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr = {
|
||||
enable = true;
|
||||
dataDir = "/mnt/storage/tdarr";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Per-Node Data Directories {#module-services-tdarr-advanced-node-datadir}
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.nodes = {
|
||||
ssd-node.dataDir = "/mnt/ssd/tdarr-node";
|
||||
hdd-node.dataDir = "/mnt/hdd/tdarr-node";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Distributed Setup {#module-services-tdarr-distributed}
|
||||
|
||||
Tdarr's distributed architecture allows running nodes on separate machines from the server.
|
||||
|
||||
### Server Machine {#module-services-tdarr-distributed-server}
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.server = {
|
||||
enable = true;
|
||||
serverIP = "0.0.0.0";
|
||||
openFirewall = true;
|
||||
auth.enable = true;
|
||||
environmentFile = "/run/secrets/tdarr-server-env";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Worker Machines {#module-services-tdarr-distributed-nodes}
|
||||
|
||||
```nix
|
||||
{
|
||||
services.tdarr.nodes.remote-worker = {
|
||||
serverURL = "http://192.168.1.100:8266";
|
||||
environmentFile = "/run/secrets/tdarr-node-env";
|
||||
workers = {
|
||||
transcodeCPU = 4;
|
||||
healthcheckCPU = 2;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
::: {.note}
|
||||
Ensure the server's firewall allows incoming connections on the configured ports. Both server and nodes must have access to the same media and transcode cache paths (for mapped nodes).
|
||||
:::
|
||||
@@ -1638,6 +1638,7 @@ in
|
||||
taskchampion-sync-server = runTest ./taskchampion-sync-server.nix;
|
||||
taskserver = runTest ./taskserver.nix;
|
||||
tayga = runTest ./tayga.nix;
|
||||
tdarr = runTest ./tdarr.nix;
|
||||
technitium-dns-server = runTest ./technitium-dns-server.nix;
|
||||
teeworlds = runTest ./teeworlds.nix;
|
||||
telegraf = runTest ./telegraf.nix;
|
||||
|
||||
124
nixos/tests/tdarr.nix
Normal file
124
nixos/tests/tdarr.nix
Normal file
@@ -0,0 +1,124 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "tdarr";
|
||||
meta = with lib.maintainers; {
|
||||
maintainers = [ mistyttm ];
|
||||
};
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
services.tdarr = {
|
||||
enable = true;
|
||||
server = {
|
||||
serverPort = 9266;
|
||||
webUIPort = 9265;
|
||||
};
|
||||
nodes = {
|
||||
main = {
|
||||
type = "mapped";
|
||||
priority = -1;
|
||||
pollInterval = 2000;
|
||||
startPaused = false;
|
||||
maxLogSizeMB = 10;
|
||||
workers = {
|
||||
transcodeCPU = 1;
|
||||
healthcheckCPU = 1;
|
||||
};
|
||||
};
|
||||
secondary = {
|
||||
enable = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
|
||||
machine.wait_for_unit("tdarr-server.service")
|
||||
machine.wait_for_unit("tdarr-node-main.service")
|
||||
|
||||
with subtest("disabled node should not have a service"):
|
||||
machine.fail("systemctl is-enabled tdarr-node-secondary.service")
|
||||
|
||||
with subtest("data directories created with correct ownership"):
|
||||
machine.succeed("test -d /var/lib/tdarr")
|
||||
machine.succeed("stat -c '%U:%G' /var/lib/tdarr | grep -q 'tdarr:tdarr'")
|
||||
|
||||
with subtest("disabled node directory should not exist"):
|
||||
machine.fail("test -d /var/lib/tdarr/nodes/secondary")
|
||||
|
||||
with subtest("server environment variables are set correctly"):
|
||||
env = machine.succeed(
|
||||
"systemctl show tdarr-server.service --property=Environment"
|
||||
)
|
||||
assert "serverPort=9266" in env, f"serverPort not found in: {env}"
|
||||
assert "webUIPort=9265" in env, f"webUIPort not found in: {env}"
|
||||
assert "serverIP=0.0.0.0" in env, f"serverIP not found in: {env}"
|
||||
assert "serverBindIP=false" in env, f"serverBindIP not found in: {env}"
|
||||
assert "serverDualStack=false" in env, f"serverDualStack not found in: {env}"
|
||||
assert "openBrowser=false" in env, f"openBrowser not found in: {env}"
|
||||
assert "auth=false" in env, f"auth not found in: {env}"
|
||||
assert "maxLogSizeMB=10" in env, f"maxLogSizeMB not found in: {env}"
|
||||
assert "cronPluginUpdate=" in env, f"cronPluginUpdate not found in: {env}"
|
||||
|
||||
with subtest("node environment variables are set correctly"):
|
||||
env = machine.succeed(
|
||||
"systemctl show tdarr-node-main.service --property=Environment"
|
||||
)
|
||||
assert "serverURL=http://127.0.0.1:9266" in env, f"serverURL not found in: {env}"
|
||||
assert "nodeType=mapped" in env, f"nodeType not found in: {env}"
|
||||
assert "priority=-1" in env, f"priority not found in: {env}"
|
||||
assert "pollInterval=2000" in env, f"pollInterval not found in: {env}"
|
||||
assert "startPaused=false" in env, f"startPaused not found in: {env}"
|
||||
assert "maxLogSizeMB=10" in env, f"maxLogSizeMB not found in: {env}"
|
||||
assert "transcodecpuWorkers=1" in env, f"transcodecpuWorkers not found in: {env}"
|
||||
assert "healthcheckcpuWorkers=1" in env, f"healthcheckcpuWorkers not found in: {env}"
|
||||
assert "transcodegpuWorkers=0" in env, f"transcodegpuWorkers not found in: {env}"
|
||||
assert "healthcheckgpuWorkers=0" in env, f"healthcheckgpuWorkers not found in: {env}"
|
||||
|
||||
with subtest("custom ports are listening"):
|
||||
machine.wait_for_open_port(9265)
|
||||
machine.wait_for_open_port(9266)
|
||||
|
||||
with subtest("server reports healthy status"):
|
||||
status = json.loads(machine.succeed("curl -sf http://localhost:9266/api/v2/status"))
|
||||
assert "version" in status, f"version missing from status: {status}"
|
||||
assert status.get("uptime", -1) >= 0, f"unexpected uptime in status: {status}"
|
||||
|
||||
with subtest("web UI serves HTML"):
|
||||
html = machine.succeed("curl --fail http://localhost:9265/")
|
||||
assert "<!DOCTYPE html>" in html or "<html" in html, f"web UI did not return HTML: {html[:200]}"
|
||||
|
||||
with subtest("node registers with server and reports correct config"):
|
||||
machine.wait_until_succeeds(
|
||||
"curl -sf http://localhost:9266/api/v2/get-nodes | grep -q 'main'"
|
||||
)
|
||||
response = machine.succeed("curl -sf http://localhost:9266/api/v2/get-nodes")
|
||||
nodes = json.loads(response)
|
||||
node = next((v for v in nodes.values() if "main" in v.get("nodeName", "")), None)
|
||||
assert node is not None, f"node 'main' not found in: {nodes}"
|
||||
node_config = node["config"]
|
||||
node_worker_limits = node["workerLimits"]
|
||||
|
||||
# Worker limits
|
||||
assert node_worker_limits.get("transcodecpu") == 1, f"unexpected transcodecpu worker count: {node}"
|
||||
assert node_worker_limits.get("healthcheckcpu") == 1, f"unexpected healthcheckcpu worker count: {node}"
|
||||
assert node_worker_limits.get("transcodegpu") == 0, f"unexpected transcodegpu worker count: {node}"
|
||||
assert node_worker_limits.get("healthcheckgpu") == 0, f"unexpected healthcheckgpu worker count: {node}"
|
||||
|
||||
# Node config as registered with the server
|
||||
assert "pathTranslators" in node_config, f"pathTranslators missing from node config: {node_config}"
|
||||
assert node_config.get("nodeType") == "mapped", f"unexpected nodeType: {node_config}"
|
||||
assert node_config.get("priority") == -1, f"unexpected priority: {node_config}"
|
||||
assert node_config.get("pollInterval") == 2000, f"unexpected pollInterval: {node_config}"
|
||||
assert node_config.get("startPaused") == False, f"unexpected startPaused: {node_config}"
|
||||
assert node_config.get("maxLogSizeMB") == "10", f"unexpected maxLogSizeMB: {node_config}"
|
||||
assert node_config.get("cronPluginUpdate") == "", f"unexpected cronPluginUpdate: {node_config}"
|
||||
|
||||
# Top-level node state
|
||||
assert node.get("nodePaused") == False, f"unexpected nodePaused: {node}"
|
||||
assert node.get("nodeTags") == "mapped", f"unexpected nodeTags: {node}"
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user