Bash common.sh: consolidate docker_cmd/retry_docker/verify_running/emby_api, fix tailscale dups
common.sh gains: - docker_cmd() + verify_running() + retry_docker() — removed from all 3 Docker_Essentials scripts where they were byte-for-byte duplicates - emby_api(endpoint, [timeout=30]) — removed from 6 Media/Tools scripts that each defined their own _emby_api() with the same curl/parse/error pattern; call sites renamed emby_api - format_duration() extended with days/hours branch (was capped at minutes+seconds) - notify() comment: scripts do not need to preflight the notify script via validate_unraid_cmd Tailscale deduplication: - arr_sync.sh: _resolve_node_ip() and inline block in _delete_remote_item() both replaced with resolve_tailscale_ip() from common.sh - git_pull_execute.sh: inline tailscale ip -4 replaced with resolve_tailscale_ip() (adds the tailscale status fallback that was missing)
This commit is contained in:
@@ -152,35 +152,7 @@ fi
|
|||||||
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
|
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
# Wraps docker commands with a 30 second timeout.
|
# docker_cmd, verify_running, retry_docker — defined in common.sh
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
||||||
# Builds a dependency-safe restart order from DAILY_RESTART_CONTAINERS.
|
# Builds a dependency-safe restart order from DAILY_RESTART_CONTAINERS.
|
||||||
# Containers that are dependencies of others restart first.
|
# Containers that are dependencies of others restart first.
|
||||||
@@ -247,28 +219,6 @@ check_dependency_delay() {
|
|||||||
fi
|
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 ━━━
|
# ━━━ Daily Restart ━━━
|
||||||
|
|||||||
@@ -151,42 +151,7 @@ fi
|
|||||||
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
|
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
DOCKER_TIMEOUT=30
|
# docker_cmd, retry_docker, verify_running — defined in common.sh
|
||||||
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" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ━━━ Pull Updates ━━━
|
# ━━━ Pull Updates ━━━
|
||||||
|
|||||||
@@ -136,55 +136,7 @@ fi
|
|||||||
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
|
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
# Wraps docker commands with a 30 second timeout.
|
# docker_cmd, retry_docker, verify_running — defined in common.sh
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
||||||
# Builds a dependency-safe restart order from WEEKLY_RESTART_CONTAINERS.
|
# Builds a dependency-safe restart order from WEEKLY_RESTART_CONTAINERS.
|
||||||
# Containers that are dependencies of others restart first.
|
# Containers that are dependencies of others restart first.
|
||||||
|
|||||||
+2
-13
@@ -315,12 +315,8 @@ _delete_remote_item() {
|
|||||||
local node_id="$1" port="$2" api_ver="$3" endpoint="$4" config_xml="$5"
|
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 id_field="$6" id_type="$7" stable_id="$8"
|
||||||
local node_name="${!node_id}"
|
local node_name="${!node_id}"
|
||||||
local ts_name="${node_name,,}"
|
|
||||||
local node_ip
|
local node_ip
|
||||||
node_ip=$(tailscale ip -4 "$ts_name" 2>/dev/null)
|
node_ip=$(resolve_tailscale_ip "$node_name") || return 1
|
||||||
[[ -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
|
|
||||||
|
|
||||||
local select_expr
|
local select_expr
|
||||||
if [[ "$id_type" == "string" ]]; then
|
if [[ "$id_type" == "string" ]]; then
|
||||||
@@ -478,14 +474,7 @@ fi
|
|||||||
|
|
||||||
_resolve_node_ip() {
|
_resolve_node_ip() {
|
||||||
local node_id="$1"
|
local node_id="$1"
|
||||||
local node_name="${!node_id}"
|
resolve_tailscale_ip "${!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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if arr is reachable on remote node
|
# Check if arr is reachable on remote node
|
||||||
|
|||||||
@@ -186,18 +186,6 @@ fi
|
|||||||
# ── API HELPERS ───────────────────────────────────────────────────────────────────────────────
|
# ── 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() {
|
_lidarr_get() {
|
||||||
curl -sf --max-time 20 \
|
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_EPOCH=$(date +%s)
|
||||||
TODAY=$(date +%Y-%m-%d)
|
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"
|
error "Could not fetch Emby activity log"
|
||||||
exit 1
|
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)
|
LIDARR_COUNT=$(echo "$LIDARR_NAMES" | grep -c . 2>/dev/null || echo 0)
|
||||||
log "$LIDARR_COUNT artists in Lidarr"
|
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"
|
warn "Could not fetch Emby artist library — skipping Emby filter"
|
||||||
EMBY_ARTIST_NAMES=""
|
EMBY_ARTIST_NAMES=""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,17 +194,6 @@ fi
|
|||||||
# ── API HELPERS ───────────────────────────────────────────────────────────────────────────────
|
# ── 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() {
|
_radarr_get() {
|
||||||
curl -sf --max-time 20 \
|
curl -sf --max-time 20 \
|
||||||
@@ -306,7 +295,7 @@ echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) | lookback: ${LOOKBACK_DAYS}d | thr
|
|||||||
echo ""
|
echo ""
|
||||||
echo "━━━ $ICON_SYNC Emby Watch History ━━━"
|
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')
|
CUTOFF_ISO=$(date -d "${LOOKBACK_DAYS} days ago" '+%Y-%m-%dT%H:%M:%SZ')
|
||||||
TODAY_EPOCH=$(date +%s)
|
TODAY_EPOCH=$(date +%s)
|
||||||
TODAY=$(date +%Y-%m-%d)
|
TODAY=$(date +%Y-%m-%d)
|
||||||
@@ -321,7 +310,7 @@ for lib_name in "${SEED_LIBRARIES[@]}"; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
log "Indexing library: $lib_name (ItemId: $lib_id)"
|
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"
|
warn "Could not index library: $lib_name"
|
||||||
continue
|
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).
|
# 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
|
# SortBy=DatePlayed on Items requires UserId context and errors without one; the activity log
|
||||||
# is server-scoped and doesn't have that limitation.
|
# 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"
|
error "Could not fetch Emby activity log"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -366,7 +355,7 @@ if [[ "${#ITEM_PLAYS[@]}" -gt 0 ]]; then
|
|||||||
for (( _b=0; _b<${#ALL_ITEM_IDS[@]}; _b+=BATCH_SIZE )); do
|
for (( _b=0; _b<${#ALL_ITEM_IDS[@]}; _b+=BATCH_SIZE )); do
|
||||||
BATCH=("${ALL_ITEM_IDS[@]:_b:BATCH_SIZE}")
|
BATCH=("${ALL_ITEM_IDS[@]:_b:BATCH_SIZE}")
|
||||||
IDS_CSV=$(printf '%s,' "${BATCH[@]}"); IDS_CSV="${IDS_CSV%,}"
|
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"
|
warn "Could not fetch item batch starting at $_b"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,17 +216,6 @@ fi
|
|||||||
# ── API HELPERS ───────────────────────────────────────────────────────────────────────────────
|
# ── 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() {
|
_sonarr_get() {
|
||||||
curl -sf --max-time 30 \
|
curl -sf --max-time 30 \
|
||||||
@@ -348,7 +337,7 @@ echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) | lookback: ${LOOKBACK_DAYS}d | thr
|
|||||||
echo ""
|
echo ""
|
||||||
echo "━━━ $ICON_SYNC Emby Series Library ━━━"
|
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')
|
CUTOFF_ISO=$(date -d "${LOOKBACK_DAYS} days ago" '+%Y-%m-%dT%H:%M:%SZ')
|
||||||
TODAY_EPOCH=$(date +%s)
|
TODAY_EPOCH=$(date +%s)
|
||||||
TODAY=$(date +%Y-%m-%d)
|
TODAY=$(date +%Y-%m-%d)
|
||||||
@@ -369,7 +358,7 @@ for lib_name in "${SONARR_EMBY_LIBRARIES[@]}"; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
log "Scanning library: $lib_name (ItemId: $lib_id)"
|
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"
|
warn "Could not fetch series from library: $lib_name"
|
||||||
continue
|
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_USER_PLAYS # "episode_item_id|user_id" → play count
|
||||||
declare -A ITEM_LAST_PLAY # episode_item_id → ISO date of most recent play
|
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"
|
error "Could not fetch Emby activity log"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -448,7 +437,7 @@ if [[ "${#ALL_EPISODE_IDS[@]}" -gt 0 ]]; then
|
|||||||
for (( _b=0; _b<${#ALL_EPISODE_IDS[@]}; _b+=BATCH_SIZE )); do
|
for (( _b=0; _b<${#ALL_EPISODE_IDS[@]}; _b+=BATCH_SIZE )); do
|
||||||
BATCH=("${ALL_EPISODE_IDS[@]:_b:BATCH_SIZE}")
|
BATCH=("${ALL_EPISODE_IDS[@]:_b:BATCH_SIZE}")
|
||||||
IDS_CSV=$(printf '%s,' "${BATCH[@]}"); IDS_CSV="${IDS_CSV%,}"
|
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"
|
warn "Could not fetch episode batch starting at $_b"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,18 +89,6 @@ fi
|
|||||||
# ── API HELPERS ───────────────────────────────────────────────────────────────────────────────
|
# ── 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() {
|
_lidarr_get() {
|
||||||
curl -sf --max-time 30 \
|
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,
|
# Query MusicAlbum items and extract AlbumArtists — gives primary album artists only,
|
||||||
# not the full tag credits (guest features, songwriters, etc.) that MusicArtist returns
|
# 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"
|
error "Could not fetch Emby music albums"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,17 +90,6 @@ fi
|
|||||||
# ── API HELPERS ───────────────────────────────────────────────────────────────────────────────
|
# ── 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() {
|
_radarr_get() {
|
||||||
curl -sf --max-time 30 \
|
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)
|
declare -A EMBY_MOVIES # name → tmdb_id (empty string if none)
|
||||||
|
|
||||||
if [[ "${#RADARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then
|
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
|
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)
|
lib_id=$(echo "$LIBRARIES_JSON" | jq -r --arg n "$lib_name" '.[] | select(.Name == $n) | .ItemId' 2>/dev/null)
|
||||||
if [[ -z "$lib_id" ]]; then
|
if [[ -z "$lib_id" ]]; then
|
||||||
@@ -168,7 +157,7 @@ if [[ "${#RADARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
log "Scanning library: $lib_name (ItemId: $lib_id)"
|
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"
|
warn "Could not fetch movies from library: $lib_name"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -180,7 +169,7 @@ if [[ "${#RADARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then
|
|||||||
done
|
done
|
||||||
else
|
else
|
||||||
log "RADARR_EMBY_LIBRARIES empty — scanning all Emby libraries"
|
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"
|
error "Could not fetch Emby movies"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,17 +90,6 @@ fi
|
|||||||
# ── API HELPERS ───────────────────────────────────────────────────────────────────────────────
|
# ── 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() {
|
_sonarr_get() {
|
||||||
curl -sf --max-time 30 \
|
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)
|
declare -A EMBY_SERIES # name → tvdb_id (empty string if none)
|
||||||
|
|
||||||
if [[ "${#SONARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then
|
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
|
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)
|
lib_id=$(echo "$LIBRARIES_JSON" | jq -r --arg n "$lib_name" '.[] | select(.Name == $n) | .ItemId' 2>/dev/null)
|
||||||
if [[ -z "$lib_id" ]]; then
|
if [[ -z "$lib_id" ]]; then
|
||||||
@@ -162,7 +151,7 @@ if [[ "${#SONARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
log "Scanning library: $lib_name (ItemId: $lib_id)"
|
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"
|
warn "Could not fetch series from library: $lib_name"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -174,7 +163,7 @@ if [[ "${#SONARR_EMBY_LIBRARIES[@]}" -gt 0 ]]; then
|
|||||||
done
|
done
|
||||||
else
|
else
|
||||||
log "SONARR_EMBY_LIBRARIES empty — scanning all Emby libraries"
|
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"
|
error "Could not fetch Emby series"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ notify() {
|
|||||||
|
|
||||||
log "$ICON_NOTIFY Sending notification: $subject — $message"
|
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
|
if [[ "${NOTIFY_UNRAID:-false}" == true ]]; then
|
||||||
local notify_script="/usr/local/emhttp/plugins/dynamix/scripts/notify"
|
local notify_script="/usr/local/emhttp/plugins/dynamix/scripts/notify"
|
||||||
if [[ -x "$notify_script" ]]; then
|
if [[ -x "$notify_script" ]]; then
|
||||||
@@ -261,9 +262,14 @@ notify() {
|
|||||||
|
|
||||||
format_duration() {
|
format_duration() {
|
||||||
local secs=$1
|
local secs=$1
|
||||||
local mins=$((secs / 60))
|
local days=$(( secs / 86400 ))
|
||||||
local rem=$((secs % 60))
|
local hrs=$(( (secs % 86400) / 3600 ))
|
||||||
[[ $mins -gt 0 ]] && echo "${mins}m${rem}s" || echo "${rem}s"
|
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()
|
# Tracks which remote containers were running before stop — used by start_containers()
|
||||||
RUNNING_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 <endpoint> [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.
|
# Stop containers on the REMOTE server via SSH.
|
||||||
# Reads CRITICAL_CONTAINER_NAMES — set from profile arrays by rsync.sh.
|
# Reads CRITICAL_CONTAINER_NAMES — set from profile arrays by rsync.sh.
|
||||||
# Tracks running state in RUNNING_CONTAINERS for restart after sync.
|
# Tracks running state in RUNNING_CONTAINERS for restart after sync.
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ if docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$";
|
|||||||
else
|
else
|
||||||
# Gitea not running locally — find it on the remote server via Tailscale
|
# Gitea not running locally — find it on the remote server via Tailscale
|
||||||
log "Gitea not running locally — checking remote server"
|
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
|
if [[ -n "$GITEA_IP" ]]; then
|
||||||
echo " Gitea on $REMOTE_SERVER_NAME — connecting via Tailscale $GITEA_IP"
|
echo " Gitea on $REMOTE_SERVER_NAME — connecting via Tailscale $GITEA_IP"
|
||||||
elif [[ -n "${GITEA_DOMAIN:-}" ]]; then
|
elif [[ -n "${GITEA_DOMAIN:-}" ]]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user