Changeset da84110 in Klonkt for src/services
- Timestamp:
- 08/07/2026 06:46:34 PM (5 weeks ago)
- Branches:
- main
- Children:
- 3e1aab1, a8364ee
- Parents:
- 4b5db37
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
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,
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)