diff --git a/Master.conf b/Master.conf index 82e146d..5200d12 100644 --- a/Master.conf +++ b/Master.conf @@ -649,40 +649,58 @@ MEDIA_MAINTENANCE_JOBS=( # ============================================================================================== # ── TRANSCODES ──────────────────────────────────────────────────────────────────────────────── # ============================================================================================== -# Session-based storage allocator using filesystem indirection. -# ffmpeg resolves the symlink path ONCE at session start — existing sessions are never affected -# by symlink changes. Only NEW sessions care about where the symlink currently points. +# Session-based storage allocator 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. # # How it works: # ramdisk_setup.sh — run once at array start, creates tmpfs and sets symlink -# transcode_manager.sh — every 3 min, monitors ramdisk usage and flips symlink if needed +# transcode_manager.sh — every 3 min, monitors usage and manages symlink direction # transcode_cleanup.sh — every 5 min, removes old inactive files from both locations # -# Hysteresis gap between RAMDISK_WARN_GB and RAMDISK_LOW_GB prevents flip-flop -# when usage hovers near the threshold. Gap should be at least 0.5-1GB. -# Ramdisk is dynamic tmpfs — only uses RAM actually needed, RAMDISK_SIZE is the ceiling. - +# ⚠️ Docker mount warning: +# Do NOT add a static SSD transcode path as a second volume mount in your Emby container. +# If the SSD path is mounted inside the container Emby can see it and will use it +# independently of the symlink — breaking symlink-based routing entirely. +# The symlink IS your emergency lever — one mount only: +# /mnt/ram-transcode → /ext-ram-transcode + RAMDISK_PATH="/mnt/ramdisk_transcodes" # tmpfs mount point created at array start RAMDISK_SIZE="8G" # ceiling — tmpfs only uses RAM actually needed TRANSCODE_LINK="/mnt/ram-transcode" # symlink Emby points at — location never changes - TRANSCODE_SSD="/mnt/cache/Temp_Storage/Emby/Transcodes/" # SSD fallback for edge cases only - -# Usage thresholds in GB + TRANSCODE_SSD="/mnt/cache/Temp_Storage/Emby/Transcodes/" # SSD fallback location + +# Usage thresholds in GB — hysteresis gap prevents flip-flop near threshold RAMDISK_WARN_GB=6.8 # flip symlink to SSD at or above this usage - RAMDISK_LOW_GB=5.0 # flip symlink back to ramdisk when usage drops here + RAMDISK_LOW_GB=5.5 # flip symlink back to ramdisk when usage drops here RAMDISK_SSD_MIN_GB=20 # minimum free GB on SSD required before allowing flip to SSD - + # Cleanup age thresholds — files must be older than these AND not open by any process TRANSCODE_MAX_AGE=20 # minutes before a transcode file is eligible for cleanup - TRANSCODE_ORPHAN_AGE=30 # minutes before an orphaned file is eligible — extra caution buffer - + TRANSCODE_ORPHAN_AGE=30 # minutes before an orphaned file is eligible + # Flip frequency alert — too many flips per hour may indicate ramdisk needs to be larger TRANSCODE_FLIP_WARN=3 # notify if symlink flips this many times in one hour - -# Permissions applied to ramdisk and SSD fallback — must match your Emby container user + +# Permissions — must match your Emby container user TRANSCODE_OWNER="nobody:users" - TRANSCODE_MODE="755" - + TRANSCODE_CHMOD="755" # renamed from TRANSCODE_MODE to avoid ambiguity with manager mode + +# Operating mode — controls symlink routing behavior +# smart — auto-flips between ramdisk and SSD based on usage thresholds (default) +# ramdisk — always uses ramdisk, never flips to SSD regardless of usage +# useful when load is light and you want guaranteed ramdisk performance +# warns if usage exceeds threshold but does not flip +# ssd — always uses SSD, never uses ramdisk +# useful during ramdisk maintenance, testing, or after a flip issue +# switch to this mode to drain ramdisk sessions gracefully + TRANSCODE_MANAGER_MODE="smart" + +# Emby container check — skips threshold checks when Emby is not running +# Prevents unnecessary symlink flips when no transcoding is happening + TRANSCODE_CHECK_EMBY=true + TRANSCODE_EMBY_CONTAINER="Emby" # exact Docker container name — case sensitive + # ============================================================================================== # ── MONITOR ─────────────────────────────────────────────────────────────────────────────────── # ============================================================================================== diff --git a/Transcodes/transcode_manager.sh b/Transcodes/transcode_manager.sh index b447123..38a44e1 100644 --- a/Transcodes/transcode_manager.sh +++ b/Transcodes/transcode_manager.sh @@ -2,21 +2,25 @@ # ----------------------------------------------------------------------------------------------- # --------------------------------- Transcode Manager ------------------------------------------ # ----------------------------------------------------------------------------------------------- -# Monitors ramdisk usage and manages the transcode symlink direction. -# New transcode sessions land wherever the symlink points — existing sessions are unaffected. -# Run every 2-3 minutes via cron/User Scripts plugin. +# 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. # -# Logic: -# Ramdisk not found → ensure symlink points to SSD, notify -# Usage < RAMDISK_LOW_GB → ensure symlink points to ramdisk -# Usage >= RAMDISK_WARN_GB → flip symlink to SSD (after checking SSD has space) -# Only notifies on STATE CHANGE — silent when nothing changes +# Three operating modes — set TRANSCODE_MANAGER_MODE in Master.conf: +# smart — auto-flips between ramdisk and SSD based on usage thresholds (default) +# ramdisk — always uses ramdisk, never flips to SSD regardless of usage +# ssd — always uses SSD, never uses ramdisk # -# Hysteresis gap between RAMDISK_WARN_GB and RAMDISK_LOW_GB prevents flip-flop -# when usage hovers near the threshold. +# 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 # -# State tracked in /tmp/transcode_state.db — resets on reboot (correct, ramdisk also resets) # All configuration in Master.conf under Transcode Manager section. +# Supports --dry-run and --status. # ----------------------------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -26,8 +30,6 @@ source "$SCRIPT_DIR/../common.sh" parse_args "$@" -STATE_FILE="/tmp/transcode_state.db" - # ----------------------------------------------------------------------------------------------- # ━━━ $ICON_GEAR Setup ━━━ # ----------------------------------------------------------------------------------------------- @@ -41,140 +43,105 @@ fi success "Running as root" +case "$TRANSCODE_MANAGER_MODE" in + smart|ramdisk|ssd) info "$ICON_GEAR 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" + # ----------------------------------------------------------------------------------------------- # ━━━ $ICON_SUMMARY Status ━━━ # ----------------------------------------------------------------------------------------------- if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" - echo "$ICON_RAM Ramdisk path: $RAMDISK_PATH" - echo "$ICON_RAM Warn at: ${RAMDISK_WARN_GB}GB" - echo "$ICON_RAM Back to RAM at: ${RAMDISK_LOW_GB}GB" - echo "$ICON_DISK SSD fallback: $TRANSCODE_SSD" - echo "$ICON_DISK SSD min free: ${RAMDISK_SSD_MIN_GB}GB" - echo "$ICON_LINK Symlink: $TRANSCODE_LINK" - echo "$ICON_NOTIFY Flip warn: $TRANSCODE_FLIP_WARN per hour" - echo "$ICON_GEAR Dry Run: $DRY_RUN" - - if [[ -f "$STATE_FILE" ]]; then - echo "" - echo "━━━ Current State ━━━" - cat "$STATE_FILE" + 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_KB=$(df "$RAMDISK_PATH" --output=used | tail -1 | tr -d ' ') + USED_GB=$(awk "BEGIN {printf \"%.2f\", $USED_KB / 1048576}") + echo "$ICON_RAM Ramdisk now: ${USED_GB}GB used" + else + echo "$ICON_RAM Ramdisk: NOT MOUNTED" fi echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi -[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no symlink changes will be made" - # ----------------------------------------------------------------------------------------------- -# STATE HELPERS +# HELPERS # ----------------------------------------------------------------------------------------------- -get_state() { - local key="$1" - grep -E "^${key}=" "$STATE_FILE" 2>/dev/null | cut -d'=' -f2 -} - -set_state() { - local key="$1" value="$2" - if [[ -f "$STATE_FILE" ]]; then - grep -vE "^${key}=" "$STATE_FILE" > "${STATE_FILE}.tmp" - echo "${key}=${value}" >> "${STATE_FILE}.tmp" - mv "${STATE_FILE}.tmp" "$STATE_FILE" - else - echo "${key}=${value}" > "$STATE_FILE" - fi -} - -init_state() { - if [[ ! -f "$STATE_FILE" ]]; then - local NOW - NOW=$(date +%s) - cat > "$STATE_FILE" <= 3600 )); then - FLIP_COUNT=0 - set_state "flip_hour_start" "$NOW" - fi - - ((FLIP_COUNT++)) - set_state "flip_count_hour" "$FLIP_COUNT" - set_state "last_flip_time" "$NOW" - - log "Symlink flip count this hour: $FLIP_COUNT" - - if (( FLIP_COUNT >= TRANSCODE_FLIP_WARN )); then - warn "$ICON_LINK Symlink has flipped $FLIP_COUNT times this hour — consider increasing ramdisk size" - notify "Transcode symlink flipping frequently on $(hostname) — $FLIP_COUNT flips in 1hr — consider increasing RAMDISK_SIZE" "Transcode Manager" "warning" - fi -} - -# ----------------------------------------------------------------------------------------------- -# SYMLINK MANAGEMENT -# Sets the symlink to the desired target. -# Only flips if current target differs — tracks flip for frequency monitoring. -# Force mode skips the state check and always sets the symlink (used for recovery). -# ----------------------------------------------------------------------------------------------- -set_symlink() { - local target="$1" force="${2:-false}" - local current_target - current_target=$(get_state "current_target") - - if [[ "$force" == false && "$current_target" == "$target" ]]; then - log "Symlink already points to $target — no change" - return 0 - fi - +flip_symlink() { + local target="$1" reason="$2" if [[ "$DRY_RUN" == true ]]; then - warn "DRY RUN — would set symlink: $TRANSCODE_LINK → $target" + warn "DRY RUN — would flip symlink to: $target ($reason)" return 0 fi + ln -sfn "$target" "$TRANSCODE_LINK" + success "$ICON_LINK Symlink flipped to: $target ($reason)" +} - info "$ICON_LINK Setting symlink: $TRANSCODE_LINK → $target" +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" +} - # Ensure parent directory exists - mkdir -p "$(dirname "$TRANSCODE_LINK")" +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}" +} - if ln -sfn "$target" "$TRANSCODE_LINK"; then - success "$ICON_LINK Symlink set → $target" - set_state "current_target" "$target" +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}" +} - # Only track flip and notify if this was a real direction change - if [[ "$current_target" != "$target" ]]; then - track_flip - if [[ "$target" == "$RAMDISK_PATH" ]]; then - notify "Transcode symlink flipped to RAMDISK on $(hostname) — ramdisk has space" "Transcode Manager" "normal" - else - notify "Transcode symlink flipped to SSD on $(hostname) — ramdisk usage high" "Transcode Manager" "warning" - fi - fi - else - error "Failed to set symlink: $TRANSCODE_LINK → $target" - notify "Transcode symlink failed to set on $(hostname)" "Transcode Manager" "warning" - return 1 - fi +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}" +} + +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 + 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" } # ----------------------------------------------------------------------------------------------- @@ -182,132 +149,221 @@ set_symlink() { # ----------------------------------------------------------------------------------------------- echo "" echo "━━━ $ICON_RAM Transcode Manager — $(date '+%Y-%m-%d %H:%M:%S') ━━━" +echo "$ICON_GEAR Mode: $TRANSCODE_MANAGER_MODE" echo "" -init_state START=$(date +%s) +RAMDISK_HEALTHY=true +SSD_HEALTHY=true +EMBY_RUNNING=true # ----------------------------------------------------------------------------------------------- -# Symlink integrity check -# If symlink is missing or target doesn't exist — force correct it immediately +# CHECK 1 — Emby running # ----------------------------------------------------------------------------------------------- +if [[ "$TRANSCODE_CHECK_EMBY" == true ]]; then + info "$ICON_CONTAINERS Checking $TRANSCODE_EMBY_CONTAINER..." + if ! 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 + else + success "$TRANSCODE_EMBY_CONTAINER is running" + fi +fi + +# ----------------------------------------------------------------------------------------------- +# CHECK 2 — Symlink integrity +# ----------------------------------------------------------------------------------------------- +echo "" info "$ICON_LINK Checking symlink integrity..." -SYMLINK_OK=true +CURRENT_TARGET=$(readlink "$TRANSCODE_LINK" 2>/dev/null) -if [[ ! -L "$TRANSCODE_LINK" ]]; then - error "$ICON_LINK Symlink missing: $TRANSCODE_LINK — recovering" - SYMLINK_OK=false +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) — recreated" "Transcode Manager" "warning" + fi +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" + fi else - LINK_TARGET=$(readlink "$TRANSCODE_LINK" 2>/dev/null) - if [[ ! -d "$LINK_TARGET" ]]; then - error "$ICON_LINK Symlink target does not exist: $LINK_TARGET — recovering" - SYMLINK_OK=false - else - info "$ICON_LINK Symlink valid: $TRANSCODE_LINK → $LINK_TARGET" - fi -fi - -# Force recovery — point at ramdisk if mounted, SSD otherwise -if [[ "$SYMLINK_OK" == false ]]; then - if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then - warn "Recovering symlink → ramdisk" - set_symlink "$RAMDISK_PATH" force - else - warn "Ramdisk not mounted — recovering symlink → SSD" - set_symlink "$TRANSCODE_SSD" force - fi + success "$ICON_LINK Symlink valid: $TRANSCODE_LINK → $CURRENT_TARGET" fi # ----------------------------------------------------------------------------------------------- -# Ramdisk mount check +# CHECK 3 — Ramdisk health # ----------------------------------------------------------------------------------------------- +echo "" info "$ICON_RAM Checking ramdisk..." -RAMDISK_MOUNTED=true if ! mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then - warn "$ICON_RAM Ramdisk not mounted at $RAMDISK_PATH — falling back to SSD" - RAMDISK_MOUNTED=false - set_symlink "$TRANSCODE_SSD" - notify "Ramdisk not found on $(hostname) — transcode symlink set to SSD fallback" "Transcode Manager" "warning" + error "$ICON_RAM Ramdisk not mounted at $RAMDISK_PATH" + RAMDISK_HEALTHY=false + 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" + fi +else + RAMDISK_SIZE_ACTUAL=$(df "$RAMDISK_PATH" --output=size -h | tail -1 | tr -d ' ') + success "$ICON_RAM Ramdisk mounted — size: $RAMDISK_SIZE_ACTUAL" + fix_permissions "$RAMDISK_PATH" fi -if [[ "$RAMDISK_MOUNTED" == true ]]; then - # Verify ramdisk size matches expected - MOUNTED_SIZE=$(df -BG "$RAMDISK_PATH" | awk 'NR==2 {print $2}' | tr -d 'G') - EXPECTED_SIZE=$(echo "$RAMDISK_SIZE" | tr -d 'G') - if [[ "$MOUNTED_SIZE" != "$EXPECTED_SIZE" ]]; then - warn "$ICON_RAM Ramdisk size mismatch — expected ${RAMDISK_SIZE} got ${MOUNTED_SIZE}G" +# ----------------------------------------------------------------------------------------------- +# CHECK 4 — SSD health +# ----------------------------------------------------------------------------------------------- +echo "" +info "$ICON_DISK Checking SSD fallback..." + +if [[ ! -d "$TRANSCODE_SSD" ]]; then + warn "$ICON_DISK SSD fallback path missing: $TRANSCODE_SSD" + SSD_HEALTHY=false + 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" + exit 1 else - success "$ICON_RAM Ramdisk mounted and correct size (${RAMDISK_SIZE})" + 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" + fix_permissions "$TRANSCODE_SSD" +fi - # Get current usage in KB — use raw bytes to avoid scientific notation issues - RAMDISK_USED_KB=$(df "$RAMDISK_PATH" --output=used | tail -1 | tr -d ' ') - RAMDISK_AVAIL_KB=$(df "$RAMDISK_PATH" --output=avail | tail -1 | tr -d ' ') +# ----------------------------------------------------------------------------------------------- +# USAGE STATS +# ----------------------------------------------------------------------------------------------- +echo "" +RAMDISK_USED_GB="0.00" +RAMDISK_AVAIL_GB="0.00" - # Convert to GB for display using awk — avoids bash integer/float issues - RAMDISK_USED_GB=$(awk "BEGIN {printf \"%.2f\", $RAMDISK_USED_KB / 1048576}") - RAMDISK_AVAIL_GB=$(awk "BEGIN {printf \"%.2f\", $RAMDISK_AVAIL_KB / 1048576}") - - info "$ICON_RAM Ramdisk usage: ${RAMDISK_USED_GB}GB used / ${RAMDISK_AVAIL_GB}GB available" - - # File counts and sizes +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) - SSD_FILES=$(find "$TRANSCODE_SSD" -type f 2>/dev/null | wc -l) - RAMDISK_SIZE_HUMAN=$(du -sh "$RAMDISK_PATH" 2>/dev/null | cut -f1) - SSD_SIZE_HUMAN=$(du -sh "$TRANSCODE_SSD" 2>/dev/null | cut -f1) - - info "$ICON_RAM Ramdisk files: $RAMDISK_FILES ($RAMDISK_SIZE_HUMAN)" - info "$ICON_DISK SSD files: $SSD_FILES ($SSD_SIZE_HUMAN)" - - # ----------------------------------------------------------------------------------------------- - # Symlink direction decision — all comparisons via awk to handle float thresholds - # ----------------------------------------------------------------------------------------------- - WARN_EXCEEDED=$(awk "BEGIN {print ($RAMDISK_USED_GB >= $RAMDISK_WARN_GB) ? 1 : 0}") - LOW_RECOVERED=$(awk "BEGIN {print ($RAMDISK_USED_GB < $RAMDISK_LOW_GB) ? 1 : 0}") - - if [[ "$WARN_EXCEEDED" == "1" ]]; then - # Ramdisk is getting full — check SSD has enough space before flipping - info "$ICON_DISK Checking SSD fallback space..." - SSD_AVAIL_KB=$(df "$TRANSCODE_SSD" --output=avail | tail -1 | tr -d ' ') - SSD_AVAIL_GB=$(awk "BEGIN {printf \"%.2f\", $SSD_AVAIL_KB / 1048576}") - SSD_SUFFICIENT=$(awk "BEGIN {print ($SSD_AVAIL_GB >= $RAMDISK_SSD_MIN_GB) ? 1 : 0}") - - if [[ "$SSD_SUFFICIENT" == "0" ]]; then - error "$ICON_DISK SSD fallback only has ${SSD_AVAIL_GB}GB free — minimum is ${RAMDISK_SSD_MIN_GB}GB" - notify "Transcode SSD fallback critically low on $(hostname) — ${SSD_AVAIL_GB}GB free" "Transcode Manager" "warning" - else - info "$ICON_DISK SSD has ${SSD_AVAIL_GB}GB free — flipping to SSD" - set_symlink "$TRANSCODE_SSD" - fi - - elif [[ "$LOW_RECOVERED" == "1" ]]; then - # Ramdisk has recovered — flip back - set_symlink "$RAMDISK_PATH" - - else - # In hysteresis gap — hold current state - CURRENT=$(get_state "current_target") - log "Usage ${RAMDISK_USED_GB}GB in hysteresis gap (${RAMDISK_LOW_GB}–${RAMDISK_WARN_GB}GB) — holding: $CURRENT" - info "$ICON_RAM Usage in hysteresis gap — holding current symlink" - fi + 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) + +# ----------------------------------------------------------------------------------------------- +# MODE LOGIC +# ----------------------------------------------------------------------------------------------- +echo "" + +case "$TRANSCODE_MANAGER_MODE" in + + ramdisk) + info "$ICON_RAM 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" + 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" + 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" + fi + fi + ;; + + ssd) + info "$ICON_DISK Mode: SSD — forcing symlink to SSD" + if [[ "$SSD_HEALTHY" == false ]]; then + error "SSD mode selected but SSD path is not available" + 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" + fi + fi + ;; + + smart) + info "$ICON_GEAR Mode: SMART — auto threshold management" + + if [[ "$EMBY_RUNNING" == false ]]; then + info "Emby not running — skipping threshold checks" + + elif [[ "$RAMDISK_HEALTHY" == false ]]; then + info "Ramdisk unavailable — staying on SSD until ramdisk recovers" + + elif [[ "$CURRENT_TARGET" == "$RAMDISK_PATH" ]]; then + # On ramdisk — check if we need to flip to SSD + 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" + 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" + else + warn "$ICON_RAM 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 + 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" + fi + fi + fi + else + log "Symlink already points to ramdisk — no change" + fi + + elif [[ "$CURRENT_TARGET" == "$TRANSCODE_SSD" ]]; then + # On SSD — check if we can flip back to ramdisk + 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" + flip_symlink "$RAMDISK_PATH" "usage recovered" + CURRENT_TARGET="$RAMDISK_PATH" + NEW_COUNT=$(increment_flip_count) + FLIP_COUNT=$NEW_COUNT + 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") # ----------------------------------------------------------------------------------------------- # ━━━ $ICON_SUMMARY Summary ━━━ # ----------------------------------------------------------------------------------------------- echo "" echo "━━━━━ $ICON_SUMMARY TRANSCODE MANAGER SUMMARY ━━━━━" -if [[ "$RAMDISK_MOUNTED" == true ]]; then - echo "$ICON_RAM Ramdisk: ${RAMDISK_USED_GB}GB used / ${RAMDISK_AVAIL_GB}GB available" -else - echo "$ICON_RAM Ramdisk: NOT MOUNTED — using SSD" -fi -echo "$ICON_LINK Symlink: $TRANSCODE_LINK → $(readlink "$TRANSCODE_LINK" 2>/dev/null || echo "BROKEN")" -echo "$ICON_LINK Flips: $(get_state "flip_count_hour") this hour (warn at $TRANSCODE_FLIP_WARN)" -echo "$ICON_TIME Duration: $(format_duration $((END - START)))" -echo "$ICON_DONE Status: $ICON_SUCCESS DONE" +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_TIME Duration: $(format_duration $((END - START)))" +echo "$ICON_DONE Status: $ICON_SUCCESS DONE" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" \ No newline at end of file