{ config, lib, pkgs, ... }: let interconnectScript = pkgs.writeShellScript "arr-interconnect" '' set -euo pipefail PATH="${lib.makeBinPath [ pkgs.curl pkgs.jq pkgs.gnused pkgs.gnugrep pkgs.coreutils pkgs.systemd pkgs.sqlite ]}:$PATH" BASE="http://127.0.0.1" # --- Extract API keys --- extract_arr_key() { if [ -f "$1" ]; then sed -n 's/.*\(.*\)<\/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/config/config.yaml" ]; then BAZARR_KEY=$(${pkgs.yq-go}/bin/yq '.auth.apikey' /var/lib/bazarr/config/config.yaml || true) fi # SAB writes its api_key into [misc] of sabnzbd.ini on first run; until # the user opens the UI once and the key materialises, the SAB blocks # below silently skip. SABNZBD_KEY="" if [ -f "/var/lib/sabnzbd/sabnzbd.ini" ]; then SABNZBD_KEY=$(grep -oP '^api_key\s*=\s*\K\S+' /var/lib/sabnzbd/sabnzbd.ini | head -n1 || true) fi # Jellyfin has no config.xml api key; any AccessToken in its db works as # an API key. Reuse the first one (create one in the Jellyfin UI once if # the table is empty — same first-run caveat as SAB above). JELLYFIN_KEY="" if [ -f "/var/lib/jellyfin/data/jellyfin.db" ]; then JELLYFIN_KEY=$(sqlite3 /var/lib/jellyfin/data/jellyfin.db "SELECT AccessToken FROM ApiKeys LIMIT 1;" 2>/dev/null || 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 ########################################################################## # Sonarr → SABnzbd (usenet download client for TV) ########################################################################## if [ -n "$SONARR_KEY" ] && [ -n "$SABNZBD_KEY" ]; then if ! exists_by_name "$BASE:8989/api/v3/downloadclient" "$SONARR_KEY" "SABnzbd"; then echo "Adding SABnzbd to Sonarr..." curl -sf -X POST \ -H "Content-Type: application/json" \ -H "X-Api-Key: $SONARR_KEY" \ "$BASE:8989/api/v3/downloadclient" \ -d "$(jq -n --arg key "$SABNZBD_KEY" '{ enable: true, protocol: "usenet", priority: 1, removeCompletedDownloads: true, removeFailedDownloads: true, name: "SABnzbd", implementation: "Sabnzbd", configContract: "SabnzbdSettings", implementationName: "SABnzbd", fields: [ {name: "host", value: "localhost"}, {name: "port", value: 8085}, {name: "useSsl", value: false}, {name: "urlBase", value: ""}, {name: "apiKey", value: $key}, {name: "username", value: ""}, {name: "password", value: ""}, {name: "tvCategory", value: "tv-sonarr"}, {name: "recentTvPriority", value: -100}, {name: "olderTvPriority", value: -100} ], tags: [] }')" > /dev/null && echo " done" || echo " failed" else echo "Sonarr → SABnzbd already configured" fi fi ########################################################################## # Radarr → SABnzbd (usenet download client for movies) ########################################################################## if [ -n "$RADARR_KEY" ] && [ -n "$SABNZBD_KEY" ]; then if ! exists_by_name "$BASE:7878/api/v3/downloadclient" "$RADARR_KEY" "SABnzbd"; then echo "Adding SABnzbd to Radarr..." curl -sf -X POST \ -H "Content-Type: application/json" \ -H "X-Api-Key: $RADARR_KEY" \ "$BASE:7878/api/v3/downloadclient" \ -d "$(jq -n --arg key "$SABNZBD_KEY" '{ enable: true, protocol: "usenet", priority: 1, removeCompletedDownloads: true, removeFailedDownloads: true, name: "SABnzbd", implementation: "Sabnzbd", configContract: "SabnzbdSettings", implementationName: "SABnzbd", fields: [ {name: "host", value: "localhost"}, {name: "port", value: 8085}, {name: "useSsl", value: false}, {name: "urlBase", value: ""}, {name: "apiKey", value: $key}, {name: "username", value: ""}, {name: "password", value: ""}, {name: "movieCategory", value: "radarr"}, {name: "recentMoviePriority", value: -100}, {name: "olderMoviePriority", value: -100} ], tags: [] }')" > /dev/null && echo " done" || echo " failed" else echo "Radarr → SABnzbd 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 ########################################################################## # Sonarr → Jellyfin (refresh library on import so new shows appear # without waiting for Jellyfin's flaky filesystem watcher / full scan) ########################################################################## if [ -n "$SONARR_KEY" ] && [ -n "$JELLYFIN_KEY" ]; then if ! exists_by_name "$BASE:8989/api/v3/notification" "$SONARR_KEY" "Jellyfin"; then echo "Adding Jellyfin notification to Sonarr..." curl -sf -X POST \ -H "Content-Type: application/json" \ -H "X-Api-Key: $SONARR_KEY" \ "$BASE:8989/api/v3/notification" \ -d "$(jq -n --arg key "$JELLYFIN_KEY" '{ name: "Jellyfin", implementation: "MediaBrowser", configContract: "MediaBrowserSettings", implementationName: "Emby / Jellyfin", onDownload: true, onUpgrade: true, onRename: true, onSeriesDelete: true, onEpisodeFileDelete: true, onEpisodeFileDeleteForUpgrade: true, fields: [ {name: "host", value: "localhost"}, {name: "port", value: 8096}, {name: "useSsl", value: false}, {name: "apiKey", value: $key}, {name: "notify", value: false}, {name: "updateLibrary", value: true} ], tags: [] }')" > /dev/null && echo " done" || echo " failed" else echo "Sonarr → Jellyfin already configured" fi fi ########################################################################## # Radarr → Jellyfin (refresh library on import) ########################################################################## if [ -n "$RADARR_KEY" ] && [ -n "$JELLYFIN_KEY" ]; then if ! exists_by_name "$BASE:7878/api/v3/notification" "$RADARR_KEY" "Jellyfin"; then echo "Adding Jellyfin notification to Radarr..." curl -sf -X POST \ -H "Content-Type: application/json" \ -H "X-Api-Key: $RADARR_KEY" \ "$BASE:7878/api/v3/notification" \ -d "$(jq -n --arg key "$JELLYFIN_KEY" '{ name: "Jellyfin", implementation: "MediaBrowser", configContract: "MediaBrowserSettings", implementationName: "Emby / Jellyfin", onDownload: true, onUpgrade: true, onRename: true, onMovieDelete: true, onMovieFileDelete: true, onMovieFileDeleteForUpgrade: true, fields: [ {name: "host", value: "localhost"}, {name: "port", value: 8096}, {name: "useSsl", value: false}, {name: "apiKey", value: $key}, {name: "notify", value: false}, {name: "updateLibrary", value: true} ], tags: [] }')" > /dev/null && echo " done" || echo " failed" else echo "Radarr → Jellyfin already configured" fi 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 "Enabled" "$PROWLARR_CONFIG"; then echo "Prowlarr auth: switching to DisabledForLocalAddresses..." sed -i 's|Enabled|DisabledForLocalAddresses|' "$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" "sabnzbd.service" ]; wants = [ "sonarr.service" "radarr.service" "prowlarr.service" "bazarr.service" "qbittorrent-nox.service" "sabnzbd.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; }; }; }; }