Changeset 98acb97 in Klonkt
- Timestamp:
- 08/14/2026 12:03:55 AM (4 weeks ago)
- Branches:
- main
- Children:
- eb071a7
- Parents:
- 9529d7b
- Files:
-
- 6 edited
-
src/routes/activitypub.js (modified) (2 diffs)
-
src/services/ArchiveExportService.js (modified) (4 diffs)
-
src/services/ArchiveImportService.js (modified) (5 diffs)
-
src/services/MigrationService.js (modified) (6 diffs)
-
src/services/music/index.js (modified) (2 diffs)
-
test/archive-audio.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
r9529d7b r98acb97 1165 1165 }); 1166 1166 1167 router.get('/ap/users/:slug/playlists/:id', (req, res) => {1167 router.get('/ap/users/:slug/playlists/:id', async (req, res) => { 1168 1168 const site = publicSite(req.params.slug); 1169 1169 if (!site) return res.status(404).end(); … … 1171 1171 .get(req.params.id, site.id); 1172 1172 if (!pl) return res.status(404).end(); 1173 AP.sendAP(res, AP.buildPlaylistCollection(baseUrl(req), site, pl, AP.playlistOpenTracks(pl.id))); 1173 // De doel-actor van een verhuizing krijgt de VOLLEDIGE plaat, niet alleen de 1174 // nummers die voor de fediverse opengezet zijn (FEP-1580). 1175 const alles = await magAlles(req, site.slug); 1176 AP.sendAP(res, AP.buildPlaylistCollection(baseUrl(req), site, pl, AP.playlistOpenTracks(pl.id, { alles }))); 1174 1177 }); 1175 1178 -
src/services/ArchiveExportService.js
r9529d7b r98acb97 182 182 * @returns {Map<string,string>} trackId -> pad in het archief 183 183 */ 184 function audioBibliotheek(site, bestanden, tellingen, ontbrekend) { 184 /** 185 * Een hoes in het archief leggen. 186 * 187 * cover_url reisde wel mee als STRING en het bestand niet, dus kwam een track 188 * aan met een verwijzing naar een plaatje dat er niet was. Precies dezelfde 189 * fout als bij de audio zelf, een laag hoger: een verwijzing zonder bytes. 190 * 191 * @returns {string|null} het pad in het archief, of null 192 */ 193 function hoesToevoegen(url, origin, bestanden, tellingen) { 194 const schijf = localMediaPath(url, origin); 195 if (!schijf) return null; 196 let bytes = null; 197 try { bytes = fs.readFileSync(schijf); } catch { return null; } 198 const hash = sha256(bytes); 199 const naam = `media/${hash}${extOf(url) ? `.${extOf(url)}` : ''}`; 200 if (!bestanden.has(naam)) { bestanden.set(naam, bytes); tellingen.media += 1; } 201 return naam; 202 } 203 204 function audioBibliotheek(site, origin, bestanden, tellingen, ontbrekend) { 185 205 const kaart = new Map(); 186 206 let tracks = []; … … 217 237 credit: t.credit || undefined, license: t.license || undefined, 218 238 'shaer:coverUrl': t.cover_url || undefined, 239 // De BYTES van de hoes, niet alleen de verwijzing. 240 'shaer:coverFile': hoesToevoegen(t.cover_url, origin, bestanden, tellingen) || undefined, 219 241 'shaer:downloadable': t.downloadable ? 1 : 0, 220 242 'shaer:fediOpen': t.fedi_open ? 1 : 0, … … 243 265 id: p.id, name: p.title || '', artist: p.artist || undefined, year: p.year || undefined, 244 266 'shaer:kind': p.kind || undefined, 'shaer:coverUrl': p.cover_url || undefined, 267 'shaer:coverFile': hoesToevoegen(p.cover_url, origin, bestanden, tellingen) || undefined, 245 268 'shaer:tracks': db.prepare('SELECT track_id, position FROM playlist_tracks WHERE playlist_id = ? ORDER BY position') 246 269 .all(p.id).map((r) => ({ id: r.track_id, position: r.position })), … … 459 482 // De audiobibliotheek EERST. De posts verwijzen ernaar met [[track:]], dus de 460 483 // kaart moet klaar zijn voor de eerste post gebouwd wordt. 461 const audioKaart = audioBibliotheek(site, bestanden, tellingen, ontbrekend);484 const audioKaart = audioBibliotheek(site, origin, bestanden, tellingen, ontbrekend); 462 485 463 486 // Vaste volgorde: eerst op publicatiedatum, dan op id. Zonder tweede sleutel -
src/services/ArchiveImportService.js
r9529d7b r98acb97 135 135 * (en bij een droogloop dus niet, maar het verslag klopt wel) 136 136 */ 137 /** 138 * Een hoes uit het archief terugzetten. Geeft het nieuwe /media-pad terug, of 139 * null als het bestand er niet in zat: dan liever GEEN cover_url dan een 140 * verwijzing naar niets. 141 */ 142 function hoesTerug(files, bestand, werk) { 143 if (!bestand) return null; 144 const bytes = files.get(bestand); 145 if (!bytes || !bytes.length) return null; 146 const naam = path.basename(String(bestand)); 147 if (!naam || naam.includes('/') || naam.includes('\\') || naam.startsWith('.')) return null; 148 const urlPad = `/media/archief/${naam}`; 149 const doel = veiligMediaPad(urlPad); 150 if (!doel) return null; 151 werk.push({ soort: 'media', doel, bytes }); 152 return urlPad; 153 } 154 137 155 function tracksTerug(files, site, rapport) { 138 156 const buf = files.get('tracks.json'); … … 160 178 continue; 161 179 } 162 werk.push({ soort: 'track', doel: path.join(path.resolve(AUDIO_ROOT), naam), bytes, id, t, naam }); 180 const hoes = hoesTerug(files, t['shaer:coverFile'], werk); 181 werk.push({ soort: 'track', doel: path.join(path.resolve(AUDIO_ROOT), naam), bytes, id, t, naam, hoes }); 163 182 rapport.tracks += 1; 164 183 } … … 182 201 const kwijt = (p['shaer:tracks'] || []).length - items.length; 183 202 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 }); 203 const hoes = hoesTerug(files, p['shaer:coverFile'], werk); 204 werk.push({ soort: 'playlist', p, id, items, hoes }); 185 205 rapport.playlists += 1; 186 206 } … … 395 415 .run(s.id, site.id, s.t.name || 'zonder titel', s.t.artist || null, s.t.album || null, 396 416 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,417 s.hoes || null, s.t['shaer:downloadable'] ? 1 : 0, s.t['shaer:fediOpen'] ? 1 : 0, 398 418 link('spotify'), link('youtube'), link('soundcloud')); 399 419 continue; … … 403 423 VALUES (?,?,?,?,?,?,?)`) 404 424 .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);425 s.hoes || null, s.p['shaer:kind'] || null); 406 426 db.prepare('DELETE FROM playlist_tracks WHERE playlist_id = ?').run(s.id); 407 427 const insPT = db.prepare('INSERT OR IGNORE INTO playlist_tracks (playlist_id, track_id, position) VALUES (?,?,?)'); -
src/services/MigrationService.js
r9529d7b r98acb97 219 219 const AFBEELDING = /^image\//i; 220 220 221 /** AS2 geeft de duur als ISO-8601 ("PT212S"), de database wil seconden. */ 222 function duurSeconden(v) { 223 if (v == null) return null; 224 if (typeof v === 'number') return Math.round(v) || null; 225 const m = /^P(?:.*?T)?(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?$/.exec(String(v)); 226 if (!m) { const n = Number(v); return Number.isFinite(n) && n > 0 ? Math.round(n) : null; } 227 const sec = (Number(m[1]) || 0) * 3600 + (Number(m[2]) || 0) * 60 + (Number(m[3]) || 0); 228 return sec > 0 ? Math.round(sec) : null; 229 } 230 221 231 /** 222 232 * De titel terugwinnen uit de content. … … 336 346 const rapport = { 337 347 bron: bronActor.id, posts: 0, overgeslagen: 0, media: 0, mediaMislukt: 0, 338 blocks: 0, tracksBinnen: 0, tracksMislukt: 0, overgeslagenTracks: 0, waarschuwingen: [], 348 blocks: 0, tracksBinnen: 0, tracksMislukt: 0, overgeslagenTracks: 0, 349 playlistsBinnen: 0, playlistsMislukt: 0, waarschuwingen: [], 339 350 }; 340 351 … … 459 470 // geldt voor de bestanden zelf, die anders achter de gated audio-route 460 471 // blijven. 472 const trackKaart = new Map(); // bron-URI van een nummer -> ons nieuwe id 461 473 const streams = [].concat(bronActor.streams || []).filter((u) => typeof u === 'string'); 462 474 const tracksUrl = streams.find((u) => /\/tracks\/?$/.test(u)); … … 480 492 continue; // dezelfde regel als bij de zip: geen bestand, geen track 481 493 } 494 // De hoes. Die reisde als URL wel mee en als bestand niet, dus kwam een 495 // nummer aan met een verwijzing naar een plaatje dat er niet is. 496 let hoes = null; 497 const hoesUrl = (a.icon && (a.icon.url || a.icon)) || (a.image && (a.image.url || a.image)) || null; 498 if (hoesUrl && /^https?:\/\//i.test(String(hoesUrl))) { 499 const h = await haalBijlage(String(hoesUrl), { 500 safeFetch, mediaRoot, fs, path, maxBytes, 501 headers: signHeaders ? signHeaders(site.slug, String(hoesUrl), '*/*') : null, 502 }).catch(() => null); 503 if (h) { hoes = h.url; rapport.media++; } 504 else rapport.waarschuwingen.push(`hoes niet opgehaald: ${a.name || hoesUrl}`); 505 } 482 506 const trackId = crypto.randomUUID(); 483 507 const mediaId = crypto.randomUUID(); … … 485 509 db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)') 486 510 .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);511 db.prepare(`INSERT INTO audio_tracks (id, site_id, title, artist, album, duration, media_id, cover_url, fedi_open) 512 VALUES (?,?,?,?,?,?,?,?,0)`) 513 .run(trackId, site.id, a.name || 'zonder titel', a.summary || a.artist || null, a.album || null, 514 duurSeconden(a.duration), mediaId, hoes); 491 515 recordMigrated(site.slug, { origin: a.id, target: `${me}/ap/tracks/${trackId}`, sourceActor: bronActor.id, isPublic: false }); 516 trackKaart.set(String(a.id), trackId); 492 517 rapport.tracksBinnen++; 493 518 } catch (e) { … … 499 524 rapport.waarschuwingen.push('muziekbibliotheek overgeslagen: geen audiomap meegegeven'); 500 525 } 526 527 // ── De playlists ────────────────────────────────────────────── 528 // 529 // Los van de nummers, want de VOLGORDE is de playlist. Die staat nergens 530 // anders: haal je alleen de tracks op, dan heb je wel alle muziek en geen 531 // enkele plaat. De bron geeft ons de volledige lijst omdat we de doel-actor 532 // zijn; anders zaten er alleen de opengezette nummers in en kreeg je een 533 // plaat met gaten. 534 const plUrl = streams.find((u) => /\/playlists\/?$/.test(u)); 535 if (plUrl && trackKaart.size) { 536 const coll = await getJson(site.slug, plUrl); 537 const lijst = (coll && (coll.orderedItems || coll.items)) || []; 538 for (const p of (Array.isArray(lijst) ? lijst : []).slice(0, 200)) { 539 const uri = typeof p === 'string' ? p : (p && p.id); 540 if (!uri) continue; 541 const plc = typeof p === 'object' && (p.orderedItems || p.items) ? p : await getJson(site.slug, uri); 542 if (!plc) { rapport.playlistsMislukt++; continue; } 543 const nummers = (plc.orderedItems || plc.items || []) 544 .map((x) => (x && typeof x === 'object' ? x.id : x)) 545 .map((id) => trackKaart.get(String(id))) 546 .filter(Boolean); 547 if (!nummers.length) { 548 rapport.waarschuwingen.push(`playlist ${plc.name || uri}: geen van de nummers is aangekomen, overgeslagen`); 549 continue; 550 } 551 const plId = crypto.randomUUID(); 552 try { 553 db.prepare('INSERT INTO playlists (id, site_id, title, artist, year, kind) VALUES (?,?,?,?,?,?)') 554 .run(plId, site.id, plc.name || 'zonder titel', plc.attributedTo && plc.artist || plc.artist || null, 555 plc.year || null, plc['shaer:kind'] || null); 556 const ins = db.prepare('INSERT OR IGNORE INTO playlist_tracks (playlist_id, track_id, position) VALUES (?,?,?)'); 557 nummers.forEach((tid, i) => ins.run(plId, tid, i)); 558 rapport.playlistsBinnen++; 559 const kwijt = (plc.orderedItems || plc.items || []).length - nummers.length; 560 if (kwijt > 0) rapport.waarschuwingen.push(`playlist ${plc.name || uri}: ${kwijt} nummer(s) ontbraken en zijn eruit gelaten`); 561 } catch (e) { 562 rapport.playlistsMislukt++; 563 rapport.waarschuwingen.push(`playlist niet opgeslagen: ${plc.name || uri} (${e && e.message})`); 564 } 565 } 566 } 501 567 } catch (e) { 502 568 // 9-bij-mislukking: de vlag blijft OPEN staan. Derden blijven dan kijken, -
src/services/music/index.js
r9529d7b r98acb97 22 22 m.filename, m.storage_path, m.mime_type, m.size`; 23 23 24 export function playlistOpenTracks(playlistId) { 24 /** 25 * `alles` net als bij siteOpenTracks (FEP-1580): zonder die tak krijgt de 26 * instantie waar je naartoe verhuist een playlist met gaten erin, want alleen 27 * de opengezette nummers zitten erin. Een halve plaat is geen plaat. 28 */ 29 export function playlistOpenTracks(playlistId, { alles = false } = {}) { 25 30 return db.prepare( 26 31 `SELECT ${TRACK_KOLOMMEN} … … 28 33 JOIN audio_tracks t ON t.id = pt.track_id 29 34 JOIN media m ON m.id = t.media_id 30 WHERE pt.playlist_id = ? AND t.fedi_open = 135 WHERE pt.playlist_id = ?${alles ? '' : ' AND t.fedi_open = 1'} 31 36 ORDER BY pt.position` 32 37 ).all(playlistId); -
test/archive-audio.test.js
r9529d7b r98acb97 188 188 }); 189 189 190 191 test('de hoes van een nummer reist mee als BESTAND, niet als losse verwijzing', () => { 192 // Robin na de tweede ronde: "de audio tracks hadden images, die zijn niet 193 // meegegaan". cover_url ging wel mee als string en het bestand niet, dus kwam 194 // een nummer aan met een verwijzing naar een plaatje dat er niet was. 195 // Dezelfde fout als bij de audio zelf, een laag hoger. 196 leeg(); 197 const hoesDir = path.join(MEDIA, 'hoes'); 198 fs.mkdirSync(hoesDir, { recursive: true }); 199 const PNG = Buffer.from('nep-png-bytes'); 200 fs.writeFileSync(path.join(hoesDir, 'a.png'), PNG); 201 track('h1', 'Met hoes'); 202 db.prepare("UPDATE audio_tracks SET cover_url = '/media/hoes/a.png' WHERE id = 'h1'").run(); 203 db.prepare("INSERT INTO playlists (id, site_id, title, cover_url) VALUES ('plh','s1','Plaat','/media/hoes/a.png')").run(); 204 db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('plh','h1',0)").run(); 205 206 const uit = AX.buildArchive('me'); 207 const rij = JSON.parse(uit.files.get('tracks.json').toString('utf8')).orderedItems[0]; 208 assert.ok(rij['shaer:coverFile'], 'de hoes hoort een plek in het archief te hebben'); 209 assert.deepEqual(uit.files.get(rij['shaer:coverFile']), PNG, 'met de echte bytes erin'); 210 const pl = JSON.parse(uit.files.get('playlists.json').toString('utf8')).orderedItems[0]; 211 assert.ok(pl['shaer:coverFile'], 'en de hoes van de plaat ook'); 212 213 leeg(); 214 AI.importArchive(uit.files, { slug: 'me', origin: 'https://nieuw.test' }); 215 const t = db.prepare("SELECT cover_url FROM audio_tracks WHERE id = 'h1'").get(); 216 assert.ok(t.cover_url, 'na de import wijst het nummer naar een hoes'); 217 const opSchijf = path.join(MEDIA, t.cover_url.replace(/^\/media\//, '')); 218 assert.deepEqual(fs.readFileSync(opSchijf), PNG, 'en die staat er ook echt'); 219 }); 220 221 test('een hoes die niet in het archief zit levert GEEN kapotte verwijzing op', () => { 222 // Liever geen hoes dan een <img> die 404't. Dezelfde regel als bij de tracks. 223 leeg(); 224 track('h2', 'Hoes zoek'); 225 db.prepare("UPDATE audio_tracks SET cover_url = '/media/bestaat/niet.png' WHERE id = 'h2'").run(); 226 const uit = AX.buildArchive('me'); 227 const rij = JSON.parse(uit.files.get('tracks.json').toString('utf8')).orderedItems[0]; 228 assert.equal(rij['shaer:coverFile'], undefined); 229 230 leeg(); 231 AI.importArchive(uit.files, { slug: 'me', origin: 'https://nieuw.test' }); 232 assert.equal(db.prepare("SELECT cover_url FROM audio_tracks WHERE id = 'h2'").get().cover_url, null, 233 'geen verwijzing naar een plaatje dat er niet is'); 234 }); 235 190 236 test.after(() => { try { fs.rmSync(TMP, { recursive: true, force: true }); } catch { /* niets */ } });
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)