| 1 | /**
|
|---|
| 2 | * De muziekkant van ActivityPub: een track als AS2-object, de collecties
|
|---|
| 3 | * eromheen, en welke post hem uitbrengt.
|
|---|
| 4 | *
|
|---|
| 5 | * Waarom een eigen map (shaer-drc): ActivityPubService was 6400 regels over
|
|---|
| 6 | * tweeentwintig onderwerpen. Dit is het eerste onderwerp dat er als geheel uit
|
|---|
| 7 | * kan, en het is meteen het onderwerp dat gaat GROEIEN -- de typering uit
|
|---|
| 8 | * shaer-cyg (playlist versus album, afgeleid uit wat er in de post staat) landt
|
|---|
| 9 | * hier straks.
|
|---|
| 10 | *
|
|---|
| 11 | * De regel die guardianship al aanhoudt geldt hier ook: deze map importeert
|
|---|
| 12 | * alleen db en ap-core, en NOOIT terug uit ActivityPubService.
|
|---|
| 13 | */
|
|---|
| 14 |
|
|---|
| 15 | import db from '../../config/database.js';
|
|---|
| 16 | import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType } from '../ap-core.js';
|
|---|
| 17 |
|
|---|
| 18 | // m.size hoort erbij voor de RSS-enclosure: die eist een lengte in bytes.
|
|---|
| 19 | const TRACK_KOLOMMEN = `t.id, t.title, t.artist, t.duration, t.cover_url, t.created_at,
|
|---|
| 20 | t.position, t.license,
|
|---|
| 21 | m.filename, m.storage_path, m.mime_type, m.size`;
|
|---|
| 22 |
|
|---|
| 23 | export function playlistOpenTracks(playlistId) {
|
|---|
| 24 | return db.prepare(
|
|---|
| 25 | `SELECT ${TRACK_KOLOMMEN}
|
|---|
| 26 | FROM playlist_tracks pt
|
|---|
| 27 | JOIN audio_tracks t ON t.id = pt.track_id
|
|---|
| 28 | JOIN media m ON m.id = t.media_id
|
|---|
| 29 | WHERE pt.playlist_id = ? AND t.fedi_open = 1
|
|---|
| 30 | ORDER BY pt.position`
|
|---|
| 31 | ).all(playlistId);
|
|---|
| 32 | }
|
|---|
| 33 |
|
|---|
| 34 | /**
|
|---|
| 35 | * Alle tracks die deze site aan de federatie heeft opengezet (shaer-0nh, stap 3).
|
|---|
| 36 | *
|
|---|
| 37 | * Dit is de KANONIEKE plek, niet de playlist: een playlist is een keuze, dit is
|
|---|
| 38 | * wat de artiest heeft uitgebracht. Een track die in geen enkele playlist zit
|
|---|
| 39 | * was tot nu toe onzichtbaar voor de federatie -- die staat hier wel.
|
|---|
| 40 | */
|
|---|
| 41 | export function siteOpenTracks(siteId) {
|
|---|
| 42 | return db.prepare(
|
|---|
| 43 | `SELECT ${TRACK_KOLOMMEN}
|
|---|
| 44 | FROM audio_tracks t JOIN media m ON m.id = t.media_id
|
|---|
| 45 | WHERE t.site_id = ? AND t.fedi_open = 1
|
|---|
| 46 | ORDER BY t.position, t.created_at, t.id`
|
|---|
| 47 | ).all(siteId);
|
|---|
| 48 | }
|
|---|
| 49 |
|
|---|
| 50 | export function openTrack(siteId, trackId) {
|
|---|
| 51 | return db.prepare(
|
|---|
| 52 | `SELECT ${TRACK_KOLOMMEN}
|
|---|
| 53 | FROM audio_tracks t JOIN media m ON m.id = t.media_id
|
|---|
| 54 | WHERE t.site_id = ? AND t.id = ? AND t.fedi_open = 1`
|
|---|
| 55 | ).get(siteId, trackId);
|
|---|
| 56 | }
|
|---|
| 57 |
|
|---|
| 58 | /**
|
|---|
| 59 | * Eén track als AS2 `Audio`, met een EIGEN id (shaer-0nh, stap 3).
|
|---|
| 60 | *
|
|---|
| 61 | * Waarom dat id het verschil maakt: zonder id is een track een naamloze bijlage
|
|---|
| 62 | * die alleen bestaat zolang je het omhullende object vasthoudt. Met id is het
|
|---|
| 63 | * een ding waar je naar kunt wijzen, dat je los kunt ophalen, en dat in twee
|
|---|
| 64 | * playlists hetzelfde ding is. Funkwhale adresseert zijn Audio-objecten
|
|---|
| 65 | * precies zo, per stuk, in Create en Delete.
|
|---|
| 66 | *
|
|---|
| 67 | * `url` is een Link-ARRAY, net als bij Funkwhale en net als wat onze eigen
|
|---|
| 68 | * inbox sinds bdcb3a3 verwacht: de mediaType hoort bij de link, niet bij het
|
|---|
| 69 | * object. Er zit GEEN text/html-link in: Klonkt heeft geen trackpagina -- een
|
|---|
| 70 | * track wordt getoond binnen een post, en een post over vijf nummers is niet de
|
|---|
| 71 | * pagina van dit ene nummer. Liever geen link dan een link die iets anders
|
|---|
| 72 | * belooft.
|
|---|
| 73 | */
|
|---|
| 74 | /**
|
|---|
| 75 | * Bij welke post hoort een track? (shaer-0nh)
|
|---|
| 76 | *
|
|---|
| 77 | * Een track staat nooit los in Klonkt: hij wordt getoond BINNEN een post, via
|
|---|
| 78 | * een van drie insluitingen in posts.content. Die relatie stond alleen in die
|
|---|
| 79 | * tekst en nergens op de draad -- waardoor Shaer, dat zijn feed uit de outbox
|
|---|
| 80 | * bouwt, sinds fb22f78 losse Audio-kaarten kreeg zonder inhoud.
|
|---|
| 81 | *
|
|---|
| 82 | * ALLES IN EEN ZOEKOPDRACHT, niet per track. De collectie loopt over elke open
|
|---|
| 83 | * track, en drie LIKE-scans per stuk wordt bij tweehonderd nummers zeshonderd
|
|---|
| 84 | * scans. Nu is het er een, en de map gaat mee als optie.
|
|---|
| 85 | *
|
|---|
| 86 | * De rang bepaalt welke post wint als er meerdere zijn: rechtstreeks ingesloten
|
|---|
| 87 | * is specifieker dan via een playlist, en die weer specifieker dan via een
|
|---|
| 88 | * albumnaam. Bij gelijke rang de nieuwste post -- dat is waar iemand hem het
|
|---|
| 89 | * laatst heeft uitgebracht.
|
|---|
| 90 | */
|
|---|
| 91 | export function trackHostPosts(siteId) {
|
|---|
| 92 | const rijen = db.prepare(`
|
|---|
| 93 | SELECT tid, post_id, post_slug, rang, wanneer FROM (
|
|---|
| 94 | SELECT t.id AS tid, p.id AS post_id, p.slug AS post_slug, 1 AS rang,
|
|---|
| 95 | COALESCE(p.published_at, p.created_at) AS wanneer
|
|---|
| 96 | FROM audio_tracks t
|
|---|
| 97 | JOIN posts p ON p.site_id = t.site_id AND p.status = 'published'
|
|---|
| 98 | AND p.content LIKE '%[[track:' || t.id || ']]%'
|
|---|
| 99 | WHERE t.site_id = ? AND t.fedi_open = 1
|
|---|
| 100 | UNION ALL
|
|---|
| 101 | SELECT t.id, p.id, p.slug, 2, COALESCE(p.published_at, p.created_at)
|
|---|
| 102 | FROM playlist_tracks pt
|
|---|
| 103 | JOIN audio_tracks t ON t.id = pt.track_id
|
|---|
| 104 | JOIN posts p ON p.site_id = t.site_id AND p.status = 'published'
|
|---|
| 105 | AND p.content LIKE '%[[playlist:' || pt.playlist_id || ']]%'
|
|---|
| 106 | WHERE t.site_id = ? AND t.fedi_open = 1
|
|---|
| 107 | UNION ALL
|
|---|
| 108 | SELECT t.id, p.id, p.slug, 3, COALESCE(p.published_at, p.created_at)
|
|---|
| 109 | FROM audio_tracks t
|
|---|
| 110 | JOIN posts p ON p.site_id = t.site_id AND p.status = 'published'
|
|---|
| 111 | AND p.content LIKE '%[[album:' || t.album || ']]%'
|
|---|
| 112 | WHERE t.site_id = ? AND t.fedi_open = 1 AND t.album IS NOT NULL AND t.album <> ''
|
|---|
| 113 | ) ORDER BY rang, wanneer DESC
|
|---|
| 114 | `).all(siteId, siteId, siteId);
|
|---|
| 115 | const uit = new Map();
|
|---|
| 116 | for (const r of rijen) if (!uit.has(r.tid)) uit.set(r.tid, { id: r.post_id, slug: r.post_slug });
|
|---|
| 117 | return uit;
|
|---|
| 118 | }
|
|---|
| 119 |
|
|---|
| 120 | export function buildTrackAudio(base, site, r, opts = {}) {
|
|---|
| 121 | const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
|
|---|
| 122 | const fn = r.filename || (r.storage_path || '').split('/').pop();
|
|---|
| 123 | // De bestandsgegevens horen bij de LINK, niet bij het object: het is die ene
|
|---|
| 124 | // representatie die zoveel bytes is en die bitrate heeft, niet het nummer.
|
|---|
| 125 | // Zo doet Funkwhale het ook.
|
|---|
| 126 | // De post waar dit nummer in staat. Meegegeven door de collectie (een
|
|---|
| 127 | // zoekopdracht voor alles), of hier opgezocht als deze track los wordt
|
|---|
| 128 | // opgehaald. `hostPosts` mag expliciet null zijn: dan is er niets te zoeken.
|
|---|
| 129 | const post = opts.hostPosts !== undefined
|
|---|
| 130 | ? (opts.hostPosts && opts.hostPosts.get(r.id)) || null
|
|---|
| 131 | : ((site.id && trackHostPosts(site.id).get(r.id)) || null);
|
|---|
| 132 |
|
|---|
| 133 | const bestand = { type: 'Link', href: `${base}/audio/stream/${encodeURIComponent(fn)}`, mediaType: r.mime_type || 'audio/mpeg' };
|
|---|
| 134 | if (Number(r.size)) bestand.size = Number(r.size);
|
|---|
| 135 | // Bitrate leiden we af uit bytes en seconden. Geen gok: voor een bestand IS
|
|---|
| 136 | // dat de gemiddelde bitrate, en bij CBR ook de echte. Alleen als we allebei
|
|---|
| 137 | // de getallen hebben -- liever geen veld dan een verzonnen getal.
|
|---|
| 138 | if (Number(r.size) && Number(r.duration)) bestand.bitrate = Math.round((Number(r.size) * 8) / Number(r.duration));
|
|---|
| 139 |
|
|---|
| 140 | const a = {
|
|---|
| 141 | ...(opts.standalone ? { '@context': AP_CONTEXT } : {}),
|
|---|
| 142 | id: `${actorId(base, site.slug)}/tracks/${encodeURIComponent(r.id)}`,
|
|---|
| 143 | type: 'Audio',
|
|---|
| 144 | name: r.title || 'Audio',
|
|---|
| 145 | attributedTo: actorId(base, site.slug),
|
|---|
| 146 | // Op het OBJECT, niet alleen op de omhullende Create: een los opgehaalde
|
|---|
| 147 | // track moet zelf kunnen zeggen dat hij openbaar is.
|
|---|
| 148 | to: [PUBLIC],
|
|---|
| 149 | // De post die dit nummer uitbrengt staat VOORAAN als text/html, precies
|
|---|
| 150 | // zoals Funkwhale zijn trackpagina zet. Wij hadden dat veld leeg gelaten
|
|---|
| 151 | // omdat Klonkt geen trackpagina heeft -- maar de post IS waar je het kunt
|
|---|
| 152 | // horen, en dat is wat zo'n link betekent.
|
|---|
| 153 | url: [...(post ? [{ type: 'Link', href: `${base}/${post.slug}`, mediaType: 'text/html' }] : []), bestand],
|
|---|
| 154 | };
|
|---|
| 155 | if (r.artist) a.summary = r.artist; // artiest als summary: kaal AS2, geen eigen vocab
|
|---|
| 156 | // AS2-kern `context`: "de context waarbinnen dit object bestaat". Voor een
|
|---|
| 157 | // track is dat de post die hem uitbrengt. Daarmee is de relatie die tot nu
|
|---|
| 158 | // toe alleen in posts.content stond, op de draad te zien -- en kan een lezer
|
|---|
| 159 | // die de post al heeft dit nummer overslaan in plaats van er een lege kaart
|
|---|
| 160 | // van te maken.
|
|---|
| 161 | if (post) a.context = noteId(base, post.id);
|
|---|
| 162 | if (r.duration) a.duration = `PT${Math.round(r.duration)}S`;
|
|---|
| 163 | if (r.created_at) a.published = new Date(r.created_at).toISOString();
|
|---|
| 164 | if (Number(r.position)) a.position = Number(r.position);
|
|---|
| 165 | const lic = licentieUri(r.license);
|
|---|
| 166 | if (lic) a.license = lic;
|
|---|
| 167 | const art = abs(r.cover_url || opts.coverFallback || null);
|
|---|
| 168 | // icon EN image: allebei AS2-kern. Wij gebruikten alleen icon; Funkwhale
|
|---|
| 169 | // leest image. Dezelfde hoes, twee namen, niemand die iets misloopt.
|
|---|
| 170 | if (art) {
|
|---|
| 171 | const plaat = { type: 'Image', mediaType: guessMediaType(art), url: art };
|
|---|
| 172 | a.icon = plaat;
|
|---|
| 173 | a.image = plaat;
|
|---|
| 174 | }
|
|---|
| 175 | return a;
|
|---|
| 176 | }
|
|---|
| 177 |
|
|---|
| 178 | /**
|
|---|
| 179 | * Onze licentie is VRIJE TEKST uit een keuzelijst ("CC BY 4.0", "Alle rechten
|
|---|
| 180 | * voorbehouden"); schema.org en Funkwhale willen een URI. Alleen de waarden die
|
|---|
| 181 | * onze eigen keuzelijst aanbiedt worden vertaald -- die kennen we exact. Al het
|
|---|
| 182 | * andere levert niets op: een zelfbedachte licentie-URI is erger dan geen, want
|
|---|
| 183 | * een lezer gelooft hem.
|
|---|
| 184 | */
|
|---|
| 185 | const LICENTIES = {
|
|---|
| 186 | 'cc0 1.0 (publiek domein)': 'http://creativecommons.org/publicdomain/zero/1.0/',
|
|---|
| 187 | 'cc by 4.0': 'http://creativecommons.org/licenses/by/4.0/',
|
|---|
| 188 | 'cc by-sa 4.0': 'http://creativecommons.org/licenses/by-sa/4.0/',
|
|---|
| 189 | 'cc by-nc 4.0': 'http://creativecommons.org/licenses/by-nc/4.0/',
|
|---|
| 190 | 'cc by-nc-sa 4.0': 'http://creativecommons.org/licenses/by-nc-sa/4.0/',
|
|---|
| 191 | 'cc by-nd 4.0': 'http://creativecommons.org/licenses/by-nd/4.0/',
|
|---|
| 192 | };
|
|---|
| 193 | export function licentieUri(waarde) {
|
|---|
| 194 | const s = String(waarde || '').trim();
|
|---|
| 195 | if (!s) return null;
|
|---|
| 196 | if (/^https?:\/\//i.test(s)) return safeUrl(s); // iemand vulde al een URI in
|
|---|
| 197 | return LICENTIES[s.toLowerCase()] || null; // "Alle rechten voorbehouden" heeft er geen
|
|---|
| 198 | }
|
|---|
| 199 |
|
|---|
| 200 | /** De collectie van alle open tracks van een site (shaer-0nh, stap 3). */
|
|---|
| 201 | export function buildTrackCollection(base, site, rows) {
|
|---|
| 202 | return {
|
|---|
| 203 | '@context': AP_CONTEXT,
|
|---|
| 204 | id: `${actorId(base, site.slug)}/tracks`,
|
|---|
| 205 | type: 'OrderedCollection',
|
|---|
| 206 | attributedTo: actorId(base, site.slug),
|
|---|
| 207 | totalItems: (rows || []).length,
|
|---|
| 208 | // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts.
|
|---|
| 209 | orderedItems: (() => {
|
|---|
| 210 | const posts = site.id ? trackHostPosts(site.id) : null;
|
|---|
| 211 | return (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts }));
|
|---|
| 212 | })(),
|
|---|
| 213 | };
|
|---|
| 214 | }
|
|---|
| 215 |
|
|---|
| 216 | // Een post die een playlist insluit wijst in zijn AS2 ook naar de collectie
|
|---|
| 217 | // (shaer-ayc, stap 2): een Link-tag per ingesloten playlist. Mastodon
|
|---|
| 218 | // parseert alleen Mention/Hashtag/Emoji en negeert een Link geruisloos; een
|
|---|
| 219 | // client die hem kent haalt de collectie op. Opgelost uit post.content en
|
|---|
| 220 | // ALLEEN binnen de eigen site: playlist-ids zijn een globale primary key, dus
|
|---|
| 221 | // zonder site-check zou een post van site A naar de collectie van site B
|
|---|
| 222 | // kunnen wijzen.
|
|---|
| 223 | export function playlistLinkTags(base, site, content) {
|
|---|
| 224 | const out = [];
|
|---|
| 225 | const seen = new Set();
|
|---|
| 226 | try {
|
|---|
| 227 | for (const m of (content || '').matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) {
|
|---|
| 228 | if (seen.has(m[1])) continue;
|
|---|
| 229 | seen.add(m[1]);
|
|---|
| 230 | const pl = db.prepare('SELECT id, title FROM playlists WHERE id = ? AND site_id = ?').get(m[1], site.id);
|
|---|
| 231 | if (!pl) continue;
|
|---|
| 232 | out.push({ type: 'Link', href: `${actorId(base, site.slug)}/playlists/${pl.id}`, mediaType: 'application/activity+json', name: pl.title });
|
|---|
| 233 | }
|
|---|
| 234 | } catch { /* niet-fataal: een tag minder, geen kapotte Note */ }
|
|---|
| 235 | return out;
|
|---|
| 236 | }
|
|---|
| 237 |
|
|---|
| 238 | // De lijst van alle playlist-collecties van een site (shaer-ayc, stap 2).
|
|---|
| 239 | // Kaal standaard (URI's), verrijkt op verzoek (FEP-9876, zelfde conventie als
|
|---|
| 240 | // followers/following): een stub per playlist met naam, hoes en de EERLIJKE
|
|---|
| 241 | // telling -- totalItems van de stub telt het open deel, dezelfde regel als de
|
|---|
| 242 | // collectie zelf, want ook een lijst mag niet verklappen wat er achter de
|
|---|
| 243 | // poort staat.
|
|---|
| 244 | export function listPlaylistsAP(base, site, enriched) {
|
|---|
| 245 | const rows = db.prepare(
|
|---|
| 246 | 'SELECT id, title, artist, year, cover_url FROM playlists WHERE site_id = ? ORDER BY created_at, id'
|
|---|
| 247 | ).all(site.id);
|
|---|
| 248 | const colId = `${actorId(base, site.slug)}/playlists`;
|
|---|
| 249 | const items = rows.map((p) => {
|
|---|
| 250 | const uri = `${actorId(base, site.slug)}/playlists/${p.id}`;
|
|---|
| 251 | if (!enriched) return uri;
|
|---|
| 252 | const stub = buildPlaylistCollection(base, site, p, playlistOpenTracks(p.id));
|
|---|
| 253 | delete stub['@context']; // genest object draagt de context van zijn omhulsel
|
|---|
| 254 | delete stub.orderedItems; // stub: wie de tracks wil, haalt de collectie op
|
|---|
| 255 | return stub;
|
|---|
| 256 | });
|
|---|
| 257 | return {
|
|---|
| 258 | '@context': AP_CONTEXT,
|
|---|
| 259 | id: colId,
|
|---|
| 260 | type: 'OrderedCollection',
|
|---|
| 261 | attributedTo: actorId(base, site.slug),
|
|---|
| 262 | totalItems: items.length,
|
|---|
| 263 | orderedItems: items,
|
|---|
| 264 | };
|
|---|
| 265 | }
|
|---|
| 266 |
|
|---|
| 267 | export function buildPlaylistCollection(base, site, playlist, rows) {
|
|---|
| 268 | const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
|
|---|
| 269 | // Dezelfde objecten als in de actor-collectie, met hetzelfde id (shaer-0nh,
|
|---|
| 270 | // stap 3). Een playlist is een KEUZE uit wat de artiest heeft uitgebracht,
|
|---|
| 271 | // geen tweede exemplaar ervan: staat een track in twee playlists, dan is het
|
|---|
| 272 | // twee keer hetzelfde ding en niet twee dingen die toevallig gelijk klinken.
|
|---|
| 273 | // De hoes van de playlist dient als terugval voor een track zonder eigen hoes.
|
|---|
| 274 | const hostPosts = site.id ? trackHostPosts(site.id) : null;
|
|---|
| 275 | const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts }));
|
|---|
| 276 | const out = {
|
|---|
| 277 | '@context': AP_CONTEXT,
|
|---|
| 278 | id: `${actorId(base, site.slug)}/playlists/${playlist.id}`,
|
|---|
| 279 | type: 'OrderedCollection',
|
|---|
| 280 | name: playlist.title,
|
|---|
| 281 | attributedTo: actorId(base, site.slug),
|
|---|
| 282 | totalItems: items.length,
|
|---|
| 283 | orderedItems: items,
|
|---|
| 284 | };
|
|---|
| 285 | // Album of playlist is presentatie; op de draad is het één samenvattingsveld.
|
|---|
| 286 | const parts = [];
|
|---|
| 287 | if (playlist.artist) parts.push(playlist.artist);
|
|---|
| 288 | if (playlist.year) parts.push(String(playlist.year));
|
|---|
| 289 | if (parts.length) out.summary = parts.join(' · ');
|
|---|
| 290 | const cover = abs(playlist.cover_url || null);
|
|---|
| 291 | if (cover) out.icon = { type: 'Image', mediaType: guessMediaType(cover), url: cover };
|
|---|
| 292 | return out;
|
|---|
| 293 | }
|
|---|
| 294 |
|
|---|
| 295 | /**
|
|---|
| 296 | * Een track als publicatie: Create(Audio) (shaer-0nh, stap 4).
|
|---|
| 297 | *
|
|---|
| 298 | * Zelfde vorm als buildCreate voor een post, met een STABIEL id: dezelfde track
|
|---|
| 299 | * levert altijd dezelfde activiteit, zodat een lezer die de outbox twee keer
|
|---|
| 300 | * ophaalt niet denkt dat er iets nieuws is.
|
|---|
| 301 | */
|
|---|
| 302 | export function buildTrackCreate(base, site, r, opts = {}) {
|
|---|
| 303 | const audio = buildTrackAudio(base, site, r, opts);
|
|---|
| 304 | const me = actorId(base, site.slug);
|
|---|
| 305 | return {
|
|---|
| 306 | '@context': AP_CONTEXT,
|
|---|
| 307 | id: `${audio.id}#create`,
|
|---|
| 308 | type: 'Create',
|
|---|
| 309 | actor: me,
|
|---|
| 310 | published: audio.published,
|
|---|
| 311 | to: [PUBLIC],
|
|---|
| 312 | cc: [`${me}/followers`],
|
|---|
| 313 | object: audio,
|
|---|
| 314 | };
|
|---|
| 315 | }
|
|---|
| 316 |
|
|---|
| 317 | /**
|
|---|
| 318 | * `category` is kanaal-vocabulaire, en de waarde is 'music' (Robins keuze, 7-8).
|
|---|
| 319 | * Alleen gezet als de site ECHT audio publiceert: een blog zonder muziek als
|
|---|
| 320 | * muziekkanaal aankondigen is erger dan geen label. Het signaal is een track in
|
|---|
| 321 | * de kast, niet enable_audio_player -- die staat standaard aan en zegt niets.
|
|---|
| 322 | */
|
|---|
| 323 | export function channelCategory(site) {
|
|---|
| 324 | try {
|
|---|
| 325 | // ALLEEN opengezette tracks tellen. Eerst keek dit naar elke track, ook een
|
|---|
| 326 | // gated -- en dan roept een site met uitsluitend afgeschermde muziek toch
|
|---|
| 327 | // "hier is muziek" naar de hele fediverse. Dat botst met de regel die we
|
|---|
| 328 | // overal aanhouden: een gesloten track is AFWEZIG, niet stilletjes
|
|---|
| 329 | // aanwezig. Naar buiten toe is een kanaal zonder publieke muziek geen
|
|---|
| 330 | // muziekkanaal.
|
|---|
| 331 | return db.prepare('SELECT 1 FROM audio_tracks WHERE site_id = ? AND fedi_open = 1 LIMIT 1').get(site.id) ? 'music' : null;
|
|---|
| 332 | } catch { return null; }
|
|---|
| 333 | }
|
|---|
| 334 |
|
|---|
| 335 | // ── Welk soort muzikale uitgave is deze post? (shaer-cyg) ─────────────────
|
|---|
| 336 |
|
|---|
| 337 | /**
|
|---|
| 338 | * Het type van een post afleiden uit de muziek die erin staat.
|
|---|
| 339 | *
|
|---|
| 340 | * DE REGEL GAAT OVER IDENTITEIT, NIET OVER TELLEN (Robins herformulering, 9-8):
|
|---|
| 341 | * een post neemt het type van zijn muziek over als hij precies EEN muzikale
|
|---|
| 342 | * eenheid bevat. Zijn het er meer, dan is de post een post die naar muziek
|
|---|
| 343 | * verwijst, en houden de collecties hun eigen identiteit.
|
|---|
| 344 | *
|
|---|
| 345 | * losse track(s) -> playlist metadata van de post geleend
|
|---|
| 346 | * een collectie -> die soort metadata van de post geleend
|
|---|
| 347 | * een collectie + losse tracks -> album de losse zijn BONUS-TRACKS
|
|---|
| 348 | * twee of meer collecties -> post NIETS geleend
|
|---|
| 349 | *
|
|---|
| 350 | * Waarom de lening bij de laatste vervalt: die bestaat omdat een collectie soms
|
|---|
| 351 | * dun is -- geen eigen hoes, geen eigen titel. Bij twee is de post niet meer de
|
|---|
| 352 | * drager van EEN identiteit, en vervalt de reden vanzelf. Dezelfde regel zet
|
|---|
| 353 | * zichzelf uit.
|
|---|
| 354 | *
|
|---|
| 355 | * EEN COLLECTIE IS EEN PLAYLIST OF EEN ALBUM-INSLUITING. Robins regel noemde
|
|---|
| 356 | * alleen playlists, maar [[album:naam]] groepeert net zo goed tracks en is
|
|---|
| 357 | * letterlijk een album; hem als losse tracks tellen zou een albumpost tot
|
|---|
| 358 | * playlist maken. Telt hij straks anders, dan is dat hier een regel.
|
|---|
| 359 | */
|
|---|
| 360 | export function postMusicType(content) {
|
|---|
| 361 | const c = String(content || '');
|
|---|
| 362 | const uniek = (re) => [...new Set([...c.matchAll(re)].map((m) => m[1].trim()))];
|
|---|
| 363 |
|
|---|
| 364 | const playlists = uniek(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g);
|
|---|
| 365 | const albums = uniek(/\[\[album:([^\]]+)\]\]/g);
|
|---|
| 366 | const tracks = uniek(/\[\[track:([A-Za-z0-9_-]+)\]\]/g);
|
|---|
| 367 |
|
|---|
| 368 | const collecties = [
|
|---|
| 369 | ...playlists.map((id) => ({ soort: 'playlist', id })),
|
|---|
| 370 | ...albums.map((naam) => ({ soort: 'album', naam })),
|
|---|
| 371 | ];
|
|---|
| 372 |
|
|---|
| 373 | if (!collecties.length && !tracks.length) return null; // geen muziek
|
|---|
| 374 |
|
|---|
| 375 | if (!collecties.length) {
|
|---|
| 376 | // Ook EEN losse track wordt een playlist: naar buiten toe is er dan altijd
|
|---|
| 377 | // een collectie om naar te wijzen. Hoe Klonkt dat toont is een aparte vraag.
|
|---|
| 378 | return { type: 'playlist', collectie: null, tracks, bonus: [], leentMetadata: true };
|
|---|
| 379 | }
|
|---|
| 380 |
|
|---|
| 381 | if (collecties.length === 1) {
|
|---|
| 382 | const c0 = collecties[0];
|
|---|
| 383 | // Losse tracks naast een collectie zijn geen rommelrestje maar bonus-tracks,
|
|---|
| 384 | // en dat maakt het geheel een album.
|
|---|
| 385 | if (tracks.length) return { type: 'album', collectie: c0, tracks: [], bonus: tracks, leentMetadata: true };
|
|---|
| 386 | return { type: c0.soort, collectie: c0, tracks: [], bonus: [], leentMetadata: true };
|
|---|
| 387 | }
|
|---|
| 388 |
|
|---|
| 389 | return { type: 'post', collecties, tracks, bonus: [], leentMetadata: false };
|
|---|
| 390 | }
|
|---|