Unverified Commit e2f3c268 authored by Mistyttm's avatar Mistyttm
Browse files

nixos/tdarr: init module

parent 6f4c85b5
Loading
Loading
Loading
Loading
+63 −0
Original line number Diff line number Diff line
@@ -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"
  ],
+2 −0
Original line number Diff line number Diff line
@@ -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. -->
+1 −0
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
{
  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 −0
Original line number Diff line number Diff line
{
  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;
  };
}
Loading