Changeset 5300652 in Klonkt


Ignore:
Timestamp:
08/13/2026 01:08:32 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
6401681
Parents:
a117862
git-author:
Robin <roboburr@…> (08/13/2026 12:10:40 PM)
git-committer:
Robin <roboburr@…> (08/13/2026 01:08:32 PM)
Message:

Stap 2: "Ben jij dit?" in Beheer -> Audio (shaer-mbz)

Een paneel bovenaan het audioscherm: zoek op je artiestennaam (voorgevuld met
wat er al in de site staat), kies je eigen pagina, klaar. Drie talen.

DE TOELICHTING IS HET HELE PUNT. Elke kandidaat toont MusicBrainz'
disambiguation naast soort, land en jaren -- er zijn drie bands die Nirvana
heten, en met alleen een naam kiest iemand de verkeerde. Wij kiezen NIET voor
de artiest, ook niet als er precies een treffer is: een verkeerd geraden MBID
zet jouw naam onder andermans werk.

ONTKOPPELEN KAN. Om dezelfde reden: zo'n vergissing hoort met een klik terug
te draaien te zijn, niet met een databasebewerking.

Alleen een echte MBID komt de kolom in; de route weigert een URL of een handle.
De naam bewaren we ernaast zodat het scherm kan tonen WAT er hangt zonder
ervoor te netwerken -- en zodat een verkeerde koppeling opvalt.

TWEE DINGEN UIT DE HUISREGELS. De kandidaten gaan met textContent het scherm op
en niet als HTML: dat is tekst uit een vreemd register. En het blok hangt in
run() en niet op moduleniveau -- deze modules krijgen bij elke paginawissel
opnieuw init(), en een blok dat maar een keer per sessie draait is precies wat
shaer-5s1 opleverde. Ik had hem er eerst buiten gezet.

Als iemand niet in MusicBrainz staat zegt het scherm dat hij zich daar kan
aanmelden -- en erbij dat dat alleen dáár kan, niet vanuit Klonkt. Dat is geen
bescheidenheid maar hun API: artiesten zijn er niet mee aan te maken.

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

Files:
5 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/mod/admin-audio.js

    ra117862 r5300652  
    452452  });
    453453})();
     454
     455  // MusicBrainz-paneel (shaer-mbz). Binnen run(), niet op moduleniveau:
     456  // deze modules krijgen bij elke paginawissel opnieuw init(), en een blok dat
     457  // maar een keer per sessie draait is precies wat shaer-5s1 opleverde.
     458  wireMusicBrainz();
    454459}
     460
     461// ── MusicBrainz: ben jij dit? (shaer-mbz, stap 2) ─────────────────
     462//
     463// De kandidaten komen van de server, want MusicBrainz eist een verzoek per
     464// seconde per APPLICATIE en een User-Agent met contact -- allebei niet vanuit
     465// een browser af te dwingen.
     466//
     467// WIJ KIEZEN NIET. Ook niet als er precies een treffer is: een verkeerd
     468// geraden MBID zet jouw naam onder andermans werk. De knop staat er, de klik
     469// is van de artiest.
     470function wireMusicBrainz() {
     471  const knop = document.getElementById('mb-zoek-btn');
     472  const veld = document.getElementById('mb-q');
     473  const uit = document.getElementById('mb-uit');
     474  if (!knop || !veld || !uit || knop.__wired) return;
     475  knop.__wired = true;
     476
     477  const el = (tag, cls, tekst) => {
     478    const e = document.createElement(tag);
     479    if (cls) e.className = cls;
     480    if (tekst != null) e.textContent = tekst;   // textContent: nooit HTML uit een vreemd register
     481    return e;
     482  };
     483
     484  async function zoek() {
     485    const q = (veld.value || '').trim();
     486    if (!q) return;
     487    uit.hidden = false;
     488    uit.replaceChildren(el('p', 'ax-hint', T.aaud_mb_busy || 'Zoeken…'));
     489    knop.disabled = true;
     490    try {
     491      const r = await fetch(`api/musicbrainz?q=${encodeURIComponent(q)}`, { credentials: 'same-origin' });
     492      const j = await r.json();
     493      toon((j && j.kandidaten) || []);
     494    } catch {
     495      uit.replaceChildren(el('p', 'ax-hint', T.aaud_mb_fail || 'MusicBrainz is even niet bereikbaar.'));
     496    } finally {
     497      knop.disabled = false;
     498    }
     499  }
     500
     501  function toon(kandidaten) {
     502    if (!kandidaten.length) {
     503      uit.replaceChildren(el('p', 'ax-hint', T.aaud_mb_none || 'Niets gevonden.'));
     504      return;
     505    }
     506    const lijst = el('ul', 'mb-lijst');
     507    for (const k of kandidaten) {
     508      const li = el('li', 'mb-kandidaat');
     509      li.appendChild(el('strong', null, k.naam));
     510      // De toelichting is het hele punt: er zijn drie bands die Nirvana heten,
     511      // en zonder dit veld kiest iemand de verkeerde.
     512      const bij = [k.toelichting, k.soort, k.land, k.jaren].filter(Boolean).join(' · ');
     513      if (bij) li.appendChild(el('small', 'mb-bij', bij));
     514      const open = el('a', 'mb-open', T.aaud_mb_open || 'Bekijk op MusicBrainz');
     515      open.href = k.url; open.target = '_blank'; open.rel = 'noopener';
     516      li.appendChild(open);
     517      li.appendChild(kiesForm(k));
     518      lijst.appendChild(li);
     519    }
     520    uit.replaceChildren(lijst);
     521  }
     522
     523  function kiesForm(k) {
     524    const f = document.createElement('form');
     525    f.method = 'POST';
     526    f.action = 'musicbrainz/link';
     527    for (const [naam, waarde] of [['_csrf', csrf()], ['mbid', k.mbid], ['naam', k.naam]]) {
     528      const i = document.createElement('input');
     529      i.type = 'hidden'; i.name = naam; i.value = waarde || '';
     530      f.appendChild(i);
     531    }
     532    const b = el('button', 'ax-btn', T.aaud_mb_pick || 'Dit ben ik');
     533    b.type = 'submit';
     534    f.appendChild(b);
     535    return f;
     536  }
     537
     538  // Het token staat al in elk formulier op deze pagina; er is er geen apart voor.
     539  const csrf = () => (document.querySelector('input[name="_csrf"]') || {}).value || '';
     540
     541  knop.addEventListener('click', zoek);
     542  veld.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); zoek(); } });
     543}
  • src/routes/admin-audio.js

    ra117862 r5300652  
    414414});
    415415
     416/**
     417 * De keuze vastleggen. Alleen een echte MBID komt de kolom in: de naam die we
     418 * ernaast bewaren is voor het scherm, de MBID is het enige dat naar buiten gaat.
     419 */
     420router.post('/musicbrainz/link', requireGod, (req, res) => {
     421  const site = res.locals.site;
     422  if (!site) return res.status(404).end();
     423  const mbid = String(req.body.mbid || '').trim().toLowerCase();
     424  const naam = String(req.body.naam || '').trim().slice(0, 200);
     425  const terug = (res.locals.siteUrlBase || '') + '/admin/audio';
     426  if (!MusicBrainz.isMbid(mbid)) return res.redirect(`${terug}?error=` + encodeURIComponent('Geen geldige MusicBrainz-id.'));
     427  db.prepare('UPDATE sites SET mb_artist_id = ?, mb_artist_name = ? WHERE id = ?').run(mbid, naam || null, site.id);
     428  res.redirect(`${terug}?success=` + encodeURIComponent('Gekoppeld aan MusicBrainz.'));
     429});
     430
     431/** Terugdraaien. Een verkeerde koppeling zet jouw naam onder andermans werk. */
     432router.post('/musicbrainz/unlink', requireGod, (req, res) => {
     433  const site = res.locals.site;
     434  if (!site) return res.status(404).end();
     435  db.prepare('UPDATE sites SET mb_artist_id = NULL, mb_artist_name = NULL WHERE id = ?').run(site.id);
     436  res.redirect((res.locals.siteUrlBase || '') + '/admin/audio?success=' + encodeURIComponent('Ontkoppeld.'));
     437});
     438
    416439router.get('/api/albums', requireGod, (req, res) => {
    417440  const site = res.locals.site;
  • src/services/i18n.js

    ra117862 r5300652  
    360360    'aaud.fedi_off': 'Niet op de fediverse gedeeld (alleen webspeler, bestand verborgen) — klik om te delen',
    361361    'aaud.embed_player': 'Embedbare speler',
     362    'aaud.mb_title': 'Ben jij dit in MusicBrainz?',
     363    'aaud.mb_hint': 'MusicBrainz is het open register waar muziekdiensten je aan herkennen. Zoek jezelf op en kies je eigen pagina — dan weten anderen dat jouw muziek van jou is.',
     364    'aaud.mb_search': 'Zoeken',
     365    'aaud.mb_none': 'Niets gevonden. Sta je er nog niet in? Dan kun je jezelf aanmelden op musicbrainz.org — dat kan alleen daar, niet vanuit Klonkt.',
     366    'aaud.mb_pick': 'Dit ben ik',
     367    'aaud.mb_linked': 'Gekoppeld aan',
     368    'aaud.mb_unlink': 'Ontkoppelen',
     369    'aaud.mb_open': 'Bekijk op MusicBrainz',
     370    'aaud.mb_busy': 'Zoeken…',
     371    'aaud.mb_fail': 'MusicBrainz is even niet bereikbaar.',
    362372    'aaud.embed_hint': 'Plak deze code op je eigen website/blog om je muziek met deze speler in te sluiten:',
    363373    'aaud.preview_player': 'Speler-voorbeeld openen',
     
    13071317    'aaud.fedi_off': 'Not shared on the fediverse (web player only, file hidden) — click to share',
    13081318    'aaud.embed_player': 'Embeddable player',
     1319    'aaud.mb_title': 'Is this you on MusicBrainz?',
     1320    'aaud.mb_hint': 'MusicBrainz is the open register music services recognise you by. Look yourself up and pick your own page — then others know your music is yours.',
     1321    'aaud.mb_search': 'Search',
     1322    'aaud.mb_none': 'Nothing found. Not in there yet? You can add yourself on musicbrainz.org — that can only be done there, not from Klonkt.',
     1323    'aaud.mb_pick': 'This is me',
     1324    'aaud.mb_linked': 'Linked to',
     1325    'aaud.mb_unlink': 'Unlink',
     1326    'aaud.mb_open': 'View on MusicBrainz',
     1327    'aaud.mb_busy': 'Searching…',
     1328    'aaud.mb_fail': 'MusicBrainz is unreachable right now.',
    13091329    'aaud.embed_hint': 'Paste this code on your own website/blog to embed your music with this player:',
    13101330    'aaud.preview_player': 'Open player preview',
     
    22532273    'aaud.fedi_off': 'Nicht im Fediverse geteilt (nur Web-Player, Datei verborgen) — zum Teilen klicken',
    22542274    'aaud.embed_player': 'Einbettbarer Player',
     2275    'aaud.mb_title': 'Bist das du bei MusicBrainz?',
     2276    'aaud.mb_hint': 'MusicBrainz ist das offene Register, an dem Musikdienste dich erkennen. Suche dich und wähle deine eigene Seite — dann wissen andere, dass deine Musik von dir ist.',
     2277    'aaud.mb_search': 'Suchen',
     2278    'aaud.mb_none': 'Nichts gefunden. Noch nicht dabei? Du kannst dich auf musicbrainz.org eintragen — das geht nur dort, nicht aus Klonkt heraus.',
     2279    'aaud.mb_pick': 'Das bin ich',
     2280    'aaud.mb_linked': 'Verknüpft mit',
     2281    'aaud.mb_unlink': 'Trennen',
     2282    'aaud.mb_open': 'Bei MusicBrainz ansehen',
     2283    'aaud.mb_busy': 'Suche läuft…',
     2284    'aaud.mb_fail': 'MusicBrainz ist gerade nicht erreichbar.',
    22552285    'aaud.embed_hint': 'Füge diesen Code auf deiner eigenen Website/deinem Blog ein, um deine Musik mit diesem Player einzubetten:',
    22562286    'aaud.preview_player': 'Player-Vorschau öffnen',
  • src/views/pages/admin-audio.ejs

    ra117862 r5300652  
    1212  <% if (success) { %><div class="ax-flash ax-flash-ok"><%= success %></div><% } %>
    1313  <% if (error) { %><div class="ax-flash ax-flash-err"><%= error %></div><% } %>
     14
     15  <%# ── MUSICBRAINZ ─────────────────────────────────────────────
     16      Wie ben jij, buiten Klonkt? Een MBID is geen vocabulaire maar een
     17      register: eraan koppelen is als je ISBN noemen. Andere muziekdiensten
     18      -- Funkwhale voorop -- herkennen je eraan zonder dat wij hun model
     19      hoeven over te nemen (shaer-mbz). %>
     20  <section class="ax-card" id="mb-card">
     21    <div class="ax-card-title"><%= t('aaud.mb_title') %></div>
     22    <p class="ax-hint"><%= t('aaud.mb_hint') %></p>
     23
     24    <% if (site.mb_artist_id) { %>
     25      <p class="mb-linked">
     26        <%= t('aaud.mb_linked') %>
     27        <strong><%= site.mb_artist_name || site.mb_artist_id %></strong>
     28        <a href="https://musicbrainz.org/artist/<%= site.mb_artist_id %>"
     29           target="_blank" rel="noopener"><%= t('aaud.mb_open') %></a>
     30      </p>
     31      <%# Ontkoppelen moet kunnen: een verkeerde koppeling zet jouw naam onder
     32          andermans werk, en dat hoort met een klik terug te draaien zijn. %>
     33      <form method="POST" action="<%= (typeof siteUrlBase !== 'undefined' ? siteUrlBase : '') %>/admin/audio/musicbrainz/unlink">
     34        <input type="hidden" name="_csrf" value="<%= csrfToken %>">
     35        <button type="submit" class="ax-btn ax-btn-quiet"><%= t('aaud.mb_unlink') %></button>
     36      </form>
     37    <% } %>
     38
     39    <div class="mb-zoek">
     40      <input type="text" id="mb-q" value="<%= site.author || site.title || '' %>"
     41             placeholder="<%= t('aaud.artist') %>" autocomplete="off">
     42      <button type="button" class="ax-btn" id="mb-zoek-btn"><%= t('aaud.mb_search') %></button>
     43    </div>
     44    <div id="mb-uit" class="mb-uit" hidden></div>
     45  </section>
    1446
    1547  <%# ── UPLOAD ──────────────────────────────────────────────── %>
  • test/musicbrainz.test.js

    ra117862 r5300652  
    9797
    9898test.after(() => { globalThis.fetch = echt; });
     99
     100// ── Stap 2: de keuze vastleggen ───────────────────────────────────────────
     101
     102test('alleen een echte MBID komt de kolom in', async () => {
     103  // De route weigert alles wat geen UUID is. Zonder die zeef sluipt er een
     104  // hele URL of een handle in het veld dat straks naar buiten gaat.
     105  const db = dbMod.default;
     106  db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
     107    .run('u1', 'u1', 'u1@t', 'x', 'god');
     108  db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'band', 'Band', 'u1');
     109  for (const rommel of ['https://musicbrainz.org/artist/8be31978-1884-4773-beae-f73df35b92aa', 'nirvana', '']) {
     110    assert.equal(MB.isMbid(rommel), false, `${rommel} hoort geweigerd te worden`);
     111  }
     112  db.prepare('UPDATE sites SET mb_artist_id = ?, mb_artist_name = ? WHERE id = ?')
     113    .run('8be31978-1884-4773-beae-f73df35b92aa', 'Nirvana', 's1');
     114  const s = db.prepare("SELECT mb_artist_id, mb_artist_name FROM sites WHERE id = 's1'").get();
     115  assert.equal(s.mb_artist_id, '8be31978-1884-4773-beae-f73df35b92aa');
     116  assert.equal(s.mb_artist_name, 'Nirvana', 'de naam ernaast, zodat het scherm kan tonen WAT er hangt');
     117});
     118
     119test('ontkoppelen maakt beide velden leeg', () => {
     120  const db = dbMod.default;
     121  db.prepare('UPDATE sites SET mb_artist_id = NULL, mb_artist_name = NULL WHERE id = ?').run('s1');
     122  const s = db.prepare("SELECT mb_artist_id, mb_artist_name FROM sites WHERE id = 's1'").get();
     123  assert.equal(s.mb_artist_id, null);
     124  assert.equal(s.mb_artist_name, null, 'anders blijft er een naam staan zonder koppeling');
     125});
Note: See TracChangeset for help on using the changeset viewer.