diff --git a/README.md b/README.md index 37bf7e0..0cbcf7b 100644 --- a/README.md +++ b/README.md @@ -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 Seerr’s 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. diff --git a/bin/config.sh b/bin/config.sh index 2ff366d..df4d1a0 100644 --- a/bin/config.sh +++ b/bin/config.sh @@ -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 diff --git a/bin/setup.sh b/bin/setup.sh index fa0f0fc..56228de 100644 --- a/bin/setup.sh +++ b/bin/setup.sh @@ -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 "$@" diff --git a/lib/arr.sh b/lib/arr.sh index 335fdbb..53343c9 100644 --- a/lib/arr.sh +++ b/lib/arr.sh @@ -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 --- diff --git a/lib/jellyfin.sh b/lib/jellyfin.sh index d36b9b1..9ff4e3d 100644 --- a/lib/jellyfin.sh +++ b/lib/jellyfin.sh @@ -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 } diff --git a/lib/seerr.sh b/lib/seerr.sh index 0d607dd..4c5096e 100644 --- a/lib/seerr.sh +++ b/lib/seerr.sh @@ -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