massive update. Master conf split, now modular with a load sceriprt to drive all configs to scripts. with unraid scpecific safeguard tests , and improved standardized ux. including dynamic host detect, who am i who else it there. EVERY SINGLE SCRIPT UPDATED. DEBATING THAT THIS IS ACUALLY V2

This commit is contained in:
2026-05-03 17:16:49 -04:00
parent 2691a35e80
commit ec7de648dc
72 changed files with 25640 additions and 14629 deletions
+253 -235
View File
@@ -1,63 +1,105 @@
#!/bin/bash
# -----------------------------------------------------------------------------------------------
# --------------------------------- Transcode Manager ------------------------------------------
# -----------------------------------------------------------------------------------------------
# ==============================================================================================
# ================================= Transcode Manager ==========================================
# ==============================================================================================
# Manages Emby transcode storage using filesystem symlink indirection.
# ffmpeg resolves the symlink ONCE at session start — existing sessions are never affected.
# Only new sessions care about where the symlink currently points.
# Called every 5 minutes by User Scripts — must be fast, non-blocking, and silent when healthy.
#
# Three operating modes — set TRANSCODE_MANAGER_MODE in Master.conf:
# ── HOW IT WORKS ──────────────────────────────────────────────────────────────────────────────
# Emby's transcode path is set to TRANSCODE_LINK (a symlink).
# ffmpeg resolves the symlink ONCE at session start — existing sessions are never affected.
# Only NEW sessions care about where the symlink currently points.
# Flipping the symlink mid-stream is safe — in-progress transcodes continue uninterrupted.
#
# ── THREE MODES ───────────────────────────────────────────────────────────────────────────────
# smart — auto-flips between ramdisk and SSD based on usage thresholds (default)
# ramdisk — always uses ramdisk, never flips to SSD regardless of usage
# ramdisk above RAMDISK_WARN_GB → flip to SSD
# ramdisk below RAMDISK_LOW_GB → flip back to ramdisk
# ramdisk — always uses ramdisk, warns if above threshold, never flips
# ssd — always uses SSD, never uses ramdisk
#
# Safety checks on every run regardless of mode:
# Ramdisk disappeared → auto-flip to SSD, notify warning
# SSD path missing → disable SSD fallback, notify warning
# Symlink broken → auto-recreate, notify
# Symlink missing → auto-recreate, notify
# Permissions drift → fix silently
# Emby not running → skip threshold checks, verify symlink only
# ── SAFETY CHECKS — EVERY RUN ─────────────────────────────────────────────────────────────────
# Symlink missing/broken → auto-recreate pointing at ramdisk, notify
# Ramdisk disappeared → auto-flip to SSD, notify warning
# SSD path missing → disable SSD fallback / error if mode=ssd
# transcoding-temp missing → recreate on ramdisk immediately
# Permissions drift → fix silently
# Emby not running → skip threshold checks, verify symlink only
#
# Session display shows active Emby streams with storage state.
# Split state detected when sessions exist on both ramdisk and SSD simultaneously —
# ── SESSION DISPLAY ───────────────────────────────────────────────────────────────────────────
# Shows active Emby/Jellyfin/Plex streams with user, title, type, and play method.
# Split state shown when sessions exist on both ramdisk and SSD simultaneously —
# this happens naturally when symlink flips mid-session.
#
# All configuration in Master.conf under Transcode Manager section.
# Supports --dry-run and --status.
# -----------------------------------------------------------------------------------------------
# ── DAILY LOG ─────────────────────────────────────────────────────────────────────────────────
# Appends to TRANSCODE_DAILY_LOG after each run — read by weekly_health_digest.sh.
# Format: DATE|RAMDISK_USED_GB|FLIP_COUNT|RAM_SESSION_COUNT|SSD_SESSION_COUNT|FILES_CLEANED
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# detect_hosts() sets MY_ID and aliases HOST*_TRANSCODE_SERVERS, HOST*_RAMDISK_PATH,
# HOST*_TRANSCODE_SSD, HOST*_RAMDISK_WARN_GB, HOST*_RAMDISK_LOW_GB, HOST*_RAMDISK_SIZE.
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock "wait" — wait if previous run still active
# detect_hosts() — correct paths and thresholds per host
# DOCKER_TIMEOUT — all docker calls protected against daemon hangs
# validate_unraid_cmd — notify validated before use
# Silent by default — runs every 5 minutes, only speaks when something changes
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# transcode_manager.sh — normal run
# transcode_manager.sh --dry-run — preview without making changes
# transcode_manager.sh --status — show current state and exit
# transcode_manager.sh --log — verbose output
# transcode_manager.sh --no-log — suppress daily log write (called by cleanup)
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../Master.conf"
source "$SCRIPT_DIR/../common.sh"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
# Handle --no-log flag before parse_args
NO_LOG=false
FILTERED_ARGS=()
for arg in "$@"; do
case "$arg" in
--no-log) NO_LOG=true ;;
*) FILTERED_ARGS+=("$arg") ;;
esac
done
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_GEAR Setup ━━━
# -----------------------------------------------------------------------------------------------
echo ""
echo "━━━ $ICON_GEAR Setup ━━━"
parse_args "${FILTERED_ARGS[@]}"
DOCKER_TIMEOUT=15
STATE_FILE="/tmp/transcode_state.db"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
success "Running as root"
validate_unraid_cmd \
"/usr/local/emhttp/plugins/dynamix/scripts/notify" \
"" "" \
"unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"
acquire_lock "wait"
# Primary server for container running check — first entry in TRANSCODE_SERVERS
# Full session display loops over all entries in the array
# detect_hosts() sets MY_ID and aliases all HOST*_TRANSCODE_* vars
detect_hosts
# Primary container — first entry in TRANSCODE_SERVERS
TRANSCODE_EMBY_CONTAINER="Emby"
if [[ "${#TRANSCODE_SERVERS[@]}" -gt 0 ]]; then
IFS='|' read -r PRIMARY_CONTAINER _ _ _ <<< "${TRANSCODE_SERVERS[0]}"
TRANSCODE_EMBY_CONTAINER="${PRIMARY_CONTAINER:-Emby}"
fi
case "$TRANSCODE_MANAGER_MODE" in
smart|ramdisk|ssd) info "$ICON_GEAR Mode: $TRANSCODE_MANAGER_MODE" ;;
smart|ramdisk|ssd) log "Mode: $TRANSCODE_MANAGER_MODE" ;;
*)
warn "Unknown TRANSCODE_MANAGER_MODE: $TRANSCODE_MANAGER_MODE — defaulting to smart"
TRANSCODE_MANAGER_MODE="smart"
@@ -66,12 +108,13 @@ esac
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_SUMMARY Status ━━━
# -----------------------------------------------------------------------------------------------
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "━━━━━ $ICON_SUMMARY TRANSCODE MANAGER STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Mode: $TRANSCODE_MANAGER_MODE"
echo "$ICON_RAM Ramdisk: $RAMDISK_PATH ($RAMDISK_SIZE)"
echo "$ICON_LINK Symlink: $TRANSCODE_LINK"
@@ -85,19 +128,19 @@ if [[ "$SHOW_STATUS" == true ]]; then
CURRENT_TARGET=$(readlink "$TRANSCODE_LINK" 2>/dev/null || echo "missing")
echo "$ICON_LINK Symlink now: $TRANSCODE_LINK$CURRENT_TARGET"
if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
USED_KB=$(df "$RAMDISK_PATH" --output=used | tail -1 | tr -d ' ')
USED_GB=$(awk "BEGIN {printf \"%.2f\", $USED_KB / 1048576}")
USED_GB=$(df "$RAMDISK_PATH" --output=used 2>/dev/null | \
tail -1 | tr -d ' ' | awk '{printf "%.2f", $1/1048576}')
echo "$ICON_RAM Ramdisk now: ${USED_GB}GB used"
else
echo "$ICON_RAM Ramdisk: NOT MOUNTED"
echo "$ICON_RAM Ramdisk: NOT MOUNTED"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# -----------------------------------------------------------------------------------------------
# HELPERS
# -----------------------------------------------------------------------------------------------
# ==============================================================================================
# ── HELPER FUNCTIONS ──────────────────────────────────────────────────────────────────────────
# ==============================================================================================
flip_symlink() {
local target="$1" reason="$2"
@@ -106,7 +149,7 @@ flip_symlink() {
return 0
fi
ln -sfn "$target" "$TRANSCODE_LINK"
success "$ICON_LINK Symlink flipped to: $target ($reason)"
warn "$ICON_LINK Symlink flipped to: $target ($reason)"
}
fix_permissions() {
@@ -118,30 +161,27 @@ fix_permissions() {
}
get_ramdisk_used_gb() {
local used_kb
used_kb=$(df "$RAMDISK_PATH" --output=used 2>/dev/null | tail -1 | tr -d ' ')
awk "BEGIN {printf \"%.2f\", ${used_kb:-0} / 1048576}"
df "$RAMDISK_PATH" --output=used 2>/dev/null | \
tail -1 | tr -d ' ' | awk '{printf "%.2f", $1/1048576}'
}
get_ramdisk_avail_gb() {
local avail_kb
avail_kb=$(df "$RAMDISK_PATH" --output=avail 2>/dev/null | tail -1 | tr -d ' ')
awk "BEGIN {printf \"%.2f\", ${avail_kb:-0} / 1048576}"
df "$RAMDISK_PATH" --output=avail 2>/dev/null | \
tail -1 | tr -d ' ' | awk '{printf "%.2f", $1/1048576}'
}
get_ssd_free_gb() {
local free_kb
free_kb=$(df "$TRANSCODE_SSD" --output=avail 2>/dev/null | tail -1 | tr -d ' ')
awk "BEGIN {printf \"%.2f\", ${free_kb:-0} / 1048576}"
df "$TRANSCODE_SSD" --output=avail 2>/dev/null | \
tail -1 | tr -d ' ' | awk '{printf "%.2f", $1/1048576}'
}
get_flip_count() {
local state_file="/tmp/transcode_flip_state.db"
local current_hour
current_hour=$(date '+%Y-%m-%d-%H')
if [[ ! -f "$state_file" ]]; then echo "0"; return; fi
[[ ! -f "$state_file" ]] && echo "0" && return
local stored_hour stored_count
stored_hour=$(awk -F'|' 'NR==1{print $1}' "$state_file" 2>/dev/null)
stored_hour=$(awk -F'|' 'NR==1{print $1}' "$state_file" 2>/dev/null)
stored_count=$(awk -F'|' 'NR==1{print $2}' "$state_file" 2>/dev/null)
[[ "$stored_hour" == "$current_hour" ]] && echo "${stored_count:-0}" || echo "0"
}
@@ -152,76 +192,55 @@ increment_flip_count() {
current_hour=$(date '+%Y-%m-%d-%H')
local current_count
current_count=$(get_flip_count)
current_count=$((current_count + 1))
current_count=$(( current_count + 1 ))
echo "${current_hour}|${current_count}" > "$state_file"
echo "$current_count"
}
emby_api() {
local endpoint="$1"
[[ -z "$EMBY_API_KEY" ]] && return 1
curl -sf \
--max-time 10 \
-H "X-Emby-Token: $EMBY_API_KEY" \
"${EMBY_URL}/${endpoint}" 2>/dev/null
}
get_media_type_label() {
local type="$1"
case "$type" in
LiveTv) echo "Live TV" ;;
TvChannel) echo "Live TV" ;;
Episode) echo "TV Show" ;;
Movie) echo "Movie" ;;
Audio) echo "Music" ;;
MusicVideo) echo "Music Video" ;;
*) echo "$type" ;;
case "$1" in
LiveTv|TvChannel) echo "Live TV" ;;
Episode) echo "TV Show" ;;
Movie) echo "Movie" ;;
Audio) echo "Music" ;;
MusicVideo) echo "Music Video" ;;
*) echo "$1" ;;
esac
}
get_play_method_label() {
local method="$1"
case "$method" in
Transcode) echo "Transcode" ;;
DirectStream) echo "Direct Stream" ;;
DirectPlay) echo "Direct Play" ;;
*) echo "$method" ;;
case "$1" in
Transcode) echo "Transcode" ;;
DirectStream) echo "Direct Stream" ;;
DirectPlay) echo "Direct Play" ;;
*) echo "$1" ;;
esac
}
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_RAM Transcode Manager ━━━
# -----------------------------------------------------------------------------------------------
echo ""
echo "━━━ $ICON_RAM Transcode Manager — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
echo "$ICON_GEAR Mode: $TRANSCODE_MANAGER_MODE"
echo ""
# ==============================================================================================
# ━━━ Transcode Manager ━━━
# ==============================================================================================
START=$(date +%s)
RAMDISK_HEALTHY=true
SSD_HEALTHY=true
EMBY_RUNNING=true
SOMETHING_HAPPENED=false # controls whether summary is printed
# -----------------------------------------------------------------------------------------------
# CHECK 1 — Emby running
# -----------------------------------------------------------------------------------------------
# ── Check 1 — Emby running ───────────────────────────────────────────────────────────────────
if [[ "$TRANSCODE_CHECK_EMBY" == true ]]; then
info "$ICON_CONTAINERS Checking $TRANSCODE_EMBY_CONTAINER..."
if ! docker inspect "$TRANSCODE_EMBY_CONTAINER" \
log "Checking $TRANSCODE_EMBY_CONTAINER..."
if ! timeout "$DOCKER_TIMEOUT" docker inspect "$TRANSCODE_EMBY_CONTAINER" \
--format '{{.State.Running}}' 2>/dev/null | grep -q "true"; then
warn "$TRANSCODE_EMBY_CONTAINER is not running — skipping threshold checks"
EMBY_RUNNING=false
SOMETHING_HAPPENED=true
else
success "$TRANSCODE_EMBY_CONTAINER is running"
log "$TRANSCODE_EMBY_CONTAINER is running"
fi
fi
# -----------------------------------------------------------------------------------------------
# CHECK 2 — Symlink integrity
# -----------------------------------------------------------------------------------------------
echo ""
info "$ICON_LINK Checking symlink integrity..."
# ── Check 2 — Symlink integrity ───────────────────────────────────────────────────────────────
log "Checking symlink integrity..."
CURRENT_TARGET=$(readlink "$TRANSCODE_LINK" 2>/dev/null)
if [[ -z "$CURRENT_TARGET" ]]; then
@@ -229,175 +248,162 @@ if [[ -z "$CURRENT_TARGET" ]]; then
if [[ "$DRY_RUN" == false ]]; then
ln -sfn "$RAMDISK_PATH" "$TRANSCODE_LINK"
CURRENT_TARGET="$RAMDISK_PATH"
notify "Transcode symlink was missing on $(hostname) — recreated" "Transcode Manager" "warning"
notify "Transcode symlink was missing on $(hostname) ($MY_ID) — recreated" \
"Transcode Manager" "warning"
fi
SOMETHING_HAPPENED=true
elif [[ ! -e "$CURRENT_TARGET" ]]; then
warn "$ICON_LINK Symlink target missing: $CURRENT_TARGET — resetting to ramdisk"
if [[ "$DRY_RUN" == false ]]; then
ln -sfn "$RAMDISK_PATH" "$TRANSCODE_LINK"
CURRENT_TARGET="$RAMDISK_PATH"
notify "Transcode symlink target was missing on $(hostname) — reset to ramdisk" "Transcode Manager" "warning"
notify "Transcode symlink target was missing on $(hostname) ($MY_ID) — reset to ramdisk" \
"Transcode Manager" "warning"
fi
SOMETHING_HAPPENED=true
else
success "$ICON_LINK Symlink valid: $TRANSCODE_LINK$CURRENT_TARGET"
log "Symlink valid: $TRANSCODE_LINK$CURRENT_TARGET"
fi
# -----------------------------------------------------------------------------------------------
# CHECK 3 — Ramdisk health
# -----------------------------------------------------------------------------------------------
echo ""
info "$ICON_RAM Checking ramdisk..."
# ── Check 3 — Ramdisk health ──────────────────────────────────────────────────────────────────
log "Checking ramdisk..."
if ! mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
error "$ICON_RAM Ramdisk not mounted at $RAMDISK_PATH"
error "Ramdisk not mounted at $RAMDISK_PATH"
RAMDISK_HEALTHY=false
SOMETHING_HAPPENED=true
if [[ "$DRY_RUN" == false ]]; then
warn "Flipping symlink to SSD — ramdisk unavailable"
flip_symlink "$TRANSCODE_SSD" "ramdisk disappeared"
CURRENT_TARGET="$TRANSCODE_SSD"
notify "Ramdisk disappeared on $(hostname) — transcodes falling back to SSD. Run ramdisk_setup.sh to restore." "Transcode Manager" "warning"
notify "Ramdisk disappeared on $(hostname) ($MY_ID) — transcodes falling back to SSD. Run ramdisk_setup.sh to restore." \
"Transcode Manager" "warning"
fi
else
RAMDISK_SIZE_ACTUAL=$(df "$RAMDISK_PATH" --output=size -h | tail -1 | tr -d ' ')
success "$ICON_RAM Ramdisk mounted — size: $RAMDISK_SIZE_ACTUAL"
RAMDISK_SIZE_ACTUAL=$(df "$RAMDISK_PATH" --output=size -h 2>/dev/null | tail -1 | tr -d ' ')
log "Ramdisk mounted — size: $RAMDISK_SIZE_ACTUAL"
fix_permissions "$RAMDISK_PATH"
# Guarantee transcoding-temp exists on ramdisk
# If missing Emby creates it wherever it finds a writable path first —
# which may be the SSD fallback — locking all new sessions onto SSD
# even when the symlink correctly points at the ramdisk.
TRANSCODE_TEMP_RAM="${RAMDISK_PATH}/transcoding-temp"
TRANSCODE_TEMP_SSD="${TRANSCODE_SSD}/transcoding-temp"
if [[ ! -d "$TRANSCODE_TEMP_RAM" ]]; then
warn "transcoding-temp missing from ramdisk — creating now"
mkdir -p "$TRANSCODE_TEMP_RAM"
chmod "$TRANSCODE_CHMOD" "$TRANSCODE_TEMP_RAM"
chown "$TRANSCODE_OWNER" "$TRANSCODE_TEMP_RAM"
success "transcoding-temp created on ramdisk — new sessions will use ramdisk"
warn "transcoding-temp created on ramdisk — new sessions will use ramdisk"
SOMETHING_HAPPENED=true
else
log "transcoding-temp exists on ramdisk — ok"
log "transcoding-temp exists on ramdisk "
fi
fi
# -----------------------------------------------------------------------------------------------
# CHECK 4 — SSD health
# -----------------------------------------------------------------------------------------------
echo ""
info "$ICON_DISK Checking SSD fallback..."
# ── Check 4 — SSD health ─────────────────────────────────────────────────────────────────────
log "Checking SSD fallback..."
if [[ ! -d "$TRANSCODE_SSD" ]]; then
warn "$ICON_DISK SSD fallback path missing: $TRANSCODE_SSD"
warn "SSD fallback path missing: $TRANSCODE_SSD"
SSD_HEALTHY=false
SOMETHING_HAPPENED=true
if [[ "$TRANSCODE_MANAGER_MODE" == "ssd" ]]; then
error "Mode is 'ssd' but SSD path is missing — cannot continue"
notify "Transcode SSD path missing on $(hostname) — mode is 'ssd', manual intervention needed" "Transcode Manager" "warning"
notify "Transcode SSD path missing on $(hostname) ($MY_ID) — mode is 'ssd', manual intervention needed" \
"Transcode Manager" "warning"
exit 1
else
warn "SSD fallback disabled — will stay on ramdisk"
fi
else
SSD_FREE_GB=$(get_ssd_free_gb)
success "$ICON_DISK SSD available — ${SSD_FREE_GB}GB free"
log "SSD available — ${SSD_FREE_GB}GB free"
fix_permissions "$TRANSCODE_SSD"
fi
# -----------------------------------------------------------------------------------------------
# USAGE STATS
# -----------------------------------------------------------------------------------------------
echo ""
# ── Usage stats ───────────────────────────────────────────────────────────────────────────────
RAMDISK_USED_GB="0.00"
RAMDISK_AVAIL_GB="0.00"
RAMDISK_FILES=0
if [[ "$RAMDISK_HEALTHY" == true ]]; then
RAMDISK_USED_GB=$(get_ramdisk_used_gb)
RAMDISK_AVAIL_GB=$(get_ramdisk_avail_gb)
RAMDISK_FILES=$(find "$RAMDISK_PATH" -type f 2>/dev/null | wc -l)
RAMDISK_SIZE_H=$(du -sh "$RAMDISK_PATH" 2>/dev/null | cut -f1)
fi
SSD_FILES=$(find "$TRANSCODE_SSD" -type f 2>/dev/null | wc -l)
SSD_SIZE_H=$(du -sh "$TRANSCODE_SSD" 2>/dev/null | cut -f1)
info "$ICON_RAM Ramdisk usage: ${RAMDISK_USED_GB}GB used / ${RAMDISK_AVAIL_GB}GB available"
info "$ICON_RAM Ramdisk files: ${RAMDISK_FILES:-0} ($RAMDISK_SIZE_H)"
info "$ICON_DISK SSD files: $SSD_FILES ($SSD_SIZE_H)"
FLIP_COUNT=$(get_flip_count)
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_EMBY Active Transcode Sessions ━━━
# Queries all configured servers in TRANSCODE_SERVERS array
# Aggregates sessions, counts, and storage state across all servers
# -----------------------------------------------------------------------------------------------
echo ""
echo "━━━ $ICON_EMBY Active Transcode Sessions ━━━"
log "Ramdisk: ${RAMDISK_USED_GB}GB used / ${RAMDISK_AVAIL_GB}GB available ($RAMDISK_FILES files)"
log "SSD: $SSD_FILES files"
log "Flips: $FLIP_COUNT this hour"
# ==============================================================================================
# ━━━ Active Transcode Sessions ━━━
# ==============================================================================================
TOTAL_SESSIONS=0
LIVE_TV=0
TRANSCODING=0
DIRECT=0
ANY_SERVER_RUNNING=false
RAM_SESSION_COUNT=0
SSD_SESSION_COUNT=0
for server_entry in "${TRANSCODE_SERVERS[@]}"; do
# Parse entry — ContainerName|URL|APIKey|Type
IFS='|' read -r SRV_CONTAINER SRV_URL SRV_KEY SRV_TYPE <<< "$server_entry"
# Skip placeholder entries
if [[ "$SRV_KEY" == *"api-key"* ]] || [[ "$SRV_KEY" == *"token"* && ${#SRV_KEY} -lt 20 ]]; then
if [[ "$SRV_KEY" == *"api-key"* ]] || \
[[ "$SRV_KEY" == *"token"* && ${#SRV_KEY} -lt 20 ]]; then
log "Skipping $SRV_CONTAINER — placeholder API key"
continue
fi
# Check if container is running
if ! docker inspect "$SRV_CONTAINER" --format '{{.State.Running}}' 2>/dev/null | grep -q "true"; then
info "$ICON_CONTAINERS $SRV_CONTAINER — not running, skipping"
# Container running check with timeout
if ! timeout "$DOCKER_TIMEOUT" docker inspect "$SRV_CONTAINER" \
--format '{{.State.Running}}' 2>/dev/null | grep -q "true"; then
log "$SRV_CONTAINER — not running, skipping"
continue
fi
ANY_SERVER_RUNNING=true
# Check API reachability
if ! check_api "$SRV_URL" "$SRV_CONTAINER" 5; then
warn "$SRV_CONTAINER — API unreachable, skipping (container up but API not responding)"
warn "$SRV_CONTAINER — API unreachable, skipping"
continue
fi
# Query sessions based on server type
case "$SRV_TYPE" in
emby|jellyfin)
SESSION_DATA=$(curl -sf --max-time 10 \
-H "X-Emby-Token: $SRV_KEY" \
"${SRV_URL}/Sessions" 2>/dev/null)
;;
"${SRV_URL}/Sessions" 2>/dev/null) ;;
plex)
SESSION_DATA=$(curl -sf --max-time 10 \
-H "X-Plex-Token: $SRV_KEY" \
"${SRV_URL}/status/sessions" 2>/dev/null)
;;
"${SRV_URL}/status/sessions" 2>/dev/null) ;;
*)
warn "Unknown server type '$SRV_TYPE' for $SRV_CONTAINER — skipping"
continue
;;
continue ;;
esac
if [[ -z "$SESSION_DATA" ]] || ! command -v jq >/dev/null 2>&1; then
warn "Could not retrieve session data from $SRV_CONTAINER"
continue
fi
[[ -z "$SESSION_DATA" ]] && continue
! command -v jq >/dev/null 2>&1 && continue
# Count sessions (Emby/Jellyfin format)
if [[ "$SRV_TYPE" == "emby" || "$SRV_TYPE" == "jellyfin" ]]; then
SRV_TOTAL=$(echo "$SESSION_DATA" | \
jq '[.[] | select(.NowPlayingItem != null)] | length' 2>/dev/null || echo 0)
SRV_LIVE=$(echo "$SESSION_DATA" | \
jq '[.[] | select(.NowPlayingItem != null) | select(.NowPlayingItem.Type == "LiveTv" or .NowPlayingItem.Type == "TvChannel")] | length' \
jq '[.[] | select(.NowPlayingItem != null) |
select(.NowPlayingItem.Type == "LiveTv" or
.NowPlayingItem.Type == "TvChannel")] | length' \
2>/dev/null || echo 0)
SRV_TRANSCODE=$(echo "$SESSION_DATA" | \
jq '[.[] | select(.NowPlayingItem != null) | select(.PlayState.PlayMethod == "Transcode")] | length' \
jq '[.[] | select(.NowPlayingItem != null) |
select(.PlayState.PlayMethod == "Transcode")] | length' \
2>/dev/null || echo 0)
SRV_DIRECT=$(echo "$SESSION_DATA" | \
jq '[.[] | select(.NowPlayingItem != null) | select(.PlayState.PlayMethod != "Transcode")] | length' \
jq '[.[] | select(.NowPlayingItem != null) |
select(.PlayState.PlayMethod != "Transcode")] | length' \
2>/dev/null || echo 0)
TOTAL_SESSIONS=$(( TOTAL_SESSIONS + SRV_TOTAL ))
@@ -405,138 +411,134 @@ for server_entry in "${TRANSCODE_SERVERS[@]}"; do
TRANSCODING=$(( TRANSCODING + SRV_TRANSCODE ))
DIRECT=$(( DIRECT + SRV_DIRECT ))
# Display server header if more than one server configured and running
if [[ "${#TRANSCODE_SERVERS[@]}" -gt 1 ]]; then
echo " $ICON_EMBY $SRV_CONTAINER$SRV_TOTAL streams"
fi
# List each active session
if [[ "$SRV_TOTAL" -gt 0 ]]; then
SOMETHING_HAPPENED=true
[[ "${#TRANSCODE_SERVERS[@]}" -gt 1 ]] && \
echo " $ICON_EMBY $SRV_CONTAINER$SRV_TOTAL streams"
while IFS= read -r session; do
USER=$(echo "$session" | jq -r '.UserName // "Unknown"' 2>/dev/null)
USER=$(echo "$session" | jq -r '.UserName // "Unknown"' 2>/dev/null)
TITLE=$(echo "$session" | jq -r '.NowPlayingItem.Name // "Unknown"' 2>/dev/null)
MEDIA_TYPE=$(echo "$session" | jq -r '.NowPlayingItem.Type // "Unknown"' 2>/dev/null)
PLAY_METHOD=$(echo "$session" | jq -r '.PlayState.PlayMethod // "Unknown"' 2>/dev/null)
MEDIA_LABEL=$(get_media_type_label "$MEDIA_TYPE")
METHOD_LABEL=$(get_play_method_label "$PLAY_METHOD")
echo " $ICON_EMBY $(printf '%-12s' "$USER")$(printf '%-30s' "$TITLE")$(printf '%-10s' "$MEDIA_LABEL")$METHOD_LABEL"
done < <(echo "$SESSION_DATA" | \
jq -c '.[] | select(.NowPlayingItem != null)' 2>/dev/null)
MTYPE=$(echo "$session" | jq -r '.NowPlayingItem.Type // "Unknown"' 2>/dev/null)
METH=$(echo "$session" | jq -r '.PlayState.PlayMethod // "Unknown"' 2>/dev/null)
echo " $ICON_EMBY $(printf '%-12s' "$USER")$(printf '%-30s' "$TITLE")$(get_media_type_label "$MTYPE")$(get_play_method_label "$METH")"
done < <(echo "$SESSION_DATA" | jq -c '.[] | select(.NowPlayingItem != null)' 2>/dev/null)
fi
fi
done
# Totals header — shown after all servers
echo ""
echo " $ICON_EMBY Total: $TOTAL_SESSIONS | $ICON_RAM Live TV: $LIVE_TV | $ICON_SYNC Transcoding: $TRANSCODING | $ICON_DONE Direct: $DIRECT"
# Storage state
RAM_SESSION_COUNT=$(find "$RAMDISK_PATH/transcoding-temp" \
-mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
SSD_SESSION_COUNT=$(find "$TRANSCODE_SSD/transcoding-temp" \
-mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
# Storage state — based on total ramdisk/SSD session folders
if [[ "$TOTAL_SESSIONS" -gt 0 ]]; then
TRANSCODE_TEMP="transcoding-temp"
RAM_SESSION_COUNT=$(find "$RAMDISK_PATH/$TRANSCODE_TEMP" \
-mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
SSD_SESSION_COUNT=$(find "$TRANSCODE_SSD/$TRANSCODE_TEMP" \
-mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
echo ""
echo " $ICON_EMBY Total: $TOTAL_SESSIONS | Live TV: $LIVE_TV | Transcoding: $TRANSCODING | Direct: $DIRECT"
if [[ "$RAM_SESSION_COUNT" -gt 0 && "$SSD_SESSION_COUNT" -gt 0 ]]; then
warn "$ICON_WARN Split state — $RAM_SESSION_COUNT folder(s) on ramdisk / $SSD_SESSION_COUNT on SSD"
warn "Split state — $RAM_SESSION_COUNT folder(s) ramdisk / $SSD_SESSION_COUNT SSD"
warn "Older sessions remain on original location until they end naturally"
echo " $ICON_LINK Storage: $ICON_RAM ramdisk ($RAM_SESSION_COUNT) + $ICON_DISK SSD ($SSD_SESSION_COUNT)"
elif [[ "$RAM_SESSION_COUNT" -gt 0 ]]; then
echo " $ICON_LINK Storage: $ICON_RAM ramdisk"
log "Storage: ramdisk ($RAM_SESSION_COUNT sessions)"
elif [[ "$SSD_SESSION_COUNT" -gt 0 ]]; then
echo " $ICON_LINK Storage: $ICON_DISK SSD"
log "Storage: SSD ($SSD_SESSION_COUNT sessions)"
fi
fi
if [[ "$ANY_SERVER_RUNNING" == false && "$TRANSCODE_CHECK_EMBY" == true ]]; then
info "No configured media servers are running — skipping threshold checks"
EMBY_RUNNING=false
fi
# -----------------------------------------------------------------------------------------------
# MODE LOGIC
# -----------------------------------------------------------------------------------------------
echo ""
[[ "$ANY_SERVER_RUNNING" == false && "$TRANSCODE_CHECK_EMBY" == true ]] && \
{ log "No configured media servers running — skipping threshold checks"; EMBY_RUNNING=false; }
# ==============================================================================================
# ━━━ Mode Logic ━━━
# ==============================================================================================
case "$TRANSCODE_MANAGER_MODE" in
ramdisk)
info "$ICON_RAM Mode: RAMDISK — forcing symlink to ramdisk"
log "Mode: RAMDISK — forcing symlink to ramdisk"
if [[ "$RAMDISK_HEALTHY" == false ]]; then
error "Ramdisk mode selected but ramdisk is not available"
notify "Transcode ramdisk mode failed on $(hostname) — ramdisk not mounted" "Transcode Manager" "warning"
notify "Transcode ramdisk mode failed on $(hostname) ($MY_ID) — ramdisk not mounted" \
"Transcode Manager" "warning"
SOMETHING_HAPPENED=true
else
if [[ "$CURRENT_TARGET" != "$RAMDISK_PATH" ]]; then
flip_symlink "$RAMDISK_PATH" "ramdisk mode"
increment_flip_count > /dev/null
else
log "Symlink already points to ramdisk — no change"
FLIP_COUNT=$(get_flip_count)
SOMETHING_HAPPENED=true
fi
if (( $(awk "BEGIN {print ($RAMDISK_USED_GB >= $RAMDISK_WARN_GB) ? 1 : 0}") )); then
warn "$ICON_RAM Ramdisk usage ${RAMDISK_USED_GB}GB above threshold ${RAMDISK_WARN_GB}GB — consider switching to smart mode"
notify "Ramdisk usage high on $(hostname)${RAMDISK_USED_GB}GB used in ramdisk-only mode" "Transcode Manager" "warning"
warn "Ramdisk usage ${RAMDISK_USED_GB}GB above threshold ${RAMDISK_WARN_GB}GB — consider switching to smart mode"
notify "Ramdisk high on $(hostname) ($MY_ID)${RAMDISK_USED_GB}GB in ramdisk-only mode" \
"Transcode Manager" "warning"
SOMETHING_HAPPENED=true
fi
fi
;;
ssd)
info "$ICON_DISK Mode: SSD — forcing symlink to SSD"
log "Mode: SSD — forcing symlink to SSD"
if [[ "$SSD_HEALTHY" == false ]]; then
error "SSD mode selected but SSD path is not available"
SOMETHING_HAPPENED=true
else
if [[ "$CURRENT_TARGET" != "$TRANSCODE_SSD" ]]; then
flip_symlink "$TRANSCODE_SSD" "ssd mode"
increment_flip_count > /dev/null
else
log "Symlink already points to SSD — no change"
FLIP_COUNT=$(get_flip_count)
SOMETHING_HAPPENED=true
fi
fi
;;
smart)
info "$ICON_GEAR Mode: SMART — auto threshold management"
log "Mode: SMART — auto threshold management"
if [[ "$EMBY_RUNNING" == false ]]; then
info "No media servers running — skipping threshold checks"
log "No media servers running — skipping threshold checks"
elif [[ "$RAMDISK_HEALTHY" == false ]]; then
info "Ramdisk unavailable — staying on SSD until ramdisk recovers"
log "Ramdisk unavailable — staying on SSD until ramdisk recovers"
elif [[ "$CURRENT_TARGET" == "$RAMDISK_PATH" ]]; then
if (( $(awk "BEGIN {print ($RAMDISK_USED_GB >= $RAMDISK_WARN_GB) ? 1 : 0}") )); then
if [[ "$SSD_HEALTHY" == false ]]; then
error "Ramdisk above threshold but SSD unavailable — cannot flip"
notify "Transcode ramdisk full on $(hostname) and SSD unavailable — intervention needed" "Transcode Manager" "warning"
notify "Transcode ramdisk full on $(hostname) ($MY_ID) and SSD unavailable" \
"Transcode Manager" "warning"
SOMETHING_HAPPENED=true
else
SSD_FREE_GB=$(get_ssd_free_gb)
if (( $(awk "BEGIN {print ($SSD_FREE_GB < $RAMDISK_SSD_MIN_GB) ? 1 : 0}") )); then
warn "$ICON_DISK SSD only ${SSD_FREE_GB}GB free — below ${RAMDISK_SSD_MIN_GB}GB minimum, not flipping"
warn "SSD only ${SSD_FREE_GB}GB free — below ${RAMDISK_SSD_MIN_GB}GB minimum, not flipping"
SOMETHING_HAPPENED=true
else
warn "$ICON_RAM Ramdisk ${RAMDISK_USED_GB}GB — above ${RAMDISK_WARN_GB}GB, flipping to SSD"
warn "Ramdisk ${RAMDISK_USED_GB}GB — above ${RAMDISK_WARN_GB}GB, flipping to SSD"
flip_symlink "$TRANSCODE_SSD" "threshold exceeded"
CURRENT_TARGET="$TRANSCODE_SSD"
NEW_COUNT=$(increment_flip_count)
FLIP_COUNT=$NEW_COUNT
SOMETHING_HAPPENED=true
if [[ "$NEW_COUNT" -ge "$TRANSCODE_FLIP_WARN" ]]; then
notify "Transcode flipped to SSD on $(hostname)${NEW_COUNT} flips this hour. Consider increasing RAMDISK_SIZE." "Transcode Manager" "warning"
notify "Transcode flipped to SSD on $(hostname) ($MY_ID)${NEW_COUNT} flips this hour — consider increasing HOST*_RAMDISK_SIZE" \
"Transcode Manager" "warning"
fi
fi
fi
else
log "Symlink already points to ramdisk — no change"
log "Ramdisk ${RAMDISK_USED_GB}GB — below threshold — no action needed"
fi
elif [[ "$CURRENT_TARGET" == "$TRANSCODE_SSD" ]]; then
if (( $(awk "BEGIN {print ($RAMDISK_USED_GB <= $RAMDISK_LOW_GB) ? 1 : 0}") )); then
info "$ICON_RAM Ramdisk ${RAMDISK_USED_GB}GB — below ${RAMDISK_LOW_GB}GB, flipping back to ramdisk"
warn "Ramdisk ${RAMDISK_USED_GB}GB — below ${RAMDISK_LOW_GB}GB, flipping back to ramdisk"
flip_symlink "$RAMDISK_PATH" "usage recovered"
CURRENT_TARGET="$RAMDISK_PATH"
NEW_COUNT=$(increment_flip_count)
FLIP_COUNT=$NEW_COUNT
SOMETHING_HAPPENED=true
else
log "On SSD — ramdisk ${RAMDISK_USED_GB}GB still above low threshold ${RAMDISK_LOW_GB}GB"
fi
@@ -547,16 +549,32 @@ esac
END=$(date +%s)
CURRENT_TARGET=$(readlink "$TRANSCODE_LINK" 2>/dev/null || echo "unknown")
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_SUMMARY Summary ━━━
# -----------------------------------------------------------------------------------------------
echo ""
echo "━━━━━ $ICON_SUMMARY TRANSCODE MANAGER SUMMARY ━━━━━"
echo "$ICON_GEAR Mode: $TRANSCODE_MANAGER_MODE"
echo "$ICON_RAM Ramdisk: ${RAMDISK_USED_GB}GB used / ${RAMDISK_AVAIL_GB}GB available"
echo "$ICON_LINK Symlink: $TRANSCODE_LINK$CURRENT_TARGET"
echo "$ICON_LINK Flips: $FLIP_COUNT this hour (warn at $TRANSCODE_FLIP_WARN)"
echo "$ICON_EMBY Streams: ${TOTAL_SESSIONS} total | Live TV: ${LIVE_TV} | Transcoding: ${TRANSCODING} | Direct: ${DIRECT}"
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ==============================================================================================
# ━━━ Daily Log Write ━━━
# ==============================================================================================
# Read by weekly_health_digest.sh — format: DATE|USED_GB|FLIPS|RAM_SESSIONS|SSD_SESSIONS
if [[ "$DRY_RUN" == false && "$NO_LOG" == false && -n "${TRANSCODE_DAILY_LOG:-}" ]]; then
TODAY=$(date '+%Y-%m-%d')
mkdir -p "$(dirname "$TRANSCODE_DAILY_LOG")"
echo "${TODAY}|${RAMDISK_USED_GB}|${FLIP_COUNT}|${RAM_SESSION_COUNT}|${SSD_SESSION_COUNT}" \
>> "$TRANSCODE_DAILY_LOG" 2>/dev/null || true
log "Daily log written: $TRANSCODE_DAILY_LOG"
fi
# ==============================================================================================
# ━━━ Summary — only shown when something happened ━━━
# ==============================================================================================
if [[ "$SOMETHING_HAPPENED" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY TRANSCODE MANAGER SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Mode: $TRANSCODE_MANAGER_MODE"
echo "$ICON_RAM Ramdisk: ${RAMDISK_USED_GB}GB used / ${RAMDISK_AVAIL_GB}GB available"
echo "$ICON_LINK Symlink: $TRANSCODE_LINK$CURRENT_TARGET"
echo "$ICON_LINK Flips: $FLIP_COUNT this hour (warn at $TRANSCODE_FLIP_WARN)"
echo "$ICON_EMBY Streams: $TOTAL_SESSIONS total | Live TV: $LIVE_TV | Transcoding: $TRANSCODING | Direct: $DIRECT"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
else
log "Transcode manager — all healthy — $MY_ID ($(format_duration $(( END - START ))))"
fi