Changeset a5d14c7 in Klonkt for src/services/music/index.js
- Timestamp:
- 08/09/2026 08:20:24 AM (4 weeks ago)
- Branches:
- main
- Children:
- 0677bac
- Parents:
- b2adc20
- File:
-
- 1 edited
-
src/services/music/index.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/music/index.js
rb2adc20 ra5d14c7 15 15 import db from '../../config/database.js'; 16 16 import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType } from '../ap-core.js'; 17 import { afleidenUitInsluitingen } from '../../assets/js/shared/post-music-type.js';17 import { afleidenUitInsluitingen, ingeslotenPlaylists } from '../../assets/js/shared/post-music-type.js'; 18 18 19 19 // m.size hoort erbij voor de RSS-enclosure: die eist een lengte in bytes. … … 222 222 // zonder site-check zou een post van site A naar de collectie van site B 223 223 // kunnen wijzen. 224 export function playlistLinkTags(base, site, content ) {224 export function playlistLinkTags(base, site, content, post = null) { 225 225 const out = []; 226 const seen = new Set();227 226 try { 228 for (const m of (content || '').matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) { 229 if (seen.has(m[1])) continue; 230 seen.add(m[1]); 231 const pl = db.prepare('SELECT id, title FROM playlists WHERE id = ? AND site_id = ?').get(m[1], site.id); 227 // Zelfde patroon als de renderer en als de afleiding: wat niet insluit, 228 // krijgt ook geen link. Dit stond hier met een eigen patroon dat 229 // underscores accepteerde die nergens anders meetellen. 230 for (const id of ingeslotenPlaylists(content)) { 231 const pl = db.prepare('SELECT id, title FROM playlists WHERE id = ? AND site_id = ?').get(id, site.id); 232 232 if (!pl) continue; 233 233 out.push({ type: 'Link', href: `${actorId(base, site.slug)}/playlists/${pl.id}`, mediaType: 'application/activity+json', name: pl.title }); 234 } 235 // Losse tracks in een post zijn ook een uitgave (shaer-38y): ze krijgen een 236 // eigen collectie, en de post wijst er langs dezelfde weg naar. Zonder deze 237 // link zou die collectie bestaan maar door niemand te vinden zijn. 238 if (post && post.id) { 239 const eenheid = postMusicType(content, site.id); 240 if (eenheid && !eenheid.collectie && eenheid.tracks?.length && losseTracksVanPost(site.id, eenheid.tracks).length) { 241 out.push({ 242 type: 'Link', 243 href: postTracksId(base, site, post.id), 244 mediaType: 'application/activity+json', 245 name: post.title || 'Tracks', 246 }); 247 } 234 248 } 235 249 } catch { /* niet-fataal: een tag minder, geen kapotte Note */ } … … 291 305 const cover = abs(playlist.cover_url || null); 292 306 if (cover) out.icon = { type: 'Image', mediaType: guessMediaType(cover), url: cover }; 293 return out; 307 return leenVanPost(base, site, out, uitgavePost(site.id, playlist.id)); 308 } 309 310 // ── De post als uitgave (shaer-38y) ─────────────────────────────────────── 311 312 /** Het AS2-id van de collectie losse tracks van een post. */ 313 function postTracksId(base, site, postId) { 314 return `${actorId(base, site.slug)}/posts/${encodeURIComponent(postId)}/tracks`; 315 } 316 317 /** 318 * Welke post brengt deze playlist uit, en mag die zijn gegevens uitlenen? 319 * 320 * Niet zomaar de eerste post die de playlist noemt: alleen een post die er EEN 321 * muzikale eenheid van maakt leent uit. Staan er twee collecties in, dan is de 322 * post niet meer de drager van een identiteit en houdt de playlist de zijne -- 323 * dezelfde regel als in de afleiding, hier alleen toegepast. 324 * 325 * De nieuwste wint als er meerdere zijn: dat is waar hij het laatst is 326 * uitgebracht. 327 */ 328 export function uitgavePost(siteId, playlistId) { 329 if (!siteId || !playlistId) return null; 330 try { 331 const rijen = db.prepare(` 332 SELECT id, slug, title, excerpt, content, cover_image_url, tags 333 FROM posts 334 WHERE site_id = ? AND status = 'published' 335 AND content LIKE '%[[playlist:' || ? || ']]%' 336 ORDER BY COALESCE(published_at, created_at) DESC 337 `).all(siteId, playlistId); 338 for (const p of rijen) { 339 const r = postMusicType(p.content, siteId); 340 if (r && r.leentMetadata && r.collectie && r.collectie.id === playlistId) return p; 341 } 342 } catch { /* geen lening is geen fout */ } 343 return null; 344 } 345 346 /** 347 * De post leent zijn gegevens aan de uitgave (shaer-38y, punt 3). 348 * 349 * WAAROM DE POST WINT EN NIET DE PLAYLIST. Een playlist heeft een titel en soms 350 * een hoes; een post heeft een titel, een tekst, een hoes, tags EN een datum. 351 * Voor audio-gebaseerde inhoud is de post de uitgave -- dat is waar iemand hem 352 * heeft uitgebracht en waar het verhaal erbij staat. Een Funkwhale-achtige 353 * lezer vindt een collectie met alleen een naam te mager, en dat is precies wat 354 * hij nu krijgt. 355 * 356 * De naam van de playlist gaat niet verloren: die blijft als `alsoKnownAs` 357 * staan, zodat de eigen naam terug te vinden is als hij afwijkt. 358 */ 359 function leenVanPost(base, site, obj, post) { 360 if (!post) return obj; 361 const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`); 362 363 if (post.title) { 364 if (obj.name && obj.name !== post.title) obj.alsoKnownAs = obj.name; 365 obj.name = post.title; 366 } 367 // De tekst als `content`, niet als `summary`: in AS2 is summary de korte 368 // samenvatting en content het lijf. Artiest en jaar blijven dus in summary 369 // staan -- dat is een samenvatting, en de posttekst is dat niet. 370 const tekst = (post.excerpt || '').trim(); 371 if (tekst) obj.content = tekst; 372 373 const cover = abs(post.cover_image_url || null); 374 if (cover) { 375 obj.image = { type: 'Image', mediaType: guessMediaType(cover), url: cover }; 376 if (!obj.icon) obj.icon = obj.image; // geen eigen hoes? dan die van de post 377 } 378 379 const tags = hashtagsVanPost(base, post.tags); 380 if (tags.length) obj.tag = tags; 381 382 // Waar je hem kunt horen, en waar hij bij hoort. Zelfde paar als bij een 383 // losse track: url wijst een mens naar de post, context zegt waar dit object 384 // thuishoort. 385 obj.url = `${base}/${post.slug}`; 386 obj.context = noteId(base, post.id); 387 return obj; 388 } 389 390 /** De tags van een post als AS2 Hashtags. Zelfde vorm als buildHashtagList. */ 391 function hashtagsVanPost(base, tagsField) { 392 const ruw = Array.isArray(tagsField) 393 ? tagsField 394 : String(tagsField || '').split(','); 395 const uit = [], gezien = new Set(); 396 for (const t of ruw) { 397 const label = String(t || '').trim().replace(/^#/, ''); 398 if (!label) continue; 399 const slug = label.toLowerCase().replace(/\s+/g, '-'); 400 if (gezien.has(slug)) continue; 401 gezien.add(slug); 402 uit.push({ type: 'Hashtag', href: `${base}/tag/${encodeURIComponent(slug)}`, name: '#' + label }); 403 } 404 return uit; 405 } 406 407 /** De open tracks uit een lijst ids, in de volgorde van die lijst. */ 408 function losseTracksVanPost(siteId, ids) { 409 if (!siteId || !ids?.length) return []; 410 const gaten = ids.map(() => '?').join(','); 411 const rijen = db.prepare( 412 `SELECT ${TRACK_KOLOMMEN} 413 FROM audio_tracks t JOIN media m ON m.id = t.media_id 414 WHERE t.site_id = ? AND t.fedi_open = 1 AND t.id IN (${gaten})` 415 ).all(siteId, ...ids); 416 // De volgorde van de POST, niet die van de tabel (shaer-38y, punt 1): zoals 417 // iemand ze heeft neergezet is de volgorde waarin ze bedoeld zijn. 418 const opId = new Map(rijen.map((r) => [r.id, r])); 419 return ids.map((id) => opId.get(id)).filter(Boolean); 420 } 421 422 /** 423 * De losse tracks van een post als EEN uitgave (shaer-38y). 424 * 425 * Tot nu toe gingen die los de deur uit: losse Audio-objecten die een lezer 426 * nergens kon plaatsen. Ze horen bij elkaar omdat ze in dezelfde post staan, en 427 * dat is wat deze collectie zegt -- met de gegevens van de post erbij, want die 428 * heeft ze wel en de losse tracks niet. 429 * 430 * Geeft null als er niets te tonen is: geen post, geen losse tracks, of een 431 * post die geen enkele muzikale eenheid IS. 432 */ 433 export function buildPostTrackCollection(base, site, post) { 434 if (!post || !post.id) return null; 435 const eenheid = postMusicType(post.content, site.id); 436 if (!eenheid || eenheid.collectie || !eenheid.tracks?.length) return null; 437 438 const rows = losseTracksVanPost(site.id, eenheid.tracks); 439 if (!rows.length) return null; 440 441 const hostPosts = new Map(rows.map((r) => [r.id, { id: post.id, slug: post.slug }])); 442 const out = { 443 '@context': AP_CONTEXT, 444 id: postTracksId(base, site, post.id), 445 type: 'OrderedCollection', 446 attributedTo: actorId(base, site.slug), 447 totalItems: rows.length, 448 orderedItems: rows.map((r) => buildTrackAudio(base, site, r, { hostPosts })), 449 }; 450 return leenVanPost(base, site, out, post); 294 451 } 295 452
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)