302 lines
15 KiB
Bash
Executable File
302 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ============================= Conf Auto-Populate =============================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Reads credentials and settings from locally running services and writes
|
|
# them into the local host conf. Safe to run multiple times — only populates
|
|
# EMPTY fields, never overwrites existing values unless --overwrite is passed.
|
|
#
|
|
# After populating, pushes the updated conf to all partners via conf_sync.sh
|
|
# so they have the fresh keys in their /tmp/.vv/ cache immediately.
|
|
#
|
|
# ==============================================================================================
|
|
# AUTO-DETECTED FIELDS
|
|
# ==============================================================================================
|
|
#
|
|
# HOSTN_OWNER from hostname (strip unRAID- prefix, lowercase)
|
|
# HOSTN_RADARR_API_KEY from Radarr config.xml (found via docker volume mount)
|
|
# HOSTN_RADARR_URL from Radarr config.xml port
|
|
# HOSTN_SONARR_API_KEY from Sonarr config.xml
|
|
# HOSTN_SONARR_URL from Sonarr config.xml port
|
|
# HOSTN_LIDARR_API_KEY from Lidarr config.xml
|
|
# HOSTN_LIDARR_URL from Lidarr config.xml port
|
|
# HOSTN_RADARR_MOVIE_ROOT from Radarr rootFolder API
|
|
# HOSTN_SONARR_TV_ROOT from Sonarr rootFolder API
|
|
# HOSTN_LIDARR_MUSIC_ROOT from Lidarr rootFolder API
|
|
# HOSTN_SABNZBD_API_KEY from sabnzbd.ini
|
|
# HOSTN_SABNZBD_URL from sabnzbd.ini port
|
|
# HOSTN_SLSKD_API_KEY from slskd config.yml
|
|
# HOSTN_SLSKD_URL from slskd config.yml port
|
|
# HOSTN_QBIT_URL from qBittorrent.conf WebUI port
|
|
# HOSTN_QBIT_USERNAME from qBittorrent.conf WebUI username
|
|
# HOSTN_QBIT_PASSWORD from qBittorrent.conf WebUI password (plaintext only)
|
|
# HOSTN_EMBY_CONTAINER fuzzy match from docker ps
|
|
# HOSTN_EMBY_URL from docker port binding
|
|
# HOSTN_JELLYFIN_CONTAINER fuzzy match from docker ps
|
|
# HOSTN_JELLYFIN_URL from docker port binding
|
|
# HOSTN_TRANSCODE_SSD from Emby/Jellyfin container /transcode volume mount
|
|
# HOSTN_SYS_WATCHDOG_NIC from ip route default gateway interface
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# conf_populate.sh Populate empty fields only
|
|
# conf_populate.sh --overwrite Overwrite all detected fields (re-sync after arr key rotation)
|
|
# conf_populate.sh --dry-run Show what would be written, no changes
|
|
# conf_populate.sh --log Verbose output
|
|
# conf_populate.sh --no-push Skip pushing to partners after update
|
|
#
|
|
# ==============================================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$SCRIPT_DIR/../../../load_config.sh"
|
|
SCRIPTS_ROOT="$SCRIPTS_DIR"
|
|
|
|
OVERWRITE=false
|
|
NO_PUSH=false
|
|
FILTERED_ARGS=()
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--overwrite) OVERWRITE=true ;;
|
|
--no-push) NO_PUSH=true ;;
|
|
*) FILTERED_ARGS+=("$arg") ;;
|
|
esac
|
|
done
|
|
parse_args "${FILTERED_ARGS[@]}"
|
|
detect_hosts
|
|
|
|
[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
|
|
|
|
CONF_FILE="$SCRIPTS_ROOT/Configurations/${MY_ID,,}.conf"
|
|
[[ ! -f "$CONF_FILE" ]] && { error "Conf file not found: $CONF_FILE"; exit 1; }
|
|
|
|
log "$ICON_GEAR Config: conf=${CONF_FILE} overwrite=${OVERWRITE:-false} no-push=${NO_PUSH:-false}"
|
|
|
|
UPDATED=0
|
|
SKIPPED=0
|
|
|
|
echo ""
|
|
echo "━━━ $ICON_GEAR Conf Auto-Populate — $MY_ID ($LOCAL_SERVER_NAME) ━━━"
|
|
echo ""
|
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
|
[[ "$OVERWRITE" == true ]] && warn "OVERWRITE mode — existing values will be replaced"
|
|
|
|
# ── Helper: write a var into conf if empty (or --overwrite) ──────────────────
|
|
_set_conf_var() {
|
|
local var_name="$1" value="$2" label="$3"
|
|
[[ -z "$value" ]] && return
|
|
|
|
local current
|
|
current=$(grep -oP "(?<=^\s*${var_name}=\")[^\"]*" "$CONF_FILE" 2>/dev/null | head -1)
|
|
|
|
if [[ -n "$current" ]] && [[ "$OVERWRITE" == false ]]; then
|
|
log "$label: already set (${current:0:8}…) — skipping"
|
|
(( SKIPPED++ ))
|
|
return
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would set $var_name = ${value:0:8}…"
|
|
return
|
|
fi
|
|
|
|
if grep -q "^\s*${var_name}=" "$CONF_FILE"; then
|
|
sed -i "s|^\(\s*${var_name}\s*=\s*\)\"[^\"]*\"|\1\"${value}\"|" "$CONF_FILE"
|
|
else
|
|
printf '\n %s="%s"\n' "$var_name" "$value" >> "$CONF_FILE"
|
|
fi
|
|
info "$label: set ✅"
|
|
(( UPDATED++ ))
|
|
}
|
|
|
|
# ── Helper: find arr config dir via docker volume mount ───────────────────────
|
|
_arr_config_dir() {
|
|
local pattern="$1"
|
|
local container_name
|
|
container_name=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
|
|
[[ -z "$container_name" ]] && return 1
|
|
|
|
local config_path
|
|
config_path=$(docker inspect "$container_name" 2>/dev/null | \
|
|
jq -r '.[0].Mounts[]? | select(.Destination == "/config") | .Source' 2>/dev/null | head -1)
|
|
[[ -z "$config_path" ]] && config_path="${DOCKER_APPDATA_BASE:-/mnt/user/appdata}/${container_name}"
|
|
|
|
[[ -d "$config_path" ]] && echo "$config_path" || return 1
|
|
}
|
|
|
|
# ── Helper: read XML tag value ────────────────────────────────────────────────
|
|
_xml_val() {
|
|
local file="$1" tag="$2"
|
|
grep -oP "(?<=<${tag}>)[^<]+" "$file" 2>/dev/null | head -1
|
|
}
|
|
|
|
# ── Helper: get host-side port for a container's internal port ────────────────
|
|
_docker_host_port() {
|
|
local container="$1" container_port="$2"
|
|
docker inspect "$container" 2>/dev/null | \
|
|
jq -r --arg p "${container_port}/tcp" \
|
|
'.[0].NetworkSettings.Ports[$p]?[0].HostPort // empty' 2>/dev/null | head -1
|
|
}
|
|
|
|
# ── Helper: get host path for a container destination mount ──────────────────
|
|
_docker_volume_host() {
|
|
local container="$1" dest="$2"
|
|
docker inspect "$container" 2>/dev/null | \
|
|
jq -r --arg d "$dest" \
|
|
'.[0].Mounts[]? | select(.Destination == $d) | .Source' 2>/dev/null | head -1
|
|
}
|
|
|
|
# ==============================================================================================
|
|
# ── Owner short name ──────────────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
owner=$(echo "$LOCAL_SERVER_NAME" | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//i' | tr '[:upper:]' '[:lower:]')
|
|
_set_conf_var "${MY_ID}_OWNER" "$owner" "Owner short name"
|
|
|
|
# ==============================================================================================
|
|
# ── Arr API keys + URLs + root paths ─────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
for arr in radarr sonarr lidarr; do
|
|
arr_upper="${arr^^}"
|
|
config_dir=$(_arr_config_dir "$arr") || {
|
|
log "${arr_upper}: no running container found — skipping"
|
|
continue
|
|
}
|
|
config_xml="${config_dir}/config.xml"
|
|
|
|
if [[ ! -f "$config_xml" ]]; then
|
|
log "${arr_upper}: config.xml not found at $config_xml — skipping"
|
|
continue
|
|
fi
|
|
|
|
key=$(_xml_val "$config_xml" "ApiKey")
|
|
port=$(_xml_val "$config_xml" "Port")
|
|
port="${port:-$(case $arr in radarr) echo 7878;; sonarr) echo 8989;; lidarr) echo 8686;; esac)}"
|
|
url_base="http://localhost:${port}"
|
|
|
|
_set_conf_var "${MY_ID}_${arr_upper}_API_KEY" "$key" "${arr_upper} API key"
|
|
_set_conf_var "${MY_ID}_${arr_upper}_URL" "$url_base" "${arr_upper} URL"
|
|
|
|
if [[ -n "$key" ]]; then
|
|
case "$arr" in lidarr) api_ver="v1" ;; *) api_ver="v3" ;; esac
|
|
root_json=$(curl -sf --max-time 5 \
|
|
-H "X-Api-Key: $key" "${url_base}/api/${api_ver}/rootfolder" 2>/dev/null)
|
|
root_path=$(echo "$root_json" | jq -r '.[0].path // empty' 2>/dev/null)
|
|
|
|
case "$arr" in
|
|
radarr) _set_conf_var "${MY_ID}_RADARR_MOVIE_ROOT" "$root_path" "Radarr movie root" ;;
|
|
sonarr) _set_conf_var "${MY_ID}_SONARR_TV_ROOT" "$root_path" "Sonarr TV root" ;;
|
|
lidarr) _set_conf_var "${MY_ID}_LIDARR_MUSIC_ROOT" "$root_path" "Lidarr music root" ;;
|
|
esac
|
|
fi
|
|
done
|
|
|
|
# ==============================================================================================
|
|
# ── SABnzbd API key + URL ─────────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
sab_dir=$(_arr_config_dir "sabnzbd") && {
|
|
sab_ini=$(find "$sab_dir" -maxdepth 2 -name "sabnzbd.ini" 2>/dev/null | head -1)
|
|
if [[ -f "$sab_ini" ]]; then
|
|
sab_key=$(grep -oP '(?<=^api_key\s*=\s*)\S+' "$sab_ini" 2>/dev/null | head -1)
|
|
sab_port=$(grep -oP '(?<=^port\s*=\s*)\d+' "$sab_ini" 2>/dev/null | head -1)
|
|
_set_conf_var "${MY_ID}_SABNZBD_API_KEY" "$sab_key" "SABnzbd API key"
|
|
_set_conf_var "${MY_ID}_SABNZBD_URL" "http://localhost:${sab_port:-8080}" "SABnzbd URL"
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================================
|
|
# ── slskd API key + URL ───────────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
slskd_dir=$(_arr_config_dir "slskd") && {
|
|
slskd_yml=$(find "$slskd_dir" -maxdepth 2 \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | head -1)
|
|
if [[ -f "$slskd_yml" ]]; then
|
|
slskd_key=$(grep -oP '(?<=api_key:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
|
[[ -z "$slskd_key" ]] && \
|
|
slskd_key=$(grep -oP '(?<=apikey:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
|
slskd_port=$(grep -oP '(?<=port:\s)\d+' "$slskd_yml" 2>/dev/null | head -1)
|
|
_set_conf_var "${MY_ID}_SLSKD_API_KEY" "$slskd_key" "slskd API key"
|
|
_set_conf_var "${MY_ID}_SLSKD_URL" "http://localhost:${slskd_port:-5030}" "slskd URL"
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================================
|
|
# ── qBittorrent URL + credentials ────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
qbit_dir=$(_arr_config_dir "qbittorrent") && {
|
|
qbit_conf=$(find "$qbit_dir" -maxdepth 3 -name "qBittorrent.conf" 2>/dev/null | head -1)
|
|
if [[ -f "$qbit_conf" ]]; then
|
|
qbit_port=$(grep -oP '(?<=WebUI\\Port=)\d+' "$qbit_conf" 2>/dev/null | head -1)
|
|
qbit_user=$(grep -oP '(?<=WebUI\\Username=)\S+' "$qbit_conf" 2>/dev/null | head -1)
|
|
# Only capture plaintext password — PBKDF2 hashes are not usable
|
|
qbit_pass=$(grep -oP '(?<=WebUI\\Password=)[^\r\n]+' "$qbit_conf" 2>/dev/null | \
|
|
grep -v '@ByteArray' | head -1)
|
|
_set_conf_var "${MY_ID}_QBIT_URL" "http://localhost:${qbit_port:-8080}" "qBittorrent URL"
|
|
_set_conf_var "${MY_ID}_QBIT_USERNAME" "$qbit_user" "qBittorrent username"
|
|
_set_conf_var "${MY_ID}_QBIT_PASSWORD" "$qbit_pass" "qBittorrent password"
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================================
|
|
# ── Media server container names + URLs + transcode path ─────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
transcode_dir=""
|
|
|
|
for pattern in "emby" "jellyfin"; do
|
|
container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
|
|
[[ -z "$container" ]] && continue
|
|
|
|
case "$pattern" in
|
|
emby)
|
|
host_port=$(_docker_host_port "$container" "8096")
|
|
_set_conf_var "${MY_ID}_EMBY_CONTAINER" "$container" "Emby container name"
|
|
_set_conf_var "${MY_ID}_EMBY_URL" "http://localhost:${host_port:-8096}" "Emby URL"
|
|
;;
|
|
jellyfin)
|
|
host_port=$(_docker_host_port "$container" "8096")
|
|
_set_conf_var "${MY_ID}_JELLYFIN_CONTAINER" "$container" "Jellyfin container name"
|
|
_set_conf_var "${MY_ID}_JELLYFIN_URL" "http://localhost:${host_port:-8095}" "Jellyfin URL"
|
|
;;
|
|
esac
|
|
|
|
# Transcode path: first container with a /transcode mount wins
|
|
if [[ -z "$transcode_dir" ]]; then
|
|
transcode_dir=$(_docker_volume_host "$container" "/transcode")
|
|
fi
|
|
done
|
|
|
|
[[ -n "$transcode_dir" ]] && \
|
|
_set_conf_var "${MY_ID}_TRANSCODE_SSD" "${transcode_dir%/}/" "Transcode SSD path"
|
|
|
|
# ==============================================================================================
|
|
# ── Network interface ─────────────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
nic=$(ip route show default 2>/dev/null | grep -oP '(?<=dev )\S+' | head -1)
|
|
_set_conf_var "${MY_ID}_SYS_WATCHDOG_NIC" "$nic" "Default NIC"
|
|
|
|
# ==============================================================================================
|
|
# ── Summary + push ────────────────────────────────────────────────────────────────────────────
|
|
# ==============================================================================================
|
|
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY Conf Populate Summary ━━━━━"
|
|
echo " Updated: $UPDATED field(s)"
|
|
echo " Skipped: $SKIPPED already set"
|
|
echo " Conf: $CONF_FILE"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
if [[ "$UPDATED" -gt 0 ]] && [[ "$DRY_RUN" == false ]] && [[ "$NO_PUSH" == false ]]; then
|
|
echo ""
|
|
info "Pushing updated conf to partners..."
|
|
bash "$SCRIPTS_ROOT/System_Essentials/conf_sync.sh" --push-only "${EXTRA_FLAGS[@]}" || true
|
|
fi
|