feat: slskd reconnect guard in downloaders_reset, mass v2 sync

- downloaders_reset: connection check block before slskd API sections;
  triggers PUT /api/v0/server reconnect if disconnected, polls 60s,
  gates Stuck Searches and Dead Transfer Records on SLSKD_CONNECTED
- Sync all modified/new/deleted files from v2 refactor across Docker_Essentials,
  Media, Monitors, Partnership, Rsync, Tools, Transcodes, unRAID_Essentials,
  common.sh, master confs, and new Manual/README docs
This commit is contained in:
Gmer4Lfe
2026-05-19 20:00:10 -04:00
parent 5cb16d4b18
commit e13f2fa14f
81 changed files with 12164 additions and 10656 deletions
+105 -692
View File
@@ -1,724 +1,137 @@
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 🎬 TRANSCODING
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ━━━━━ TRANSCODES ━━━━━
**Ramdisk-based transcode storage with automatic SSD fallback, session-safe symlink
flipping, and stale file cleanup.** Emby transcodes to RAM at full speed. If the ramdisk
fills up, new sessions automatically shift to SSD — without interrupting anything
already playing. When pressure drops, new sessions shift back.
**Ramdisk-based transcode storage with automatic SSD fallback.** Emby transcodes to RAM
at full speed. When the ramdisk fills, new sessions shift to SSD automatically —
without interrupting anything already playing. When pressure drops, new sessions shift
back to RAM.
> **This system has two subtle configuration requirements that are not obvious and
> both were discovered the hard way in production.** The Docker mount must use
> `bind-propagation=shared` or symlink flips are silently ignored after the first
> flip. The `transcoding-temp` directory must be pre-created on the ramdisk or Emby
> finds the SSD version and routes all sessions there until restarted. Both are
> documented and both will bite you if missed.
> **Two configuration requirements that are not obvious and were both discovered the
> hard way in production.** The Docker mount must use `bind-propagation=shared` or
> symlink flips are silently ignored after the first flip. The `transcoding-temp`
> directory must be pre-created on the ramdisk or Emby finds the SSD version and
> routes all sessions there until restarted. Both are documented in Manual-Transcoding.md.
---
## ━━━ THE PROBLEM THAT BUILT THIS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
## ━━━ THE PROBLEM THAT BUILT THIS ━━━
**Three Storage Options, None Perfect on Their Own**
Hard drives: seek times cause buffering on multi-stream transcoding. SSD: fast enough,
but constant small file writes at Emby volume accelerate wear over months. RAM: fastest,
no wear, files vanish instantly on session end — but limited by available memory.
Fix: RAM by default, SSD as a safety net. The system manages the transition automatically.
**Changing Transcode Location Requires Restarting Emby**
Configuring Emby to switch between ramdisk and SSD requires a restart. Restarting
during active streams drops everyone. A 7-person household with 5 Live TV streams at
9pm is not a good moment to restart Emby.
Fix: symlink indirection. Emby points at a fixed path. The symlink target changes.
ffmpeg resolves the symlink once at session start — existing sessions are completely
unaffected by flips. Only new sessions follow the new target.
**Docker Bind Mount Silently Ignored After First Flip**
Symlink flip from ramdisk → SSD worked. Flip back: nothing. All new sessions still land
on SSD. The symlink on the host is correct. Emby doesn't see it.
Cause: Docker's default `rprivate` propagation resolves the symlink target at mount time
and locks that inode. Subsequent flips are invisible to the container.
Fix: `bind-propagation=shared` in Extra Parameters. Host mount changes propagate into
the container in real time. Requires `--mount` syntax — the path mapping UI doesn't
support propagation.
**Sessions Drifting to SSD After a Day of Operation**
System working correctly for hours, then sessions gradually drift to SSD despite the
ramdisk having plenty of space.
Cause: cleanup was removing the empty `transcoding-temp` directory from the ramdisk.
Emby then found the SSD fallback version and routed all sessions there.
Fix: `transcoding-temp` is excluded from cleanup by name. `ramdisk_setup.sh` pre-creates
it at mount time. Both protections together prevent this permanently.
**lsof Per File on a Live TV System**
Early cleanup called `lsof filename` per file to check if anything had it open. On a busy
Live TV night with 5 simultaneous streams, the ramdisk contains thousands of HLS segment
files — thousands of subprocess calls every 3 minutes.
Fix: lsof called once per location to build a complete open-file map. All subsequent
checks are O(1) lookups against that map.
---
### 🔴 Three Storage Options, None Perfect on Their Own
## ━━━ WHAT THIS FOLDER DOES ━━━
Emby transcodes generate hundreds of small HLS segment files written and read
continuously at high throughput. Where those files live matters a lot:
Three scripts, one goal: keep transcodes on RAM, fall back to SSD when needed.
```bash
# ─────────────────────────────────────────────────────────────────────────────
# Hard drives:
# Seek times cause buffering on simultaneous multi-stream transcoding.
# 5 streams trying to seek on spinning disks = constant buffering for everyone.
#
# SSD (cache pool):
# Fast enough for any realistic load.
# But: constant small file writes at Emby volume accelerate SSD wear.
# A busy Live TV night writes and deletes thousands of segment files.
# Over months, this adds up.
#
# RAM (tmpfs):
# Fastest possible — no disk I/O at all.
# No wear — RAM doesn't have write cycles.
# Files disappear instantly on session end — no cleanup needed for normal exits.
# One risk: running out of RAM during heavy load.
# ─────────────────────────────────────────────────────────────────────────────
```
`ramdisk_setup.sh` runs at array start — creates the tmpfs, SSD fallback directory,
symlink, and pre-creates `transcoding-temp`. Everything that must exist before Emby starts.
The correct answer is RAM by default, SSD as a safety net. The ramdisk handles normal
operation. The SSD absorbs unexpected load spikes. The system manages the transition
automatically.
`transcode_cleanup.sh` runs first in every 3-minute cycle — removes stale files from both
ramdisk and SSD. Cleans up before usage is measured, so the manager sees real load.
`transcode_manager.sh` runs second — measures ramdisk usage, flips the symlink if
thresholds are crossed, runs safety checks, displays active sessions, writes the daily log.
The symlink is the mechanism that makes this seamless. Emby writes to a fixed path. That
path is a symlink whose target is managed at runtime. Sessions in progress never notice.
---
### 🔴 Changing Transcode Location Requires Restarting Emby
The obvious approach — configure Emby to use the ramdisk, configure SSD as fallback in
Emby's settings — requires restarting Emby to switch between them. Restarting Emby
during active streams drops everyone. A 7-person household with 5 Live TV streams
running at 9pm is not a good moment to restart Emby.
The fix: symlink indirection. Emby points at a fixed path that never changes.
The symlink target changes. ffmpeg resolves the symlink once at session start and
holds the resolved path — existing sessions are completely unaffected by symlink
changes. Only new sessions care about where the symlink currently points.
The transition is seamless. Sessions in progress when the flip happens continue
writing to wherever they started. Only new sessions after the flip go to the new
location. No restart. No interruption.
---
### 🔴 Docker Bind Mount Silently Ignored After First Flip
Got the symlink system working. First flip from ramdisk to SSD: works. Flip back to
ramdisk: nothing. All new sessions still land on SSD. The symlink on the host clearly
points at the ramdisk — `readlink /mnt/ram-transcode` shows the correct path — but
Emby keeps writing to SSD.
The cause: Docker's default bind mount uses `rprivate` propagation. With `rprivate`,
Docker resolves the symlink target at first mount and locks that inode. When the
symlink flips, Docker ignores it — it already has a private SSD binding locked in.
The container sees the path but the mount behind it doesn't update.
The fix: `bind-propagation=shared` in Extra Parameters. With shared propagation, host
mount changes propagate into the container in real time. Symlink flips on the host are
immediately visible inside the container. This requires using `--mount` syntax instead
of a standard template path mapping — that syntax supports propagation, the path
mapping UI does not.
---
### 🔴 Sessions Drifting to SSD After a Day of Operation
System working correctly for hours. Then gradually sessions start landing on SSD even
though the ramdisk has plenty of space and the symlink points at the ramdisk. Next day
all sessions are on SSD.
The cause: cleanup was removing the empty `transcoding-temp` directory from the
ramdisk. When `transcoding-temp` doesn't exist on the ramdisk, Emby searches its
accessible paths for an existing one. It finds the SSD fallback version. All subsequent
sessions route there until Emby is restarted.
The fix: `transcoding-temp` is protected from cleanup — excluded by name from the
`find` command. And `ramdisk_setup.sh` pre-creates it at mount time so Emby always
finds it on the ramdisk first. Both fixes together prevent this permanently.
---
### 🔴 lsof Per File on a Live TV System
Early cleanup implementation called `lsof filename` per file to check if anything had
it open. On a busy Live TV night with 5 simultaneous streams, the ramdisk contains
thousands of HLS segment files. Calling `lsof` once per file was creating thousands
of subprocess calls every 3 minutes. The cleanup script was spending more time on
lsof calls than on actual cleanup.
The fix: `lsof` is called once per location to build a complete open-file map. All
subsequent file checks are O(1) lookups against that pre-built map. Thousands of files,
one `lsof` call, no performance penalty.
---
## ━━━ THE DESIGN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
### ── The Symlink Architecture ─────────────────────────────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# Emby is configured to write transcodes to TRANSCODE_LINK.
# TRANSCODE_LINK is a symlink — its target is managed at runtime.
#
# Normal operation (ramdisk has headroom):
# /mnt/ram-transcode → /mnt/ramdisk_transcodes/ (ramdisk, fast, no wear)
#
# Heavy load (ramdisk filling up):
# /mnt/ram-transcode → /mnt/cache/Temp_Storage/Emby/Transcodes/ (SSD)
#
# Emby doesn't know this symlink exists. It writes to /mnt/ram-transcode.
# ffmpeg resolves the symlink ONCE when a session starts.
# After that it holds a direct reference to the actual directory.
# Flipping the symlink has ZERO effect on sessions already in progress.
# Only NEW sessions care about where the symlink currently points.
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── The Threshold Logic ──────────────────────────────────────────────────────
```bash
# master.conf
# ─────────────────────────────────────────────────────────────────────────────
# Two thresholds with a hysteresis gap to prevent flip-flopping.
#
RAMDISK_WARN_GB=8.8 # flip to SSD when ramdisk usage exceeds this
RAMDISK_LOW_GB=6.5 # flip back to ramdisk when usage drops below this
#
# The 2.3GB hysteresis gap:
# Without this gap: usage hovers at 8.7GB → flip to SSD → sessions drain
# → usage drops to 8.5GB → flip back → new sessions fill → flip again
# The symlink would oscillate every few minutes under moderate load.
#
# With the gap: usage must drop all the way to 6.5GB before flipping back.
# That requires multiple sessions to end completely — a genuine recovery,
# not a brief fluctuation. Stable, predictable behaviour.
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── What Lives Where ──────────────────────────────────────────────────────────
## ━━━ RELATIONSHIP TO OTHER FOLDERS ━━━
```
/mnt/ramdisk_transcodes/ ← tmpfs (HOST*_RAMDISK_SIZE ceiling)
transcoding-temp/ ← pre-created by ramdisk_setup.sh — always here
E0D8DC/ ← Emby session (Live TV HLS segments)
F1A9BB/ ← another session
unRAID_Essentials/
array_start.sh ──────────────────────────────► ramdisk_setup.sh (at array start)
/mnt/ram-transcode ← symlink — managed at runtime by transcode_manager.sh
currently points at: /mnt/ramdisk_transcodes/
Orchestrators/
transcode_management.sh ──── cleanup first ──► transcode_cleanup.sh
──── then manager ──► transcode_manager.sh
(every 3 minutes — order non-negotiable)
/mnt/cache/Temp_Storage/Emby/Transcodes/ ← SSD fallback
transcoding-temp/ ← also pre-created — Emby finds ramdisk version first
xyz789/ ← sessions that started when ramdisk was full
Monitors/
weekly_health_digest.sh ◄─── reads ──────────── TRANSCODE_DAILY_LOG
```
---
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
## ⚙️ DOCKER MOUNT — READ THIS BEFORE ANYTHING ELSE
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> **This is the most important configuration requirement in the entire folder.**
> Get this wrong and symlink flips silently stop working after the first flip.
> The system appears to work initially and fails subtly.
Do not schedule `transcode_cleanup.sh` or `transcode_manager.sh` directly.
Both are called by `transcode_management.sh` in the correct order.
---
### ── Required Mount Configuration ───────────────────────────────────────────
## ━━━ SCRIPTS IN THIS FOLDER ━━━
```bash
# In Emby's Extra Parameters in the unRAID Docker template:
# ─────────────────────────────────────────────────────────────────────────────
--mount type=bind,source=/mnt/ram-transcode,target=/ext-ram-transcode,bind-propagation=shared
# ─────────────────────────────────────────────────────────────────────────────
# This REPLACES the transcode path in the standard template path mapping UI.
# Do NOT add this via the path mapping UI — that UI does not support propagation.
# Use Extra Parameters only.
```
| Script | Role | When It Runs |
|--------|------|-------------|
| `ramdisk_setup.sh` | Create tmpfs, SSD fallback dir, symlink, transcoding-temp | At array start (via array_start.sh) |
| `transcode_cleanup.sh` | Remove stale files, check for flip-back opportunity | Every 3 min via transcode_management.sh — runs first |
| `transcode_manager.sh` | Check usage, flip symlink, safety checks, session display, daily log | Every 3 min via transcode_management.sh — runs second |
---
### ── Why `shared` Is Required ────────────────────────────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# rprivate (Docker's default):
# Docker resolves the symlink target at first mount and locks that inode.
# Flip: ramdisk → SSD → flip works.
# Flip: SSD → ramdisk → Docker ignores it. Container still sees SSD binding.
# All sessions continue to land on SSD forever until Emby restarts.
# Symptom: symlink on host is correct, Emby still uses SSD. Confusing.
#
# shared:
# Host mount changes propagate into the container in real time.
# Flip: ramdisk → SSD → visible in container immediately ✅
# Flip: SSD → ramdisk → visible in container immediately ✅
# System works as designed. Every flip is immediate and correct.
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── Verify the Mount ─────────────────────────────────────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# Check the propagation — must show "shared" not "rprivate":
docker inspect Emby | grep -A4 "ext-ram"
# Expected output includes: "Propagation": "shared"
# Wrong output: "Propagation": "rprivate"
# Check Emby's transcode path setting (inside container):
docker exec Emby cat /config/config/encoding.xml | grep TranscodingTempPath
# Expected: /ext-ram-transcode
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── What NOT to Do ──────────────────────────────────────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# DO NOT add a static SSD transcode path as a second path mapping in the template:
# /mnt/cache/Temp_Storage/Emby/Transcodes → /ssd-transcode
#
# If the SSD path is mounted inside the container, Emby can see it as an
# accessible transcode location. It will route sessions there independently of
# the symlink — completely bypassing the management system. Sessions land on SSD
# regardless of symlink state. The whole system stops working.
# ─────────────────────────────────────────────────────────────────────────────
```
---
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
## 🚀 ramdisk_setup.sh
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Creates the ramdisk, SSD fallback directory, symlink, and `transcoding-temp` on the
ramdisk. Run once at array start. Idempotent — if the ramdisk is already mounted it
reports status and exits cleanly.
```bash
# Scheduled: At Startup of Array (via array_start.sh)
# This runs BEFORE Emby starts — order matters in ARRAY_START_SCRIPTS
```
---
### ── What It Creates ──────────────────────────────────────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# 1. RAMDISK_PATH (/mnt/ramdisk_transcodes)
# mount -t tmpfs -o size=HOST*_RAMDISK_SIZE tmpfs /mnt/ramdisk_transcodes
# In-memory tmpfs — uses only as much RAM as actually needed.
# RAMDISK_SIZE is a ceiling, not a reservation — an empty ramdisk uses ~0 RAM.
#
# 2. transcoding-temp inside the ramdisk
# mkdir -p /mnt/ramdisk_transcodes/transcoding-temp
# Pre-created so Emby always finds it here first.
# Without this: Emby creates transcoding-temp at its first writable location,
# which may be the SSD fallback even when the symlink points at the ramdisk.
# chown nobody:users — correct ownership for Emby to write as PUID=99
#
# 3. TRANSCODE_SSD (/mnt/cache/Temp_Storage/Emby/Transcodes/)
# mkdir -p — creates if missing, silent if exists
# Also pre-creates transcoding-temp/ inside SSD fallback for consistency
#
# 4. TRANSCODE_LINK (/mnt/ram-transcode)
# ln -sfn /mnt/ramdisk_transcodes /mnt/ram-transcode
# Always reset to ramdisk at array start — clean state every boot
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── Verify After Setup ───────────────────────────────────────────────────────
```bash
# Run these after first setup — all three must pass:
mountpoint /mnt/ramdisk_transcodes
# Expected: /mnt/ramdisk_transcodes is a mountpoint
readlink /mnt/ram-transcode
# Expected: /mnt/ramdisk_transcodes
ls /mnt/ramdisk_transcodes/
# Expected: transcoding-temp/
```
---
### ── Usage ───────────────────────────────────────────────────────────────────
```bash
ramdisk_setup.sh # normal run (at array start via array_start.sh)
ramdisk_setup.sh --dry-run # show what would be created without creating
ramdisk_setup.sh --status # show current ramdisk, symlink, and SSD state
ramdisk_setup.sh --log # verbose — show each creation step
```
---
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
## 🔄 transcode_manager.sh
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Monitors ramdisk usage and manages the symlink direction. Called by
`transcode_management.sh` — not scheduled directly. Every 3 minutes it checks usage,
makes a flip decision if needed, runs safety checks, and shows active sessions.
```bash
# Called by: transcode_management.sh (every 3 minutes)
# Not scheduled directly — use transcode_management.sh
```
---
### ── Three Modes ──────────────────────────────────────────────────────────────
```bash
# master.conf
# ─────────────────────────────────────────────────────────────────────────────
TRANSCODE_MANAGER_MODE="smart" # smart | ramdisk | ssd
#
# smart:
# Auto-flips between ramdisk and SSD based on usage thresholds.
# Normal operation — use this in production.
# Ramdisk above RAMDISK_WARN_GB → flip to SSD.
# Ramdisk below RAMDISK_LOW_GB → flip back to ramdisk.
#
# ramdisk:
# Always uses ramdisk. Never flips to SSD.
# Use: light load server, guaranteed RAM performance, testing ramdisk behaviour.
# Warning logged if usage exceeds threshold — no automatic action.
#
# ssd:
# Always uses SSD. Never uses ramdisk.
# Use: post-flip drain (waiting for ramdisk sessions to end naturally),
# maintenance windows, ramdisk capacity testing.
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── Safety Checks — Every Run Regardless of Mode ───────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# These run on EVERY cycle — they protect against state drift:
#
# Symlink missing or broken
# → Recreate pointing at ramdisk, notify
# → Can happen after manual intervention or filesystem issue
#
# Ramdisk disappeared (unmounted)
# → Auto-flip to SSD immediately, notify warning
# → Can happen if tmpfs was manually unmounted or system ran out of memory
#
# SSD path missing
# → Disable SSD fallback (mode=ssd: error)
# → Can happen if SSD pool is not mounted
#
# transcoding-temp missing from ramdisk
# → Recreate immediately, no notification
# → Prevents sessions silently routing to SSD version
#
# Permissions drift
# → Fix silently every run
# → nobody:users ownership, TRANSCODE_CHMOD mode
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── Session Display ──────────────────────────────────────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# Each run shows active sessions from all configured TRANSCODE_SERVERS:
#
━━━ 🎬 Active Emby Sessions ━━━
🎬 Total: 7 | 💨 Live TV: 5 | 🔄 Transcoding: 5 | 🏁 Direct: 2
🔗 Storage: 💨 ramdisk
🎬 Sunny — ABC (WTAE) — Live TV — Transcode
🎬 Mama Bear — Cinemax — Live TV — Transcode
🎬 Gmer4Lfe — WAN Show — TV Show — Transcode
# Split state — sessions on both ramdisk and SSD simultaneously:
# Normal during a flip — ramdisk sessions draining, new sessions on SSD
#
⚠️ Split state — 4 folder(s) on ramdisk / 2 on SSD
🔗 Storage: 💨 ramdisk (4) + 💾 SSD (2)
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── Multi-Server Configuration ─────────────────────────────────────────────
```bash
# master.conf
# ─────────────────────────────────────────────────────────────────────────────
# Any number of media servers can share the same ramdisk scratch space.
# They never touch each other's files — each writes to its own session subfolder.
# Format: "ContainerName|URL|APIKey|Type"
#
TRANSCODE_SERVERS=(
"Emby|http://localhost:8096|0c27448d93a7431f9ac63569f7655829|emby"
# Temporarily cover HOST2's Emby during maintenance:
# "Emby-Jayred365|http://100.x.x.x:8096|HOST2-api-key|emby"
# Jellyfin instance (separate port):
# "Jellyfin|http://localhost:8097|jellyfin-api-key|jellyfin"
)
#
# Type field controls which API endpoint format is used:
# emby → /Sessions endpoint
# jellyfin → /Sessions endpoint (same format, same code path)
# plex → /status/sessions (different format)
#
# ⚠️ Tdarr does NOT belong here.
# Tdarr encodes full video files — large working files would fill the ramdisk
# rapidly and cause constant flips. Tdarr belongs on SSD permanently.
# dedicated tdarr_cleanup.sh handles Tdarr orphan management separately.
#
# Entries with placeholder API keys are skipped automatically.
# Comment out unused entries rather than deleting — placeholders show what's available.
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── Sizing the Ramdisk ───────────────────────────────────────────────────────
```bash
# master_host1.conf
# ─────────────────────────────────────────────────────────────────────────────
# tmpfs uses only as much RAM as actually needed — RAMDISK_SIZE is a ceiling.
# An empty ramdisk uses essentially zero RAM.
#
HOST1_RAMDISK_SIZE="10G" # 10GB ceiling — verified against production usage below
# Production data from this setup (7-household Live TV system):
# Normal (2-3 streams) → ~1.5-2.0GB
# Busy evening (5-6 streams) → ~3.5-4.5GB
# Peak (8 streams, Live TV) → ~5.2GB
# Current ramdisk → 10G with 8.8GB threshold → 1.2GB safety headroom
# ─────────────────────────────────────────────────────────────────────────────
# Threshold sizing — keep ~1.5-2.5GB hysteresis gap:
#
# ┌──────────────┬─────────────────┬────────────────┐
# │ RAMDISK_SIZE │ RAMDISK_WARN_GB │ RAMDISK_LOW_GB │
# ├──────────────┼─────────────────┼────────────────┤
# │ 6G │ 4.8 │ 3.5 │
# │ 8G │ 6.8 │ 5.5 │
# │ 10G │ 8.8 │ 6.5 │
# │ 12G │ 10.5 │ 8.5 │
# └──────────────┴─────────────────┴────────────────┘
```
---
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
## 🧹 transcode_cleanup.sh
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Removes stale transcode files from both ramdisk and SSD fallback locations. Called by
`transcode_management.sh` before `transcode_manager.sh` — order is critical.
```bash
# Called by: transcode_management.sh (cleanup runs BEFORE manager)
# Not scheduled directly — use transcode_management.sh
```
---
### ── Deletion Rules ────────────────────────────────────────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# A file is eligible for deletion only when ALL conditions are true:
#
# 1. Older than TRANSCODE_MAX_AGE minutes (mtime — last write time)
# Active segments are being written every few seconds.
# A file not touched in 20 minutes is from a session that ended.
#
# 2. Not currently open by any process
# lsof map built once per location — O(1) lookup per file.
# If ffmpeg has a file open, it is not deleted regardless of age.
# "Session ended in API but ffmpeg still writing" → safe, not deleted.
#
# transcoding-temp directory:
# NEVER deleted, even when empty.
# Protected by name exclusion in find command.
# Deleting it causes Emby to find the SSD version and route there permanently.
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── Why Not Session-Aware Cleanup ────────────────────────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# ffmpeg generates its own folder names inside transcoding-temp, independently
# of the media server API session IDs. There is no reliable mapping between
# API session IDs and the actual folder names on disk.
#
# Attempting to correlate them: session E0D8DC → folder E0D8DC → safe assumption?
# No. The folder name is an internal ffmpeg identifier. It may match, may not.
# Using this correlation would falsely treat active sessions as ended.
#
# lsof is the correct check:
# If ffmpeg has a file open, the file is active regardless of session state.
# If no process has the file open, the file is safely deletable.
# No correlation needed. No race condition. Always correct.
# ─────────────────────────────────────────────────────────────────────────────
```
---
### ── Flip-Back After Cleanup ──────────────────────────────────────────────────
```bash
# ─────────────────────────────────────────────────────────────────────────────
# transcode_cleanup.sh checks ramdisk usage AFTER removing stale files.
# If usage dropped below RAMDISK_LOW_GB → triggers flip-back to ramdisk.
# This handles the recovery direction so transcode_manager.sh doesn't have to.
#
# Without cleanup running first, the manager would see inflated usage from stale
# files and potentially flip to SSD unnecessarily.
# With cleanup running first, the manager always sees real active session usage.
# This is why the order in transcode_management.sh is non-negotiable.
# ─────────────────────────────────────────────────────────────────────────────
```
---
## ━━━ FULL CONFIGURATION REFERENCE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```bash
# master.conf
# ─────────────────────────────────────────────────────────────────────────────
# ── Paths ──────────────────────────────────────────────────────────────────
TRANSCODE_LINK="/mnt/ram-transcode" # the symlink Emby points at
TRANSCODE_SSD="/mnt/cache/Temp_Storage/Emby/Transcodes/" # SSD fallback
# ── Per-Host (master_host*.conf) ──────────────────────────────────────────
HOST1_RAMDISK_PATH="/mnt/ramdisk_transcodes" # ramdisk mount point
HOST1_RAMDISK_SIZE="10G" # tmpfs ceiling (not a reservation)
HOST1_RAMDISK_WARN_GB=8.8 # flip to SSD above this
HOST1_RAMDISK_LOW_GB=6.5 # flip back below this
# ── Thresholds ─────────────────────────────────────────────────────────────
RAMDISK_SSD_MIN_GB=20 # minimum SSD free space before allowing SSD flip
# prevents filling the SSD cache pool accidentally
TRANSCODE_FLIP_WARN=3 # notify if symlink flips this many times in an hour
# high flip count = ramdisk undersized
# ── Cleanup ────────────────────────────────────────────────────────────────
TRANSCODE_MAX_AGE=20 # minutes — files older than this are stale
TRANSCODE_ORPHAN_AGE=30 # minutes — orphaned session folders removed after this
# ── Manager Mode ───────────────────────────────────────────────────────────
TRANSCODE_MANAGER_MODE="smart" # smart | ramdisk | ssd
TRANSCODE_CHECK_EMBY=true # skip threshold checks when Emby not running
# prevents unnecessary flips at night
# ── Permissions ────────────────────────────────────────────────────────────
TRANSCODE_OWNER="nobody:users" # matches PUID=99 PGID=100 container env
TRANSCODE_CHMOD="755"
# ── Daily Log ──────────────────────────────────────────────────────────────
TRANSCODE_DAILY_LOG="$DATA_DIR/transcode_daily.db"
TRANSCODE_LOG_RETENTION=90 # days — bounded, trimmed on every write
TRANSCODE_STATE_FILE="/tmp/transcode_state.db" # /tmp — resets on reboot
# ── Multi-Server ───────────────────────────────────────────────────────────
TRANSCODE_SERVERS=(
"ContainerName|http://host:port|api-key|type" # emby|jellyfin|plex
)
```
---
## ━━━ SCHEDULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
## ━━━ HOW THE SCRIPTS RELATE ━━━
```
At Startup of Array:
ramdisk_setup.sh ← via array_start.sh — creates ramdisk + symlink + transcoding-temp
Array starts
ramdisk_setup.sh
Creates: /mnt/ramdisk_transcodes (tmpfs)
/mnt/ramdisk_transcodes/transcoding-temp/
/mnt/cache/Temp_Storage/Emby/Transcodes/ (SSD fallback)
/mnt/ram-transcode → /mnt/ramdisk_transcodes (symlink)
Emby starts, reads transcode path from config
Sees: /ext-ram-transcode (bind-mounted from /mnt/ram-transcode)
All new sessions write to: /mnt/ram-transcode → /mnt/ramdisk_transcodes/
Every 3 minutes:
transcode_management.sh ← cleanup first, then manager — order non-negotiable
1. transcode_cleanup.sh ← remove stale files, check if flip-back possible
2. transcode_manager.sh ← check usage, flip if needed, show sessions
Do NOT schedule transcode_manager.sh or transcode_cleanup.sh directly.
Every 3 minutes (transcode_management.sh):
├─ transcode_cleanup.sh
│ Remove files older than TRANSCODE_MAX_AGE, not open by any process
│ transcoding-temp: never deleted
│ If ramdisk recovered below RAMDISK_LOW_GB → trigger flip-back
└─ transcode_manager.sh
Safety checks (symlink, ramdisk mount, transcoding-temp, permissions)
smart mode: ramdisk > RAMDISK_WARN_GB → flip symlink to SSD
ramdisk < RAMDISK_LOW_GB → flip symlink back to ramdisk
Session display (all TRANSCODE_SERVERS)
Append to TRANSCODE_DAILY_LOG
```
---
## ━━━ TROUBLESHOOTING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
---
### 🔴 Sessions Landing on SSD Despite Symlink Pointing at Ramdisk
```bash
# Check 1 — Docker mount propagation (most common cause):
docker inspect Emby | grep Propagation
# Expected: "Propagation": "shared"
# Wrong: "Propagation": "rprivate"
#
# Fix: Add to Emby Extra Parameters and restart Emby:
# --mount type=bind,source=/mnt/ram-transcode,target=/ext-ram-transcode,bind-propagation=shared
# Remove any standard path mapping for the transcode directory.
# Check 2 — transcoding-temp exists on ramdisk:
ls /mnt/ramdisk_transcodes/
# Expected: transcoding-temp/
#
# Fix if missing:
mkdir -p /mnt/ramdisk_transcodes/transcoding-temp
chown nobody:users /mnt/ramdisk_transcodes/transcoding-temp
# Check 3 — no duplicate SSD mount in Emby template:
docker inspect Emby | grep -A3 "Mounts"
# Should show: only /mnt/ram-transcode → /ext-ram-transcode
# Should NOT show: /mnt/cache/Temp_Storage/... as a second mount
```
---
### 🔴 Flip Count High — 3+ Per Hour
```bash
# Ramdisk filling up regularly — sessions draining before more arrive.
# Check peak usage from the weekly coffee report:
# Transcodes section → "Week peak: X.XGB"
#
# If peak is close to RAMDISK_WARN_GB → increase ramdisk:
# master_host1.conf
HOST1_RAMDISK_SIZE="12G" # increase by 2G
HOST1_RAMDISK_WARN_GB=10.5 # adjust thresholds accordingly
HOST1_RAMDISK_LOW_GB=8.5
#
# Then re-run ramdisk_setup.sh to remount at new size:
bash /mnt/user/appdata/unraid_scripts/Transcodes/ramdisk_setup.sh
```
---
### 🔴 Ramdisk Not Mounting at Array Start
```bash
# Check if tmpfs mounted:
mountpoint /mnt/ramdisk_transcodes
# "not a mountpoint" → setup failed or not run yet
# Run manually to see the error:
bash /mnt/user/appdata/unraid_scripts/Transcodes/ramdisk_setup.sh --log
# Common causes:
# /mnt/ramdisk_transcodes directory missing → mkdir -p /mnt/ramdisk_transcodes
# Insufficient RAM → check free RAM: free -h
# RAMDISK_SIZE too large for available RAM → reduce HOST*_RAMDISK_SIZE
```
---
### 🔴 Emergency Manual Flip
```bash
# Flip to SSD immediately — all new sessions go to SSD:
ln -sfn /mnt/cache/Temp_Storage/Emby/Transcodes /mnt/ram-transcode
# Flip back to ramdisk — all new sessions go to ramdisk:
ln -sfn /mnt/ramdisk_transcodes /mnt/ram-transcode
# Check current symlink target:
readlink /mnt/ram-transcode
# Existing sessions in progress are NEVER affected by these changes.
# Only new sessions follow the new symlink target.
```