Changes in src/services/music/index.js [9026de5:98acb97] in Klonkt
- File:
-
- 1 edited
-
src/services/music/index.js (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/music/index.js
r9026de5 r98acb97 14 14 15 15 import db from '../../config/database.js'; 16 import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection , isMbid} from '../ap-core.js';16 import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection } 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 zodat19 // ActivityPubService niet in een submap hoeft te grijpen.20 export * as luisteraars from './luisteraars.js';21 18 22 19 // m.size hoort erbij voor de RSS-enclosure: die eist een lengte in bytes. … … 136 133 } 137 134 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`, want143 * daar verwacht hun model hem. Er een id per artiestnaam van maken zou144 * identiteit uit een string zijn, en dat is de fout die we bij albums juist145 * vermijden.146 *147 * Eén functie voor beide, zodat een track en het album waar hij op staat nooit148 * 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 167 135 export function buildTrackAudio(base, site, r, opts = {}) { 168 136 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); … … 200 168 url: [...(post ? [{ type: 'Link', href: `${base}/${post.slug}`, mediaType: 'text/html' }] : []), bestand], 201 169 }; 202 // De bak waar dit bestand in hangt (shaer-0nh). Voor Funkwhale is dit het203 // haakje waaraan een upload komt te zitten; zonder dit veld blijft een track204 // daar een naam zonder geluid.205 a.library = libraryId(base, site);206 170 if (r.artist) a.summary = r.artist; // artiest als summary: kaal AS2, geen eigen vocab 207 171 // AS2-kern `context`: "de context waarbinnen dit object bestaat". Voor een … … 211 175 // van te maken. 212 176 if (post) a.context = noteId(base, post.id); 213 // Het NUMMER, los van dit bestand (shaer-3f8a, spoor B). Funkwhale en214 // Emissary lezen allebei `fw:track`, en petitminion noemde het ontbreken215 // ervan als eerste wat hem opviel aan onze objecten.216 //217 // EIGEN ID MET #track, en niet hetzelfde id als de Audio. Emissary hergebruikt218 // daar het object-id, maar dan zijn in JSON-LD de Audio en de Track EEN knoop219 // met twee typen -- en een bestand is geen werk. Dat verschil moeten we straks220 // toch maken, want een album verzamelt nummers en geen mp3's. Een fragment is221 // 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 ons224 // een tekstkolom; er hier een adres van maken zou een ding beloven dat niet225 // 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 een228 // artiest een tekstkolom op de track. Die twee verzoenen we zo: de entiteit229 // is de site-ACTOR -- een echt, opvraagbaar adres, het account dat dit230 // uitbrengt -- en de tekst uit de kolom gaat naar `credit`, want dat is231 // 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 een234 // artist_credit af uit onze attributedTo. We maken alleen expliciet wat daar235 // toch al gebeurde.236 //237 // DE GRENS ERVAN: brengt een site werk van iemand anders uit, dan zegt dit238 // dat de site de artiest is. Dat stond al in attributedTo, dus we maken het239 // niet erger -- maar het is wel de reden dat we hier geen id per artiestnaam240 // verzinnen. Identiteit uit een string is dezelfde fout als bij het album241 // (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 !== undefined256 ? (opts.albums && opts.albums.get(r.id)) || null257 : ((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 dat261 // allebei, en het scheelt een lezer het uitpakken van de track.262 a.album = a.track.album.id;263 }264 177 if (r.duration) a.duration = `PT${Math.round(r.duration)}S`; 265 178 if (r.created_at) a.published = new Date(r.created_at).toISOString(); … … 300 213 } 301 214 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 twee311 * vragen entiteiten waar wij tekst hebben; hiervoor hoeven we niets te312 * verzinnen. Een library is precies wat er al staat: onze open tracks, met een313 * echte telling en een echt id.314 *315 * WAAROM HET NODIG IS, gemeten op 13-8. open.audio heeft onze vier tracks316 * binnengehaald langs de AP-weg -- met ONZE track-id's, en met een artist_credit317 * dat Funkwhale zelf uit onze attributedTo afleidde. Maar `uploads` is leeg en318 * `is_playable` false. Bij hen hangt een upload aan een library; zonder library319 * is er geen bak om het bestand in te hangen. Het audiobestand zelf is wel320 * gewoon op te halen (200, audio/mpeg, ook anoniem) -- ze hebben het niet321 * 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 en325 * 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 er327 * 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 typen338 // (as:Collection of fw:Library) en zijn CollectionPageSerializer alleen339 // `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 noemt346 // `audience` optioneel, maar zijn create() doet er meteen347 // `privacy[validated_data["audience"]]` mee -- zonder de sleutel is dat348 // een KeyError en geeft hun server een 500. Dat is wat open.audio op 15-8349 // 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' en353 // '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 de356 // 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 363 215 /** De collectie van alle open tracks van een site (shaer-0nh, stap 3). */ 364 export function buildTrackCollection(base, site, rows , { page = false } = {}) {216 export function buildTrackCollection(base, site, rows) { 365 217 // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts. 366 218 const posts = site.id ? trackHostPosts(site.id) : null; 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) } }); 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) } }); 370 221 } 371 222 … … 412 263 // collectie zelf, want ook een lijst mag niet verklappen wat er achter de 413 264 // poort staat. 414 export function listPlaylistsAP(base, site, enriched , { page = false } = {}) {265 export function listPlaylistsAP(base, site, enriched) { 415 266 const rows = db.prepare( 416 267 'SELECT id, title, artist, year, cover_url FROM playlists WHERE site_id = ? ORDER BY created_at, id' … … 425 276 return stub; 426 277 }); 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; 278 return pagedCollection(colId, items, { extra: { attributedTo: actorId(base, site.slug) } }); 529 279 } 530 280 … … 537 287 // De hoes van de playlist dient als terugval voor een track zonder eigen hoes. 538 288 const hostPosts = site.id ? trackHostPosts(site.id) : null; 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 })); 289 const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts })); 546 290 const out = pagedCollection(`${actorId(base, site.slug)}/playlists/${playlist.id}`, items, { 547 291 extra: { name: playlist.title, attributedTo: actorId(base, site.slug) }, … … 554 298 const cover = abs(playlist.cover_url || null); 555 299 if (cover) out.icon = { type: 'Image', mediaType: guessMediaType(cover), url: cover }; 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); 300 return leenVanPost(base, site, out, uitgavePost(site.id, playlist.id)); 576 301 } 577 302 … … 598 323 try { 599 324 const rijen = db.prepare(` 600 SELECT id, slug, title, excerpt, content, cover_image_url, tags, 601 COALESCE(published_at, created_at) AS uit_wanneer 325 SELECT id, slug, title, excerpt, content, cover_image_url, tags 602 326 FROM posts 603 327 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)