Changeset 6c4ff7e in Klonkt for src/services/MigrationService.js


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/MigrationService.js

    rfbfd7a1 r6c4ff7e  
    248248 * moet stranden, ook als die bron "van onszelf" is.
    249249 */
    250 async function haalBijlage(url, { safeFetch, mediaRoot, fs, path, maxBytes }) {
    251   const r = await safeFetch(url, { headers: { accept: '*/*' } }).catch(() => null);
     250async function haalBijlage(url, { safeFetch, mediaRoot, fs, path, maxBytes, submap = 'migrated', headers = null }) {
     251  // Ondertekend als het moet. Gehoste audio zit achter dezelfde poort als de
     252  // rest van de bron, en een kale fetch krijgt daar een 403: de bron kan dan
     253  // niet zien dat wij de doel-actor van zijn Move zijn.
     254  const r = await safeFetch(url, { headers: headers || { accept: '*/*' } }).catch(() => null);
    252255  if (!r || !r.ok) return null;
    253256  const buf = Buffer.from(await r.arrayBuffer());
     
    261264  })();
    262265  const naam = `${crypto.randomUUID()}.${ext}`;
    263   const rel = `migrated/${naam}`;
    264   const abs = path.join(mediaRoot, 'migrated', naam);
     266  // Zonder submap komt het bestand in de root zelf: dat is wat gehoste audio
     267  // nodig heeft, want de speler zoekt AUDIO_ROOT + bestandsnaam en kijkt niet
     268  // in mappen eronder.
     269  const rel = submap ? `${submap}/${naam}` : naam;
     270  const abs = submap ? path.join(mediaRoot, submap, naam) : path.join(mediaRoot, naam);
    265271  fs.mkdirSync(path.dirname(abs), { recursive: true });
    266272  fs.writeFileSync(abs, buf);
     
    294300    // privacygrens en niet een weergavedetail.
    295301    noteVisibility = () => 'followers',
     302    audioRoot = null, signHeaders = null,
    296303  } = deps;
    297304  const zichtbaarheid = noteVisibility;
     
    329336  const rapport = {
    330337    bron: bronActor.id, posts: 0, overgeslagen: 0, media: 0, mediaMislukt: 0,
    331     blocks: 0, waarschuwingen: [],
     338    blocks: 0, tracksBinnen: 0, tracksMislukt: 0, overgeslagenTracks: 0, waarschuwingen: [],
    332339  };
    333340
     
    441448    }
    442449    if (gezien >= max) rapport.waarschuwingen.push(`gestopt bij ${max} berichten, draai het nog eens voor de rest`);
     450
     451    // ── De muziekbibliotheek ──────────────────────────────────────
     452    //
     453    // Losse nummers staan niet in de outbox: die hangen aan de tracks-collectie
     454    // waar de actor via AS2 `streams` naar wijst. Zonder deze lus verhuist een
     455    // muzieksite zijn berichten en laat hij zijn bibliotheek achter.
     456    //
     457    // De bron geeft ons hier alles, niet alleen de fedi_open-nummers, omdat we
     458    // de doel-actor van zijn Move zijn (siteOpenTracks({alles})). Hetzelfde
     459    // geldt voor de bestanden zelf, die anders achter de gated audio-route
     460    // blijven.
     461    const streams = [].concat(bronActor.streams || []).filter((u) => typeof u === 'string');
     462    const tracksUrl = streams.find((u) => /\/tracks\/?$/.test(u));
     463    if (tracksUrl && safeFetch && fs && path && audioRoot) {
     464      const coll = await getJson(site.slug, tracksUrl);
     465      const lijst = (coll && (coll.orderedItems || coll.items)) || [];
     466      for (const it of (Array.isArray(lijst) ? lijst : []).slice(0, max)) {
     467        const a = (it && typeof it.object === 'object' && it.object) ? it.object : it;
     468        if (!a || !a.id) continue;
     469        if (a.type && a.type !== 'Audio') continue;
     470        if (alGemigreerd(site.slug, a.id)) { rapport.overgeslagenTracks++; continue; }
     471        const bron = a.url && (typeof a.url === 'string' ? a.url : (Array.isArray(a.url) ? (a.url[0] && (a.url[0].href || a.url[0])) : a.url.href));
     472        if (!bron || !/^https?:\/\//i.test(String(bron))) { rapport.tracksMislukt++; continue; }
     473        const g = await haalBijlage(String(bron), {
     474          safeFetch, mediaRoot: audioRoot, fs, path, maxBytes, submap: '',
     475          headers: signHeaders ? signHeaders(site.slug, String(bron), '*/*') : null,
     476        }).catch(() => null);
     477        if (!g) {
     478          rapport.tracksMislukt++;
     479          rapport.waarschuwingen.push(`nummer niet opgehaald: ${a.name || bron}`);
     480          continue;                       // dezelfde regel als bij de zip: geen bestand, geen track
     481        }
     482        const trackId = crypto.randomUUID();
     483        const mediaId = crypto.randomUUID();
     484        try {
     485          db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)')
     486            .run(mediaId, site.id, g.filename, g.mediaType, g.size, g.storage_path);
     487          db.prepare(`INSERT INTO audio_tracks (id, site_id, title, artist, album, duration, media_id, fedi_open)
     488                      VALUES (?,?,?,?,?,?,?,0)`)
     489            .run(trackId, site.id, a.name || 'zonder titel', a.artist || null, a.album || null,
     490              Number(a.duration) || null, mediaId);
     491          recordMigrated(site.slug, { origin: a.id, target: `${me}/ap/tracks/${trackId}`, sourceActor: bronActor.id, isPublic: false });
     492          rapport.tracksBinnen++;
     493        } catch (e) {
     494          rapport.tracksMislukt++;
     495          rapport.waarschuwingen.push(`nummer niet opgeslagen: ${a.name || a.id} (${e && e.message})`);
     496        }
     497      }
     498    } else if (tracksUrl) {
     499      rapport.waarschuwingen.push('muziekbibliotheek overgeslagen: geen audiomap meegegeven');
     500    }
    443501  } catch (e) {
    444502    // 9-bij-mislukking: de vlag blijft OPEN staan. Derden blijven dan kijken,
Note: See TracChangeset for help on using the changeset viewer.