From 19ea2dc02bf4476e77b00b1f695cc3768be4c4f0 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Sun, 12 Apr 2026 21:29:37 -0400 Subject: [PATCH] prowlarr: handle bitmagnet restart --- services/bitmagnet.nix | 82 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/services/bitmagnet.nix b/services/bitmagnet.nix index f59e97a..a01c7ce 100644 --- a/services/bitmagnet.nix +++ b/services/bitmagnet.nix @@ -5,6 +5,57 @@ 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") @@ -25,9 +76,38 @@ }; http_server = { # TODO! make issue about this being a string and not a `port` type - port = ":" + (builtins.toString service_configs.ports.private.bitmagnet.port); + 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"; + }; + }; }