nixos/services/arr-interconnect.nix
rope 81d1b80a53 arr-interconnect: declarative root folders for radarr/sonarr
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 11:42:04 +01:00

761 lines
32 KiB
Nix

{ config, lib, pkgs, ... }:
let
# Declarative root folders. The script reconciles against these: adds any
# that are missing, removes any that aren't listed. Removing a root folder
# only unregisters the path in Radarr/Sonarr — it never touches media files.
# Download-client staging dirs (/mnt/storage/usenet/downloads, the torrent
# downloads dir) must never appear here; the *arrs would import from library.
rootFolders = {
radarr = [ "/mnt/storage/torrents/movies" ];
sonarr = [ "/mnt/storage/torrents/shows" ];
};
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 pkgs.docker ]}:$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/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
# Seerr generates main.apiKey into settings.json on first boot, before the
# setup wizard runs, so the key alone doesn't mean it's configured.
SEERR_SETTINGS=/var/lib/jellyseerr/config/settings.json
SEERR_KEY=""
if [ -f "$SEERR_SETTINGS" ]; then
SEERR_KEY=$(jq -r '.main.apiKey // empty' "$SEERR_SETTINGS" 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
##########################################################################
# Root folders reconcile against the declarative list above.
#
# Runs before the Seerr blocks on purpose: they read rootfolder[0] to pick
# activeDirectory, which is only unambiguous once this has pruned the
# strays. Deleting a root folder unregisters the path only; existing
# movies/series keep their absolute paths and no files are touched.
##########################################################################
reconcile_root_folders() {
local name="$1" port="$2" key="$3" desired="$4" current
[ -n "$key" ] || return 0
current=$(curl -sf -H "X-Api-Key: $key" "$BASE:$port/api/v3/rootfolder" || true)
if [ -z "$current" ]; then
echo "$name not reachable, skipping root folders"
return 0
fi
# Refuse to prune against an empty list that would unregister every
# root folder the moment the Nix attrset is mistyped.
if [ "$(echo "$desired" | jq 'length')" = "0" ]; then
echo "$name has no declared root folders, skipping"
return 0
fi
echo "$desired" | jq -r '.[]' | while read -r path; do
if ! echo "$current" | jq -e --arg p "$path" 'any(.[]; .path == $p)' > /dev/null; then
echo "Adding root folder to $name: $path"
curl -sf -X POST \
-H "Content-Type: application/json" \
-H "X-Api-Key: $key" \
"$BASE:$port/api/v3/rootfolder" \
-d "$(jq -n --arg p "$path" '{path: $p}')" > /dev/null \
&& echo " done" || echo " failed"
fi
done
# Tab-separated: root folder paths routinely contain spaces and brackets.
echo "$current" \
| jq -r --argjson d "$desired" \
'.[] | select(.path as $p | ($d | index($p)) == null) | "\(.id)\t\(.path)"' \
| while IFS=$'\t' read -r id path; do
echo "Removing stray root folder from $name: $path"
curl -sf -X DELETE -H "X-Api-Key: $key" \
"$BASE:$port/api/v3/rootfolder/$id" > /dev/null \
&& echo " done" || echo " failed"
done
}
reconcile_root_folders "Radarr" 7878 "$RADARR_KEY" '${builtins.toJSON rootFolders.radarr}'
reconcile_root_folders "Sonarr" 8989 "$SONARR_KEY" '${builtins.toJSON rootFolders.sonarr}'
##########################################################################
# 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
##########################################################################
# Shelfarr Prowlarr + SABnzbd (audiobook indexer + download client)
#
# Shelfarr's own settings/download-client config lives behind session
# auth in its admin UI, not a public REST API, so this shells into the
# container and drives ActiveRecord directly via `rails runner`. That
# needs two secrets Shelfarr generates on first boot and writes into its
# storage volume .encryption_keys (Setting values aren't encrypted,
# but DownloadClient#api_key/#password are) and .secret_key_base
# (Rails always requires this to boot at all). Both only exist after the
# container has started once.
##########################################################################
if [ -f /var/lib/shelfarr/.encryption_keys ] && [ -f /var/lib/shelfarr/.secret_key_base ]; then
wait_for "Shelfarr" "$BASE:5056/up" "" || true
SHELFARR_SKB=$(cat /var/lib/shelfarr/.secret_key_base)
docker exec -i \
-e SECRET_KEY_BASE="$SHELFARR_SKB" \
-e ARR_PROWLARR_URL="$BASE:9696" \
-e ARR_PROWLARR_KEY="$PROWLARR_KEY" \
-e ARR_SABNZBD_URL="$BASE:8085" \
-e ARR_SABNZBD_KEY="$SABNZBD_KEY" \
shelfarr sh -c '. /rails/storage/.encryption_keys && bin/rails runner -' <<'RUBY' && echo " done" || echo " failed"
if ENV["ARR_PROWLARR_URL"].to_s != "" && ENV["ARR_PROWLARR_KEY"].to_s != "" && !SettingsService.prowlarr_configured?
SettingsService.set(:prowlarr_url, ENV["ARR_PROWLARR_URL"])
SettingsService.set(:prowlarr_api_key, ENV["ARR_PROWLARR_KEY"])
SettingsService.set(:indexer_provider, "prowlarr")
puts "Prowlarr configured in Shelfarr"
else
puts "Shelfarr Prowlarr already configured, or key missing"
end
if ENV["ARR_SABNZBD_URL"].to_s != "" && ENV["ARR_SABNZBD_KEY"].to_s != "" && !DownloadClient.exists?(name: "SABnzbd")
DownloadClient.create!(
name: "SABnzbd",
client_type: "sabnzbd",
url: ENV["ARR_SABNZBD_URL"],
api_key: ENV["ARR_SABNZBD_KEY"],
category: "shelfarr",
enabled: true,
priority: 0
)
puts "SABnzbd added to Shelfarr"
else
puts "Shelfarr SABnzbd already configured, or key missing"
end
RUBY
else
echo "Shelfarr hasn't completed its first boot yet (no encryption keys), skipping"
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
##########################################################################
# Seerr Jellyfin / Radarr / Sonarr, plus auto-approve for all users
#
# Every /api/v1/settings/* route resolves X-Api-Key to user id 1 and 403s
# if that row is missing. User 1 is only created when someone completes
# the setup wizard by signing in with Jellyfin admin credentials, which we
# deliberately don't automate it would mean parking the Jellyfin admin
# password in a runtime secrets file for a job that runs once. So probe
# for the admin first and skip the whole block until the wizard is done.
##########################################################################
if [ -n "$SEERR_KEY" ]; then
wait_for "Seerr" "$BASE:5055/api/v1/status" "" || true
if ! curl -sf -o /dev/null -H "X-Api-Key: $SEERR_KEY" "$BASE:5055/api/v1/settings/main"; then
echo "Seerr setup wizard not completed yet (no admin user), skipping Seerr config"
else
seerr_get() { curl -sf -H "X-Api-Key: $SEERR_KEY" "$BASE:5055/api/v1$1"; }
seerr_post() { curl -sf -X POST -H "Content-Type: application/json" \
-H "X-Api-Key: $SEERR_KEY" "$BASE:5055/api/v1$1" -d "$2"; }
# --- Jellyfin: wizard sets ip/port/serverId but leaves apiKey empty,
# --- which the library scan and user import both need.
if [ -n "$JELLYFIN_KEY" ]; then
if [ -z "$(seerr_get /settings/jellyfin | jq -r '.apiKey // empty')" ]; then
echo "Configuring Jellyfin in Seerr..."
seerr_post /settings/jellyfin "$(jq -n --arg key "$JELLYFIN_KEY" '{
ip: "127.0.0.1",
port: 8096,
useSsl: false,
urlBase: "",
apiKey: $key
}')" > /dev/null && echo " done" || echo " failed"
else
echo "Seerr Jellyfin already configured"
fi
fi
# --- Radarr. activeProfileId/activeDirectory are required and have no
# --- sane default, so pull them from Radarr itself. rootfolder[0] is
# --- unambiguous because reconcile_root_folders ran first.
# ponytail: still takes the first quality profile; name it explicitly
# here if you ever run more than one.
if [ -n "$RADARR_KEY" ] && [ "$(seerr_get /settings/radarr | jq 'length')" = "0" ]; then
# || true on every assignment: set -e aborts the whole job on a bare
# failing substitution, and Radarr being briefly down must not take
# the rest of the interconnect with it.
R_PROFILE=$(curl -sf -H "X-Api-Key: $RADARR_KEY" "$BASE:7878/api/v3/qualityprofile" | jq '.[0]' || true)
R_ROOT=$(curl -sf -H "X-Api-Key: $RADARR_KEY" "$BASE:7878/api/v3/rootfolder" | jq -r '.[0].path' || true)
if [ -n "$R_PROFILE" ] && [ "$R_PROFILE" != "null" ] && [ -n "$R_ROOT" ]; then
echo "Adding Radarr to Seerr..."
seerr_post /settings/radarr "$(jq -n \
--arg key "$RADARR_KEY" --arg root "$R_ROOT" \
--argjson pid "$(echo "$R_PROFILE" | jq '.id')" \
--arg pname "$(echo "$R_PROFILE" | jq -r '.name')" '{
name: "Radarr",
hostname: "localhost",
port: 7878,
apiKey: $key,
useSsl: false,
baseUrl: "",
activeProfileId: $pid,
activeProfileName: $pname,
activeDirectory: $root,
is4k: false,
minimumAvailability: "released",
isDefault: true,
externalUrl: "https://radarr.nordhammer.it",
syncEnabled: true,
preventSearch: false,
tagRequests: false,
tags: []
}')" > /dev/null && echo " done" || echo " failed"
else
echo "Radarr has no quality profile or root folder yet, skipping"
fi
else
echo "Seerr Radarr already configured, or Radarr key missing"
fi
# --- Sonarr. Anime slots point at the same profile/folder as the
# --- regular ones; split them in the UI if you ever separate anime.
if [ -n "$SONARR_KEY" ] && [ "$(seerr_get /settings/sonarr | jq 'length')" = "0" ]; then
S_PROFILE=$(curl -sf -H "X-Api-Key: $SONARR_KEY" "$BASE:8989/api/v3/qualityprofile" | jq '.[0]' || true)
S_ROOT=$(curl -sf -H "X-Api-Key: $SONARR_KEY" "$BASE:8989/api/v3/rootfolder" | jq -r '.[0].path' || true)
if [ -n "$S_PROFILE" ] && [ "$S_PROFILE" != "null" ] && [ -n "$S_ROOT" ]; then
echo "Adding Sonarr to Seerr..."
seerr_post /settings/sonarr "$(jq -n \
--arg key "$SONARR_KEY" --arg root "$S_ROOT" \
--argjson pid "$(echo "$S_PROFILE" | jq '.id')" \
--arg pname "$(echo "$S_PROFILE" | jq -r '.name')" '{
name: "Sonarr",
hostname: "localhost",
port: 8989,
apiKey: $key,
useSsl: false,
baseUrl: "",
activeProfileId: $pid,
activeProfileName: $pname,
activeDirectory: $root,
activeAnimeProfileId: $pid,
activeAnimeProfileName: $pname,
activeAnimeDirectory: $root,
activeLanguageProfileId: 1,
activeAnimeLanguageProfileId: 1,
is4k: false,
isDefault: true,
enableSeasonFolders: true,
externalUrl: "https://sonarr.nordhammer.it",
syncEnabled: true,
preventSearch: false,
tagRequests: false,
tags: [],
animeTags: []
}')" > /dev/null && echo " done" || echo " failed"
else
echo "Sonarr has no quality profile or root folder yet, skipping"
fi
else
echo "Seerr Sonarr already configured, or Sonarr key missing"
fi
######################################################################
# Auto-approve. REQUEST(32)|AUTO_APPROVE(128) = 160. AUTO_APPROVE is
# checked with an 'or' against the movie/TV variants, so the single
# bit covers both. defaultPermissions only lands on users imported
# *after* it's set, so existing ones get a bulk update too.
######################################################################
AUTO_APPROVE_PERMS=160
if [ "$(seerr_get /settings/main | jq '.defaultPermissions')" != "$AUTO_APPROVE_PERMS" ]; then
echo "Setting Seerr default permissions to request + auto-approve..."
seerr_post /settings/main "$(jq -n --argjson p "$AUTO_APPROVE_PERMS" \
'{defaultPermissions: $p}')" > /dev/null && echo " done" || echo " failed"
else
echo "Seerr default permissions already request + auto-approve"
fi
# Admin (id 1) is excluded it holds ADMIN and would be downgraded.
EXISTING_IDS=$(seerr_get "/user?take=1000" \
| jq -c --argjson p "$AUTO_APPROVE_PERMS" \
'[.results[] | select(.id != 1 and .permissions != $p) | .id]' || true)
if [ -n "$EXISTING_IDS" ] && [ "$EXISTING_IDS" != "[]" ]; then
echo "Granting auto-approve to existing Seerr users: $EXISTING_IDS"
curl -sf -X PUT -H "Content-Type: application/json" \
-H "X-Api-Key: $SEERR_KEY" "$BASE:5055/api/v1/user" \
-d "$(jq -n --argjson ids "$EXISTING_IDS" --argjson p "$AUTO_APPROVE_PERMS" \
'{ids: $ids, permissions: $p}')" > /dev/null && echo " done" || echo " failed"
else
echo "All existing Seerr users already have auto-approve"
fi
fi
else
echo "Seerr hasn't written settings.json yet, skipping"
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"
"sabnzbd.service"
"docker-shelfarr.service"
"seerr.service"
];
wants = [
"sonarr.service"
"radarr.service"
"prowlarr.service"
"bazarr.service"
"qbittorrent-nox.service"
"sabnzbd.service"
"docker-shelfarr.service"
"seerr.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;
};
};
};
}