Files
Varaverk/Tools/arr_profile_enforcer.sh
T
Gmer4Lfe f92ee4064b Add full banner headers to all scripts across the codebase
Every script now has the established header format: PURPOSE with ─────── separator,
OPERATIONAL MODEL, DESIGN PRINCIPLES, OPERATIONAL SAFEGUARDS, CONFIGURATION, and
RUNTIME MODES — structured with full ====== banner sections throughout.

Orchestrators converted from compact ── inline format to full banners. Stale
emby-fallback and dirty sync references removed from Plugin/user_script_plug-in.sh.
2026-06-26 18:50:05 -04:00

220 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.
#
# ==============================================================================================
DRY_RUN=false
RUN_SONARR=true
RUN_RADARR=true
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--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"
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"