{ 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>.*/\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 "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" "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; }; }; }; }