Changeset 6c4ff7e in Klonkt for src/services/ArchiveImportService.js
- Timestamp:
- 08/13/2026 11:39:57 PM (4 weeks ago)
- Branches:
- main
- Children:
- 9529d7b
- Parents:
- fbfd7a1
- File:
-
- 1 edited
-
src/services/ArchiveImportService.js (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ArchiveImportService.js
rfbfd7a1 r6c4ff7e 24 24 import { randomUUID } from 'crypto'; 25 25 import db from '../config/database.js'; 26 import { MEDIA_ROOT } from '../config/paths.js';26 import { MEDIA_ROOT, AUDIO_ROOT } from '../config/paths.js'; 27 27 import { FORMAT_VERSION, parseFollowingCsv } from './ArchiveExportService.js'; 28 28 import * as Migration from './MigrationService.js'; … … 120 120 121 121 /** 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 */ 137 function 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. */ 169 function 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 /** 122 191 * Volg opnieuw wie je volgde, uit de `following.csv` van een archief. 123 192 * … … 167 236 posts: 0, overgeslagen: 0, overschreven: 0, 168 237 replies: 0, media: 0, mediaMissing: 0, gemist: [], waarschuwingen: [], 238 tracks: 0, tracksMissing: 0, playlists: 0, 169 239 }; 170 240 … … 275 345 } 276 346 } 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); 277 353 278 354 if (opts.dryRun) return rapport; … … 292 368 293 369 db.transaction(() => { 294 for (const s of schrijf) {370 for (const s of [...schrijf, ...trackWerk, ...playlistWerk]) { 295 371 if (s.soort === 'media') { 296 372 fs.mkdirSync(path.dirname(s.doel), { recursive: true }); … … 301 377 insReply.run(s.postId, s.it.id || '', s.it.attributedTo || '', s.it['shaer:actorName'] || null, 302 378 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)); 303 409 continue; 304 410 } … … 338 444 view_count: o['shaer:viewCount'] || 0, 339 445 }); 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. 362 452 } 363 453
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)