source: Klonkt/test/archive-audio.test.js@ 6c4ff7e

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

Audio verhuisde niet mee: archiefformaat v2

Robin verhuisde naar soundfabrics.nl en meldde: de berichten zijn er, de
plaatjes zijn er, een deel van de audio staat er maar speelt niet af, en
daarnaast mist er veel. Drie losse oorzaken, alle drie echt.

Gemeten op de twee live instanties:

berichten 36 -> 36
tracks 140 -> 14, waarvan 13 stuk
playlists 11 -> 0

DE EXPORTER LAS storage_path, DE SPELER NIET. routes/audio.js resolvet met
AUDIO_DIR plus bestandsnaam en kijkt niet naar media.storage_path. Op
sound-fabrics.com wees storage_path voor 124 van de 139 audiorijen nog naar
/srv/prutfolio/storage/audio, van voor de dataverhuizing. De site speelde
gewoon door; de exporter vond niets, markeerde availability: missing, en liet
124 nummers stil achter. Vanuit de exporter gezien was het bestand er echt niet,
dus er kwam ook geen klacht. Nu is er een resolveAudioPath die zoekt zoals de
speler zoekt.

DE IMPORTER MAAKTE EEN TRACK ZONDER BESTAND. Bij gewone bijlagen werd
availability: missing netjes gecontroleerd, in de audio-tak eronder niet. Die
zette een media-rij met size 0 en een audio_tracks-rij neer voor bytes die nooit
zijn weggeschreven. Dat is erger dan ontbreken: het ziet eruit alsof de
verhuizing gelukt is, dus je zegt je oude instantie op. Nu geldt er een harde
regel: geen bestand, geen track.

AUDIO PASTE NIET IN HET FORMAAT. Gehoste audio staat buiten MEDIA_ROOT, achter
een eigen gated route, en het archief droeg alleen bestanden onder media/. De
exporter rekende er met path.relative een /media/../audio/x.mp3 van, en
veiligMediaPad weigert dat pad terecht. Audio kon dus principieel niet
aankomen. Daarbovenop ging alleen mee wat met [[track:]] in een bericht stond,
14 van de 140, en playlists helemaal niet.

De ophaalknop leerde audio erbij. Dat vroeg meer dan verwacht: de bron toont
over AP alleen fedi_open-nummers, en de bestanden zitten achter dezelfde poort.
Beide staan nu open voor precies de doel-actor van de Move, en de ingest haalt
de bytes ONDERTEKEND op. De eerste poging gaf drie keer "nummer niet opgehaald",
want haalBijlage deed een kale fetch en kreeg een 403 van onze eigen poort.

Bewezen op twee draaiende instanties, met een track die opzettelijk een
verouderd pad had. Zip: 3/3 included, playlist-volgorde intact, bestanden op
schijf met de goede grootte, /audio/stream geeft 200. Ophaalknop: 5 berichten
plus 3 tracks met echte bytes, ook de gesloten nummers.

Changed files:
src/config/paths.js

  • AUDIO_ROOT en resolveAudioPath(): een audiobestand zoeken zoals de speler het zoekt, met het opgeslagen pad als terugval

src/services/ArchiveExportService.js

  • FORMAT_VERSION 2
  • audioBibliotheek(): de HELE tracklijst plus de playlists, met de bestanden in een eigen audio/-gebied
  • [[track:]] weg uit de media-tak; die kon nooit aankomen
  • audioOf() wijst naar het archiefpad in plaats van naar een bijlage

src/services/ArchiveImportService.js

  • tracksTerug() en playlistsTerug(); de per-post audio-tak is weg
  • bestanden naar AUDIO_ROOT, en geen rij zonder bestand
  • een playlist verwijst nooit naar een nummer dat niet aankwam
  • nieuwe rapportvelden tracks / tracksMissing / playlists

src/services/MigrationService.js

  • de ingest loopt de tracks-collectie af en haalt de bytes ondertekend op
  • haalBijlage kreeg submap en headers

src/services/ActivityPubService.js

  • signedGetHeaders() losgetrokken uit signedGetJson, want een verhuizing moet ook bytes kunnen ophalen

src/services/music/index.js

  • siteOpenTracks({ alles }): de doel-actor ziet ook de gesloten nummers

src/routes/audio.js

  • isMoveTargetAudio(): een ondertekend verzoek van de doel-actor mag alle audio ophalen, ook wat niet fedi_open is

src/routes/activitypub.js

  • magAlles(): een plek voor de vraag "mag deze aanvrager alles zien"

src/routes/admin-migrate.js

  • AUDIO_ROOT en signedGetHeaders doorgegeven aan de ingest

test/archive-export.test.js

  • de audio-testen naar het v2-contract, plus bibliotheek en ontbrekend
  • resolveAudioPath getoetst op VOLGORDE met een nep-fs

New file:
test/archive-audio.test.js

  • de hele rondgang met audio, inclusief het verouderde pad, de playlist- volgorde en de droogloop

remarks: soundfabrics.nl staat nu met 13 lege tracks; die verdwijnen niet
vanzelf bij een nieuwe import. En een archief van voor deze commit is v1 en
bevat de audio niet, dus opnieuw exporteren is nodig. Suite 985 groen;
tegenproef gedaan op de resolver en op de geen-bestand-geen-track-regel.

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

  • Property mode set to 100644
File size: 9.0 KB
Line 
1// Audio overleeft een verhuizing. Formaat v2.
2//
3// Dit bestand bestaat door wat er op soundfabrics.nl gebeurde: 36 berichten
4// kwamen goed over, 14 van de 140 nummers stonden er, en 13 daarvan speelden
5// niet af. Drie oorzaken, alle drie hier vastgelegd:
6//
7// 1. Gehoste audio leeft BUITEN MEDIA_ROOT (eigen gated route). Het archief
8// droeg alleen bestanden onder media/, dus de exporter rekende er een
9// /media/../audio/x.mp3 van en de importer weigerde dat pad. Terecht: het
10// wijst buiten de mediamap. Gevolg was wel dat audio nooit kon aankomen.
11// 2. De importer maakte een audio_tracks-rij aan ZONDER te kijken of het
12// bestand er was. Een nummer dat in de lijst staat en 404't bij play is
13// erger dan een nummer dat ontbreekt: het ziet eruit alsof het gelukt is,
14// dus je gooit je oude instantie weg.
15// 3. Alleen audio die met [[track:]] in een bericht stond ging mee. Losse
16// nummers en playlists bleven achter (126 respectievelijk 11 stuks).
17//
18// Een verhuizing die je bibliotheek achterlaat is geen verhuizing.
19
20import { test } from 'node:test';
21import assert from 'node:assert/strict';
22import fs from 'fs';
23import os from 'os';
24import path from 'path';
25
26const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'klonkt-audio-'));
27const MEDIA = path.join(TMP, 'media');
28const AUDIO = path.join(TMP, 'audio');
29fs.mkdirSync(MEDIA, { recursive: true });
30fs.mkdirSync(AUDIO, { recursive: true });
31
32process.env.DATABASE_PATH = ':memory:';
33process.env.PUBLIC_BASE_URL = 'https://oud.test';
34process.env.MEDIA_PATH = MEDIA;
35process.env.AUDIO_PATH = AUDIO; // MOET voor de import van paths.js staan
36
37const dbMod = await import('../src/config/database.js');
38const db = dbMod.default;
39{ const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
40const AX = await import('../src/services/ArchiveExportService.js');
41const AI = await import('../src/services/ArchiveImportService.js');
42const { AUDIO_ROOT } = await import('../src/config/paths.js');
43
44db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
45 .run('u1', 'u1', 'u1@test', 'x', 'god');
46db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Mijn site', 'u1');
47
48const BYTES = (s) => Buffer.from(`RIFF${s}`.padEnd(64, '.'));
49
50/** Schone lei: tabellen leeg en de audiomap leeg. Tests lekken anders in elkaar. */
51function leeg() {
52 for (const t of ['playlist_tracks', 'playlists', 'audio_tracks', 'media']) {
53 try { db.prepare(`DELETE FROM ${t}`).run(); } catch { /* tabel bestaat niet */ }
54 }
55 for (const f of fs.readdirSync(AUDIO)) { try { fs.unlinkSync(path.join(AUDIO, f)); } catch { /* niets */ } }
56}
57
58/** Een nummer met zijn bestand op de plek waar de SPELER het zoekt. */
59function track(id, titel, { padInDb = null, schrijf = true } = {}) {
60 const naam = `${id}.mp3`;
61 const echt = path.join(AUDIO, naam);
62 if (schrijf) fs.writeFileSync(echt, BYTES(id));
63 db.prepare('INSERT OR REPLACE INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)')
64 .run(`m-${id}`, 's1', naam, 'audio/mpeg', 1, padInDb || echt);
65 db.prepare('INSERT OR REPLACE INTO audio_tracks (id, site_id, title, artist, media_id) VALUES (?,?,?,?,?)')
66 .run(id, 's1', titel, 'Robin', `m-${id}`);
67 return echt;
68}
69
70test('een nummer met een VEROUDERD pad in de database gaat gewoon mee', () => {
71 leeg();
72 // Dit is de stille moordenaar. Op sound-fabrics.com wees storage_path voor
73 // 124 van de 139 nummers nog naar /srv/prutfolio/storage/audio, van voor een
74 // dataverhuizing. De speler merkte niets (die zoekt op bestandsnaam in
75 // AUDIO_ROOT), de exporter vond niets en liet ze weg. Zonder klacht.
76 track('oudpad', 'Verouderd pad', { padInDb: '/srv/ergens/anders/oudpad.mp3' });
77 const uit = AX.buildArchive('me');
78 const rij = JSON.parse(uit.files.get('tracks.json').toString('utf8')).orderedItems.find((x) => x.id === 'oudpad');
79 assert.equal(rij['shaer:availability'], 'included',
80 'de exporter hoort te zoeken zoals de speler zoekt, niet zoals de database onthoudt');
81 assert.deepEqual(uit.files.get(rij['shaer:file']), BYTES('oudpad'));
82});
83
84test('de hele rondgang: exporteren, importeren, en het geluid staat er', () => {
85 leeg();
86 track('a', 'Nummer A');
87 track('b', 'Nummer B');
88 const uit = AX.buildArchive('me');
89 assert.equal(uit.counts.tracks, 2);
90
91 // Een LEGE tweede installatie, met zijn eigen mappen.
92 const TMP2 = fs.mkdtempSync(path.join(os.tmpdir(), 'klonkt-audio2-'));
93 const doelAudio = path.join(TMP2, 'audio');
94 fs.mkdirSync(doelAudio, { recursive: true });
95
96 // AUDIO_ROOT ligt vast bij import, dus we importeren in DEZELFDE map en
97 // controleren daarna dat het bestand er staat onder de naam uit het archief.
98 leeg();
99 assert.equal(fs.readdirSync(AUDIO).length, 0, 'schone lei');
100
101 const rapport = AI.importArchive(uit.files, { slug: 'me', origin: 'https://nieuw.test' });
102 assert.equal(rapport.tracks, 2, 'beide nummers komen terug');
103 assert.equal(rapport.tracksMissing, 0);
104
105 const rijen = db.prepare('SELECT t.id, t.title, m.storage_path, m.size FROM audio_tracks t JOIN media m ON m.id = t.media_id ORDER BY t.id').all();
106 assert.equal(rijen.length, 2);
107 for (const r of rijen) {
108 assert.ok(fs.existsSync(r.storage_path), `${r.title}: het bestand hoort er ECHT te staan`);
109 assert.ok(r.size > 0, 'en de grootte klopt, geen 0 zoals bij de kapotte import');
110 assert.ok(r.storage_path.startsWith(path.resolve(AUDIO_ROOT) + path.sep),
111 'audio hoort in AUDIO_ROOT, niet onder de publieke mediamap');
112 }
113 fs.rmSync(TMP2, { recursive: true, force: true });
114});
115
116test('een nummer zonder bestand wordt NIET aangemaakt', () => {
117 // De kern van de klacht: 13 nummers die bestonden en niet speelden. Liever
118 // eerlijk weg dan zichtbaar kapot.
119 leeg();
120 track('zoek', 'Zoekgeraakt', { padInDb: '/bestaat/echt/niet.mp3', schrijf: false });
121 const uit = AX.buildArchive('me');
122 assert.equal(uit.counts.audioMissing, 1);
123
124 leeg();
125 const rapport = AI.importArchive(uit.files, { slug: 'me', origin: 'https://nieuw.test' });
126 assert.equal(rapport.tracks, 0);
127 assert.equal(rapport.tracksMissing, 1);
128 assert.equal(db.prepare('SELECT COUNT(*) n FROM audio_tracks').get().n, 0,
129 'geen rij zonder geluid: dat is precies de val die soundfabrics.nl opleverde');
130 assert.ok(rapport.waarschuwingen.some((w) => /Zoekgeraakt/.test(w)),
131 'en de gebruiker hoort te weten welk nummer er niet is');
132});
133
134test('playlists komen mee, met hun volgorde', () => {
135 leeg();
136
137 track('p1', 'Eerste');
138 track('p2', 'Tweede');
139 track('p3', 'Derde');
140 db.prepare("INSERT INTO playlists (id, site_id, title, artist, kind) VALUES ('pl1','s1','Mijn plaat','Robin','album')").run();
141 // Bewust NIET op alfabet: de volgorde is de hele playlist.
142 db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('pl1','p3',0),('pl1','p1',1),('pl1','p2',2)").run();
143
144 const uit = AX.buildArchive('me');
145 assert.equal(uit.counts.playlists, 1);
146
147 leeg();
148
149 const rapport = AI.importArchive(uit.files, { slug: 'me', origin: 'https://nieuw.test' });
150 assert.equal(rapport.playlists, 1);
151 const pl = db.prepare("SELECT * FROM playlists WHERE id = 'pl1'").get();
152 assert.equal(pl.title, 'Mijn plaat');
153 assert.equal(pl.kind, 'album');
154 const volgorde = db.prepare("SELECT track_id FROM playlist_tracks WHERE playlist_id = 'pl1' ORDER BY position").all().map((r) => r.track_id);
155 assert.deepEqual(volgorde, ['p3', 'p1', 'p2'], 'de volgorde IS de playlist');
156});
157
158test('een playlist verwijst nooit naar een nummer dat niet aankwam', () => {
159 // Anders staat er een plaat met gaten erin die je niet kunt afspelen, en dat
160 // is weer dezelfde soort halve waarheid.
161 leeg();
162
163 track('heel', 'Heel');
164 track('stuk', 'Stuk', { padInDb: '/weg/stuk.mp3', schrijf: false });
165 db.prepare("INSERT INTO playlists (id, site_id, title) VALUES ('pl2','s1','Half')").run();
166 db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('pl2','heel',0),('pl2','stuk',1)").run();
167
168 const uit = AX.buildArchive('me');
169 leeg();
170
171 const rapport = AI.importArchive(uit.files, { slug: 'me', origin: 'https://nieuw.test' });
172 const items = db.prepare("SELECT track_id FROM playlist_tracks WHERE playlist_id = 'pl2'").all().map((r) => r.track_id);
173 assert.deepEqual(items, ['heel']);
174 assert.ok(rapport.waarschuwingen.some((w) => /Half/.test(w)), 'en het wordt gemeld');
175});
176
177test('een droogloop telt de nummers maar schrijft niets', () => {
178 leeg();
179 track('d1', 'Droog');
180 const uit = AX.buildArchive('me');
181
182 leeg();
183
184 const droog = AI.importArchive(uit.files, { slug: 'me', origin: 'https://nieuw.test', dryRun: true });
185 assert.equal(droog.tracks, 1, 'het verslag hoort te zeggen hoeveel nummers er zouden komen');
186 assert.equal(db.prepare('SELECT COUNT(*) n FROM audio_tracks').get().n, 0, 'maar er staat niets');
187 assert.equal(fs.readdirSync(AUDIO).length, 0, 'en er is geen bestand geschreven');
188});
189
190test.after(() => { try { fs.rmSync(TMP, { recursive: true, force: true }); } catch { /* niets */ } });
Note: See TracBrowser for help on using the repository browser.