Changeset abcf51a in Klonkt


Ignore:
Timestamp:
08/08/2026 04:55:00 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
4838760
Parents:
0261730
git-author:
Robin <roboburr@…> (08/08/2026 04:54:56 PM)
git-committer:
Robin <roboburr@…> (08/08/2026 04:55:00 PM)
Message:

Een track wijst naar de post die hem uitbrengt

Robins waarneming: Shaer toont sinds fb22f78 losse, niet-werkende
track-kaarten. Dat is mijn regressie -- ik zette Create(Audio) in de outbox
voor Funkwhale, en Shaer bouwt zijn HomeBase-feed uit precies die outbox.
noteFrom() daar kijkt niet naar type, dus een Audio wordt een C2SNote met lege
content, en onze url is een Link-array die as? String niet vangt.

Robins voorstel was beter dan wegfilteren: koppel ze aan de hostende post en
filter DAAROP. Die relatie bestond al, maar alleen in posts.content --
[[track:id]], [[playlist:id]], [[album:naam]] -- en nergens op de draad.

Nu wel, met twee velden die allebei al bestonden:

context AS2-KERN, "de context waarbinnen dit object bestaat". Wijst naar

de Note van de post. Funkwhale kijkt er niet naar, dus puur
additief.

url[0] de text/html-link naar de postpagina, vooraan zoals Funkwhale zijn

trackpagina zet. Dit veld lieten we bewust leeg omdat Klonkt geen
trackpagina heeft -- maar de post IS waar je het kunt horen.

Daarmee wordt de filterregel in Shaer principieel in plaats van bot: verberg
een Audio waarvan de context al in je feed staat. Een track die in GEEN post
zit houdt geen context en blijft zichtbaar -- dat is de enige plek waar je hem
zou zien.

Rangorde als een track op meerdere manieren is ingesloten: rechtstreeks boven
playlist boven albumnaam, en bij gelijke rang de nieuwste post. Specifieker
gaat dus voor recenter; daar staat een test op. Een concept telt niet als host.

EEN ZOEKOPDRACHT VOOR ALLES, niet per track: drie LIKE-scans maal
tweehonderd nummers is zeshonderd scans, en de collectie loopt over elke open
track. trackHostPosts levert een map, en die gaat als optie mee door de
collectie, de playlist en de outbox.

8 tests. context erbij in de AS2-allowlist -- het is een kernterm.

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

Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r0261730 rabcf51a  
    967967 * ophaalt niet denkt dat er iets nieuws is.
    968968 */
    969 export function buildTrackCreate(base, site, r) {
    970   const audio = buildTrackAudio(base, site, r);
     969export function buildTrackCreate(base, site, r, opts = {}) {
     970  const audio = buildTrackAudio(base, site, r, opts);
    971971  const me = actorId(base, site.slug);
    972972  return {
     
    10061006  const items = [
    10071007    ...(posts || []).map((p) => buildCreate(base, site, p)),
    1008     ...(tracks || []).map((r) => buildTrackCreate(base, site, r)),
     1008    // Eén zoekopdracht voor alle tracks samen, niet per stuk.
     1009    ...(() => {
     1010      const posts = (tracks || []).length && site.id ? trackHostPosts(site.id) : null;
     1011      return (tracks || []).map((r) => buildTrackCreate(base, site, r, { hostPosts: posts }));
     1012    })(),
    10091013  ]
    10101014    .sort((a, b) => wanneer(b) - wanneer(a))
     
    11301134 * belooft.
    11311135 */
     1136/**
     1137 * Bij welke post hoort een track? (shaer-0nh)
     1138 *
     1139 * Een track staat nooit los in Klonkt: hij wordt getoond BINNEN een post, via
     1140 * een van drie insluitingen in posts.content. Die relatie stond alleen in die
     1141 * tekst en nergens op de draad -- waardoor Shaer, dat zijn feed uit de outbox
     1142 * bouwt, sinds fb22f78 losse Audio-kaarten kreeg zonder inhoud.
     1143 *
     1144 * ALLES IN EEN ZOEKOPDRACHT, niet per track. De collectie loopt over elke open
     1145 * track, en drie LIKE-scans per stuk wordt bij tweehonderd nummers zeshonderd
     1146 * scans. Nu is het er een, en de map gaat mee als optie.
     1147 *
     1148 * De rang bepaalt welke post wint als er meerdere zijn: rechtstreeks ingesloten
     1149 * is specifieker dan via een playlist, en die weer specifieker dan via een
     1150 * albumnaam. Bij gelijke rang de nieuwste post -- dat is waar iemand hem het
     1151 * laatst heeft uitgebracht.
     1152 */
     1153export function trackHostPosts(siteId) {
     1154  const rijen = db.prepare(`
     1155    SELECT tid, post_id, post_slug, rang, wanneer FROM (
     1156      SELECT t.id AS tid, p.id AS post_id, p.slug AS post_slug, 1 AS rang,
     1157             COALESCE(p.published_at, p.created_at) AS wanneer
     1158        FROM audio_tracks t
     1159        JOIN posts p ON p.site_id = t.site_id AND p.status = 'published'
     1160                    AND p.content LIKE '%[[track:' || t.id || ']]%'
     1161       WHERE t.site_id = ? AND t.fedi_open = 1
     1162      UNION ALL
     1163      SELECT t.id, p.id, p.slug, 2, COALESCE(p.published_at, p.created_at)
     1164        FROM playlist_tracks pt
     1165        JOIN audio_tracks t ON t.id = pt.track_id
     1166        JOIN posts p ON p.site_id = t.site_id AND p.status = 'published'
     1167                    AND p.content LIKE '%[[playlist:' || pt.playlist_id || ']]%'
     1168       WHERE t.site_id = ? AND t.fedi_open = 1
     1169      UNION ALL
     1170      SELECT t.id, p.id, p.slug, 3, COALESCE(p.published_at, p.created_at)
     1171        FROM audio_tracks t
     1172        JOIN posts p ON p.site_id = t.site_id AND p.status = 'published'
     1173                    AND p.content LIKE '%[[album:' || t.album || ']]%'
     1174       WHERE t.site_id = ? AND t.fedi_open = 1 AND t.album IS NOT NULL AND t.album <> ''
     1175    ) ORDER BY rang, wanneer DESC
     1176  `).all(siteId, siteId, siteId);
     1177  const uit = new Map();
     1178  for (const r of rijen) if (!uit.has(r.tid)) uit.set(r.tid, { id: r.post_id, slug: r.post_slug });
     1179  return uit;
     1180}
     1181
    11321182export function buildTrackAudio(base, site, r, opts = {}) {
    11331183  const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
     
    11361186  // representatie die zoveel bytes is en die bitrate heeft, niet het nummer.
    11371187  // Zo doet Funkwhale het ook.
     1188  // De post waar dit nummer in staat. Meegegeven door de collectie (een
     1189  // zoekopdracht voor alles), of hier opgezocht als deze track los wordt
     1190  // opgehaald. `hostPosts` mag expliciet null zijn: dan is er niets te zoeken.
     1191  const post = opts.hostPosts !== undefined
     1192    ? (opts.hostPosts && opts.hostPosts.get(r.id)) || null
     1193    : ((site.id && trackHostPosts(site.id).get(r.id)) || null);
     1194
    11381195  const bestand = { type: 'Link', href: `${base}/audio/stream/${encodeURIComponent(fn)}`, mediaType: r.mime_type || 'audio/mpeg' };
    11391196  if (Number(r.size)) bestand.size = Number(r.size);
     
    11521209    // track moet zelf kunnen zeggen dat hij openbaar is.
    11531210    to: [PUBLIC],
    1154     url: [bestand],
     1211    // De post die dit nummer uitbrengt staat VOORAAN als text/html, precies
     1212    // zoals Funkwhale zijn trackpagina zet. Wij hadden dat veld leeg gelaten
     1213    // omdat Klonkt geen trackpagina heeft -- maar de post IS waar je het kunt
     1214    // horen, en dat is wat zo'n link betekent.
     1215    url: [...(post ? [{ type: 'Link', href: `${base}/${post.slug}`, mediaType: 'text/html' }] : []), bestand],
    11551216  };
    11561217  if (r.artist) a.summary = r.artist;              // artiest als summary: kaal AS2, geen eigen vocab
     1218  // AS2-kern `context`: "de context waarbinnen dit object bestaat". Voor een
     1219  // track is dat de post die hem uitbrengt. Daarmee is de relatie die tot nu
     1220  // toe alleen in posts.content stond, op de draad te zien -- en kan een lezer
     1221  // die de post al heeft dit nummer overslaan in plaats van er een lege kaart
     1222  // van te maken.
     1223  if (post) a.context = noteId(base, post.id);
    11571224  if (r.duration) a.duration = `PT${Math.round(r.duration)}S`;
    11581225  if (r.created_at) a.published = new Date(r.created_at).toISOString();
     
    12011268    attributedTo: actorId(base, site.slug),
    12021269    totalItems: (rows || []).length,
    1203     orderedItems: (rows || []).map((r) => buildTrackAudio(base, site, r)),
     1270    // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts.
     1271    orderedItems: (() => {
     1272      const posts = site.id ? trackHostPosts(site.id) : null;
     1273      return (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts }));
     1274    })(),
    12041275  };
    12051276}
     
    12631334  // twee keer hetzelfde ding en niet twee dingen die toevallig gelijk klinken.
    12641335  // De hoes van de playlist dient als terugval voor een track zonder eigen hoes.
    1265   const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null }));
     1336  const hostPosts = site.id ? trackHostPosts(site.id) : null;
     1337  const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts }));
    12661338  const out = {
    12671339    '@context': AP_CONTEXT,
     
    61896261  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
    61906262  channelUrls, channelCategory, timelineFields, guessMediaType,
    6191   siteOpenTracks, openTrack, buildTrackAudio, buildTrackCollection, buildTrackCreate,
     6263  siteOpenTracks, openTrack, buildTrackAudio, buildTrackCollection, buildTrackCreate, trackHostPosts,
    61926264  buildPlaylistCollection, playlistOpenTracks, listPlaylistsAP, playlistLinkTags,
    61936265  followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
  • test/activitypub-as2.test.js

    r0261730 rabcf51a  
    2626  'content', 'name', 'summary', 'url', 'href', 'mediaType',
    2727  'published', 'updated', 'attributedTo', 'inReplyTo', 'replies',
     28  // AS2-kern: de context waarbinnen een object bestaat. Een track wijst ermee
     29  // naar de post die hem uitbrengt (shaer-0nh).
     30  'context',
    2831  'attachment', 'tag', 'icon', 'image', 'duration',
    2932  'contentMap', 'nameMap', 'summaryMap', // AS2 @language-map counterparts of content/name/summary
Note: See TracChangeset for help on using the changeset viewer.