Changeset 3e86f1c in Klonkt for src/services


Ignore:
Timestamp:
06/15/2026 03:23:29 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
7f46817d
Parents:
4c9f29a
Message:

feat: automatic track duration — no more manually entering seconds

The duration of an audio track is now determined automatically instead of manually:

  • On upload: read from ffmpeg's codecData event during the existing transcode (no extra ffprobe binary). The upload INSERT never set duration before -> every track started as NULL ('--:--'); that is now fixed.
  • In the track editor: a hidden <audio preload=metadata> reads the duration and fills the field if it's still empty (manual override still possible).
  • Backfill: npm run backfill:durations fills existing tracks without duration via ffmpeg (probeDuration reads only codecData and stops immediately -> fast).

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/AudioTranscoder.js

    r4c9f29a r3e86f1c  
    9090  const tmpPath = path.join(outputDir, `${outputBaseName}.transcoding-${process.pid}.mp3`);
    9191
     92  let durationSec = null;
    9293  try {
    93     await runFfmpeg({ inputPath, tmpPath, tags });
     94    const r = await runFfmpeg({ inputPath, tmpPath, tags });
     95    durationSec = r && r.durationSec != null ? r.durationSec : null;
    9496
    9597    // Verify the output is not zero bytes — ffmpeg sometimes "succeeds" but
     
    124126      size: outStat.size,
    125127      mimeType: 'audio/mpeg',
     128      durationSec,   // hele seconden uit ffmpeg's codecData (null als onbekend)
    126129    };
    127130
     
    140143function runFfmpeg({ inputPath, tmpPath, tags }) {
    141144  return new Promise((resolve, reject) => {
     145    let durationSec = null;
    142146    const cmd = ffmpeg(inputPath)
    143147      .audioCodec('libmp3lame')
     
    168172
    169173    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); })
    170177      .on('error', (err, stdout, stderr) => {
    171178        // ffmpeg's stderr is the most useful diagnostic. fluent-ffmpeg's
     
    176183        reject(new Error(`Transcode failed: ${reason}${tail ? '\n' + tail : ''}`));
    177184      })
    178       .on('end', () => resolve())
     185      .on('end', () => resolve({ durationSec }))
    179186      .save(tmpPath);
    180187  });
    181188}
    182189
    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 */
     194function 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 */
     209export 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
     226export default { transcodeToMp3, probeDuration };
Note: See TracChangeset for help on using the changeset viewer.