Changeset f704237 in Klonkt
- Timestamp:
- 08/14/2026 02:35:06 AM (4 weeks ago)
- Branches:
- main
- Children:
- 47f1183
- Parents:
- fc664ce
- Files:
-
- 5 edited
-
src/services/ArchiveExportService.js (modified) (2 diffs)
-
src/services/ArchiveImportService.js (modified) (3 diffs)
-
src/services/MigrationService.js (modified) (2 diffs)
-
test/archive-audio.test.js (modified) (1 diff)
-
test/fep1580-migration.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ArchiveExportService.js
rfc664ce rf704237 228 228 } catch { naam = null; } // onleesbaar telt als ontbrekend, niet als stilte 229 229 } 230 if (!naam) { 230 // Een LINK-ONLY track is geen kapotte track. Klonkt kent dat type: geen 231 // gehost bestand, wel een Spotify- of YouTube-link, en buildNote maakt er 232 // een embed-kaart van (zie trackEmbedLinks). Die tellen dus niet als 233 // ontbrekend, anders meldt de export een probleem dat er niet is. 234 const alleenLinks = !t.media_id && [t.link_spotify, t.link_youtube, t.link_soundcloud].some(Boolean); 235 if (!naam && !alleenLinks) { 231 236 tellingen.audioMissing += 1; 232 237 ontbrekend.push({ track: t.title || t.id, url: t.storage_path || '(geen mediarij)' }); 233 238 } 239 if (alleenLinks) tellingen.audioLinks = (tellingen.audioLinks || 0) + 1; 234 240 items.push({ 235 241 id: t.id, name: t.title || '', artist: t.artist || undefined, album: t.album || undefined, … … 247 253 // stond. Stil weglaten zou een leugen zijn, en de importer moet hierop 248 254 // kunnen weigeren in plaats van een track zonder bestand aan te maken. 249 'shaer:availability': naam ? 'included' : 'missing', 255 // Drie staten in plaats van twee: erbij, weg, of bewust zonder bestand. 256 'shaer:availability': naam ? 'included' : (alleenLinks ? 'linkOnly' : 'missing'), 250 257 'shaer:originalPath': naam ? undefined : (t.storage_path || undefined), 251 258 url: [t.link_spotify, t.link_youtube, t.link_soundcloud].filter(Boolean), -
src/services/ArchiveImportService.js
rfc664ce rf704237 166 166 const bestand = t['shaer:file']; 167 167 const bytes = bestand ? files.get(bestand) : null; 168 if (!bytes || !bytes.length) { 168 // "Geen bestand" en "niets om te tonen" zijn niet hetzelfde. Een LINK-ONLY 169 // track heeft nooit een bestand gehad: hij bestaat uit een Spotify- of 170 // YouTube-link en Klonkt maakt daar een embed-kaart van. Die hoort gewoon 171 // mee. Mijn eerste regel gooide hem weg, en dat kostte Robin een nummer 172 // (Youngstown) dat op de oude site prima werkte. 173 const links = Array.isArray(t.url) ? t.url.filter(Boolean) : []; 174 const alleenLinks = t['shaer:availability'] === 'linkOnly' || (!bestand && links.length > 0); 175 if ((!bytes || !bytes.length) && !alleenLinks) { 169 176 rapport.tracksMissing += 1; 170 177 rapport.waarschuwingen.push(`${t.name || id}: geluidsbestand zit niet in het archief, track niet aangemaakt`); 171 continue; // de hele regel van deze functie 178 continue; 179 } 180 if (alleenLinks) { 181 // Geen bestand om weg te schrijven, geen mediarij: alleen de track zelf. 182 werk.push({ soort: 'track', id, t, naam: null, bytes: null, doel: null, hoes: hoesTerug(files, t['shaer:coverFile'], werk) }); 183 rapport.tracks += 1; 184 rapport.tracksLinks = (rapport.tracksLinks || 0) + 1; 185 continue; 172 186 } 173 187 // Naam op de schijf: de hash uit het archief, met zijn extensie. De speler … … 256 270 posts: 0, overgeslagen: 0, overschreven: 0, 257 271 replies: 0, media: 0, mediaMissing: 0, gemist: [], waarschuwingen: [], 258 tracks: 0, tracksMissing: 0, playlists: 0, linksBijgetrokken: 0,272 tracks: 0, tracksMissing: 0, tracksLinks: 0, playlists: 0, linksBijgetrokken: 0, 259 273 }; 260 274 … … 417 431 // rolt de hele transactie terug: liever geen import dan een track zonder 418 432 // geluid, want dat is precies de val waar dit uit voortkomt. 419 fs.mkdirSync(path.dirname(s.doel), { recursive: true }); 420 fs.writeFileSync(s.doel, s.bytes); 421 const mediaId = randomUUID(); 422 db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)') 423 .run(mediaId, site.id, s.naam, s.t['shaer:mediaType'] || 'audio/mpeg', s.bytes.length, s.doel); 433 // 434 // Een link-only track heeft geen bestand en dus ook geen mediarij; die 435 // krijgt media_id NULL, precies zoals op de bron. 436 let mediaId = null; 437 if (s.doel && s.bytes) { 438 fs.mkdirSync(path.dirname(s.doel), { recursive: true }); 439 fs.writeFileSync(s.doel, s.bytes); 440 mediaId = randomUUID(); 441 db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)') 442 .run(mediaId, site.id, s.naam, s.t['shaer:mediaType'] || 'audio/mpeg', s.bytes.length, s.doel); 443 } 424 444 const link = (k) => (s.t.url || []).find((u) => String(u).includes(k)) || null; 425 445 db.prepare(`INSERT OR REPLACE INTO audio_tracks -
src/services/MigrationService.js
rfc664ce rf704237 512 512 const rapport = { 513 513 bron: bronActor.id, posts: 0, overgeslagen: 0, opnieuw: 0, postsBijgewerkt: 0, media: 0, mediaMislukt: 0, 514 blocks: 0, tracksBinnen: 0, tracksMislukt: 0, tracksBijgewerkt: 0, overgeslagenTracks: 0,514 blocks: 0, tracksBinnen: 0, tracksMislukt: 0, tracksBijgewerkt: 0, tracksLinks: 0, overgeslagenTracks: 0, 515 515 playlistsBinnen: 0, playlistsMislukt: 0, waarschuwingen: [], 516 516 }; … … 769 769 } 770 770 } 771 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)); 772 if (!bron || !/^https?:\/\//i.test(String(bron))) { rapport.tracksMislukt++; continue; } 771 // Uit de url-lijst de LINK NAAR HET BESTAND vissen, niet zomaar de eerste: 772 // buildTrackAudio zet er ook een text/html-link naar de post voor. En een 773 // LINK-ONLY track (alleen Spotify of YouTube, nooit een gehost bestand) 774 // heeft er helemaal geen. Die hoort gewoon mee, met media_id NULL. 775 // Weggooien kostte Robin een nummer dat het op de oude site prima deed. 776 const urls = [].concat(a.url || []).map((u) => (typeof u === 'string' ? { href: u } : u)).filter((u) => u && u.href); 777 const bestandLink = urls.find((u) => /^audio\//i.test(String(u.mediaType || ''))) 778 || urls.find((u) => /\/audio\/stream\//.test(String(u.href))); 779 const externe = urls.map((u) => String(u.href)).filter((h) => /spotify|youtube|youtu\.be|soundcloud|bandcamp/i.test(h)); 780 const bron = bestandLink && bestandLink.href; 781 if (!bron || !/^https?:\/\//i.test(String(bron))) { 782 if (externe.length) { 783 const trackIdL = ruwId(a.id) || crypto.randomUUID(); 784 try { 785 db.prepare(`INSERT OR REPLACE INTO audio_tracks 786 (id, site_id, title, artist, media_id, link_spotify, link_youtube, link_soundcloud, fedi_open) 787 VALUES (?,?,?,?,NULL,?,?,?,0)`) 788 .run(trackIdL, site.id, a.name || 'zonder titel', a.summary || a.artist || null, 789 externe.find((u) => /spotify/i.test(u)) || null, 790 externe.find((u) => /youtube|youtu\.be/i.test(u)) || null, 791 externe.find((u) => /soundcloud/i.test(u)) || null); 792 recordMigrated(site.slug, { origin: a.id, target: `${me}/ap/tracks/${trackIdL}`, sourceActor: bronActor.id, isPublic: false }); 793 trackKaart.set(String(a.id), trackIdL); 794 ruwKaart.set(ruwId(a.id), trackIdL); 795 rapport.tracksLinks++; 796 rapport.tracksBinnen++; 797 } catch (e) { 798 rapport.tracksMislukt++; 799 rapport.waarschuwingen.push(`nummer niet opgeslagen: ${a.name || a.id} (${e && e.message})`); 800 } 801 continue; 802 } 803 rapport.tracksMislukt++; 804 continue; 805 } 773 806 const g = await haalBijlage(String(bron), { 774 807 safeFetch, mediaRoot: audioRoot, fs, path, maxBytes, submap: '', -
test/archive-audio.test.js
rfc664ce rf704237 258 258 }); 259 259 260 test('een track met alleen Spotify/YouTube-links reist gewoon mee', () => { 261 // Robins Youngstown-nummer. Klonkt kent link-only tracks: geen gehost 262 // bestand, wel externe links, en buildNote maakt daar een embed-kaart van. 263 // Mijn regel "geen bestand, geen track" gooide die weg, en dat was te grof: 264 // "geen bestand" en "niets om te tonen" zijn niet hetzelfde. 265 leeg(); 266 db.prepare(`INSERT INTO audio_tracks (id, site_id, title, artist, media_id, link_spotify, link_youtube) 267 VALUES ('lo','s1','Alleen links','Youngstown',NULL, 268 'https://open.spotify.com/track/abc','https://www.youtube.com/watch?v=xyz')`).run(); 269 270 const uit = AX.buildArchive('me'); 271 const rij = JSON.parse(uit.files.get('tracks.json').toString('utf8')).orderedItems.find((x) => x.id === 'lo'); 272 assert.equal(rij['shaer:availability'], 'linkOnly', 'een eigen staat, niet "missing"'); 273 assert.equal(uit.counts.audioMissing, 0, 'en hij telt niet als ontbrekend: er mist niets'); 274 assert.deepEqual(rij.url, ['https://open.spotify.com/track/abc', 'https://www.youtube.com/watch?v=xyz']); 275 276 leeg(); 277 const r = AI.importArchive(uit.files, { slug: 'me', origin: 'https://nieuw.test' }); 278 assert.equal(r.tracks, 1); 279 assert.equal(r.tracksMissing, 0); 280 const t = db.prepare("SELECT title, artist, media_id, link_spotify, link_youtube FROM audio_tracks WHERE id = 'lo'").get(); 281 assert.ok(t, 'de track hoort er te staan'); 282 assert.equal(t.media_id, null, 'zonder mediarij, precies zoals op de bron'); 283 assert.equal(t.title, 'Alleen links'); 284 assert.equal(t.link_spotify, 'https://open.spotify.com/track/abc'); 285 assert.equal(t.link_youtube, 'https://www.youtube.com/watch?v=xyz'); 286 }); 287 288 test('een track zonder bestand EN zonder links blijft wel geweigerd', () => { 289 // De oorspronkelijke regel blijft staan waar hij hoort: dit is een track die 290 // niets kan tonen en niets kan afspelen. 291 leeg(); 292 track('kapot', 'Kapot', { padInDb: '/weg/kapot.mp3', schrijf: false }); 293 const uit = AX.buildArchive('me'); 294 assert.equal(uit.counts.audioMissing, 1); 295 leeg(); 296 const r = AI.importArchive(uit.files, { slug: 'me', origin: 'https://nieuw.test' }); 297 assert.equal(r.tracks, 0); 298 assert.equal(r.tracksMissing, 1); 299 assert.equal(db.prepare('SELECT COUNT(*) n FROM audio_tracks').get().n, 0); 300 }); 301 260 302 test.after(() => { try { fs.rmSync(TMP, { recursive: true, force: true }); } catch { /* niets */ } }); -
test/fep1580-migration.test.js
rfc664ce rf704237 369 369 }); 370 370 371 test('een link-only nummer komt ook via de ophaalknop mee', async () => { 372 // Dezelfde regel als bij de zip. Over AP heeft zo'n track geen audio-link in 373 // zijn url-lijst, alleen de externe. De vorige versie viste blind de eerste 374 // url en strandde daarop. 375 const s = site({ aliases: [BRON] }); 376 const kaart = new Map([ 377 [BRON, { id: BRON, type: 'Person', movedTo: IK, outbox: `${BRON}/outbox`, streams: [`${BRON}/tracks`] }], 378 [`${BRON}/outbox`, { type: 'OrderedCollection', totalItems: 0, first: `${BRON}/outbox?page=1` }], 379 [`${BRON}/outbox?page=1`, { type: 'OrderedCollectionPage', orderedItems: [] }], 380 [`${BRON}/tracks`, { type: 'OrderedCollection', orderedItems: [{ 381 id: `${BRON}/tracks/lo`, type: 'Audio', name: 'Alleen links', summary: 'Youngstown', 382 url: [ 383 { type: 'Link', href: 'https://oud.example/eenpost', mediaType: 'text/html' }, 384 { type: 'Link', href: 'https://open.spotify.com/track/abc' }, 385 { type: 'Link', href: 'https://www.youtube.com/watch?v=xyz' }, 386 ], 387 }] }], 388 ]); 389 const gehaald = []; 390 const r = await stil(() => Mig.ingestFromSource(s, { deps: { 391 getJson: async (_s, url) => kaart.get(url) || null, 392 noteId: (b, id) => `${b}/ap/notes/${id}`, 393 noteVisibility: AP.noteVisibility, 394 audioRoot: '/nep/audio', mediaRoot: '/nep/media', 395 signHeaders: () => ({ Signature: 'nep' }), 396 safeFetch: async (u) => { gehaald.push(u); return { ok: true, arrayBuffer: async () => Buffer.from('x'), headers: { get: () => 'audio/mpeg' } }; }, 397 fs: { mkdirSync() {}, writeFileSync() {}, statSync() { throw new Error('ENOENT'); } }, 398 path, 399 } })); 400 assert.equal(r.tracksBinnen, 1); 401 assert.equal(r.tracksMislukt, 0); 402 assert.equal(r.tracksLinks, 1); 403 assert.deepEqual(gehaald, [], 'er valt niets te downloaden, en dat hoort ook niet geprobeerd'); 404 const t = db.prepare("SELECT title, artist, media_id, link_spotify, link_youtube FROM audio_tracks WHERE id = 'lo'").get(); 405 assert.ok(t, 'de track staat er'); 406 assert.equal(t.media_id, null); 407 assert.equal(t.artist, 'Youngstown', 'de artiest komt uit summary'); 408 assert.equal(t.link_spotify, 'https://open.spotify.com/track/abc'); 409 assert.equal(t.link_youtube, 'https://www.youtube.com/watch?v=xyz'); 410 }); 411 412 test('de audio-link wordt uit de lijst GEVIST, niet blind de eerste gepakt', async () => { 413 // buildTrackAudio zet een text/html-link naar de post VOOR het bestand. Wie 414 // element nul pakt downloadt een HTML-pagina en noemt dat een mp3. 415 const s = site({ aliases: [BRON] }); 416 const MP3 = 'https://oud.example/audio/stream/x.mp3'; 417 const kaart = new Map([ 418 [BRON, { id: BRON, type: 'Person', movedTo: IK, outbox: `${BRON}/outbox`, streams: [`${BRON}/tracks`] }], 419 [`${BRON}/outbox`, { type: 'OrderedCollection', totalItems: 0, first: `${BRON}/outbox?page=1` }], 420 [`${BRON}/outbox?page=1`, { type: 'OrderedCollectionPage', orderedItems: [] }], 421 [`${BRON}/tracks`, { type: 'OrderedCollection', orderedItems: [{ 422 id: `${BRON}/tracks/m1`, type: 'Audio', name: 'Met bestand', 423 url: [ 424 { type: 'Link', href: 'https://oud.example/depost', mediaType: 'text/html' }, 425 { type: 'Link', href: MP3, mediaType: 'audio/mpeg' }, 426 ], 427 }] }], 428 ]); 429 const gehaald = []; 430 await stil(() => Mig.ingestFromSource(s, { deps: { 431 getJson: async (_s, url) => kaart.get(url) || null, 432 noteId: (b, id) => `${b}/ap/notes/${id}`, 433 noteVisibility: AP.noteVisibility, 434 audioRoot: '/nep/audio', mediaRoot: '/nep/media', 435 signHeaders: () => ({ Signature: 'nep' }), 436 safeFetch: async (u) => { gehaald.push(u); return { ok: true, arrayBuffer: async () => Buffer.from('x'), headers: { get: () => 'audio/mpeg' } }; }, 437 fs: { mkdirSync() {}, writeFileSync() {}, statSync() { throw new Error('ENOENT'); } }, 438 path, 439 } })); 440 assert.deepEqual(gehaald, [MP3], 'het bestand, niet de postpagina'); 441 }); 442 371 443 test('een nummer waarvan de bytes niet komen levert GEEN track op', async () => { 372 444 // Dezelfde regel als bij de zip. Een nummer dat in de lijst staat en 404't is
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)