Files
music-agregator/nix/qbittorrent.nix
T
Alexander 908c37f73f Add NixOS module for full agregators stack with VM test
Module manages PostgreSQL (both databases + schema init), qBittorrent
(VPN-confined via VPN-Confinement), Jackett, metadata-agregator,
musicfs, and the main orchestrator. All services are independently
enableable with auto-wired inter-service configuration.

Includes NixOS VM test validating PostgreSQL setup, schema
initialization, service startup, and directory creation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-20 12:39:04 +02:00

137 lines
3.8 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.agregators;
qbtCfg = cfg.qbittorrent;
vpnCfg = cfg.vpn;
vpnEnabled = vpnCfg.enable && qbtCfg.vpn.enable;
# Inside VPN namespace, services bind to the namespace address
namespaceAddr = "192.168.15.1";
listenAddr = if vpnEnabled then namespaceAddr else "0.0.0.0";
in
{
options.services.agregators.qbittorrent = {
enable = lib.mkEnableOption "qBittorrent torrent client";
vpn = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Confine qBittorrent to the VPN network namespace.";
};
};
webuiPort = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "qBittorrent WebUI port.";
};
peerPort = lib.mkOption {
type = lib.types.port;
default = 6881;
description = "BitTorrent peer port (opened through VPN).";
};
downloadDir = lib.mkOption {
type = lib.types.path;
default = "${cfg.mediaDir}/downloads";
defaultText = lib.literalExpression ''"''${cfg.mediaDir}/downloads"'';
description = "Directory where qBittorrent saves completed downloads.";
};
username = lib.mkOption {
type = lib.types.str;
default = "admin";
description = "qBittorrent WebUI username.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "File containing qBittorrent WebUI password.";
};
stateDir = lib.mkOption {
type = lib.types.path;
default = "${cfg.stateDir}/qbittorrent";
defaultText = lib.literalExpression ''"''${cfg.stateDir}/qbittorrent"'';
description = "qBittorrent state/config directory.";
};
package = lib.mkPackageOption pkgs "qbittorrent-nox" { };
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open the WebUI port in the firewall.";
};
};
config = lib.mkIf (cfg.enable && qbtCfg.enable) {
assertions = [
{
assertion = qbtCfg.vpn.enable -> vpnCfg.enable;
message = "services.agregators.qbittorrent.vpn.enable requires services.agregators.vpn.enable";
}
];
systemd.tmpfiles.rules = [
"d '${qbtCfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
"d '${qbtCfg.stateDir}/qBittorrent' 0750 ${cfg.user} ${cfg.group} - -"
"d '${qbtCfg.stateDir}/qBittorrent/config' 0750 ${cfg.user} ${cfg.group} - -"
"d '${qbtCfg.downloadDir}' 0775 ${cfg.user} ${cfg.group} - -"
"d '${qbtCfg.downloadDir}/.incomplete' 0775 ${cfg.user} ${cfg.group} - -"
];
systemd.services.qbittorrent = {
description = "qBittorrent-nox BitTorrent client";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = lib.concatStringsSep " " [
"${lib.getExe qbtCfg.package}"
"--webui-port=${toString qbtCfg.webuiPort}"
"--profile=${qbtCfg.stateDir}"
];
Restart = "on-failure";
RestartSec = 5;
};
};
# VPN confinement
systemd.services.qbittorrent.vpnConfinement = lib.mkIf vpnEnabled {
enable = true;
vpnNamespace = vpnCfg.namespace;
};
vpnNamespaces.${vpnCfg.namespace} = lib.mkIf vpnEnabled {
portMappings = [
{
from = qbtCfg.webuiPort;
to = qbtCfg.webuiPort;
}
];
openVPNPorts = [
{
port = qbtCfg.peerPort;
protocol = "both";
}
];
};
networking.firewall.allowedTCPPorts = lib.mkIf qbtCfg.openFirewall [ qbtCfg.webuiPort ];
};
}