Changeset 3e1aab1 in Klonkt


Ignore:
Timestamp:
08/07/2026 06:47:09 PM (5 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
7df47b6
Parents:
da84110
git-author:
Robin <roboburr@…> (08/07/2026 06:08:25 PM)
git-committer:
Robin <roboburr@…> (08/07/2026 06:47:09 PM)
Message:

Een playlist federeert als AP-collectie (shaer-ayc, stap 1)

De eerste stap van het Funkwhale-spoor, en playlists gaan voor omdat zij als
enige al een id hebben -- een album is nog een tekstveld en kan nergens aan
hangen.

GET /ap/users/:slug/playlists/:id geeft een OrderedCollection van Audio-
objecten, dezelfde rijvorm die een post als attachment meestuurt, zodat elke
client die post-audio speelt dit ook speelt. Kaal AS2, geen eigen vocab:
artiest is summary, duur is PT..S, hoes is icon en een track zonder eigen
hoes erft die van de playlist.

De poortregel verandert niet: alleen fedi_open-tracks staan erin, met echte
bestands-URL. Een gated track is geen lege rij maar afwezig, en totalItems
telt alleen het open deel -- wie leest kan niet aftellen hoeveel er achter
de poort staat, en ziet titel noch bestandsnaam.

Tests: bestaan/vorm, poort (inclusief niet-lekken van titel en bestandsnaam),
volgorde (de gated track staat er middenin, dus meetellen zou ook de volgorde
breken), tenancy (playlist van site A op actor B is 404).

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

Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    rda84110 r3e1aab1  
    630630});
    631631
     632// ── Playlist als dereferenceerbare AP-collectie (shaer-ayc) ───────
     633// De eerste stap van het Funkwhale-spoor: een playlist heeft een id, dus een
     634// stabiele URI. Alleen het fedi_open-deel staat erin (de poort is per bestand
     635// en eenrichtings; zie setAudioFediOpen in routes/posts.js) — een collectie
     636// zonder open tracks bestaat wel maar is leeg, want de playlist zelf is niet
     637// geheim, alleen de bestanden erachter.
     638router.get('/ap/users/:slug/playlists/:id', (req, res) => {
     639  const site = publicSite(req.params.slug);
     640  if (!site) return res.status(404).end();
     641  const pl = db.prepare('SELECT id, title, artist, year, cover_url, kind FROM playlists WHERE id = ? AND site_id = ?')
     642    .get(req.params.id, site.id);
     643  if (!pl) return res.status(404).end();
     644  AP.sendAP(res, AP.buildPlaylistCollection(baseUrl(req), site, pl, AP.playlistOpenTracks(pl.id)));
     645});
     646
    632647// ── Note ──────────────────────────────────────────────────────────
    633648router.get('/ap/notes/:id', async (req, res) => {
  • src/services/ActivityPubService.js

    rda84110 r3e1aab1  
    868868    orderedItems: items,
    869869  };
     870}
     871
     872// ── Playlist als AP-collectie (shaer-ayc, stap 1 van het Funkwhale-spoor) ──
     873// Een playlist heeft, anders dan een album-als-tekstveld, een id — dus kan hij
     874// een stabiele URI dragen en federeren. De vorm is bewust kaal AS2: een
     875// OrderedCollection van Audio-objecten, dezelfde rijvorm die een post als
     876// attachment meestuurt, zodat elke client die post-audio al speelt dit ook
     877// speelt.
     878//
     879// De poortregel verandert hier NIET: alleen fedi_open-tracks staan erin, met
     880// echte bestands-URL. Een gated track is niet "een rij zonder url" maar
     881// afwezig — wie de collectie leest ziet het open deel en kan niet aftellen
     882// hoeveel er achter de poort staat. totalItems telt daarom ook alleen het
     883// open deel: een eerlijke telling over wat er werkelijk in de collectie staat,
     884// niet over wat wij thuis in de kast hebben.
     885export function playlistOpenTracks(playlistId) {
     886  return db.prepare(
     887    `SELECT t.title, t.artist, t.duration, t.cover_url, m.filename, m.storage_path, m.mime_type
     888     FROM playlist_tracks pt
     889     JOIN audio_tracks t ON t.id = pt.track_id
     890     JOIN media m ON m.id = t.media_id
     891     WHERE pt.playlist_id = ? AND t.fedi_open = 1
     892     ORDER BY pt.position`
     893  ).all(playlistId);
     894}
     895
     896export function buildPlaylistCollection(base, site, playlist, rows) {
     897  const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
     898  // Zelfde afleiding als in buildNote; die daar is functie-lokaal.
     899  const mediaType = (u) => {
     900    const e = ((u || '').split('?')[0].match(/\.(\w+)$/) || [])[1];
     901    return ({ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif' })[(e || '').toLowerCase()] || 'image/jpeg';
     902  };
     903  const items = (rows || []).map((r) => {
     904    const fn = r.filename || (r.storage_path || '').split('/').pop();
     905    const a = {
     906      type: 'Audio',
     907      mediaType: r.mime_type || 'audio/mpeg',
     908      url: `${base}/audio/stream/${encodeURIComponent(fn)}`,
     909      name: r.title || 'Audio',
     910    };
     911    if (r.artist) a.summary = r.artist; // artiest als summary: kaal AS2, geen eigen vocab
     912    if (r.duration) a.duration = `PT${Math.round(r.duration)}S`;
     913    const art = abs(r.cover_url || playlist.cover_url || null);
     914    if (art) a.icon = { type: 'Image', mediaType: mediaType(art), url: art };
     915    return a;
     916  });
     917  const out = {
     918    '@context': AP_CONTEXT,
     919    id: `${actorId(base, site.slug)}/playlists/${playlist.id}`,
     920    type: 'OrderedCollection',
     921    name: playlist.title,
     922    attributedTo: actorId(base, site.slug),
     923    totalItems: items.length,
     924    orderedItems: items,
     925  };
     926  // Album of playlist is presentatie; op de draad is het één samenvattingsveld.
     927  const parts = [];
     928  if (playlist.artist) parts.push(playlist.artist);
     929  if (playlist.year) parts.push(String(playlist.year));
     930  if (parts.length) out.summary = parts.join(' · ');
     931  const cover = abs(playlist.cover_url || null);
     932  if (cover) out.icon = { type: 'Image', mediaType: mediaType(cover), url: cover };
     933  return out;
    870934}
    871935
     
    54185482  AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions,
    54195483  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
     5484  buildPlaylistCollection, playlistOpenTracks,
    54205485  followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
    54215486  feedCursor, feedChangesSince, waitForFeedChange,
Note: See TracChangeset for help on using the changeset viewer.