Index: src/services/ap-core.js
===================================================================
--- src/services/ap-core.js	(revision 5280532d3141d729c701616c3bbe537874712e50)
+++ src/services/ap-core.js	(revision 9a585bf85092daf7cdfe5cb7983122f7018082d8)
@@ -88,4 +88,22 @@
     Track: 'fw:Track',
     track: { '@id': 'fw:track', '@type': '@id' },
+    // ARTIEST-CREDIT. Hun TrackSerializer eist minstens een artist_credit, en
+    // dat leek lang onmogelijk: het vraagt een Artist met een eigen id, en bij
+    // ons was een artiest tekst. Sinds de MusicBrainz-koppeling (shaer-mbz) is
+    // dat niet meer waar -- de site-ACTOR is de artiest. Een echte, opvraagbare
+    // URI, met de sitetitel als naam en een musicbrainzId als hij gekoppeld is.
+    // Er valt hier niets te verzinnen; open.audio leidde dit zelfs al zelf af
+    // uit onze attributedTo (gemeten 13-8).
+    //
+    // `@container: @list` is GEEN opsmuk. Ze lezen dit veld met
+    // first_attr(FW.artist_credit, "@list"), en zonder die declaratie expandeert
+    // onze array niet naar een @list -- dan staat er iets dat er goed uitziet en
+    // door hun lezer niet gevonden wordt. Letterlijk hun contexts.py regel 311.
+    Artist: 'fw:Artist',
+    ArtistCredit: 'fw:ArtistCredit',
+    artist: { '@id': 'fw:artist', '@type': '@id' },
+    artist_credit: { '@id': 'fw:artist_credit', '@type': '@id', '@container': '@list' },
+    credit: 'fw:credit',
+    musicbrainzId: 'fw:musicbrainzId',
     position: 'schema:position',
     bitrate: 'schema:bitrate',
Index: src/services/music/index.js
===================================================================
--- src/services/music/index.js	(revision 5280532d3141d729c701616c3bbe537874712e50)
+++ src/services/music/index.js	(revision 9a585bf85092daf7cdfe5cb7983122f7018082d8)
@@ -14,5 +14,5 @@
 
 import db from '../../config/database.js';
-import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection } from '../ap-core.js';
+import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection, isMbid } from '../ap-core.js';
 import { afleidenUitInsluitingen, ingeslotenPlaylists } from '../../assets/js/shared/post-music-type.js';
 // De luisteraars horen bij de muziekkant; hier doorgegeven zodat
@@ -195,9 +195,43 @@
   // een tekstkolom; er hier een adres van maken zou een ding beloven dat niet
   // bestaat. Zie shaer-k37k -- dat is de keuze die daarvoor eerst moet vallen.
+  //
+  // WIE IS DE ARTIEST. Hun Artist is een ENTITEIT met een id, en bij ons is een
+  // artiest een tekstkolom op de track. Die twee verzoenen we zo: de entiteit
+  // is de site-ACTOR -- een echt, opvraagbaar adres, het account dat dit
+  // uitbrengt -- en de tekst uit de kolom gaat naar `credit`, want dat is
+  // precies waar hun model de credittekst verwacht.
+  //
+  // Dat is eerlijk en het is niet nieuw: open.audio leidde op 13-8 al zelf een
+  // artist_credit af uit onze attributedTo. We maken alleen expliciet wat daar
+  // toch al gebeurde.
+  //
+  // DE GRENS ERVAN: brengt een site werk van iemand anders uit, dan zegt dit
+  // dat de site de artiest is. Dat stond al in attributedTo, dus we maken het
+  // niet erger -- maar het is wel de reden dat we hier geen id per artiestnaam
+  // verzinnen. Identiteit uit een string is dezelfde fout als bij het album
+  // (shaer-756s).
+  const wanneer = r.created_at ? new Date(r.created_at).toISOString()
+    : (site.created_at ? new Date(site.created_at).toISOString() : new Date(0).toISOString());
+  const artiest = {
+    type: 'Artist',
+    id: actorId(base, site.slug),
+    name: site.title || site.slug,
+    published: site.created_at ? new Date(site.created_at).toISOString() : wanneer,
+  };
+  if (isMbid(site.mb_artist_id)) artiest.musicbrainzId = String(site.mb_artist_id).trim().toLowerCase();
+
   a.track = {
     type: 'Track',
     id: `${a.id}#track`,
     name: a.name,
+    published: wanneer,
     ...(Number(r.position) ? { position: Number(r.position) } : {}),
+    artist_credit: [{
+      type: 'ArtistCredit',
+      id: `${a.id}#artist-credit`,
+      published: wanneer,
+      artist: artiest,
+      ...(r.artist ? { credit: r.artist } : {}),
+    }],
   };
   if (r.duration) a.duration = `PT${Math.round(r.duration)}S`;
