diff --git a/services/arr-interconnect.nix b/services/arr-interconnect.nix index bb55ab5..5e23ab0 100644 --- a/services/arr-interconnect.nix +++ b/services/arr-interconnect.nix @@ -38,6 +38,14 @@ let 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" @@ -473,6 +481,163 @@ RUBY 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. + # ponytail: takes Radarr's first quality profile and root folder; pick + # them explicitly here if you ever run more than one of either. + 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. @@ -504,6 +669,7 @@ in "qbittorrent-nox.service" "sabnzbd.service" "docker-shelfarr.service" + "seerr.service" ]; wants = [ "sonarr.service" @@ -513,6 +679,7 @@ in "qbittorrent-nox.service" "sabnzbd.service" "docker-shelfarr.service" + "seerr.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = {