Changeset 0d7acdf in Klonkt
- Timestamp:
- 06/20/2026 05:04:13 AM (3 months ago)
- Branches:
- main
- Children:
- 3ec691c
- Parents:
- fef781e
- Location:
- src
- Files:
-
- 8 edited
-
assets/css/audio.css (modified) (1 diff)
-
config/database.js (modified) (1 diff)
-
routes/admin-audio.js (modified) (9 diffs)
-
routes/posts.js (modified) (2 diffs)
-
services/AudioEmbedService.js (modified) (2 diffs)
-
services/AudioTranscoder.js (modified) (2 diffs)
-
views/partials/track-editor.ejs (modified) (2 diffs)
-
views/shell.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/audio.css
rfef781e r0d7acdf 69 69 font-size: 0.85rem; 70 70 color: var(--ink-muted, var(--ink-soft)); 71 white-space: nowrap; 72 overflow: hidden; 73 text-overflow: ellipsis; 74 } 75 /* Eigenaar/credit · licentie — subtiele regel onder de track. */ 76 .post-audio-track .pat-credit { 77 font-size: 0.72rem; 78 color: var(--ink-faint, var(--ink-soft)); 79 margin-top: 0.1rem; 71 80 white-space: nowrap; 72 81 overflow: hidden; -
src/config/database.js
rfef781e r0d7acdf 90 90 ensureColumn('audio_tracks', 'play_count', 'INTEGER DEFAULT 0'); // plays per track 91 91 ensureColumn('audio_tracks', 'downloadable', 'INTEGER DEFAULT 0'); // download-voor-email (premium #2) 92 ensureColumn('audio_tracks', 'credit', 'TEXT'); // eigenaar/credit (copyright-houder) 93 ensureColumn('audio_tracks', 'license', 'TEXT'); // licentie (bv. "CC BY 4.0", "Alle rechten voorbehouden") 92 94 93 95 // Playlists (v9 feature) — first-class entity. CREATE IF NOT EXISTS is -
src/routes/admin-audio.js
rfef781e r0d7acdf 20 20 import { toWebp } from '../services/ImageWebpService.js'; 21 21 import { requireGod } from '../middleware/auth.js'; 22 import { transcodeToMp3 } from '../services/AudioTranscoder.js';22 import { transcodeToMp3, retagMp3 } from '../services/AudioTranscoder.js'; 23 23 import { audioUrl } from '../services/AudioStreamService.js'; 24 24 … … 168 168 const finalArtist = artist?.trim() || null; 169 169 const finalAlbum = album?.trim() || null; 170 // Eigenaarschap/licentie. credit valt terug op de artiest; deze gaan zowel de 171 // DB in als de ID3-tags van de mp3 (copyright + comment). 172 const finalCredit = (req.body.credit || '').trim() || finalArtist || null; 173 const finalLicense = (req.body.license || '').trim() || null; 170 174 171 175 console.log('[admin-audio] upload received:', { … … 186 190 artist: finalArtist || undefined, 187 191 album: finalAlbum || undefined, 192 copyright: finalCredit || undefined, 193 comment: finalLicense || undefined, 188 194 }, 189 195 }); … … 216 222 console.log('[admin-audio] inserting audio_tracks row (duration=' + finalDuration + ')'); 217 223 db.prepare(` 218 INSERT INTO audio_tracks (id, site_id, title, artist, album, duration, cover_url, media_id, position)219 VALUES (?, ?, ?, ?, ?, ?, ?, ?, COALESCE(224 INSERT INTO audio_tracks (id, site_id, title, artist, album, duration, cover_url, credit, license, media_id, position) 225 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE( 220 226 (SELECT MAX(position) + 1 FROM audio_tracks WHERE site_id = ?), 221 227 0 … … 226 232 finalDuration, 227 233 coverUrl, 234 finalCredit, finalLicense, 228 235 mediaId, site.id 229 236 ); … … 371 378 const t = db.prepare(` 372 379 SELECT t.id, t.title, t.artist, t.album, t.duration, t.cover_url, 373 t. position, t.created_at, m.filename380 t.credit, t.license, t.position, t.created_at, m.filename 374 381 FROM audio_tracks t LEFT JOIN media m ON m.id = t.media_id 375 382 WHERE t.id = ? AND t.site_id = ? … … 388 395 * fallback keeps working. 389 396 */ 390 router.post('/api/:id', requireGod, express.json(), (req, res) => {397 router.post('/api/:id', requireGod, express.json(), async (req, res) => { 391 398 const site = res.locals.site; 392 399 if (!site) return res.status(404).json({ error: 'Site required' }); … … 433 440 fields.push('downloadable = ?'); values.push(body.downloadable ? 1 : 0); 434 441 } 442 if (Object.prototype.hasOwnProperty.call(body, 'credit')) { 443 fields.push('credit = ?'); values.push(String(body.credit || '').trim() || null); 444 } 445 if (Object.prototype.hasOwnProperty.call(body, 'license')) { 446 fields.push('license = ?'); values.push(String(body.license || '').trim() || null); 447 } 435 448 436 449 if (fields.length === 0) { … … 445 458 } 446 459 447 // Return fresh row so the caller can update its UI without reloading 460 // Verse rij + (als tag-velden wijzigden) de mp3 her-taggen, zodat de eigenaar/ 461 // licentie ook IN het bestand staat (ID3) en meereist bij een download. 448 462 const fresh = db.prepare(` 449 SELECT id, title, artist, album, duration, cover_url 450 FROM audio_tracks WHERE id = ? AND site_id = ? 463 SELECT t.id, t.title, t.artist, t.album, t.duration, t.cover_url, t.credit, t.license, m.storage_path 464 FROM audio_tracks t LEFT JOIN media m ON m.id = t.media_id 465 WHERE t.id = ? AND t.site_id = ? 451 466 `).get(req.params.id, site.id); 452 res.json({ ok: true, track: fresh }); 467 468 const tagsChanged = ['title', 'artist', 'album', 'credit', 'license'] 469 .some((f) => Object.prototype.hasOwnProperty.call(body, f)); 470 if (fresh && fresh.storage_path && tagsChanged) { 471 try { 472 await retagMp3({ filePath: fresh.storage_path, tags: { 473 title: fresh.title || undefined, 474 artist: fresh.artist || undefined, 475 album: fresh.album || undefined, 476 copyright: fresh.credit || undefined, 477 comment: fresh.license || undefined, 478 } }); 479 } catch (e) { 480 console.warn('[admin-audio] ID3 her-taggen mislukt (DB is wel bijgewerkt):', e.message); 481 } 482 } 483 const { storage_path, ...trackOut } = fresh || {}; 484 res.json({ ok: true, track: trackOut }); 453 485 }); 454 486 -
src/routes/posts.js
rfef781e r0d7acdf 529 529 const placeholders = trackIds.map(() => '?').join(','); 530 530 const rows = db.prepare(` 531 SELECT t.id, t.title, t.artist, t.cover_url, m.filename531 SELECT t.id, t.title, t.artist, t.cover_url, t.credit, t.license, m.filename 532 532 FROM audio_tracks t LEFT JOIN media m ON m.id = t.media_id 533 533 WHERE t.site_id = ? AND t.id IN (${placeholders}) … … 542 542 artist: r.artist, 543 543 cover: r.cover_url, 544 credit: r.credit || '', 545 license: r.license || '', 544 546 url: audioUrl(r.filename), 545 547 }; -
src/services/AudioEmbedService.js
rfef781e r0d7acdf 253 253 artist: t.artist || '', 254 254 cover: t.cover || '', 255 credit: t.credit || '', 256 license: t.license || '', 255 257 }); 256 258 const titleH = this.escape(t.title || 'Untitled'); 257 259 const artistH = this.escape(t.artist || ''); 258 260 const urlH = this.escape(t.url); 261 // Zichtbare eigenaar/licentie-regel onder de track. 262 const creditBits = [this.escape(t.credit || ''), this.escape(t.license || '')].filter(Boolean).join(' · '); 259 263 const dataAttr = trackJson 260 264 .replace(/&/g, '&').replace(/'/g, ''').replace(/</g, '<'); … … 267 271 <div class="pat-title">${titleH}</div> 268 272 ${artistH ? `<div class="pat-artist">${artistH}</div>` : ''} 273 ${creditBits ? `<div class="pat-credit">${creditBits}</div>` : ''} 269 274 </div> 270 275 </div>`; -
src/services/AudioTranscoder.js
rfef781e r0d7acdf 138 138 139 139 /** 140 * Herschrijf de ID3-tags van een BESTAANDE mp3 zonder her-encoden (`-c copy`). 141 * Gebruikt bij het bewerken van track-metadata (titel/artiest/album/credit/licentie) 142 * zodat de eigendomsinfo in het bestand zelf meereist bij een download. 143 * ffmpeg kan niet in-place editen → schrijf naar tmp en hernoem atomisch terug. 144 */ 145 export async function retagMp3({ filePath, tags = {} }) { 146 if (!filePath) throw new Error('retagMp3: filePath required'); 147 await stat(filePath); // throws als 't bestand mist 148 const dir = path.dirname(filePath); 149 const base = path.basename(filePath, path.extname(filePath)); 150 const tmpPath = path.join(dir, `${base}.retag-${process.pid}.mp3`); 151 try { 152 await new Promise((resolve, reject) => { 153 const cmd = ffmpeg(filePath) 154 .audioCodec('copy') // geen her-encode → snel, geen kwaliteitsverlies 155 .format('mp3') 156 .outputOptions('-id3v2_version', '3') 157 .outputOptions('-map_metadata', '-1') 158 .outputOptions('-vn'); 159 if (tags.title) cmd.outputOptions('-metadata', `title=${tags.title}`); 160 if (tags.artist) cmd.outputOptions('-metadata', `artist=${tags.artist}`); 161 if (tags.album) cmd.outputOptions('-metadata', `album=${tags.album}`); 162 if (tags.copyright) cmd.outputOptions('-metadata', `copyright=${tags.copyright}`); 163 if (tags.comment) cmd.outputOptions('-metadata', `comment=${tags.comment}`); 164 cmd.on('error', (err, so, se) => reject(new Error(((err && err.message) || 'ffmpeg') + (se ? ' | ' + se : '')))) 165 .on('end', () => resolve()) 166 .save(tmpPath); 167 }); 168 const s = await stat(tmpPath); 169 if (s.size === 0) throw new Error('retag output is leeg'); 170 await rename(tmpPath, filePath); 171 return { filePath, size: s.size }; 172 } catch (err) { 173 try { await unlink(tmpPath); } catch { /* tmp bestaat mogelijk niet */ } 174 throw err; 175 } 176 } 177 178 /** 140 179 * Run a single ffmpeg pass: input -> tmp output. 141 180 * Returns a promise that resolves when ffmpeg exits cleanly, rejects otherwise. … … 167 206 // breaks any value containing a space (e.g. "Test Artist" gets parsed 168 207 // as a separate output filename). 169 if (tags.title) cmd.outputOptions('-metadata', `title=${tags.title}`); 170 if (tags.artist) cmd.outputOptions('-metadata', `artist=${tags.artist}`); 171 if (tags.album) cmd.outputOptions('-metadata', `album=${tags.album}`); 208 if (tags.title) cmd.outputOptions('-metadata', `title=${tags.title}`); 209 if (tags.artist) cmd.outputOptions('-metadata', `artist=${tags.artist}`); 210 if (tags.album) cmd.outputOptions('-metadata', `album=${tags.album}`); 211 if (tags.copyright) cmd.outputOptions('-metadata', `copyright=${tags.copyright}`); // ID3 TCOP — eigenaar/credit 212 if (tags.comment) cmd.outputOptions('-metadata', `comment=${tags.comment}`); // ID3 COMM — licentie 172 213 173 214 cmd -
src/views/partials/track-editor.ejs
rfef781e r0d7acdf 385 385 value="${track.duration || ''}" placeholder="auto"> 386 386 </label> 387 388 <div class="te-row te-row-2"> 389 <label class="te-field"> 390 <span>Eigenaar / credit <small>(copyright-houder)</small></span> 391 <input type="text" id="te-credit" maxlength="200" 392 autocomplete="off" spellcheck="false" 393 placeholder="bv. © 2025 Robin Genis" 394 value="${esc(track.credit || '')}"> 395 </label> 396 <label class="te-field"> 397 <span>Licentie</span> 398 <input type="text" id="te-license" maxlength="120" 399 autocomplete="off" spellcheck="false" list="te-license-list" 400 placeholder="Alle rechten voorbehouden" 401 value="${esc(track.license || '')}"> 402 <datalist id="te-license-list"> 403 <option value="Alle rechten voorbehouden"></option> 404 <option value="CC BY 4.0"></option> 405 <option value="CC BY-SA 4.0"></option> 406 <option value="CC BY-NC 4.0"></option> 407 <option value="CC BY-NC-SA 4.0"></option> 408 <option value="CC BY-ND 4.0"></option> 409 <option value="CC0 1.0 (publiek domein)"></option> 410 </datalist> 411 </label> 412 </div> 387 413 388 414 <div class="te-field"> … … 603 629 artist: $('#te-artist').value.trim() || null, 604 630 album: $('#te-album').value.trim() || null, 631 credit: $('#te-credit').value.trim() || null, 632 license: $('#te-license').value.trim() || null, 605 633 duration: $('#te-duration').value ? Number($('#te-duration').value) : null, 606 634 cover_url: urlInput.value.trim() || null, -
src/views/shell.ejs
rfef781e r0d7acdf 168 168 anywhere (admin previews, post embeds, etc). The player itself is 169 169 a singleton — see the script tag near </body>. --> 170 <link rel="stylesheet" href="/assets/css/audio.css?v= 6">170 <link rel="stylesheet" href="/assets/css/audio.css?v=7"> 171 171 <!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) in huisstijl. --> 172 172 <link rel="stylesheet" href="/assets/css/embed.css?v=8">
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)