Changeset 3e86f1c in Klonkt
- Timestamp:
- 06/15/2026 03:23:29 AM (3 months ago)
- Branches:
- main
- Children:
- 7f46817d
- Parents:
- 4c9f29a
- Files:
-
- 1 added
- 4 edited
-
package.json (modified) (1 diff)
-
scripts/backfill-durations.mjs (added)
-
src/routes/admin-audio.js (modified) (2 diffs)
-
src/services/AudioTranscoder.js (modified) (5 diffs)
-
src/views/partials/track-editor.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
package.json
r4c9f29a r3e86f1c 9 9 "dev": "node --watch src/server.js", 10 10 "migrate": "node src/db/migrate.js", 11 "reset-admin": "node scripts/reset-admin.mjs" 11 "reset-admin": "node scripts/reset-admin.mjs", 12 "backfill:durations": "node scripts/backfill-durations.mjs" 12 13 }, 13 14 "dependencies": { -
src/routes/admin-audio.js
r4c9f29a r3e86f1c 185 185 `).run(mediaId, site.id, transcoded.filename, transcoded.mimeType, transcoded.size, transcoded.path); 186 186 187 console.log('[admin-audio] inserting audio_tracks row'); 187 // Duur automatisch: primair uit de transcode (ffmpeg codecData), anders een 188 // optionele client-side waarde (bulk-uploader leest <audio>.duration uit), 189 // anders NULL (UI toont dan '—:—', handmatig bij te werken in de editor). 190 const clientDur = req.body.duration != null ? parseInt(req.body.duration, 10) : NaN; 191 const finalDuration = 192 (transcoded.durationSec != null && transcoded.durationSec > 0) ? transcoded.durationSec 193 : (Number.isFinite(clientDur) && clientDur > 0) ? clientDur 194 : null; 195 196 console.log('[admin-audio] inserting audio_tracks row (duration=' + finalDuration + ')'); 188 197 db.prepare(` 189 INSERT INTO audio_tracks (id, site_id, title, artist, album, cover_url, media_id, position)190 VALUES (?, ?, ?, ?, ?, ?, ?, COALESCE(198 INSERT INTO audio_tracks (id, site_id, title, artist, album, duration, cover_url, media_id, position) 199 VALUES (?, ?, ?, ?, ?, ?, ?, ?, COALESCE( 191 200 (SELECT MAX(position) + 1 FROM audio_tracks WHERE site_id = ?), 192 201 0 … … 195 204 trackId, site.id, 196 205 finalTitle, finalArtist, finalAlbum, 206 finalDuration, 197 207 coverUrl, 198 208 mediaId, site.id -
src/services/AudioTranscoder.js
r4c9f29a r3e86f1c 90 90 const tmpPath = path.join(outputDir, `${outputBaseName}.transcoding-${process.pid}.mp3`); 91 91 92 let durationSec = null; 92 93 try { 93 await runFfmpeg({ inputPath, tmpPath, tags }); 94 const r = await runFfmpeg({ inputPath, tmpPath, tags }); 95 durationSec = r && r.durationSec != null ? r.durationSec : null; 94 96 95 97 // Verify the output is not zero bytes — ffmpeg sometimes "succeeds" but … … 124 126 size: outStat.size, 125 127 mimeType: 'audio/mpeg', 128 durationSec, // hele seconden uit ffmpeg's codecData (null als onbekend) 126 129 }; 127 130 … … 140 143 function runFfmpeg({ inputPath, tmpPath, tags }) { 141 144 return new Promise((resolve, reject) => { 145 let durationSec = null; 142 146 const cmd = ffmpeg(inputPath) 143 147 .audioCodec('libmp3lame') … … 168 172 169 173 cmd 174 // codecData geeft de duur van de INPUT als "HH:MM:SS.xx" — zo bepalen we 175 // de tracklengte automatisch zonder aparte ffprobe-binary. 176 .on('codecData', (data) => { durationSec = parseHmsToSeconds(data && data.duration); }) 170 177 .on('error', (err, stdout, stderr) => { 171 178 // ffmpeg's stderr is the most useful diagnostic. fluent-ffmpeg's … … 176 183 reject(new Error(`Transcode failed: ${reason}${tail ? '\n' + tail : ''}`)); 177 184 }) 178 .on('end', () => resolve( ))185 .on('end', () => resolve({ durationSec })) 179 186 .save(tmpPath); 180 187 }); 181 188 } 182 189 183 export default { transcodeToMp3 }; 190 /** 191 * Parse een ffmpeg-duurstring "HH:MM:SS.xx" naar hele seconden. Geeft null bij 192 * "N/A" of een onverwacht formaat. 193 */ 194 function parseHmsToSeconds(hms) { 195 if (!hms || typeof hms !== 'string') return null; 196 const m = hms.match(/^(\d+):(\d{2}):(\d{2})(?:\.(\d+))?$/); 197 if (!m) return null; 198 const sec = (+m[1]) * 3600 + (+m[2]) * 60 + (+m[3]) + (m[4] ? Number('0.' + m[4]) : 0); 199 return Number.isFinite(sec) ? Math.round(sec) : null; 200 } 201 202 /** 203 * Lees de duur (hele seconden) van een audiobestand ZONDER te transcoderen. 204 * Start een ffmpeg-pass en leest enkel het codecData-event (duur), waarna we het 205 * proces direct stoppen — snel en zonder aparte ffprobe-binary (ffmpeg-static 206 * levert alleen ffmpeg). Bedoeld voor het backfill-script. 207 * @returns {Promise<number|null>} 208 */ 209 export function probeDuration(filePath) { 210 return new Promise((resolve) => { 211 let durationSec = null, done = false; 212 const finish = () => { if (!done) { done = true; resolve(durationSec); } }; 213 const cmd = ffmpeg(filePath) 214 .on('codecData', (data) => { 215 durationSec = parseHmsToSeconds(data && data.duration); 216 try { cmd.kill('SIGKILL'); } catch { /* al klaar */ } 217 finish(); 218 }) 219 .on('error', finish) 220 .on('end', finish) 221 .format('null') 222 .save(process.platform === 'win32' ? 'NUL' : '/dev/null'); 223 }); 224 } 225 226 export default { transcodeToMp3, probeDuration }; -
src/views/partials/track-editor.ejs
r4c9f29a r3e86f1c 380 380 381 381 <label class="te-field"> 382 <span>Duur <small>(seconden — bv. 142 voor 2:22)</small></span>382 <span>Duur <small>(seconden — automatisch bepaald, hier te overschrijven)</small></span> 383 383 <input type="number" id="te-duration" min="0" step="1" 384 384 inputmode="numeric" pattern="[0-9]*" 385 value="${track.duration || ''}" placeholder=" 0">385 value="${track.duration || ''}" placeholder="auto"> 386 386 </label> 387 387 … … 619 619 }); 620 620 621 // ── Auto-duur ─────────────────────────────────────────── 622 // De server bepaalt de duur al automatisch bij upload. Dit is de vangnet/ 623 // UX-laag: opent een admin een bestaande track zónder duur, dan lezen we 'm 624 // hier uit de audio-metadata en vullen het veld — zodat je nooit seconden 625 // hoeft te typen. Bestaande waarde wordt nooit overschreven. We halen de 626 // bytes via dezelfde header-gate als de speler (X-Audio-Player). 627 (async function autoDuration() { 628 const durEl = $('#te-duration'); 629 if (!durEl || !track.stream_url) return; 630 if (durEl.value && Number(durEl.value) > 0) return; // al ingevuld → met rust laten 631 let objUrl = null; 632 try { 633 const r = await fetch(track.stream_url, { credentials: 'same-origin', headers: { 'X-Audio-Player': '1' } }); 634 if (!r.ok) return; 635 objUrl = URL.createObjectURL(await r.blob()); 636 const probe = new Audio(); 637 probe.preload = 'metadata'; 638 probe.addEventListener('loadedmetadata', () => { 639 if (isFinite(probe.duration) && probe.duration > 0 && !(durEl.value && Number(durEl.value) > 0)) { 640 durEl.value = Math.round(probe.duration); 641 } 642 if (objUrl) URL.revokeObjectURL(objUrl); 643 }); 644 probe.addEventListener('error', () => { if (objUrl) URL.revokeObjectURL(objUrl); }); 645 probe.src = objUrl; 646 } catch (e) { if (objUrl) URL.revokeObjectURL(objUrl); } 647 })(); 648 621 649 // Focus title for fast typing 622 650 setTimeout(() => $('#te-title').focus(), 60);
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)