Skip to main content

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.jsserver.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:

  1. Opens one ssh (with SSH_OPTS, see below) to run the curl pull command.
  2. 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.
  3. 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=notrunc for .m3u (one IN_MODIFY), cat tmp > final for media files; mv would create a new inode and the watch would never fire.
  4. stat -c %s returns the final size for verification.
  5. 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)

MethodPathGuardsServes
GET/api/sync/media?rel=<rel>&token=<t>SYNC_TOKEN, rel path-guarded under MEDIA_ROOTAny media file
GET/api/sync/m3u?token=<t>SYNC_TOKENPLAYLIST_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:

  • finalizeSync reads the local M3U and compares it to lastPushedM3uContent. 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=1Mone write syscall = one IN_MODIFY (a plain cat tmp > final truncates then writes = two events → double reload → head-replay). Media files use cat tmp > final. The M3U is padded to a fixed 32768 bytes locally so dd-notrunc fully overwrites with no stale tail. Never mv -f.
  • An empty M3U is never pushed — an empty overwrite makes Liquidsoap emit Fetch failed: empty and 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/stat command 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 just test -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):

SettingValue
Remote targetsms@10.10.8.230:/home/sms/radio/media
Extensions.mp3, .ogg, .wav
Skip ruleFile already present and same size
ModeWalk 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.js is 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.