Changeset 47ca083 in Klonkt for src/routes/feed.js


Ignore:
Timestamp:
08/08/2026 05:06:43 AM (5 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
2220c49
Parents:
fb22f78
git-author:
Robin <roboburr@…> (08/08/2026 05:05:39 AM)
git-committer:
Robin <roboburr@…> (08/08/2026 05:06:43 AM)
Message:

De feed draagt de muziek, niet alleen de wegwijzer ernaartoe

De actor adverteert /feed.xml als kanaal-feed sinds bdcb3a3. Maar die feed was
een blogfeed: twee items, NUL enclosures. Een podcast-app zoekt precies op
<enclosure>, dus voor hem was hij leeg hoe veel items er ook in stonden. Ik had
de wegwijzer gebouwd zonder te kijken waar hij heen wees.

Elke opengezette track is nu een eigen item met een enclosure: url, lengte in
bytes uit media.size, en het echte mime-type. Plus itunes:duration, want een
lezer die een lengte kan tonen hoort er een te krijgen.

EEN ITEM PER TRACK, en dat is geen stijlkeuze: RSS 2.0 staat maar EEN enclosure
per item toe. Een album in een item proppen zou van vijf nummers er vier laten
verdwijnen. Dezelfde tweedeling als in de outbox -- de post is het bericht, de
track is de publicatie -- en ze staan door elkaar op datum.

Geen <link> op een track-item: Klonkt heeft geen trackpagina, en een post over
vijf nummers is niet de pagina van dit ene nummer. De guid is daarom geen
permalink maar de stabiele AP-id van de track, zodat een lezer een track
herkent die hij al kende.

Welke tracks open zijn beslist siteOpenTracks in de AP-service, niet deze
route: die poortregel hoort op een plek te staan, en de test bewaakt dat een
gesloten track titel noch bestandsnaam lekt.

6 tests.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/feed.js

    rfb22f78 r47ca083  
    1212import express from 'express';
    1313import db from '../config/database.js';
     14import { siteOpenTracks } from '../services/ActivityPubService.js';
    1415
    1516const router = express.Router();
     
    5051  const base = origin + (res.locals.siteUrlBase || '');
    5152  const posts = postsForFeed(site.id);
     53
     54  // De tracks die deze site aan de federatie heeft opengezet, elk als eigen
     55  // item met een <enclosure> (shaer-0nh). Dat laatste is wat een podcast-app
     56  // zoekt: zonder enclosure is een feed voor hem leeg, hoe veel items er ook
     57  // in staan -- en de actor adverteert deze feed nu juist als kanaal-feed.
     58  //
     59  // EEN ITEM PER TRACK, want RSS 2.0 staat maar EEN enclosure per item toe.
     60  // Een album in een item proppen zou betekenen dat er van vijf nummers vier
     61  // verdwijnen. Dezelfde keuze als in de outbox: de post is het bericht, de
     62  // track is de publicatie.
     63  //
     64  // Welke tracks open zijn beslist de AP-service, niet deze route: dat is een
     65  // poortregel en die hoort op een plek te staan.
     66  const tracks = siteOpenTracks(site.id);
    5267  const lastBuild = posts[0]?.published_at || new Date().toISOString();
     68
     69  const wanneer = (d) => { const t = Date.parse(d); return Number.isNaN(t) ? 0 : t; };
     70  const items = [
     71    ...posts.map((p) => ({ op: wanneer(p.published_at), xml: `    <item>
     72      <title>${escapeXml(p.title || '(untitled)')}</title>
     73      <link>${escapeXml(base + '/' + p.slug)}</link>
     74      <guid isPermaLink="true">${escapeXml(base + '/' + p.slug)}</guid>
     75      <pubDate>${new Date(p.published_at).toUTCString()}</pubDate>
     76      <author>${escapeXml((p.author_email || 'noreply@localhost') + ' (' + p.author_username + ')')}</author>
     77      <description>${escapeXml(p.excerpt || '')}</description>
     78    </item>` })),
     79    ...tracks.map((t) => {
     80      const fn = t.filename || (t.storage_path || '').split('/').pop();
     81      // Geen <link>: Klonkt heeft geen trackpagina, en een post over vijf
     82      // nummers is niet de pagina van dit ene nummer. De guid is daarom geen
     83      // permalink maar de stabiele AP-id van de track.
     84      return { op: wanneer(t.created_at), xml: `    <item>
     85      <title>${escapeXml(t.title || 'Audio')}</title>
     86      <guid isPermaLink="false">${escapeXml(`${base}/ap/users/${encodeURIComponent(site.slug)}/tracks/${encodeURIComponent(t.id)}`)}</guid>
     87      <pubDate>${new Date(t.created_at || Date.now()).toUTCString()}</pubDate>
     88      <description>${escapeXml(t.artist || '')}</description>
     89      <enclosure url="${escapeXml(`${base}/audio/stream/${encodeURIComponent(fn)}`)}" length="${Number(t.size) || 0}" type="${escapeXml(t.mime_type || 'audio/mpeg')}" />${t.duration ? `
     90      <itunes:duration>${Math.round(t.duration)}</itunes:duration>` : ''}
     91    </item>` };
     92    }),
     93  ].sort((a, b) => b.op - a.op).map((x) => x.xml).join('\n');
    5394
    5495  res.set('Content-Type', 'application/rss+xml; charset=utf-8');
    5596  res.send(`<?xml version="1.0" encoding="UTF-8"?>
    56 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
     97<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
    5798  <channel>
    5899    <title>${escapeXml(site.title)}</title>
     
    62103    <lastBuildDate>${new Date(lastBuild).toUTCString()}</lastBuildDate>
    63104    <atom:link href="${escapeXml(base + '/feed.xml')}" rel="self" type="application/rss+xml" />
    64 ${posts.map(p => `    <item>
    65       <title>${escapeXml(p.title || '(untitled)')}</title>
    66       <link>${escapeXml(base + '/' + p.slug)}</link>
    67       <guid isPermaLink="true">${escapeXml(base + '/' + p.slug)}</guid>
    68       <pubDate>${new Date(p.published_at).toUTCString()}</pubDate>
    69       <author>${escapeXml((p.author_email || 'noreply@localhost') + ' (' + p.author_username + ')')}</author>
    70       <description>${escapeXml(p.excerpt || '')}</description>
    71     </item>`).join('\n')}
     105${items}
    72106  </channel>
    73107</rss>`);
Note: See TracChangeset for help on using the changeset viewer.