Fail setup when service wiring critically fails

Track required vs optional wiring steps with real exit codes, stop
claiming success after partial failures, and note Jellyfin import scan
in the README.
This commit is contained in:
soulis-1256 2026-08-05 18:12:37 +03:00
parent 35b408d441
commit 188975fea2
No known key found for this signature in database
6 changed files with 283 additions and 176 deletions

View file

@ -15,6 +15,7 @@ Self-hosted media automation, set up in minutes, not a weekend.
- **Auto-wired services:** Post-install wiring connects Radarr, Sonarr, Prowlarr, qBittorrent, Seerr, Recyclarr, and Jellyfin (libraries, auth, root folders, download clients) instead of a manual weekend of clicking.
- **Quality profiles that ship ready:** Recyclarr syncs assemblrr-named HD and UHD (and TV) profiles from TRaSH Guides; setup only picks Seerrs default. Both resolutions stay available for overrides.
- **Request → library path:** Seerr in front of Radarr/Sonarr for a simple request UX, with hardlinks-friendly media layout for Jellyfin/Emby/Plex.
- **Jellyfin auto-scan on import:** When Jellyfin is selected, Radarr/Sonarr run a small hook after each import that tells Jellyfin to rescan the library — new movies/episodes show up without a manual scan.
- **VPN-first downloads:** Gluetun integration, download client traffic forced through the VPN context, `check-vpn` / start-time verification, and a vpn-watchdog for stalled routing.
- **Operator CLI:** `start` / `stop` / `restart` / `status` / `health` / `logs`, `config` (show / edit / sync), `backup` / `restore`, `update-containers` / `update-cli`, and a careful `uninstall` that preserves media unless you opt in.
- **Fail-safe backups:** CLI snapshots of configuration before risky updates so you can roll back without rebuilding from scratch.

View file

@ -1,10 +1,9 @@
#!/bin/bash
set -euo pipefail
# assemblrr service wiring (config sync)
# Wires Radarr, Prowlarr, and related services after initial deployment
# Called by setup.sh after containers start, or via: assemblrr config sync
# Uses jq for JSON parsing
# assemblrr service wiring — setup + `assemblrr config sync`
# Exit 0 if all required steps succeed; exit 1 if any required step fails.
# Required failures are counted and the script keeps going so you see a full report.
# --- Config discovery ---
@ -65,13 +64,40 @@ CONFIGURE_LOG="/tmp/${APP_NAME}-configure-$(date '+%Y%m%d-%H%M%S').log"
# Extended logging for configure (step markers on top of lib/core.sh base, tee to log file)
_cfg_log_info() { echo " $1" | tee -a "$CONFIGURE_LOG"; }
# Track step counts for end-of-run summary
# Step markers for the summary line (exit code uses _wire_critical_fail)
_configure_total=0
_configure_ok=0
_configure_fail=0
log_step() { echo -e " ${GREEN}${NC} $1" | tee -a "$CONFIGURE_LOG"; _configure_ok=$((_configure_ok + 1)); _configure_total=$((_configure_total + 1)); }
log_step_fail() { echo -e " ${RED}${NC} $1" | tee -a "$CONFIGURE_LOG"; _configure_fail=$((_configure_fail + 1)); _configure_total=$((_configure_total + 1)); }
_wire_critical_fail=0
_wire_optional_fail=0
# Required step: on failure count and continue
run_critical() {
set +e
"$@"
local rc=$?
set -e
if [ "$rc" -ne 0 ]; then
_wire_critical_fail=$((_wire_critical_fail + 1))
fi
return 0
}
# Nice-to-have step: never fails the overall run
run_optional() {
set +e
"$@"
local rc=$?
set -e
if [ "$rc" -ne 0 ]; then
_wire_optional_fail=$((_wire_optional_fail + 1))
fi
return 0
}
# Prompt for custom indexers (fzf TUI)
source "$_lib_dir/fzf-tui.sh"
@ -86,88 +112,107 @@ source "$_lib_dir/seerr.sh"
# --- Main ---
echo
_cfg_log_info "Auto-configuring ${APP_DISPLAY_NAME} services..."
_cfg_log_info "Wiring ${APP_DISPLAY_NAME} services..."
echo
# Read API keys from config.xml (created by services on first start)
RADARR_API_KEY=$(read_api_key "radarr") || RADARR_API_KEY=""
SONARR_API_KEY=$(read_api_key "sonarr") || SONARR_API_KEY=""
PROWLARR_API_KEY=$(read_api_key "prowlarr") || PROWLARR_API_KEY=""
# API keys from config.xml (services write these on first start)
RADARR_API_KEY=""
SONARR_API_KEY=""
PROWLARR_API_KEY=""
if [ -z "$RADARR_API_KEY" ]; then
log_step_fail "Cannot read Radarr API key — skipping Radarr configuration"
if ! RADARR_API_KEY=$(read_api_key "radarr"); then
RADARR_API_KEY=""
_wire_critical_fail=$((_wire_critical_fail + 1))
fi
if [ -z "$SONARR_API_KEY" ]; then
log_step_fail "Cannot read Sonarr API key — skipping Sonarr configuration"
if ! SONARR_API_KEY=$(read_api_key "sonarr"); then
SONARR_API_KEY=""
_wire_critical_fail=$((_wire_critical_fail + 1))
fi
if [ -z "$PROWLARR_API_KEY" ]; then
log_step_fail "Cannot read Prowlarr API key — skipping Prowlarr configuration"
if ! PROWLARR_API_KEY=$(read_api_key "prowlarr"); then
PROWLARR_API_KEY=""
_wire_critical_fail=$((_wire_critical_fail + 1))
fi
# Wait for APIs to be fully ready
# Drop key if API never becomes ready so later steps skip that service
if [ -n "$RADARR_API_KEY" ]; then
wait_for_api "Radarr" "7878" "$RADARR_API_KEY" || true
if ! wait_for_api "Radarr" "7878" "$RADARR_API_KEY"; then
_wire_critical_fail=$((_wire_critical_fail + 1))
RADARR_API_KEY=""
fi
fi
if [ -n "$SONARR_API_KEY" ]; then
wait_for_api "Sonarr" "8989" "$SONARR_API_KEY" || true
if ! wait_for_api "Sonarr" "8989" "$SONARR_API_KEY"; then
_wire_critical_fail=$((_wire_critical_fail + 1))
SONARR_API_KEY=""
fi
fi
if [ -n "$PROWLARR_API_KEY" ]; then
wait_for_api "Prowlarr" "9696" "$PROWLARR_API_KEY" "/api/v1/system/status" || true
configure_indexers "$PROWLARR_API_KEY"
if wait_for_api "Prowlarr" "9696" "$PROWLARR_API_KEY" "/api/v1/system/status"; then
run_optional configure_indexers "$PROWLARR_API_KEY"
else
_wire_critical_fail=$((_wire_critical_fail + 1))
PROWLARR_API_KEY=""
fi
fi
# Set qBittorrent credentials once (before configuring any *arr service)
if [ -n "$RADARR_API_KEY" ] || [ -n "$SONARR_API_KEY" ]; then
qbit_set_credentials || true
run_critical qbit_set_credentials
fi
# Configure services
if [ -n "$RADARR_API_KEY" ]; then
configure_radarr "$RADARR_API_KEY" || true
run_critical configure_radarr "$RADARR_API_KEY"
fi
if [ -n "$SONARR_API_KEY" ]; then
configure_sonarr "$SONARR_API_KEY" || true
run_critical configure_sonarr "$SONARR_API_KEY"
fi
if [ -n "$PROWLARR_API_KEY" ]; then
configure_prowlarr "$PROWLARR_API_KEY" "$RADARR_API_KEY" "$SONARR_API_KEY" || true
run_critical configure_prowlarr "$PROWLARR_API_KEY" "$RADARR_API_KEY" "$SONARR_API_KEY"
fi
if [ "${MEDIA_SERVICE:-}" = "jellyfin" ]; then
configure_jellyfin || true
configure_jellyfin_notifications || true
run_critical configure_jellyfin
run_optional configure_jellyfin_notifications
fi
# Configure Recyclarr (must run before Seerr so quality profiles exist in Radarr/Sonarr)
configure_recyclarr || true
# Recyclarr before Seerr (profiles must exist for Seerr defaults)
run_critical configure_recyclarr
# Enforce minSize=0 and record preferred quality profiles after Recyclarr sync.
if [ -n "$RADARR_API_KEY" ]; then
relax_quality_sizes "Radarr" "7878" "$RADARR_API_KEY" || true
set_default_quality_profile "Radarr" "7878" "$RADARR_API_KEY" "lookup_radarr_profile" || true
run_critical relax_quality_sizes "Radarr" "7878" "$RADARR_API_KEY"
run_critical set_default_quality_profile "Radarr" "7878" "$RADARR_API_KEY" "lookup_radarr_profile"
fi
if [ -n "$SONARR_API_KEY" ]; then
relax_quality_sizes "Sonarr" "8989" "$SONARR_API_KEY" || true
set_default_quality_profile "Sonarr" "8989" "$SONARR_API_KEY" "lookup_sonarr_profile" || true
run_critical relax_quality_sizes "Sonarr" "8989" "$SONARR_API_KEY"
run_critical set_default_quality_profile "Sonarr" "8989" "$SONARR_API_KEY" "lookup_sonarr_profile"
fi
# Configure Seerr (must run after media server is configured AND Recyclarr has synced profiles)
configure_seerr || true
run_critical configure_seerr
# Show compact summary
if [ "${_configure_fail:-0}" -gt 0 ]; then
echo
if [ "$_wire_critical_fail" -gt 0 ]; then
echo -e " ${RED}Wiring failed: ${_wire_critical_fail} required step(s)${NC}" | tee -a "$CONFIGURE_LOG"
if [ "$_configure_ok" -gt 0 ] || [ "$_configure_fail" -gt 0 ]; then
echo -e " ${YELLOW}${_configure_ok} ok, ${_configure_fail} failed (see markers above)${NC}" | tee -a "$CONFIGURE_LOG"
fi
echo -e " ${YELLOW}Log: $CONFIGURE_LOG${NC}" | tee -a "$CONFIGURE_LOG"
echo
echo -e " ${YELLOW}${_configure_ok} succeeded, ${_configure_fail} failed${NC}"
echo -e " ${YELLOW}Details: $CONFIGURE_LOG${NC}"
_cfg_log_info "Fix issues, then: ${APP_CLI_NAME:-assemblrr} config sync"
echo
exit 1
fi
if [ "$_configure_fail" -gt 0 ] || [ "$_wire_optional_fail" -gt 0 ]; then
echo -e " ${GREEN}Required wiring succeeded${NC}" | tee -a "$CONFIGURE_LOG"
echo -e " ${YELLOW}Some optional steps had issues — log: $CONFIGURE_LOG${NC}" | tee -a "$CONFIGURE_LOG"
echo
else
echo
echo -e " ${GREEN}All ${_configure_ok} steps succeeded${NC}"
echo -e " ${GREEN}All ${_configure_ok} steps succeeded${NC}" | tee -a "$CONFIGURE_LOG"
echo
fi
@ -185,3 +230,5 @@ echo
if [ -n "$AUTH_USERNAME" ]; then
_cfg_log_info "Login credentials: $AUTH_USERNAME / (the password you set during setup)"
fi
exit 0

View file

@ -661,40 +661,42 @@ if ! run_docker compose "${COMPOSE_ARGS[@]}" --profile "$media_service" up -d; t
fi
# Wire services (Radarr, Prowlarr, etc.)
local wiring_ok=1
if [ -f "$install_directory/config.sh" ]; then
if ! bash "$install_directory/config.sh"; then
log_warning "Auto-configuration had issues. You can re-run: $APP_CLI_NAME config sync"
wiring_ok=0
log_warning "Service wiring had critical failures. Containers are still running."
log_warning "Fix the issues above, then re-run: $APP_CLI_NAME config sync"
fi
fi
# Drop legacy name if an older install left it behind
rm -f "$install_directory/configure.sh" 2>/dev/null || true
# Install CLI and set permissions
# Install CLI (needed for config sync even if wiring failed)
echo
log_info "Installing CLI and configuring permissions..."
install_cli
set_permissions
log_success "All done! Enjoy ${APP_DISPLAY_NAME}!"
log_info "You can check the installation in $install_directory"
log_info "========================================================"
log_info "Everything should be running now! To check everything running, go to:"
echo
running_services_location
echo
log_info "All the service locations are also saved in ~/${APP_SERVICE_FILE}"
log_info "Service locations also saved in ~/${APP_SERVICE_FILE}"
running_services_location > ~/"${APP_SERVICE_FILE}"
log_info "========================================================"
echo
log_info "To configure ${APP_DISPLAY_NAME}, check the documentation at"
log_info "${APP_REPO_URL}"
echo
log_info "========================================================"
if [ "$wiring_ok" -eq 1 ]; then
log_success "All done! Enjoy ${APP_DISPLAY_NAME}!"
log_info "Install directory: $install_directory"
log_info "Docs: ${APP_REPO_URL}"
exit 0
fi
exit 0
log_warning "Setup finished, but service wiring failed."
log_warning "Install directory: $install_directory"
log_warning "Re-run wiring: $APP_CLI_NAME config sync"
log_info "Docs: ${APP_REPO_URL}"
exit 1
}
main "$@"

View file

@ -16,13 +16,15 @@ set_arr_auth() {
local api_version="${4:-v3}"
if [ -z "$AUTH_USERNAME" ] || [ -z "$AUTH_PASSWORD" ]; then
return
log_step_fail "${service_name}: auth credentials missing — cannot enable forms auth"
return 1
fi
local host_config
host_config=$(api_get "$port" "/api/${api_version}/config/host" "$apikey")
if [ -z "$host_config" ]; then
return
log_step_fail "${service_name}: failed to read host config for authentication"
return 1
fi
local host_id
@ -33,17 +35,19 @@ set_arr_auth() {
updated_config=$(echo "$host_config" | jq --arg hid "$host_id" --arg user "$AUTH_USERNAME" --arg pass "$AUTH_PASSWORD" \
'.id = ($hid | tonumber) | .authenticationMethod = "forms" | .authenticationRequired = "enabled" | .username = $user | .password = $pass | .passwordConfirmation = $pass' 2>/dev/null || echo "")
if [ -n "$updated_config" ]; then
local auth_result
auth_result=$(api_put "$port" "/api/${api_version}/config/host" "$apikey" "$updated_config")
if jq_json_has_key "$auth_result" "id"; then
log_step "${service_name}: set authentication (username: $AUTH_USERNAME)"
else
log_step_fail "${service_name}: failed to set authentication"
fi
else
if [ -z "$updated_config" ]; then
log_step_fail "${service_name}: failed to build auth config payload"
return 1
fi
local auth_result
auth_result=$(api_put "$port" "/api/${api_version}/config/host" "$apikey" "$updated_config")
if jq_json_has_key "$auth_result" "id"; then
log_step "${service_name}: set authentication (username: $AUTH_USERNAME)"
return 0
fi
log_step_fail "${service_name}: failed to set authentication"
return 1
}
# --- qBittorrent helpers ---
@ -128,25 +132,28 @@ qbit_create_category() {
"http://${API_HOST}:${qbit_port}/api/v2/auth/login" 2>/dev/null >/dev/null || true
local qbit_sid
qbit_sid=$(qbit_cookie_sid "$qbit_cookie_jar")
if [ -n "$qbit_sid" ]; then
# createCategory + editCategory so savePath is set whether the category is new or existing
curl -s --connect-timeout 10 \
"http://${API_HOST}:${qbit_port}/api/v2/torrents/createCategory" \
-b "$qbit_cookie_jar" \
-H "Referer: http://${API_HOST}:${qbit_port}" \
-d "category=${category}&savePath=${save_path}" \
2>/dev/null >/dev/null || true
curl -s --connect-timeout 10 \
"http://${API_HOST}:${qbit_port}/api/v2/torrents/editCategory" \
-b "$qbit_cookie_jar" \
-H "Referer: http://${API_HOST}:${qbit_port}" \
-d "category=${category}&savePath=${save_path}" \
2>/dev/null >/dev/null || true
log_step "${service_name}: qBittorrent '${category}' category → ${save_path}"
else
if [ -z "$qbit_sid" ]; then
log_step_fail "${service_name}: could not login to qBittorrent to create categories"
rm -f "$qbit_cookie_jar"
return 1
fi
# createCategory + editCategory so savePath is set whether the category is new or existing
curl -s --connect-timeout 10 \
"http://${API_HOST}:${qbit_port}/api/v2/torrents/createCategory" \
-b "$qbit_cookie_jar" \
-H "Referer: http://${API_HOST}:${qbit_port}" \
-d "category=${category}&savePath=${save_path}" \
2>/dev/null >/dev/null || true
curl -s --connect-timeout 10 \
"http://${API_HOST}:${qbit_port}/api/v2/torrents/editCategory" \
-b "$qbit_cookie_jar" \
-H "Referer: http://${API_HOST}:${qbit_port}" \
-d "category=${category}&savePath=${save_path}" \
2>/dev/null >/dev/null || true
log_step "${service_name}: qBittorrent '${category}' category → ${save_path}"
rm -f "$qbit_cookie_jar"
return 0
}
# --- Shared *arr service configuration ---
@ -170,6 +177,7 @@ configure_arr_service() {
local category_path="$6"
local unmonitor_field="$7"
local naming_jq="$8"
local critical_errors=0
# 1. Add root folder
local root_folders
@ -184,6 +192,7 @@ configure_arr_service() {
log_step "${service_name}: added ${root_path} as root folder"
else
log_step_fail "${service_name}: failed to add root folder"
critical_errors=$((critical_errors + 1))
fi
else
log_step "${service_name}: root folder ${root_path} already exists"
@ -248,16 +257,20 @@ configure_arr_service() {
log_step "${service_name}: added qBittorrent as download client (host: ${QBITTORRENT_HOST})"
else
log_step_fail "${service_name}: failed to add qBittorrent download client"
critical_errors=$((critical_errors + 1))
fi
else
log_step_fail "${service_name}: failed to build qBittorrent payload"
critical_errors=$((critical_errors + 1))
fi
else
log_step "${service_name}: qBittorrent download client already exists"
fi
# 2b. Create qBittorrent category with correct save path
qbit_create_category "$service_name" "$category_name" "$category_path"
if ! qbit_create_category "$service_name" "$category_name" "$category_path"; then
critical_errors=$((critical_errors + 1))
fi
# 2c. Enable hardlinks in Media Management
local mediamgmt
@ -276,7 +289,7 @@ configure_arr_service() {
fi
fi
# 3. Set naming convention (if jq expression provided)
# 3. Set naming convention
if [ -n "$naming_jq" ]; then
local naming
naming=$(api_get "$port" "/api/v3/config/naming" "$apikey")
@ -298,7 +311,11 @@ configure_arr_service() {
fi
# 4. Set authentication
set_arr_auth "$service_name" "$port" "$apikey"
if ! set_arr_auth "$service_name" "$port" "$apikey"; then
critical_errors=$((critical_errors + 1))
fi
[ "$critical_errors" -eq 0 ]
}
# --- Quality sizes & default profiles ---
@ -577,8 +594,9 @@ configure_prowlarr() {
local apikey="$1"
local radarr_apikey="$2"
local sonarr_apikey="${3:-}"
local critical_errors=0
# 1) App profile once — so new fullSync prefers min seeders 0
# 1) App profile min seeders (best-effort)
prowlarr_set_app_profile_min_seeders "$apikey" || true
# 2. Add Radarr as connected application
@ -586,27 +604,34 @@ configure_prowlarr() {
apps=$(api_get "9696" "/api/v1/applications" "$apikey")
local existing_impls
existing_impls=$(jq_json_list_values "$apps" "implementationName")
local app_schema=""
if ! echo "$existing_impls" | grep -q "Radarr"; then
# Get the Radarr application schema first to get correct field names
local app_schema
app_schema=$(api_get "9696" "/api/v1/applications/schema" "$apikey")
local app_payload
app_payload=$(echo "$app_schema" | jq --arg prowlarr_url "$PROWLARR_DOCKER_URL" --arg radarr_url "$RADARR_DOCKER_URL" --arg radarr_key "$radarr_apikey" '
[.[] | select(.implementationName == "Radarr")][0] |
.fields = [.fields[] | if .name == "prowlarrUrl" then .value = $prowlarr_url elif .name == "baseUrl" then .value = $radarr_url elif .name == "apiKey" then .value = $radarr_key elif .name == "syncCategories" then .value = [2000] elif .name == "syncRejectBlocklistedTorrentHashesWhileGrabbing" then .value = false else . end] |
.enable = true | .syncLevel = "fullSync" | .name = "Radarr" | .priority = (.priority // 25) | .tags = []
' 2>/dev/null || echo "")
if [ -z "$app_payload" ]; then
log_step_fail "Prowlarr: Radarr application schema not found"
return 0
fi
local app_result
app_result=$(api_post_force "9696" "/api/v1/applications" "$apikey" "$app_payload")
if jq_json_has_key "$app_result" "id"; then
log_step "Prowlarr: added Radarr as connected application"
if [ -z "$radarr_apikey" ]; then
log_step_fail "Prowlarr: Radarr API key missing — cannot connect application"
critical_errors=$((critical_errors + 1))
else
log_step_fail "Prowlarr: failed to add Radarr application"
# Get application schema once for Radarr/Sonarr field names
app_schema=$(api_get "9696" "/api/v1/applications/schema" "$apikey")
local app_payload
app_payload=$(echo "$app_schema" | jq --arg prowlarr_url "$PROWLARR_DOCKER_URL" --arg radarr_url "$RADARR_DOCKER_URL" --arg radarr_key "$radarr_apikey" '
[.[] | select(.implementationName == "Radarr")][0] |
.fields = [.fields[] | if .name == "prowlarrUrl" then .value = $prowlarr_url elif .name == "baseUrl" then .value = $radarr_url elif .name == "apiKey" then .value = $radarr_key elif .name == "syncCategories" then .value = [2000] elif .name == "syncRejectBlocklistedTorrentHashesWhileGrabbing" then .value = false else . end] |
.enable = true | .syncLevel = "fullSync" | .name = "Radarr" | .priority = (.priority // 25) | .tags = []
' 2>/dev/null || echo "")
if [ -z "$app_payload" ]; then
log_step_fail "Prowlarr: Radarr application schema not found"
critical_errors=$((critical_errors + 1))
else
local app_result
app_result=$(api_post_force "9696" "/api/v1/applications" "$apikey" "$app_payload")
if jq_json_has_key "$app_result" "id"; then
log_step "Prowlarr: added Radarr as connected application"
else
log_step_fail "Prowlarr: failed to add Radarr application"
critical_errors=$((critical_errors + 1))
fi
fi
fi
else
log_step "Prowlarr: Radarr application already connected"
@ -615,6 +640,9 @@ configure_prowlarr() {
# 1b. Add Sonarr as connected application
if [ -n "$sonarr_apikey" ]; then
if ! echo "$existing_impls" | grep -q "Sonarr"; then
if [ -z "$app_schema" ]; then
app_schema=$(api_get "9696" "/api/v1/applications/schema" "$apikey")
fi
local sonarr_app_payload
sonarr_app_payload=$(echo "$app_schema" | jq --arg prowlarr_url "$PROWLARR_DOCKER_URL" --arg sonarr_url "$SONARR_DOCKER_URL" --arg sonarr_key "$sonarr_apikey" '
[.[] | select(.implementationName == "Sonarr")][0] |
@ -623,6 +651,7 @@ configure_prowlarr() {
' 2>/dev/null || echo "")
if [ -z "$sonarr_app_payload" ]; then
log_step_fail "Prowlarr: Sonarr application schema not found"
critical_errors=$((critical_errors + 1))
else
local sonarr_app_result
sonarr_app_result=$(api_post_force "9696" "/api/v1/applications" "$apikey" "$sonarr_app_payload")
@ -630,6 +659,7 @@ configure_prowlarr() {
log_step "Prowlarr: added Sonarr as connected application"
else
log_step_fail "Prowlarr: failed to add Sonarr application"
critical_errors=$((critical_errors + 1))
fi
fi
else
@ -637,79 +667,89 @@ configure_prowlarr() {
fi
fi
# 2. Add selected indexers
echo "Adding ${#SELECTED_INDEXERS[@]} indexer(s) to Prowlarr" >&2
# 2. Selected indexers (optional; can add later in UI)
local indexer_schemas
indexer_schemas=$(api_get "9696" "/api/v1/indexer/schema" "$apikey")
if [ -z "$indexer_schemas" ]; then
log_step_fail "Prowlarr: failed to fetch indexer schemas"
return 0
fi
# Get existing indexers to avoid duplicates
local existing_indexers
existing_indexers=$(api_get "9696" "/api/v1/indexer" "$apikey")
local existing_names
existing_names=$(jq_json_list_values "$existing_indexers" "name")
for indexer_name in ${SELECTED_INDEXERS[@]+"${SELECTED_INDEXERS[@]}"}; do
# Skip if already added
if echo "$existing_names" | grep -q -F -x "$indexer_name"; then
log_step "Prowlarr: ${indexer_name} indexer already exists"
continue
else
local selected_indexers=()
if [ -n "${SELECTED_INDEXERS+x}" ] && [ "${#SELECTED_INDEXERS[@]}" -gt 0 ]; then
selected_indexers=("${SELECTED_INDEXERS[@]}")
echo "Adding ${#selected_indexers[@]} indexer(s) to Prowlarr" >&2
fi
# Build the indexer payload using jq to safely extract from schema
# Cardigann indexers use 'name' field to match, not implementationName
# Skip empty field values — Prowlarr uses Cardigann definition defaults
# Add as disabled for Cloudflare-protected indexers (need FlareSolverr)
local indexer_payload
indexer_payload=$(echo "$indexer_schemas" | jq --arg idx "$indexer_name" '
[.[] | select(.name == $idx or (.name | ascii_downcase | startswith($idx | ascii_downcase)))][0] |
.fields = [.fields[] | select(.value != "") | {name: .name, value: .value}] |
.enable = true | .enableAutoSearch = true | .appProfileId = 1 | .priority = (.priority // 25)
' 2>/dev/null || echo "")
# Get existing indexers to avoid duplicates
local existing_indexers
existing_indexers=$(api_get "9696" "/api/v1/indexer" "$apikey")
local existing_names
existing_names=$(jq_json_list_values "$existing_indexers" "name")
if [ -z "$indexer_payload" ]; then
log_step_fail "Prowlarr: ${indexer_name} schema not found"
continue
fi
local indexer_name
for indexer_name in "${selected_indexers[@]+"${selected_indexers[@]}"}"; do
# Skip if already added
if echo "$existing_names" | grep -q -F -x "$indexer_name"; then
log_step "Prowlarr: ${indexer_name} indexer already exists"
continue
fi
# Try adding with forceSave to bypass connectivity validation
# If that fails (e.g. Cloudflare), retry with enable=false
local idx_result
idx_result=$(curl -s --connect-timeout 5 -X POST \
-H "Content-Type: application/json" \
-d "$indexer_payload" \
"http://${API_HOST}:9696/api/v1/indexer?apikey=${apikey}&forceSave=true" 2>/dev/null)
if jq_json_has_key "$idx_result" "id"; then
log_step "Prowlarr: added ${indexer_name} indexer"
else
# Retry with enable=false
local disabled_payload
disabled_payload=$(echo "$indexer_payload" | jq -c '.enable = false' 2>/dev/null || echo "")
# Build the indexer payload using jq to safely extract from schema
# Cardigann indexers use 'name' field to match, not implementationName
# Skip empty field values — Prowlarr uses Cardigann definition defaults
local indexer_payload
indexer_payload=$(echo "$indexer_schemas" | jq --arg idx "$indexer_name" '
[.[] | select(.name == $idx or (.name | ascii_downcase | startswith($idx | ascii_downcase)))][0] |
.fields = [.fields[] | select(.value != "") | {name: .name, value: .value}] |
.enable = true | .enableAutoSearch = true | .appProfileId = 1 | .priority = (.priority // 25)
' 2>/dev/null || echo "")
if [ -z "$indexer_payload" ]; then
log_step_fail "Prowlarr: ${indexer_name} schema not found"
continue
fi
# Try adding with forceSave to bypass connectivity validation
# If that fails (e.g. Cloudflare), retry with enable=false
local idx_result
idx_result=$(curl -s --connect-timeout 5 -X POST \
-H "Content-Type: application/json" \
-d "$disabled_payload" \
-d "$indexer_payload" \
"http://${API_HOST}:9696/api/v1/indexer?apikey=${apikey}&forceSave=true" 2>/dev/null)
if jq_json_has_key "$idx_result" "id"; then
log_step "Prowlarr: added ${indexer_name} indexer (disabled — needs FlareSolverr for Cloudflare)"
log_step "Prowlarr: added ${indexer_name} indexer"
else
log_step_fail "Prowlarr: failed to add ${indexer_name} indexer"
# Retry with enable=false
local disabled_payload
disabled_payload=$(echo "$indexer_payload" | jq -c '.enable = false' 2>/dev/null || echo "")
idx_result=$(curl -s --connect-timeout 5 -X POST \
-H "Content-Type: application/json" \
-d "$disabled_payload" \
"http://${API_HOST}:9696/api/v1/indexer?apikey=${apikey}&forceSave=true" 2>/dev/null)
if jq_json_has_key "$idx_result" "id"; then
log_step "Prowlarr: added ${indexer_name} indexer (disabled — needs FlareSolverr for Cloudflare)"
else
log_step_fail "Prowlarr: failed to add ${indexer_name} indexer"
fi
fi
done
if [ "${#selected_indexers[@]}" -gt 0 ]; then
echo >&2
log_success "Indexer setup finished (check markers above for any failures)" >&2
fi
done
echo >&2
log_success "Indexers added to Prowlarr!" >&2
fi
# 3) Patch *arr once after fullSync has created indexers (profile alone is not enough)
# 3) After fullSync, set min seeders on *arr indexers (best-effort)
wait_for_arr_indexer_sync "$radarr_apikey" || true
arr_set_indexer_min_seeders "Radarr" "7878" "$radarr_apikey" || true
arr_set_indexer_min_seeders "Sonarr" "8989" "$sonarr_apikey" || true
# 4. Set authentication
set_arr_auth "Prowlarr" "9696" "$apikey" "v1"
if ! set_arr_auth "Prowlarr" "9696" "$apikey" "v1"; then
critical_errors=$((critical_errors + 1))
fi
[ "$critical_errors" -eq 0 ]
}
# --- Sonarr configuration ---

View file

@ -47,9 +47,10 @@ configure_jellyfin() {
if [ "$wizard_complete" = "True" ] || [ "$wizard_complete" = "true" ]; then
log_step "Jellyfin: startup wizard already completed"
echo "Configuring Jellyfin libraries" >&2
configure_jellyfin_libraries
local lib_rc=0
configure_jellyfin_libraries || lib_rc=$?
echo >&2
return 0
return "$lib_rc"
fi
fi
@ -152,8 +153,10 @@ configure_jellyfin() {
# Step 8: Add media libraries
echo "Configuring Jellyfin libraries" >&2
configure_jellyfin_libraries
local lib_rc=0
configure_jellyfin_libraries || lib_rc=$?
echo >&2
return "$lib_rc"
}
configure_jellyfin_xml_fallback() {
@ -273,6 +276,7 @@ configure_jellyfin_libraries() {
"http://${API_HOST}:${jellyfin_port}/Library/VirtualFolders" 2>/dev/null || echo "[]")
fi
local critical_errors=0
for i in "${!lib_names[@]}"; do
local lib_name="${lib_names[$i]}"
local lib_type="${lib_types[$i]}"
@ -303,6 +307,9 @@ configure_jellyfin_libraries() {
log_step "Jellyfin: added ${lib_name} library (${lib_path})"
else
log_step_fail "Jellyfin: failed to add ${lib_name} library (HTTP $add_code)"
if [ "$lib_type" = "movies" ] || [ "$lib_type" = "tvshows" ]; then
critical_errors=$((critical_errors + 1))
fi
fi
done
@ -375,6 +382,8 @@ configure_jellyfin_libraries() {
else
log_step_fail "Jellyfin: failed to create API key (HTTP $key_create_code)"
fi
[ "$critical_errors" -eq 0 ]
}
# --- Jellyfin notification connections (Radarr/Sonarr → Jellyfin) ---
@ -388,16 +397,19 @@ configure_jellyfin_notifications() {
return 1
fi
local jf_host="jellyfin"
local jf_port=8096
# NOTE: The MediaBrowser/Emby notification type calls /Library/Media/Updated which
# only refreshes EXISTING items in Jellyfin's DB — it cannot discover NEW files.
# We use a CustomScript that calls POST /Library/Refresh instead, which performs a
# full library scan and discovers newly downloaded movies/episodes.
add_jellyfin_refresh_notif "Radarr" "7878" "$RADARR_API_KEY"
add_jellyfin_refresh_notif "Sonarr" "8989" "$SONARR_API_KEY"
local errors=0
if ! add_jellyfin_refresh_notif "Radarr" "7878" "$RADARR_API_KEY"; then
errors=$((errors + 1))
fi
if ! add_jellyfin_refresh_notif "Sonarr" "8989" "$SONARR_API_KEY"; then
errors=$((errors + 1))
fi
[ "$errors" -eq 0 ]
}
# Add a CustomScript notification to an *arr app that triggers Jellyfin library refresh.
@ -446,13 +458,14 @@ add_jellyfin_refresh_notif() {
result=$(api_post_force "$port" "/api/v3/notification" "$apikey" "$payload")
if jq_json_has_key "$result" "id"; then
log_step "${app_name}: added Jellyfin Refresh script (auto-scan on import)"
else
local err_msg
err_msg=$(echo "$result" | jq -r 'if type == "array" then .[0].errorMessage // empty else .errorMessage // empty end' 2>/dev/null || echo "")
if [ -n "$err_msg" ]; then
log_step_fail "${app_name}: failed to add Jellyfin Refresh script (${err_msg})"
else
log_step_fail "${app_name}: failed to add Jellyfin Refresh script"
fi
return 0
fi
local err_msg
err_msg=$(echo "$result" | jq -r 'if type == "array" then .[0].errorMessage // empty else .errorMessage // empty end' 2>/dev/null || echo "")
if [ -n "$err_msg" ]; then
log_step_fail "${app_name}: failed to add Jellyfin Refresh script (${err_msg})"
else
log_step_fail "${app_name}: failed to add Jellyfin Refresh script"
fi
return 1
}

View file

@ -296,12 +296,14 @@ configure_seerr() {
echo "Connecting Seerr services" >&2
local connect_errors=0
if [ -n "$RADARR_API_KEY" ]; then
local radarr_id
radarr_id=$(seerr_service_id "radarr" "$seerr_cookie_jar" || true)
if ! seerr_connect_service "radarr" 7878 "$RADARR_API_KEY" "lookup_radarr_profile" \
"/data/media/movies" "${SEERR_IS_4K:-false}" "$seerr_cookie_jar" "$radarr_id"; then
log_step_fail "Seerr: failed to configure Radarr (re-run to retry)"
connect_errors=$((connect_errors + 1))
fi
fi
@ -311,12 +313,14 @@ configure_seerr() {
if ! seerr_connect_service "sonarr" 8989 "$SONARR_API_KEY" "lookup_sonarr_profile" \
"/data/media/tv" "false" "$seerr_cookie_jar" "$sonarr_id"; then
log_step_fail "Seerr: failed to configure Sonarr (re-run to retry)"
connect_errors=$((connect_errors + 1))
fi
fi
echo >&2
rm -f "$seerr_cookie_jar"
return 0
[ "$connect_errors" -eq 0 ]
return $?
fi
fi