source: Klonkt/test/feed-tracks-only.test.js@ 59d963f

main
Last change on this file since 59d963f was 59d963f, checked in by Robin <roboburr@…>, 4 weeks ago

De hele site als muziekkanaal: /tracks.xml (Robins vraag, 10-8)

Hij vroeg om een "hele site, open tracks"-library, in de hoop dat Funkwhale er
iets mee zou kunnen. Als AS2-object doet zo'n ding daar niets: een
Funkwhale-KANAAL (federation/actors/tnd, opgehaald en nagelopen) noemt nergens
een library, en hun library is een eigen actor met inbox en sleutel -- de optie
die op 7 augustus bewust is afgewezen.

Maar de vorm waarin Funkwhale een kanaal WEL uitgeeft is RSS 2.0 met de
iTunes-namespace en een enclosure per item. Naast onze feed gelegd:

Funkwhale 1 item, 1 enclosure audio/mpeg itunes + atom
Klonkt 8 items, 4 enclosures audio/mpeg itunes + atom

Dezelfde vorm dus, en die had Klonkt al. Het enige verschil was dat onze feed
ook gewone posts draagt, en een muziekkanaal met blogberichten ertussen is er
geen. Vandaar een tweede ingang op dezelfde bouwer in plaats van een nieuw
object: /tracks.xml, alleen de open tracks.

Twee dingen die geen detail zijn. De self-link wijst naar ZICHZELF -- anders
stuurt een lezer die opnieuw ophaalt zichzelf naar de gemengde variant en staan
de posts er alsnog in. En de titel krijgt " -- muziek" erachter, want twee
feeds met dezelfde naam zijn in een podcast-app niet te scheiden.

Wat NIET verandert: de actor blijft /feed.xml adverteren. Die is door Apple
Podcasts gevalideerd en werkt; een tweede rss-link erbij zou een podcast-app
laten kiezen tussen twee kanaalfeeds. /tracks.xml is een URL om te plakken.

En dicht blijft dicht: dezelfde poortregel als overal, siteOpenTracks beslist.

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

  • Property mode set to 100644
File size: 5.0 KB
Line 
1// /tracks.xml -- de hele site als muziekkanaal (Robins vraag, 10-8).
2//
3// Hij vroeg naar een "hele site, open tracks"-library voor Funkwhale. Als
4// AS2-object helpt dat daar niet: een Funkwhale-kanaal wijst nergens naar een
5// library. Maar de vorm waarin Funkwhale een kanaal WEL uitgeeft is RSS met
6// iTunes en een enclosure per item, en dat doet Klonkt al -- alleen droeg die
7// feed ook gewone posts, en een muziekkanaal met blogberichten ertussen is er
8// geen. Vandaar een tweede ingang op dezelfde bouwer.
9//
10// Per ITEM lezen en niet met een patroon over het hele document: dat laatste
11// telt een titel in de kanaalkop net zo hard mee als een titel in een item, en
12// dan meet de test iets anders dan hij beweert.
13import { test } from 'node:test';
14import assert from 'node:assert/strict';
15
16process.env.DATABASE_PATH = ':memory:';
17process.env.PUBLIC_BASE_URL = 'https://test.example';
18
19const dbMod = await import('../src/config/database.js');
20const db = dbMod.default;
21dbMod.initializeDatabase();
22const express = (await import('express')).default;
23const feed = (await import('../src/routes/feed.js')).default;
24
25db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
26 .run('u1', 'zanger', 'zanger@t', 'x', 'god');
27db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
28 .run('s1', 'band', 'De Band', 'u1');
29
30db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, excerpt, content, status, published_at)
31 VALUES (?,?,?,?,?,?,?,'published',?)`)
32 .run('p1', 's1', 'u1', 'een-blog', 'Een blogbericht', 'kort', '<p>x</p>', '2026-01-01T00:00:00Z');
33
34const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,?)');
35const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, fedi_open, created_at) VALUES (?,?,?,?,?,?,?,?)');
36insM.run('m1', 's1', 'open.mp3', 'audio/open.mp3', 'audio/mpeg', 4210688);
37insM.run('m2', 's1', 'dicht.mp3', 'audio/dicht.mp3', 'audio/mpeg', 999);
38insT.run('t1', 's1', 'Het open nummer', 'De Band', 203, 'm1', 1, '2026-03-01 12:00:00');
39insT.run('t2', 's1', 'Achter de poort', 'De Band', 100, 'm2', 0, '2026-04-01 12:00:00');
40
41const app = express();
42app.use((req, res, next) => {
43 res.locals.site = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
44 res.locals.siteUrlBase = '';
45 next();
46});
47app.use(feed);
48const server = app.listen(0);
49await new Promise((r) => server.once('listening', r));
50const poort = server.address().port;
51const haal = async (pad) => (await fetch(`http://127.0.0.1:${poort}${pad}`)).text();
52
53/** Splitsen op <item>-grenzen en dan per item op tagniveau lezen, in plaats
54 * van een patroon over het hele document laten lopen: dat laatste telt een
55 * titel in de kop net zo hard mee als een titel in een item. */
56function items(xml) {
57 return [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)].map((m) => m[1]);
58}
59function tekst(blok, tag) {
60 const m = blok.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`));
61 return m ? m[1] : null;
62}
63
64const muziek = await haal('/tracks.xml');
65const alles = await haal('/feed.xml');
66
67test('de muziekfeed draagt ALLEEN tracks, geen blogberichten', () => {
68 const it = items(muziek);
69 assert.equal(it.length, 1, 'een open track, en de post hoort er niet in');
70 assert.equal(tekst(it[0], 'title'), 'Het open nummer');
71 assert.ok(!muziek.includes('Een blogbericht'), 'de post staat er niet in');
72});
73
74test('en de gewone feed draagt ze allebei nog steeds', () => {
75 const it = items(alles);
76 assert.equal(it.length, 2, 'de sitefeed verandert niet: post en track');
77 assert.ok(alles.includes('Een blogbericht'));
78});
79
80test('een gesloten track blijft ook hier weg -- dicht is dicht', () => {
81 assert.ok(!muziek.includes('Achter de poort'));
82 assert.ok(!muziek.includes('dicht.mp3'));
83});
84
85test('elke item heeft de enclosure die een podcast-lezer zoekt', () => {
86 const enc = muziek.match(/<enclosure[^>]*>/g) || [];
87 assert.equal(enc.length, 1);
88 assert.match(enc[0], /url="[^"]*\/audio\/stream\/open\.mp3"/);
89 assert.match(enc[0], /length="4210688"/);
90 assert.match(enc[0], /type="audio\/mpeg"/);
91});
92
93test('de self-link wijst naar ZICHZELF en niet naar de sitefeed', () => {
94 // Anders stuurt een lezer die de feed opnieuw ophaalt zichzelf naar de
95 // gemengde variant, en staan de blogberichten er alsnog in.
96 assert.match(muziek, /<atom:link href="[^"]*\/tracks\.xml" rel="self"/);
97 assert.ok(!/<atom:link href="[^"]*\/feed\.xml" rel="self"/.test(muziek));
98 assert.match(alles, /<atom:link href="[^"]*\/feed\.xml" rel="self"/);
99});
100
101test('en hij heet anders, want twee gelijknamige feeds zijn niet te scheiden', () => {
102 assert.ok(muziek.includes('De Band — muziek'));
103 assert.ok(alles.includes('<title>De Band</title>'));
104});
105
106test('de iTunes-categorie staat er, want er is echt muziek', () => {
107 assert.match(muziek, /<itunes:category text="Music" \/>/);
108 assert.match(muziek, /<itunes:duration>203<\/itunes:duration>/);
109});
110
111test.after(() => server.close());
Note: See TracBrowser for help on using the repository browser.