#!/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 MODEL # ============================================================================================== # # Sonarr then Radarr, each independently: # # 1. Fetch quality profiles and resolve the configured names to live IDs # 2. Fetch the library # 3. Per item, derive the expected profile from its rootFolderPath # contains "kids" or "anime" → kids profile # anything else → default profile # 4. Compare against the item's current profile # already correct → skip, no call made # wrong → queue for the bulk editor PUT # 5. Issue the change and report counts # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Resolve Profile IDs at Runtime # Profiles are looked up by name from the API on every run rather than hardcoding IDs. # Profile IDs differ between hosts and change when profiles are recreated; a stale # hardcoded ID would silently assign the wrong profile rather than failing. # # Change Only What Is Wrong # Items already on the correct profile are never touched. That makes the tool safe to # re-run at any time and keeps the reported count meaningful — a non-zero result means # real drift, not just "it ran". # # Root Folder Is the Source of Truth # Classification comes from where the item actually lives, not from its metadata. The # classification scans own the harder question of whether an item is in the right root; # this tool just makes the profile agree with the answer already on disk. # # ============================================================================================== # 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 "$@" # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== # Consistent with the rest of Arrs_Stack/ and Tools/ — this issues bulk profile PUTs against # the arrs and should not be runnable by an unprivileged account that happens to hold a key. if [[ "$EUID" -ne 0 ]]; then error "Must be run as root" exit 1 fi # Bulk PUTs against the same items — two concurrent runs could interleave and leave a subset # of series/movies on the wrong profile. acquire_lock 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