Ignore:
Timestamp:
08/16/2026 03:06:18 PM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
a5caa73
Parents:
a5515b4 (diff), 9026de5 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Bibliotheek en album-federatie terug op main (shaer-0nh, shaer-756s)

Achttien commits, in drie brokken.

DE BIBLIOTHEEK. Een Library-skelet met een eigen volgerssoort: wie de
bibliotheek volgt krijgt de muziek en met opzet niet de gewone posts, en
die staan daarom in een eigen tabel -- een vlag die iemand vergeet te
filteren bezorgt stilletjes de Krant aan mensen die er niet om vroegen.
Daarna wat er nodig bleek om werkelijk gelezen te worden: audience
(zonder dat gaf hun create() een KeyError en dus een 500 op onze URL),
de ongeordende collectievorm, en paginering op elke collectie die first
belooft -- die laatste vond meteen dat featured hetzelfde gat had.

HET VOCABULAIRE. fw:track in de kleine vorm die Emissary ook stuurt,
artist_credit met de site-actor als Artist (mogelijk geworden door de
MusicBrainz-koppeling), en het album als ingesloten fw:Album met
released, musicbrainzId en een eigen artist_credit. Daarvoor kwamen er
twee kolommen op playlists, alleen bij kind='album' en afgedwongen in
PlaylistService -- niet in het scherm, want de API ligt open.

HET BEHEER. De vijf mediapagina's stonden op drie verschillende
ontwerpen; nu op een gedeelde ax-romp, met de primitieven op een plek in
plaats van in acht kopieen. De playlist-modal ging alsnog door i18n: die
teksten stonden hard in het script, wat niet opviel omdat het toevallig
de brontaal was.

WAT ER NIET IN ZIT: bitrate en size top-level als fw:. Dat is bewust
(shaer-3f8a). Hun eigen outbox zet ze op de Link, net als wij, en het
wordt een gesprek in plaats van een aanpassing -- briefing in
~/bewaard/funkwhale-briefing-en.md.

Gereedschap dat meekomt: scripts/funkwhale-gat.mjs meet het gat naar hun
ingest, admin-css.mjs en admin-css-gelijk.mjs ontleden en vergelijken de
beheer-CSS, render-admin-views.mjs rendert de views gevuld en leeg. Die
laatste drie omdat de testsuite geen enkele view aanraakt.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/mod/playlist-editor.js

    ra5515b4 r749605a  
    66
    77import { pageData } from './lib.js';
     8
     9/**
     10 * De teksten van deze modal, uit het gegevensblok van partials/playlist-editor.ejs.
     11 *
     12 * EIGEN BLOK en niet pageData(): die pakt met querySelector er EEN, en deze
     13 * modal hangt onder pagina's die er zelf al een hebben. Een tweede blok zou
     14 * daar genegeerd worden.
     15 *
     16 * De terugval is bewust de sleutelnaam en niet de Nederlandse tekst: ontbreekt
     17 * er iets, dan zie je DAT er iets ontbreekt in plaats van een pagina die er
     18 * half vertaald uitziet en waarvan niemand merkt welke helft.
     19 */
     20let _T = null;
     21function T(sleutel) {
     22  if (_T === null) {
     23    try {
     24      const el = document.querySelector('script[type="application/json"][data-playlist-editor-i18n]');
     25      _T = el ? (JSON.parse(el.textContent) || {}) : {};
     26    } catch (e) { _T = {}; }
     27  }
     28  return _T[sleutel] != null ? _T[sleutel] : sleutel;
     29}
    830
    931(function() {
     
    5476      if (Array.isArray(j.tracks)) tracks = j.tracks;
    5577    } catch (e) {
    56       alert('Tracks ophalen mislukt');
     78      alert(T('e_tracks'));
    5779      return;
    5880    }
    5981    if (!tracks.length) {
    60       alert('Geen audio-tracks beschikbaar. Upload eerst tracks via Admin → Audio.');
     82      alert(T('e_geen_tracks'));
    6183      return;
    6284    }
    6385
    6486    // Existing playlist data when editing
    65     let initial = { title: '', artist: '', year: '', cover: '', kind: 'album', track_ids: [] };
     87    let initial = { title: '', artist: '', year: '', cover: '', kind: 'album', release_date: '', mb_release_id: '', track_ids: [] };
    6688    if (isEdit && opts.id) {
    6789      try {
     
    7597    backdrop.className = 'pl-modal-backdrop';
    7698    backdrop.innerHTML = `
    77       <div class="pl-modal" role="dialog" aria-label="Playlist editor">
     99      <div class="pl-modal" role="dialog" aria-label="${T('dialoog')}">
    78100        <div class="pl-modal-header">
    79           <h3>${isEdit ? '✎ Playlist bewerken' : '+ Nieuwe playlist'}</h3>
    80           <button type="button" class="pl-modal-close" aria-label="Sluiten">×</button>
     101          <h3>${isEdit ? '✎ ' + T('t_edit') : '+ ' + T('t_new')}</h3>
     102          <button type="button" class="pl-modal-close" aria-label="${T('sluiten')}">×</button>
    81103        </div>
    82104        <div class="pl-modal-body">
     
    85107              <div class="pl-meta-grid">
    86108                <label class="pl-field pl-field-full">
    87                   <span>Titel *</span>
     109                  <span>${T('titel')}</span>
    88110                  <input type="text" id="pli-title" maxlength="200" autofocus value="${esc(initial.title)}">
    89111                </label>
    90112                <label class="pl-field">
    91                   <span>Artiest</span>
     113                  <span>${T('artiest')}</span>
    92114                  <input type="text" id="pli-artist" maxlength="200" value="${esc(initial.artist)}">
    93115                </label>
    94116                <label class="pl-field">
    95                   <span>Jaar</span>
     117                  <span>${T('jaar')}</span>
    96118                  <input type="number" id="pli-year" min="1900" max="2099" value="${initial.year || ''}">
    97119                </label>
    98120                <label class="pl-field">
    99                   <span>Type</span>
     121                  <span>${T('type')}</span>
    100122                  <select id="pli-kind">
    101                     <option value="album"    ${initial.kind === 'album' ? 'selected' : ''}>💿 Album (genummerd)</option>
    102                     <option value="playlist" ${initial.kind === 'playlist' ? 'selected' : ''}>📃 Playlist (track-covers)</option>
     123                    <option value="album"    ${initial.kind === 'album' ? 'selected' : ''}>💿 ${T('k_album')}</option>
     124                    <option value="playlist" ${initial.kind === 'playlist' ? 'selected' : ''}>📃 ${T('k_playlist')}</option>
    103125                  </select>
    104126                </label>
     127                <label class="pl-field pl-uitgave">
     128                  <span>${T('uitgave')}</span>
     129                  <input type="date" id="pli-release-date" value="${esc(initial.release_date || '')}">
     130                </label>
     131                <label class="pl-field pl-uitgave">
     132                  <span>${T('mb_release')}</span>
     133                  <input type="text" id="pli-mb-release" maxlength="36" spellcheck="false"
     134                         placeholder="00000000-0000-0000-0000-000000000000"
     135                         value="${esc(initial.mb_release_id || '')}">
     136                </label>
    105137                <div class="pl-field pl-field-full">
    106                   <span>Cover</span>
     138                  <span>${T('cover')}</span>
    107139                  <div class="pl-cover-row">
    108140                    <span class="pl-cover-thumb" id="pli-cover-thumb">
     
    111143                        : `<span class="pl-cover-empty">🎨</span>`}
    112144                    </span>
    113                     <input type="text" id="pli-cover" placeholder="https://… of upload" value="${esc(initial.cover)}" style="flex:1">
     145                    <input type="text" id="pli-cover" placeholder="${T('cover_url')}" value="${esc(initial.cover)}" style="flex:1">
    114146                  </div>
    115147                  <div class="pl-cover-upload-row">
    116148                    <input type="file" id="pli-cover-file" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
    117                     <button type="button" class="pl-btn-small" id="pli-cover-pick">📷 Foto kiezen…</button>
     149                    <button type="button" class="pl-btn-small" id="pli-cover-pick">📷 ${T('cover_kies')}</button>
    118150                    <span class="pl-cover-status" id="pli-cover-status"></span>
    119151                  </div>
     
    121153              </div>
    122154              <div class="pl-section-title">
    123                 Tracks in playlist <span class="pl-track-count" id="pli-count">0</span>
    124                 <small>(sleep ⠿ om te ordenen)</small>
     155                ${T('tracks_in')} <span class="pl-track-count" id="pli-count">0</span>
     156                <small>${T('sleep_hint')}</small>
    125157              </div>
    126158              <div id="pli-selected" class="pl-selected-list"></div>
    127159            </div>
    128160            <div class="pl-editor-right">
    129               <div class="pl-section-title">Beschikbare tracks</div>
    130               <input type="search" id="pli-search" placeholder="Zoek..." class="pl-search-input">
     161              <div class="pl-section-title">${T('beschikbaar')}</div>
     162              <input type="search" id="pli-search" placeholder="${T('zoek')}" class="pl-search-input">
    131163              <div id="pli-available" class="pl-available-list"></div>
    132164            </div>
     
    134166        </div>
    135167        <div class="pl-modal-footer">
    136           <button type="button" class="btn" id="pli-cancel">Annuleren</button>
     168          <button type="button" class="btn" id="pli-cancel">${T('annuleren')}</button>
    137169          <button type="button" class="btn btn-primary" id="pli-save" disabled>
    138             ${isEdit ? 'Opslaan' : 'Aanmaken'}
     170            ${isEdit ? T('opslaan') : T('aanmaken')}
    139171          </button>
    140172        </div>
     
    157189    const yearEl = $('#pli-year');
    158190    const kindEl = $('#pli-kind');
     191    const releaseEl = $('#pli-release-date');
     192    const mbReleaseEl = $('#pli-mb-release');
    159193    const coverEl = $('#pli-cover');
     194
     195    // Uitgavedatum en release-id horen bij een ALBUM, niet bij een
     196    // afspeellijst -- dat is wat de keuze album/playlist betekent. Ze
     197    // verdwijnen dus als je omschakelt, en de server maakt ze dan ook leeg;
     198    // dit scherm is de uitleg, niet de bewaking.
     199    const toonUitgave = () => {
     200      const album = kindEl.value !== 'playlist';
     201      for (const el of backdrop.querySelectorAll('.pl-uitgave')) el.hidden = !album;
     202    };
     203    kindEl.addEventListener('change', toonUitgave);
     204    toonUitgave();
    160205    const coverThumb = $('#pli-cover-thumb');
    161206    const saveBtn = $('#pli-save');
     
    176221    // requireAuth) dat een /media/-URL teruggeeft — dat heeft GEEN playlist-id
    177222    // nodig, dus uploaden kan ook al vóór het aanmaken. De URL belandt in het
    178     // cover-veld en wordt bij 'Aanmaken'/'Opslaan' met de playlist meegestuurd.
     223    // cover-veld en wordt bij het opslaan met de playlist meegestuurd.
    179224    const coverFileInput = $('#pli-cover-file');
    180225    const coverPickBtn   = $('#pli-cover-pick');
     
    187232        if (!file) return;
    188233        if (!/^image\//.test(file.type)) {
    189           coverStatus.textContent = 'Alleen afbeeldingen';
     234          coverStatus.textContent = T('e_alleen_afb');
    190235          coverStatus.className = 'pl-cover-status is-error';
    191236          return;
    192237        }
    193         coverStatus.textContent = 'Uploaden…';
     238        coverStatus.textContent = T('bezig');
    194239        coverStatus.className = 'pl-cover-status';
    195240        const fd = new FormData();
     
    200245          );
    201246          const j = await r.json();
    202           if (!r.ok || !j.url) throw new Error(j.error || 'Upload mislukt');
     247          if (!r.ok || !j.url) throw new Error(j.error || T('e_upload'));
    203248          const url = j.url || '';
    204249          coverEl.value = url;
     
    209254          coverStatus.className = 'pl-cover-status is-ok';
    210255        } catch (err) {
    211           coverStatus.textContent = 'Mislukt: ' + err.message;
     256          coverStatus.textContent = T('e_mislukt') + err.message;
    212257          coverStatus.className = 'pl-cover-status is-error';
    213258        }
     
    222267      countEl.textContent = selected.length;
    223268      if (selected.length === 0) {
    224         selectedEl.innerHTML = '<div class="pl-empty">Klik tracks rechts om toe te voegen.</div>';
     269        selectedEl.innerHTML = '<div class="pl-empty">' + T('leeg_sel') + '</div>';
    225270        updateSaveBtn();
    226271        return;
     
    233278          : `<span class="pl-row-cover pl-row-cover-empty">♪</span>`;
    234279        return `<div class="pl-row" data-id="${esc(id)}" data-pos="${i}">
    235           <span class="pl-row-handle" aria-label="Verslepen">⠿</span>
     280          <span class="pl-row-handle" aria-label="${T('versleep')}">⠿</span>
    236281          <span class="pl-row-num">${i + 1}</span>
    237282          ${cover}
     
    240285            ${t.artist ? `<span class="pl-row-artist">${esc(t.artist)}</span>` : ''}
    241286          </span>
    242           <button type="button" class="pl-row-x" data-id="${esc(id)}" aria-label="Verwijderen">×</button>
     287          <button type="button" class="pl-row-x" data-id="${esc(id)}" aria-label="${T('verwijder')}">×</button>
    243288        </div>`;
    244289      }).join('');
     
    263308      });
    264309      if (matches.length === 0) {
    265         availEl.innerHTML = '<div class="pl-empty">Geen resultaten.</div>';
     310        availEl.innerHTML = '<div class="pl-empty">' + T('geen_res') + '</div>';
    266311        return;
    267312      }
     
    274319          ? `<span class="pl-row-cover"><img src="${esc(t.cover)}" alt=""></span>`
    275320          : `<span class="pl-row-cover pl-row-cover-empty">♪</span>`;
    276         return `<div class="${cls.join(' ')}" data-id="${esc(t.id)}" ${t.playable ? '' : 'title="Track heeft geen audio-bestand"'}>
     321        return `<div class="${cls.join(' ')}" data-id="${esc(t.id)}" ${t.playable ? '' : `title="${T('geen_audio')}"`}>
    277322          ${cover}
    278323          <span class="pl-row-info">
     
    368413      const orig = saveBtn.textContent;
    369414      saveBtn.disabled = true;
    370       saveBtn.textContent = 'Opslaan...';
     415      saveBtn.textContent = T('bezig_opslaan');
    371416
    372417      const payload = {
     
    376421        cover:  coverEl.value.trim(),
    377422        kind:   kindEl.value === 'playlist' ? 'playlist' : 'album',
     423        release_date:  releaseEl.value.trim(),
     424        mb_release_id: mbReleaseEl.value.trim(),
    378425        tracks: selected.slice(),
    379426      };
     
    385432        const j = await api('POST', url, payload);
    386433        if (!j.ok) {
    387           alert('Opslaan mislukt: ' + (j.error || 'onbekend'));
     434          alert(T('e_opslaan') + (j.error || 'onbekend'));
    388435          saveBtn.disabled = false;
    389436          saveBtn.textContent = orig;
     
    396443        }
    397444      } catch (err) {
    398         alert('Opslaan mislukt: ' + err.message);
     445        alert(T('e_opslaan') + err.message);
    399446        saveBtn.disabled = false;
    400447        saveBtn.textContent = orig;
Note: See TracChangeset for help on using the changeset viewer.