Media Sync
Keeping the production node's library in step with the local library is a core operation. Since 2026-08-07 the engine uploads over plain HTTP pull — scp was eliminated because the SYSTEM-session engine had no SSH keys and every ssh fell back to a password prompt on a hidden session-0 console, hanging forever.
HTTP-pull upload (radio-engine.js → server.js)
The data path never touches SSH bytes. The engine opens exactly one ssh connection per file — and that ssh is only used to run a tiny control command on the remote that pulls the file itself:
remoteWrite() (radio-engine.js) does, per file:
- Opens one ssh (with
SSH_OPTS, see below) to run the curl pull command. - The remote
curls the file over plain HTTP from the LAN (http://10.40.3.174:5000/api/sync/media?...) — the same host:port Liquidsoap already uses for/metadata. - The pull is written to a tmp file, then moved into place in place so
Liquidsoap's
reload_mode="watch"inotify fires (see Liquidsoap):dd conv=notruncfor.m3u(oneIN_MODIFY),cat tmp > finalfor media files;mvwould create a new inode and the watch would never fire. stat -c %sreturns the final size for verification.- 5 retries with exponential backoff + a 190s watchdog SIGKILL so a wedged transfer can never leak an orphan process.
Server-side token-gated endpoints (server.js)
| Method | Path | Guards | Serves |
|---|---|---|---|
| GET | /api/sync/media?rel=<rel>&token=<t> | SYNC_TOKEN, rel path-guarded under MEDIA_ROOT | Any media file |
| GET | /api/sync/m3u?token=<t> | SYNC_TOKEN | PLAYLIST_ROOT/main.m3u |
SYNC_TOKEN defaults to loklok-sync-pull (env-overridable) and must match on
both ends.
SSH options (SSH_OPTS)
Every ssh now runs with:
-o BatchMode=yes -o PreferredAuthentications=publickey
so an ssh can never hang on a password prompt again — it fails fast instead.
This was the root fix for the "~330 stuck scp/ssh orphans" incident: the engine
had run as SYSTEM (session 0, via LOKLOK_ServerGuard), and
C:\Windows\system32\config\systemprofile\.ssh was empty, so every ssh fell
back to an interactive password prompt on a hidden console. The fix copies
id_ed25519 into the SYSTEM profile .ssh (with ACLs locked down to
SYSTEM:F / Administrators:R) and makes BatchMode fail fast so the prompt can
never hang.
Playlist push (finalizeSync)
The M3U is pushed only when its content actually changed:
finalizeSyncreads the local M3U and compares it tolastPushedM3uContent. If unchanged, it skips the upload entirely and just verifies Liquidsoap is alive (ensureProcess). This prevents reload churn: Liquidsoap watches the file, and a needless rewrite would reload the rotation and rewind the stream onto tracks that just played.- When it does push, it uploads in place. For the M3U specifically the write
is
dd if='<tmp>' of='<final>' conv=notrunc bs=1M— one write syscall = oneIN_MODIFY(a plaincat tmp > finaltruncates then writes = two events → double reload → head-replay). Media files usecat tmp > final. The M3U is padded to a fixed 32768 bytes locally so dd-notrunc fully overwrites with no stale tail. Nevermv -f. - An empty M3U is never pushed — an empty overwrite makes Liquidsoap emit
Fetch failed: emptyand go silent; the last good rundown is preserved.
Rotation verification (verifyRemoteRotation)
After a playlist push the engine verifies the remote actually has every file:
- Probes are chunked to 20 paths per ssh to dodge the Windows 32 KB
command-line limit (one giant
test/statcommand failed with "The command line is too long"). - Each remote file is size-checked:
stat -c %s | grep -qx <localSize>so truncated/partial stale copies are caught and re-uploaded (not justtest -s).
Content-fingerprint manifest (remote_synced.json)
remoteSynced persists to E:\radionew\logs\remote_synced.json and migrated
from bare paths to content fingerprints (path#size#mtimeMs):
- A re-fetched/retagged file at the same path now re-uploads (the fingerprint changed).
- Legacy bare-path entries expire after one rotation (one-time re-upload on the first boot after migration).
- So a server restart never re-uploads the whole 120-file rotation.
Full sync (scripts/sync-media-full.js)
A one-shot reconciliation tool that mirrors the entire local media library to the remote (still uses scp, run from a user session where keys exist):
| Setting | Value |
|---|---|
| Remote target | sms@10.10.8.230:/home/sms/radio/media |
| Extensions | .mp3, .ogg, .wav |
| Skip rule | File already present and same size |
| Mode | Walk local tree → copy missing/changed → verify |
It is idempotent: running it repeatedly converges toward a full mirror without re-copying unchanged files. Use it after bulk library changes or when a manual full refresh is required.
Notes
- The engine's incremental sync uses HTTP-pull, not scp.
sync-media-full.jsis the exception (scp, user session). - The skip-by-filename+size rule is shared, so re-syncing an unchanged file is cheap in both paths.
- Run either sync after a successful pipeline run so the remote always gets the registry-consistent set of files.