source: Klonkt/src/services/AudioStreamService.js@ db81e56

main
Last change on this file since db81e56 was 21522ae, checked in by Robin Genis <roboburr@…>, 4 months ago

audio: Spotify-style blob playback + same-origin gate (fix playback loop)

Root cause of the "next-loops-but-never-plays after 4-5 songs" bug: every
track URL was HMAC-signed once at page-render time with a 10-min TTL. A whole
queue shared that single deadline, so tracks further down expired mid-session
-> /audio/stream returned 403 -> audio 'error' -> auto-skip -> next track also
expired -> infinite loop. The 3-strike guard never fired because the eager
'play' event reset the counter before each 403 landed.

Removed the expiring-token system entirely and replaced it with two
non-expiring layers:

  • Client fetch()es track bytes and plays from a blob: object URL (no shareable URL, no "save audio as"); blobs revoked to avoid leaks; loadSeq guards fast prev/next; pre-seed is metadata-only (no auto-download).
  • Server gates /audio/stream to same-origin browser fetches (X-Audio-Player header or Sec-Fetch-Site): blocks address-bar paste, hotlinks, curl.

Also: reset error counter on real 'playing' event (not eager 'play') so the
3-strike auto-skip-stop actually works; fix admin play-state detection to
compare logical currentTrack().url instead of the now-blob: audio.src; bump
audio-player.js cache-buster v5.

Co-Authored-By: Claude Opus 4.7 <noreply@…>

  • Property mode set to 100644
File size: 2.0 KB
Line 
1/**
2 * AudioStreamService — builds URLs for the audio streaming route.
3 *
4 * ┌─ ANTI-THEFT MODEL (Spotify-flavoured, step 1 — 2026-05-20) ─────────────┐
5 * │ audioUrl() returns a plain /audio/stream/<filename> path. There is NO │
6 * │ signed/expiring token in the URL — that earlier design baked a single │
7 * │ 10-min deadline into a whole queue at render time, so later tracks' │
8 * │ tokens expired mid-session and the player looped "next" forever. │
9 * │ │
10 * │ Protection now lives in two NON-expiring layers, so it can't cause that │
11 * │ failure again: │
12 * │ 1. Client (audio-player.js) fetch()es the bytes and plays from a │
13 * │ blob: object URL — no shareable link, no "save audio as". │
14 * │ 2. Server (routes/audio.js) gates /audio/stream to same-origin │
15 * │ browser fetches — blocks address-bar paste, hotlinks, curl/yt-dlp. │
16 * │ │
17 * │ FUTURE STEPS (deliberate, tested one at a time): │
18 * │ - step 2: MSE chunked/progressive streaming (true Spotify feel) │
19 * │ - step 3: per-session short-lived token in a header, minted JIT │
20 * │ - step 4: light byte obfuscation (XOR/key) on the wire │
21 * └──────────────────────────────────────────────────────────────────────┘
22 */
23
24/**
25 * Build the public stream URL for an audio filename.
26 * Returns null for a falsy filename so callers can guard playability.
27 */
28export function audioUrl(filename) {
29 if (!filename) return null;
30 return `/audio/stream/${encodeURIComponent(filename)}`;
31}
32
33export default { audioUrl };
Note: See TracBrowser for help on using the repository browser.