Same audit as the orchestrator standardization pass (2a062e5), extended to
every remaining script. Found the same class of bug independently recurring:
ramdisk_stop.sh checked $LOG (nothing assigns it, should be $ENABLE_LOGGING),
partnership_onboard.sh checked $LOG_MODE (same issue), emby_session_report.sh
checked $TRANSCODE_PCT which was never computed so the high-transcode alert
could never fire, and storage_migrate.sh never called detect_hosts() so
$MY_ID was empty, silently breaking the post-migration host*.conf update.
partnership_manager.sh used `local` at top-level script scope (invalid outside
a function) and had two master.conf path references missing "Configurations/".
Along the way: several scripts (share_setup.sh, conf_sync.sh,
downloaders_reset.sh, transcode_cleanup.sh, transcode_manager.sh,
remote_arr_cache_writer.sh, upgrade_webhook_handler.sh) had no explicit
trailing exit code, so they always reported success regardless of real
failures. play_state_sync.sh was missing the partnership gate its own header
documented, so remote play-state sync ran even with PARTNERSHIP_ENABLED=false;
it also always exited 0 on sync errors. arr_profile_enforcer.sh and
webhook_setup.sh hand-rolled their own flag parsing instead of common.sh's
parse_args, so --log silently did nothing on either.
system_watchdog.sh was itself an un-standardized mini-orchestrator — converted
to the shared run_orch_child()/JOB_PASS/JOB_FAIL pattern, added the missing
failure notification, and fixed dry-run to pass --dry-run down to children
instead of skipping them outright. Also fixed a stale webgui_watchdog.sh path
in master.conf.template that would break system_watchdog.sh on any fresh
install.
Closed a sibling-drift gap: radarr_cleanup.sh and sonarr_cleanup.sh were
missing lidarr_cleanup.sh's tracked-count percentage-drop safety gate and its
"not configured on this host, skip cleanly" guard — both now match Lidarr's
7-gate model.
222 lines
8.5 KiB
Bash
Executable File
222 lines
8.5 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ========================= Arr Profile Enforcer ==============================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Ensures every series/movie in Sonarr and Radarr is on the correct quality
|
|
# profile based on its root folder. Safe to re-run — only touches items whose
|
|
# current profile is wrong.
|
|
#
|
|
# RULES
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Root folder path contains "kids" OR "anime" → kids profile
|
|
# All other root folders → default profile
|
|
#
|
|
# Profile names are looked up by name from the API at runtime, so profile IDs
|
|
# do not need to be hardcoded and work across hosts.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# Idempotent — only touches items whose current profile is wrong
|
|
# API-only — no file system changes; profile ID looked up by name at runtime
|
|
# --dry-run mode — reports what would change without applying any updates
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# master.conf
|
|
#
|
|
# ARR_KIDS_PROFILE_NAME — profile name for kids/anime (default: "Kids shows")
|
|
# ARR_SONARR_DEFAULT_PROFILE — default Sonarr profile name (default: "Any")
|
|
# ARR_RADARR_DEFAULT_PROFILE — default Radarr profile name (default: "Any (mine)")
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# arr_profile_enforcer.sh
|
|
# Enforce profiles in both Sonarr and Radarr.
|
|
#
|
|
# arr_profile_enforcer.sh --dry-run
|
|
# Show which items would be updated without making changes.
|
|
#
|
|
# arr_profile_enforcer.sh --sonarr-only
|
|
# Run only Sonarr enforcement.
|
|
#
|
|
# arr_profile_enforcer.sh --radarr-only
|
|
# Run only Radarr enforcement.
|
|
#
|
|
# arr_profile_enforcer.sh --log
|
|
# Verbose output.
|
|
#
|
|
# ==============================================================================================
|
|
|
|
RUN_SONARR=true
|
|
RUN_RADARR=true
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--sonarr-only) RUN_RADARR=false ;;
|
|
--radarr-only) RUN_SONARR=false ;;
|
|
esac
|
|
done
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$SCRIPT_DIR/../load_config.sh"
|
|
parse_args "$@"
|
|
detect_hosts
|
|
|
|
KIDS_PROFILE_NAME="${ARR_KIDS_PROFILE_NAME:-Kids shows}"
|
|
SONARR_DEFAULT="${ARR_SONARR_DEFAULT_PROFILE:-Any}"
|
|
RADARR_DEFAULT="${ARR_RADARR_DEFAULT_PROFILE:-Any (mine)}"
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
_arr_get() {
|
|
local url="$1" key="$2" endpoint="$3"
|
|
curl -sf --max-time 30 -H "X-Api-Key: $key" "$url/api/v3/$endpoint"
|
|
}
|
|
|
|
_arr_put() {
|
|
local url="$1" key="$2" endpoint="$3" body="$4"
|
|
curl -sf --max-time 60 -X PUT \
|
|
-H "X-Api-Key: $key" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$body" \
|
|
"$url/api/v3/$endpoint"
|
|
}
|
|
|
|
_profile_id_by_name() {
|
|
local profiles_json="$1" name="$2"
|
|
php -r '
|
|
$profiles = json_decode(file_get_contents("php://stdin"), true);
|
|
$name = $argv[1];
|
|
foreach ($profiles as $p) {
|
|
if (strcasecmp($p["name"], $name) === 0) { echo $p["id"]; exit; }
|
|
}
|
|
exit(1);
|
|
' "$name" <<< "$profiles_json"
|
|
}
|
|
|
|
_is_kids_path() {
|
|
local path="$1"
|
|
local dir
|
|
dir=$(basename "$(dirname "$path")")
|
|
[[ "$dir" == *kids* || "$dir" == *anime* ]]
|
|
}
|
|
|
|
# ── Core enforcer ─────────────────────────────────────────────────────────────
|
|
# _enforce <label> <url> <api_key> <items_endpoint> <id_field> <editor_endpoint>
|
|
# <kids_profile_id> <default_profile_id>
|
|
_enforce() {
|
|
local label="$1" url="$2" key="$3" items_ep="$4"
|
|
local id_field="$5" editor_ep="$6"
|
|
local kids_id="$7" default_id="$8"
|
|
|
|
local items
|
|
items=$(_arr_get "$url" "$key" "$items_ep") || {
|
|
error "[$label] Cannot reach $url"
|
|
return 1
|
|
}
|
|
|
|
local to_kids=() to_default=()
|
|
|
|
while IFS='|' read -r id profile_id path; do
|
|
if _is_kids_path "$path"; then
|
|
[[ "$profile_id" -ne "$kids_id" ]] && to_kids+=("$id")
|
|
else
|
|
[[ "$profile_id" -ne "$default_id" ]] && to_default+=("$id")
|
|
fi
|
|
done < <(php -r '
|
|
$items = json_decode(file_get_contents("php://stdin"), true);
|
|
foreach ($items as $r) echo $r["id"]."|".$r["qualityProfileId"]."|".$r["path"]."\n";
|
|
' <<< "$items")
|
|
|
|
local total=$(( ${#to_kids[@]} + ${#to_default[@]} ))
|
|
log "arr_profile_enforcer" "[$label] ${#to_kids[@]} → $KIDS_PROFILE_NAME | ${#to_default[@]} → default | $total to fix"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo " [$label] DRY RUN — would set $KIDS_PROFILE_NAME on ${#to_kids[@]}, default on ${#to_default[@]}"
|
|
return 0
|
|
fi
|
|
|
|
_bulk_update() {
|
|
local profile_id="$1" prof_label="$2"
|
|
shift 2
|
|
local ids=("$@")
|
|
[[ ${#ids[@]} -eq 0 ]] && return 0
|
|
|
|
local ids_json
|
|
ids_json=$(printf '%s,' "${ids[@]}")
|
|
ids_json="[${ids_json%,}]"
|
|
|
|
local result
|
|
result=$(_arr_put "$url" "$key" "$editor_ep" \
|
|
"{\"${id_field}\":${ids_json},\"qualityProfileId\":${profile_id}}") || {
|
|
error "[$label] Bulk update failed for $prof_label"
|
|
return 1
|
|
}
|
|
|
|
local updated
|
|
updated=$(php -r 'echo count(json_decode(file_get_contents("php://stdin"), true));' <<< "$result")
|
|
log "arr_profile_enforcer" "[$label] Set $prof_label on $updated items"
|
|
}
|
|
|
|
_bulk_update "$kids_id" "$KIDS_PROFILE_NAME" "${to_kids[@]+"${to_kids[@]}"}"
|
|
_bulk_update "$default_id" "default" "${to_default[@]+"${to_default[@]}"}"
|
|
}
|
|
|
|
# ── Sonarr ────────────────────────────────────────────────────────────────────
|
|
if [[ "$RUN_SONARR" == true ]]; then
|
|
require_var SONARR_URL
|
|
require_var SONARR_API_KEY
|
|
|
|
SONARR_PROFILES=$(_arr_get "$SONARR_URL" "$SONARR_API_KEY" "qualityprofile") || {
|
|
error "Cannot reach Sonarr at $SONARR_URL"
|
|
exit 1
|
|
}
|
|
|
|
SONARR_KIDS_ID=$(_profile_id_by_name "$SONARR_PROFILES" "$KIDS_PROFILE_NAME") || {
|
|
error "Sonarr profile not found: '$KIDS_PROFILE_NAME'"
|
|
exit 1
|
|
}
|
|
SONARR_DEFAULT_ID=$(_profile_id_by_name "$SONARR_PROFILES" "$SONARR_DEFAULT") || {
|
|
error "Sonarr profile not found: '$SONARR_DEFAULT'"
|
|
exit 1
|
|
}
|
|
|
|
_enforce "Sonarr" "$SONARR_URL" "$SONARR_API_KEY" \
|
|
"series" "seriesIds" "series/editor" \
|
|
"$SONARR_KIDS_ID" "$SONARR_DEFAULT_ID"
|
|
fi
|
|
|
|
# ── Radarr ────────────────────────────────────────────────────────────────────
|
|
if [[ "$RUN_RADARR" == true ]]; then
|
|
require_var RADARR_URL
|
|
require_var RADARR_API_KEY
|
|
|
|
RADARR_PROFILES=$(_arr_get "$RADARR_URL" "$RADARR_API_KEY" "qualityprofile") || {
|
|
error "Cannot reach Radarr at $RADARR_URL"
|
|
exit 1
|
|
}
|
|
|
|
RADARR_KIDS_ID=$(_profile_id_by_name "$RADARR_PROFILES" "$KIDS_PROFILE_NAME") || {
|
|
error "Radarr profile not found: '$KIDS_PROFILE_NAME'"
|
|
exit 1
|
|
}
|
|
RADARR_DEFAULT_ID=$(_profile_id_by_name "$RADARR_PROFILES" "$RADARR_DEFAULT") || {
|
|
error "Radarr profile not found: '$RADARR_DEFAULT'"
|
|
exit 1
|
|
}
|
|
|
|
_enforce "Radarr" "$RADARR_URL" "$RADARR_API_KEY" \
|
|
"movie" "movieIds" "movie/editor" \
|
|
"$RADARR_KIDS_ID" "$RADARR_DEFAULT_ID"
|
|
fi
|
|
|
|
log "arr_profile_enforcer" "Done"
|