feat: Tailscale resolution hardening, partnership offboard completion, Emby provisioning
common.sh:
- Add resolve_tailscale_ip() helper — tries `tailscale ip -4` first, falls back to
parsing `tailscale status` output; handles hosts where MagicDNS short-name resolution
is not active
- Add PARTNERSHIP_OWN_CONTAINERS alias in detect_hosts()
- Add aliasing for 4 Emby provisioning vars (PARTNERSHIP_PROVISION_EMBY_ADMIN,
PARTNERSHIP_EMBY_ADMIN_USER, PARTNERSHIP_EMBY_ADMIN_PASS, PARTNERSHIP_EMBY_PORT)
Partnership/partnership_manager.sh:
- Replace 9 bare `tailscale ip -4` calls with resolve_tailscale_ip()
- Add read_remote_conf_var() and read_remote_conf_array() — SSH to mirror, source its
own load_config.sh + detect_hosts(), return aliased variable; solves sparse-checkout
problem where HOST1 cannot read master_host2.conf directly
- Add derive_short_name() — strips unraid- prefix, capitalises first char
- Add cleanup_partner_containers() — removes partner containers via FolderView3 folder
if enabled, else falls back to FALLBACK_*_COVERS_*_TIER* arrays
- Add cleanup_owner_containers_on_mirror() — SSH to mirror, stops and removes containers
matching *-${OWNER_SHORT} naming convention
- Add start_own_stack() and start_mirror_own_stack() — restart own containers locally
or on mirror via SSH using PARTNERSHIP_OWN_CONTAINERS
- Add provision_emby_admin() — reads mirror credentials via read_remote_conf_var, checks
for username collision, creates user + sets password + grants admin policy via Emby API
- Add revoke_emby_admin() — looks up mirror username on local Emby, deletes via REST API
- Wire offboard paths (both mirror-initiated and owner-initiated) to call container
cleanup and stack restart; update --check finalisation paths accordingly
- Fix write_state_file in --onboard not gated on DRY_RUN (was writing ACTIVE state on
dry runs)
master_host1.conf:
- Add HOST1_PARTNERSHIP_OWN_CONTAINERS array
- Add partnership Emby provisioning config (toggle + port + per-host credentials)
master_host2.conf:
- Add HOST2_PARTNERSHIP_OWN_CONTAINERS array
- Add HOST2_PARTNERSHIP_EMBY_ADMIN_USER and HOST2_PARTNERSHIP_EMBY_ADMIN_PASS
Tailscale fix applied to:
- Initial_run/ssh_setup.sh (2 callsites)
- unRAID_Essentials/rsync_stop.sh (1 callsite)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
6948755c86
commit
0ae31b5fa6
@@ -61,6 +61,7 @@
|
||||
# All aliased by detect_hosts() — script uses unprefixed names
|
||||
#
|
||||
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
|
||||
# LIDARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
|
||||
# LIDARR_LOCK_WARN_AGE — override default lock warning age (large libraries)
|
||||
# LIDARR_ORPHAN_AGE — days before untracked file is eligible for deletion
|
||||
# LIDARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
|
||||
@@ -293,6 +294,61 @@ is_protected_file() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Pre-flight: Lidarr Import Scan ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Pre-flight: Lidarr Import Scan ━━━"
|
||||
|
||||
# Reverse-lookup container path from path map so Lidarr gets its own path, not the host path
|
||||
LIDARR_CONTAINER_ROOT=""
|
||||
for _cp in "${!ARR_PATH_MAP[@]}"; do
|
||||
if [[ "${ARR_PATH_MAP[$_cp]}" == "$LIDARR_MUSIC_ROOT" ]]; then
|
||||
LIDARR_CONTAINER_ROOT="$_cp"
|
||||
break
|
||||
fi
|
||||
done
|
||||
unset _cp
|
||||
|
||||
if [[ -n "$LIDARR_CONTAINER_ROOT" ]]; then
|
||||
log "Triggering DownloadedAlbumsScan on: $LIDARR_CONTAINER_ROOT"
|
||||
SCAN_PAYLOAD="{\"name\": \"DownloadedAlbumsScan\", \"path\": \"$LIDARR_CONTAINER_ROOT\"}"
|
||||
else
|
||||
log "No path map match — triggering DownloadedAlbumsScan (all root folders)"
|
||||
SCAN_PAYLOAD='{"name": "DownloadedAlbumsScan"}'
|
||||
fi
|
||||
|
||||
SCAN_RESPONSE=$(curl -sf --max-time 30 -X POST \
|
||||
-H "X-Api-Key: $LIDARR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$SCAN_PAYLOAD" \
|
||||
"${LIDARR_URL}/api/v1/command" 2>/dev/null)
|
||||
|
||||
SCAN_CMD_ID=$(echo "$SCAN_RESPONSE" | jq -r '.id // empty' 2>/dev/null)
|
||||
|
||||
if [[ -z "$SCAN_CMD_ID" ]]; then
|
||||
warn "Could not trigger import scan — proceeding without pre-flight"
|
||||
else
|
||||
echo " Import scan queued (command ID: $SCAN_CMD_ID) — waiting for completion..."
|
||||
POLL_TIMEOUT=${LIDARR_IMPORT_SCAN_TIMEOUT:-600}
|
||||
POLLED=0
|
||||
while [[ "$POLLED" -lt "$POLL_TIMEOUT" ]]; do
|
||||
SCAN_STATUS=$(curl -sf --max-time 10 \
|
||||
-H "X-Api-Key: $LIDARR_API_KEY" \
|
||||
"${LIDARR_URL}/api/v1/command/${SCAN_CMD_ID}" 2>/dev/null | \
|
||||
jq -r '.status // empty' 2>/dev/null)
|
||||
case "$SCAN_STATUS" in
|
||||
completed) log "Import scan complete ✅"; break ;;
|
||||
failed) warn "Import scan reported failed — proceeding anyway"; break ;;
|
||||
esac
|
||||
sleep 10
|
||||
(( POLLED += 10 ))
|
||||
[[ $(( POLLED % 60 )) -eq 0 ]] && log " Still scanning... (${POLLED}s elapsed)"
|
||||
done
|
||||
[[ "$POLLED" -ge "$POLL_TIMEOUT" ]] && \
|
||||
warn "Import scan timed out after ${POLL_TIMEOUT}s — proceeding anyway"
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Fetch Lidarr Tracked Files ━━━
|
||||
# ==============================================================================================
|
||||
|
||||
+49
-12
@@ -88,6 +88,13 @@ acquire_lock
|
||||
|
||||
detect_hosts
|
||||
|
||||
# Build path map for translate_path() — container path → host path
|
||||
declare -A ARR_PATH_MAP
|
||||
for _key in "${!HOST1_LIDARR_PATH_MAP[@]}"; do
|
||||
ARR_PATH_MAP["$_key"]="${HOST1_LIDARR_PATH_MAP[$_key]}"
|
||||
done
|
||||
unset _key
|
||||
|
||||
# HOST guard — Lidarr runs on HOST1 only
|
||||
if [[ -z "$LIDARR_URL" ]]; then
|
||||
log "Lidarr not configured for $MY_ID — nothing to do"
|
||||
@@ -121,6 +128,8 @@ fi
|
||||
# ── Temp dir for subshell fetch/fail counters ─────────────────────────────────────────────────
|
||||
LIDARR_TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$LIDARR_TMP"' EXIT
|
||||
touch "$LIDARR_TMP/album_fetches" "$LIDARR_TMP/album_fails" \
|
||||
"$LIDARR_TMP/artist_fetches" "$LIDARR_TMP/artist_fails"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
|
||||
@@ -178,7 +187,7 @@ deezer_artist_image() {
|
||||
local query
|
||||
query=$(printf "%s" "$artist" | sed 's/ /+/g')
|
||||
curl_json "https://api.deezer.com/search/artist?q=$query" |
|
||||
jq -r '.data[0].picture_xl // empty'
|
||||
jq -r '.data[0].picture_xl // empty' 2>/dev/null
|
||||
}
|
||||
|
||||
lastfm_artist_image() {
|
||||
@@ -186,7 +195,7 @@ lastfm_artist_image() {
|
||||
local encoded
|
||||
encoded=$(printf "%s" "$artist" | sed 's/ /%20/g')
|
||||
curl_json "https://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist=$encoded&api_key=$LASTFM_API_KEY&format=json" |
|
||||
jq -r '.artist.image[-1]["#text"] // empty'
|
||||
jq -r '.artist.image[-1]["#text"] // empty' 2>/dev/null
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
@@ -210,6 +219,30 @@ ALBUMS_COMPLETE=0
|
||||
ARTISTS_CHECKED=0
|
||||
ARTISTS_COMPLETE=0
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Build Album Directory Map ━━━
|
||||
# ==============================================================================================
|
||||
# Lidarr's album API never populates .path — derive album dirs from track file paths instead.
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Building Album Directory Map ━━━"
|
||||
|
||||
declare -A ALBUM_DIR_MAP
|
||||
_artist_list=$(curl_json "$LIDARR_URL/api/v1/artist?apikey=$LIDARR_API_KEY")
|
||||
_map_artist_count=$(echo "$_artist_list" | jq '. | length')
|
||||
echo " Fetching track files for $_map_artist_count artists..."
|
||||
|
||||
while IFS= read -r _artist_id; do
|
||||
[[ -z "$_artist_id" ]] && continue
|
||||
while IFS=$'\t' read -r _album_id _track_path; do
|
||||
[[ -z "$_album_id" || -z "$_track_path" || "$_track_path" == "null" ]] && continue
|
||||
ALBUM_DIR_MAP["$_album_id"]=$(dirname "$_track_path")
|
||||
done < <(curl_json "$LIDARR_URL/api/v1/trackFile?artistId=${_artist_id}&apikey=$LIDARR_API_KEY" | \
|
||||
jq -r '.[] | [(.albumId | tostring), .path] | @tsv' 2>/dev/null)
|
||||
done < <(echo "$_artist_list" | jq -r '.[].id')
|
||||
unset _artist_list _map_artist_count _artist_id _album_id _track_path
|
||||
|
||||
echo " Mapped ${#ALBUM_DIR_MAP[@]} albums with local tracks"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Albums ━━━
|
||||
# ==============================================================================================
|
||||
@@ -227,7 +260,10 @@ fi
|
||||
total_albums=$(echo "$albums" | jq '. | length')
|
||||
echo " Processing $total_albums albums..."
|
||||
|
||||
while IFS=$'\t' read -r local_path mbid artist_name album_name; do
|
||||
while IFS=$'\t' read -r mbid artist_name album_name album_id; do
|
||||
raw_dir="${ALBUM_DIR_MAP[$album_id]:-}"
|
||||
[[ -z "$raw_dir" ]] && continue # not downloaded, skip
|
||||
local_path=$(translate_path "$raw_dir")
|
||||
(( ALBUMS_CHECKED++ ))
|
||||
|
||||
[[ ! -d "$local_path" ]] && continue
|
||||
@@ -254,13 +290,13 @@ while IFS=$'\t' read -r local_path mbid artist_name album_name; do
|
||||
fi
|
||||
|
||||
if [[ ! -f "$local_path/cover.jpg" ]]; then
|
||||
IMG=$(echo "$JSON" | jq -r '.[].albumcover[0].url // empty')
|
||||
IMG=$(echo "$JSON" | jq -r '.[].albumcover[0].url // empty' 2>/dev/null)
|
||||
if download_if_valid "$IMG" "$local_path/cover.jpg"; then
|
||||
(( _fetches++ ))
|
||||
else
|
||||
query=$(printf "%s %s" "$artist_name" "$album_name" | sed 's/ /+/g')
|
||||
itunes=$(curl_json "https://itunes.apple.com/search?term=$query&entity=album&limit=1" |
|
||||
jq -r '.results[0].artworkUrl100 // empty' | sed 's/100x100/600x600/')
|
||||
jq -r '.results[0].artworkUrl100 // empty' 2>/dev/null | sed 's/100x100/600x600/')
|
||||
if download_if_valid "$itunes" "$local_path/cover.jpg"; then
|
||||
(( _fetches++ ))
|
||||
else
|
||||
@@ -270,12 +306,12 @@ while IFS=$'\t' read -r local_path mbid artist_name album_name; do
|
||||
fi
|
||||
|
||||
if [[ ! -f "$local_path/cdart.png" ]]; then
|
||||
IMG=$(echo "$JSON" | jq -r '.[].cdart[0].url // empty')
|
||||
IMG=$(echo "$JSON" | jq -r '.[].cdart[0].url // empty' 2>/dev/null)
|
||||
if download_if_valid "$IMG" "$local_path/cdart.png"; then (( _fetches++ )); else (( _fails++ )); fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$local_path/back.jpg" ]]; then
|
||||
IMG=$(echo "$JSON" | jq -r '.[].albumback[0].url // empty')
|
||||
IMG=$(echo "$JSON" | jq -r '.[].albumback[0].url // empty' 2>/dev/null)
|
||||
if download_if_valid "$IMG" "$local_path/back.jpg"; then (( _fetches++ )); else (( _fails++ )); fi
|
||||
fi
|
||||
|
||||
@@ -283,7 +319,7 @@ while IFS=$'\t' read -r local_path mbid artist_name album_name; do
|
||||
(( _fails > 0 )) && printf '1\n' >> "$LIDARR_TMP/album_fails"
|
||||
) &
|
||||
|
||||
done < <(echo "$albums" | jq -r '.[] | [.path, .foreignAlbumId, .artist.artistName, .title] | @tsv')
|
||||
done < <(echo "$albums" | jq -r '.[] | [(.foreignAlbumId // ""), (.artist.artistName // ""), (.title // ""), (.id | tostring)] | @tsv')
|
||||
|
||||
wait
|
||||
|
||||
@@ -310,6 +346,7 @@ total_artists=$(echo "$artists" | jq '. | length')
|
||||
echo " Processing $total_artists artists..."
|
||||
|
||||
while IFS=$'\t' read -r local_path mbid name; do
|
||||
local_path=$(translate_path "$local_path")
|
||||
(( ARTISTS_CHECKED++ ))
|
||||
|
||||
[[ ! -d "$local_path" ]] && continue
|
||||
@@ -335,7 +372,7 @@ while IFS=$'\t' read -r local_path mbid name; do
|
||||
sleep "$LIDARR_ART_SLEEP_BETWEEN"
|
||||
|
||||
if [[ ! -f "$local_path/folder.jpg" ]]; then
|
||||
IMG=$(echo "$JSON" | jq -r '.artistthumb[0].url // empty')
|
||||
IMG=$(echo "$JSON" | jq -r '.artistthumb[0].url // empty' 2>/dev/null)
|
||||
if download_if_valid "$IMG" "$local_path/folder.jpg"; then
|
||||
(( _fetches++ ))
|
||||
else
|
||||
@@ -354,7 +391,7 @@ while IFS=$'\t' read -r local_path mbid name; do
|
||||
fi
|
||||
|
||||
if [[ ! -f "$local_path/fanart.jpg" ]]; then
|
||||
IMG=$(echo "$JSON" | jq -r '.artistbackground[0].url // empty')
|
||||
IMG=$(echo "$JSON" | jq -r '.artistbackground[0].url // empty' 2>/dev/null)
|
||||
if download_if_valid "$IMG" "$local_path/fanart.jpg"; then
|
||||
(( _fetches++ ))
|
||||
else
|
||||
@@ -364,12 +401,12 @@ while IFS=$'\t' read -r local_path mbid name; do
|
||||
fi
|
||||
|
||||
if [[ ! -f "$local_path/logo.png" ]]; then
|
||||
IMG=$(echo "$JSON" | jq -r '.hdmusiclogo[0].url // empty')
|
||||
IMG=$(echo "$JSON" | jq -r '.hdmusiclogo[0].url // empty' 2>/dev/null)
|
||||
if download_if_valid "$IMG" "$local_path/logo.png"; then (( _fetches++ )); else (( _fails++ )); fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$local_path/banner.jpg" ]]; then
|
||||
IMG=$(echo "$JSON" | jq -r '.musicbanner[0].url // empty')
|
||||
IMG=$(echo "$JSON" | jq -r '.musicbanner[0].url // empty' 2>/dev/null)
|
||||
if download_if_valid "$IMG" "$local_path/banner.jpg"; then (( _fetches++ )); else (( _fails++ )); fi
|
||||
fi
|
||||
|
||||
|
||||
+62
-6
@@ -58,12 +58,13 @@
|
||||
# All aliased by detect_hosts() — script uses unprefixed names
|
||||
#
|
||||
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
|
||||
# RADARR_ORPHAN_AGE — days before untracked file eligible for deletion
|
||||
# RADARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
|
||||
# RADARR_EXTENSIONS — video file extensions considered for orphan classification
|
||||
# RADARR_PROTECTED_PATTERNS — file patterns never deleted
|
||||
# RADARR_VERSION_MAJOR — expected Radarr major version for API safety check
|
||||
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
|
||||
# RADARR_ORPHAN_AGE — days before untracked file eligible for deletion
|
||||
# RADARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
|
||||
# RADARR_EXTENSIONS — video file extensions considered for orphan classification
|
||||
# RADARR_PROTECTED_PATTERNS — file patterns never deleted
|
||||
# RADARR_VERSION_MAJOR — expected Radarr major version for API safety check
|
||||
# RADARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
|
||||
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
|
||||
#
|
||||
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# radarr_cleanup.sh — normal run
|
||||
@@ -282,6 +283,61 @@ format_bytes() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Pre-flight: Radarr Import Scan ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Pre-flight: Radarr Import Scan ━━━"
|
||||
|
||||
# Reverse-lookup container path from path map so Radarr gets its own path, not the host path
|
||||
RADARR_CONTAINER_ROOT=""
|
||||
for _cp in "${!ARR_PATH_MAP[@]}"; do
|
||||
if [[ "${ARR_PATH_MAP[$_cp]}" == "$RADARR_MOVIES_ROOT" ]]; then
|
||||
RADARR_CONTAINER_ROOT="$_cp"
|
||||
break
|
||||
fi
|
||||
done
|
||||
unset _cp
|
||||
|
||||
if [[ -n "$RADARR_CONTAINER_ROOT" ]]; then
|
||||
log "Triggering DownloadedMoviesScan on: $RADARR_CONTAINER_ROOT"
|
||||
SCAN_PAYLOAD="{\"name\": \"DownloadedMoviesScan\", \"path\": \"$RADARR_CONTAINER_ROOT\"}"
|
||||
else
|
||||
log "No path map match — triggering DownloadedMoviesScan (all root folders)"
|
||||
SCAN_PAYLOAD='{"name": "DownloadedMoviesScan"}'
|
||||
fi
|
||||
|
||||
SCAN_RESPONSE=$(curl -sf --max-time 30 -X POST \
|
||||
-H "X-Api-Key: $RADARR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$SCAN_PAYLOAD" \
|
||||
"${RADARR_URL}/api/v3/command" 2>/dev/null)
|
||||
|
||||
SCAN_CMD_ID=$(echo "$SCAN_RESPONSE" | jq -r '.id // empty' 2>/dev/null)
|
||||
|
||||
if [[ -z "$SCAN_CMD_ID" ]]; then
|
||||
warn "Could not trigger import scan — proceeding without pre-flight"
|
||||
else
|
||||
echo " Import scan queued (command ID: $SCAN_CMD_ID) — waiting for completion..."
|
||||
POLL_TIMEOUT=${RADARR_IMPORT_SCAN_TIMEOUT:-600}
|
||||
POLLED=0
|
||||
while [[ "$POLLED" -lt "$POLL_TIMEOUT" ]]; do
|
||||
SCAN_STATUS=$(curl -sf --max-time 10 \
|
||||
-H "X-Api-Key: $RADARR_API_KEY" \
|
||||
"${RADARR_URL}/api/v3/command/${SCAN_CMD_ID}" 2>/dev/null | \
|
||||
jq -r '.status // empty' 2>/dev/null)
|
||||
case "$SCAN_STATUS" in
|
||||
completed) log "Import scan complete ✅"; break ;;
|
||||
failed) warn "Import scan reported failed — proceeding anyway"; break ;;
|
||||
esac
|
||||
sleep 10
|
||||
(( POLLED += 10 ))
|
||||
[[ $(( POLLED % 60 )) -eq 0 ]] && log " Still scanning... (${POLLED}s elapsed)"
|
||||
done
|
||||
[[ "$POLLED" -ge "$POLL_TIMEOUT" ]] && \
|
||||
warn "Import scan timed out after ${POLL_TIMEOUT}s — proceeding anyway"
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Fetch Radarr Tracked Files ━━━
|
||||
# ==============================================================================================
|
||||
|
||||
+72
-8
@@ -58,12 +58,13 @@
|
||||
# All aliased by detect_hosts() — script uses unprefixed names
|
||||
#
|
||||
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
|
||||
# SONARR_ORPHAN_AGE — days before untracked file eligible for deletion
|
||||
# SONARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
|
||||
# SONARR_EXTENSIONS — video file extensions considered for orphan classification
|
||||
# SONARR_PROTECTED_PATTERNS — file patterns never deleted
|
||||
# SONARR_VERSION_MAJOR — expected Sonarr major version for API safety check
|
||||
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
|
||||
# SONARR_ORPHAN_AGE — days before untracked file eligible for deletion
|
||||
# SONARR_MAX_DELETE_GB — require --i-know-what-im-doing above this
|
||||
# SONARR_EXTENSIONS — video file extensions considered for orphan classification
|
||||
# SONARR_PROTECTED_PATTERNS — file patterns never deleted
|
||||
# SONARR_VERSION_MAJOR — expected Sonarr major version for API safety check
|
||||
# SONARR_IMPORT_SCAN_TIMEOUT — seconds to wait for pre-flight import scan (default 600)
|
||||
# ARR_CLEANUP_STATS — stats file path (read by coffee report)
|
||||
#
|
||||
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# sonarr_cleanup.sh — normal run
|
||||
@@ -282,6 +283,61 @@ format_bytes() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Pre-flight: Sonarr Import Scan ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Pre-flight: Sonarr Import Scan ━━━"
|
||||
|
||||
# Reverse-lookup container path from path map so Sonarr gets its own path, not the host path
|
||||
SONARR_CONTAINER_ROOT=""
|
||||
for _cp in "${!ARR_PATH_MAP[@]}"; do
|
||||
if [[ "${ARR_PATH_MAP[$_cp]}" == "$SONARR_TV_ROOT" ]]; then
|
||||
SONARR_CONTAINER_ROOT="$_cp"
|
||||
break
|
||||
fi
|
||||
done
|
||||
unset _cp
|
||||
|
||||
if [[ -n "$SONARR_CONTAINER_ROOT" ]]; then
|
||||
log "Triggering DownloadedEpisodesScan on: $SONARR_CONTAINER_ROOT"
|
||||
SCAN_PAYLOAD="{\"name\": \"DownloadedEpisodesScan\", \"path\": \"$SONARR_CONTAINER_ROOT\"}"
|
||||
else
|
||||
log "No path map match — triggering DownloadedEpisodesScan (all root folders)"
|
||||
SCAN_PAYLOAD='{"name": "DownloadedEpisodesScan"}'
|
||||
fi
|
||||
|
||||
SCAN_RESPONSE=$(curl -sf --max-time 30 -X POST \
|
||||
-H "X-Api-Key: $SONARR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$SCAN_PAYLOAD" \
|
||||
"${SONARR_URL}/api/v3/command" 2>/dev/null)
|
||||
|
||||
SCAN_CMD_ID=$(echo "$SCAN_RESPONSE" | jq -r '.id // empty' 2>/dev/null)
|
||||
|
||||
if [[ -z "$SCAN_CMD_ID" ]]; then
|
||||
warn "Could not trigger import scan — proceeding without pre-flight"
|
||||
else
|
||||
echo " Import scan queued (command ID: $SCAN_CMD_ID) — waiting for completion..."
|
||||
POLL_TIMEOUT=${SONARR_IMPORT_SCAN_TIMEOUT:-600}
|
||||
POLLED=0
|
||||
while [[ "$POLLED" -lt "$POLL_TIMEOUT" ]]; do
|
||||
SCAN_STATUS=$(curl -sf --max-time 10 \
|
||||
-H "X-Api-Key: $SONARR_API_KEY" \
|
||||
"${SONARR_URL}/api/v3/command/${SCAN_CMD_ID}" 2>/dev/null | \
|
||||
jq -r '.status // empty' 2>/dev/null)
|
||||
case "$SCAN_STATUS" in
|
||||
completed) log "Import scan complete ✅"; break ;;
|
||||
failed) warn "Import scan reported failed — proceeding anyway"; break ;;
|
||||
esac
|
||||
sleep 10
|
||||
(( POLLED += 10 ))
|
||||
[[ $(( POLLED % 60 )) -eq 0 ]] && log " Still scanning... (${POLLED}s elapsed)"
|
||||
done
|
||||
[[ "$POLLED" -ge "$POLL_TIMEOUT" ]] && \
|
||||
warn "Import scan timed out after ${POLL_TIMEOUT}s — proceeding anyway"
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Fetch Sonarr Tracked Files ━━━
|
||||
# ==============================================================================================
|
||||
@@ -343,6 +399,14 @@ while IFS= read -r series_id; do
|
||||
done <<< "$SERIES_IDS"
|
||||
|
||||
sort -u "$TRACKED_FILE" -o "$TRACKED_FILE"
|
||||
|
||||
# Build in-memory lookup map — O(1) per lookup vs O(n) grep per file
|
||||
declare -A TRACKED_MAP
|
||||
while IFS= read -r _tracked_path; do
|
||||
[[ -n "$_tracked_path" ]] && TRACKED_MAP["$_tracked_path"]=1
|
||||
done < "$TRACKED_FILE"
|
||||
unset _tracked_path
|
||||
log "Built in-memory lookup map: ${#TRACKED_MAP[@]} tracked paths"
|
||||
TRACKED_COUNT=$(wc -l < "$TRACKED_FILE")
|
||||
|
||||
# Safety Layer 5 — tracked count > 0
|
||||
@@ -378,7 +442,7 @@ MAX_DELETE_BYTES=$(awk "BEGIN {printf \"%d\", $SONARR_MAX_DELETE_GB * 1073741824
|
||||
while IFS= read -r filepath; do
|
||||
[[ -z "$filepath" ]] && continue
|
||||
|
||||
if grep -qF "$filepath" "$TRACKED_FILE" 2>/dev/null; then
|
||||
if [[ -n "${TRACKED_MAP[$filepath]:-}" ]]; then
|
||||
log "TRACKED: $filepath"
|
||||
continue
|
||||
fi
|
||||
@@ -442,7 +506,7 @@ fi
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
while IFS= read -r filepath; do
|
||||
[[ -z "$filepath" ]] && continue
|
||||
grep -qF "$filepath" "$TRACKED_FILE" 2>/dev/null && continue
|
||||
[[ -n "${TRACKED_MAP[$filepath]:-}" ]] && continue
|
||||
is_protected_file "$filepath" && continue
|
||||
|
||||
FILE_MTIME=$(stat -c %Y "$filepath" 2>/dev/null || echo 0)
|
||||
|
||||
Reference in New Issue
Block a user