This repository has been archived on 2026-04-18. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
server-config/services/qbittorrent.nix

171 lines
6.4 KiB
Nix

{
pkgs,
config,
service_configs,
lib,
inputs,
...
}:
let
categoriesFile = pkgs.writeText "categories.json" (
builtins.toJSON (lib.mapAttrs (_: path: { save_path = path; }) service_configs.torrent.categories)
);
in
{
imports = [
(lib.serviceMountWithZpool "qbittorrent" service_configs.zpool_hdds [
service_configs.torrents_path
])
(lib.serviceMountWithZpool "qbittorrent" service_configs.zpool_ssds [
"${config.services.qbittorrent.profileDir}/qBittorrent"
])
(lib.vpnNamespaceOpenPort config.services.qbittorrent.webuiPort "qbittorrent")
(lib.serviceFilePerms "qbittorrent" [
# 0770: group (media) needs write to delete files during upgrades —
# Radarr/Sonarr must unlink the old file before placing the new one.
# Non-recursive (z not Z): UMask=0007 ensures new files get correct perms.
# A recursive Z rule would walk millions of files on the HDD pool at every boot.
"z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.SavePath} 0770 ${config.services.qbittorrent.user} ${service_configs.media_group}"
"z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.TempPath} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}"
"Z ${config.services.qbittorrent.profileDir} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}"
])
(lib.mkCaddyReverseProxy {
subdomain = "torrent";
port = service_configs.ports.private.torrent.port;
auth = true;
vpn = true;
})
];
services.qbittorrent = {
enable = true;
webuiPort = service_configs.ports.private.torrent.port;
profileDir = "/var/lib/qBittorrent";
# Set the service group to 'media' so the systemd unit runs with media as
# the primary GID. Linux assigns new file ownership from the process's GID
# (set by systemd's Group= directive), not from /etc/passwd. Without this,
# downloads land as qbittorrent:qbittorrent (0700), blocking Radarr/Sonarr.
group = service_configs.media_group;
serverConfig.LegalNotice.Accepted = true;
serverConfig.Preferences = {
WebUI = {
AlternativeUIEnabled = true;
RootFolder = "${pkgs.vuetorrent}/share/vuetorrent";
# disable auth because we use caddy for auth
AuthSubnetWhitelist = "0.0.0.0/0";
AuthSubnetWhitelistEnabled = true;
};
Downloads = {
inherit (service_configs.torrent) SavePath TempPath;
};
};
serverConfig.BitTorrent = {
Session = {
MaxConnectionsPerTorrent = 100;
MaxUploadsPerTorrent = 50;
MaxConnections = -1;
MaxUploads = -1;
MaxActiveCheckingTorrents = 2;
# queueing
QueueingSystemEnabled = true;
MaxActiveDownloads = 15;
MaxActiveUploads = -1;
MaxActiveTorrents = -1;
IgnoreSlowTorrentsForQueueing = true;
GlobalUPSpeedLimit = 0;
GlobalDLSpeedLimit = 0;
# Alternate speed limits for when Jellyfin is streaming
AlternativeGlobalUPSpeedLimit = 500; # 500 KB/s when throttled
AlternativeGlobalDLSpeedLimit = 800; # 800 KB/s when throttled
IncludeOverheadInLimits = true;
GlobalMaxRatio = 7.0;
AddTrackersEnabled = true;
AdditionalTrackers = lib.concatStringsSep "\\n" (
lib.lists.filter (x: x != "") (
lib.strings.splitString "\n" (builtins.readFile "${inputs.trackerlist}/trackers_all.txt")
)
);
AnnounceToAllTrackers = true;
# idk why it also has to be specified here too?
inherit (config.services.qbittorrent.serverConfig.Preferences.Downloads) TempPath;
TempPathEnabled = true;
ConnectionSpeed = 200; # half-open connections/s; faster peer discovery
SaveResumeDataInterval = 300; # save resume data every 5 min (default 60s)
ResumeDataStorageType = "SQLite"; # SQLite is more efficient than legacy per-file .fastresume storage
# Automatic Torrent Management: use category save paths for new torrents
DisableAutoTMMByDefault = false;
DisableAutoTMMTriggers.CategorySavePathChanged = false;
DisableAutoTMMTriggers.DefaultSavePathChanged = false;
ChokingAlgorithm = "RateBased";
SeedChokingAlgorithm = "FastestUpload"; # unchoke peers we upload to fastest
PieceExtentAffinity = true;
SuggestMode = true;
# POSIX-compliant disk I/O: uses pread/pwrite instead of mmap.
# On ZFS, mmap forces data into BOTH ARC and Linux page cache (double-caching),
# wasting RAM. pread/pwrite goes only through ARC, maximizing its effectiveness.
DiskIOType = "Posix";
FilePoolSize = 500; # keep more files open to reduce open/close overhead
AioThreads = 24; # 6 cores * 4; better disk I/O parallelism
SendBufferLowWatermark = 512; # 512 KiB -- trigger reads sooner to prevent upload stalls
SendBufferWatermark = 3072; # 3 MiB -- matches high_performance_seed
SendBufferWatermarkFactor = 150; # percent -- matches high_performance_seed
};
Network = {
# traffic is routed through a vpn, we don't need
# port forwarding
PortForwardingEnabled = false;
};
Session.UseUPnP = false;
};
};
systemd.services.qbittorrent.serviceConfig = {
TimeoutStopSec = lib.mkForce 10;
# Default UMask=0022 creates files as 0644 (group read-only). With 0007,
# new files get 0660/0770 so the media group has read+write immediately
# instead of relying on the tmpfiles Z rule to fix permissions at restart.
UMask = lib.mkForce "0007";
};
# Pre-define qBittorrent categories with explicit save paths so every
# torrent routes to its category directory instead of the SavePath root.
systemd.tmpfiles.settings.qbittorrent-categories = {
"${config.services.qbittorrent.profileDir}/qBittorrent/config/categories.json"."L+" = {
argument = "${categoriesFile}";
user = config.services.qbittorrent.user;
group = config.services.qbittorrent.group;
mode = "1400";
};
};
# Ensure category directories exist with correct ownership before first use.
systemd.tmpfiles.rules = lib.mapAttrsToList (
_: path: "d ${path} 0770 ${config.services.qbittorrent.user} ${service_configs.media_group} -"
) service_configs.torrent.categories;
users.users.${config.services.qbittorrent.user}.extraGroups = [
service_configs.media_group
];
}