Changeset aebde2b in Klonkt
- Timestamp:
- 08/16/2026 02:03:14 PM (3 weeks ago)
- Branches:
- main
- Children:
- 7090fd2
- Parents:
- d68ee3a
- Files:
-
- 2 edited
-
src/services/music/index.js (modified) (8 diffs)
-
test/ap-fw-track.test.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/music/index.js
rd68ee3a raebde2b 134 134 for (const r of rijen) if (!uit.has(r.tid)) uit.set(r.tid, { id: r.post_id, slug: r.post_slug }); 135 135 return uit; 136 } 137 138 /** 139 * De artiest-credit, gedeeld door track en album (shaer-3f8a / shaer-756s). 140 * 141 * De ENTITEIT is de site-actor: een echt, opvraagbaar adres. De credittekst -- 142 * de artiestkolom van de track of van de uitgave -- gaat naar `credit`, want 143 * daar verwacht hun model hem. Er een id per artiestnaam van maken zou 144 * identiteit uit een string zijn, en dat is de fout die we bij albums juist 145 * vermijden. 146 * 147 * Eén functie voor beide, zodat een track en het album waar hij op staat nooit 148 * een verschillende artiest kunnen krijgen door twee keer hetzelfde te bouwen. 149 */ 150 function artistCredit(base, site, creditTekst, wanneer) { 151 const artiest = { 152 type: 'Artist', 153 id: actorId(base, site.slug), 154 name: site.title || site.slug, 155 published: site.created_at ? new Date(site.created_at).toISOString() : wanneer, 156 }; 157 if (isMbid(site.mb_artist_id)) artiest.musicbrainzId = String(site.mb_artist_id).trim().toLowerCase(); 158 return [{ 159 type: 'ArtistCredit', 160 id: `${actorId(base, site.slug)}#artist-credit`, 161 published: artiest.published, 162 artist: artiest, 163 ...(creditTekst ? { credit: creditTekst } : {}), 164 }]; 136 165 } 137 166 … … 213 242 const wanneer = r.created_at ? new Date(r.created_at).toISOString() 214 243 : (site.created_at ? new Date(site.created_at).toISOString() : new Date(0).toISOString()); 215 const artiest = {216 type: 'Artist',217 id: actorId(base, site.slug),218 name: site.title || site.slug,219 published: site.created_at ? new Date(site.created_at).toISOString() : wanneer,220 };221 if (isMbid(site.mb_artist_id)) artiest.musicbrainzId = String(site.mb_artist_id).trim().toLowerCase();222 244 223 245 a.track = { … … 227 249 published: wanneer, 228 250 ...(Number(r.position) ? { position: Number(r.position) } : {}), 229 artist_credit: [{ 230 type: 'ArtistCredit', 231 id: `${a.id}#artist-credit`, 232 published: wanneer, 233 artist: artiest, 234 ...(r.artist ? { credit: r.artist } : {}), 235 }], 251 artist_credit: artistCredit(base, site, r.artist, wanneer), 236 252 }; 253 // De uitgave waar dit nummer op staat, INGESLOTEN (shaer-756s, stap 2). 254 // `albums` mag expliciet null zijn: dan is er niets op te zoeken. 255 const uitgave = opts.albums !== undefined 256 ? (opts.albums && opts.albums.get(r.id)) || null 257 : ((site.id && trackAlbums(site.id).get(r.id)) || null); 258 if (uitgave) { 259 a.track.album = buildAlbumObject(base, site, uitgave); 260 // Ook op het Audio-object zelf, als URI. Funkwhale 2.0 en Emissary doen dat 261 // allebei, en het scheelt een lezer het uitpakken van de track. 262 a.album = a.track.album.id; 263 } 237 264 if (r.duration) a.duration = `PT${Math.round(r.duration)}S`; 238 265 if (r.created_at) a.published = new Date(r.created_at).toISOString(); … … 303 330 const id = libraryId(base, site); 304 331 const hostPosts = site.id ? trackHostPosts(site.id) : null; 305 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts })); 332 const albums = site.id ? trackAlbums(site.id) : null; 333 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts, albums })); 306 334 return pagedCollection(id, items, { 307 335 page, … … 337 365 // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts. 338 366 const posts = site.id ? trackHostPosts(site.id) : null; 339 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts })); 367 const albums = site.id ? trackAlbums(site.id) : null; 368 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts, albums })); 340 369 return pagedCollection(`${actorId(base, site.slug)}/tracks`, items, { page, extra: { attributedTo: actorId(base, site.slug) } }); 341 370 } … … 399 428 } 400 429 430 /** 431 * Bij welke UITGAVE hoort een track? (shaer-756s, stap 2) 432 * 433 * Alleen playlists met kind='album' tellen: een mixtape is geen uitgave, en dat 434 * onderscheid is precies wat de keuze album/playlist betekent. Zit een track in 435 * twee albums, dan wint de oudste -- willekeurig maar STABIEL, en dat is wat 436 * telt: een id dat per ophaalactie verspringt is erger dan een id dat niet de 437 * mooiste keuze is. 438 * 439 * Eén zoekopdracht voor alle rijen samen, zoals trackHostPosts. Per track 440 * vragen wordt bij tweehonderd nummers tweehonderd zoekopdrachten. 441 */ 442 export function trackAlbums(siteId) { 443 const rijen = db.prepare(` 444 SELECT pt.track_id AS tid, p.id, p.title, p.artist, p.year, p.cover_url, 445 p.release_date, p.mb_release_id, p.created_at 446 FROM playlist_tracks pt 447 JOIN playlists p ON p.id = pt.playlist_id 448 WHERE p.site_id = ? AND p.kind = 'album' 449 ORDER BY p.created_at, p.id 450 `).all(siteId); 451 const uit = new Map(); 452 for (const r of rijen) if (!uit.has(r.tid)) uit.set(r.tid, r); 453 return uit; 454 } 455 456 /** 457 * Een uitgave als `fw:Album`. 458 * 459 * INGESLOTEN EN NIET ALS URI, en dat is het hele punt van deze stap. Funkwhale's 460 * TrackSerializer heeft `album = AlbumSerializer()` -- een object met name, 461 * published en een eigen artist_credit. Een kale URI expandeert naar een knoop 462 * met alleen een @id en valt daar dus af. Emissary stuurt precies zo'n kale URI, 463 * en dat is waarom hun tracks bij Funkwhale net zo goed stranden. 464 * 465 * Het `id` is de bestaande playlist-collectie: dereferenceerbaar, en het is 466 * werkelijk hetzelfde ding. We verzinnen geen tweede adres voor iets dat er al 467 * een heeft. 468 */ 469 export function buildAlbumObject(base, site, pl) { 470 if (!pl) return null; 471 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); 472 const wanneer = pl.created_at ? new Date(pl.created_at).toISOString() : new Date(0).toISOString(); 473 const album = { 474 type: 'Album', 475 id: `${actorId(base, site.slug)}/playlists/${pl.id}`, 476 name: pl.title, 477 published: wanneer, 478 attributedTo: actorId(base, site.slug), 479 artist_credit: artistCredit(base, site, pl.artist, wanneer), 480 }; 481 // `released` alleen als er een ECHTE datum is. `year` vult hem niet aan: een 482 // jaartal is geen dag, en dat is de reden dat release_date bestaat. 483 if (pl.release_date) album.released = pl.release_date; 484 if (pl.mb_release_id) album.musicbrainzId = pl.mb_release_id; 485 const hoes = abs(pl.cover_url || null); 486 if (hoes) album.image = { type: 'Image', mediaType: guessMediaType(hoes), url: hoes }; 487 return album; 488 } 489 401 490 export function buildPlaylistCollection(base, site, playlist, rows) { 402 491 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); … … 407 496 // De hoes van de playlist dient als terugval voor een track zonder eigen hoes. 408 497 const hostPosts = site.id ? trackHostPosts(site.id) : null; 409 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts })); 498 const albums = site.id ? trackAlbums(site.id) : null; 499 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts, albums })); 410 500 const out = pagedCollection(`${actorId(base, site.slug)}/playlists/${playlist.id}`, items, { 411 501 extra: { name: playlist.title, attributedTo: actorId(base, site.slug) }, … … 418 508 const cover = abs(playlist.cover_url || null); 419 509 if (cover) out.icon = { type: 'Image', mediaType: guessMediaType(cover), url: cover }; 510 511 // Is dit een UITGAVE, dan draagt deze collectie ook de albumvelden 512 // (shaer-756s, stap 2): het is het adres waar track.album naar wijst, en dan 513 // hoort hier hetzelfde te staan als in het ingesloten object. 514 // 515 // `type` blijft OrderedCollection, EN BLIJFT EEN STRING. Er stond hier even 516 // ['OrderedCollection', 'Album'] -- geldig AS2, en het is ook werkelijk 517 // allebei -- maar een bestaande test viel erover, en die test had gelijk: een 518 // lezer die `type` als tekst uitpakt (Shaer doet dat) verliest dan in stilte 519 // de hele playlist. Het kost ons niets, want hun AlbumSerializer declareert 520 // geen type-veld en valideert het dus niet: haalt Funkwhale dit adres op als 521 // album, dan leest hij deze velden gewoon. En het object dat hij echt gebruikt 522 // staat toch al ingesloten op de track. 523 if ((playlist.kind || 'album') === 'album') { 524 const album = buildAlbumObject(base, site, playlist); 525 for (const veld of ['published', 'released', 'musicbrainzId', 'artist_credit', 'image']) { 526 if (album[veld] !== undefined) out[veld] = album[veld]; 527 } 528 } 420 529 return leenVanPost(base, site, out, uitgavePost(site.id, playlist.id)); 421 530 } -
test/ap-fw-track.test.js
rd68ee3a raebde2b 55 55 }); 56 56 57 test('geen album zolang het bij ons geen object is', () => { 58 // Een tekstkolom als URI meesturen is een adres beloven dat niet bestaat. 59 assert.equal(audio('t1').track.album, undefined); 57 test('een track zonder uitgave draagt GEEN album', () => { 58 // Dit stond er in stap 1 als "nog geen album, want we hebben er geen object 59 // voor". Sinds stap 2 hebben we dat wel -- de album-playlist -- en is de regel 60 // scherper: geen uitgave, geen veld. De tekstkolom `album` op de track blijft 61 // buiten de draad, want dat is een label en geen adres. 62 db.prepare("INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES ('m9','s1','los.mp3','a/los.mp3','audio/mpeg',10)").run(); 63 db.prepare("INSERT INTO audio_tracks (id, site_id, title, album, media_id, fedi_open) VALUES ('t9','s1','Losse track','Een Albumnaam','m9',1)").run(); 64 const a = audio('t9'); 65 assert.equal(a.track.album, undefined, 'een albumNAAM is geen album'); 66 assert.equal(a.album, undefined); 60 67 }); 61 68 … … 136 143 db.prepare("UPDATE sites SET mb_artist_id = NULL WHERE id = 's1'").run(); 137 144 }); 145 146 // ── het album ingesloten (shaer-756s, stap 2) ──────────────────────── 147 // 148 // Hun TrackSerializer heeft `album = AlbumSerializer()`: een OBJECT met name, 149 // published en een eigen artist_credit. Een kale URI expandeert naar een knoop 150 // met alleen een @id en valt daar af -- dat is precies waarom Emissary's tracks 151 // bij Funkwhale ook stranden. 152 153 db.prepare(`INSERT INTO playlists (id, site_id, title, artist, year, cover_url, kind, release_date, mb_release_id, created_at) 154 VALUES ('de-plaat','s1','De Plaat','De Band',2024,'/media/hoes.jpg','album','2024-03-15','7c5a9b2e-1111-4222-8333-944455556666','2026-01-01T00:00:00Z')`).run(); 155 db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('de-plaat','t1',1)").run(); 156 // t2 zit in een MIXTAPE en hoort dus geen album te krijgen. 157 db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('de-mix','s1','De Mix','playlist')").run(); 158 db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('de-mix','t2',1)").run(); 159 160 test('een track op een uitgave draagt het album als OBJECT', () => { 161 const a = audio('t1'); 162 const al = a.track.album; 163 assert.ok(al && typeof al === 'object', 'album is geen object'); 164 assert.equal(al.type, 'Album'); 165 assert.equal(al.id, `${BASE}/ap/users/band/playlists/de-plaat`, 'het id is de bestaande collectie'); 166 assert.equal(al.name, 'De Plaat'); 167 assert.ok(al.published, 'MusicEntitySerializer eist published'); 168 assert.equal(al.released, '2024-03-15'); 169 assert.equal(al.musicbrainzId, '7c5a9b2e-1111-4222-8333-944455556666'); 170 assert.ok(Array.isArray(al.artist_credit) && al.artist_credit.length >= 1, 'album zonder artist_credit'); 171 assert.equal(al.image.type, 'Image'); 172 // Ook als URI op de Audio zelf -- Funkwhale 2.0 en Emissary doen dat allebei. 173 assert.equal(a.album, al.id); 174 }); 175 176 test('een track in een MIXTAPE krijgt geen album', () => { 177 // Een afspeellijst is geen uitgave. Zou hij hier een album krijgen, dan was 178 // het onderscheid album/playlist decoratie. 179 const a = audio('t2'); 180 assert.equal(a.track.album, undefined); 181 assert.equal(a.album, undefined); 182 }); 183 184 test('track en album delen dezelfde artiest', () => { 185 // Twee keer los bouwen is hoe ze uit elkaar gaan lopen; er is een functie. 186 const a = audio('t1'); 187 assert.deepEqual(a.track.artist_credit[0].artist, a.track.album.artist_credit[0].artist); 188 }); 189 190 test('de playlist-collectie draagt de albumvelden, met type als STRING', () => { 191 const pl = db.prepare("SELECT * FROM playlists WHERE id = 'de-plaat'").get(); 192 const col = AP.buildPlaylistCollection(BASE, site, pl, AP.playlistOpenTracks('de-plaat')); 193 assert.equal(typeof col.type, 'string', 'een array breekt lezers die type als tekst uitpakken'); 194 assert.equal(col.type, 'OrderedCollection'); 195 assert.equal(col.released, '2024-03-15'); 196 assert.equal(col.musicbrainzId, '7c5a9b2e-1111-4222-8333-944455556666'); 197 assert.ok(col.artist_credit); 198 }); 199 200 test('een mixtape-collectie krijgt GEEN albumvelden', () => { 201 const pl = db.prepare("SELECT * FROM playlists WHERE id = 'de-mix'").get(); 202 const col = AP.buildPlaylistCollection(BASE, site, pl, AP.playlistOpenTracks('de-mix')); 203 assert.equal(col.released, undefined); 204 assert.equal(col.artist_credit, undefined); 205 });
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)