Changeset fef781e in Klonkt


Ignore:
Timestamp:
06/20/2026 04:46:35 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
0d7acdf
Parents:
8ab20b6
Message:

revert: remove YouTube audio functionality (doesn't work)

YouTube blocks audio-only/hidden embed playback with a "Sign in to confirm
you're not a bot" gate → not reliably usable. All YT-audio reverted:

  • embed-player.js + embed.css → state before the feature (audio-only card, YT-album, buildYtAlbum, prewarm, mode-routing removed). Video embeds + the YT/SC volume button remain.
  • audio-player.js → YT backend (ytMode/ensureYT/playYouTube/playYouTubeList/…) removed; only the blob-<audio> player + existing features.
  • AudioEmbedService.js → audio:/video: mode parsing + data-embed-mode removed.
  • audio.js → /yt-title oEmbed proxy removed.
  • post-edit.ejs → audio/video choice on the embed button removed ([[embed:url]]

normal).

busters audio-player.js?v=28, embed-player.js?v=16, embed.css?v=8.

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

Location:
src
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • src/assets/css/embed.css

    r8ab20b6 rfef781e  
    119119  width: 320px;
    120120  height: 60px;
    121   opacity: 0;
    122   pointer-events: none;
    123 }
    124 /* Verborgen YouTube-audio-speler ván de site-audiospeler (onderbalk). Off-screen
    125  * met echte grootte zodat de browser 'm niet pauzeert; alleen het geluid telt. */
    126 #pcms-yt-host {
    127   position: absolute;
    128   left: -99999px;
    129   top: 0;
    130   width: 320px;
    131   height: 180px;
    132   opacity: 0;
    133   pointer-events: none;
    134 }
    135 /* YouTube-afspeellijst als album-kaart: geen losse embed-card-chrome, de
    136  * .post-album-stijl (uit style.css) stuurt de weergave. */
    137 .pcms-ytalbum { background: none; border: 0; padding: 0; margin: 1.5rem 0; }
    138 .pcms-ytalbum-msg { padding: .75rem 1rem; color: var(--ink-soft, #6b6560); font-size: .9rem; }
    139 
    140 /* Audio-only embed-kaart die nu via de onderbalk speelt = launcher-hint. */
    141 .pcms-embed-elsewhere { opacity: .9; }
    142 .pcms-embed-elsewhere .pcms-embed-title::after {
    143   content: ' — speelt in de speler ↓';
    144   font-weight: normal;
    145   opacity: .6;
    146   font-size: .85em;
    147 }
    148 
    149 /* Audio-only YouTube: de YT-speler draait verborgen (off-screen) maar moet een
    150  * echte grootte houden, anders pauzeert de browser 'm. Alleen het geluid telt. */
    151 .pcms-embed-card--youtube.pcms-embed-audio-mode .pcms-embed-mount {
    152   position: absolute;
    153   left: -99999px;
    154   top: 0;
    155   width: 320px;
    156   height: 180px;
    157121  opacity: 0;
    158122  pointer-events: none;
  • src/assets/js/audio-player.js

    r8ab20b6 rfef781e  
    289289  });
    290290
    291   // ============================================================
    292   // 4a. YouTube-backend — speelt YT-AUDIO via een verborgen IFrame-speler in de
    293   // (persistente) speler-root. Zo speelt YouTube net als de site-tracks door bij
    294   // htmx-navigatie en is 't bedienbaar in de onderbalk. Video wordt niet getoond.
    295   // ============================================================
    296   let ytMode = false, ytPlayer = null, ytReady = false, ytApiLoading = null, ytTick = null;
    297 
    298   function loadYTApi() {
    299     if (window.YT && window.YT.Player) return Promise.resolve(window.YT);
    300     if (ytApiLoading) return ytApiLoading;
    301     ytApiLoading = new Promise((resolve) => {
    302       const prev = window.onYouTubeIframeAPIReady;
    303       window.onYouTubeIframeAPIReady = function () { if (prev) { try { prev(); } catch (e) {} } resolve(window.YT); };
    304       if (!document.querySelector('script[src*="youtube.com/iframe_api"]')) {
    305         const s = document.createElement('script'); s.src = 'https://www.youtube.com/iframe_api'; s.async = true; document.head.appendChild(s);
    306       }
    307       setTimeout(() => { if (window.YT && window.YT.Player) resolve(window.YT); }, 8000);
    308     });
    309     return ytApiLoading;
    310   }
    311 
    312   function ensureYT() {
    313     if (ytPlayer) return Promise.resolve(ytPlayer);
    314     return loadYTApi().then((YT) => new Promise((resolve) => {
    315       if (!YT || !YT.Player) { resolve(null); return; }
    316       let host = document.getElementById('pcms-yt-host');
    317       if (!host) {
    318         host = document.createElement('div'); host.id = 'pcms-yt-host';
    319         const m = document.createElement('div'); m.id = 'pcms-yt-mount'; host.appendChild(m);
    320         root.appendChild(host);
    321       }
    322       ytPlayer = new YT.Player('pcms-yt-mount', {
    323         host: 'https://www.youtube-nocookie.com',
    324         playerVars: { controls: 0, playsinline: 1, rel: 0, modestbranding: 1, iv_load_policy: 3, origin: window.location.origin },
    325         events: {
    326           onReady: () => { ytReady = true; try { ytPlayer.setVolume(audio.muted ? 0 : Math.round(audio.volume * 100)); } catch (e) {} resolve(ytPlayer); },
    327           onStateChange: onYTState,
    328         },
    329       });
    330       setTimeout(() => resolve(ytPlayer), 8000);
    331     }));
    332   }
    333 
    334   function onYTState(e) {
    335     if (!ytMode) return;
    336     const s = e.data;  // 1 playing, 2 paused, 0 ended
    337     if (s === 1) {
    338       isPlaying = true; root.classList.add('is-playing'); root.classList.remove('audio-needs-tap');
    339       // We startten gedempt (autoplay-policy) → nu 't speelt het geluid terugzetten,
    340       // tenzij de gebruiker zelf gedempt heeft.
    341       try { if (!audio.muted) { ytPlayer.unMute(); ytPlayer.setVolume(Math.round(audio.volume * 100)); } } catch (e) {}
    342       mediaRegistry().setActive(registrySelf); startYTTick(); pullYTMeta();
    343     } else if (s === 2) {
    344       isPlaying = false; root.classList.remove('is-playing'); stopYTTick();
    345     } else if (s === 0) {
    346       stopYTTick();
    347       // Bij een afspeellijst regelt YouTube zelf het doorschakelen → niet dubbel.
    348       const t = queue[currentIndex];
    349       if (!(t && t.ytList)) next();
    350     }
    351   }
    352 
    353   function startYTTick() {
    354     if (ytTick) return;
    355     ytTick = setInterval(() => {
    356       if (!ytPlayer || !ytMode) return;
    357       try { updateProgressUI(ytPlayer.getCurrentTime() || 0, ytPlayer.getDuration() || 0); } catch (e) {}
    358     }, 250);
    359   }
    360   function stopYTTick() { if (ytTick) { clearInterval(ytTick); ytTick = null; } }
    361 
    362   function pullYTMeta() {
    363     try {
    364       const t = queue[currentIndex]; if (!t || (!t.yt && !t.ytList) || !ytPlayer.getVideoData) return;
    365       const vd = ytPlayer.getVideoData() || {};
    366       // Bij een afspeellijst wisselt de titel per nummer → altijd bijwerken.
    367       if (vd.title && (t.ytList || !t.title || t.title === 'YouTube')) {
    368         titleEl.textContent = vd.title; sheetTitle.textContent = vd.title;
    369         if (!t.ytList) t.title = vd.title;
    370       }
    371       if (vd.video_id) {
    372         setYtCover(vd.video_id);
    373         markPlaying(vd.video_id);  // licht de bijbehorende album-rij op (id="track-<videoId>")
    374       }
    375     } catch (e) {}
    376   }
    377 
    378   // Gedeelde voortgang-UI (beide backends). Audio gebruikt 'timeupdate', YT de tick.
    379   function updateProgressUI(cur, dur) {
    380     if (!dur || isNaN(dur)) return;
    381     const pct = Math.max(0, Math.min(100, (cur / dur) * 100));
    382     seekFill.style.width = pct + '%'; sheetSeekFill.style.width = pct + '%';
    383     currentEl.textContent = formatTime(cur); totalEl.textContent = formatTime(dur);
    384     sheetCurrent.textContent = formatTime(cur); sheetTotal.textContent = formatTime(dur);
    385   }
    386 
    387291  function loadTrack(index, autoplay, metaOnly) {
    388292    if (!queue[index]) {
     
    392296    currentIndex = index;
    393297    const t = queue[index];
    394     // Metadata + chrome update synchronously (geldt voor beide backends).
    395     titleEl.textContent  = t.title  || (t.yt ? 'YouTube' : 'Untitled');
     298    if (!t.url) {
     299      console.error('[pcms-audio] track has no url', t);
     300      return;
     301    }
     302    console.log('[pcms-audio] loading', t.title, t.url, metaOnly ? '(meta only)' : '');
     303    // Metadata + chrome update synchronously so the UI reacts instantly while
     304    // the bytes download.
     305    titleEl.textContent  = t.title  || 'Untitled';
    396306    artistEl.textContent = t.artist || '';
    397     sheetTitle.textContent  = t.title  || (t.yt ? 'YouTube' : 'Untitled');
     307    sheetTitle.textContent  = t.title  || 'Untitled';
    398308    sheetArtist.textContent = t.artist || '';
    399309    sheetAlbum.textContent  = albumName || '';
     
    406316
    407317    if (metaOnly) return;
    408 
    409     // YouTube-bron (los nummer óf afspeellijst/album) → verborgen YT-speler.
    410     if (t.yt || t.ytList) {
    411       ytMode = true;
    412       try { audio.pause(); } catch (e) {}
    413       loadSeq++; dropPreload();
    414       updateProgressUI(0, 1); currentEl.textContent = '0:00'; totalEl.textContent = '0:00';
    415       seekFill.style.width = '0%'; sheetSeekFill.style.width = '0%';
    416       root.classList.add('audio-loading');
    417       ensureYT().then((p) => {
    418         if (queue[currentIndex] !== t) return;  // inmiddels andere track gekozen
    419         root.classList.remove('audio-loading');
    420         if (!p) { onLoadError(new Error('YT API niet beschikbaar'), loadSeq); return; }
    421         try {
    422           // Gedempt starten = autoplay is ALTIJD toegestaan (YouTube blokkeert
    423           // anders het geluid). onYTState ontdempt zodra 't echt speelt.
    424           if (autoplay) { try { p.mute(); } catch (e) {} }
    425           if (t.ytList) {
    426             const idx = t.ytIndex || 0;
    427             // listType:'playlist' is vereist voor de object-vorm; zonder dit laadt
    428             // (en speelt) de afspeellijst niet betrouwbaar.
    429             if (autoplay) p.loadPlaylist({ listType: 'playlist', list: t.ytList, index: idx });
    430             else p.cuePlaylist({ listType: 'playlist', list: t.ytList, index: idx });
    431           } else {
    432             if (autoplay) p.loadVideoById(t.yt); else p.cueVideoById(t.yt);
    433           }
    434           p.setVolume(audio.muted ? 0 : Math.round(audio.volume * 100));
    435         } catch (e) {}
    436       });
    437       return;
    438     }
    439 
    440     // Normale blob-audio track.
    441     ytMode = false;
    442     stopYTTick();
    443     try { if (ytPlayer && ytPlayer.stopVideo) ytPlayer.stopVideo(); } catch (e) {}
    444     if (!t.url) { console.error('[pcms-audio] track has no url', t); return; }
    445318
    446319    const mySeq = ++loadSeq;
     
    494367
    495368  function play() {
    496     if (ytMode) {
    497       if (ytPlayer && ytReady) { try { ytPlayer.playVideo(); } catch (e) {} }
    498       else if (queue[currentIndex]) loadTrack(currentIndex, true);
    499       return;
    500     }
    501369    if (!audio.src) {
    502370      // Nothing fetched yet (pre-seed showed metadata only, or a load is still
     
    520388    }
    521389  }
    522   function pause()      { if (ytMode) { try { ytPlayer && ytPlayer.pauseVideo(); } catch (e) {} return; } audio.pause(); }
    523   function togglePlay() { if (ytMode) { isPlaying ? pause() : play(); return; } audio.paused ? play() : pause(); }
     390  function pause()      { audio.pause(); }
     391  function togglePlay() { audio.paused ? play() : pause(); }
    524392  function next() {
    525     const t = queue[currentIndex];
    526     if (t && t.ytList && ytPlayer && ytMode) { try { ytPlayer.nextVideo(); } catch (e) {} return; }
    527393    if (!queue.length) return;
    528394    loadTrack((currentIndex + 1) % queue.length, true);
    529395  }
    530396  function prev() {
    531     const t = queue[currentIndex];
    532     if (t && t.ytList && ytPlayer && ytMode) { try { ytPlayer.previousVideo(); } catch (e) {} return; }
    533397    if (!queue.length) return;
    534398    loadTrack(currentIndex === 0 ? queue.length - 1 : currentIndex - 1, true);
     
    540404    document.body.classList.remove('has-audio-player');
    541405    closeSheet();
    542     ytMode = false; stopYTTick();
    543     try { if (ytPlayer && ytPlayer.stopVideo) ytPlayer.stopVideo(); } catch (e) {}
    544406    queue = [];
    545407    albumName = '';
     
    600462    return r;
    601463  }
    602   const registrySelf = { pause() { try { audio.pause(); } catch (e) {} try { if (ytPlayer && ytPlayer.pauseVideo) ytPlayer.pauseVideo(); } catch (e) {} } };
     464  const registrySelf = { pause() { try { audio.pause(); } catch (e) {} } };
    603465
    604466  // ============================================================
     
    636498  audio.addEventListener('volumechange', () => { root.classList.toggle('is-muted', audio.muted || audio.volume === 0); });
    637499  audio.addEventListener('timeupdate', () => {
    638     if (ytMode) return;  // YT-voortgang loopt via de tick
    639     updateProgressUI(audio.currentTime, audio.duration);
     500    if (!audio.duration || isNaN(audio.duration)) return;
     501    const pct = (audio.currentTime / audio.duration) * 100;
     502    seekFill.style.width = pct + '%';
     503    sheetSeekFill.style.width = pct + '%';
     504    currentEl.textContent  = formatTime(audio.currentTime);
     505    totalEl.textContent    = formatTime(audio.duration);
     506    sheetCurrent.textContent = formatTime(audio.currentTime);
     507    sheetTotal.textContent   = formatTime(audio.duration);
    640508  });
    641509
     
    648516  function attachSeek(seekEl) {
    649517    seekEl.addEventListener('click', (e) => {
     518      if (!audio.duration) return;
    650519      const rect = seekEl.getBoundingClientRect();
    651520      const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
    652       if (ytMode) {
    653         try { const d = ytPlayer.getDuration() || 0; if (d) ytPlayer.seekTo(ratio * d, true); } catch (e2) {}
    654         return;
    655       }
    656       if (!audio.duration) return;
    657521      audio.currentTime = ratio * audio.duration;
    658522    });
     
    663527  audio.volume = 0.8;
    664528  volumeSlider.addEventListener('input', () => {
    665     const v = parseInt(volumeSlider.value, 10) || 0;
    666     audio.volume = v / 100;
    667     if (v > 0) audio.muted = false;
    668     if (ytPlayer) { try { ytPlayer.setVolume(v); if (v > 0 && ytPlayer.unMute) ytPlayer.unMute(); } catch (e) {} }
    669   });
    670   muteBtn.addEventListener('click', () => {
    671     audio.muted = !audio.muted;
    672     if (ytPlayer) { try { audio.muted ? ytPlayer.mute() : ytPlayer.unMute(); } catch (e) {} }
    673     root.classList.toggle('is-muted', audio.muted);
    674   });
     529    audio.volume = volumeSlider.value / 100;
     530    if (volumeSlider.value > 0) audio.muted = false;
     531  });
     532  muteBtn.addEventListener('click', () => { audio.muted = !audio.muted; });
    675533
    676534  // ============================================================
     
    939797  // 9. Public API
    940798  // ============================================================
    941   // Speel een YouTube-bron als AUDIO af in deze (persistente) site-speler — door
    942   // de embed-speler aangeroepen voor een audio-only YouTube-embed. Zo verschijnt
    943   // 'ie in de onderbalk én speelt 'ie door bij navigeren.
    944   function playYouTube(opts) {
    945     if (!opts || (!opts.id && !opts.list)) return;
    946     setQueue([{
    947       yt: opts.id || null,
    948       ytList: opts.list || null,     // YouTube-afspeellijst/album (OLAK…/PL…)
    949       ytIndex: opts.index || 0,      // startindex binnen de lijst
    950       title: opts.title || 'YouTube',
    951       artist: opts.artist || '',
    952       cover: opts.cover || '',
    953       postUrl: opts.postUrl || '',
    954     }], 0);
    955     // De scherpe cover + echte titel komen via pullYTMeta() zodra het nummer speelt.
    956   }
    957 
    958   // Zet de bar-cover op de thumbnail van een video (mqdefault = balkloos), en
    959   // upgrade async naar de scherpe maxresdefault als die bestaat.
    960   function setYtCover(videoId) {
    961     if (!videoId) return;
    962     const mq = 'https://i.ytimg.com/vi/' + videoId + '/mqdefault.jpg';
    963     setCoverImage(cover, mq); setCoverImage(sheetCover, mq);
    964     try {
    965       const maxres = 'https://i.ytimg.com/vi/' + videoId + '/maxresdefault.jpg';
    966       const im = new Image();
    967       im.onload = () => {
    968         if (im.naturalWidth <= 120) return;
    969         const cur = ytPlayer && ytPlayer.getVideoData ? (ytPlayer.getVideoData() || {}).video_id : null;
    970         if (cur === videoId) { setCoverImage(cover, maxres); setCoverImage(sheetCover, maxres); }
    971       };
    972       im.src = maxres;
    973     } catch (e) {}
    974   }
    975 
    976   // Speel een YouTube-AFSPEELLIJST af door 'm te expanderen naar onze eigen queue
    977   // van losse YouTube-tracks (elk via loadVideoById = bewezen betrouwbaar; next/
    978   // prev/ended lopen via onze queue). items = [{yt,title,cover,postUrl}, …].
    979   function playYouTubeList(items, startIndex) {
    980     if (!Array.isArray(items) || !items.length) return;
    981     setQueue(items.map((it) => ({
    982       yt: it.yt,
    983       title: it.title || 'YouTube',
    984       artist: it.artist || '',
    985       cover: it.cover || '',
    986       postUrl: it.postUrl || '',
    987     })), startIndex || 0);
    988   }
    989 
    990   // Maak de verborgen YT-speler vast aan (zonder iets te spelen), zodat 'ie bij de
    991   // eerste klik al klaar staat → loadVideoById valt dan binnen de user-gesture en
    992   // YouTube's autoplay-policy blokkeert het niet.
    993   function prewarmYouTube() { try { ensureYT(); } catch (e) {} }
    994 
    995799  window.pcmsAudioPlayer = {
    996     setQueue, play, pause, next, prev, close, openSheet, closeSheet, playYouTube, playYouTubeList, prewarmYouTube,
     800    setQueue, play, pause, next, prev, close, openSheet, closeSheet,
    997801    isPlaying: () => isPlaying,
    998802    currentTrack: () => queue[currentIndex] || null,
  • src/assets/js/embed-player.js

    r8ab20b6 rfef781e  
    162162    if (!provider) return;
    163163
    164     // Audio-only YouTube met een afspeellijst (?list=…) → album-kaart met tracklijst.
    165     if (provider === 'youtube' && el.dataset.embedMode === 'audio') {
    166       const lm = url.match(/[?&]list=([A-Za-z0-9_-]+)/);
    167       if (lm) { try { buildYtAlbum(el, lm[1]); } catch (e) { console.error('[pcms-embed] yt-album faalde', e); } return; }
    168     }
    169 
    170164    el.classList.add('pcms-embed-card', 'pcms-embed-card--' + provider);
    171165    el.classList.remove('pcms-embed-loading');
     
    189183
    190184    // --- UI ophangen (verschilt per provider-type) ---
    191     // Audio-only YouTube (data-embed-mode="audio"): render als audio-kaart met
    192     // onze controls; de YT-speler draait verborgen (off-screen) en levert alleen
    193     // het geluid. Video blijft de default.
    194     const audioOnly = provider === 'youtube' && el.dataset.embedMode === 'audio';
    195     if (audioOnly) el.classList.add('pcms-embed-audio-mode');
    196     const isVideo = provider === 'youtube' && !audioOnly;
    197     // Audio-only YT speelt via de site-onderbalk → geen eigen in-kaart controlebar.
    198     const custom = (provider === 'youtube' || provider === 'soundcloud') && !audioOnly;
     185    const isVideo = provider === 'youtube';
     186    const custom = provider === 'youtube' || provider === 'soundcloud'; // eigen controls
    199187    const poster = provider === 'youtube'
    200188      ? `https://i.ytimg.com/vi/${ytId(ref, url)}/hqdefault.jpg` : '';
    201     // 16:9-thumbnail ZONDER zwarte balken (mqdefault) voor de vierkante cover —
    202     // hqdefault is 4:3 met letterbox en vult een vierkant niet schoon.
    203     const ytThumb = provider === 'youtube'
    204       ? `https://i.ytimg.com/vi/${ytId(ref, url)}/mqdefault.jpg` : '';
    205189
    206190    el.innerHTML = ''
     
    238222    let vol = 100;       // huidige volume 0-100 (wordt op de adapter toegepast)
    239223    let preMuteVol = 100;
    240     // Audio-only YouTube: zet de thumbnail als albumhoes op de audio-kaart.
    241     let ytTitle = '';  // echte videotitel (via same-origin oEmbed-proxy)
    242     if (audioOnly && ytThumb) {
    243       const art0 = el.querySelector('.pcms-embed-art');
    244       if (art0) { art0.style.backgroundImage = `url('${ytThumb}')`; art0.classList.add('has-art'); }
    245       // Speler vast aanmaken → eerste klik valt binnen de user-gesture (autoplay).
    246       if (window.pcmsAudioPlayer && window.pcmsAudioPlayer.prewarmYouTube) window.pcmsAudioPlayer.prewarmYouTube();
    247       // Haal de echte titel op (server-proxy, want YT-oEmbed heeft geen CORS) en
    248       // toon 'm i.p.v. "YouTube".
    249       fetch('/audio/yt-title?url=' + encodeURIComponent(url))
    250         .then((r) => (r.ok ? r.json() : null))
    251         .then((d) => {
    252           if (d && d.title) {
    253             ytTitle = d.title;
    254             const tt = el.querySelector('.pcms-embed-title');
    255             if (tt) tt.textContent = d.title;
    256           }
    257         })
    258         .catch(() => {});
    259     }
    260224
    261225    function setPlayingUI(on) {
     
    332296    // Eerste interactie → adapter mounten + spelen. Daarna toggelt de knop.
    333297    async function ensureMountedAndPlay() {
    334       // Audio-only YouTube → speel via de site-audiospeler (onderbalk): zichtbaar
    335       // én doorspelen bij navigeren. De inline-kaart is enkel de launcher.
    336       if (audioOnly && window.pcmsAudioPlayer && typeof window.pcmsAudioPlayer.playYouTube === 'function') {
    337         window.pcmsAudioPlayer.playYouTube({ id: ytId(ref, url), title: ytTitle || undefined, cover: ytThumb, postUrl: location.pathname + location.search });
    338         el.classList.add('pcms-embed-elsewhere');
    339         return;
    340       }
    341298      if (mounted) { if (adapter) adapter.play(); return; }
    342299      mounted = true;
     
    391348  }
    392349
    393   // Haal de video-IDs van een YouTube-afspeellijst op via een verborgen,
    394   // tijdelijke speler (cuePlaylist → getPlaylist). Geen API-key nodig.
    395   function ytPlaylistIds(listId) {
    396     return ytApi().then((YT) => new Promise((resolve) => {
    397       if (!YT || !YT.Player) { resolve([]); return; }
    398       const host = document.createElement('div');
    399       host.style.cssText = 'position:absolute;left:-99999px;top:0;width:320px;height:180px;opacity:0;pointer-events:none';
    400       const mount = document.createElement('div'); host.appendChild(mount); document.body.appendChild(host);
    401       let done = false;
    402       const finish = (ids) => { if (done) return; done = true; try { p.destroy(); } catch (e) {} try { host.remove(); } catch (e) {} resolve(ids || []); };
    403       const p = new YT.Player(mount, {
    404         host: 'https://www.youtube-nocookie.com', playerVars: { playsinline: 1 },
    405         events: { onReady: () => {
    406           try { p.cuePlaylist({ listType: 'playlist', list: listId }); } catch (e) {}
    407           setTimeout(() => { let ids = []; try { ids = p.getPlaylist() || []; } catch (e) {} finish(ids); }, 2500);
    408         } },
    409       });
    410       setTimeout(() => finish([]), 9000);
    411     }));
    412   }
    413 
    414   // Render een audio-only YouTube-AFSPEELLIJST als album-kaart (zelfde stijl als
    415   // de eigen albums) met tracklijst; afspelen loopt via de site-audiospeler.
    416   async function buildYtAlbum(el, listId) {
    417     el.className = 'folio-embed pcms-ytalbum';  // geen losse card-chrome; .post-album stuurt de stijl
    418     const thumb = (id) => `https://i.ytimg.com/vi/${id}/mqdefault.jpg`;
    419     el.innerHTML = ''
    420       + '<div class="post-album">'
    421       +   '<div class="post-album-header">'
    422       +     '<button type="button" class="post-album-cover-btn" aria-label="Album afspelen">'
    423       +       '<img class="post-album-cover-img" src="" alt="">'
    424       +       '<span class="post-album-play-overlay" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 4l12 8-12 8z"/></svg></span>'
    425       +     '</button>'
    426       +     '<div class="post-album-info">'
    427       +       '<h3 class="post-album-title">Album laden…</h3>'
    428       +       '<p class="post-album-artist"></p>'
    429       +       '<p class="post-album-count">YouTube</p>'
    430       +     '</div>'
    431       +   '</div>'
    432       +   '<ol class="post-album-tracks"><li class="pcms-ytalbum-msg">Tracks laden…</li></ol>'
    433       + '</div>';
    434     const coverImg = el.querySelector('.post-album-cover-img');
    435     const titleEl = el.querySelector('.post-album-title');
    436     const countEl = el.querySelector('.post-album-count');
    437     const olEl = el.querySelector('.post-album-tracks');
    438     const coverBtn = el.querySelector('.post-album-cover-btn');
    439 
    440     // Albumnaam via de oEmbed-proxy (playlist-url).
    441     fetch('/audio/yt-title?url=' + encodeURIComponent('https://www.youtube.com/playlist?list=' + listId))
    442       .then((r) => (r.ok ? r.json() : null)).then((d) => { if (d && d.title) titleEl.textContent = d.title; }).catch(() => {});
    443 
    444     let ids = [];
    445     try { ids = await ytPlaylistIds(listId); } catch (e) { ids = []; }
    446     if (!ids.length) { olEl.innerHTML = '<li class="pcms-ytalbum-msg">Kon de afspeellijst niet laden.</li>'; return; }
    447     countEl.textContent = ids.length + ' track' + (ids.length === 1 ? '' : 's');
    448     if (coverImg) coverImg.src = thumb(ids[0]);
    449     // Speler vast aanmaken → eerste klik valt binnen de user-gesture (autoplay).
    450     if (window.pcmsAudioPlayer && window.pcmsAudioPlayer.prewarmYouTube) window.pcmsAudioPlayer.prewarmYouTube();
    451 
    452     olEl.innerHTML = ids.map((id, i) =>
    453       `<li class="post-album-track-compact" id="track-${id}" data-yt-id="${id}" data-yt-index="${i}">`
    454       + `<button type="button" class="pat-row" aria-label="Speel track ${i + 1}">`
    455       +   `<span class="pat-cover" style="background-image:url('${thumb(id)}')" aria-hidden="true"></span>`
    456       +   `<span class="pat-meta"><span class="pat-title">Track ${i + 1}</span><span class="pat-artist"></span></span>`
    457       +   `<span class="pat-duration pat-duration-empty">—:—</span>`
    458       + `</button></li>`
    459     ).join('');
    460 
    461     // Tracktitels lazy invullen (oEmbed-proxy per video).
    462     ids.forEach((id, i) => {
    463       fetch('/audio/yt-title?url=' + encodeURIComponent('https://www.youtube.com/watch?v=' + id))
    464         .then((r) => (r.ok ? r.json() : null))
    465         .then((d) => { if (d && d.title) { const tt = olEl.querySelector(`[data-yt-index="${i}"] .pat-title`); if (tt) tt.textContent = d.title; } })
    466         .catch(() => {});
    467     });
    468 
    469     function playAt(i) {
    470       if (!(window.pcmsAudioPlayer && window.pcmsAudioPlayer.playYouTubeList)) return;
    471       // Expandeer naar losse YT-tracks (betrouwbaarder dan loadPlaylist).
    472       const items = ids.map((id, j) => ({
    473         yt: id,
    474         title: (olEl.querySelector(`[data-yt-index="${j}"] .pat-title`) || {}).textContent || ('Track ' + (j + 1)),
    475         cover: thumb(id),
    476         postUrl: location.pathname + location.search,
    477       }));
    478       window.pcmsAudioPlayer.playYouTubeList(items, i);
    479     }
    480     coverBtn.addEventListener('click', () => playAt(0));
    481     olEl.addEventListener('click', (e) => { const li = e.target.closest('[data-yt-index]'); if (li) playAt(parseInt(li.dataset.ytIndex, 10) || 0); });
    482   }
    483 
    484350  // ============================================================
    485351  // 4. Adapters — normaliseren de 3 zeer verschillende API's naar 1 vorm.
     
    503369          events: {
    504370            onReady() {
    505               let d = 0; const meta = {};
    506               try { d = player.getDuration() || 0; } catch (e) {}
    507               try { const vd = player.getVideoData(); if (vd && vd.title) meta.title = vd.title; } catch (e) {}
    508               meta.duration = d;
    509               hooks.onReady(meta);
     371              let d = 0; try { d = player.getDuration() || 0; } catch (e) {}
     372              hooks.onReady({ duration: d });
    510373              resolve({
    511374                play() { try { player.playVideo(); } catch (e) {} },
  • src/routes/audio.js

    r8ab20b6 rfef781e  
    158158});
    159159
    160 // Same-origin proxy voor de YouTube-titel (oEmbed). YouTube's oEmbed stuurt geen
    161 // CORS-header, dus de browser kan 'm niet direct fetchen — wij halen 'm server-
    162 // side op (geen CORS) en cachen 't. Gebruikt door de audio-only YT-embed om de
    163 // echte videotitel te tonen i.p.v. "YouTube".
    164 const ytTitleCache = new Map();
    165 router.get('/yt-title', async (req, res) => {
    166   const url = String(req.query.url || '');
    167   if (!/^https:\/\/(www\.)?(youtube\.com|youtu\.be|youtube-nocookie\.com)\//i.test(url)) {
    168     return res.status(400).json({ error: 'bad url' });
    169   }
    170   if (ytTitleCache.has(url)) return res.json({ title: ytTitleCache.get(url) });
    171   try {
    172     const ac = new AbortController();
    173     const timer = setTimeout(() => ac.abort(), 6000);
    174     const r = await fetch('https://www.youtube.com/oembed?format=json&url=' + encodeURIComponent(url), { signal: ac.signal });
    175     clearTimeout(timer);
    176     if (!r.ok) { ytTitleCache.set(url, ''); return res.json({ title: '' }); }
    177     const d = await r.json();
    178     const title = (d && d.title) ? String(d.title).slice(0, 200) : '';
    179     ytTitleCache.set(url, title);
    180     res.json({ title });
    181   } catch (e) {
    182     res.status(502).json({ error: 'fetch failed' });
    183   }
    184 });
    185 
    186160export default router;
  • src/services/AudioEmbedService.js

    r8ab20b6 rfef781e  
    6767      case 'youtube':
    6868        return this.embedPlaceholder('youtube', config.id, 'video',
    69           config.url || `https://youtu.be/${config.id}`, config.mode);
     69          config.url || `https://youtu.be/${config.id}`);
    7070      case 'soundcloud':
    7171        return this.embedPlaceholder('soundcloud', config.url, 'track', config.url);
     
    9191   * ALLE waarden via escape() — post.content_html wordt ongeescaped uitgevoerd.
    9292   */
    93   static embedPlaceholder(provider, ref, type, url, mode) {
     93  static embedPlaceholder(provider, ref, type, url) {
    9494    const attrs = [
    9595      `data-embed-provider="${this.escape(provider)}"`,
    9696      `data-embed-ref="${this.escape(ref)}"`,
    9797      type ? `data-embed-type="${this.escape(type)}"` : '',
    98       mode ? `data-embed-mode="${this.escape(mode)}"` : '',
    9998      `data-embed-url="${this.escape(url)}"`,
    10099    ].filter(Boolean).join(' ');
     
    228227  static embedMediaShortcodes(html) {
    229228    if (!html) return html;
    230     return html.replace(/\[\[embed:([^\]]+)\]\]/gi, (match, rawSpec) => {
    231       // Optionele modus-prefix: [[embed:audio:URL]] = alleen-audio speler,
    232       // [[embed:video:URL]] / [[embed:URL]] = normaal (video waar van toepassing).
    233       let spec = rawSpec.trim();
    234       let mode = null;
    235       const mm = spec.match(/^(audio|video):(.+)$/i);
    236       if (mm) { mode = mm[1].toLowerCase(); spec = mm[2].trim(); }
    237       const url = spec.replace(/&amp;/g, '&');
     229    return html.replace(/\[\[embed:([^\]]+)\]\]/gi, (match, rawUrl) => {
     230      const url = rawUrl.trim().replace(/&amp;/g, '&');
    238231      const detected = this.detectProvider(url);
    239232      if (!detected) {
    240233        return `<div class="post-embed-missing"><em>Embed: niet-ondersteunde of ongeldige URL.</em></div>`;
    241234      }
    242       return this.generateIframe(detected.provider, { ...detected, mode }) || match;
     235      return this.generateIframe(detected.provider, detected) || match;
    243236    });
    244237  }
  • src/views/pages/post-edit.ejs

    r8ab20b6 rfef781e  
    14631463      const url = raw.trim();
    14641464      if (!/^https?:\/\//i.test(url)) { alert('Geef een volledige URL (https://…).'); return; }
    1465       // YouTube: keuze audio-only (alleen geluid) of video.
    1466       let val = url;
    1467       if (/(youtube\.com|youtu\.be)/i.test(url)) {
    1468         const audioOnly = window.confirm('YouTube — alleen audio afspelen?\n\nOK = audio-only speler (geen video)\nAnnuleren = video');
    1469         if (audioOnly) val = 'audio:' + url;
    1470       }
    1471       insertChip('embed', val);
     1465      insertChip('embed', url);
    14721466    });
    14731467  }
  • src/views/shell.ejs

    r8ab20b6 rfef781e  
    170170<link rel="stylesheet" href="/assets/css/audio.css?v=6">
    171171<!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) in huisstijl. -->
    172 <link rel="stylesheet" href="/assets/css/embed.css?v=7">
     172<link rel="stylesheet" href="/assets/css/embed.css?v=8">
    173173
    174174<%- include('partials/shared-styles') %>
     
    300300     ?v=N — cache-buster: bump bij elke audio-player.js wijziging zodat
    301301     Cloudflare (max-age=1y) niet de oude versie blijft serveren. -->
    302 <script src="/assets/js/audio-player.js?v=27"></script>
     302<script src="/assets/js/audio-player.js?v=28"></script>
    303303<!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) via de echte
    304304     player-API's + gedeelde mutual-exclusion registry met de site-speler. -->
    305 <script src="/assets/js/embed-player.js?v=15" defer></script>
     305<script src="/assets/js/embed-player.js?v=16" defer></script>
    306306<% if (site && site.enable_audio_player && audioTracks && audioTracks.length > 0) { %>
    307307  <script>window.PCMS_SITE_TRACKS = <%- JSON.stringify(audioTracks) %>;</script>
Note: See TracChangeset for help on using the changeset viewer.