Changeset da84110 in Klonkt
- Timestamp:
- 08/07/2026 06:46:34 PM (5 weeks ago)
- Branches:
- main
- Children:
- 3e1aab1, a8364ee
- Parents:
- 4b5db37
- Files:
-
- 2 deleted
- 3 edited
-
src/routes/activitypub.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (4 diffs)
-
test/activitypub-as2.test.js (modified) (1 diff)
-
test/ap-playlist-discovery.test.js (deleted)
-
test/ap-playlist.test.js (deleted)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
r4b5db37 rda84110 630 630 }); 631 631 632 // ── Playlist als dereferenceerbare AP-collectie (shaer-ayc) ───────633 // De eerste stap van het Funkwhale-spoor: een playlist heeft een id, dus een634 // stabiele URI. Alleen het fedi_open-deel staat erin (de poort is per bestand635 // en eenrichtings; zie setAudioFediOpen in routes/posts.js) — een collectie636 // zonder open tracks bestaat wel maar is leeg, want de playlist zelf is niet637 // geheim, alleen de bestanden erachter.638 // De lijst van alle playlist-collecties (shaer-ayc, stap 2). De actor wijst639 // hierheen via AS2 `streams`. Kaal standaard; verrijkte stubs op verzoek640 // (FEP-9876), dezelfde conventie als followers/following.641 router.get('/ap/users/:slug/playlists', (req, res) => {642 const site = publicSite(req.params.slug);643 if (!site) return res.status(404).end();644 AP.sendAP(res, AP.listPlaylistsAP(baseUrl(req), site, wantsEnriched(req, res)));645 });646 647 router.get('/ap/users/:slug/playlists/:id', (req, res) => {648 const site = publicSite(req.params.slug);649 if (!site) return res.status(404).end();650 const pl = db.prepare('SELECT id, title, artist, year, cover_url, kind FROM playlists WHERE id = ? AND site_id = ?')651 .get(req.params.id, site.id);652 if (!pl) return res.status(404).end();653 AP.sendAP(res, AP.buildPlaylistCollection(baseUrl(req), site, pl, AP.playlistOpenTracks(pl.id)));654 });655 656 632 // ── Note ────────────────────────────────────────────────────────── 657 633 router.get('/ap/notes/:id', async (req, res) => { -
src/services/ActivityPubService.js
r4b5db37 rda84110 195 195 following: `${id}/following`, 196 196 featured: `${id}/featured`, 197 // AS2-kern `streams`: "supplementary Collections which may be of198 // interest" -- precies wat de playlist-lijst is (shaer-ayc, stap 2).199 // Geen eigen vocabulaire nodig, en wie het niet kent negeert het.200 streams: [`${id}/playlists`],201 197 // AP §5.6: the private blocked collection (owner-only GET). The server 202 198 // list is the source of truth for Shaer's "in Orbit"; clients keep no … … 552 548 ...((post.fan_only || post.ap_visibility === 'quiet') ? [] : [`${aId}/followers`]), 553 549 ..._mentionCc])], 554 tag: [...buildHashtagList(base, post.tags, body), ..._mentionTags , ...playlistLinkTags(base, site, post.content)],550 tag: [...buildHashtagList(base, post.tags, body), ..._mentionTags], 555 551 replies: `${id}/replies`, 556 552 // NSFW → Mastodon-style content warning: sensitive (blurs media) + a summary/spoiler … … 872 868 orderedItems: items, 873 869 }; 874 }875 876 // ── Playlist als AP-collectie (shaer-ayc, stap 1 van het Funkwhale-spoor) ──877 // Een playlist heeft, anders dan een album-als-tekstveld, een id — dus kan hij878 // een stabiele URI dragen en federeren. De vorm is bewust kaal AS2: een879 // OrderedCollection van Audio-objecten, dezelfde rijvorm die een post als880 // attachment meestuurt, zodat elke client die post-audio al speelt dit ook881 // speelt.882 //883 // De poortregel verandert hier NIET: alleen fedi_open-tracks staan erin, met884 // echte bestands-URL. Een gated track is niet "een rij zonder url" maar885 // afwezig — wie de collectie leest ziet het open deel en kan niet aftellen886 // hoeveel er achter de poort staat. totalItems telt daarom ook alleen het887 // open deel: een eerlijke telling over wat er werkelijk in de collectie staat,888 // niet over wat wij thuis in de kast hebben.889 export function playlistOpenTracks(playlistId) {890 return db.prepare(891 `SELECT t.title, t.artist, t.duration, t.cover_url, m.filename, m.storage_path, m.mime_type892 FROM playlist_tracks pt893 JOIN audio_tracks t ON t.id = pt.track_id894 JOIN media m ON m.id = t.media_id895 WHERE pt.playlist_id = ? AND t.fedi_open = 1896 ORDER BY pt.position`897 ).all(playlistId);898 }899 900 // Een post die een playlist insluit wijst in zijn AS2 ook naar de collectie901 // (shaer-ayc, stap 2): een Link-tag per ingesloten playlist. Mastodon902 // parseert alleen Mention/Hashtag/Emoji en negeert een Link geruisloos; een903 // client die hem kent haalt de collectie op. Opgelost uit post.content en904 // ALLEEN binnen de eigen site: playlist-ids zijn een globale primary key, dus905 // zonder site-check zou een post van site A naar de collectie van site B906 // kunnen wijzen.907 export function playlistLinkTags(base, site, content) {908 const out = [];909 const seen = new Set();910 try {911 for (const m of (content || '').matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) {912 if (seen.has(m[1])) continue;913 seen.add(m[1]);914 const pl = db.prepare('SELECT id, title FROM playlists WHERE id = ? AND site_id = ?').get(m[1], site.id);915 if (!pl) continue;916 out.push({ type: 'Link', href: `${actorId(base, site.slug)}/playlists/${pl.id}`, mediaType: 'application/activity+json', name: pl.title });917 }918 } catch { /* niet-fataal: een tag minder, geen kapotte Note */ }919 return out;920 }921 922 // De lijst van alle playlist-collecties van een site (shaer-ayc, stap 2).923 // Kaal standaard (URI's), verrijkt op verzoek (FEP-9876, zelfde conventie als924 // followers/following): een stub per playlist met naam, hoes en de EERLIJKE925 // telling -- totalItems van de stub telt het open deel, dezelfde regel als de926 // collectie zelf, want ook een lijst mag niet verklappen wat er achter de927 // poort staat.928 export function listPlaylistsAP(base, site, enriched) {929 const rows = db.prepare(930 'SELECT id, title, artist, year, cover_url FROM playlists WHERE site_id = ? ORDER BY created_at, id'931 ).all(site.id);932 const colId = `${actorId(base, site.slug)}/playlists`;933 const items = rows.map((p) => {934 const uri = `${actorId(base, site.slug)}/playlists/${p.id}`;935 if (!enriched) return uri;936 const stub = buildPlaylistCollection(base, site, p, playlistOpenTracks(p.id));937 delete stub['@context']; // genest object draagt de context van zijn omhulsel938 delete stub.orderedItems; // stub: wie de tracks wil, haalt de collectie op939 return stub;940 });941 return {942 '@context': AP_CONTEXT,943 id: colId,944 type: 'OrderedCollection',945 attributedTo: actorId(base, site.slug),946 totalItems: items.length,947 orderedItems: items,948 };949 }950 951 export function buildPlaylistCollection(base, site, playlist, rows) {952 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);953 // Zelfde afleiding als in buildNote; die daar is functie-lokaal.954 const mediaType = (u) => {955 const e = ((u || '').split('?')[0].match(/\.(\w+)$/) || [])[1];956 return ({ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif' })[(e || '').toLowerCase()] || 'image/jpeg';957 };958 const items = (rows || []).map((r) => {959 const fn = r.filename || (r.storage_path || '').split('/').pop();960 const a = {961 type: 'Audio',962 mediaType: r.mime_type || 'audio/mpeg',963 url: `${base}/audio/stream/${encodeURIComponent(fn)}`,964 name: r.title || 'Audio',965 };966 if (r.artist) a.summary = r.artist; // artiest als summary: kaal AS2, geen eigen vocab967 if (r.duration) a.duration = `PT${Math.round(r.duration)}S`;968 const art = abs(r.cover_url || playlist.cover_url || null);969 if (art) a.icon = { type: 'Image', mediaType: mediaType(art), url: art };970 return a;971 });972 const out = {973 '@context': AP_CONTEXT,974 id: `${actorId(base, site.slug)}/playlists/${playlist.id}`,975 type: 'OrderedCollection',976 name: playlist.title,977 attributedTo: actorId(base, site.slug),978 totalItems: items.length,979 orderedItems: items,980 };981 // Album of playlist is presentatie; op de draad is het één samenvattingsveld.982 const parts = [];983 if (playlist.artist) parts.push(playlist.artist);984 if (playlist.year) parts.push(String(playlist.year));985 if (parts.length) out.summary = parts.join(' · ');986 const cover = abs(playlist.cover_url || null);987 if (cover) out.icon = { type: 'Image', mediaType: mediaType(cover), url: cover };988 return out;989 870 } 990 871 … … 5537 5418 AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, 5538 5419 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured, 5539 buildPlaylistCollection, playlistOpenTracks, listPlaylistsAP, playlistLinkTags,5540 5420 followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins, 5541 5421 feedCursor, feedChangesSince, waitForFeedChange, -
test/activitypub-as2.test.js
r4b5db37 rda84110 32 32 // ActivityPub §5.6: the private blocked collection (owner-only GET). 33 33 'blocked', 34 // ActivityPub §4.1: supplementary collections on the actor — Klonkt wijst35 // ermee naar de playlist-lijst (shaer-ayc).36 'streams',37 34 // FEP-633c (Guardians): the owner-only dashboard queues on the actor; the 38 35 // sub-keys are the daemon-contract collection names the Shaer clients read.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)