212 lines
8.5 KiB
Nix
212 lines
8.5 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
|
||
|
||
# Send buffer watermarks control how much upload data qBittorrent pre-reads
|
||
# from disk per peer. Pushed well above libtorrent defaults (and above
|
||
# high_performance_seed's 3 MiB) to let the disk I/O thread issue larger,
|
||
# less-frequent preadv() calls. Larger watermarks give ZFS's vdev aggregator
|
||
# (4 MiB limit, 128 KiB gap) more contiguous requests to merge per sweep.
|
||
# Memory cost is roughly watermark × active-peer-count; 6 MiB × a few hundred
|
||
# peers is well within muffin's RAM budget.
|
||
SendBufferLowWatermark = 512; # 512 KiB -- trigger reads sooner to prevent upload stalls
|
||
SendBufferWatermark = 6144; # 6 MiB -- bigger pre-reads per peer than high_performance_seed's 3 MiB
|
||
SendBufferWatermarkFactor = 200; # percent -- scale watermark more aggressively with upload rate
|
||
|
||
# Maximum outstanding block requests per peer (libtorrent max_out_request_queue).
|
||
# Default 500. Tripled so libtorrent's disk I/O thread sees deeper per-peer
|
||
# request queues, giving it more contiguous blocks to coalesce into single
|
||
# preadv() calls before issuing them to the kernel.
|
||
RequestQueueSize = 1500;
|
||
};
|
||
|
||
Network = {
|
||
# traffic is routed through a vpn, we don't need
|
||
# port forwarding
|
||
PortForwardingEnabled = false;
|
||
};
|
||
|
||
Session.UseUPnP = false;
|
||
};
|
||
};
|
||
|
||
systemd.services.qbittorrent.serviceConfig = {
|
||
# 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;
|
||
|
||
# Periodically checkpoint qBittorrent's SQLite WAL (Write-Ahead Log).
|
||
# qBittorrent holds a read transaction open for its entire lifetime,
|
||
# preventing SQLite's auto-checkpoint from running. The WAL grows
|
||
# unbounded (observed: 405 MB) and must be replayed on next startup,
|
||
# causing 10+ minute "internal preparations" hangs.
|
||
# A second sqlite3 connection can checkpoint concurrently and safely.
|
||
# See: https://github.com/qbittorrent/qBittorrent/issues/20433
|
||
systemd.services.qbittorrent-wal-checkpoint = {
|
||
description = "Checkpoint qBittorrent SQLite WAL";
|
||
after = [ "qbittorrent.service" ];
|
||
requires = [ "qbittorrent.service" ];
|
||
serviceConfig = {
|
||
Type = "oneshot";
|
||
ExecStart = "${pkgs.sqlite}/bin/sqlite3 ${config.services.qbittorrent.profileDir}/qBittorrent/data/torrents.db 'PRAGMA wal_checkpoint(TRUNCATE);'";
|
||
User = config.services.qbittorrent.user;
|
||
Group = config.services.qbittorrent.group;
|
||
};
|
||
};
|
||
|
||
systemd.timers.qbittorrent-wal-checkpoint = {
|
||
description = "Periodically checkpoint qBittorrent SQLite WAL";
|
||
wantedBy = [ "timers.target" ];
|
||
timerConfig = {
|
||
OnUnitActiveSec = "4h";
|
||
OnBootSec = "30min";
|
||
RandomizedDelaySec = "10min";
|
||
};
|
||
};
|
||
|
||
users.users.${config.services.qbittorrent.user}.extraGroups = [
|
||
service_configs.media_group
|
||
];
|
||
}
|