lidarr_missing_art.sh's album-directory-map loop (both the cache-hit and live-fallback branches) and arr_profile_enforcer.sh's _is_kids_path() called dirname/basename once per item -- 127K+ tracks and ~4000 series/movies respectively, each call forking a subprocess. Measured: 0.39s vs 72.2s for 20K calls, ~185x. Verified identical output against real paths (including unicode/space/paren edge cases) before switching.
225 lines
8.8 KiB
Bash
Executable File
225 lines
8.8 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"
|
|
# Parameter expansion instead of external dirname+basename — called once per series/movie
|
|
# (~4000 items combined), each call was forking two subprocesses. Measured 2026-07-17:
|
|
# ~185x faster per equivalent call (0.39s vs 72.2s per 20K) for the identical result.
|
|
local parent="${path%/*}"
|
|
local dir="${parent##*/}"
|
|
[[ "$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"
|