fix: add POST /assets handler for capture registration + thumbnail job dispatch
This commit is contained in:
parent
cd0c724bdd
commit
db73235149
1 changed files with 100 additions and 21 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import express from 'express';
|
||||
import { Queue } from 'bullmq';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import pool from '../db/pool.js';
|
||||
import { getSignedUrlForObject, deleteObject } from '../s3/client.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
|
|
@ -7,6 +9,16 @@ const router = express.Router();
|
|||
|
||||
router.use(requireAuth);
|
||||
|
||||
// BullMQ queue connection (mirrors worker/src/index.js)
|
||||
const parseRedisUrl = (url) => {
|
||||
const parsed = new URL(url);
|
||||
return { host: parsed.hostname, port: parseInt(parsed.port, 10) };
|
||||
};
|
||||
|
||||
const thumbnailQueue = new Queue('thumbnail', {
|
||||
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
|
||||
});
|
||||
|
||||
// GET / - List assets with filtering
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
|
|
@ -60,7 +72,7 @@ router.get('/', async (req, res, next) => {
|
|||
params.push(limit, offset);
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
const total = result.rows.length > 0 ? result.rows[0].full_count : 0;
|
||||
const total = result.rows.length > 0 ? parseInt(result.rows[0].full_count, 10) : 0;
|
||||
|
||||
res.json({
|
||||
assets: result.rows,
|
||||
|
|
@ -71,6 +83,83 @@ router.get('/', async (req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
// POST / - Register a new asset from a completed capture session
|
||||
//
|
||||
// Called by the capture service immediately after stop() completes.
|
||||
// At this point both the HiRes and Proxy files already exist in S3
|
||||
// (written by the dual FFmpeg stream), so we set status='processing'
|
||||
// and immediately dispatch a thumbnail job — no proxy_gen needed.
|
||||
router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
projectId,
|
||||
binId,
|
||||
clipName,
|
||||
hiresKey,
|
||||
proxyKey,
|
||||
duration, // seconds (integer)
|
||||
capturedAt, // ISO 8601 string
|
||||
} = req.body;
|
||||
|
||||
if (!projectId || !clipName) {
|
||||
return res.status(400).json({ error: 'projectId and clipName are required' });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
const thumbnailKey = `thumbnails/${id}.jpg`;
|
||||
const durationMs = duration ? Math.round(duration * 1000) : null;
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO assets (
|
||||
id, project_id, bin_id,
|
||||
filename, display_name,
|
||||
status, media_type,
|
||||
original_s3_key, proxy_s3_key,
|
||||
duration_ms,
|
||||
created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3,
|
||||
$4, $4,
|
||||
'processing', 'video',
|
||||
$5, $6,
|
||||
$7,
|
||||
COALESCE($8::timestamptz, NOW()), NOW()
|
||||
)
|
||||
RETURNING *`,
|
||||
[
|
||||
id, projectId, binId || null,
|
||||
clipName,
|
||||
hiresKey || null, proxyKey || null,
|
||||
durationMs,
|
||||
capturedAt || null,
|
||||
]
|
||||
);
|
||||
|
||||
const asset = result.rows[0];
|
||||
|
||||
// Dispatch thumbnail job — proxy already in S3 from capture
|
||||
if (proxyKey) {
|
||||
await thumbnailQueue.add('generate', {
|
||||
assetId: id,
|
||||
proxyKey,
|
||||
outputKey: thumbnailKey,
|
||||
});
|
||||
} else {
|
||||
// No proxy yet — mark ready immediately (e.g. audio-only or test mode)
|
||||
await pool.query(
|
||||
`UPDATE assets SET status = 'ready', updated_at = NOW() WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
asset.status = 'ready';
|
||||
}
|
||||
|
||||
res.status(201).json(asset);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /:id - Single asset
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
|
|
@ -87,7 +176,7 @@ router.get('/:id', async (req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
// PATCH /:id - Update asset
|
||||
// PATCH /:id - Update asset metadata
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
|
@ -145,7 +234,6 @@ router.delete('/:id', async (req, res, next) => {
|
|||
const { hard } = req.query;
|
||||
|
||||
if (hard === 'true') {
|
||||
// Hard delete: get asset info, delete from S3, delete from DB
|
||||
const assetResult = await pool.query(
|
||||
'SELECT * FROM assets WHERE id = $1',
|
||||
[id]
|
||||
|
|
@ -157,26 +245,17 @@ router.delete('/:id', async (req, res, next) => {
|
|||
|
||||
const asset = assetResult.rows[0];
|
||||
|
||||
// Delete from S3
|
||||
if (asset.proxy_s3_key) {
|
||||
await deleteObject(asset.proxy_s3_key);
|
||||
}
|
||||
if (asset.thumbnail_s3_key) {
|
||||
await deleteObject(asset.thumbnail_s3_key);
|
||||
}
|
||||
if (asset.original_s3_key) {
|
||||
await deleteObject(asset.original_s3_key);
|
||||
}
|
||||
if (asset.proxy_s3_key) await deleteObject(asset.proxy_s3_key);
|
||||
if (asset.thumbnail_s3_key) await deleteObject(asset.thumbnail_s3_key);
|
||||
if (asset.original_s3_key) await deleteObject(asset.original_s3_key);
|
||||
|
||||
// Delete from database
|
||||
await pool.query('DELETE FROM assets WHERE id = $1', [id]);
|
||||
|
||||
res.json({ message: 'Asset deleted permanently' });
|
||||
} else {
|
||||
// Soft delete: set status to archived
|
||||
const result = await pool.query(
|
||||
'UPDATE assets SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
||||
['archived', id]
|
||||
`UPDATE assets SET status = 'archived', updated_at = NOW()
|
||||
WHERE id = $1 RETURNING *`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
|
|
@ -206,7 +285,7 @@ router.get('/:id/stream', async (req, res, next) => {
|
|||
const { proxy_s3_key } = result.rows[0];
|
||||
|
||||
if (!proxy_s3_key) {
|
||||
return res.status(400).json({ error: 'No proxy available' });
|
||||
return res.status(400).json({ error: 'No proxy available for this asset' });
|
||||
}
|
||||
|
||||
const url = await getSignedUrlForObject(proxy_s3_key);
|
||||
|
|
@ -216,7 +295,7 @@ router.get('/:id/stream', async (req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
// GET /:id/thumbnail - Signed URL for thumbnail
|
||||
// GET /:id/thumbnail - Signed URL for thumbnail image
|
||||
router.get('/:id/thumbnail', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
|
@ -232,7 +311,7 @@ router.get('/:id/thumbnail', async (req, res, next) => {
|
|||
const { thumbnail_s3_key } = result.rows[0];
|
||||
|
||||
if (!thumbnail_s3_key) {
|
||||
return res.status(400).json({ error: 'No thumbnail available' });
|
||||
return res.status(404).json({ error: 'Thumbnail not yet available' });
|
||||
}
|
||||
|
||||
const url = await getSignedUrlForObject(thumbnail_s3_key);
|
||||
|
|
|
|||
Loading…
Reference in a new issue