Changes in src/services/music/index.js [98acb97:9026de5] in Klonkt
- File:
-
- 1 edited
-
src/services/music/index.js (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/music/index.js
r98acb97 r9026de5 14 14 15 15 import db from '../../config/database.js'; 16 import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection } from '../ap-core.js';16 import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection, isMbid } from '../ap-core.js'; 17 17 import { afleidenUitInsluitingen, ingeslotenPlaylists } from '../../assets/js/shared/post-music-type.js'; 18 // De luisteraars horen bij de muziekkant; hier doorgegeven zodat 19 // ActivityPubService niet in een submap hoeft te grijpen. 20 export * as luisteraars from './luisteraars.js'; 18 21 19 22 // m.size hoort erbij voor de RSS-enclosure: die eist een lengte in bytes. … … 133 136 } 134 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 }]; 165 } 166 135 167 export function buildTrackAudio(base, site, r, opts = {}) { 136 168 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); … … 168 200 url: [...(post ? [{ type: 'Link', href: `${base}/${post.slug}`, mediaType: 'text/html' }] : []), bestand], 169 201 }; 202 // De bak waar dit bestand in hangt (shaer-0nh). Voor Funkwhale is dit het 203 // haakje waaraan een upload komt te zitten; zonder dit veld blijft een track 204 // daar een naam zonder geluid. 205 a.library = libraryId(base, site); 170 206 if (r.artist) a.summary = r.artist; // artiest als summary: kaal AS2, geen eigen vocab 171 207 // AS2-kern `context`: "de context waarbinnen dit object bestaat". Voor een … … 175 211 // van te maken. 176 212 if (post) a.context = noteId(base, post.id); 213 // Het NUMMER, los van dit bestand (shaer-3f8a, spoor B). Funkwhale en 214 // Emissary lezen allebei `fw:track`, en petitminion noemde het ontbreken 215 // ervan als eerste wat hem opviel aan onze objecten. 216 // 217 // EIGEN ID MET #track, en niet hetzelfde id als de Audio. Emissary hergebruikt 218 // daar het object-id, maar dan zijn in JSON-LD de Audio en de Track EEN knoop 219 // met twee typen -- en een bestand is geen werk. Dat verschil moeten we straks 220 // toch maken, want een album verzamelt nummers en geen mp3's. Een fragment is 221 // een geldige IRI en wijst naar hetzelfde document. 222 // 223 // GEEN `album`. Dat veld is bij hen een URI naar een Album-object en bij ons 224 // een tekstkolom; er hier een adres van maken zou een ding beloven dat niet 225 // bestaat. Zie shaer-k37k -- dat is de keuze die daarvoor eerst moet vallen. 226 // 227 // WIE IS DE ARTIEST. Hun Artist is een ENTITEIT met een id, en bij ons is een 228 // artiest een tekstkolom op de track. Die twee verzoenen we zo: de entiteit 229 // is de site-ACTOR -- een echt, opvraagbaar adres, het account dat dit 230 // uitbrengt -- en de tekst uit de kolom gaat naar `credit`, want dat is 231 // precies waar hun model de credittekst verwacht. 232 // 233 // Dat is eerlijk en het is niet nieuw: open.audio leidde op 13-8 al zelf een 234 // artist_credit af uit onze attributedTo. We maken alleen expliciet wat daar 235 // toch al gebeurde. 236 // 237 // DE GRENS ERVAN: brengt een site werk van iemand anders uit, dan zegt dit 238 // dat de site de artiest is. Dat stond al in attributedTo, dus we maken het 239 // niet erger -- maar het is wel de reden dat we hier geen id per artiestnaam 240 // verzinnen. Identiteit uit een string is dezelfde fout als bij het album 241 // (shaer-756s). 242 const wanneer = r.created_at ? new Date(r.created_at).toISOString() 243 : (site.created_at ? new Date(site.created_at).toISOString() : new Date(0).toISOString()); 244 245 a.track = { 246 type: 'Track', 247 id: `${a.id}#track`, 248 name: a.name, 249 published: wanneer, 250 ...(Number(r.position) ? { position: Number(r.position) } : {}), 251 artist_credit: artistCredit(base, site, r.artist, wanneer), 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 } 177 264 if (r.duration) a.duration = `PT${Math.round(r.duration)}S`; 178 265 if (r.created_at) a.published = new Date(r.created_at).toISOString(); … … 213 300 } 214 301 302 /** Het AS2-id van de bibliotheek van een site. */ 303 export function libraryId(base, site) { 304 return `${actorId(base, site.slug)}/library`; 305 } 306 307 /** 308 * De site als Funkwhale-LIBRARY (skelet). 309 * 310 * WAAROM DIT GEEN DIALECT IS ZOALS track EN ArtistCredit DAT WEL ZIJN. Die twee 311 * vragen entiteiten waar wij tekst hebben; hiervoor hoeven we niets te 312 * verzinnen. Een library is precies wat er al staat: onze open tracks, met een 313 * echte telling en een echt id. 314 * 315 * WAAROM HET NODIG IS, gemeten op 13-8. open.audio heeft onze vier tracks 316 * binnengehaald langs de AP-weg -- met ONZE track-id's, en met een artist_credit 317 * dat Funkwhale zelf uit onze attributedTo afleidde. Maar `uploads` is leeg en 318 * `is_playable` false. Bij hen hangt een upload aan een library; zonder library 319 * is er geen bak om het bestand in te hangen. Het audiobestand zelf is wel 320 * gewoon op te halen (200, audio/mpeg, ook anoniem) -- ze hebben het niet 321 * geprobeerd. 322 * 323 * SKELET, en dat woord is letterlijk bedoeld. Dit is de vorm uit hun docs: 324 * type, id, name, followers, totalItems, first, last, plus attributedTo en 325 * summary. Wat er NIET is: de volg-afhandeling. Onze bibliotheek is openbaar -- 326 * elke track erin heeft fedi_open -- dus er valt niets goed te keuren. Komt er 327 * ooit een besloten variant, dan hoort daar het Follow/Accept-werk bij. 328 */ 329 export function buildLibrary(base, site, rows, { page = false } = {}) { 330 const id = libraryId(base, site); 331 const hostPosts = site.id ? trackHostPosts(site.id) : null; 332 const albums = site.id ? trackAlbums(site.id) : null; 333 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts, albums })); 334 return pagedCollection(id, items, { 335 page, 336 // Een platenkast is geen tijdlijn: `Collection`, niet `OrderedCollection`. 337 // Funkwhale's LibrarySerializer accepteert ook alleen die twee typen 338 // (as:Collection of fw:Library) en zijn CollectionPageSerializer alleen 339 // `CollectionPage` met `items`. 340 ongeordend: true, 341 extra: { 342 type: 'Library', 343 name: site.title || site.slug, 344 attributedTo: actorId(base, site.slug), 345 // WAAROM DIT VELD ER MOET STAAN. Funkwhale's LibrarySerializer noemt 346 // `audience` optioneel, maar zijn create() doet er meteen 347 // `privacy[validated_data["audience"]]` mee -- zonder de sleutel is dat 348 // een KeyError en geeft hun server een 500. Dat is wat open.audio op 15-8 349 // teruggaf toen Robin onze library-URI daar opzocht. 350 // 351 // Het is bovendien gewoon waar: alles hierin is fedi_open, dus openbaar. 352 // Bij hen is dit precies het verschil tussen privacy_level 'everyone' en 353 // 'me' -- oftewel of onze nummers daar afspeelbaar zijn. 354 audience: 'https://www.w3.org/ns/activitystreams#Public', 355 // Vereist volgens hun docs. Openbaar, dus de telling is eerlijk en de 356 // lijst blijft leeg -- wie ons volgt volgt de ACTOR, niet de bak. 357 followers: `${id}/followers`, 358 ...(site.description ? { summary: String(site.description).slice(0, 500) } : {}), 359 }, 360 }); 361 } 362 215 363 /** De collectie van alle open tracks van een site (shaer-0nh, stap 3). */ 216 export function buildTrackCollection(base, site, rows ) {364 export function buildTrackCollection(base, site, rows, { page = false } = {}) { 217 365 // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts. 218 366 const posts = site.id ? trackHostPosts(site.id) : null; 219 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts })); 220 return pagedCollection(`${actorId(base, site.slug)}/tracks`, items, { extra: { attributedTo: actorId(base, site.slug) } }); 367 const albums = site.id ? trackAlbums(site.id) : null; 368 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts, albums })); 369 return pagedCollection(`${actorId(base, site.slug)}/tracks`, items, { page, extra: { attributedTo: actorId(base, site.slug) } }); 221 370 } 222 371 … … 263 412 // collectie zelf, want ook een lijst mag niet verklappen wat er achter de 264 413 // poort staat. 265 export function listPlaylistsAP(base, site, enriched ) {414 export function listPlaylistsAP(base, site, enriched, { page = false } = {}) { 266 415 const rows = db.prepare( 267 416 'SELECT id, title, artist, year, cover_url FROM playlists WHERE site_id = ? ORDER BY created_at, id' … … 276 425 return stub; 277 426 }); 278 return pagedCollection(colId, items, { extra: { attributedTo: actorId(base, site.slug) } }); 427 return pagedCollection(colId, items, { page, extra: { attributedTo: actorId(base, site.slug) } }); 428 } 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 // De post die deze plaat uitbrengt, EEN keer per album opgezocht en niet per 454 // track: uitgavePost() doet er echt werk voor (hij leest de typering van de 455 // post) en een site heeft veel meer nummers dan platen. 456 // 457 // WAAROM DIT ERBIJ MOET: buildPlaylistCollection laat leenVanPost de naam van 458 // de post overnemen -- de post IS de uitgave. Zonder dezelfde lening hier zou 459 // het ingesloten Album "Cartoon Epic" heten en zijn eigen URI "Geen koffie, 460 // wel thee!". Een id met twee namen, en dat is precies wat op 16-8 uit de 461 // meting rolde. 462 const perAlbum = new Map(); 463 for (const r of uit.values()) { 464 if (perAlbum.has(r.id)) continue; 465 perAlbum.set(r.id, uitgavePost(siteId, r.id)); 466 } 467 for (const r of uit.values()) r._post = perAlbum.get(r.id) || null; 468 return uit; 469 } 470 471 /** 472 * Een uitgave als `fw:Album`. 473 * 474 * INGESLOTEN EN NIET ALS URI, en dat is het hele punt van deze stap. Funkwhale's 475 * TrackSerializer heeft `album = AlbumSerializer()` -- een object met name, 476 * published en een eigen artist_credit. Een kale URI expandeert naar een knoop 477 * met alleen een @id en valt daar dus af. Emissary stuurt precies zo'n kale URI, 478 * en dat is waarom hun tracks bij Funkwhale net zo goed stranden. 479 * 480 * Het `id` is de bestaande playlist-collectie: dereferenceerbaar, en het is 481 * werkelijk hetzelfde ding. We verzinnen geen tweede adres voor iets dat er al 482 * een heeft. 483 */ 484 export function buildAlbumObject(base, site, pl) { 485 if (!pl) return null; 486 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); 487 // WANNEER IS DEZE PLAAT GEPUBLICEERD. De post die hem uitbrengt gaat voor, en 488 // niet als noodgreep maar omdat hij het beter weet: playlists.created_at is 489 // het moment waarop de RIJ is aangemaakt, en dat kan weken eerder zijn terwijl 490 // je nog aan het samenstellen was. AS2 `published` vraagt wanneer het object 491 // openbaar werd, en dat is de post. 492 // 493 // GEEN epoch als laatste terugval. `published` is bij hen verplicht, maar 1970 494 // is een ANTWOORD en geen ontbrekend veld -- en dat is erger: een lezer kan een 495 // gat opmerken, een leugen niet. Zo kwam op 16-8 de route boven water die id, 496 // title, artist, year, cover_url en kind selecteerde en de rest niet. 497 // 498 // OOK VOOR `released`, en daar had ik het eerst mis (Robin, 16-8). Mijn 499 // bezwaar was: post je vandaag een plaat uit 2018, dan beweert dit dat hij 500 // vandaag uitkwam. Dat gebeurt ook -- maar bij de meeste Klonkt-sites IS de 501 // post het uitbrengen, en GEEN datum is slechter dan een datum die op het 502 // gewone geval klopt. Het handmatige veld is precies het gereedschap voor de 503 // uitzondering: bij een heruitgave vul je hem in en die wint. 504 const postDatum = (pl._post && pl._post.uit_wanneer) ? new Date(pl._post.uit_wanneer) : null; 505 const wanneer = postDatum ? postDatum.toISOString() 506 : (pl.created_at ? new Date(pl.created_at).toISOString() : null); 507 // Dezelfde lening als in buildPlaylistCollection: de post die de plaat 508 // uitbrengt geeft zijn titel, en de eigen titel blijft als alsoKnownAs staan. 509 const titel = (pl._post && pl._post.title) || pl.title; 510 const album = { 511 type: 'Album', 512 id: `${actorId(base, site.slug)}/playlists/${pl.id}`, 513 name: titel, 514 ...(wanneer ? { published: wanneer } : {}), 515 attributedTo: actorId(base, site.slug), 516 artist_credit: artistCredit(base, site, pl.artist, wanneer || new Date().toISOString()), 517 }; 518 if (titel !== pl.title) album.alsoKnownAs = pl.title; 519 // Het ingevulde veld wint altijd; anders de DAG waarop de post verscheen. 520 // `year` vult hem nog steeds niet aan, en dat is geen inconsequentie: een 521 // jaartal is geen dag, terwijl de postdatum een gebeurtenis is die werkelijk 522 // heeft plaatsgevonden. Het verschil is verzinnen versus afleiden. 523 if (pl.release_date) album.released = pl.release_date; 524 else if (postDatum) album.released = postDatum.toISOString().slice(0, 10); 525 if (pl.mb_release_id) album.musicbrainzId = pl.mb_release_id; 526 const hoes = abs(pl.cover_url || null); 527 if (hoes) album.image = { type: 'Image', mediaType: guessMediaType(hoes), url: hoes }; 528 return album; 279 529 } 280 530 … … 287 537 // De hoes van de playlist dient als terugval voor een track zonder eigen hoes. 288 538 const hostPosts = site.id ? trackHostPosts(site.id) : null; 289 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts })); 539 const albums = site.id ? trackAlbums(site.id) : null; 540 // EEN keer opzoeken en twee keer gebruiken: het Album leent er zijn datums en 541 // titel van, leenVanPost onderaan zijn tekst en tags. Twee losse aanroepen 542 // zouden niet alleen dubbel werk zijn maar ook uiteen kunnen lopen -- en dan 543 // staat er weer iets anders op het ingesloten object dan op zijn eigen URI. 544 const post = site.id ? uitgavePost(site.id, playlist.id) : null; 545 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts, albums })); 290 546 const out = pagedCollection(`${actorId(base, site.slug)}/playlists/${playlist.id}`, items, { 291 547 extra: { name: playlist.title, attributedTo: actorId(base, site.slug) }, … … 298 554 const cover = abs(playlist.cover_url || null); 299 555 if (cover) out.icon = { type: 'Image', mediaType: guessMediaType(cover), url: cover }; 300 return leenVanPost(base, site, out, uitgavePost(site.id, playlist.id)); 556 557 // Is dit een UITGAVE, dan draagt deze collectie ook de albumvelden 558 // (shaer-756s, stap 2): het is het adres waar track.album naar wijst, en dan 559 // hoort hier hetzelfde te staan als in het ingesloten object. 560 // 561 // `type` blijft OrderedCollection, EN BLIJFT EEN STRING. Er stond hier even 562 // ['OrderedCollection', 'Album'] -- geldig AS2, en het is ook werkelijk 563 // allebei -- maar een bestaande test viel erover, en die test had gelijk: een 564 // lezer die `type` als tekst uitpakt (Shaer doet dat) verliest dan in stilte 565 // de hele playlist. Het kost ons niets, want hun AlbumSerializer declareert 566 // geen type-veld en valideert het dus niet: haalt Funkwhale dit adres op als 567 // album, dan leest hij deze velden gewoon. En het object dat hij echt gebruikt 568 // staat toch al ingesloten op de track. 569 if ((playlist.kind || 'album') === 'album') { 570 const album = buildAlbumObject(base, site, { ...playlist, _post: post }); 571 for (const veld of ['published', 'released', 'musicbrainzId', 'artist_credit', 'image']) { 572 if (album[veld] !== undefined) out[veld] = album[veld]; 573 } 574 } 575 return leenVanPost(base, site, out, post); 301 576 } 302 577 … … 323 598 try { 324 599 const rijen = db.prepare(` 325 SELECT id, slug, title, excerpt, content, cover_image_url, tags 600 SELECT id, slug, title, excerpt, content, cover_image_url, tags, 601 COALESCE(published_at, created_at) AS uit_wanneer 326 602 FROM posts 327 603 WHERE site_id = ? AND status = 'published'
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)