Changeset 39a9d21 in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 08/08/2026 04:50:48 AM (5 weeks ago)
- Branches:
- main
- Children:
- fb22f78
- Parents:
- bdcb3a3
- git-author:
- Robin <roboburr@…> (08/08/2026 04:47:48 AM)
- git-committer:
- Robin <roboburr@…> (08/08/2026 04:50:48 AM)
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (11 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
rbdcb3a3 r39a9d21 179 179 // ── document builders ───────────────────────────────────────────── 180 180 export function actorId(base, slug) { return `${base}/ap/users/${encodeURIComponent(slug)}`; } 181 182 /** 183 * mediaType raden uit een bestandsnaam. Stond twee keer functie-lokaal in dit 184 * bestand, met een commentaar dat ze "dezelfde afleiding" waren -- en dat was 185 * niet zo: de ene kende video, de andere alleen beeld. Nu een kaart, hier. 186 * De terugval is image/jpeg omdat dit alleen op omslagen en bijlagen wordt 187 * losgelaten, nooit op geluid: dat draagt zijn eigen mime_type uit de database. 188 */ 189 export function guessMediaType(u) { 190 const e = ((u || '').split('?')[0].match(/\.(\w+)$/) || [])[1]; 191 return ({ 192 jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', 193 webp: 'image/webp', avif: 'image/avif', 194 mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', 195 })[(e || '').toLowerCase()] || 'image/jpeg'; 196 } 181 197 export function noteId(base, postId) { return `${base}/ap/notes/${encodeURIComponent(postId)}`; } 182 198 … … 311 327 // interest" -- precies wat de playlist-lijst is (shaer-ayc, stap 2). 312 328 // Geen eigen vocabulaire nodig, en wie het niet kent negeert het. 313 streams: [`${id}/ playlists`],329 streams: [`${id}/tracks`, `${id}/playlists`], 314 330 // AP §5.6: the private blocked collection (owner-only GET). The server 315 331 // list is the source of truth for Shaer's "in Orbit"; clients keep no … … 483 499 // to avoid duplicate rendering on clients that DO keep them. 484 500 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); 485 const mediaType = (u) => {486 const e = ((u || '').split('?')[0].match(/\.(\w+)$/) || [])[1];487 return ({ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif', mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime' })[(e || '').toLowerCase()] || 'image/jpeg';488 };489 501 const hadAudio = /\[\[(track|album|playlist):/i.test(post.content || ''); 490 502 const playable = hasPlayableAudio(post.content || '', site && site.id); … … 580 592 // Mastodon renders it as the artwork thumbnail on its native audio player. 581 593 const art = abs(r.cover_url || post.cover_image_url || null); 582 if (art) a.icon = { type: 'Image', mediaType: mediaType(art), url: art };594 if (art) a.icon = { type: 'Image', mediaType: guessMediaType(art), url: art }; 583 595 openAudio.push(a); 584 596 }; … … 635 647 const attachment = urls.filter((x) => x && x.url) 636 648 .filter((x) => { if (seen.has(x.url)) return false; seen.add(x.url); return true; }) 637 .map((x) => { const mt = x.mt || mediaType(x.url); // the stored type wins; the extension map is the fallback649 .map((x) => { const mt = x.mt || guessMediaType(x.url); // the stored type wins; the extension map is the fallback 638 650 const ty = /^image\//i.test(mt) ? 'Image' : /^video\//i.test(mt) ? 'Video' : /^audio\//i.test(mt) ? 'Audio' : 'Document'; 639 651 const a = { type: ty, mediaType: mt, url: x.url }; … … 684 696 if (post.cover_image_url && noImages) { 685 697 const cov = abs(post.cover_image_url); 686 if (cov) { note.image = { type: 'Image', mediaType: mediaType(cov), url: cov }; if (post.cover_alt) note.image.name = String(post.cover_alt).slice(0, 1500); }698 if (cov) { note.image = { type: 'Image', mediaType: guessMediaType(cov), url: cov }; if (post.cover_alt) note.image.name = String(post.cover_alt).slice(0, 1500); } 687 699 } 688 700 // Experiment (mirrors PeerTube / schema.org `embedUrl`): point at the GATED player page … … 1000 1012 // open deel: een eerlijke telling over wat er werkelijk in de collectie staat, 1001 1013 // niet over wat wij thuis in de kast hebben. 1014 const TRACK_KOLOMMEN = `t.id, t.title, t.artist, t.duration, t.cover_url, t.created_at, 1015 m.filename, m.storage_path, m.mime_type`; 1016 1002 1017 export function playlistOpenTracks(playlistId) { 1003 1018 return db.prepare( 1004 `SELECT t.title, t.artist, t.duration, t.cover_url, m.filename, m.storage_path, m.mime_type1019 `SELECT ${TRACK_KOLOMMEN} 1005 1020 FROM playlist_tracks pt 1006 1021 JOIN audio_tracks t ON t.id = pt.track_id … … 1009 1024 ORDER BY pt.position` 1010 1025 ).all(playlistId); 1026 } 1027 1028 /** 1029 * Alle tracks die deze site aan de federatie heeft opengezet (shaer-0nh, stap 3). 1030 * 1031 * Dit is de KANONIEKE plek, niet de playlist: een playlist is een keuze, dit is 1032 * wat de artiest heeft uitgebracht. Een track die in geen enkele playlist zit 1033 * was tot nu toe onzichtbaar voor de federatie -- die staat hier wel. 1034 */ 1035 export function siteOpenTracks(siteId) { 1036 return db.prepare( 1037 `SELECT ${TRACK_KOLOMMEN} 1038 FROM audio_tracks t JOIN media m ON m.id = t.media_id 1039 WHERE t.site_id = ? AND t.fedi_open = 1 1040 ORDER BY t.position, t.created_at, t.id` 1041 ).all(siteId); 1042 } 1043 1044 export function openTrack(siteId, trackId) { 1045 return db.prepare( 1046 `SELECT ${TRACK_KOLOMMEN} 1047 FROM audio_tracks t JOIN media m ON m.id = t.media_id 1048 WHERE t.site_id = ? AND t.id = ? AND t.fedi_open = 1` 1049 ).get(siteId, trackId); 1050 } 1051 1052 /** 1053 * Eén track als AS2 `Audio`, met een EIGEN id (shaer-0nh, stap 3). 1054 * 1055 * Waarom dat id het verschil maakt: zonder id is een track een naamloze bijlage 1056 * die alleen bestaat zolang je het omhullende object vasthoudt. Met id is het 1057 * een ding waar je naar kunt wijzen, dat je los kunt ophalen, en dat in twee 1058 * playlists hetzelfde ding is. Funkwhale adresseert zijn Audio-objecten 1059 * precies zo, per stuk, in Create en Delete. 1060 * 1061 * `url` is een Link-ARRAY, net als bij Funkwhale en net als wat onze eigen 1062 * inbox sinds bdcb3a3 verwacht: de mediaType hoort bij de link, niet bij het 1063 * object. Er zit GEEN text/html-link in: Klonkt heeft geen trackpagina -- een 1064 * track wordt getoond binnen een post, en een post over vijf nummers is niet de 1065 * pagina van dit ene nummer. Liever geen link dan een link die iets anders 1066 * belooft. 1067 */ 1068 export function buildTrackAudio(base, site, r, opts = {}) { 1069 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); 1070 const fn = r.filename || (r.storage_path || '').split('/').pop(); 1071 const a = { 1072 ...(opts.standalone ? { '@context': AP_CONTEXT } : {}), 1073 id: `${actorId(base, site.slug)}/tracks/${encodeURIComponent(r.id)}`, 1074 type: 'Audio', 1075 name: r.title || 'Audio', 1076 attributedTo: actorId(base, site.slug), 1077 url: [{ type: 'Link', href: `${base}/audio/stream/${encodeURIComponent(fn)}`, mediaType: r.mime_type || 'audio/mpeg' }], 1078 }; 1079 if (r.artist) a.summary = r.artist; // artiest als summary: kaal AS2, geen eigen vocab 1080 if (r.duration) a.duration = `PT${Math.round(r.duration)}S`; 1081 if (r.created_at) a.published = new Date(r.created_at).toISOString(); 1082 const art = abs(r.cover_url || opts.coverFallback || null); 1083 if (art) a.icon = { type: 'Image', mediaType: guessMediaType(art), url: art }; 1084 return a; 1085 } 1086 1087 /** De collectie van alle open tracks van een site (shaer-0nh, stap 3). */ 1088 export function buildTrackCollection(base, site, rows) { 1089 return { 1090 '@context': AP_CONTEXT, 1091 id: `${actorId(base, site.slug)}/tracks`, 1092 type: 'OrderedCollection', 1093 attributedTo: actorId(base, site.slug), 1094 totalItems: (rows || []).length, 1095 orderedItems: (rows || []).map((r) => buildTrackAudio(base, site, r)), 1096 }; 1011 1097 } 1012 1098 … … 1064 1150 export function buildPlaylistCollection(base, site, playlist, rows) { 1065 1151 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); 1066 // Zelfde afleiding als in buildNote; die daar is functie-lokaal. 1067 const mediaType = (u) => { 1068 const e = ((u || '').split('?')[0].match(/\.(\w+)$/) || [])[1]; 1069 return ({ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif' })[(e || '').toLowerCase()] || 'image/jpeg'; 1070 }; 1071 const items = (rows || []).map((r) => { 1072 const fn = r.filename || (r.storage_path || '').split('/').pop(); 1073 const a = { 1074 type: 'Audio', 1075 mediaType: r.mime_type || 'audio/mpeg', 1076 url: `${base}/audio/stream/${encodeURIComponent(fn)}`, 1077 name: r.title || 'Audio', 1078 }; 1079 if (r.artist) a.summary = r.artist; // artiest als summary: kaal AS2, geen eigen vocab 1080 if (r.duration) a.duration = `PT${Math.round(r.duration)}S`; 1081 const art = abs(r.cover_url || playlist.cover_url || null); 1082 if (art) a.icon = { type: 'Image', mediaType: mediaType(art), url: art }; 1083 return a; 1084 }); 1152 // Dezelfde objecten als in de actor-collectie, met hetzelfde id (shaer-0nh, 1153 // stap 3). Een playlist is een KEUZE uit wat de artiest heeft uitgebracht, 1154 // geen tweede exemplaar ervan: staat een track in twee playlists, dan is het 1155 // twee keer hetzelfde ding en niet twee dingen die toevallig gelijk klinken. 1156 // De hoes van de playlist dient als terugval voor een track zonder eigen hoes. 1157 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null })); 1085 1158 const out = { 1086 1159 '@context': AP_CONTEXT, … … 1098 1171 if (parts.length) out.summary = parts.join(' · '); 1099 1172 const cover = abs(playlist.cover_url || null); 1100 if (cover) out.icon = { type: 'Image', mediaType: mediaType(cover), url: cover };1173 if (cover) out.icon = { type: 'Image', mediaType: guessMediaType(cover), url: cover }; 1101 1174 return out; 1102 1175 } … … 5895 5968 AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, 5896 5969 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured, 5897 channelUrls, timelineFields, 5970 channelUrls, timelineFields, guessMediaType, 5971 siteOpenTracks, openTrack, buildTrackAudio, buildTrackCollection, 5898 5972 buildPlaylistCollection, playlistOpenTracks, listPlaylistsAP, playlistLinkTags, 5899 5973 followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
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)