diff --git a/services/arr-interconnect.nix b/services/arr-interconnect.nix index 5e23ab0..c498dd1 100644 --- a/services/arr-interconnect.nix +++ b/services/arr-interconnect.nix @@ -1,5 +1,15 @@ { 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" @@ -73,6 +83,59 @@ let 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) ########################################################################## @@ -519,9 +582,10 @@ RUBY fi # --- Radarr. activeProfileId/activeDirectory are required and have no - # --- sane default, so pull them from Radarr itself. - # ponytail: takes Radarr's first quality profile and root folder; pick - # them explicitly here if you ever run more than one of either. + # --- 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