Add docker actions, arr profile enforcer, monitor caching, and web file symlink

Web files now served via symlink to the git repo so git pull changes survive
reboots without rebuilding the txz. Also includes: docker pull/rebuild/restart
with live log streaming, arr_profile_enforcer for Sonarr/Radarr quality
profiles, monitor page cache fix (background writer now in cron), and
ARR_KIDS/SONARR/RADARR profile name vars in master.conf.
This commit is contained in:
Gmer4Lfe
2026-06-19 11:09:40 -04:00
parent ac2986b141
commit f42ecc8464
15 changed files with 657 additions and 62 deletions
+59 -23
View File
@@ -270,6 +270,32 @@ for i in $(seq 0 $(( _srv_count - 1 ))); do
done < <(echo "${SRV_USERS[$i]}" | jq -r '.[] | [.Id, .Name] | @tsv' 2>/dev/null)
done
# ── Step 2.5: Pre-build provider ID → item ID lookup map ─────────────────────
# AnyProviderIdEquals is broken in Jellyfin 10.11+ (ignores the filter entirely).
# Pre-fetching all items' provider IDs once and using a local map avoids the broken
# per-item API call and is faster overall.
declare -A PROV_LOOKUP # "si|tvdb.{id}" | "si|imdb.{id}" | "si|mb.{id}" → item_id
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)
done
# ── Step 3: Sync per matched user ────────────────────────────────────────────
_date_filter=""
if [[ "$SYNC_DAYS" -gt 0 ]]; then
@@ -386,23 +412,28 @@ for lname in "${!USER_MAP[@]}"; do
[[ "$_has_entries" == false ]] && continue
# Find the authoritative server: newest LastPlayedDate epoch
# Tie-break: higher PlayCount, then higher Ticks
# Tie-break: higher PlayCount, then higher Ticks, then Played=true
# Init at -1 so servers with epoch=0 (batch-marks with null LastPlayedDate) can win
_auth_si=""
_auth_epoch=0
_auth_pcount=0
_auth_ticks=0
_auth_epoch=-1
_auth_pcount=-1
_auth_ticks=-1
_auth_pf="false"
for _si in "${!E_SIDX[@]}"; do
_e="${E_EPOCH[$_si]:-0}"
_pc="${E_PCOUNT[$_si]:-0}"
_tk="${E_TICKS[$_si]:-0}"
_pf="${E_PLAYED[$_si]:-false}"
if [[ "$_e" -gt "$_auth_epoch" ]] || \
[[ "$_e" -eq "$_auth_epoch" && "$_pc" -gt "$_auth_pcount" ]] || \
[[ "$_e" -eq "$_auth_epoch" && "$_pc" -eq "$_auth_pcount" && "$_tk" -gt "$_auth_ticks" ]]; then
[[ "$_e" -eq "$_auth_epoch" && "$_pc" -eq "$_auth_pcount" && "$_tk" -gt "$_auth_ticks" ]] || \
[[ "$_e" -eq "$_auth_epoch" && "$_pc" -eq "$_auth_pcount" && "$_tk" -eq "$_auth_ticks" && "$_pf" == "true" && "$_auth_pf" != "true" ]]; then
_auth_si="$_si"
_auth_epoch="$_e"
_auth_pcount="$_pc"
_auth_ticks="$_tk"
_auth_pf="$_pf"
fi
done
@@ -422,12 +453,16 @@ for lname in "${!USER_MAP[@]}"; do
# Skip if they already have the same/newer state
_skip=false
if [[ "$_auth_played" == "true" ]]; then
# Played: skip if target is already marked played with same/newer date
[[ "$_their_epoch" -ge "$_auth_epoch" && "$_their_played" == "true" ]] && _skip=true
# Both servers already have this played — nothing to propagate regardless of dates.
# Date-based comparison caused a ping-pong: syncing without a DatePlayed param lets
# the target server stamp the current time, making it the new authority next cycle.
[[ "$_their_played" == "true" ]] && _skip=true
else
# Resume only: skip if target already has same or more ticks
# (posting ticks via /UserData doesn't set LastPlayedDate, so epoch comparison is useless here)
[[ "${E_TICKS[$_si]:-0}" -ge "${_auth_ticks:-0}" && "$_their_played" == "false" ]] && _skip=true
# Resume only: skip if target already has same or more ticks.
# Allow 5-second tolerance (50_000_000 ticks) — Emby may round tick values slightly
# differently on read, causing an exact-match check to miss and re-sync every cycle.
_tick_gap=$(( ${_auth_ticks:-0} - ${E_TICKS[$_si]:-0} ))
[[ "$_tick_gap" -le 50000000 && "${E_TICKS[$_si]:-0}" -gt 0 && "$_their_played" == "false" ]] && _skip=true
fi
if [[ "$_skip" == true ]]; then
log " SKIP $_pkey${SRV_NAME[$_si]} already up to date"
@@ -441,24 +476,25 @@ for lname in "${!USER_MAP[@]}"; do
# Find item ID on target server by provider key if not in our map
if [[ -z "$_iid" ]]; then
_ptype="${_pkey%%:*}"
_pval="${_pkey##*:}"
case "$_ptype" in
imdb) _search_field="imdb.${_pval}" ;;
tmdb) _search_field="tmdb.${_pval##movie:}" ;;
imdb)
_pval="${_pkey#imdb:}"
_iid="${PROV_LOOKUP[${_si}|imdb.${_pval}]:-}"
;;
tmdb)
_pval="${_pkey#tmdb:movie:}"
_iid="${PROV_LOOKUP[${_si}|tmdb.${_pval}]:-}"
;;
tvdb)
# _pkey format: tvdb:ep:{tvdb_id}:s{season}e{ep}
# ##*: gives "s7e2" (wrong); strip prefix then first :
# pkey format: tvdb:ep:{tvdb_id}:s{season}e{ep}
_tvdb_num="${_pkey#tvdb:ep:}"; _tvdb_num="${_tvdb_num%%:*}"
_search_field="tvdb.${_tvdb_num}"
_iid="${PROV_LOOKUP[${_si}|tvdb.${_tvdb_num}]:-}"
;;
mb)
_pval="${_pkey#mb:track:}"
_iid="${PROV_LOOKUP[${_si}|mb.${_pval}]:-}"
;;
mb) _search_field="" ;; # skip music if not found
esac
if [[ -n "$_search_field" ]]; then
_iid=$(_api_get "${SRV_URL[$_si]}" "${SRV_KEY[$_si]}" \
"Items?AnyProviderIdEquals=${_search_field}&Recursive=true&Fields=ProviderIds&ExcludeLocationTypes=Virtual&Limit=1" 2>/dev/null \
| jq -r '.Items[0].Id // empty' 2>/dev/null)
fi
[[ -z "$_iid" ]] && log " SKIP $_pkey${SRV_NAME[$_si]} item not found on server" && continue
fi