import { pathToFileURL } from 'node:url'; import fs from 'node:fs/promises'; import { loadConfig } from './config.js'; import { getPrismaClient } from './db.js'; import { ensureStorageDirs, uploadPath, outputPath, deleteIfExists } from './storage.js'; import { outputMimeType } from './mime.js'; import { resolve as resolveConverter } from './converters/registry.js'; import { registerImageConverters } from './converters/image.js'; import { registerImageToPdfConverter } from './converters/imageToPdf.js'; import { registerDocumentConverters } from './converters/document.js'; 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 { registerArchiveConverters } from './converters/archive.js'; import { registerAudioConverters } from './converters/audio.js'; import { registerVideoConverters } from './converters/video.js'; import { findPendingJobs, markProcessing, markDone, markFailed } from './jobs/jobRepository.js'; const JOB_TIMEOUT_MS = 60000; function withTimeout(promise, ms) { let timeoutId; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => reject(new Error(`Conversion timed out after ${ms}ms`)), ms); }); return Promise.race([promise, timeout]).finally(() => clearTimeout(timeoutId)); } async function processJob(prisma, config, job) { await markProcessing(prisma, job.id); const inputFilePath = uploadPath(config, job.uuid, job.sourceFormat); const outputFilePath = outputPath(config, job.uuid, job.targetFormat); try { const entry = resolveConverter(job.sourceFormat, job.targetFormat); if (!entry) { throw new Error(`No converter registered for ${job.sourceFormat} -> ${job.targetFormat}`); } const timeoutMs = entry.family === 'ebook' ? config.ebookJobTimeoutMs : entry.family === 'video' ? config.videoJobTimeoutMs : JOB_TIMEOUT_MS; const startedAt = Date.now(); await withTimeout( entry.convert(inputFilePath, outputFilePath, { quality: job.quality, iconSize: job.iconSize, timeoutMs }), timeoutMs ); const conversionDurationSeconds = (Date.now() - startedAt) / 1000; const { size: outputSizeBytes } = await fs.stat(outputFilePath); await markDone(prisma, job.id, { outputPath: `${job.uuid}.${job.targetFormat}`, outputMimeType: outputMimeType(job.targetFormat), outputSizeBytes, conversionDurationSeconds, }); } catch (error) { await deleteIfExists(outputFilePath); await markFailed(prisma, job.id, { errorMessage: 'Conversion failed, please try again.', errorLog: error.stack ?? String(error), }); } } export async function processPendingJobs(prisma, config) { const jobs = await findPendingJobs(prisma, config.workerConcurrency); await Promise.all(jobs.map((job) => processJob(prisma, config, job))); return jobs.length; } export function startWorker(prisma, config) { const interval = setInterval(() => { processPendingJobs(prisma, config).catch((error) => { console.error('Error while processing pending jobs:', error); }); }, config.workerPollIntervalMs); return () => clearInterval(interval); } async function main() { const config = loadConfig(); await ensureStorageDirs(config); const prisma = getPrismaClient(config); registerImageConverters(); registerImageToPdfConverter(); registerDocumentConverters(); registerIcoConverter(); registerHeicConverter(); registerFontConverter(); registerDfontConverter(); registerEbookConverter(); registerArchiveConverters(); registerAudioConverters(); registerVideoConverters(); startWorker(prisma, config); console.log(`Worker started, polling every ${config.workerPollIntervalMs}ms`); } main();