Changeset aebde2b in Klonkt for src


Ignore:
Timestamp:
08/16/2026 02:03:14 PM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
7090fd2
Parents:
d68ee3a
Message:

Album als eigen object op de draad (shaer-756s, stap 2)

De uitgave -- een playlist met kind='album' -- gaat nu als fw:Album mee op
elke track die erop staat: type, id, name, published, released,
musicbrainzId, image en een eigen artist_credit.

INGESLOTEN EN NIET ALS URI, en dat is de kern. Hun TrackSerializer heeft
album = AlbumSerializer(): een object met eigen verplichte velden. Een
kale URI expandeert naar een knoop met alleen een @id en valt daar af --
precies waarom Emissary's tracks bij Funkwhale net zo goed stranden,
terwijl ze het veld wel sturen. Als URI staat het album daarnaast op het
Audio-object zelf, zoals Funkwhale 2.0 en Emissary allebei doen.

Het id is de BESTAANDE playlist-collectie. Geen tweede adres voor een
ding dat er al een heeft, en die URI draagt nu dezelfde albumvelden zodat
er hetzelfde staat als in het ingesloten object.

type op die collectie blijft OrderedCollection, en blijft een STRING.
Er stond hier even ['OrderedCollection','Album'] -- geldig AS2, en het is
ook werkelijk allebei -- maar een bestaande test viel erover en had
gelijk: een lezer die type als tekst uitpakt (Shaer) verliest dan in
stilte de hele playlist. Het kost niets, want hun AlbumSerializer
declareert geen type-veld en valideert het dus niet.

trackAlbums() zoekt in EEN query op, zoals trackHostPosts: per track
vragen wordt bij tweehonderd nummers tweehonderd queries. Alleen
kind='album' telt mee -- een mixtape is geen uitgave, en zou hij hier wel
een album opleveren dan was dat onderscheid decoratie.

artistCredit() is nu gedeeld door track en album. Twee keer los bouwen is
hoe een nummer en de plaat waar hij op staat een verschillende artiest
krijgen.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/music/index.js

    rd68ee3a raebde2b  
    134134  for (const r of rijen) if (!uit.has(r.tid)) uit.set(r.tid, { id: r.post_id, slug: r.post_slug });
    135135  return uit;
     136}
     137
     138/**
     139 * De artiest-credit, gedeeld door track en album (shaer-3f8a / shaer-756s).
     140 *
     141 * De ENTITEIT is de site-actor: een echt, opvraagbaar adres. De credittekst --
     142 * de artiestkolom van de track of van de uitgave -- gaat naar `credit`, want
     143 * daar verwacht hun model hem. Er een id per artiestnaam van maken zou
     144 * identiteit uit een string zijn, en dat is de fout die we bij albums juist
     145 * vermijden.
     146 *
     147 * Eén functie voor beide, zodat een track en het album waar hij op staat nooit
     148 * een verschillende artiest kunnen krijgen door twee keer hetzelfde te bouwen.
     149 */
     150function artistCredit(base, site, creditTekst, wanneer) {
     151  const artiest = {
     152    type: 'Artist',
     153    id: actorId(base, site.slug),
     154    name: site.title || site.slug,
     155    published: site.created_at ? new Date(site.created_at).toISOString() : wanneer,
     156  };
     157  if (isMbid(site.mb_artist_id)) artiest.musicbrainzId = String(site.mb_artist_id).trim().toLowerCase();
     158  return [{
     159    type: 'ArtistCredit',
     160    id: `${actorId(base, site.slug)}#artist-credit`,
     161    published: artiest.published,
     162    artist: artiest,
     163    ...(creditTekst ? { credit: creditTekst } : {}),
     164  }];
    136165}
    137166
     
    213242  const wanneer = r.created_at ? new Date(r.created_at).toISOString()
    214243    : (site.created_at ? new Date(site.created_at).toISOString() : new Date(0).toISOString());
    215   const artiest = {
    216     type: 'Artist',
    217     id: actorId(base, site.slug),
    218     name: site.title || site.slug,
    219     published: site.created_at ? new Date(site.created_at).toISOString() : wanneer,
    220   };
    221   if (isMbid(site.mb_artist_id)) artiest.musicbrainzId = String(site.mb_artist_id).trim().toLowerCase();
    222244
    223245  a.track = {
     
    227249    published: wanneer,
    228250    ...(Number(r.position) ? { position: Number(r.position) } : {}),
    229     artist_credit: [{
    230       type: 'ArtistCredit',
    231       id: `${a.id}#artist-credit`,
    232       published: wanneer,
    233       artist: artiest,
    234       ...(r.artist ? { credit: r.artist } : {}),
    235     }],
     251    artist_credit: artistCredit(base, site, r.artist, wanneer),
    236252  };
     253  // De uitgave waar dit nummer op staat, INGESLOTEN (shaer-756s, stap 2).
     254  // `albums` mag expliciet null zijn: dan is er niets op te zoeken.
     255  const uitgave = opts.albums !== undefined
     256    ? (opts.albums && opts.albums.get(r.id)) || null
     257    : ((site.id && trackAlbums(site.id).get(r.id)) || null);
     258  if (uitgave) {
     259    a.track.album = buildAlbumObject(base, site, uitgave);
     260    // Ook op het Audio-object zelf, als URI. Funkwhale 2.0 en Emissary doen dat
     261    // allebei, en het scheelt een lezer het uitpakken van de track.
     262    a.album = a.track.album.id;
     263  }
    237264  if (r.duration) a.duration = `PT${Math.round(r.duration)}S`;
    238265  if (r.created_at) a.published = new Date(r.created_at).toISOString();
     
    303330  const id = libraryId(base, site);
    304331  const hostPosts = site.id ? trackHostPosts(site.id) : null;
    305   const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts }));
     332  const albums = site.id ? trackAlbums(site.id) : null;
     333  const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts, albums }));
    306334  return pagedCollection(id, items, {
    307335    page,
     
    337365  // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts.
    338366  const posts = site.id ? trackHostPosts(site.id) : null;
    339   const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts }));
     367  const albums = site.id ? trackAlbums(site.id) : null;
     368  const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts, albums }));
    340369  return pagedCollection(`${actorId(base, site.slug)}/tracks`, items, { page, extra: { attributedTo: actorId(base, site.slug) } });
    341370}
     
    399428}
    400429
     430/**
     431 * Bij welke UITGAVE hoort een track? (shaer-756s, stap 2)
     432 *
     433 * Alleen playlists met kind='album' tellen: een mixtape is geen uitgave, en dat
     434 * onderscheid is precies wat de keuze album/playlist betekent. Zit een track in
     435 * twee albums, dan wint de oudste -- willekeurig maar STABIEL, en dat is wat
     436 * telt: een id dat per ophaalactie verspringt is erger dan een id dat niet de
     437 * mooiste keuze is.
     438 *
     439 * Eén zoekopdracht voor alle rijen samen, zoals trackHostPosts. Per track
     440 * vragen wordt bij tweehonderd nummers tweehonderd zoekopdrachten.
     441 */
     442export function trackAlbums(siteId) {
     443  const rijen = db.prepare(`
     444    SELECT pt.track_id AS tid, p.id, p.title, p.artist, p.year, p.cover_url,
     445           p.release_date, p.mb_release_id, p.created_at
     446      FROM playlist_tracks pt
     447      JOIN playlists p ON p.id = pt.playlist_id
     448     WHERE p.site_id = ? AND p.kind = 'album'
     449     ORDER BY p.created_at, p.id
     450  `).all(siteId);
     451  const uit = new Map();
     452  for (const r of rijen) if (!uit.has(r.tid)) uit.set(r.tid, r);
     453  return uit;
     454}
     455
     456/**
     457 * Een uitgave als `fw:Album`.
     458 *
     459 * INGESLOTEN EN NIET ALS URI, en dat is het hele punt van deze stap. Funkwhale's
     460 * TrackSerializer heeft `album = AlbumSerializer()` -- een object met name,
     461 * published en een eigen artist_credit. Een kale URI expandeert naar een knoop
     462 * met alleen een @id en valt daar dus af. Emissary stuurt precies zo'n kale URI,
     463 * en dat is waarom hun tracks bij Funkwhale net zo goed stranden.
     464 *
     465 * Het `id` is de bestaande playlist-collectie: dereferenceerbaar, en het is
     466 * werkelijk hetzelfde ding. We verzinnen geen tweede adres voor iets dat er al
     467 * een heeft.
     468 */
     469export function buildAlbumObject(base, site, pl) {
     470  if (!pl) return null;
     471  const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
     472  const wanneer = pl.created_at ? new Date(pl.created_at).toISOString() : new Date(0).toISOString();
     473  const album = {
     474    type: 'Album',
     475    id: `${actorId(base, site.slug)}/playlists/${pl.id}`,
     476    name: pl.title,
     477    published: wanneer,
     478    attributedTo: actorId(base, site.slug),
     479    artist_credit: artistCredit(base, site, pl.artist, wanneer),
     480  };
     481  // `released` alleen als er een ECHTE datum is. `year` vult hem niet aan: een
     482  // jaartal is geen dag, en dat is de reden dat release_date bestaat.
     483  if (pl.release_date) album.released = pl.release_date;
     484  if (pl.mb_release_id) album.musicbrainzId = pl.mb_release_id;
     485  const hoes = abs(pl.cover_url || null);
     486  if (hoes) album.image = { type: 'Image', mediaType: guessMediaType(hoes), url: hoes };
     487  return album;
     488}
     489
    401490export function buildPlaylistCollection(base, site, playlist, rows) {
    402491  const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
     
    407496  // De hoes van de playlist dient als terugval voor een track zonder eigen hoes.
    408497  const hostPosts = site.id ? trackHostPosts(site.id) : null;
    409   const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts }));
     498  const albums = site.id ? trackAlbums(site.id) : null;
     499  const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts, albums }));
    410500  const out = pagedCollection(`${actorId(base, site.slug)}/playlists/${playlist.id}`, items, {
    411501    extra: { name: playlist.title, attributedTo: actorId(base, site.slug) },
     
    418508  const cover = abs(playlist.cover_url || null);
    419509  if (cover) out.icon = { type: 'Image', mediaType: guessMediaType(cover), url: cover };
     510
     511  // Is dit een UITGAVE, dan draagt deze collectie ook de albumvelden
     512  // (shaer-756s, stap 2): het is het adres waar track.album naar wijst, en dan
     513  // hoort hier hetzelfde te staan als in het ingesloten object.
     514  //
     515  // `type` blijft OrderedCollection, EN BLIJFT EEN STRING. Er stond hier even
     516  // ['OrderedCollection', 'Album'] -- geldig AS2, en het is ook werkelijk
     517  // allebei -- maar een bestaande test viel erover, en die test had gelijk: een
     518  // lezer die `type` als tekst uitpakt (Shaer doet dat) verliest dan in stilte
     519  // de hele playlist. Het kost ons niets, want hun AlbumSerializer declareert
     520  // geen type-veld en valideert het dus niet: haalt Funkwhale dit adres op als
     521  // album, dan leest hij deze velden gewoon. En het object dat hij echt gebruikt
     522  // staat toch al ingesloten op de track.
     523  if ((playlist.kind || 'album') === 'album') {
     524    const album = buildAlbumObject(base, site, playlist);
     525    for (const veld of ['published', 'released', 'musicbrainzId', 'artist_credit', 'image']) {
     526      if (album[veld] !== undefined) out[veld] = album[veld];
     527    }
     528  }
    420529  return leenVanPost(base, site, out, uitgavePost(site.id, playlist.id));
    421530}
Note: See TracChangeset for help on using the changeset viewer.