Changeset 21522ae in Klonkt for src/services/PlaylistService.js


Ignore:
Timestamp:
05/20/2026 10:14:01 PM (4 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
353c39c
Parents:
46f23fd
git-author:
Robin Genis <roboburr@…> (05/20/2026 10:13:26 PM)
git-committer:
Robin Genis <roboburr@…> (05/20/2026 10:14:01 PM)
Message:

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@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/PlaylistService.js

    r46f23fd r21522ae  
    7777   * Returns null if the playlist doesn't exist.
    7878   *
    79    * `signUrl` is an optional callback that takes a media filename and returns
    80    * a (possibly signed) URL. If not provided, tracks come back with no `url`
    81    * and the caller has to resolve them. The render pipeline in posts.js
    82    * always passes signUrl.
    83    */
    84   static get(siteId, id, signUrl) {
     79   * `urlFor` is an optional callback that takes a media filename and returns
     80   * its stream URL. If not provided, tracks come back with no `url` and the
     81   * caller has to resolve them. The render pipeline in posts.js always passes
     82   * urlFor.
     83   */
     84  static get(siteId, id, urlFor) {
    8585    id = this.normalizeId(id);
    8686    if (!id) return null;
     
    117117          cover: t.cover_url || p.cover_url || '',
    118118          duration: t.duration || 0,
    119           url: signUrl ? signUrl(t.filename).url : null,
     119          url: urlFor ? urlFor(t.filename) : null,
    120120        })),
    121121    };
Note: See TracChangeset for help on using the changeset viewer.