nixos/services/arr-interconnect.nix
ediblerope 88c1b8b2fe arr-interconnect: enforce Prowlarr local-auth bypass
Sonarr/Radarr/Bazarr default to DisabledForLocalAddresses so that requests
coming via the nginx reverse proxy (from 127.0.0.1) skip the app's own
login, leaving Authelia as the single gate. Prowlarr defaults to Enabled,
which produces a 401 behind Authelia.

Idempotent: only rewrites config.xml + restarts prowlarr when it finds
the "Enabled" value; logs a no-op otherwise. Added pkgs.systemd to PATH
so the restart call works.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 14:39:53 +01:00

356 lines
14 KiB
Nix

{ config, lib, pkgs, ... }:
let
interconnectScript = pkgs.writeShellScript "arr-interconnect" ''
set -euo pipefail
PATH="${lib.makeBinPath [ pkgs.curl pkgs.jq pkgs.gnused pkgs.gnugrep pkgs.gawk pkgs.coreutils pkgs.systemd ]}:$PATH"
BASE="http://127.0.0.1"
# --- Extract API keys ---
extract_arr_key() {
if [ -f "$1" ]; then
sed -n 's/.*<ApiKey>\(.*\)<\/ApiKey>.*/\1/p' "$1"
fi
}
SONARR_KEY=$(extract_arr_key "/var/lib/sonarr/config.xml")
RADARR_KEY=$(extract_arr_key "/var/lib/radarr/config.xml")
PROWLARR_KEY=$(extract_arr_key "/var/lib/prowlarr/config.xml")
BAZARR_KEY=""
if [ -f "/var/lib/bazarr/data/config/config.ini" ]; then
BAZARR_KEY=$(grep -oP '(?<=apikey = ).*' /var/lib/bazarr/data/config/config.ini || true)
fi
# --- Helpers ---
wait_for() {
local name="$1" url="$2" key="$3"
echo "Waiting for $name..."
for i in $(seq 1 30); do
if curl -sf -o /dev/null -H "X-Api-Key: $key" "$url"; then
echo "$name is ready"
return 0
fi
sleep 2
done
echo "WARNING: $name not ready after 60s, skipping"
return 1
}
exists_by_name() {
local url="$1" key="$2" name="$3"
local count
count=$(curl -sf -H "X-Api-Key: $key" "$url" | jq --arg n "$name" '[.[] | select(.name == $n)] | length')
[ "$count" -gt "0" ]
}
# --- Wait for services ---
wait_for "Sonarr" "$BASE:8989/api/v3/system/status" "$SONARR_KEY" || true
wait_for "Radarr" "$BASE:7878/api/v3/system/status" "$RADARR_KEY" || true
wait_for "Prowlarr" "$BASE:9696/api/v1/system/status" "$PROWLARR_KEY" || true
##########################################################################
# Prowlarr Sonarr (push indexers for TV)
##########################################################################
if [ -n "$PROWLARR_KEY" ] && [ -n "$SONARR_KEY" ]; then
if ! exists_by_name "$BASE:9696/api/v1/applications" "$PROWLARR_KEY" "Sonarr"; then
echo "Adding Sonarr to Prowlarr..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-H "X-Api-Key: $PROWLARR_KEY" \
"$BASE:9696/api/v1/applications" \
-d "$(jq -n --arg key "$SONARR_KEY" '{
name: "Sonarr",
syncLevel: "fullSync",
implementation: "Sonarr",
configContract: "SonarrSettings",
implementationName: "Sonarr",
fields: [
{name: "prowlarrUrl", value: "http://localhost:9696"},
{name: "baseUrl", value: "http://localhost:8989"},
{name: "apiKey", value: $key},
{name: "syncCategories", value: [5000,5010,5020,5030,5040,5045,5050,5060,5070,5080]}
],
tags: []
}')" > /dev/null && echo " done" || echo " failed"
else
echo "Prowlarr Sonarr already configured"
fi
fi
##########################################################################
# Prowlarr Radarr (push indexers for movies)
##########################################################################
if [ -n "$PROWLARR_KEY" ] && [ -n "$RADARR_KEY" ]; then
if ! exists_by_name "$BASE:9696/api/v1/applications" "$PROWLARR_KEY" "Radarr"; then
echo "Adding Radarr to Prowlarr..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-H "X-Api-Key: $PROWLARR_KEY" \
"$BASE:9696/api/v1/applications" \
-d "$(jq -n --arg key "$RADARR_KEY" '{
name: "Radarr",
syncLevel: "fullSync",
implementation: "Radarr",
configContract: "RadarrSettings",
implementationName: "Radarr",
fields: [
{name: "prowlarrUrl", value: "http://localhost:9696"},
{name: "baseUrl", value: "http://localhost:7878"},
{name: "apiKey", value: $key},
{name: "syncCategories", value: [2000,2010,2020,2030,2040,2045,2050,2060,2070,2080]}
],
tags: []
}')" > /dev/null && echo " done" || echo " failed"
else
echo "Prowlarr Radarr already configured"
fi
fi
##########################################################################
# Sonarr qBittorrent (download client for TV)
##########################################################################
if [ -n "$SONARR_KEY" ]; then
if ! exists_by_name "$BASE:8989/api/v3/downloadclient" "$SONARR_KEY" "qBittorrent"; then
echo "Adding qBittorrent to Sonarr..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-H "X-Api-Key: $SONARR_KEY" \
"$BASE:8989/api/v3/downloadclient" \
-d '{
"enable": true,
"protocol": "torrent",
"priority": 1,
"removeCompletedDownloads": false,
"removeFailedDownloads": true,
"name": "qBittorrent",
"implementation": "QBittorrent",
"configContract": "QBittorrentSettings",
"implementationName": "qBittorrent",
"fields": [
{"name": "host", "value": "localhost"},
{"name": "port", "value": 8080},
{"name": "useSsl", "value": false},
{"name": "urlBase", "value": ""},
{"name": "username", "value": ""},
{"name": "password", "value": ""},
{"name": "category", "value": "tv-sonarr"},
{"name": "recentPriority", "value": 0},
{"name": "olderPriority", "value": 0},
{"name": "initialState", "value": 0},
{"name": "sequentialOrder", "value": false},
{"name": "firstAndLastFirst", "value": false}
],
"tags": []
}' > /dev/null && echo " done" || echo " failed"
else
echo "Sonarr qBittorrent already configured"
fi
fi
##########################################################################
# Radarr qBittorrent (download client for movies)
##########################################################################
if [ -n "$RADARR_KEY" ]; then
if ! exists_by_name "$BASE:7878/api/v3/downloadclient" "$RADARR_KEY" "qBittorrent"; then
echo "Adding qBittorrent to Radarr..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-H "X-Api-Key: $RADARR_KEY" \
"$BASE:7878/api/v3/downloadclient" \
-d '{
"enable": true,
"protocol": "torrent",
"priority": 1,
"removeCompletedDownloads": false,
"removeFailedDownloads": true,
"name": "qBittorrent",
"implementation": "QBittorrent",
"configContract": "QBittorrentSettings",
"implementationName": "qBittorrent",
"fields": [
{"name": "host", "value": "localhost"},
{"name": "port", "value": 8080},
{"name": "useSsl", "value": false},
{"name": "urlBase", "value": ""},
{"name": "username", "value": ""},
{"name": "password", "value": ""},
{"name": "category", "value": "radarr"},
{"name": "recentPriority", "value": 0},
{"name": "olderPriority", "value": 0},
{"name": "initialState", "value": 0},
{"name": "sequentialOrder", "value": false},
{"name": "firstAndLastFirst", "value": false}
],
"tags": []
}' > /dev/null && echo " done" || echo " failed"
else
echo "Radarr qBittorrent already configured"
fi
fi
##########################################################################
# Bazarr Sonarr (subtitle management for TV)
##########################################################################
if [ -n "$BAZARR_KEY" ] && [ -n "$SONARR_KEY" ]; then
# Check if Sonarr is already configured in Bazarr
CURRENT_SONARR_KEY=$(curl -sf -H "X-API-KEY: $BAZARR_KEY" \
"$BASE:6767/api/system/settings" | jq -r '.data.settings.sonarr.apikey // empty' 2>/dev/null || true)
if [ -z "$CURRENT_SONARR_KEY" ]; then
echo "Configuring Sonarr in Bazarr..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-H "X-API-KEY: $BAZARR_KEY" \
"$BASE:6767/api/system/settings" \
-d "$(jq -n --arg key "$SONARR_KEY" '{
settings: {
sonarr: {
ip: "127.0.0.1",
port: "8989",
base_url: "/",
ssl: "false",
apikey: $key,
full_update: "Daily",
only_monitored: "false",
series_sync: "60",
episodes_sync: "60"
}
}
}')" > /dev/null && echo " done" || echo " failed"
else
echo "Bazarr Sonarr already configured"
fi
fi
##########################################################################
# Bazarr Radarr (subtitle management for movies)
##########################################################################
if [ -n "$BAZARR_KEY" ] && [ -n "$RADARR_KEY" ]; then
CURRENT_RADARR_KEY=$(curl -sf -H "X-API-KEY: $BAZARR_KEY" \
"$BASE:6767/api/system/settings" | jq -r '.data.settings.radarr.apikey // empty' 2>/dev/null || true)
if [ -z "$CURRENT_RADARR_KEY" ]; then
echo "Configuring Radarr in Bazarr..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-H "X-API-KEY: $BAZARR_KEY" \
"$BASE:6767/api/system/settings" \
-d "$(jq -n --arg key "$RADARR_KEY" '{
settings: {
radarr: {
ip: "127.0.0.1",
port: "7878",
base_url: "/",
ssl: "false",
apikey: $key,
full_update: "Daily",
only_monitored: "false",
movies_sync: "60"
}
}
}')" > /dev/null && echo " done" || echo " failed"
else
echo "Bazarr Radarr already configured"
fi
fi
##########################################################################
# Quality Definitions floor 1080p sources at 10 MB/min (~1.3 Mbps)
# so sub-bitrate releases (e.g. 163 MiB 40-min garbage) get rejected.
##########################################################################
set_quality_floor() {
local base="$1" key="$2" title="$3" min_size="$4"
local current
current=$(curl -sf -H "X-Api-Key: $key" "$base/api/v3/qualitydefinition" \
| jq --arg t "$title" '.[] | select(.title == $t)')
if [ -z "$current" ]; then
echo " $title: not found, skipping"
return 0
fi
local cur_min
cur_min=$(echo "$current" | jq -r '.minSize')
if awk -v a="$cur_min" -v b="$min_size" 'BEGIN{exit !(a==b)}'; then
echo " $title: already at min=$min_size"
return 0
fi
local id updated
id=$(echo "$current" | jq -r '.id')
updated=$(echo "$current" | jq --argjson min "$min_size" '.minSize = $min')
curl -sf -X PUT \
-H "Content-Type: application/json" \
-H "X-Api-Key: $key" \
"$base/api/v3/qualitydefinition/$id" \
-d "$updated" > /dev/null \
&& echo " $title: min=$cur_min $min_size" \
|| echo " $title: update failed"
}
QUALITY_FLOOR=10
QUALITY_TITLES=("HDTV-1080p" "WEBDL-1080p" "WEBRip-1080p" "Bluray-1080p")
if [ -n "$SONARR_KEY" ]; then
echo "Setting Sonarr 1080p quality floors..."
for title in "''${QUALITY_TITLES[@]}"; do
set_quality_floor "$BASE:8989" "$SONARR_KEY" "$title" "$QUALITY_FLOOR"
done
fi
if [ -n "$RADARR_KEY" ]; then
echo "Setting Radarr 1080p quality floors..."
for title in "''${QUALITY_TITLES[@]}"; do
set_quality_floor "$BASE:7878" "$RADARR_KEY" "$title" "$QUALITY_FLOOR"
done
fi
##########################################################################
# Prowlarr auth trust localhost so Authelia is the only gate. Other
# *arr apps default to this; Prowlarr does not.
##########################################################################
PROWLARR_CONFIG=/var/lib/prowlarr/config.xml
if [ -f "$PROWLARR_CONFIG" ]; then
if grep -q "<AuthenticationRequired>Enabled</AuthenticationRequired>" "$PROWLARR_CONFIG"; then
echo "Prowlarr auth: switching to DisabledForLocalAddresses..."
sed -i 's|<AuthenticationRequired>Enabled</AuthenticationRequired>|<AuthenticationRequired>DisabledForLocalAddresses</AuthenticationRequired>|' "$PROWLARR_CONFIG"
systemctl restart prowlarr
else
echo "Prowlarr auth: already DisabledForLocalAddresses"
fi
fi
echo "Interconnect setup complete."
'';
in
{
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
systemd.services.arr-interconnect = {
description = "Auto-configure connections between *arr services";
after = [
"sonarr.service"
"radarr.service"
"prowlarr.service"
"bazarr.service"
"qbittorrent-nox.service"
];
wants = [
"sonarr.service"
"radarr.service"
"prowlarr.service"
"bazarr.service"
"qbittorrent-nox.service"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = interconnectScript;
RemainAfterExit = true;
# Retry once if services weren't ready
Restart = "on-failure";
RestartSec = "30s";
StartLimitBurst = 3;
};
};
};
}