From 050ea5ba6488c338184496a357cf0bc51d2cdd9c Mon Sep 17 00:00:00 2001 From: Anthony GAEREMYNCK <1@anthony.sh> Date: Sat, 1 Aug 2026 14:15:13 +0200 Subject: [PATCH] feat(audio): wire audio converters and bitrate validation into app.js/worker.js --- src/app.js | 4 +++ src/worker.js | 2 ++ test/api/jobs.test.js | 82 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/src/app.js b/src/app.js index deaf365..40890cf 100644 --- a/src/app.js +++ b/src/app.js @@ -14,6 +14,7 @@ import { registerFontConverter } from './converters/font.js'; import { registerDfontConverter } from './converters/dfont.js'; import { registerEbookConverter } from './converters/ebook.js'; import { registerArchiveConverters } from './converters/archive.js'; +import { registerAudioConverters } from './converters/audio.js'; import { extractExtension, stripExtension } from './archiveExtensions.js'; import { resolveInputFormat } from './mime.js'; import { deleteIfExists } from './storage.js'; @@ -28,6 +29,8 @@ function isValidQuality(targetFormat, quality) { if (targetFormat === 'gif' || targetFormat === 'ico' || targetFormat === 'tar') return false; if (targetFormat === 'png') return quality >= 0 && quality <= 9; if (['zip', 'tar.gz', 'tar.bz2', '7z', 'tar.7z'].includes(targetFormat)) return quality >= 0 && quality <= 9; + if (['wav', 'flac'].includes(targetFormat)) return false; + if (['mp3', 'ogg', 'aac', 'm4a'].includes(targetFormat)) return [128, 192, 256, 320].includes(quality); return quality >= 1 && quality <= 100; } @@ -50,6 +53,7 @@ function registerAllConverters() { registerDfontConverter(); registerEbookConverter(); registerArchiveConverters(); + registerAudioConverters(); convertersRegistered = true; } diff --git a/src/worker.js b/src/worker.js index 9be557d..d3eb3af 100644 --- a/src/worker.js +++ b/src/worker.js @@ -14,6 +14,7 @@ import { registerFontConverter } from './converters/font.js'; import { registerDfontConverter } from './converters/dfont.js'; import { registerEbookConverter } from './converters/ebook.js'; import { registerArchiveConverters } from './converters/archive.js'; +import { registerAudioConverters } from './converters/audio.js'; import { findPendingJobs, markProcessing, markDone, markFailed } from './jobs/jobRepository.js'; const JOB_TIMEOUT_MS = 60000; @@ -91,6 +92,7 @@ async function main() { registerDfontConverter(); registerEbookConverter(); registerArchiveConverters(); + registerAudioConverters(); startWorker(prisma, config); console.log(`Worker started, polling every ${config.workerPollIntervalMs}ms`); diff --git a/test/api/jobs.test.js b/test/api/jobs.test.js index 0e7818a..5343d2c 100644 --- a/test/api/jobs.test.js +++ b/test/api/jobs.test.js @@ -445,3 +445,85 @@ describe('POST /api/jobs — archives', () => { await fs.unlink(fixturePath); }); }); + +describe('POST /api/jobs — audio', () => { + it('creates a pending job converting a wav upload to mp3 with a valid bitrate', async () => { + const fixturePath = path.join(config.storageDir, 'clip.wav'); + // Minimal RIFF/WAVE header: 'RIFF' + 4-byte size (unchecked) + 'WAVE'. + await fs.writeFile(fixturePath, Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WAVE')])); + + const response = await request(app) + .post('/api/jobs') + .field('targetFormats', JSON.stringify(['mp3'])) + .field('qualities', JSON.stringify([192])) + .attach('files', fixturePath, 'clip.wav'); + + expect(response.status).toBe(201); + expect(response.body.jobs[0].status).toBe('pending'); + + const job = await getJobByUuid(prisma, response.body.jobs[0].id); + expect(job.sourceFormat).toBe('wav'); + expect(job.targetFormat).toBe('mp3'); + expect(job.family).toBe('audio'); + expect(job.quality).toBe(192); + + await fs.unlink(fixturePath); + }); + + it('rejects a bitrate outside the fixed chip set for an mp3 target', async () => { + const fixturePath = path.join(config.storageDir, 'bad-bitrate.wav'); + await fs.writeFile(fixturePath, Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WAVE')])); + + const response = await request(app) + .post('/api/jobs') + .field('targetFormats', JSON.stringify(['mp3'])) + .field('qualities', JSON.stringify([200])) + .attach('files', fixturePath, 'bad-bitrate.wav'); + + expect(response.status).toBe(201); + expect(response.body.jobs[0].error).toMatch(/Invalid quality/); + + await fs.unlink(fixturePath); + }); + + it('rejects a quality value for a flac target (lossless, no bitrate control)', async () => { + const fixturePath = path.join(config.storageDir, 'to-flac.wav'); + await fs.writeFile(fixturePath, Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WAVE')])); + + const response = await request(app) + .post('/api/jobs') + .field('targetFormats', JSON.stringify(['flac'])) + .field('qualities', JSON.stringify([192])) + .attach('files', fixturePath, 'to-flac.wav'); + + expect(response.status).toBe(201); + expect(response.body.jobs[0].error).toMatch(/Invalid quality/); + + await fs.unlink(fixturePath); + }); + + it('creates a pending job converting flac to wav with no quality value', async () => { + const fixturePath = path.join(config.storageDir, 'clip.flac'); + await fs.writeFile(fixturePath, Buffer.from('fLaC')); + + const response = await request(app) + .post('/api/jobs') + .field('targetFormats', JSON.stringify(['wav'])) + .attach('files', fixturePath, 'clip.flac'); + + expect(response.status).toBe(201); + const job = await getJobByUuid(prisma, response.body.jobs[0].id); + expect(job.sourceFormat).toBe('flac'); + expect(job.targetFormat).toBe('wav'); + expect(job.quality).toBeNull(); + + await fs.unlink(fixturePath); + }); + + it('lists the other 5 audio formats as targets for mp3, and never lists mp3 as its own target', async () => { + const response = await request(app).get('/api/formats').query({ source: 'mp3' }); + + expect(response.body.targets).toEqual(expect.arrayContaining(['wav', 'ogg', 'flac', 'aac', 'm4a'])); + expect(response.body.targets).not.toContain('mp3'); + }); +});