{ pkgs, service_configs, config, lib, ... }: let prowlarrPort = toString service_configs.ports.private.prowlarr.port; sonarrPort = toString service_configs.ports.private.sonarr.port; radarrPort = toString service_configs.ports.private.radarr.port; bitmagnetPort = toString service_configs.ports.private.bitmagnet.port; bridgeAddr = config.vpnNamespaces.wg.bridgeAddress; prowlarrConfigXml = "${service_configs.prowlarr.dataDir}/config.xml"; sonarrConfigXml = "${service_configs.sonarr.dataDir}/config.xml"; radarrConfigXml = "${service_configs.radarr.dataDir}/config.xml"; curl = "${pkgs.curl}/bin/curl"; jq = "${pkgs.jq}/bin/jq"; # Clears the escalating failure backoff for the Bitmagnet indexer across # Prowlarr, Sonarr, and Radarr so searches resume immediately after # Bitmagnet restarts instead of waiting hours for disable timers to expire. recoveryScript = pkgs.writeShellScript "prowlarr-bitmagnet-recovery" '' set -euo pipefail wait_for() { for _ in $(seq 1 "$2"); do ${curl} -sf --max-time 5 "$1" > /dev/null && return 0 sleep 5 done echo "$1 not reachable, aborting" >&2; exit 1 } # Test a Bitmagnet-named indexer to clear its failure status. # A successful test triggers RecordSuccess() which resets the backoff. clear_status() { local key indexer key=$(${lib.extractArrApiKey ''"$3"''}) || return 0 indexer=$(${curl} -sf --max-time 10 \ -H "X-Api-Key: $key" "$2/api/$1/indexer" | \ ${jq} 'first(.[] | select(.name | test("Bitmagnet"; "i")))') || return 0 [ -n "$indexer" ] && [ "$indexer" != "null" ] || return 0 ${curl} -sf --max-time 30 \ -H "X-Api-Key: $key" -H "Content-Type: application/json" \ -X POST "$2/api/$1/indexer/test" -d "$indexer" > /dev/null } wait_for "http://localhost:${bitmagnetPort}" 12 wait_for "http://localhost:${prowlarrPort}/ping" 6 # Prowlarr first — downstream apps route searches through it. clear_status v1 "http://localhost:${prowlarrPort}" "${prowlarrConfigXml}" || true clear_status v3 "http://${bridgeAddr}:${sonarrPort}" "${sonarrConfigXml}" || true clear_status v3 "http://${bridgeAddr}:${radarrPort}" "${radarrConfigXml}" || true ''; in { imports = [ (lib.vpnNamespaceOpenPort service_configs.ports.private.bitmagnet.port "bitmagnet") (lib.mkCaddyReverseProxy { subdomain = "bitmagnet"; port = service_configs.ports.private.bitmagnet.port; auth = true; vpn = true; }) ]; services.bitmagnet = { enable = true; settings = { postgres = { host = service_configs.postgres.socket; }; http_server = { # TODO! make issue about this being a string and not a `port` type port = ":" + (toString service_configs.ports.private.bitmagnet.port); }; }; }; # The upstream default (Restart=on-failure) leaves Bitmagnet dead after # clean exits (e.g. systemd stop during deploy). Always restart it. systemd.services.bitmagnet.serviceConfig = { Restart = lib.mkForce "always"; RestartSec = 10; }; # After Bitmagnet restarts, clear the escalating failure backoff across # Prowlarr, Sonarr, and Radarr so searches resume immediately instead of # waiting hours for the disable timers to expire. systemd.services.prowlarr-bitmagnet-recovery = { description = "Clear Prowlarr/Sonarr/Radarr failure status for Bitmagnet indexer"; after = [ "bitmagnet.service" "prowlarr.service" "sonarr.service" "radarr.service" ]; bindsTo = [ "bitmagnet.service" ]; wantedBy = [ "bitmagnet.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = recoveryScript; # Same VPN namespace as Bitmagnet and Prowlarr. NetworkNamespacePath = "/run/netns/wg"; }; }; }