Ignore:
Timestamp:
08/13/2026 11:39:57 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
9529d7b
Parents:
fbfd7a1
Message:

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@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ArchiveImportService.js

    rfbfd7a1 r6c4ff7e  
    2424import { randomUUID } from 'crypto';
    2525import db from '../config/database.js';
    26 import { MEDIA_ROOT } from '../config/paths.js';
     26import { MEDIA_ROOT, AUDIO_ROOT } from '../config/paths.js';
    2727import { FORMAT_VERSION, parseFollowingCsv } from './ArchiveExportService.js';
    2828import * as Migration from './MigrationService.js';
     
    120120
    121121/**
     122 * De audiobibliotheek terugzetten (formaat v2).
     123 *
     124 * EEN REGEL DIE HIER ALLES BEPAALT: geen bestand, geen track. Dat klinkt
     125 * vanzelfsprekend en was het niet. De oude per-post-tak maakte een
     126 * audio_tracks-rij aan zodra er metadata was, ook als de bytes ontbraken. Op
     127 * soundfabrics.nl leverde dat 13 nummers op die in de lijst stonden en 404'den
     128 * bij het afspelen. Dat is erger dan ontbreken: het ziet eruit alsof de
     129 * verhuizing gelukt is, dus je gooit de oude instantie weg.
     130 *
     131 * De bestanden gaan naar AUDIO_ROOT en niet onder MEDIA_ROOT, want daar hoort
     132 * gehoste audio: de publieke /media-handler mag er niet bij (routes/audio.js).
     133 *
     134 * @returns {Array} de schrijfopdrachten; de beller voert ze in zijn transactie uit
     135 * (en bij een droogloop dus niet, maar het verslag klopt wel)
     136 */
     137function tracksTerug(files, site, rapport) {
     138  const buf = files.get('tracks.json');
     139  if (!buf) return [];
     140  let coll;
     141  try { coll = JSON.parse(buf.toString('utf8')); } catch { rapport.waarschuwingen.push('tracks.json is onleesbaar'); return []; }
     142  if (coll['shaer:archive'] !== true) { rapport.waarschuwingen.push('tracks.json: niet gemarkeerd als archief, overgeslagen'); return []; }
     143
     144  const werk = [];
     145  for (const t of (coll.orderedItems || [])) {
     146    const id = String(t.id || '').trim();
     147    if (!id) continue;
     148    const bestand = t['shaer:file'];
     149    const bytes = bestand ? files.get(bestand) : null;
     150    if (!bytes || !bytes.length) {
     151      rapport.tracksMissing += 1;
     152      rapport.waarschuwingen.push(`${t.name || id}: geluidsbestand zit niet in het archief, track niet aangemaakt`);
     153      continue;                       // de hele regel van deze functie
     154    }
     155    // Naam op de schijf: de hash uit het archief, met zijn extensie. De speler
     156    // zoekt op bestandsnaam in AUDIO_ROOT, dus dit is meteen het pad dat werkt.
     157    const naam = path.basename(String(bestand));
     158    if (!naam || naam.includes('/') || naam.includes('\\') || naam.startsWith('.')) {
     159      rapport.waarschuwingen.push(`${t.name || id}: onbruikbare bestandsnaam, overgeslagen`);
     160      continue;
     161    }
     162    werk.push({ soort: 'track', doel: path.join(path.resolve(AUDIO_ROOT), naam), bytes, id, t, naam });
     163    rapport.tracks += 1;
     164  }
     165  return werk;
     166}
     167
     168/** De playlists terug, inclusief hun volgorde: die volgorde IS de playlist. */
     169function playlistsTerug(files, site, rapport, bekendeTracks) {
     170  const buf = files.get('playlists.json');
     171  if (!buf) return [];
     172  let coll;
     173  try { coll = JSON.parse(buf.toString('utf8')); } catch { rapport.waarschuwingen.push('playlists.json is onleesbaar'); return []; }
     174  if (coll['shaer:archive'] !== true) return [];
     175  const werk = [];
     176  for (const p of (coll.orderedItems || [])) {
     177    const id = String(p.id || '').trim();
     178    if (!id) continue;
     179    // Alleen verwijzen naar tracks die er echt gekomen zijn, anders staat er
     180    // straks een playlist vol gaten die niemand kan afspelen.
     181    const items = (p['shaer:tracks'] || []).filter((x) => bekendeTracks.has(String(x && x.id)));
     182    const kwijt = (p['shaer:tracks'] || []).length - items.length;
     183    if (kwijt) rapport.waarschuwingen.push(`playlist ${p.name || id}: ${kwijt} nummer(s) ontbreken en zijn eruit gelaten`);
     184    werk.push({ soort: 'playlist', p, id, items });
     185    rapport.playlists += 1;
     186  }
     187  return werk;
     188}
     189
     190/**
    122191 * Volg opnieuw wie je volgde, uit de `following.csv` van een archief.
    123192 *
     
    167236    posts: 0, overgeslagen: 0, overschreven: 0,
    168237    replies: 0, media: 0, mediaMissing: 0, gemist: [], waarschuwingen: [],
     238    tracks: 0, tracksMissing: 0, playlists: 0,
    169239  };
    170240
     
    275345    }
    276346  }
     347
     348  // De audiobibliotheek. Telt ook in een droogloop mee in het verslag, want
     349  // "hoeveel nummers komen er" is precies wat je wilt weten voor je besluit.
     350  const trackWerk = tracksTerug(files, site, rapport);
     351  const bekendeTracks = new Set(trackWerk.map((w) => w.id));
     352  const playlistWerk = playlistsTerug(files, site, rapport, bekendeTracks);
    277353
    278354  if (opts.dryRun) return rapport;
     
    292368
    293369  db.transaction(() => {
    294     for (const s of schrijf) {
     370    for (const s of [...schrijf, ...trackWerk, ...playlistWerk]) {
    295371      if (s.soort === 'media') {
    296372        fs.mkdirSync(path.dirname(s.doel), { recursive: true });
     
    301377        insReply.run(s.postId, s.it.id || '', s.it.attributedTo || '', s.it['shaer:actorName'] || null,
    302378          s.it['shaer:actorHandle'] || null, s.it.content || '', s.it.published || null, s.it.inReplyTo || null);
     379        continue;
     380      }
     381      if (s.soort === 'track') {
     382        // Bestand eerst, dan pas de rijen. Faalt het schrijven, dan gooit dit en
     383        // rolt de hele transactie terug: liever geen import dan een track zonder
     384        // geluid, want dat is precies de val waar dit uit voortkomt.
     385        fs.mkdirSync(path.dirname(s.doel), { recursive: true });
     386        fs.writeFileSync(s.doel, s.bytes);
     387        const mediaId = randomUUID();
     388        db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)')
     389          .run(mediaId, site.id, s.naam, s.t['shaer:mediaType'] || 'audio/mpeg', s.bytes.length, s.doel);
     390        const link = (k) => (s.t.url || []).find((u) => String(u).includes(k)) || null;
     391        db.prepare(`INSERT OR REPLACE INTO audio_tracks
     392            (id, site_id, title, artist, album, duration, media_id, position, credit, license,
     393             cover_url, downloadable, fedi_open, link_spotify, link_youtube, link_soundcloud)
     394          VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
     395          .run(s.id, site.id, s.t.name || 'zonder titel', s.t.artist || null, s.t.album || null,
     396            s.t.duration || null, mediaId, s.t.position ?? null, s.t.credit || null, s.t.license || null,
     397            s.t['shaer:coverUrl'] || null, s.t['shaer:downloadable'] ? 1 : 0, s.t['shaer:fediOpen'] ? 1 : 0,
     398            link('spotify'), link('youtube'), link('soundcloud'));
     399        continue;
     400      }
     401      if (s.soort === 'playlist') {
     402        db.prepare(`INSERT OR REPLACE INTO playlists (id, site_id, title, artist, year, cover_url, kind)
     403                    VALUES (?,?,?,?,?,?,?)`)
     404          .run(s.id, site.id, s.p.name || 'zonder titel', s.p.artist || null, s.p.year || null,
     405            s.p['shaer:coverUrl'] || null, s.p['shaer:kind'] || null);
     406        db.prepare('DELETE FROM playlist_tracks WHERE playlist_id = ?').run(s.id);
     407        const insPT = db.prepare('INSERT OR IGNORE INTO playlist_tracks (playlist_id, track_id, position) VALUES (?,?,?)');
     408        s.items.forEach((it, i) => insPT.run(s.id, String(it.id), it.position ?? i));
    303409        continue;
    304410      }
     
    338444        view_count: o['shaer:viewCount'] || 0,
    339445      });
    340       // Gehoste audio terug: [[track:]] in de content valt anders op niets terug.
    341       for (const t of (o['shaer:audio'] || [])) {
    342         const trackId = String(t['shaer:ref'] || '').replace(/^\[\[track:|\]\]$/g, '');
    343         if (!trackId) continue;
    344         let mediaId = null;
    345         const bij = (o.attachment || []).find((a) => a.url === t['shaer:media']);
    346         const doel = bij && veiligMediaPad(bestemming(bij));
    347         if (doel) {
    348           mediaId = randomUUID();
    349           try {
    350             db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)')
    351               .run(mediaId, site.id, path.basename(doel), bij.mediaType || 'audio/mpeg', (files.get(bij.url) || []).length || 0, doel);
    352           } catch { mediaId = null; }
    353         }
    354         try {
    355           db.prepare(`INSERT OR REPLACE INTO audio_tracks (id, site_id, title, artist, album, duration, media_id, credit, license, link_spotify, link_youtube, link_soundcloud)
    356                       VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`)
    357             .run(trackId, site.id, t.name || 'zonder titel', t.artist || null, t.album || null, t.duration || null,
    358               mediaId, t.credit || null, t.license || null,
    359               ...['spotify', 'youtube', 'soundcloud'].map((k) => (t.url || []).find((u) => String(u).includes(k)) || null));
    360         } catch { /* geen audio-tabellen op deze installatie */ }
    361       }
     446      // De per-post audio-tak is weg. Tracks komen sinds v2 uit tracks.json,
     447      // dat de HELE bibliotheek draagt in plaats van alleen wat in een bericht
     448      // stond. Hier stond bovendien de fout die soundfabrics.nl opleverde: deze
     449      // lus maakte een audio_tracks-rij aan ZONDER te kijken of het bestand er
     450      // wel was, dus je kreeg 13 nummers die bestonden, in de lijst stonden, en
     451      // 404'den zodra je op play drukte. Zie tracksTerug hieronder.
    362452    }
    363453
Note: See TracChangeset for help on using the changeset viewer.