feat: register ebook converter in the app so the API serves the new family

Also fixes a real bug found via the fb2 API test: file-type sniffs a
real fb2 file's XML declaration as generic "xml", not "fb2" and not
undetected, so it never reached the undetectable-format fallback added
in the previous commit. Added an fb2->xml alias in normalizeFormat,
same pattern as the existing azw3->mobi one.
This commit is contained in:
2026-07-31 13:29:06 +02:00
parent 977dac35d9
commit 86b555b334
4 changed files with 68 additions and 1 deletions
+2
View File
@@ -12,6 +12,7 @@ import { registerIcoConverter } from './converters/ico.js';
import { registerHeicConverter } from './converters/heic.js';
import { registerFontConverter } from './converters/font.js';
import { registerDfontConverter } from './converters/dfont.js';
import { registerEbookConverter } from './converters/ebook.js';
import { resolveInputFormat } from './mime.js';
import { deleteIfExists } from './storage.js';
import { createJob, getJobByUuid } from './jobs/jobRepository.js';
@@ -44,6 +45,7 @@ function registerAllConverters() {
registerHeicConverter();
registerFontConverter();
registerDfontConverter();
registerEbookConverter();
convertersRegistered = true;
}
+1
View File
@@ -66,6 +66,7 @@ function normalizeFormat(format) {
if (format === 'jpg') return 'jpeg';
if (format === 'heif') return 'heic';
if (format === 'azw3') return 'mobi';
if (format === 'fb2') return 'xml';
return format;
}
+37
View File
@@ -58,6 +58,43 @@ describe('GET /api/formats', () => {
});
});
describe('GET /api/formats — ebooks', () => {
it('lists the other 9 ebook formats as targets for epub, and never lists epub as its own target', async () => {
const response = await request(app).get('/api/formats').query({ source: 'epub' });
expect(response.body.targets).toEqual(
expect.arrayContaining(['fb2', 'lrf', 'mobi', 'pdb', 'rb', 'snb', 'tcr', 'azw3', 'pdf'])
);
expect(response.body.targets).not.toContain('epub');
});
});
describe('POST /api/jobs — ebooks', () => {
it('creates a pending job converting an fb2 upload to epub, accepting it despite file-type sniffing its XML declaration as generic "xml" rather than "fb2"', async () => {
const fixturePath = path.join(config.storageDir, 'book.fb2');
await fs.writeFile(
fixturePath,
'<?xml version="1.0" encoding="utf-8"?><FictionBook><body><p>hello</p></body></FictionBook>'
);
const response = await request(app)
.post('/api/jobs')
.field('targetFormats', JSON.stringify(['epub']))
.attach('files', fixturePath, 'book.fb2');
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('fb2');
expect(job.targetFormat).toBe('epub');
expect(job.family).toBe('ebook');
expect(job.inputMimeType).toBe('application/xml');
await fs.unlink(fixturePath);
});
});
describe('POST /api/jobs', () => {
it('creates a pending job for a valid image upload', async () => {
const fixturePath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.png');
+28 -1
View File
@@ -169,7 +169,6 @@ describe('resolveInputFormat — azw3/mobi collision', () => {
describe('resolveInputFormat — undetectable ebook formats', () => {
const undetectableFormats = {
fb2: 'application/x-fictionbook+xml',
lrf: 'application/octet-stream',
pdb: 'application/vnd.palm',
rb: 'application/octet-stream',
@@ -192,6 +191,34 @@ describe('resolveInputFormat — undetectable ebook formats', () => {
);
});
describe('resolveInputFormat — fb2', () => {
it('accepts real fb2 content (file-type sniffs its XML declaration as generic "xml", not "fb2")', async () => {
const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample-real.fb2');
await fs.writeFile(
fixturePath,
'<?xml version="1.0" encoding="utf-8"?><FictionBook><body><p>hello</p></body></FictionBook>'
);
const detected = await detectInputMime(fixturePath);
expect(detected).toEqual({ ext: 'xml', mime: 'application/xml' });
const result = await resolveInputFormat(fixturePath, 'fb2');
expect(result).toEqual({ mime: 'application/xml', valid: true });
await fs.unlink(fixturePath);
});
it('falls back to trusting the declared format for fb2 content with no recognizable XML declaration', async () => {
const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample-no-prolog.fb2');
await fs.writeFile(fixturePath, 'arbitrary bytes with no recognizable magic number');
const result = await resolveInputFormat(fixturePath, 'fb2');
expect(result).toEqual({ mime: 'application/x-fictionbook+xml', valid: true });
await fs.unlink(fixturePath);
});
});
describe('resolveInputFormat — dfont', () => {
it('confirms file-type alone misidentifies the dfont fixture as ico (documents the bug this fix works around)', async () => {
const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample.dfont');