Changeset b0ccd0d in Klonkt for src


Ignore:
Timestamp:
08/22/2026 08:14:01 AM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
25dd08f
Parents:
8ed36de
Message:

De mixtape als eigen soort: data en draad

Een bandje is geen album en geen afspeellijst. Een album is een uitgave -- de
nummers bestaan er los van. Bij een mixtape is de VOLGORDE het werk en zijn de
nummers onderdelen. Vandaar een eigen soort, en op de draad een enkel object
met de nummers erin.

De valkuil zat in de vorm die er al stond. De keuze album/playlist stond vijf
keer als x === 'playlist' ? 'playlist' : 'album' verspreid over drie
bestanden, en zo'n vorm valt bij een derde soort niet om -- hij slikt hem. Een
mixtape zou stilzwijgend een album zijn geworden en als Album de deur uit gaan.
De lijst staat nu een keer, in de pure gedeelde module, want die kent de
browser ook.

Op de draad:

  • type is de STRING 'Mixtape'. Geen array, en dat is geleerd: bij de playlist-collectie stond ooit ['OrderedCollection','Album'] -- geldig AS2 en werkelijk allebei -- en een lezer die type als tekst uitpakt (Shaer) verloor daarmee stil het hele object.
  • orderedItems op het object zelf, niet een collectie ernaast.
  • Geen released en geen musicbrainzId: die zouden beweren dat dit een uitgave is.
  • Geen eigen url. Robins keuze: het bandje is een omhulling, geen gerenderd bestand. Prijs ervan is opgeschreven bij de functie -- wie het type niet kent heeft geen stream, al staan de nummers er stuk voor stuk in.

Een mixtape met losse tracks erbij blijft een mixtape. De oude regel maakte
daar een album van met bonustracks; dat klopt voor een plaat en niet voor een
bandje. Album en playlist houden bewust hun oude gedrag, want dat zijn
bestaande posts.

Acht tests, waaronder de weg door de database heen in plaats van langs een
handgemaakte opzoeker.

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

Location:
src
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/shared/post-music-type.js

    r8ed36de rb0ccd0d  
    2121 *   een collectie                 -> die soort  metadata van de post geleend
    2222 *   een collectie + losse tracks  -> album      de losse zijn BONUS-TRACKS
     23 *   een MIXTAPE + losse tracks    -> mixtape    de losse zijn BONUS-TRACKS
    2324 *   twee of meer collecties       -> post       NIETS geleend
     25 *
     26 * Die vierde regel is de uitzondering op de derde en staat er met opzet naast:
     27 * bonustracks maken van een uitgave een album, maar een bandje is geen uitgave.
    2428 *
    2529 * Waarom de lening bij de laatste vervalt: die bestaat omdat een collectie soms
     
    3943// insluit, telt hier niet mee. Anders zou een shortcode die niets oplevert wel
    4044// het type van de post kunnen bepalen.
     45// De soorten die een playlist kan zijn, en dit is de ENIGE lijst. PlaylistService
     46// en music/index.js halen hem hier op, precies omdat het andersom niet kan:
     47// deze module draait ook in de browser en mag niets van de database weten.
     48//
     49// Waarom dat uitmaakt: bij twee soorten stond de keuze vijf keer als
     50// `x === 'playlist' ? 'playlist' : 'album'` verspreid over drie bestanden. Zo'n
     51// vorm valt niet om bij een derde soort, hij slikt hem -- een mixtape werd
     52// stilzwijgend een album en ging als Album de deur uit.
     53export const SOORTEN = ['album', 'playlist', 'mixtape'];
     54
    4155const RE_PLAYLIST = /\[\[playlist:([a-z0-9][a-z0-9-]*)\]\]/gi;
    4256const RE_ALBUM    = /\[\[album:([^\]]+)\]\]/g;
     
    6478  const uitPlaylists = playlists.map((id) => {
    6579    const kind = zoek(id);
    66     if (kind !== 'album' && kind !== 'playlist') { onbekend.push(id); return null; }
     80    if (!SOORTEN.includes(kind)) { onbekend.push(id); return null; }
    6781    return { soort: kind, id };
    6882  }).filter(Boolean);
     
    87101  if (collecties.length === 1) {
    88102    const c0 = collecties[0];
     103    // EEN MIXTAPE BLIJFT EEN MIXTAPE, ook met losse tracks erbij. De regel
     104    // hieronder maakt van collectie + losse tracks een album met bonustracks, en
     105    // dat klopt voor een uitgave: extra nummers bij een plaat zijn bonus. Een
     106    // bandje is geen uitgave. Er een album van maken omdat er een los nummer
     107    // naast staat zou het ding hernoemen op grond van iets wat er niet bij
     108    // hoort. Album en playlist houden bewust hun oude gedrag: dat zijn
     109    // bestaande posts en die mogen hier niet stilletjes van soort wisselen.
     110    if (c0.soort === 'mixtape') {
     111      return { type: 'mixtape', collectie: c0, tracks: [], bonus: tracks, leentMetadata: true };
     112    }
    89113    // Losse tracks naast een collectie zijn geen rommelrestje maar bonus-tracks,
    90114    // en dat maakt het geheel een album.
  • src/services/PlaylistService.js

    r8ed36de rb0ccd0d  
    1414import db from '../config/database.js';
    1515import { v4 as uuid } from 'uuid';
     16import { SOORTEN } from '../assets/js/shared/post-music-type.js';
    1617
    1718/**
     
    4142  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(s) ? s : null;
    4243}
     44
     45/**
     46 * De drie soorten die een playlist kan zijn, op EEN plek.
     47 *
     48 * Stond eerder vijf keer als `data.kind === 'playlist' ? 'playlist' : 'album'`
     49 * verspreid over dit bestand. Met twee soorten kon dat nog; bij een derde is
     50 * het een fout die staat te wachten, want een van de vijf vergeten betekent dat
     51 * een mixtape stilletjes als album wordt opgeslagen en als Album de deur uit
     52 * gaat.
     53 *
     54 * Album blijft de terugval: een onbekende waarde hoort niet stilzwijgend iets
     55 * nieuws te worden. De lijst zelf staat in de gedeelde pure module, want de
     56 * editor in de browser moet dezelfde drie kennen.
     57 */
     58export function normKind(v) {
     59  const s = String(v == null ? '' : v).trim().toLowerCase();
     60  return SOORTEN.includes(s) ? s : 'album';
     61}
     62
     63/**
     64 * Draagt deze soort uitgavegegevens? Alleen een album. Een afspeellijst en een
     65 * mixtape hebben geen uitgavedatum en geen release-id: het zijn samenstellingen
     66 * van andermans of eigen werk, geen uitgave op zichzelf.
     67 */
     68export const isUitgave = (kind) => normKind(kind) === 'album';
    4369
    4470class PlaylistService {
     
    92118      year: r.year || 0,
    93119      cover: r.cover_url || '',
    94       kind: r.kind || 'album',
     120      kind: normKind(r.kind),
    95121      release_date: r.release_date || '',
    96122      mb_release_id: r.mb_release_id || '',
     
    154180      year: p.year || 0,
    155181      cover: p.cover_url || fallbackCover,
    156       kind: (p.kind === 'playlist') ? 'playlist' : 'album',
     182      kind: normKind(p.kind),
    157183      // created_at hoort erbij omdat de AP-kant er `published` van maakt. Zonder
    158184      // dit veld viel buildAlbumObject terug op 1970, en dat stond op 16-8
     
    162188      // maar dit is de plek waar de editor leest en die mag niet afhangen van
    163189      // wat er toevallig in de kolom stond.
    164       release_date: p.kind === 'playlist' ? '' : (p.release_date || ''),
    165       mb_release_id: p.kind === 'playlist' ? '' : (p.mb_release_id || ''),
     190      release_date: isUitgave(p.kind) ? (p.release_date || '') : '',
     191      mb_release_id: isUitgave(p.kind) ? (p.mb_release_id || '') : '',
    166192      tracks: mappedTracks,
    167193    };
     
    178204    const id = this.generateId(siteId, title);
    179205    const now = new Date().toISOString();
    180     const kind = data.kind === 'playlist' ? 'playlist' : 'album';
     206    const kind = normKind(data.kind);
    181207
    182208    // Alleen een UITGAVE draagt deze twee. Een afspeellijst heeft geen
     
    186212    // voor de post-editor -- en dan staat er stille rommel op een mixtape die
    187213    // later als Album de deur uit gaat.
    188     const uitgave = kind === 'album';
     214    const uitgave = isUitgave(kind);
    189215    const releaseDate = uitgave ? normDatum(data.release_date) : null;
    190216    const mbRelease = uitgave ? normMbid(data.mb_release_id) : null;
     
    229255    // val terug op wat er ligt.
    230256    const nieuwKind = Object.prototype.hasOwnProperty.call(data, 'kind')
    231       ? (data.kind === 'playlist' ? 'playlist' : 'album')
    232       : (existing.kind || 'album');
     257      ? normKind(data.kind)
     258      : normKind(existing.kind);
    233259    if (Object.prototype.hasOwnProperty.call(data, 'title')) {
    234260      const v = String(data.title || '').trim();
  • src/services/music/index.js

    r8ed36de rb0ccd0d  
    1515import db from '../../config/database.js';
    1616import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection, isMbid } from '../ap-core.js';
    17 import { afleidenUitInsluitingen, ingeslotenPlaylists } from '../../assets/js/shared/post-music-type.js';
     17import { afleidenUitInsluitingen, ingeslotenPlaylists, SOORTEN } from '../../assets/js/shared/post-music-type.js';
    1818// De luisteraars horen bij de muziekkant; hier doorgegeven zodat
    1919// ActivityPubService niet in een submap hoeft te grijpen.
     
    495495 * een heeft.
    496496 */
     497/**
     498 * Het bandje: EEN object, samengesteld uit de nummers van een playlist.
     499 *
     500 * Waarom het een eigen soort is en geen album met een ander jasje. Een album is
     501 * een uitgave: het heeft een uitgavedatum, een release-id, en de nummers
     502 * bestaan er los van. Een mixtape is het omgekeerde -- de volgorde IS het werk,
     503 * en de nummers zijn er onderdelen van. Vandaar `orderedItems` op het object
     504 * zelf in plaats van een collectie ernaast, en vandaar geen `released` en geen
     505 * `musicbrainzId`: die zouden beweren dat dit een uitgave is.
     506 *
     507 * GEEN EIGEN `url`, en dat is een keuze van Robin (21-8) met een prijs die het
     508 * waard is om hier op te schrijven. Het bandje is een logische omhulling, geen
     509 * gerenderd bestand: er wordt niets samengevoegd. Een ontvanger die `Mixtape`
     510 * niet kent heeft dus geen stream om te spelen. Dat is bewust -- de nummers
     511 * staan er stuk voor stuk in, met hun eigen id en hun eigen url, dus er gaat
     512 * niets verloren; het kost alleen een consument die het type wel begrijpt.
     513 *
     514 * `type` is een STRING en geen array. Dat is geen slordigheid maar een geleerde
     515 * les: er stond bij de playlist-collectie ooit ['OrderedCollection', 'Album'],
     516 * geldig AS2 en werkelijk allebei, en een lezer die `type` als tekst uitpakt
     517 * (Shaer doet dat) verloor daarmee stil het hele object.
     518 */
     519export function buildMixtapeObject(base, site, pl, rows) {
     520  if (!pl) return null;
     521  const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
     522  const postDatum = (pl._post && pl._post.uit_wanneer) ? new Date(pl._post.uit_wanneer) : null;
     523  const wanneer = postDatum ? postDatum.toISOString()
     524    : (pl.created_at ? new Date(pl.created_at).toISOString() : null);
     525  // Dezelfde lening als bij het album: de post die het bandje uitbrengt geeft
     526  // zijn titel, en de eigen titel blijft als alsoKnownAs staan.
     527  const titel = (pl._post && pl._post.title) || pl.title;
     528  const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: pl.cover_url || null }));
     529  const tape = {
     530    type: 'Mixtape',
     531    id: `${actorId(base, site.slug)}/playlists/${pl.id}`,
     532    name: titel,
     533    ...(wanneer ? { published: wanneer } : {}),
     534    attributedTo: actorId(base, site.slug),
     535    artist_credit: artistCredit(base, site, pl.artist, wanneer || new Date().toISOString()),
     536    // De kant die het bandje maakt: eerst dit nummer, dan dat. Vooruit en
     537    // achteruit is de speler; de volgorde is het object.
     538    totalItems: items.length,
     539    orderedItems: items,
     540  };
     541  if (titel !== pl.title) tape.alsoKnownAs = pl.title;
     542  // De speelduur van het geheel, want dat is wat een bandje heeft: een lengte.
     543  // Alleen als we van ELK nummer de duur kennen -- een som met gaten erin is
     544  // een verzonnen getal, en die zetten we hier niet neer (zelfde regel als bij
     545  // de bitrate van een track).
     546  const duren = (rows || []).map((r) => Number(r.duration) || 0);
     547  if (duren.length && duren.every((d) => d > 0)) {
     548    tape.duration = `PT${Math.round(duren.reduce((a, b) => a + b, 0))}S`;
     549  }
     550  const hoes = abs(pl.cover_url || null);
     551  if (hoes) tape.image = { type: 'Image', mediaType: guessMediaType(hoes), url: hoes };
     552  return tape;
     553}
     554
    497555export function buildAlbumObject(base, site, pl) {
    498556  if (!pl) return null;
     
    580638  // album, dan leest hij deze velden gewoon. En het object dat hij echt gebruikt
    581639  // staat toch al ingesloten op de track.
    582   if ((playlist.kind || 'album') === 'album') {
     640  const soort = SOORTEN.includes(playlist.kind) ? playlist.kind : 'album';
     641  if (soort === 'album') {
    583642    const album = buildAlbumObject(base, site, { ...playlist, _post: post });
    584643    for (const veld of ['published', 'released', 'musicbrainzId', 'artist_credit', 'image']) {
    585644      if (album[veld] !== undefined) out[veld] = album[veld];
     645    }
     646  }
     647  // Een mixtape draagt hier zijn eigen velden, om dezelfde reden als het album:
     648  // dit adres is waar een lezer terechtkomt die het bandje wil ophalen, en dan
     649  // hoort er hetzelfde te staan als in het ingesloten object. `type` blijft ook
     650  // hier OrderedCollection -- zie de uitleg hierboven over Shaer.
     651  if (soort === 'mixtape') {
     652    const tape = buildMixtapeObject(base, site, { ...playlist, _post: post }, rows);
     653    for (const veld of ['published', 'artist_credit', 'image', 'duration', 'alsoKnownAs']) {
     654      if (tape[veld] !== undefined) out[veld] = tape[veld];
    586655    }
    587656  }
     
    801870
    802871/**
    803  * De gekozen soort van een playlist: 'album' | 'playlist', of null als hij niet
    804  * (op deze site) bestaat. Zelfde normalisatie als PlaylistService: alles wat
    805  * geen 'playlist' zegt is een album.
     872 * De gekozen soort van een playlist: 'album' | 'playlist' | 'mixtape', of null
     873 * als hij niet (op deze site) bestaat. Zelfde lijst als PlaylistService, via
     874 * de gedeelde pure module -- alles wat er niet in staat is een album.
    806875 */
    807876function playlistKind(id, siteId) {
     
    810879    const r = db.prepare('SELECT kind FROM playlists WHERE id = ? AND site_id = ?').get(id, siteId);
    811880    if (!r) return null;
    812     return r.kind === 'playlist' ? 'playlist' : 'album';
     881    return SOORTEN.includes(r.kind) ? r.kind : 'album';
    813882  } catch { return null; }
    814883}
Note: See TracChangeset for help on using the changeset viewer.