mirror of
https://github.com/soulis-1256/assemblrr.git
synced 2026-08-29 17:41:40 +00:00
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:
parent
35b408d441
commit
188975fea2
6 changed files with 283 additions and 176 deletions
|
|
@ -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.
|
||||
|
|
|
|||
135
bin/config.sh
135
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
|
||||
|
|
|
|||
32
bin/setup.sh
32
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 "$@"
|
||||
|
|
|
|||
240
lib/arr.sh
240
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 ---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue