Changeset 995b100 in Klonkt for src/services/AudioEmbedService.js


Ignore:
Timestamp:
08/17/2026 07:32:49 AM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
e854ace
Parents:
d97c58c
git-author:
Robin <roboburr@…> (08/17/2026 07:30:41 AM)
git-committer:
Robin <roboburr@…> (08/17/2026 07:32:49 AM)
Message:

YouTube-playlists in een post, met dezelfde parsing als de hub

Een link naar een YouTube-album speelde het eerste nummer en stopte, en
een kale playlist-link werd helemaal niet als YouTube herkend.
detectProvider hield alleen de video-id vast en gooide list= weg.

De ref kent nu drie vormen, exact die van de Klonkt hub, zodat één ref
tussen de twee heen en weer kan zonder vertaling:

"<video>" een video
"<video>?list=<L>" die video, en door de lijst heen
"list:<L>" de hele playlist (YouTube's videoseries)

list mag voor of na v= staan en is in een gebakken href vaak
entity-gecodeerd (&amp;), dus er wordt over de hele URL gezocht in plaats
van op een vaste volgorde. youtube-nocookie.com telt mee als host: dat is
wat een embed zelf uitzendt en dus wat mensen terugplakken.

videoseries is EXACT elf tekens, net als een video-id. Geen lengte- of
grensregel vangt hem -- hij moet bij naam uitgesloten worden. Ik liep er
tijdens het bouwen zelf in met een grenscontrole die eroverheen leek te
gaan; vandaar de test die hem apart vastlegt.

Aan de clientkant (embed-player.js) kennen ytId/ytList/ytEmbedSrc dezelfde
drie vormen. Twee dingen die daar meekomen:

  • De poster interpoleerde een null video-id tot i.ytimg.com/vi/null/hqdefault.jpg -- een 404 als achtergrond. Een kale playlist heeft geen video, en krijgt nu gewoon geen poster.
  • YouTube meldt binnen een playlist "ended" TUSSEN elk tweetal nummers. Daar meteen onEnded op vuren geeft de wachtrij door na nummer één en kapt het album af. Op een lijst wachten we daarom 2,5 seconde, en telt alleen een stilte die niet door het volgende nummer wordt onderbroken -- dezelfde regel als in de hub.

De dode youtubeIframe() is meegegaan: hij wordt nergens aangeroepen, maar
een terugval die de playlist stil laat vallen is de ergste soort, want
die ziet eruit alsof het werkte.

Wat hier NIET in zit: de hub haalt ook de nummerlijst van een playlist op
(Data API met sleutel, anders keyless via de RSS-feed). Dat vraagt een
sleutel en een bewaarplek en is een aparte keuze.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/AudioEmbedService.js

    rd97c58c r995b100  
    6868    }
    6969
    70     // YouTube — video id is always exactly 11 characters (aligns with the client-side
    71     // ytId() in embed-player.js, which also expects {11}).
    72     if (/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/|youtube\.com\/live\/)([A-Za-z0-9_-]{11})/i.test(url)) {
    73       const match = url.match(/(?:v=|youtu\.be\/|embed\/|shorts\/|live\/)([A-Za-z0-9_-]{11})/i);
    74       return { provider: 'youtube', id: match[1], url };
     70    // YouTube — a video id is always exactly 11 characters (aligns with the
     71    // client-side ytId() in embed-player.js, which also expects {11}).
     72    //
     73    // A link may carry a video, a playlist, or both, and until now we kept only
     74    // the video and threw `list=` away -- so a link to an album played its first
     75    // song and stopped. The ref now keeps whichever is there, in the same three
     76    // shapes the Klonkt hub uses, so one ref travels between the two unchanged:
     77    //
     78    //   "<video>"           one video
     79    //   "<video>?list=<L>"  that video, and on through the list
     80    //   "list:<L>"          the whole playlist (YouTube's `videoseries`)
     81    //
     82    // `list` may sit before or after `v=` and is often entity-encoded (&amp;)
     83    // in a baked href, hence the scan over the whole URL rather than a fixed
     84    // order. A list id is 10-60 chars: longer and looser than a video id.
     85    if (/(?:youtube(?:-nocookie)?\.com\/(?:watch\?|playlist\?|embed\/|shorts\/|live\/)|youtu\.be\/)/i.test(url)) {
     86      const vm = url.match(/(?:[?&](?:amp;)?v=|youtu\.be\/|\/embed\/|\/shorts\/|\/live\/)([A-Za-z0-9_-]{11})(?![A-Za-z0-9_-])/i);
     87      const lm = url.match(/[?&](?:amp;)?list=([A-Za-z0-9_-]{10,60})/i);
     88      // `videoseries` is a marker, not a video: a bare playlist embed URL reads
     89      // /embed/videoseries?list=..., and taking that for an id gives a dead
     90      // frame. It is EXACTLY eleven characters, so no length rule catches it --
     91      // it has to be named. (Measured, not assumed: it slipped through a
     92      // boundary check that looked like it covered this.)
     93      const id = vm && vm[1] !== 'videoseries' ? vm[1] : null;
     94      const list = lm ? lm[1] : null;
     95      if (id || list) {
     96        const ref = id ? (list ? `${id}?list=${list}` : id) : `list:${list}`;
     97        // `id` stays exactly what it was for every caller that only wants a
     98        // video; `list` and `ref` are additions.
     99        return { provider: 'youtube', id, list, ref, url };
     100      }
    75101    }
    76102
     
    122148      // iframe, so the embed appears in OUR brand style.
    123149      case 'youtube':
    124         return this.embedPlaceholder('youtube', config.id, 'video',
    125           config.url || `https://youtu.be/${config.id}`);
     150        // The ref carries the list when there is one; `id` alone would drop it
     151        // and play a single song out of an album.
     152        return this.embedPlaceholder('youtube', config.ref || config.id, 'video',
     153          config.url || (config.id ? `https://youtu.be/${config.id}`
     154                                   : `https://www.youtube.com/playlist?list=${config.list}`));
    126155      case 'soundcloud':
    127156        return this.embedPlaceholder('soundcloud', config.url, 'track', config.url);
     
    224253  }
    225254
    226   static youtubeIframe({ id }) {
    227     const src = `https://www.youtube-nocookie.com/embed/${id}`;
     255  /**
     256   * The plain provider iframe. Takes the same ref shapes as the placeholder:
     257   * "<video>", "<video>?list=<L>" and "list:<L>" -- a bare playlist embeds as
     258   * `videoseries`. Kept in step with the placeholder path on purpose: this is
     259   * the fallback, and a fallback that silently drops the playlist is the worst
     260   * kind, because it looks like it worked.
     261   */
     262  static youtubeIframe({ id, ref }) {
     263    const r = ref || id || '';
     264    const base = 'https://www.youtube-nocookie.com/embed/';
     265    let src;
     266    if (r.startsWith('list:')) {
     267      src = `${base}videoseries?list=${encodeURIComponent(r.slice(5))}`;
     268    } else if (r.includes('?list=')) {
     269      const [v, l] = r.split('?list=');
     270      src = `${base}${encodeURIComponent(v)}?list=${encodeURIComponent(l)}`;
     271    } else {
     272      src = base + encodeURIComponent(r);
     273    }
    228274    return `
    229275      <figure class="folio-embed folio-embed--youtube">
Note: See TracChangeset for help on using the changeset viewer.