From 881403b128d982bc255fdcf83d38a917a3a9c561 Mon Sep 17 00:00:00 2001 From: soulis-1256 Date: Sun, 16 Aug 2026 21:58:54 +0300 Subject: [PATCH] Route Prowlarr indexer HTTP through Gluetun Enable Gluetun's HTTP proxy and add it as a Prowlarr indexer proxy when VPN is on so searches leave via the tunnel, not the host ISP. Remove the managed proxy when VPN is off. Log the real Prowlarr error when an indexer can only be saved disabled, instead of always blaming FlareSolverr. --- compose/vpn.yaml | 2 + lib/arr.sh | 133 +++++++++++++++++++++++++++++++++++++++++++-- lib/config_edit.sh | 4 ++ 3 files changed, 133 insertions(+), 6 deletions(-) diff --git a/compose/vpn.yaml b/compose/vpn.yaml index 3fe1306..58dcf54 100644 --- a/compose/vpn.yaml +++ b/compose/vpn.yaml @@ -27,6 +27,8 @@ services: - PORT_FORWARD_ONLY=${PORT_FORWARD_ONLY:-off} - VPN_PORT_FORWARDING=${VPN_PORT_FORWARDING:-off} - FIREWALL_OUTBOUND_SUBNETS=172.60.0.0/24 + # Prowlarr indexer HTTP (see prowlarr_sync_gluetun_proxy). Off unless set. + - HTTPPROXY=on - TZ=${TZ} healthcheck: test: diff --git a/lib/arr.sh b/lib/arr.sh index 6c0bfcd..6477b10 100644 --- a/lib/arr.sh +++ b/lib/arr.sh @@ -2,7 +2,8 @@ # assemblrr *arr service configuration — sourced by config.sh # Provides: set_arr_auth, qbit helpers, configure_arr_service, profile lookups, # configure_radarr, configure_sonarr, configure_prowlarr, -# apply_selected_indexers, sync_selected_indexers +# apply_selected_indexers, sync_selected_indexers, +# prowlarr_sync_gluetun_proxy # Requires: lib/core.sh (logging), lib/api.sh (api_get/post/put/delete helpers), # config.sh (_cfg_log_info, log_step, log_step_fail, AUTH_USERNAME, etc.) @@ -1046,6 +1047,116 @@ wait_for_arr_indexer_sync() { return 1 } +# Route Prowlarr indexer HTTP through Gluetun when VPN is on; remove it when off. +# Empty tags = every indexer. Does not wrap Radarr/Sonarr app sync (LAN hostnames). +# Manages only the proxy named below (or an existing Http proxy already on gluetun:8888). +PROWLARR_GLUETUN_PROXY_NAME="assemblrr Gluetun" +PROWLARR_GLUETUN_PROXY_HOST="gluetun" +PROWLARR_GLUETUN_PROXY_PORT="8888" + +# $2 = "name" (managed only) or "any" (name or host:port — avoid duplicates) +_prowlarr_gluetun_proxy_id() { + local proxies="$1" + local mode="${2:-any}" + echo "$proxies" | jq -r \ + --arg name "$PROWLARR_GLUETUN_PROXY_NAME" \ + --arg host "$PROWLARR_GLUETUN_PROXY_HOST" \ + --argjson port "$PROWLARR_GLUETUN_PROXY_PORT" \ + --arg mode "$mode" ' + .[] | select( + .name == $name + or ( + $mode == "any" + and .implementation == "Http" + and (([.fields[]? | select(.name == "host") | .value][0] // "") | tostring) == $host + and (([.fields[]? | select(.name == "port") | .value][0] // 0) | tonumber) == $port + ) + ) | .id + ' 2>/dev/null | head -1 +} + +_prowlarr_gluetun_proxy_payload() { + local schema="$1" + echo "$schema" | jq -c \ + --arg name "$PROWLARR_GLUETUN_PROXY_NAME" \ + --arg host "$PROWLARR_GLUETUN_PROXY_HOST" \ + --argjson port "$PROWLARR_GLUETUN_PROXY_PORT" ' + [.[] | select(.implementation == "Http")][0] | + .fields = [.fields[] | if .name == "host" then .value = $host + elif .name == "port" then .value = $port + elif .name == "username" then .value = "" + elif .name == "password" then .value = "" + else . end] | + .name = $name | .tags = [] + ' 2>/dev/null || echo "" +} + +prowlarr_sync_gluetun_proxy() { + local apikey="$1" + local proxies existing_id + + if [ -z "$apikey" ]; then + log_step_fail "Prowlarr: missing API key — cannot sync Gluetun HTTP proxy" + return 1 + fi + + proxies=$(api_get "9696" "/api/v1/indexerProxy" "$apikey") + if [ -z "$proxies" ]; then + log_step_fail "Prowlarr: failed to list indexer proxies" + return 1 + fi + if ! qbit_vpn_enabled; then + existing_id=$(_prowlarr_gluetun_proxy_id "$proxies" "name") + if [ -z "$existing_id" ]; then + log_step "Prowlarr: no Gluetun HTTP proxy (VPN off)" + return 0 + fi + local code + code=$(api_delete "9696" "/api/v1/indexerProxy/${existing_id}" "$apikey") + if [ "$code" = "200" ] || [ "$code" = "204" ]; then + log_step "Prowlarr: removed Gluetun HTTP proxy (VPN off)" + return 0 + fi + log_step_fail "Prowlarr: failed to remove Gluetun HTTP proxy (HTTP ${code:-?})" + return 1 + fi + + existing_id=$(_prowlarr_gluetun_proxy_id "$proxies" "any") + + local schema payload result + schema=$(api_get "9696" "/api/v1/indexerProxy/schema" "$apikey") + payload=$(_prowlarr_gluetun_proxy_payload "$schema") + if [ -z "$payload" ]; then + log_step_fail "Prowlarr: Http indexer-proxy schema not found" + return 1 + fi + + if [ -n "$existing_id" ]; then + payload=$(echo "$payload" | jq -c --argjson id "$existing_id" '.id = $id' 2>/dev/null || echo "$payload") + result=$(curl -s --connect-timeout 5 -X PUT \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "http://${API_HOST}:9696/api/v1/indexerProxy/${existing_id}?apikey=${apikey}&forceSave=true" 2>/dev/null) + if jq_json_has_key "$result" "id"; then + log_step "Prowlarr: indexer traffic via Gluetun HTTP proxy" + return 0 + fi + log_step_fail "Prowlarr: failed to update Gluetun HTTP proxy" + return 1 + fi + + result=$(curl -s --connect-timeout 5 -X POST \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "http://${API_HOST}:9696/api/v1/indexerProxy?apikey=${apikey}&forceSave=true" 2>/dev/null) + if jq_json_has_key "$result" "id"; then + log_step "Prowlarr: indexer traffic via Gluetun HTTP proxy" + return 0 + fi + log_step_fail "Prowlarr: failed to add Gluetun HTTP proxy" + return 1 +} + # POST indexers named in SELECTED_INDEXERS into Prowlarr (skip existing). # Used by first-run wiring and `assemblrr config edit`. apply_selected_indexers() { @@ -1097,14 +1208,19 @@ apply_selected_indexers() { if jq_json_has_key "$idx_result" "id"; then log_step "Prowlarr: added ${indexer_name} indexer" else - local disabled_payload + local err_msg disabled_payload + err_msg=$(echo "$idx_result" | jq -r 'if type == "array" then .[0].errorMessage // empty else .errorMessage // empty end' 2>/dev/null || echo "") disabled_payload=$(echo "$indexer_payload" | jq -c '.enable = false' 2>/dev/null || echo "") idx_result=$(curl -s --connect-timeout 5 -X POST \ -H "Content-Type: application/json" \ -d "$disabled_payload" \ "http://${API_HOST}:9696/api/v1/indexer?apikey=${apikey}&forceSave=true" 2>/dev/null) if jq_json_has_key "$idx_result" "id"; then - log_step "Prowlarr: added ${indexer_name} indexer (disabled — needs FlareSolverr for Cloudflare)" + if [ -n "$err_msg" ]; then + log_step "Prowlarr: added ${indexer_name} indexer (disabled — ${err_msg})" + else + log_step "Prowlarr: added ${indexer_name} indexer (disabled — enabled save failed)" + fi else log_step_fail "Prowlarr: failed to add ${indexer_name} indexer" fi @@ -1223,15 +1339,20 @@ configure_prowlarr() { fi fi - # 2. Selected indexers (optional; can add later via `config edit`) + # 2. Indexer HTTP via Gluetun when VPN is on (empty tags = all indexers) + if ! prowlarr_sync_gluetun_proxy "$apikey"; then + critical_errors=$((critical_errors + 1)) + fi + + # 3. Selected indexers (optional; can add later via `config edit`) apply_selected_indexers "$apikey" - # 3) After fullSync, set min seeders on *arr indexers (best-effort) + # 4) After fullSync, set min seeders on *arr indexers (best-effort) wait_for_arr_indexer_sync "$radarr_apikey" || true arr_set_indexer_min_seeders "Radarr" "7878" "$radarr_apikey" || true arr_set_indexer_min_seeders "Sonarr" "8989" "$sonarr_apikey" || true - # 4. Set authentication + # 5. Set authentication if ! set_arr_auth "Prowlarr" "9696" "$apikey" "v1"; then critical_errors=$((critical_errors + 1)) fi diff --git a/lib/config_edit.sh b/lib/config_edit.sh index 02aa20b..a98502b 100644 --- a/lib/config_edit.sh +++ b/lib/config_edit.sh @@ -450,6 +450,10 @@ _config_edit_vpn() { wait_for_api "Sonarr" "8989" "$SONARR_API_KEY" || true arr_update_qb_host "Sonarr" "8989" "$SONARR_API_KEY" || true fi + if [ -n "${PROWLARR_API_KEY:-}" ]; then + wait_for_api "Prowlarr" "9696" "$PROWLARR_API_KEY" "/api/v1/system/status" || true + prowlarr_sync_gluetun_proxy "$PROWLARR_API_KEY" || true + fi } _config_edit_media() {