diff --git a/Docker_Essentials/docker_daily_restart.sh b/Docker_Essentials/docker_daily_restart.sh index b862b78..558274d 100755 --- a/Docker_Essentials/docker_daily_restart.sh +++ b/Docker_Essentials/docker_daily_restart.sh @@ -152,35 +152,7 @@ fi # ── FUNCTIONS ───────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -# Wraps docker commands with a 30 second timeout. -# Prevents a hung Docker daemon from causing the script to hang indefinitely. -# Usage: docker_cmd docker restart ContainerName -DOCKER_TIMEOUT=30 -docker_cmd() { - timeout "$DOCKER_TIMEOUT" "$@" - local exit_code=$? - if [[ "$exit_code" -eq 124 ]]; then - error "Docker command timed out after ${DOCKER_TIMEOUT}s: $*" - return 1 - fi - return "$exit_code" -} - -# Verifies a container is still running after restart. -# Gives the container a short settle period before checking. -# Returns 0 if running, 1 if crashed or stopped. -RESTART_VERIFY_WAIT=5 # seconds to wait before checking state post-restart -verify_running() { - local container="$1" - sleep "$RESTART_VERIFY_WAIT" - local state - state=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) - if [[ "$state" != "true" ]]; then - error "$container failed to stay running after restart — may have crashed" - return 1 - fi - return 0 -} +# docker_cmd, verify_running, retry_docker — defined in common.sh # Builds a dependency-safe restart order from DAILY_RESTART_CONTAINERS. # Containers that are dependencies of others restart first. @@ -247,28 +219,6 @@ check_dependency_delay() { fi } -# Retries a docker command up to RETRY_COUNT times with SLEEP seconds between attempts. -# Uses docker_cmd wrapper for timeout protection on each attempt. -# Usage: retry_docker docker restart ContainerName -retry_docker() { - local attempt=1 - - while [[ "$attempt" -le "$RETRY_COUNT" ]]; do - log "$ICON_RETRY Attempt $attempt of $RETRY_COUNT: $*" - - if docker_cmd "$@"; then - log "Succeeded on attempt $attempt" - return 0 - else - warn "Attempt $attempt failed" - (( attempt++ )) - [[ "$attempt" -le "$RETRY_COUNT" ]] && sleep "$SLEEP" - fi - done - - error "Command failed after $RETRY_COUNT attempts: $*" - return 1 -} # ============================================================================================== # ━━━ Daily Restart ━━━ diff --git a/Docker_Essentials/docker_update_remaining.sh b/Docker_Essentials/docker_update_remaining.sh index 80bf32d..51de1c3 100755 --- a/Docker_Essentials/docker_update_remaining.sh +++ b/Docker_Essentials/docker_update_remaining.sh @@ -151,42 +151,7 @@ fi # ── FUNCTIONS ───────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -DOCKER_TIMEOUT=30 -docker_cmd() { - timeout "$DOCKER_TIMEOUT" "$@" - local exit_code=$? - if [[ "$exit_code" -eq 124 ]]; then - error "Docker command timed out after ${DOCKER_TIMEOUT}s: $*" - return 1 - fi - return "$exit_code" -} - -retry_docker() { - local attempt=1 - while [[ "$attempt" -le "$RETRY_COUNT" ]]; do - log "$ICON_RETRY Attempt $attempt of $RETRY_COUNT: $*" - if docker_cmd "$@"; then - log "Succeeded on attempt $attempt" - return 0 - else - warn "Attempt $attempt failed" - (( attempt++ )) - [[ "$attempt" -le "$RETRY_COUNT" ]] && sleep "$SLEEP" - fi - done - error "Command failed after $RETRY_COUNT attempts: $*" - return 1 -} - -RESTART_VERIFY_WAIT=5 -verify_running() { - local container="$1" - sleep "$RESTART_VERIFY_WAIT" - local state - state=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) - [[ "$state" == "true" ]] -} +# docker_cmd, retry_docker, verify_running — defined in common.sh # ============================================================================================== # ━━━ Pull Updates ━━━ diff --git a/Docker_Essentials/docker_weekly_restart.sh b/Docker_Essentials/docker_weekly_restart.sh index 57ff33c..8d11117 100755 --- a/Docker_Essentials/docker_weekly_restart.sh +++ b/Docker_Essentials/docker_weekly_restart.sh @@ -136,55 +136,7 @@ fi # ── FUNCTIONS ───────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -# Wraps docker commands with a 30 second timeout. -# Prevents a hung Docker daemon from causing the script to hang indefinitely. -DOCKER_TIMEOUT=30 -docker_cmd() { - timeout "$DOCKER_TIMEOUT" "$@" - local exit_code=$? - if [[ "$exit_code" -eq 124 ]]; then - error "Docker command timed out after ${DOCKER_TIMEOUT}s: $*" - return 1 - fi - return "$exit_code" -} - -# Retries a docker command up to RETRY_COUNT times with SLEEP seconds between attempts. -# Uses docker_cmd wrapper for timeout protection on each attempt. -retry_docker() { - local attempt=1 - - while [[ "$attempt" -le "$RETRY_COUNT" ]]; do - log "$ICON_RETRY Attempt $attempt of $RETRY_COUNT: $*" - - if docker_cmd "$@"; then - log "Succeeded on attempt $attempt" - return 0 - else - warn "Attempt $attempt failed" - (( attempt++ )) - [[ "$attempt" -le "$RETRY_COUNT" ]] && sleep "$SLEEP" - fi - done - - error "Command failed after $RETRY_COUNT attempts: $*" - return 1 -} - -# Verifies a container is still running after restart. -# Gives the container a short settle period before checking. -RESTART_VERIFY_WAIT=5 -verify_running() { - local container="$1" - sleep "$RESTART_VERIFY_WAIT" - local state - state=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) - if [[ "$state" != "true" ]]; then - error "$container failed to stay running after restart — may have crashed" - return 1 - fi - return 0 -} +# docker_cmd, retry_docker, verify_running — defined in common.sh # Builds a dependency-safe restart order from WEEKLY_RESTART_CONTAINERS. # Containers that are dependencies of others restart first. diff --git a/Media/arr_sync.sh b/Media/arr_sync.sh index 3bb8f90..06e3a88 100755 --- a/Media/arr_sync.sh +++ b/Media/arr_sync.sh @@ -315,12 +315,8 @@ _delete_remote_item() { local node_id="$1" port="$2" api_ver="$3" endpoint="$4" config_xml="$5" local id_field="$6" id_type="$7" stable_id="$8" local node_name="${!node_id}" - local ts_name="${node_name,,}" local node_ip - node_ip=$(tailscale ip -4 "$ts_name" 2>/dev/null) - [[ -z "$node_ip" ]] && node_ip=$(tailscale status 2>/dev/null | \ - awk -v n="$ts_name" '$2 ~ "^" n { print $1; exit }') - [[ -z "$node_ip" ]] && return 1 + node_ip=$(resolve_tailscale_ip "$node_name") || return 1 local select_expr if [[ "$id_type" == "string" ]]; then @@ -478,14 +474,7 @@ fi _resolve_node_ip() { local node_id="$1" - local node_name="${!node_id}" - local ts_name="${node_name,,}" - local ip - ip=$(tailscale ip -4 "$ts_name" 2>/dev/null) - [[ -z "$ip" ]] && ip=$(tailscale status 2>/dev/null | \ - awk -v n="$ts_name" '$2 ~ "^" n { print $1; exit }') - [[ -z "$ip" ]] && return 1 - echo "$ip" + resolve_tailscale_ip "${!node_id}" } # Check if arr is reachable on remote node diff --git a/Media/playback_aware_lidarr_discovery.sh b/Media/playback_aware_lidarr_discovery.sh index c1454c9..b201f55 100755 --- a/Media/playback_aware_lidarr_discovery.sh +++ b/Media/playback_aware_lidarr_discovery.sh @@ -186,18 +186,6 @@ fi # ── API HELPERS ─────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -_emby_api() { - local endpoint="$1" - local response http_code body - response=$(curl -sf --max-time 15 \ - -H "X-Emby-Token: $EMBY_API_KEY" \ - -w "\n%{http_code}" \ - "${EMBY_URL}/${endpoint}" 2>/dev/null) - http_code=$(echo "$response" | tail -1) - body=$(echo "$response" | head -n -1) - [[ "$http_code" != "200" ]] && { error "Emby API HTTP $http_code: $endpoint"; return 1; } - echo "$body" -} _lidarr_get() { curl -sf --max-time 20 \ @@ -329,7 +317,7 @@ CUTOFF_ISO=$(date -d "${LOOKBACK_DAYS} days ago" '+%Y-%m-%dT%H:%M:%SZ') TODAY_EPOCH=$(date +%s) TODAY=$(date +%Y-%m-%d) -ACTIVITY_JSON=$(_emby_api "System/ActivityLog/Entries?MinDate=${CUTOFF_ISO}&Limit=5000") || { +ACTIVITY_JSON=$(emby_api "System/ActivityLog/Entries?MinDate=${CUTOFF_ISO}&Limit=5000") || { error "Could not fetch Emby activity log" exit 1 } @@ -512,7 +500,7 @@ LIDARR_NAMES=$(echo "$LIDARR_ARTISTS_JSON" | jq -r '.[].artistName' 2>/dev/null) LIDARR_COUNT=$(echo "$LIDARR_NAMES" | grep -c . 2>/dev/null || echo 0) log "$LIDARR_COUNT artists in Lidarr" -EMBY_LIBRARY_JSON=$(_emby_api "Items?IncludeItemTypes=MusicAlbum&Recursive=true&Fields=AlbumArtists&Limit=10000") || { +EMBY_LIBRARY_JSON=$(emby_api "Items?IncludeItemTypes=MusicAlbum&Recursive=true&Fields=AlbumArtists&Limit=10000") || { warn "Could not fetch Emby artist library — skipping Emby filter" EMBY_ARTIST_NAMES="" } diff --git a/Media/playback_aware_radarr_discovery.sh b/Media/playback_aware_radarr_discovery.sh index 3336c32..78f219f 100755 --- a/Media/playback_aware_radarr_discovery.sh +++ b/Media/playback_aware_radarr_discovery.sh @@ -194,17 +194,6 @@ fi # ── API HELPERS ─────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -_emby_api() { - local response http_code body - response=$(curl -sf --max-time 20 \ - -H "X-Emby-Token: $EMBY_API_KEY" \ - -w "\n%{http_code}" \ - "${EMBY_URL}/${1}" 2>/dev/null) - http_code=$(echo "$response" | tail -1) - body=$(echo "$response" | head -n -1) - [[ "$http_code" != "200" ]] && { error "Emby API HTTP $http_code: $1"; return 1; } - echo "$body" -} _radarr_get() { curl -sf --max-time 20 \ @@ -306,7 +295,7 @@ echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) | lookback: ${LOOKBACK_DAYS}d | thr echo "" echo "━━━ $ICON_SYNC Emby Watch History ━━━" -LIBRARIES_JSON=$(_emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; } +LIBRARIES_JSON=$(emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; } CUTOFF_ISO=$(date -d "${LOOKBACK_DAYS} days ago" '+%Y-%m-%dT%H:%M:%SZ') TODAY_EPOCH=$(date +%s) TODAY=$(date +%Y-%m-%d) @@ -321,7 +310,7 @@ for lib_name in "${SEED_LIBRARIES[@]}"; do continue fi log "Indexing library: $lib_name (ItemId: $lib_id)" - LIB_JSON=$(_emby_api "Items?ParentId=${lib_id}&IncludeItemTypes=Movie&Recursive=true&Fields=ProviderIds&Limit=10000") || { + LIB_JSON=$(emby_api "Items?ParentId=${lib_id}&IncludeItemTypes=Movie&Recursive=true&Fields=ProviderIds&Limit=10000") || { warn "Could not index library: $lib_name" continue } @@ -335,7 +324,7 @@ log "${#EMBY_TMDB_IDS[@]} movies in Emby TMDB index" # Each entry includes an ItemId — batch-fetch those items to determine type (Movie vs. Music/TV). # SortBy=DatePlayed on Items requires UserId context and errors without one; the activity log # is server-scoped and doesn't have that limitation. -ACTIVITY_JSON=$(_emby_api "System/ActivityLog/Entries?MinDate=${CUTOFF_ISO}&Limit=5000") || { +ACTIVITY_JSON=$(emby_api "System/ActivityLog/Entries?MinDate=${CUTOFF_ISO}&Limit=5000") || { error "Could not fetch Emby activity log" exit 1 } @@ -366,7 +355,7 @@ if [[ "${#ITEM_PLAYS[@]}" -gt 0 ]]; then for (( _b=0; _b<${#ALL_ITEM_IDS[@]}; _b+=BATCH_SIZE )); do BATCH=("${ALL_ITEM_IDS[@]:_b:BATCH_SIZE}") IDS_CSV=$(printf '%s,' "${BATCH[@]}"); IDS_CSV="${IDS_CSV%,}" - ITEMS_DETAIL=$(_emby_api "Items?Ids=${IDS_CSV}&Fields=ProviderIds,Type&Limit=200") || { + ITEMS_DETAIL=$(emby_api "Items?Ids=${IDS_CSV}&Fields=ProviderIds,Type&Limit=200") || { warn "Could not fetch item batch starting at $_b" continue } diff --git a/Media/playback_aware_sonarr_discovery.sh b/Media/playback_aware_sonarr_discovery.sh index 50fa9b8..d14e142 100755 --- a/Media/playback_aware_sonarr_discovery.sh +++ b/Media/playback_aware_sonarr_discovery.sh @@ -216,17 +216,6 @@ fi # ── API HELPERS ─────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -_emby_api() { - local response http_code body - response=$(curl -sf --max-time 30 \ - -H "X-Emby-Token: $EMBY_API_KEY" \ - -w "\n%{http_code}" \ - "${EMBY_URL}/${1}" 2>/dev/null) - http_code=$(echo "$response" | tail -1) - body=$(echo "$response" | head -n -1) - [[ "$http_code" != "200" ]] && { error "Emby API HTTP $http_code: $1"; return 1; } - echo "$body" -} _sonarr_get() { curl -sf --max-time 30 \ @@ -348,7 +337,7 @@ echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) | lookback: ${LOOKBACK_DAYS}d | thr echo "" echo "━━━ $ICON_SYNC Emby Series Library ━━━" -LIBRARIES_JSON=$(_emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; } +LIBRARIES_JSON=$(emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; } CUTOFF_ISO=$(date -d "${LOOKBACK_DAYS} days ago" '+%Y-%m-%dT%H:%M:%SZ') TODAY_EPOCH=$(date +%s) TODAY=$(date +%Y-%m-%d) @@ -369,7 +358,7 @@ for lib_name in "${SONARR_EMBY_LIBRARIES[@]}"; do continue fi log "Scanning library: $lib_name (ItemId: $lib_id)" - LIB_JSON=$(_emby_api "Items?ParentId=${lib_id}&IncludeItemTypes=Series&Recursive=true&Fields=ProviderIds&Limit=5000") || { + LIB_JSON=$(emby_api "Items?ParentId=${lib_id}&IncludeItemTypes=Series&Recursive=true&Fields=ProviderIds&Limit=5000") || { warn "Could not fetch series from library: $lib_name" continue } @@ -404,7 +393,7 @@ echo "━━━ $ICON_SYNC Emby Watch History ━━━" declare -A ITEM_USER_PLAYS # "episode_item_id|user_id" → play count declare -A ITEM_LAST_PLAY # episode_item_id → ISO date of most recent play -ACTIVITY_JSON=$(_emby_api "System/ActivityLog/Entries?MinDate=${CUTOFF_ISO}&Limit=5000") || { +ACTIVITY_JSON=$(emby_api "System/ActivityLog/Entries?MinDate=${CUTOFF_ISO}&Limit=5000") || { error "Could not fetch Emby activity log" exit 1 } @@ -448,7 +437,7 @@ if [[ "${#ALL_EPISODE_IDS[@]}" -gt 0 ]]; then for (( _b=0; _b<${#ALL_EPISODE_IDS[@]}; _b+=BATCH_SIZE )); do BATCH=("${ALL_EPISODE_IDS[@]:_b:BATCH_SIZE}") IDS_CSV=$(printf '%s,' "${BATCH[@]}"); IDS_CSV="${IDS_CSV%,}" - ITEMS_DETAIL=$(_emby_api "Items?Ids=${IDS_CSV}&Fields=SeriesId,Type&Limit=200") || { + ITEMS_DETAIL=$(emby_api "Items?Ids=${IDS_CSV}&Fields=SeriesId,Type&Limit=200") || { warn "Could not fetch episode batch starting at $_b" continue } diff --git a/Tools/emby_to_lidarr_sync.sh b/Tools/emby_to_lidarr_sync.sh index a09c601..ab85652 100755 --- a/Tools/emby_to_lidarr_sync.sh +++ b/Tools/emby_to_lidarr_sync.sh @@ -89,18 +89,6 @@ fi # ── API HELPERS ─────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -_emby_api() { - local endpoint="$1" - local response http_code body - response=$(curl -sf --max-time 30 \ - -H "X-Emby-Token: $EMBY_API_KEY" \ - -w "\n%{http_code}" \ - "${EMBY_URL}/${endpoint}" 2>/dev/null) - http_code=$(echo "$response" | tail -1) - body=$(echo "$response" | head -n -1) - [[ "$http_code" != "200" ]] && { error "Emby API HTTP $http_code: $endpoint"; return 1; } - echo "$body" -} _lidarr_get() { curl -sf --max-time 30 \ @@ -163,7 +151,7 @@ echo "━━━ $ICON_SYNC Emby Music Library ━━━" # Query MusicAlbum items and extract AlbumArtists — gives primary album artists only, # not the full tag credits (guest features, songwriters, etc.) that MusicArtist returns -EMBY_ALBUMS_JSON=$(_emby_api "Items?IncludeItemTypes=MusicAlbum&Recursive=true&Fields=AlbumArtists&Limit=10000") || { +EMBY_ALBUMS_JSON=$(emby_api "Items?IncludeItemTypes=MusicAlbum&Recursive=true&Fields=AlbumArtists&Limit=10000") || { error "Could not fetch Emby music albums" exit 1 } diff --git a/Tools/emby_to_radarr_sync.sh b/Tools/emby_to_radarr_sync.sh index 119f279..0744d3c 100755 --- a/Tools/emby_to_radarr_sync.sh +++ b/Tools/emby_to_radarr_sync.sh @@ -90,17 +90,6 @@ fi # ── API HELPERS ─────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -_emby_api() { - local response http_code body - response=$(curl -sf --max-time 30 \ - -H "X-Emby-Token: $EMBY_API_KEY" \ - -w "\n%{http_code}" \ - "${EMBY_URL}/${1}" 2>/dev/null) - http_code=$(echo "$response" | tail -1) - body=$(echo "$response" | head -n -1) - [[ "$http_code" != "200" ]] && { error "Emby API HTTP $http_code: $1"; return 1; } - echo "$body" -} _radarr_get() { curl -sf --max-time 30 \ @@ -160,7 +149,7 @@ echo "━━━ $ICON_SYNC Emby Movie Library ━━━" declare -A EMBY_MOVIES # name → tmdb_id (empty string if none) if [[ "${#RADARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then - LIBRARIES_JSON=$(_emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; } + LIBRARIES_JSON=$(emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; } for lib_name in "${RADARR_EMBY_LIBRARIES[@]}"; do lib_id=$(echo "$LIBRARIES_JSON" | jq -r --arg n "$lib_name" '.[] | select(.Name == $n) | .ItemId' 2>/dev/null) if [[ -z "$lib_id" ]]; then @@ -168,7 +157,7 @@ if [[ "${#RADARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then continue fi log "Scanning library: $lib_name (ItemId: $lib_id)" - LIB_JSON=$(_emby_api "Items?ParentId=${lib_id}&IncludeItemTypes=Movie&Recursive=true&Fields=ProviderIds&Limit=5000") || { + LIB_JSON=$(emby_api "Items?ParentId=${lib_id}&IncludeItemTypes=Movie&Recursive=true&Fields=ProviderIds&Limit=5000") || { warn "Could not fetch movies from library: $lib_name" continue } @@ -180,7 +169,7 @@ if [[ "${#RADARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then done else log "RADARR_EMBY_LIBRARIES empty — scanning all Emby libraries" - EMBY_MOVIES_JSON=$(_emby_api "Items?IncludeItemTypes=Movie&Recursive=true&Fields=ProviderIds&Limit=10000") || { + EMBY_MOVIES_JSON=$(emby_api "Items?IncludeItemTypes=Movie&Recursive=true&Fields=ProviderIds&Limit=10000") || { error "Could not fetch Emby movies" exit 1 } diff --git a/Tools/emby_to_sonarr_sync.sh b/Tools/emby_to_sonarr_sync.sh index 163087a..19fd3d5 100755 --- a/Tools/emby_to_sonarr_sync.sh +++ b/Tools/emby_to_sonarr_sync.sh @@ -90,17 +90,6 @@ fi # ── API HELPERS ─────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -_emby_api() { - local response http_code body - response=$(curl -sf --max-time 30 \ - -H "X-Emby-Token: $EMBY_API_KEY" \ - -w "\n%{http_code}" \ - "${EMBY_URL}/${1}" 2>/dev/null) - http_code=$(echo "$response" | tail -1) - body=$(echo "$response" | head -n -1) - [[ "$http_code" != "200" ]] && { error "Emby API HTTP $http_code: $1"; return 1; } - echo "$body" -} _sonarr_get() { curl -sf --max-time 30 \ @@ -154,7 +143,7 @@ echo "━━━ $ICON_SYNC Emby Series Library ━━━" declare -A EMBY_SERIES # name → tvdb_id (empty string if none) if [[ "${#SONARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then - LIBRARIES_JSON=$(_emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; } + LIBRARIES_JSON=$(emby_api "Library/VirtualFolders") || { error "Could not fetch Emby libraries"; exit 1; } for lib_name in "${SONARR_EMBY_LIBRARIES[@]}"; do lib_id=$(echo "$LIBRARIES_JSON" | jq -r --arg n "$lib_name" '.[] | select(.Name == $n) | .ItemId' 2>/dev/null) if [[ -z "$lib_id" ]]; then @@ -162,7 +151,7 @@ if [[ "${#SONARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then continue fi log "Scanning library: $lib_name (ItemId: $lib_id)" - LIB_JSON=$(_emby_api "Items?ParentId=${lib_id}&IncludeItemTypes=Series&Recursive=true&Fields=ProviderIds&Limit=5000") || { + LIB_JSON=$(emby_api "Items?ParentId=${lib_id}&IncludeItemTypes=Series&Recursive=true&Fields=ProviderIds&Limit=5000") || { warn "Could not fetch series from library: $lib_name" continue } @@ -174,7 +163,7 @@ if [[ "${#SONARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then done else log "SONARR_EMBY_LIBRARIES empty — scanning all Emby libraries" - ALL_JSON=$(_emby_api "Items?IncludeItemTypes=Series&Recursive=true&Fields=ProviderIds&Limit=5000") || { + ALL_JSON=$(emby_api "Items?IncludeItemTypes=Series&Recursive=true&Fields=ProviderIds&Limit=5000") || { error "Could not fetch Emby series" exit 1 } diff --git a/common.sh b/common.sh index e8a3f7a..d842801 100755 --- a/common.sh +++ b/common.sh @@ -228,6 +228,7 @@ notify() { log "$ICON_NOTIFY Sending notification: $subject — $message" + # Guard is built in — scripts do NOT need to validate_unraid_cmd for the notify script. if [[ "${NOTIFY_UNRAID:-false}" == true ]]; then local notify_script="/usr/local/emhttp/plugins/dynamix/scripts/notify" if [[ -x "$notify_script" ]]; then @@ -261,9 +262,14 @@ notify() { format_duration() { local secs=$1 - local mins=$((secs / 60)) - local rem=$((secs % 60)) - [[ $mins -gt 0 ]] && echo "${mins}m${rem}s" || echo "${rem}s" + local days=$(( secs / 86400 )) + local hrs=$(( (secs % 86400) / 3600 )) + local mins=$(( (secs % 3600) / 60 )) + local rem=$(( secs % 60 )) + if [[ $days -gt 0 ]]; then echo "${days}d ${hrs}h ${mins}m" + elif [[ $hrs -gt 0 ]]; then echo "${hrs}h ${mins}m" + elif [[ $mins -gt 0 ]]; then echo "${mins}m${rem}s" + else echo "${rem}s"; fi } # ============================================================================================== @@ -1102,6 +1108,72 @@ check_remote_disks() { # Tracks which remote containers were running before stop — used by start_containers() RUNNING_CONTAINERS=() +# ── Docker command helpers (used by docker_daily_restart, docker_weekly_restart, docker_update) ── + +# Default timeouts — scripts can override before calling. +DOCKER_TIMEOUT=${DOCKER_TIMEOUT:-30} # seconds before docker command is killed +RESTART_VERIFY_WAIT=${RESTART_VERIFY_WAIT:-5} # seconds to wait before checking state post-restart + +# Run a docker command with a hard timeout. Returns 124 on timeout, else docker's exit code. +docker_cmd() { + timeout "$DOCKER_TIMEOUT" "$@" + local exit_code=$? + if [[ "$exit_code" -eq 124 ]]; then + error "Docker command timed out after ${DOCKER_TIMEOUT}s: $*" + return 1 + fi + return "$exit_code" +} + +# Verify a container is still running after a restart. +# Waits RESTART_VERIFY_WAIT seconds to let it settle, then checks State.Running. +verify_running() { + local container="$1" + sleep "$RESTART_VERIFY_WAIT" + local state + state=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) + if [[ "$state" != "true" ]]; then + error "$container failed to stay running after restart — may have crashed" + return 1 + fi + return 0 +} + +# Retry a docker command up to RETRY_COUNT times, sleeping SLEEP seconds between attempts. +retry_docker() { + local attempt=1 + while [[ "$attempt" -le "$RETRY_COUNT" ]]; do + log "$ICON_RETRY Attempt $attempt of $RETRY_COUNT: $*" + if docker_cmd "$@"; then + log "Succeeded on attempt $attempt" + return 0 + else + warn "Attempt $attempt failed" + (( attempt++ )) + [[ "$attempt" -le "$RETRY_COUNT" ]] && sleep "$SLEEP" + fi + done + error "Command failed after $RETRY_COUNT attempts: $*" + return 1 +} + +# ── Emby API helper ─────────────────────────────────────────────────────────── +# General Emby REST call. Uses EMBY_URL and EMBY_API_KEY (aliased by detect_hosts). +# Usage: emby_api [timeout_seconds] +# Prints response body on 200; returns 1 on HTTP error or curl failure. +emby_api() { + local endpoint="$1" max_time="${2:-30}" + local response http_code body + response=$(curl -sf --max-time "$max_time" \ + -H "X-Emby-Token: $EMBY_API_KEY" \ + -w "\n%{http_code}" \ + "${EMBY_URL}/${endpoint}" 2>/dev/null) + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | head -n -1) + [[ "$http_code" != "200" ]] && { error "Emby API HTTP $http_code: $endpoint"; return 1; } + echo "$body" +} + # Stop containers on the REMOTE server via SSH. # Reads CRITICAL_CONTAINER_NAMES — set from profile arrays by rsync.sh. # Tracks running state in RUNNING_CONTAINERS for restart after sync. diff --git a/git_pull_execute.sh b/git_pull_execute.sh index 2684cb0..652aba2 100755 --- a/git_pull_execute.sh +++ b/git_pull_execute.sh @@ -82,7 +82,7 @@ if docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; else # Gitea not running locally — find it on the remote server via Tailscale log "Gitea not running locally — checking remote server" - GITEA_IP=$(tailscale ip -4 "${REMOTE_SERVER_NAME,,}" 2>/dev/null) + GITEA_IP=$(resolve_tailscale_ip "${REMOTE_SERVER_NAME}") if [[ -n "$GITEA_IP" ]]; then echo " Gitea on $REMOTE_SERVER_NAME — connecting via Tailscale $GITEA_IP" elif [[ -n "${GITEA_DOMAIN:-}" ]]; then