Changeset d43ea3f in Klonkt for src/assets/js/audio-player.js


Ignore:
Timestamp:
06/20/2026 04:22:32 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
71e8fa5
Parents:
b56afa7
Message:

feat(embed): YouTube playlist/album as audio on the site

An audio-only YouTube embed with ?list=… now renders as an album card (same
style as own albums): cover + album name + numbered track list (titles via the
oEmbed proxy; video IDs via getPlaylist). Clicking a track or the album art
plays via the site audio player (loadPlaylist on index → next/prev within the
album; title/cover update per track; row highlight via #track-<videoId>).
Duration per track is empty (—:—) — not available without a YouTube Data API key.
busters audio-player.js?v=23, embed-player.js?v=12, embed.css?v=7.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/audio-player.js

    rb56afa7 rd43ea3f  
    341341      isPlaying = false; root.classList.remove('is-playing'); stopYTTick();
    342342    } else if (s === 0) {
    343       stopYTTick(); next();
     343      stopYTTick();
     344      // Bij een afspeellijst regelt YouTube zelf het doorschakelen → niet dubbel.
     345      const t = queue[currentIndex];
     346      if (!(t && t.ytList)) next();
    344347    }
    345348  }
     
    356359  function pullYTMeta() {
    357360    try {
    358       const t = queue[currentIndex]; if (!t || !t.yt || !ytPlayer.getVideoData) return;
    359       const vd = ytPlayer.getVideoData();
    360       if (vd && vd.title && (!t.title || t.title === 'YouTube')) {
    361         t.title = vd.title; titleEl.textContent = vd.title; sheetTitle.textContent = vd.title; renderQueue(); markPlaying(t.id);
     361      const t = queue[currentIndex]; if (!t || (!t.yt && !t.ytList) || !ytPlayer.getVideoData) return;
     362      const vd = ytPlayer.getVideoData() || {};
     363      // Bij een afspeellijst wisselt de titel per nummer → altijd bijwerken.
     364      if (vd.title && (t.ytList || !t.title || t.title === 'YouTube')) {
     365        titleEl.textContent = vd.title; sheetTitle.textContent = vd.title;
     366        if (!t.ytList) t.title = vd.title;
     367      }
     368      if (vd.video_id) {
     369        setYtCover(vd.video_id);
     370        markPlaying(vd.video_id);  // licht de bijbehorende album-rij op (id="track-<videoId>")
    362371      }
    363372    } catch (e) {}
     
    395404    if (metaOnly) return;
    396405
    397     // YouTube-bron → verborgen YT-speler i.p.v. de blob-<audio>.
    398     if (t.yt) {
     406    // YouTube-bron (los nummer óf afspeellijst/album) → verborgen YT-speler.
     407    if (t.yt || t.ytList) {
    399408      ytMode = true;
    400409      try { audio.pause(); } catch (e) {}
     
    408417        if (!p) { onLoadError(new Error('YT API niet beschikbaar'), loadSeq); return; }
    409418        try {
    410           if (autoplay) p.loadVideoById(t.yt); else p.cueVideoById(t.yt);
     419          if (t.ytList) {
     420            const idx = t.ytIndex || 0;
     421            if (autoplay) p.loadPlaylist({ list: t.ytList, index: idx }); else p.cuePlaylist({ list: t.ytList, index: idx });
     422          } else {
     423            if (autoplay) p.loadVideoById(t.yt); else p.cueVideoById(t.yt);
     424          }
    411425          p.setVolume(audio.muted ? 0 : Math.round(audio.volume * 100));
    412426        } catch (e) {}
     
    500514  function togglePlay() { if (ytMode) { isPlaying ? pause() : play(); return; } audio.paused ? play() : pause(); }
    501515  function next() {
     516    const t = queue[currentIndex];
     517    if (t && t.ytList && ytPlayer && ytMode) { try { ytPlayer.nextVideo(); } catch (e) {} return; }
    502518    if (!queue.length) return;
    503519    loadTrack((currentIndex + 1) % queue.length, true);
    504520  }
    505521  function prev() {
     522    const t = queue[currentIndex];
     523    if (t && t.ytList && ytPlayer && ytMode) { try { ytPlayer.previousVideo(); } catch (e) {} return; }
    506524    if (!queue.length) return;
    507525    loadTrack(currentIndex === 0 ? queue.length - 1 : currentIndex - 1, true);
     
    916934  // 'ie in de onderbalk én speelt 'ie door bij navigeren.
    917935  function playYouTube(opts) {
    918     if (!opts || !opts.id) return;
     936    if (!opts || (!opts.id && !opts.list)) return;
    919937    setQueue([{
    920       yt: opts.id,
     938      yt: opts.id || null,
     939      ytList: opts.list || null,     // YouTube-afspeellijst/album (OLAK…/PL…)
     940      ytIndex: opts.index || 0,      // startindex binnen de lijst
    921941      title: opts.title || 'YouTube',
    922942      artist: opts.artist || '',
     
    924944      postUrl: opts.postUrl || '',
    925945    }], 0);
    926     // Upgrade de cover async naar de scherpe maxresdefault (ook 16:9, geen balken)
    927     // als die bestaat. Async → de user-gesture voor afspelen blijft intact; de
    928     // direct gezette mqdefault toont al meteen een balkloze cover.
     946    // De scherpe cover + echte titel komen via pullYTMeta() zodra het nummer speelt.
     947  }
     948
     949  // Zet de bar-cover op de thumbnail van een video (mqdefault = balkloos), en
     950  // upgrade async naar de scherpe maxresdefault als die bestaat.
     951  function setYtCover(videoId) {
     952    if (!videoId) return;
     953    const mq = 'https://i.ytimg.com/vi/' + videoId + '/mqdefault.jpg';
     954    setCoverImage(cover, mq); setCoverImage(sheetCover, mq);
    929955    try {
    930       const maxres = 'https://i.ytimg.com/vi/' + opts.id + '/maxresdefault.jpg';
     956      const maxres = 'https://i.ytimg.com/vi/' + videoId + '/maxresdefault.jpg';
    931957      const im = new Image();
    932958      im.onload = () => {
    933         if (im.naturalWidth <= 120) return;  // 120x90 = YouTube-placeholder (geen maxres)
    934         const t = queue[currentIndex];
    935         if (t && t.yt === opts.id) { t.cover = maxres; setCoverImage(cover, maxres); setCoverImage(sheetCover, maxres); }
     959        if (im.naturalWidth <= 120) return;
     960        const cur = ytPlayer && ytPlayer.getVideoData ? (ytPlayer.getVideoData() || {}).video_id : null;
     961        if (cur === videoId) { setCoverImage(cover, maxres); setCoverImage(sheetCover, maxres); }
    936962      };
    937963      im.src = maxres;
Note: See TracChangeset for help on using the changeset viewer.