Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbe6a54e41 | ||
|
|
d058cf15c9 | ||
|
|
4de10a7d01 | ||
|
|
01e4f97361 | ||
|
|
284896fbd9 | ||
|
|
c4d4d8160d | ||
|
|
438de76655 | ||
|
|
86b8e894e5 |
@@ -453,6 +453,7 @@
|
||||
# State file on /boot/config — survives reboots (interval gate independent of uptime gate).
|
||||
MONTHLY_MAINTENANCE_SCRIPTS=(
|
||||
"Docker_Essentials/docker_update.sh --remainder" # update all containers not in daily/weekly managed lists
|
||||
"Tools/docker_prune_images.sh --all" # remove stopped containers then all unused images (tagged orphans)
|
||||
"Tools/zfs_pool_scrub.sh" # ZFS pool integrity scrub — all pools, skip ZFS_REPORT_IGNORE_POOLS
|
||||
"Tools/smart_long_test.sh" # SMART extended self-test — all drives, sequential
|
||||
)
|
||||
@@ -691,6 +692,19 @@
|
||||
FALLBACK_PARTNERSHIP_SUSPEND_AFTER=120 # minutes without active partnership before suspending
|
||||
# 0 = suspend immediately when partnership goes inactive
|
||||
|
||||
# ━━━ Play State Sync ━━━
|
||||
PLAY_SYNC_ENABLED=true
|
||||
PLAY_SYNC_REMOTE=true # sync across hosts via Tailscale
|
||||
PLAY_SYNC_DAYS=90 # days back to check for played items (0 = all time)
|
||||
PLAY_SYNC_TYPES="Movie,Episode" # Audio excluded — music library too large; favorites handled separately
|
||||
PLAY_SYNC_FAV_TYPES="MusicArtist,MusicAlbum,Movie,Series" # union sync — never unmarks; Audio tracks future
|
||||
|
||||
# ━━━ Play State Sync — Handback ━━━
|
||||
# play_state_sync runs before DNS cutover so users land on current watch state.
|
||||
# Retried until success — DNS is held until sync completes or retries are exhausted.
|
||||
PLAY_SYNC_HANDBACK_RETRIES=5 # attempts before giving up and proceeding to DNS cutover
|
||||
PLAY_SYNC_HANDBACK_RETRY_DELAY=60 # seconds between retry attempts
|
||||
|
||||
# ━━━ Failover Test ━━━
|
||||
# Controlled simulation of a failover event — run manually via fallback_test.sh.
|
||||
FALLBACK_TEST_BLOCK_WAIT=150 # seconds to wait after blocking connectivity
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
HOSTN_TIER4_DELAY=1440 # 24 hours
|
||||
|
||||
# ━━━ Rsync Writeback ━━━
|
||||
HOSTN_TIER1_WRITEBACK_DELAY=60 # skip Emby writeback if outage under 1hr
|
||||
HOSTN_TIER1_WRITEBACK_DELAY=60 # minimum outage minutes before Tier 1 writeback runs
|
||||
|
||||
FALLBACK_HOSTN_WRITEBACK_TIER1=(
|
||||
# "/mnt/user/Media_Server/Emby"
|
||||
|
||||
@@ -686,6 +686,19 @@
|
||||
FALLBACK_PARTNERSHIP_SUSPEND_AFTER=120 # minutes without active partnership before suspending
|
||||
# 0 = suspend immediately when partnership goes inactive
|
||||
|
||||
# ━━━ Play State Sync ━━━
|
||||
PLAY_SYNC_ENABLED=true
|
||||
PLAY_SYNC_REMOTE=true # sync across hosts via Tailscale
|
||||
PLAY_SYNC_DAYS=90 # days back to check for played items (0 = all time)
|
||||
PLAY_SYNC_TYPES="Movie,Episode" # Audio excluded — music library too large; favorites handled separately
|
||||
PLAY_SYNC_FAV_TYPES="MusicArtist,MusicAlbum,Movie,Series" # union sync — never unmarks; Audio tracks future
|
||||
|
||||
# ━━━ Play State Sync — Handback ━━━
|
||||
# play_state_sync runs before DNS cutover so users land on current watch state.
|
||||
# Retried until success — DNS is held until sync completes or retries are exhausted.
|
||||
PLAY_SYNC_HANDBACK_RETRIES=5 # attempts before giving up and proceeding to DNS cutover
|
||||
PLAY_SYNC_HANDBACK_RETRY_DELAY=60 # seconds between retry attempts
|
||||
|
||||
# ━━━ Failover Test ━━━
|
||||
# Controlled simulation of a failover event — run manually via fallback_test.sh.
|
||||
FALLBACK_TEST_BLOCK_WAIT=150 # seconds to wait after blocking connectivity
|
||||
|
||||
@@ -397,7 +397,9 @@ if [[ ${#UPDATED[@]} -gt 0 ]]; then
|
||||
echo "$ICON_DONE $container rebuilt ✅"
|
||||
REBUILT+=("$container")
|
||||
else
|
||||
error "Failed to rebuild $container — will be picked up by docker_daily_restart.sh"
|
||||
_fallback_msg=$([[ "$WEEKLY_MODE" == true ]] && echo "docker_weekly_restart.sh" || ([[ "$REMAINDER_MODE" == true ]] && echo "no follow-up restart" || echo "docker_daily_restart.sh"))
|
||||
error "Failed to rebuild $container — $_fallback_msg"
|
||||
unset _fallback_msg
|
||||
notify "$container failed to rebuild after image update on $(hostname)" "Docker Update" "warning"
|
||||
REBUILD_FAILED+=("$container")
|
||||
fi
|
||||
|
||||
@@ -268,6 +268,21 @@ done
|
||||
|
||||
END=$(date +%s)
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Prune Old Images ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Pruning Dangling Images — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would prune dangling images"
|
||||
PRUNED_SUMMARY="(dry run)"
|
||||
else
|
||||
PRUNED_OUTPUT=$(docker image prune -f 2>&1)
|
||||
[[ "$ENABLE_LOGGING" == "true" ]] && echo "$PRUNED_OUTPUT" | sed 's/^/ /'
|
||||
PRUNED_SUMMARY=$(echo "$PRUNED_OUTPUT" | grep -E "^Total reclaimed" || echo "nothing reclaimed")
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
@@ -280,6 +295,7 @@ if [[ ${#RESTARTED[@]} -gt 0 ]]; then
|
||||
fi
|
||||
[[ ${#SKIPPED[@]} -gt 0 ]] && log "$ICON_NOT_RUNNING Skipped: ${SKIPPED[*]} (were stopped)"
|
||||
[[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}"
|
||||
echo "$ICON_SYNC Pruned: ${PRUNED_SUMMARY:-none}"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "$ICON_WARN Status: DRY RUN — no changes made"
|
||||
|
||||
+29
-7
@@ -772,11 +772,7 @@ run_handback() {
|
||||
[[ -z "$job" ]] && continue
|
||||
log "Syncing: $job"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if [[ "$(basename "$job")" == "Emby" ]]; then
|
||||
bash "$SCRIPT_DIR/../Rsync/rsync.sh" "$job" --profile=emby-fallback
|
||||
else
|
||||
bash "$SCRIPT_DIR/../Rsync/rsync.sh" "$job"
|
||||
fi
|
||||
bash "$SCRIPT_DIR/../Rsync/rsync.sh" "$job"
|
||||
else
|
||||
warn "DRY RUN — would rsync: $job"
|
||||
fi
|
||||
@@ -797,7 +793,33 @@ run_handback() {
|
||||
[[ "$is_ddns" == false ]] && remote_start "$container"
|
||||
done
|
||||
|
||||
# ── Step 7: DNS Cutover — final step ────────────────────────────────────────────────────
|
||||
# ── Step 7: Play State Sync — DNS held until sync succeeds or retries exhausted ──────────
|
||||
# Retried so users land on current watch state — after a 3hr outage one successful
|
||||
# run catches everything regardless of how many cron cycles were missed.
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Play State Sync ━━━"
|
||||
local _sync_ok=false
|
||||
local _retry_max="${PLAY_SYNC_HANDBACK_RETRIES:-5}"
|
||||
local _retry_delay="${PLAY_SYNC_HANDBACK_RETRY_DELAY:-60}"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
for _attempt in $(seq 1 "$_retry_max"); do
|
||||
if bash "$SCRIPT_DIR/../Media/play_state_sync.sh" --wait; then
|
||||
_sync_ok=true
|
||||
log "Play state synced ✅ (attempt ${_attempt}/${_retry_max})"
|
||||
break
|
||||
fi
|
||||
if [[ "$_attempt" -lt "$_retry_max" ]]; then
|
||||
warn "Play state sync failed (attempt ${_attempt}/${_retry_max}) — retrying in ${_retry_delay}s"
|
||||
sleep "$_retry_delay"
|
||||
fi
|
||||
done
|
||||
[[ "$_sync_ok" == false ]] && \
|
||||
warn "Play state sync failed after ${_retry_max} attempts — proceeding to DNS cutover"
|
||||
else
|
||||
warn "DRY RUN — would retry play_state_sync --wait up to ${_retry_max} times before DNS cutover"
|
||||
fi
|
||||
|
||||
# ── Step 8: DNS Cutover — final step ────────────────────────────────────────────────────
|
||||
# DNS cuts over ONLY after Tier 1 containers confirmed up
|
||||
echo ""
|
||||
echo "━━━ $ICON_NET DNS Cutover ━━━"
|
||||
@@ -807,7 +829,7 @@ run_handback() {
|
||||
done
|
||||
warn "$ICON_NET Remote DDNS started — DNS now points at $REMOTE_SERVER_NAME"
|
||||
|
||||
# ── Step 8: Return to NORMAL ─────────────────────────────────────────────────────────────
|
||||
# ── Step 9: Return to NORMAL ─────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
state_set state "NORMAL"
|
||||
state_set fallback_start "0"
|
||||
|
||||
+198
-20
@@ -73,11 +73,18 @@
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../load_config.sh"
|
||||
|
||||
# ── Handle --full before parse_args ───────────────────────────────────────────
|
||||
# ── Handle --full and --wait before parse_args ────────────────────────────────
|
||||
FULL_SYNC=false
|
||||
LOCK_MODE="strict"
|
||||
_FILTERED=()
|
||||
for _a in "$@"; do
|
||||
[[ "$_a" == "--full" ]] && FULL_SYNC=true || _FILTERED+=("$_a")
|
||||
if [[ "$_a" == "--full" ]]; then
|
||||
FULL_SYNC=true
|
||||
elif [[ "$_a" == "--wait" ]]; then
|
||||
LOCK_MODE="wait"
|
||||
else
|
||||
_FILTERED+=("$_a")
|
||||
fi
|
||||
done
|
||||
parse_args "${_FILTERED[@]}"
|
||||
|
||||
@@ -87,13 +94,14 @@ parse_args "${_FILTERED[@]}"
|
||||
[[ "${PLAY_SYNC_ENABLED:-true}" != "true" ]] && echo "Play state sync disabled" && exit 0
|
||||
|
||||
SYNC_DAYS="${PLAY_SYNC_DAYS:-90}"
|
||||
SYNC_TYPES="${PLAY_SYNC_TYPES:-Movie,Episode,Audio}"
|
||||
SYNC_TYPES="${PLAY_SYNC_TYPES:-Movie,Episode}"
|
||||
FAV_TYPES="${PLAY_SYNC_FAV_TYPES:-MusicArtist,MusicAlbum,Movie,Series}"
|
||||
[[ "$FULL_SYNC" == true ]] && SYNC_DAYS=0
|
||||
log "$ICON_GEAR Config: days=${SYNC_DAYS} types=${SYNC_TYPES} remote=${PLAY_SYNC_REMOTE:-true}"
|
||||
log "$ICON_GEAR Config: days=${SYNC_DAYS} types=${SYNC_TYPES} favs=${FAV_TYPES} remote=${PLAY_SYNC_REMOTE:-true}"
|
||||
|
||||
command -v jq >/dev/null 2>&1 || { error "jq is required but not installed"; exit 1; }
|
||||
|
||||
acquire_lock
|
||||
acquire_lock "$LOCK_MODE"
|
||||
|
||||
# ── Build server list across ALL hosts ────────────────────────────────────────
|
||||
# All HOST*_TRANSCODE_SERVERS arrays are loaded into env by load_config.sh.
|
||||
@@ -276,24 +284,62 @@ done
|
||||
# per-item API call and is faster overall.
|
||||
declare -A PROV_LOOKUP # "si|tvdb.{id}" | "si|imdb.{id}" | "si|mb.{id}" → item_id
|
||||
|
||||
# Jellyfin's mixed-type query (Movie,Episode,Audio) changes sort order unpredictably,
|
||||
# pushing items to positions far beyond the first pages. Query each type separately so
|
||||
# each list sorts within its own type and items appear where expected.
|
||||
_PROV_PAGE=2000
|
||||
# Combine play state types + fav types for the map build (deduplicated)
|
||||
declare -A _prov_seen_dedup
|
||||
_prov_types=()
|
||||
IFS=',' read -ra _prov_combined <<< "${SYNC_TYPES},${FAV_TYPES}"
|
||||
for _t in "${_prov_combined[@]}"; do
|
||||
[[ -n "${_prov_seen_dedup[$_t]:-}" ]] && continue
|
||||
_prov_seen_dedup[$_t]=1; _prov_types+=("$_t")
|
||||
done
|
||||
unset _prov_seen_dedup _prov_combined
|
||||
|
||||
for _psi in $(seq 0 $(( _srv_count - 1 ))); do
|
||||
[[ -z "${SRV_URL[$_psi]}" ]] && continue
|
||||
log "Building provider ID map for ${SRV_NAME[$_psi]}..."
|
||||
_raw_prov=$(_api_get "${SRV_URL[$_psi]}" "${SRV_KEY[$_psi]}" \
|
||||
"Items?Recursive=true&IncludeItemTypes=${SYNC_TYPES}&Fields=ProviderIds&ExcludeLocationTypes=Virtual" 2>/dev/null)
|
||||
[[ -z "$_raw_prov" ]] && continue
|
||||
while IFS=$'\t' read -r _pid _ptvdb _pimdb _ptmdb _pmbtrack; do
|
||||
[[ "$_ptvdb" != "null" && -n "$_ptvdb" ]] && PROV_LOOKUP["${_psi}|tvdb.${_ptvdb}"]="$_pid"
|
||||
[[ "$_pimdb" != "null" && -n "$_pimdb" ]] && PROV_LOOKUP["${_psi}|imdb.${_pimdb}"]="$_pid"
|
||||
[[ "$_ptmdb" != "null" && -n "$_ptmdb" ]] && PROV_LOOKUP["${_psi}|tmdb.${_ptmdb}"]="$_pid"
|
||||
[[ "$_pmbtrack" != "null" && -n "$_pmbtrack" ]] && PROV_LOOKUP["${_psi}|mb.${_pmbtrack}"]="$_pid"
|
||||
done < <(echo "$_raw_prov" | jq -r '.Items[] | [
|
||||
.Id,
|
||||
(.ProviderIds.Tvdb // "null"),
|
||||
(.ProviderIds.Imdb // "null"),
|
||||
(.ProviderIds.Tmdb // "null"),
|
||||
(.ProviderIds.MusicBrainzTrackId // "null")
|
||||
] | @tsv' 2>/dev/null)
|
||||
for _prov_type in "${_prov_types[@]}"; do
|
||||
_prov_start=0
|
||||
_prov_total=-1
|
||||
while true; do
|
||||
_page=$(curl -sf --max-time 60 \
|
||||
-H "X-Emby-Token: ${SRV_KEY[$_psi]}" \
|
||||
"${SRV_URL[$_psi]%/}/Items?Recursive=true&IncludeItemTypes=${_prov_type}&Fields=ProviderIds&Limit=${_PROV_PAGE}&StartIndex=${_prov_start}" 2>/dev/null)
|
||||
[[ -z "$_page" ]] && break
|
||||
[[ "$_prov_total" -lt 0 ]] && _prov_total=$(echo "$_page" | jq '.TotalRecordCount // 0' 2>/dev/null || echo 0)
|
||||
_prov_count=$(echo "$_page" | jq '.Items | length' 2>/dev/null || echo 0)
|
||||
[[ "$_prov_count" -eq 0 ]] && break
|
||||
while IFS=$'\t' read -r _pid _ptype _ptvdb _pimdb _ptmdb _pmbtrack _pmbartist _pmbalbum; do
|
||||
[[ "$_pimdb" != "null" && -n "$_pimdb" ]] && PROV_LOOKUP["${_psi}|imdb.${_pimdb}"]="$_pid"
|
||||
[[ "$_ptmdb" != "null" && -n "$_ptmdb" ]] && PROV_LOOKUP["${_psi}|tmdb.${_ptmdb}"]="$_pid"
|
||||
[[ "$_pmbtrack" != "null" && -n "$_pmbtrack" ]] && PROV_LOOKUP["${_psi}|mb.${_pmbtrack}"]="$_pid"
|
||||
[[ "$_pmbartist" != "null" && -n "$_pmbartist" ]] && PROV_LOOKUP["${_psi}|mb.artist.${_pmbartist}"]="$_pid"
|
||||
[[ "$_pmbalbum" != "null" && -n "$_pmbalbum" ]] && PROV_LOOKUP["${_psi}|mb.album.${_pmbalbum}"]="$_pid"
|
||||
# tvdb: namespace by type so Series and Episode IDs don't collide
|
||||
if [[ "$_ptvdb" != "null" && -n "$_ptvdb" ]]; then
|
||||
case "$_ptype" in
|
||||
Series) PROV_LOOKUP["${_psi}|tvdb.series.${_ptvdb}"]="$_pid" ;;
|
||||
Episode) PROV_LOOKUP["${_psi}|tvdb.${_ptvdb}"]="$_pid" ;;
|
||||
*) PROV_LOOKUP["${_psi}|tvdb.${_ptvdb}"]="$_pid" ;;
|
||||
esac
|
||||
fi
|
||||
done < <(echo "$_page" | jq -r '.Items[] | [
|
||||
.Id,
|
||||
.Type,
|
||||
(.ProviderIds.Tvdb // "null"),
|
||||
(.ProviderIds.Imdb // "null"),
|
||||
(.ProviderIds.Tmdb // "null"),
|
||||
(.ProviderIds.MusicBrainzTrackId // "null"),
|
||||
(.ProviderIds.MusicBrainzArtistId // "null"),
|
||||
(.ProviderIds.MusicBrainzAlbumId // "null")
|
||||
] | @tsv' 2>/dev/null)
|
||||
_prov_start=$(( _prov_start + _prov_count ))
|
||||
[[ "$_prov_total" -gt 0 && "$_prov_start" -ge "$_prov_total" ]] && break
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
# ── Step 3: Sync per matched user ────────────────────────────────────────────
|
||||
@@ -559,6 +605,136 @@ for lname in "${!USER_MAP[@]}"; do
|
||||
unset ITEM_MAP U_IDX U_UID
|
||||
done
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Favorite Sync ━━━
|
||||
# ==============================================================================================
|
||||
# Union semantics — if favorited on any server, sync to all others. Never unmarks.
|
||||
# Covers MusicArtist, MusicAlbum, Movie, Series (configured via PLAY_SYNC_FAV_TYPES).
|
||||
FAV_TOTAL_SYNCED=0
|
||||
FAV_TOTAL_SKIPPED=0
|
||||
FAV_TOTAL_ERRORS=0
|
||||
|
||||
IFS=',' read -ra _fav_type_list <<< "$FAV_TYPES"
|
||||
|
||||
for lname in "${!USER_MAP[@]}"; do
|
||||
read -ra _pairs <<< "${USER_MAP[$lname]}"
|
||||
[[ "${#_pairs[@]}" -lt 2 ]] && continue
|
||||
|
||||
declare -A UF_IDX UF_UID
|
||||
for _pair in "${_pairs[@]}"; do
|
||||
IFS=':' read -r _si _ui <<< "$_pair"
|
||||
UF_IDX["$_si"]="$_si"
|
||||
UF_UID["$_si"]="$_ui"
|
||||
done
|
||||
|
||||
declare -A FAV_MAP # provider_key → space-separated "si:iid" pairs
|
||||
|
||||
for _si in "${!UF_IDX[@]}"; do
|
||||
_uid="${UF_UID[$_si]}"
|
||||
for _ftype in "${_fav_type_list[@]}"; do
|
||||
_resp=$(_api_get "${SRV_URL[$_si]}" "${SRV_KEY[$_si]}" \
|
||||
"Users/${_uid}/Items?Recursive=true&IncludeItemTypes=${_ftype}&Filters=IsFavorite&Fields=ProviderIds")
|
||||
[[ -z "$_resp" ]] && continue
|
||||
_fcount=$(echo "$_resp" | jq '.Items | length' 2>/dev/null || echo 0)
|
||||
[[ "$_fcount" -eq 0 ]] && continue
|
||||
log " ${SRV_NAME[$_si]} — $_fcount ${_ftype} favorite(s) for $lname"
|
||||
|
||||
while IFS=$'\t' read -r _iid _itype _pimdb _ptmdb _ptvdb _pmbartist _pmbalbum _pmbtrack; do
|
||||
_pkey=""
|
||||
case "$_itype" in
|
||||
Movie)
|
||||
[[ "$_pimdb" != "null" && -n "$_pimdb" ]] && _pkey="imdb:${_pimdb}"
|
||||
[[ -z "$_pkey" && "$_ptmdb" != "null" && -n "$_ptmdb" ]] && _pkey="tmdb:movie:${_ptmdb}"
|
||||
;;
|
||||
Series)
|
||||
[[ "$_ptvdb" != "null" && -n "$_ptvdb" ]] && _pkey="tvdb:series:${_ptvdb}"
|
||||
;;
|
||||
MusicArtist)
|
||||
[[ "$_pmbartist" != "null" && -n "$_pmbartist" ]] && _pkey="mb:artist:${_pmbartist}"
|
||||
;;
|
||||
MusicAlbum)
|
||||
[[ "$_pmbalbum" != "null" && -n "$_pmbalbum" ]] && _pkey="mb:album:${_pmbalbum}"
|
||||
;;
|
||||
Audio)
|
||||
[[ "$_pmbtrack" != "null" && -n "$_pmbtrack" ]] && _pkey="mb:track:${_pmbtrack}"
|
||||
;;
|
||||
esac
|
||||
[[ -z "$_pkey" ]] && continue
|
||||
|
||||
if [[ -n "${FAV_MAP[$_pkey]:-}" ]]; then
|
||||
FAV_MAP[$_pkey]+=" ${_si}:${_iid}"
|
||||
else
|
||||
FAV_MAP[$_pkey]="${_si}:${_iid}"
|
||||
fi
|
||||
done < <(echo "$_resp" | jq -r '.Items[] | [
|
||||
.Id, .Type,
|
||||
(.ProviderIds.Imdb // "null"),
|
||||
(.ProviderIds.Tmdb // "null"),
|
||||
(.ProviderIds.Tvdb // "null"),
|
||||
(.ProviderIds.MusicBrainzArtistId // "null"),
|
||||
(.ProviderIds.MusicBrainzAlbumId // "null"),
|
||||
(.ProviderIds.MusicBrainzTrackId // "null")
|
||||
] | @tsv' 2>/dev/null)
|
||||
done
|
||||
done
|
||||
|
||||
for _pkey in "${!FAV_MAP[@]}"; do
|
||||
declare -A _fhave _fiid
|
||||
for _entry in ${FAV_MAP[$_pkey]}; do
|
||||
IFS=':' read -r _si _iid <<< "$_entry"
|
||||
_fhave[$_si]=true
|
||||
_fiid[$_si]="$_iid"
|
||||
done
|
||||
|
||||
for _si in "${!UF_IDX[@]}"; do
|
||||
[[ "${_fhave[$_si]:-false}" == "true" ]] && (( FAV_TOTAL_SKIPPED++ )) && continue
|
||||
_uid="${UF_UID[$_si]}"
|
||||
|
||||
# Look up item ID on target server via PROV_LOOKUP
|
||||
_iid=""
|
||||
_ptype="${_pkey%%:*}"
|
||||
case "$_ptype" in
|
||||
imdb) _iid="${PROV_LOOKUP[${_si}|imdb.${_pkey#imdb:}]:-}" ;;
|
||||
tmdb) _iid="${PROV_LOOKUP[${_si}|tmdb.${_pkey#tmdb:movie:}]:-}" ;;
|
||||
tvdb) _iid="${PROV_LOOKUP[${_si}|tvdb.series.${_pkey#tvdb:series:}]:-}" ;;
|
||||
mb)
|
||||
_mbsub="${_pkey#mb:}"
|
||||
case "${_mbsub%%:*}" in
|
||||
artist) _iid="${PROV_LOOKUP[${_si}|mb.artist.${_mbsub#artist:}]:-}" ;;
|
||||
album) _iid="${PROV_LOOKUP[${_si}|mb.album.${_mbsub#album:}]:-}" ;;
|
||||
track) _iid="${PROV_LOOKUP[${_si}|mb.${_mbsub#track:}]:-}" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "$_iid" ]]; then
|
||||
log " SKIP FAV $_pkey → ${SRV_NAME[$_si]} not in library"
|
||||
continue
|
||||
fi
|
||||
|
||||
log " FAV $_pkey → ${SRV_NAME[$_si]} ($lname)"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
_http=$(_api_post "${SRV_URL[$_si]}" "${SRV_KEY[$_si]}" \
|
||||
"Users/${_uid}/FavoriteItems/${_iid}")
|
||||
if [[ "$_http" == "200" || "$_http" == "201" ]]; then
|
||||
success " ✓ FAV $_pkey → ${SRV_NAME[$_si]}"
|
||||
(( FAV_TOTAL_SYNCED++ ))
|
||||
else
|
||||
warn " ✗ FAV $_pkey → ${SRV_NAME[$_si]} failed (HTTP ${_http:-err})"
|
||||
(( FAV_TOTAL_ERRORS++ ))
|
||||
fi
|
||||
else
|
||||
echo " DRY RUN: would favorite $_pkey → ${SRV_NAME[$_si]} user=$lname"
|
||||
(( FAV_TOTAL_SYNCED++ ))
|
||||
fi
|
||||
done
|
||||
|
||||
unset _fhave _fiid
|
||||
done
|
||||
|
||||
unset FAV_MAP UF_IDX UF_UID
|
||||
done
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
@@ -569,6 +745,8 @@ echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
|
||||
echo "$ICON_DONE Synced: $TOTAL_SYNCED"
|
||||
[[ "$TOTAL_SKIPPED" -gt 0 ]] && echo "$ICON_RUNNING Skipped: $TOTAL_SKIPPED (already current)"
|
||||
[[ "$TOTAL_ERRORS" -gt 0 ]] && echo "$ICON_ERROR Errors: $TOTAL_ERRORS"
|
||||
echo ""
|
||||
echo " Favorites — synced: $FAV_TOTAL_SYNCED skipped: $FAV_TOTAL_SKIPPED errors: $FAV_TOTAL_ERRORS"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — no changes written"
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Varaverk: fire all jobs with cron=array_stop (foreground — blocks until all done).
|
||||
php -r "
|
||||
require_once '/usr/local/emhttp/plugins/varaverk/include/scheduler.php';
|
||||
\$s = vv_schedule_load();
|
||||
\$runner = '/usr/local/emhttp/plugins/varaverk/run_job.sh';
|
||||
foreach (\$s as \$id => \$e) {
|
||||
if (strncmp(\$id, '__', 2) === 0) continue;
|
||||
if ((\$e['cron'] ?? '') !== 'array_stop') continue;
|
||||
if (empty(\$e['enabled'])) continue;
|
||||
\$script = SCRIPTS_DIR . '/' . \$id;
|
||||
if (!file_exists(\$script)) continue;
|
||||
\$flags = !empty(\$e['log_enabled']) ? ' --log' : '';
|
||||
passthru('bash ' . escapeshellarg(\$runner) . ' ' . escapeshellarg(\$id) . ' ' . escapeshellarg(\$script) . \$flags);
|
||||
}
|
||||
" 2>/dev/null
|
||||
# Varaverk: run array_stopping.sh on array stop (foreground — blocks until done).
|
||||
# Direct bash call to avoid PHP/scheduler chain failing silently at shutdown time.
|
||||
SCRIPTS_DIR="/boot/config/plugins/varaverk"
|
||||
cfg="$SCRIPTS_DIR/varaverk.cfg"
|
||||
[[ -f "$cfg" ]] && _sd=$(grep -oP '(?<=^SCRIPTS_DIR=")[^"]+' "$cfg" 2>/dev/null) && [[ -n "$_sd" ]] && SCRIPTS_DIR="$_sd"
|
||||
runner="$SCRIPTS_DIR/Plugin/unraid/run_job.sh"
|
||||
script="$SCRIPTS_DIR/Orchestrators/array_stopping.sh"
|
||||
[[ -f "$script" ]] && bash "$runner" "Orchestrators/array_stopping.sh" "$script"
|
||||
|
||||
Reference in New Issue
Block a user