Changeset d727e92 in Klonkt


Ignore:
Timestamp:
06/20/2026 05:37:42 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
a66f691
Parents:
183875b
Message:

feat(audio): tracks without audio (link-only) — list with open-in links, no upload

You can now add a "Track without audio" in Admin → Audio (button) — only
title/artist + the Spotify/YouTube/SoundCloud links, no file. Such tracks
appear in albums & playlists (and standalone track embeds) in the list
with the open-in icons but WITHOUT a play button, and stay outside the
playback queue (which matches on URL). Lookups (posts album/single +
PlaylistService) no longer filter out fileless tracks. New route POST
/admin/audio/create-link. buster audio.css?v=9.

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

Location:
src
Files:
7 edited

Legend:

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

    r183875b rd727e92  
    105105.pat-link--youtube    { color: #FF0000; }
    106106.pat-link--soundcloud { color: #FF5500; }
     107
     108/* Link-only tracks (geen audiobestand): plek van de afspeelknop = stil muziek-icoon. */
     109.pat-noplay {
     110  display: inline-flex; align-items: center; justify-content: center;
     111  flex: 0 0 38px; width: 38px; align-self: center;
     112  color: var(--ink-faint, #a8a29e);
     113}
     114.pat-noplay svg { width: 18px; height: 18px; }
     115.pat-row.pat-static { cursor: default; }
    107116
    108117/* ============================================================
  • src/routes/admin-audio.js

    r183875b rd727e92  
    394394
    395395/** GET /admin/audio/api/:id — single track with all metadata */
     396// Maak een track ZONDER audiobestand (alleen titel + open-in links). Verschijnt
     397// in albums/playlists in de lijst, met open-in-iconen maar zonder afspeelknop.
     398router.post('/create-link', requireGod, express.json(), (req, res) => {
     399  const site = res.locals.site;
     400  if (!site) return res.status(404).json({ error: 'Site required' });
     401  const trackId = uuid();
     402  const title = ((req.body && req.body.title) || 'Nieuwe track').toString().trim().slice(0, 200) || 'Nieuwe track';
     403  try {
     404    db.prepare(`
     405      INSERT INTO audio_tracks (id, site_id, title, media_id, position)
     406      VALUES (?, ?, ?, NULL, COALESCE((SELECT MAX(position) + 1 FROM audio_tracks WHERE site_id = ?), 0))
     407    `).run(trackId, site.id, title, site.id);
     408  } catch (e) {
     409    return res.status(500).json({ error: e.message });
     410  }
     411  res.json({ ok: true, id: trackId });
     412});
     413
    396414router.get('/api/:id', requireGod, (req, res) => {
    397415  const site = res.locals.site;
  • src/routes/posts.js

    r183875b rd727e92  
    537537      html = AudioEmbedService.embedTrackShortcodes(html, (id) => {
    538538        const r = byId.get(id);
    539         if (!r || !r.filename) return null;
     539        if (!r) return null;
    540540        return {
    541541          id: r.id,
     
    548548          link_youtube: r.link_youtube || '',
    549549          link_soundcloud: r.link_soundcloud || '',
    550           url: audioUrl(r.filename),
     550          url: r.filename ? audioUrl(r.filename) : '',  // '' = link-only track
    551551        };
    552552      });
     
    566566      const byAlbum = new Map();
    567567      for (const r of albumRows) {
    568         if (!r.filename) continue;
     568        // Link-only tracks (geen bestand) blijven in het album-overzicht (url '').
    569569        if (!byAlbum.has(r.album)) byAlbum.set(r.album, []);
    570570        byAlbum.get(r.album).push({
    571571          id: r.id,
    572           url: audioUrl(r.filename),
     572          url: r.filename ? audioUrl(r.filename) : '',
    573573          title: r.title || 'Untitled',
    574574          artist: r.artist || '',
  • src/services/AudioEmbedService.js

    r183875b rd727e92  
    270270    return html.replace(/\[\[track:([A-Za-z0-9_-]+)\]\]/g, (match, id) => {
    271271      const t = trackLookup(id);
    272       if (!t || !t.url) return match;
     272      if (!t) return match;
     273      const titleH0 = this.escape(t.title || 'Untitled');
     274      const artistH0 = this.escape(t.artist || '');
     275      const creditBits0 = [this.escape(t.credit || ''), this.escape(t.license || '')].filter(Boolean).join(' · ');
     276      // Link-only track (geen audiobestand): geen afspeelknop, wel info + open-in.
     277      if (!t.url) {
     278        return `<div class="post-audio-track post-audio-track--static" id="track-${id}">
     279  <span class="pat-noplay" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg></span>
     280  <div class="pat-info">
     281    <div class="pat-title">${titleH0}</div>
     282    ${artistH0 ? `<div class="pat-artist">${artistH0}</div>` : ''}
     283    ${creditBits0 ? `<div class="pat-credit">${creditBits0}</div>` : ''}
     284  </div>
     285  ${this.openInLinks(t)}
     286</div>`;
     287      }
    273288      const trackJson = JSON.stringify({
    274289        id,
     
    318333      // Stable DOM id for this rendering — used as data-pcms-album-id on tracks
    319334      const albumDomId = 'album-' + Math.random().toString(36).slice(2, 10);
    320       const albumJson = JSON.stringify(album.tracks)
     335      // Alleen afspeelbare tracks (met url) in de queue; link-only tracks staan
     336      // wel in de lijst maar niet in de afspeel-JSON.
     337      const albumJson = JSON.stringify(album.tracks.filter((t) => t.url))
    321338        .replace(/&/g, '&amp;').replace(/'/g, '&#39;').replace(/</g, '&lt;');
    322339      const titleH = this.escape(album.title || name);
     
    327344        const tTitle = this.escape(t.title || ('Track ' + (i + 1)));
    328345        const tArtist = this.escape(t.artist || '');
     346        // Link-only track: geen afspeelknop, wel nummer + info + open-in.
     347        if (!t.url) {
     348          return `    <li class="post-audio-track post-audio-track--static"${t.id ? ` id="track-${t.id}"` : ''}>
     349      <span class="pat-track-num">${i + 1}.</span>
     350      <div class="pat-info">
     351        <div class="pat-title">${tTitle}</div>
     352        ${tArtist && tArtist !== artistH ? `<div class="pat-artist">${tArtist}</div>` : ''}
     353      </div>
     354      ${this.openInLinks(t)}
     355    </li>`;
     356        }
    329357        const tUrl = this.escape(t.url);
    330358        return `    <li class="post-audio-track"${t.id ? ` id="track-${t.id}" data-pcms-track-id="${t.id}"` : ''} data-pcms-track-url="${tUrl}" data-pcms-album-id="${albumDomId}">
     
    404432      // Audio-player.js reads data-pcms-album for queue. Same shape as
    405433      // embedAlbumShortcodes — keep both in sync.
    406       const tracksData = pl.tracks.map(t => ({
     434      // Alleen afspeelbare tracks in de queue; link-only tracks staan wel in de
     435      // lijst maar niet in de afspeel-JSON.
     436      const tracksData = pl.tracks.filter(t => t.url).map(t => ({
    407437        id:     t.id,
    408438        url:    t.url,
     
    426456      }
    427457      const metaLine = this.escape(metaParts.join(' · '));
    428       const firstUrl = this.escape(pl.tracks[0].url);
     458      const firstUrl = this.escape((pl.tracks.find(t => t.url) || {}).url || '');
    429459
    430460      // Track items — playlist-kind shows per-track cover thumbs, album-kind shows numbers
     
    445475          : `<span class="pat-num">${i + 1}</span>`;
    446476
     477        // Link-only track: geen klikbare afspeel-rij (statische div), wel open-in.
     478        if (!t.url) {
     479          return `    <li class="post-album-track-compact post-album-track-compact--static"${t.id ? ` id="track-${t.id}"` : ''}>
     480      <div class="pat-row pat-static">
     481        ${leader}
     482        <span class="pat-meta">
     483          <span class="pat-title">${tTitleH}</span>
     484          ${showArtist ? `<span class="pat-artist">${tArtistH}</span>` : ''}
     485        </span>
     486        ${durHtml}
     487      </div>
     488      ${this.openInLinks(t)}
     489    </li>`;
     490        }
    447491        const trackBase = String(t.url).split('?')[0];
    448492        return `    <li class="post-album-track-compact"${t.id ? ` id="track-${t.id}" data-pcms-track-id="${t.id}"` : ''}>
  • src/services/PlaylistService.js

    r183875b rd727e92  
    104104
    105105    const mappedTracks = tracks
    106       .filter(t => t.filename)  // skip orphaned references
     106      // Link-only tracks (geen media-bestand) blijven in de lijst staan met url ''.
    107107      .map(t => ({
    108108        id: t.id,
     
    114114        link_youtube: t.link_youtube || '',
    115115        link_soundcloud: t.link_soundcloud || '',
    116         url: urlFor ? urlFor(t.filename) : null,
     116        url: (t.filename && urlFor) ? urlFor(t.filename) : '',
    117117      }));
    118118    // Geen eigen cover? Val terug op de eerste track-cover, zodat de kaart niet leeg is.
  • src/views/pages/admin-audio.ejs

    r183875b rd727e92  
    6262  <%# ── TRACK LIST ─────────────────────────────────────────── %>
    6363  <section class="ax-card">
    64     <div class="ax-card-title">
     64    <div class="ax-card-title" style="display:flex;align-items:center;gap:.5rem">
    6565      Tracks <span class="ax-count"><%= tracks.length %></span>
     66      <button type="button" class="ax-btn ax-btn-secondary" id="add-link-track-btn"
     67              style="margin-left:auto;font-size:.85rem" title="Een track zonder audiobestand — alleen titel + open-in links">
     68        + Track zonder audio
     69      </button>
    6670    </div>
    6771
     
    889893    });
    890894  });
     895
     896  // ── "+ Track zonder audio": maak een link-only stub + open de editor ──
     897  const addLinkBtn = document.getElementById('add-link-track-btn');
     898  if (addLinkBtn) {
     899    addLinkBtn.addEventListener('click', async () => {
     900      addLinkBtn.disabled = true;
     901      try {
     902        const r = await fetch('/admin/audio/create-link', {
     903          method: 'POST', credentials: 'same-origin',
     904          headers: { 'Content-Type': 'application/json' },
     905          body: JSON.stringify({ title: 'Nieuwe track' }),
     906        });
     907        const j = await r.json();
     908        if (!r.ok || !j.ok) throw new Error(j.error || 'Aanmaken mislukt');
     909        if (typeof window.openTrackEditor !== 'function') { location.reload(); return; }
     910        window.openTrackEditor({ id: j.id, onSaved: () => location.reload() });
     911      } catch (err) {
     912        alert('Track aanmaken mislukt: ' + err.message);
     913      } finally {
     914        addLinkBtn.disabled = false;
     915      }
     916    });
     917  }
    891918
    892919  // ── Wire all "Edit" buttons to the track-editor modal ─────────
  • src/views/shell.ejs

    r183875b rd727e92  
    168168     anywhere (admin previews, post embeds, etc). The player itself is
    169169     a singleton — see the script tag near </body>. -->
    170 <link rel="stylesheet" href="/assets/css/audio.css?v=8">
     170<link rel="stylesheet" href="/assets/css/audio.css?v=9">
    171171<!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) in huisstijl. -->
    172172<link rel="stylesheet" href="/assets/css/embed.css?v=8">
Note: See TracChangeset for help on using the changeset viewer.