From e2f3c268656aec4d2687e0a64e80fd1644cf05b6 Mon Sep 17 00:00:00 2001 From: Mistyttm Date: Wed, 21 Jan 2026 09:51:15 +1000 Subject: [PATCH] nixos/tdarr: init module --- nixos/doc/manual/redirects.json | 63 ++++ .../manual/release-notes/rl-2605.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/tdarr/default.nix | 65 ++++ nixos/modules/services/misc/tdarr/node.nix | 244 +++++++++++++++ nixos/modules/services/misc/tdarr/server.nix | 151 ++++++++++ nixos/modules/services/misc/tdarr/tdarr.md | 280 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/tdarr.nix | 124 ++++++++ 9 files changed, 931 insertions(+) create mode 100644 nixos/modules/services/misc/tdarr/default.nix create mode 100644 nixos/modules/services/misc/tdarr/node.nix create mode 100644 nixos/modules/services/misc/tdarr/server.nix create mode 100644 nixos/modules/services/misc/tdarr/tdarr.md create mode 100644 nixos/tests/tdarr.nix diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index be546b0ce617..f3bdc1ff1952 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -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" ], diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 957f80a7d12b..787d1b1815fb 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -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} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 9201540632cb..9b5d52c396d3 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -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 diff --git a/nixos/modules/services/misc/tdarr/default.nix b/nixos/modules/services/misc/tdarr/default.nix new file mode 100644 index 000000000000..63bef40e7d94 --- /dev/null +++ b/nixos/modules/services/misc/tdarr/default.nix @@ -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; + }; +} diff --git a/nixos/modules/services/misc/tdarr/node.nix b/nixos/modules/services/misc/tdarr/node.nix new file mode 100644 index 000000000000..a54203edf786 --- /dev/null +++ b/nixos/modules/services/misc/tdarr/node.nix @@ -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; + }; +} diff --git a/nixos/modules/services/misc/tdarr/server.nix b/nixos/modules/services/misc/tdarr/server.nix new file mode 100644 index 000000000000..6d28fc5b9929 --- /dev/null +++ b/nixos/modules/services/misc/tdarr/server.nix @@ -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 + ]; + }; +} diff --git a/nixos/modules/services/misc/tdarr/tdarr.md b/nixos/modules/services/misc/tdarr/tdarr.md new file mode 100644 index 000000000000..0ba94e800003 --- /dev/null +++ b/nixos/modules/services/misc/tdarr/tdarr.md @@ -0,0 +1,280 @@ +# Tdarr {#module-services-tdarr} + +*Source:* {file}`modules/services/misc/tdarr` + +*Upstream documentation:* + +[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). +::: diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index cd7d3b61325c..3d411272a2c5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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; diff --git a/nixos/tests/tdarr.nix b/nixos/tests/tdarr.nix new file mode 100644 index 000000000000..9dad9f81e757 --- /dev/null +++ b/nixos/tests/tdarr.nix @@ -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 "" in html or "