From d3e12deb18b5550ebfbe93c5c3f66f0f09a3b1c0 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Tue, 19 May 2026 00:17:00 -0400 Subject: [PATCH] feat(assets): add POST /:id/retry to re-queue errored assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assets stuck in status='error' had no recovery path without manual DB edits. Adds a retry endpoint that re-dispatches the proxy job, which chains into thumbnail generation automatically and restores the asset to 'processing' → 'ready' without operator intervention. --- services/mam-api/src/routes/assets.js | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/services/mam-api/src/routes/assets.js b/services/mam-api/src/routes/assets.js index eda9dfe..06af5d0 100644 --- a/services/mam-api/src/routes/assets.js +++ b/services/mam-api/src/routes/assets.js @@ -15,6 +15,10 @@ const parseRedisUrl = (url) => { return { host: parsed.hostname, port: parseInt(parsed.port, 10) }; }; +const proxyQueue = new Queue('proxy', { + connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'), +}); + const thumbnailQueue = new Queue('thumbnail', { connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'), }); @@ -324,6 +328,46 @@ router.post('/:id/copy', async (req, res, next) => { } }); +// POST /:id/retry - Re-queue proxy generation for an asset stuck in error state +// +// Proxy failures leave assets at status='error' with no recovery path from the +// UI. This endpoint re-dispatches the proxy job so the worker chain +// (proxy → thumbnail) runs again without manual DB edits. +router.post('/:id/retry', async (req, res, next) => { + try { + const { id } = req.params; + const r = await pool.query('SELECT * FROM assets WHERE id = $1', [id]); + if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' }); + const asset = r.rows[0]; + + if (asset.status !== 'error') { + return res.status(400).json({ error: `Asset is not in error state (current: ${asset.status})` }); + } + if (!asset.original_s3_key) { + return res.status(400).json({ error: 'Asset has no source file to reprocess' }); + } + + // Re-use the existing proxy key if one was partially written; otherwise + // construct the canonical key so the worker chain writes to the right place. + const proxyKey = asset.proxy_s3_key || `proxies/${id}.mp4`; + + await proxyQueue.add('generate', { + assetId: id, + inputKey: asset.original_s3_key, + outputKey: proxyKey, + }); + + const updated = await pool.query( + `UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = $1 RETURNING *`, + [id] + ); + + res.json(updated.rows[0]); + } catch (err) { + next(err); + } +}); + // DELETE /:id - Soft or hard delete router.delete('/:id', async (req, res, next) => { try {