{ pkgs, config, service_configs, lib, inputs, ... }: { 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. "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}" ]) ]; services.qbittorrent = { enable = true; webuiPort = service_configs.ports.torrent; 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 = 15; MaxConnections = -1; MaxUploads = -1; MaxActiveCheckingTorrents = 2; # reduce disk pressure from concurrent hash checks # queueing QueueingSystemEnabled = true; MaxActiveDownloads = 15; MaxActiveUploads = -1; MaxActiveTorrents = -1; IgnoreSlowTorrentsForQueueing = true; GlobalUPSpeedLimit = 0; GlobalDLSpeedLimit = 10000; # 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 = 100; 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"; PieceExtentAffinity = true; SuggestMode = true; CoalesceReadWrite = true; # max_queued_disk_bytes: the max bytes waiting in the disk I/O queue. # When this limit is reached, peer connections stop reading from their # sockets until the disk thread catches up -- causing the spike-then-zero # pattern. Default is 1MB; high_performance_seed() uses 7MB. # 64MB is above the preset but justified for slow raidz1 HDD random writes # where ZFS txg commits cause periodic I/O stalls. DiskQueueSize = 67108864; # 64MB # === Network buffer tuning (from libtorrent high_performance_seed preset) === # "always stuff at least 1 MiB down each peer pipe, to quickly ramp up send rates" SendBufferLowWatermark = 1024; # 1MB (KiB) -- matches high_performance_seed # "of 500 ms, and a send rate of 4 MB/s, the upper limit should be 2 MB" SendBufferWatermark = 3072; # 3MB (KiB) -- matches high_performance_seed # "put 1.5 seconds worth of data in the send buffer" 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; services.caddy.virtualHosts."torrent.${service_configs.https.domain}".extraConfig = '' import ${config.age.secrets.caddy_auth.path} reverse_proxy ${config.vpnNamespaces.wg.namespaceAddress}:${builtins.toString config.services.qbittorrent.webuiPort} ''; users.users.${config.services.qbittorrent.user}.extraGroups = [ service_configs.media_group ]; }