#!/bin/bash # ============================================================================================== # ================================= Transcode Manager ========================================== # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Monitors ramdisk usage and manages the transcode symlink direction. Called by # transcode_management.sh (Orchestrators/) every 3 minutes — always after # transcode_cleanup.sh runs first. Must be fast, non-blocking, and silent when # nothing has changed. # # Emby's transcode path points at TRANSCODE_LINK (a symlink). ffmpeg resolves # the symlink once at session start and holds a direct reference — existing # sessions are completely unaffected by symlink flips. Only new sessions care # where the symlink currently points. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # Three Modes (TRANSCODE_MANAGER_MODE) # smart — auto-flips between ramdisk and SSD based on usage thresholds (default) # 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 (use during drain or maintenance) # # Safety Checks — Every Run Regardless of Mode # Symlink missing/broken → recreate pointing at ramdisk, notify # Ramdisk disappeared → flip to SSD immediately, notify warning # SSD path missing → disable SSD fallback (error if mode=ssd) # transcoding-temp missing → recreate on ramdisk silently # Permissions drift → fix silently every run # Emby not running → skip threshold checks, verify symlink only # # Session Display # Shows active streams from all configured TRANSCODE_SERVERS with user, title, # type (Live TV / TV Show / Movie), and play method (Transcode / Direct). # Split state shown when sessions exist on both ramdisk and SSD — normal during # a flip while ramdisk sessions drain. # # Daily Log # Appends one entry per run to TRANSCODE_DAILY_LOG, read by weekly_health_digest.sh. # Format: DATE|RAMDISK_USED_GB|FLIP_COUNT|RAM_SESSION_COUNT|SSD_SESSION_COUNT|FILES_CLEANED # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Wait Lock # acquire_lock "wait" — waits if the previous run is still active. The 3-minute # interval can overlap on a system under heavy load. # # Docker Timeout # DOCKER_TIMEOUT caps all docker calls against a hung daemon. # # Notification Validated # validate_unraid_cmd confirms the notify script is present before use. # # Silent by Default # Runs every 3 minutes — only speaks when something changes or needs attention. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # master_host*.conf # # HOST*_RAMDISK_PATH / HOST*_TRANSCODE_SSD # Ramdisk mount point and SSD fallback path. # Aliased by detect_hosts(). # # HOST*_RAMDISK_WARN_GB / HOST*_RAMDISK_LOW_GB / HOST*_RAMDISK_SIZE # Thresholds and ceiling. Change all three together. # Aliased by detect_hosts(). # # HOST*_TRANSCODE_SERVERS # Array of media server definitions: "ContainerName|URL|APIKey|Type" # Type: emby | jellyfin | plex # Aliased by detect_hosts() → TRANSCODE_SERVERS. # # master.conf # # TRANSCODE_MANAGER_MODE # smart | ramdisk | ssd. (default: smart) # # TRANSCODE_CHECK_EMBY # Skip threshold checks when Emby not running — prevents unnecessary flips # overnight when no sessions are active. (default: true) # # TRANSCODE_FLIP_WARN # Notify if symlink flips this many times in one hour — indicates ramdisk # is undersized for the load. (default: 3) # # ============================================================================================== # STATE FILES # ============================================================================================== # # /tmp/transcode_state.db — current symlink target, flip count, last flip time # Lives in /tmp (ephemeral — resets correctly on reboot) # TRANSCODE_DAILY_LOG — per-run append, read by weekly_health_digest.sh # Trimmed to TRANSCODE_LOG_RETENTION days on each write # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # transcode_manager.sh # Check usage, flip if needed, run safety checks, display active sessions. # # transcode_manager.sh --dry-run # Show current usage and what flip decision would be made. No changes. # # transcode_manager.sh --status # Show current symlink target, ramdisk usage, session counts, and flip history. # # transcode_manager.sh --log # Verbose output including per-check results and session detail. # # transcode_manager.sh --no-log # Suppress daily log write. Used internally when called by transcode_cleanup.sh. # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" # 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 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 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" if ! command -v docker &>/dev/null; then error "Docker command not found" exit 1 fi # 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) log "Mode: $TRANSCODE_MANAGER_MODE" ;; *) warn "Unknown TRANSCODE_MANAGER_MODE: $TRANSCODE_MANAGER_MODE — defaulting to smart" TRANSCODE_MANAGER_MODE="smart" ;; esac [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made" # ============================================================================================== # ━━━ Status ━━━ # ============================================================================================== if [[ "$SHOW_STATUS" == true ]]; then echo "" 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" echo "$ICON_DISK SSD: $TRANSCODE_SSD" echo "$ICON_RAM Warn at: ${RAMDISK_WARN_GB}GB" echo "$ICON_RAM Low at: ${RAMDISK_LOW_GB}GB" echo "$ICON_DISK SSD min free: ${RAMDISK_SSD_MIN_GB}GB" echo "$ICON_RAM Flip warn: $TRANSCODE_FLIP_WARN per hour" echo "$ICON_GEAR Check Emby: $TRANSCODE_CHECK_EMBY ($TRANSCODE_EMBY_CONTAINER)" echo "$ICON_GEAR Dry Run: $DRY_RUN" 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_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" fi echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi # ============================================================================================== # ── HELPER FUNCTIONS ────────────────────────────────────────────────────────────────────────── # ============================================================================================== flip_symlink() { local target="$1" reason="$2" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would flip symlink to: $target ($reason)" return 0 fi ln -sfn "$target" "$TRANSCODE_LINK" warn "$ICON_LINK Symlink flipped to: $target ($reason)" } fix_permissions() { local path="$1" [[ ! -d "$path" ]] && return chown -R "$TRANSCODE_OWNER" "$path" 2>/dev/null chmod -R "$TRANSCODE_CHMOD" "$path" 2>/dev/null log "Permissions fixed on $path" } get_ramdisk_used_gb() { df "$RAMDISK_PATH" --output=used 2>/dev/null | \ tail -1 | tr -d ' ' | awk '{printf "%.2f", $1/1048576}' } get_ramdisk_avail_gb() { df "$RAMDISK_PATH" --output=avail 2>/dev/null | \ tail -1 | tr -d ' ' | awk '{printf "%.2f", $1/1048576}' } get_ssd_free_gb() { 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') [[ ! -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_count=$(awk -F'|' 'NR==1{print $2}' "$state_file" 2>/dev/null) [[ "$stored_hour" == "$current_hour" ]] && echo "${stored_count:-0}" || echo "0" } increment_flip_count() { local state_file="/tmp/transcode_flip_state.db" local current_hour current_hour=$(date '+%Y-%m-%d-%H') local current_count current_count=$(get_flip_count) current_count=$(( current_count + 1 )) echo "${current_hour}|${current_count}" > "$state_file" echo "$current_count" } get_media_type_label() { 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() { case "$1" in Transcode) echo "Transcode" ;; DirectStream) echo "Direct Stream" ;; DirectPlay) echo "Direct Play" ;; *) echo "$1" ;; esac } # ============================================================================================== # ━━━ 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 ─────────────────────────────────────────────────────────────────── if [[ "$TRANSCODE_CHECK_EMBY" == true ]]; then 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 log "$TRANSCODE_EMBY_CONTAINER is running ✅" fi fi # ── Check 2 — Symlink integrity ─────────────────────────────────────────────────────────────── log "Checking symlink integrity..." CURRENT_TARGET=$(readlink "$TRANSCODE_LINK" 2>/dev/null) if [[ -z "$CURRENT_TARGET" ]]; then warn "$ICON_LINK Symlink missing — recreating pointing to ramdisk" if [[ "$DRY_RUN" == false ]]; then ln -sfn "$RAMDISK_PATH" "$TRANSCODE_LINK" CURRENT_TARGET="$RAMDISK_PATH" 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) ($MY_ID) — reset to ramdisk" \ "Transcode Manager" "warning" fi SOMETHING_HAPPENED=true else log "Symlink valid: $TRANSCODE_LINK → $CURRENT_TARGET ✅" fi # ── Check 3 — Ramdisk health ────────────────────────────────────────────────────────────────── log "Checking ramdisk..." if ! mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then 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) ($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 2>/dev/null | tail -1 | tr -d ' ') log "Ramdisk mounted — size: $RAMDISK_SIZE_ACTUAL ✅" fix_permissions "$RAMDISK_PATH" # Guarantee transcoding-temp exists on ramdisk TRANSCODE_TEMP_RAM="${RAMDISK_PATH}/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" warn "transcoding-temp created on ramdisk — new sessions will use ramdisk ✅" SOMETHING_HAPPENED=true else log "transcoding-temp exists on ramdisk ✅" fi fi # ── Check 4 — SSD health ───────────────────────────────────────────────────────────────────── log "Checking SSD fallback..." if [[ ! -d "$TRANSCODE_SSD" ]]; then 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) ($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) log "SSD available — ${SSD_FREE_GB}GB free ✅" fix_permissions "$TRANSCODE_SSD" fi # ── 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) fi SSD_FILES=$(find "$TRANSCODE_SSD" -type f 2>/dev/null | wc -l) FLIP_COUNT=$(get_flip_count) 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 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 log "Skipping $SRV_CONTAINER — placeholder API key" continue fi # 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 if ! check_api "$SRV_URL" "$SRV_CONTAINER" 5; then warn "$SRV_CONTAINER — API unreachable, skipping" continue fi 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) ;; plex) SESSION_DATA=$(curl -sf --max-time 10 \ -H "X-Plex-Token: $SRV_KEY" \ "${SRV_URL}/status/sessions" 2>/dev/null) ;; *) warn "Unknown server type '$SRV_TYPE' for $SRV_CONTAINER — skipping" continue ;; esac [[ -z "$SESSION_DATA" ]] && continue ! command -v jq >/dev/null 2>&1 && continue 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' \ 2>/dev/null || echo 0) SRV_TRANSCODE=$(echo "$SESSION_DATA" | \ 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' \ 2>/dev/null || echo 0) TOTAL_SESSIONS=$(( TOTAL_SESSIONS + SRV_TOTAL )) LIVE_TV=$(( LIVE_TV + SRV_LIVE )) TRANSCODING=$(( TRANSCODING + SRV_TRANSCODE )) DIRECT=$(( DIRECT + SRV_DIRECT )) 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) TITLE=$(echo "$session" | jq -r '.NowPlayingItem.Name // "Unknown"' 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 # 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) if [[ "$TOTAL_SESSIONS" -gt 0 ]]; then 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 "Split state — $RAM_SESSION_COUNT folder(s) ramdisk / $SSD_SESSION_COUNT SSD" warn "Older sessions remain on original location until they end naturally" elif [[ "$RAM_SESSION_COUNT" -gt 0 ]]; then log "Storage: ramdisk ($RAM_SESSION_COUNT sessions)" elif [[ "$SSD_SESSION_COUNT" -gt 0 ]]; then log "Storage: SSD ($SSD_SESSION_COUNT sessions)" fi fi [[ "$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) 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) ($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 FLIP_COUNT=$(get_flip_count) SOMETHING_HAPPENED=true fi if (( $(awk "BEGIN {print ($RAMDISK_USED_GB >= $RAMDISK_WARN_GB) ? 1 : 0}") )); then 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) 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 FLIP_COUNT=$(get_flip_count) SOMETHING_HAPPENED=true fi fi ;; smart) log "Mode: SMART — auto threshold management" if [[ "$EMBY_RUNNING" == false ]]; then log "No media servers running — skipping threshold checks" elif [[ "$RAMDISK_HEALTHY" == false ]]; then 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) ($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 "SSD only ${SSD_FREE_GB}GB free — below ${RAMDISK_SSD_MIN_GB}GB minimum, not flipping" SOMETHING_HAPPENED=true else 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) ($MY_ID) — ${NEW_COUNT} flips this hour — consider increasing HOST*_RAMDISK_SIZE" \ "Transcode Manager" "warning" fi fi fi else 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 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 fi ;; esac END=$(date +%s) CURRENT_TARGET=$(readlink "$TRANSCODE_LINK" 2>/dev/null || echo "unknown") # ============================================================================================== # ━━━ 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 echo "Transcode manager — all healthy — $MY_ID ($(format_duration $(( END - START ))))" fi