Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 3b43e4c42e059a39a2ea3ecd827e68d6c1d8a4e6)
+++ src/routes/activitypub.js	(revision 39a9d21fd2b2e8b7263e26275d94ba5fe52fedf8)
@@ -676,4 +676,24 @@
 });
 
+// De tracks van deze site: de kanonieke plek voor onze muziek (shaer-0nh,
+// stap 3). Een playlist is een keuze hieruit; deze collectie is alles wat de
+// artiest heeft opengezet, ook wat in geen enkele playlist staat.
+router.get('/ap/users/:slug/tracks', (req, res) => {
+  const site = publicSite(req.params.slug);
+  if (!site) return res.status(404).end();
+  AP.sendAP(res, AP.buildTrackCollection(baseUrl(req), site, AP.siteOpenTracks(site.id)));
+});
+
+// Eén track, los op te halen. Een gesloten track is AFWEZIG, niet leeg: 404,
+// dezelfde regel als in de collectie, zodat het bestaan van een gated nummer
+// niet uit een ander antwoord af te leiden is.
+router.get('/ap/users/:slug/tracks/:id', (req, res) => {
+  const site = publicSite(req.params.slug);
+  if (!site) return res.status(404).end();
+  const row = AP.openTrack(site.id, req.params.id);
+  if (!row) return res.status(404).end();
+  AP.sendAP(res, AP.buildTrackAudio(baseUrl(req), site, row, { standalone: true }));
+});
+
 router.get('/ap/users/:slug/playlists/:id', (req, res) => {
   const site = publicSite(req.params.slug);
