transcode manager now handles multiple media servers

This commit is contained in:
2026-04-20 18:17:13 -04:00
parent e83473ab8e
commit 1a16ca02a9
5 changed files with 255 additions and 69 deletions
+52 -7
View File
@@ -444,11 +444,11 @@ HOST2_DDNS_CONTAINERS=(
# Own DDNS handled separately above — list additional containers here if needed
# These stop when this server loses internet — regardless of remote state
FAILOVER_HOST1_STOP_ON_NO_NET=(
"Gmer4Lfe.com" # add containers that should stop without internet
# "container-name" # add containers that should stop without internet
)
FAILOVER_HOST2_STOP_ON_NO_NET=(
"Gmer4Lfe.us"
# "container-name"
)
# ━━━ HOST1 runs these for HOST2 when HOST2 goes down ━━━
@@ -498,8 +498,7 @@ FAILOVER_HOST2_RUNS_FOR_HOST1_IMMEDIATE=(
"Dispatcharr" # Live TV — people are watching, cannot wait
"Dispatcharr-Basic" # Live TV basic profile
"Dispatcharr-Iptv-Users" # Live TV IPTV users
"ErsatzTV-Emby"
# "container-placeholder" # Live TV scheduling and channel management
"ErsatzTV-Emby" # Live TV scheduling and channel management
)
# Tier 2 — starts after HOST1_TIER2_DELAY minutes
@@ -1044,10 +1043,56 @@ RADARR_PROTECTED_PATTERNS=("*.jpg" "*.jpeg" "*.png" "*.nfo" "*.srt" "*.sub" "*.a
# switch to this mode to drain ramdisk sessions gracefully
TRANSCODE_MANAGER_MODE="smart" # smart | ramdisk | ssd
# Emby container check — skips threshold checks when Emby is not running
# Prevents unnecessary symlink flips when no transcoding is happening
# ━━━ Transcode Server Array ━━━
# All media servers that share the ramdisk transcode space.
# transcode_manager.sh reads this array and queries each server's API for active sessions.
# Session display, storage detection, and threshold decisions cover ALL servers combined.
#
# One entry → works exactly as before — single server behavior
# Many entries → aggregates sessions from all servers, one threshold on total ramdisk usage
#
# Each server writes to its own subfolder inside transcoding-temp:
# /mnt/ram-transcode/transcoding-temp/ABC123/ ← Emby session
# /mnt/ram-transcode/transcoding-temp/XYZ789/ ← Jellyfin session
# They never touch each other's files — the ramdisk is shared scratch space.
#
# Docker Extra Parameters — each media server container that uses the ramdisk needs:
# --mount type=bind,source=/mnt/ram-transcode,target=/ext-ram-transcode,bind-propagation=shared
# The target path (/ext-ram-transcode) must match what's configured in each server's
# transcoding settings. Use a different target path per container if needed.
#
# Format: "ContainerName|URL|APIKey|Type"
# ContainerName — exact Docker container name (used for running check)
# URL — API base URL including port
# APIKey — server API key (Emby/Jellyfin token, Plex token etc.)
# Type — emby | jellyfin | plex (controls API endpoint format)
#
# The first entry is the primary server — used for container running check
# (TRANSCODE_CHECK_EMBY still applies to first entry).
# Additional entries are queried if their container is running.
# Entries with placeholder APIKey values are skipped automatically.
#
# ⚠️ Tdarr does NOT belong here — Tdarr encodes full files, not HLS segments.
# Large working files would fill the ramdisk rapidly and cause constant flips.
# Keep Tdarr on SSD. Use tdarr_cleanup.sh for Tdarr orphan management.
TRANSCODE_SERVERS=(
"Emby|http://localhost:8096|0c27448d93a7431f9ac63569f7655829|emby"
# "Emby-Jayred365|http://HOST2-TAILSCALE-IP:8096|his-api-key|emby" # his Emby temporarily
# "Jellyfin|http://localhost:8097|jellyfin-api-key|jellyfin" # test Jellyfin instance
# "Plex|http://localhost:32400|plex-token|plex" # Plex if needed
)
# Container running check — still applies — skips threshold checks when no servers active
# Checked against the first entry in TRANSCODE_SERVERS automatically
TRANSCODE_CHECK_EMBY=true
TRANSCODE_EMBY_CONTAINER="Emby" # exact Docker container name — case sensitive
# Per-host Emby container names for database repair
# HOST1 repairs its own personal Emby + the shared production Emby
# HOST2 repairs its own personal Emby
# detect_hosts() selects the correct container at runtime
HOST1_EMBY_REPAIR_CONTAINER="Emby" # shared production Emby lives on HOST1
HOST2_EMBY_REPAIR_CONTAINER="Emby-Jayred365" # HOST2's personal Emby instance
# Daily transcode statistics log — read by weekly_health_digest.sh
# Tracks peak usage, flip count, session ratio, files cleaned per day
+15 -5
View File
@@ -22,7 +22,10 @@
# authentication.db — API keys and sessions
# activity.db — activity log
#
# All configuration in Master.conf — uses TRANSCODE_EMBY_CONTAINER and EMBY_URL.
# HOST1 repairs the shared production Emby (HOST1_EMBY_REPAIR_CONTAINER).
# HOST2 repairs its own personal Emby (HOST2_EMBY_REPAIR_CONTAINER).
# detect_hosts() selects the correct container at runtime.
# Config path is detected from Docker inspect — no hardcoded paths needed.
# Supports --dry-run to show what would be checked without stopping Emby.
# -----------------------------------------------------------------------------------------------
@@ -33,10 +36,6 @@ source "$SCRIPT_DIR/../common.sh"
parse_args "$@"
# Emby config path — inside the container it's /config, map to host path
# Detected from Docker inspect at runtime
EMBY_CONTAINER="$TRANSCODE_EMBY_CONTAINER"
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_GEAR Setup ━━━
# -----------------------------------------------------------------------------------------------
@@ -52,6 +51,17 @@ success "Running as root"
acquire_lock
# Select correct Emby container based on which server is running this script
detect_hosts
if [[ "$LOCAL_SERVER_NAME" == "$HOST1" ]]; then
EMBY_CONTAINER="$HOST1_EMBY_REPAIR_CONTAINER"
else
EMBY_CONTAINER="$HOST2_EMBY_REPAIR_CONTAINER"
fi
info "Emby container: $LOCAL_SERVER_NAME$EMBY_CONTAINER"
if ! command -v sqlite3 >/dev/null 2>&1; then
error "sqlite3 not found — install sqlite package"
exit 1
+47 -2
View File
@@ -223,13 +223,58 @@ TRANSCODE_CHMOD="755"
TRANSCODE_MANAGER_MODE="smart" # smart | ramdisk | ssd
TRANSCODE_CHECK_EMBY=true
TRANSCODE_EMBY_CONTAINER="Emby"
TRANSCODE_CHECK_EMBY=true # skips threshold checks when no servers active
TRANSCODE_DAILY_LOG="/boot/config/transcode_daily.db"
TRANSCODE_LOG_RETENTION=90
```
### Multi-Server Configuration — `TRANSCODE_SERVERS`
The transcode manager supports any number of media servers sharing the same ramdisk scratch space. One server or ten — the same script handles it. The threshold decision is based on total ramdisk usage across all servers combined.
```bash
TRANSCODE_SERVERS=(
"Emby|http://localhost:8096|your-api-key|emby"
# "Emby-Jayred365|http://HOST2-IP:8096|his-api-key|emby" # temporarily cover his Emby
# "Jellyfin|http://localhost:8097|jellyfin-api-key|jellyfin" # test Jellyfin instance
# "Plex|http://localhost:32400|plex-token|plex" # Plex if needed
)
```
**Format:** `"ContainerName|URL|APIKey|Type"`
| Field | Description |
|-------|-------------|
| `ContainerName` | Exact Docker container name — used for running check |
| `URL` | API base URL including port |
| `APIKey` | Server API key or token |
| `Type` | `emby` \| `jellyfin` \| `plex` — controls API endpoint format |
**How it works with multiple servers:**
Each media server writes its transcode sessions to its own subfolder inside `transcoding-temp`:
```
/mnt/ram-transcode/transcoding-temp/
E0D8DC/ ← Emby session (Live TV segment files)
abc123/ ← Jellyfin session
xyz789/ ← his Emby session
```
They never touch each other's files. The ramdisk is shared scratch space — each server is unaware of the others. The manager aggregates session counts from all servers and makes one threshold decision based on total ramdisk usage.
**Each server container needs the shared propagation mount in Extra Parameters:**
```
--mount type=bind,source=/mnt/ram-transcode,target=/ext-ram-transcode,bind-propagation=shared
```
Set the transcode path in each server's settings to `/ext-ram-transcode` (or whatever target path you use).
**Entries with placeholder API key values are skipped automatically** — comment out entries that aren't in use rather than removing them. The placeholders make it clear what's available to enable.
**⚠️ Tdarr does NOT belong in this array.** Tdarr encodes full files — large working files would fill the ramdisk rapidly and cause constant flips. Keep Tdarr on SSD. A dedicated `tdarr_cleanup.sh` handles Tdarr orphan management separately.
### Sizing the Ramdisk
`tmpfs` only uses RAM actually needed — `RAMDISK_SIZE` is a ceiling, not a reservation.
+105 -53
View File
@@ -49,15 +49,11 @@ success "Running as root"
acquire_lock "wait"
# Select correct Emby instance for session display
detect_hosts
if [[ "$LOCAL_SERVER_NAME" == "$HOST1" ]]; then
EMBY_URL="${HOST1_EMBY_URL}"
EMBY_API_KEY="${HOST1_EMBY_API_KEY}"
else
EMBY_URL="${HOST2_EMBY_URL}"
EMBY_API_KEY="${HOST2_EMBY_API_KEY}"
# Primary server for container running check — first entry in TRANSCODE_SERVERS
# Full session display loops over all entries in the array
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
@@ -335,63 +331,93 @@ FLIP_COUNT=$(get_flip_count)
echo ""
echo "━━━ $ICON_EMBY Active Emby Sessions ━━━"
# -----------------------------------------------------------------------------------------------
# ━━━ $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 ━━━"
TOTAL_SESSIONS=0
LIVE_TV=0
TRANSCODING=0
DIRECT=0
ANY_SERVER_RUNNING=false
if [[ -z "$EMBY_API_KEY" ]]; then
info "EMBY_API_KEY not set in Master.conf — skipping session display"
elif [[ "$EMBY_RUNNING" == false ]]; then
info "Emby not running — no sessions"
else
SESSION_DATA=$(emby_api "Sessions" 2>/dev/null)
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
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"
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)"
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)
;;
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
if [[ -z "$SESSION_DATA" ]] || ! command -v jq >/dev/null 2>&1; then
warn "Could not retrieve session data — check EMBY_URL and EMBY_API_KEY"
else
# Count sessions
TOTAL_SESSIONS=$(echo "$SESSION_DATA" | \
warn "Could not retrieve session data from $SRV_CONTAINER"
continue
fi
# 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)
LIVE_TV=$(echo "$SESSION_DATA" | \
SRV_LIVE=$(echo "$SESSION_DATA" | \
jq '[.[] | select(.NowPlayingItem != null) | select(.NowPlayingItem.Type == "LiveTv" or .NowPlayingItem.Type == "TvChannel")] | length' \
2>/dev/null || echo 0)
TRANSCODING=$(echo "$SESSION_DATA" | \
SRV_TRANSCODE=$(echo "$SESSION_DATA" | \
jq '[.[] | select(.NowPlayingItem != null) | select(.PlayState.PlayMethod == "Transcode")] | length' \
2>/dev/null || echo 0)
DIRECT=$(echo "$SESSION_DATA" | \
SRV_DIRECT=$(echo "$SESSION_DATA" | \
jq '[.[] | select(.NowPlayingItem != null) | select(.PlayState.PlayMethod != "Transcode")] | length' \
2>/dev/null || echo 0)
echo " $ICON_EMBY Total: $TOTAL_SESSIONS | $ICON_RAM Live TV: $LIVE_TV | $ICON_SYNC Transcoding: $TRANSCODING | $ICON_DONE Direct: $DIRECT"
echo ""
TOTAL_SESSIONS=$(( TOTAL_SESSIONS + SRV_TOTAL ))
LIVE_TV=$(( LIVE_TV + SRV_LIVE ))
TRANSCODING=$(( TRANSCODING + SRV_TRANSCODE ))
DIRECT=$(( DIRECT + SRV_DIRECT ))
if [[ "$TOTAL_SESSIONS" -gt 0 ]]; then
# 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
# Storage state — count session folders on each location
# Emby folder names don't match API session IDs — per-session location not possible
# Split state shows when symlink has flipped mid-session
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)
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 "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"
elif [[ "$SSD_SESSION_COUNT" -gt 0 ]]; then
echo " $ICON_LINK Storage: $ICON_DISK SSD"
else
echo " $ICON_LINK Storage: $ICON_INFO no transcode folders found"
fi
echo ""
# List each active session
# List each active session
if [[ "$SRV_TOTAL" -gt 0 ]]; then
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)
@@ -402,13 +428,39 @@ else
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)
else
info "No active streams"
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 — 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)
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 "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"
elif [[ "$SSD_SESSION_COUNT" -gt 0 ]]; then
echo " $ICON_LINK Storage: $ICON_DISK SSD"
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
# -----------------------------------------------------------------------------------------------
@@ -455,7 +507,7 @@ case "$TRANSCODE_MANAGER_MODE" in
info "$ICON_GEAR Mode: SMART — auto threshold management"
if [[ "$EMBY_RUNNING" == false ]]; then
info "Emby not running — skipping threshold checks"
info "No media servers running — skipping threshold checks"
elif [[ "$RAMDISK_HEALTHY" == false ]]; then
info "Ramdisk unavailable — staying on SSD until ramdisk recovers"
+36 -2
View File
@@ -21,12 +21,45 @@
# v1.5 — Full icon set expanded — each operation and state has its own distinct icon
# All function output updated to use correct icon per context
# Icons grouped and commented by category for clarity
# v1.6 — ICON_CONTAINERS added — 📦 anchors all container sections for visual consistency
# ICON_NOT_RUNNING changed to ⭕ — distinct from ICON_STOPPED 🔴
# Section dividers updated from --- to ━━━ for cleaner log readability
# Summary passed/failed lines use ICON_SUCCESS and ICON_ERROR consistently
# v1.7 — ICON_MOVER added for mover operations
# ICON_CONTAINERS replaces ICON_DOCKER for docker/container operations
# validate_int added — reusable integer validation for any script
# v1.8 — ICON_PHP added for PHP-FPM operations
# v1.9 — ICON_REBOOT added for server reboot operations
# v2.0 — ICON_PLUGIN added for User Scripts plugin operations
# v2.1 — ICON_ZFS and ICON_MEM added for ZFS and memory diagnostics
# Diagnostics icon group added to icon block
# v2.2 — ICON_WATCHDOG added for Docker watchdog monitoring operations
# v2.3 — ICON_NOTIFY added for notification operations
# notify() added — shared notification function supporting unRAID native and Discord
# NOTIFY_UNRAID and DISCORD_WEBHOOK configured in Master.conf
# v2.4 — ICON_CLEAN, ICON_TRASH added for media cleaner operations
# ICON_PERMS, ICON_UNLOCKED added for media permissions operations
# ICON_REBOOT_SMART added for smart conditional reboot
# v2.5 — ICON_RAM added for ramdisk operations
# ICON_LINK added for symlink state and management
# Transcode scripts group added to ecosystem
# v2.6 — ICON_FAILOVER added for failover operations
# check_local_array added — verifies local /mnt/user is mounted and healthy
# check_remote_array added — verifies remote /mnt/user is mounted and healthy
# check_remote_docker added — verifies remote Docker daemon is responding
# ping_remote added — non-fatal ping returning status for failover use
# ping_internet added — non-fatal external ping for failover use
# v2.7 — ICON_WEBGUI added for WebGUI watchdog operations
# ICON_DOCKER_NET added for Docker network connect operations
# v2.8 — ICON_CERT added for SSL certificate monitoring operations
# v2.9 — ICON_MONITOR added for monitoring section headers
# v3.0 — Script locking system added — prevents concurrent execution conflicts
# acquire_lock() — create PID lock file, register EXIT trap
# release_lock() — remove lock file on exit
# acquire_rsync_lock() — per-profile lock + global concurrent limit
# release_rsync_lock() — decrement global counter, remove profile lock
# check_api() — pre-flight API reachability check
# ICON_LOCK added — 🔏 script instance lock acquired/released
# ICON_SMART added for drive SMART health operations
# ICON_BANDWIDTH added for bandwidth tracking operations
# ICON_DIGEST added for health digest operations
@@ -89,6 +122,7 @@ ICON_CLEAN="🧹" # media cleaner operations
ICON_TRASH="🗑️" # files being deleted
ICON_PERMS="🔐" # permissions operation / section header
ICON_UNLOCKED="🔓" # permissions successfully applied to a share
ICON_LOCK="🔏" # script instance lock — acquired/released
# Transcode Operations
ICON_RAM="💨" # ramdisk operations — fast ephemeral storage
@@ -660,7 +694,7 @@ acquire_lock() {
# Register EXIT trap to always release lock
trap "_release_on_exit '$lockfile'" EXIT
log "Lock acquired: $script_name (PID $$)"
log "$ICON_LOCK Lock acquired: $script_name (PID $$)"
}
# -----------------------------------------------------------------------------------------------
@@ -719,7 +753,7 @@ acquire_rsync_lock() {
# Register EXIT trap
trap "_release_rsync_on_exit '$profile_lock'" EXIT
log "rsync lock acquired: profile '$profile' (PID $$, active: $(( current_count + 1 ))/$RSYNC_MAX_CONCURRENT)"
log "$ICON_LOCK rsync lock acquired: profile '$profile' (PID $$, active: $(( current_count + 1 ))/$RSYNC_MAX_CONCURRENT)"
}
# Internal — release rsync lock on exit