2026-04-07 21:58:27 -04:00
import express from 'express' ;
2026-05-15 21:24:02 -04:00
import { Queue } from 'bullmq' ;
import { v4 as uuidv4 } from 'uuid' ;
2026-05-23 10:28:42 -04:00
import { promises as fs } from 'node:fs' ;
import path from 'node:path' ;
2026-04-07 21:58:27 -04:00
import pool from '../db/pool.js' ;
2026-05-20 15:49:40 -04:00
import { getSignedUrlForObject , deleteObject , s3Client , getS3Bucket } from '../s3/client.js' ;
2026-05-19 22:47:33 -04:00
import { GetObjectCommand } from '@aws-sdk/client-s3' ;
2026-04-07 21:58:27 -04:00
import { requireAuth } from '../middleware/auth.js' ;
const router = express . Router ( ) ;
router . use ( requireAuth ) ;
2026-05-15 21:24:02 -04:00
// 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 ) } ;
} ;
2026-05-19 00:17:00 -04:00
const proxyQueue = new Queue ( 'proxy' , {
connection : parseRedisUrl ( process . env . REDIS _URL || 'redis://queue:6379' ) ,
} ) ;
2026-05-15 21:24:02 -04:00
const thumbnailQueue = new Queue ( 'thumbnail' , {
connection : parseRedisUrl ( process . env . REDIS _URL || 'redis://queue:6379' ) ,
} ) ;
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
const trimQueue = new Queue ( 'trim' , {
connection : parseRedisUrl ( process . env . REDIS _URL || 'redis://queue:6379' ) ,
} ) ;
2026-05-26 12:39:44 -04:00
const filmstripQueue = new Queue ( 'filmstrip' , {
connection : parseRedisUrl ( process . env . REDIS _URL || 'redis://queue:6379' ) ,
} ) ;
2026-04-07 21:58:27 -04:00
// GET / - List assets with filtering
router . get ( '/' , async ( req , res , next ) => {
try {
const {
project _id ,
bin _id ,
status ,
search ,
media _type ,
limit = 50 ,
offset = 0 ,
2026-05-17 12:55:36 -04:00
include _archived ,
2026-04-07 21:58:27 -04:00
} = req . query ;
let query = `
SELECT a . * ,
COUNT ( * ) OVER ( ) AS full _count
FROM assets a
WHERE 1 = 1
` ;
const params = [ ] ;
let paramCount = 1 ;
2026-05-25 19:29:23 -04:00
// Exclude archived unless explicitly requested — independent of status filter
if ( include _archived !== 'true' ) {
2026-05-17 12:55:36 -04:00
query += ` AND a.status <> 'archived' ` ;
}
2026-04-07 21:58:27 -04:00
if ( project _id ) {
query += ` AND a.project_id = $ ${ paramCount ++ } ` ;
params . push ( project _id ) ;
}
if ( bin _id ) {
query += ` AND a.bin_id = $ ${ paramCount ++ } ` ;
params . push ( bin _id ) ;
}
if ( status ) {
query += ` AND a.status = $ ${ paramCount ++ } ` ;
params . push ( status ) ;
}
if ( media _type ) {
query += ` AND a.media_type = $ ${ paramCount ++ } ` ;
params . push ( media _type ) ;
}
if ( search ) {
2026-05-19 23:10:51 -04:00
query += ` AND (a.display_name ILIKE $ ${ paramCount } OR a.filename ILIKE $ ${ paramCount } OR a.notes ILIKE $ ${ paramCount } ) ` ;
2026-04-07 21:58:27 -04:00
params . push ( ` % ${ search } % ` ) ;
paramCount ++ ;
}
query += ` ORDER BY a.created_at DESC ` ;
query += ` LIMIT $ ${ paramCount ++ } OFFSET $ ${ paramCount ++ } ` ;
params . push ( limit , offset ) ;
const result = await pool . query ( query , params ) ;
2026-05-15 21:24:02 -04:00
const total = result . rows . length > 0 ? parseInt ( result . rows [ 0 ] . full _count , 10 ) : 0 ;
2026-04-07 21:58:27 -04:00
res . json ( {
2026-05-18 23:44:14 -04:00
assets : result . rows . map ( ( { full _count , ... rest } ) => rest ) ,
2026-04-07 21:58:27 -04:00
total ,
} ) ;
} catch ( err ) {
next ( err ) ;
}
} ) ;
2026-05-15 21:24:02 -04:00
// POST / - Register a new asset from a completed capture session
router . post ( '/' , async ( req , res , next ) => {
try {
const {
projectId ,
binId ,
clipName ,
hiresKey ,
proxyKey ,
2026-05-20 15:49:40 -04:00
duration ,
capturedAt ,
2026-05-26 10:10:44 -04:00
sourceType , // Bug #64: was ignored — now used to set media_type
needsProxy , // Bug #64: was ignored — now controls proxy queue logic
2026-05-15 21:24:02 -04:00
} = req . body ;
if ( ! projectId || ! clipName ) {
return res . status ( 400 ) . json ( { error : 'projectId and clipName are required' } ) ;
}
2026-05-25 19:29:23 -04:00
const durationNum = duration !== undefined && duration !== null ? Number ( duration ) : null ;
if ( durationNum !== null && ! Number . isFinite ( durationNum ) ) {
return res . status ( 400 ) . json ( { error : 'duration must be a finite number (seconds)' } ) ;
}
const durationMs = durationNum !== null ? Math . round ( durationNum * 1000 ) : null ;
2026-05-15 21:24:02 -04:00
2026-05-18 07:29:50 -04:00
const existing = await pool . query (
` SELECT * FROM assets
WHERE project _id = $1 AND display _name = $2 AND status = 'live'
ORDER BY created _at DESC LIMIT 1 ` ,
[ projectId , clipName ]
2026-05-15 21:24:02 -04:00
) ;
2026-05-26 10:10:44 -04:00
// Bug #63: refuse to overwrite a live asset — return 409 with the existing row
if ( existing . rows . length > 0 ) {
return res . status ( 409 ) . json ( {
error : 'A live asset with this name already exists' ,
asset : existing . rows [ 0 ] ,
} ) ;
}
2026-05-18 07:29:50 -04:00
let id ;
let asset ;
2026-05-26 10:10:44 -04:00
{
2026-05-18 07:29:50 -04:00
id = uuidv4 ( ) ;
2026-05-26 10:10:44 -04:00
// Bug #64: use sourceType to set media_type (default 'video')
const mediaType = ( sourceType === 'audio' ) ? 'audio' : 'video' ;
2026-05-18 07:29:50 -04:00
const ins = 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 ,
2026-05-26 10:10:44 -04:00
'processing' , $9 ,
2026-05-18 07:29:50 -04:00
$5 , $6 ,
$7 ,
COALESCE ( $8 : : timestamptz , NOW ( ) ) , NOW ( )
)
RETURNING * ` ,
[
id , projectId , binId || null ,
clipName ,
hiresKey || null , proxyKey || null ,
durationMs ,
capturedAt || null ,
2026-05-26 10:10:44 -04:00
mediaType ,
2026-05-18 07:29:50 -04:00
]
) ;
asset = ins . rows [ 0 ] ;
}
const thumbnailKey = ` thumbnails/ ${ id } .jpg ` ;
2026-05-26 10:10:44 -04:00
// Bug #64: when needsProxy is explicitly false and proxyKey is already set,
// skip re-queuing a proxy job and mark the asset ready immediately.
if ( needsProxy === false && proxyKey ) {
await pool . query ( ` UPDATE assets SET status = 'ready', updated_at = NOW() WHERE id = $ 1 ` , [ id ] ) ;
asset . status = 'ready' ;
} else if ( proxyKey ) {
2026-05-25 19:29:23 -04:00
await thumbnailQueue . add ( 'generate' , { assetId : id , proxyKey , outputKey : thumbnailKey } ) ;
2026-05-22 23:41:03 -04:00
} else if ( asset . original _s3 _key ) {
const generatedProxyKey = ` proxies/ ${ id } .mp4 ` ;
await proxyQueue . add ( 'generate' , {
2026-05-25 19:29:23 -04:00
assetId : id , inputKey : asset . original _s3 _key , outputKey : generatedProxyKey ,
2026-05-22 23:41:03 -04:00
} ) ;
console . log ( ` [assets] queued proxy for ${ id } ( ${ asset . display _name } ) ` ) ;
2026-05-15 21:24:02 -04:00
} else {
2026-05-25 19:29:23 -04:00
await pool . query ( ` UPDATE assets SET status = 'ready', updated_at = NOW() WHERE id = $ 1 ` , [ id ] ) ;
2026-05-15 21:24:02 -04:00
asset . status = 'ready' ;
}
2026-05-26 10:10:44 -04:00
res . status ( 201 ) . json ( asset ) ;
2026-05-15 21:24:02 -04:00
} catch ( err ) {
2026-05-26 10:10:44 -04:00
// Unique constraint violation from the partial index (migration 017) — two
// concurrent captures raced through the SELECT before either INSERT landed.
if ( err . code === '23505' && err . constraint === 'idx_assets_live_unique' ) {
return res . status ( 409 ) . json ( { error : 'A live asset with this name already exists (concurrent capture race)' } ) ;
}
2026-05-15 21:24:02 -04:00
next ( err ) ;
}
2026-05-26 11:05:50 -04:00
} ) ;
2026-05-15 21:24:02 -04:00
2026-05-20 15:49:40 -04:00
// POST /cleanup-live
2026-05-19 23:10:51 -04:00
router . post ( '/cleanup-live' , async ( req , res , next ) => {
try {
const maxAgeHours = Math . max ( 1 , parseInt ( req . query . max _age _hours || '4' , 10 ) ) ;
const result = await pool . query (
2026-05-25 19:29:23 -04:00
` UPDATE assets SET status = 'error', updated_at = NOW()
WHERE status = 'live' AND created _at < NOW ( ) - ( $1 * INTERVAL '1 hour' )
2026-05-19 23:10:51 -04:00
RETURNING id , display _name , project _id , created _at ` ,
[ maxAgeHours ]
) ;
res . json ( { cleaned : result . rowCount , assets : result . rows } ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
2026-05-19 23:10:51 -04:00
} ) ;
2026-05-23 10:28:42 -04:00
// POST /cleanup-live-orphans
router . post ( '/cleanup-live-orphans' , async ( _req , res , next ) => {
try {
const liveRoot = process . env . LIVE _DIR || '/live' ;
let entries ;
try {
entries = await fs . readdir ( liveRoot , { withFileTypes : true } ) ;
} catch ( err ) {
if ( err . code === 'ENOENT' ) return res . json ( { reaped : 0 , kept : 0 , dirs : [ ] } ) ;
throw err ;
}
const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i ;
2026-05-25 19:29:23 -04:00
const dirIds = entries . filter ( e => e . isDirectory ( ) && uuidRe . test ( e . name ) ) . map ( e => e . name ) ;
2026-05-23 10:28:42 -04:00
if ( dirIds . length === 0 ) return res . json ( { reaped : 0 , kept : 0 , dirs : [ ] } ) ;
2026-05-25 19:29:23 -04:00
const known = await pool . query ( 'SELECT id FROM assets WHERE id = ANY($1::uuid[])' , [ dirIds ] ) ;
2026-05-23 10:28:42 -04:00
const keep = new Set ( known . rows . map ( r => r . id ) ) ;
2026-05-25 19:29:23 -04:00
const reaped = [ ] , kept = [ ] ;
2026-05-23 10:28:42 -04:00
for ( const id of dirIds ) {
if ( keep . has ( id ) ) { kept . push ( id ) ; continue ; }
const fullPath = path . join ( liveRoot , id ) ;
try {
await fs . rm ( fullPath , { recursive : true , force : true } ) ;
reaped . push ( id ) ;
console . log ( ` [assets] reaped orphan live dir ${ fullPath } ` ) ;
} catch ( err ) {
console . warn ( ` [assets] failed to reap ${ fullPath } : ${ err . message } ` ) ;
}
}
res . json ( { reaped : reaped . length , kept : kept . length , dirs : reaped } ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
2026-05-23 10:28:42 -04:00
} ) ;
2026-05-20 15:49:40 -04:00
// GET /:id
2026-04-07 21:58:27 -04:00
router . get ( '/:id' , async ( req , res , next ) => {
try {
const { id } = req . params ;
const result = await pool . query ( 'SELECT * FROM assets WHERE id = $1' , [ id ] ) ;
2026-05-20 15:49:40 -04:00
if ( result . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
2026-04-07 21:58:27 -04:00
res . json ( result . rows [ 0 ] ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
2026-04-07 21:58:27 -04:00
} ) ;
2026-05-20 15:49:40 -04:00
// PATCH /:id
2026-04-07 21:58:27 -04:00
router . patch ( '/:id' , async ( req , res , next ) => {
try {
const { id } = req . params ;
2026-05-17 14:48:23 -04:00
const { display _name , tags , notes , bin _id } = req . body ;
2026-05-25 19:29:23 -04:00
const updates = [ ] , params = [ ] ;
2026-04-07 21:58:27 -04:00
let paramCount = 1 ;
2026-05-20 15:49:40 -04:00
if ( display _name !== undefined ) { updates . push ( ` display_name = $ ${ paramCount ++ } ` ) ; params . push ( display _name ) ; }
if ( tags !== undefined ) { updates . push ( ` tags = $ ${ paramCount ++ } ` ) ; params . push ( tags ) ; }
if ( notes !== undefined ) { updates . push ( ` notes = $ ${ paramCount ++ } ` ) ; params . push ( notes ) ; }
if ( bin _id !== undefined ) { updates . push ( ` bin_id = $ ${ paramCount ++ } ` ) ; params . push ( bin _id || null ) ; }
if ( updates . length === 0 ) return res . status ( 400 ) . json ( { error : 'No fields to update' } ) ;
2026-04-07 21:58:27 -04:00
updates . push ( ` updated_at = NOW() ` ) ;
params . push ( id ) ;
2026-05-20 15:49:40 -04:00
const result = await pool . query (
2026-05-25 19:29:23 -04:00
` UPDATE assets SET ${ updates . join ( ', ' ) } WHERE id = $ ${ paramCount } RETURNING * ` , params
2026-05-20 15:49:40 -04:00
) ;
if ( result . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
2026-04-07 21:58:27 -04:00
res . json ( result . rows [ 0 ] ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
2026-04-07 21:58:27 -04:00
} ) ;
2026-05-20 15:49:40 -04:00
// POST /:id/copy
2026-05-17 14:48:23 -04:00
router . post ( '/:id/copy' , async ( req , res , next ) => {
try {
const { id } = req . params ;
const { binId , projectId } = req . body ;
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 src = r . rows [ 0 ] ;
const newId = uuidv4 ( ) ;
2026-05-26 10:10:44 -04:00
// Bug #60: null out proxy_s3_key and thumbnail_s3_key on the copy to avoid
// sharing S3 objects with the source. Set status to 'processing' so the copy
// gets its own proxy generated. Re-queue proxy generation below if source exists.
2026-05-17 14:48:23 -04:00
const ins = await pool . query (
` INSERT INTO assets (
id , project _id , bin _id , filename , display _name ,
status , media _type , original _s3 _key , proxy _s3 _key , thumbnail _s3 _key ,
codec , resolution , fps , duration _ms , start _tc , file _size , tags , notes ,
created _at , updated _at
) VALUES (
2026-05-26 10:10:44 -04:00
$1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , NULL , NULL , $9 , $10 , $11 , $12 , $13 , $14 , $15 , $16 , NOW ( ) , NOW ( )
2026-05-17 14:48:23 -04:00
) RETURNING * ` ,
[
2026-05-25 19:29:23 -04:00
newId , projectId || src . project _id ,
2026-05-17 14:48:23 -04:00
binId === undefined ? src . bin _id : ( binId || null ) ,
2026-05-26 10:10:44 -04:00
src . filename , src . display _name , 'processing' , src . media _type ,
src . original _s3 _key ,
2026-05-20 15:49:40 -04:00
src . codec , src . resolution , src . fps , src . duration _ms , src . start _tc ,
src . file _size , src . tags , src . notes ,
2026-05-17 14:48:23 -04:00
]
) ;
2026-05-26 10:10:44 -04:00
const copy = ins . rows [ 0 ] ;
// Re-queue proxy generation from original_s3_key so the copy gets its own proxy
if ( copy . original _s3 _key ) {
const newProxyKey = ` proxies/ ${ newId } .mp4 ` ;
await proxyQueue . add ( 'generate' , {
assetId : newId , inputKey : copy . original _s3 _key , outputKey : newProxyKey ,
} ) ;
console . log ( ` [assets] queued proxy for copy ${ newId } from ${ newProxyKey } ` ) ;
} else {
// No source to transcode from — mark ready immediately
await pool . query ( ` UPDATE assets SET status = 'ready', updated_at = NOW() WHERE id = $ 1 ` , [ newId ] ) ;
copy . status = 'ready' ;
}
res . status ( 201 ) . json ( copy ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
2026-05-17 14:48:23 -04:00
} ) ;
2026-05-25 19:29:23 -04:00
// POST /:id/mark-empty
2026-05-22 23:52:30 -04:00
router . post ( '/:id/mark-empty' , async ( req , res , next ) => {
try {
const { id } = req . params ;
2026-05-26 10:10:44 -04:00
// Bug #66: first check the asset exists and what status it is in
const check = await pool . query ( ` SELECT id, status FROM assets WHERE id = $ 1 ` , [ id ] ) ;
if ( check . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
const current = check . rows [ 0 ] . status ;
// Already terminal — nothing to do, return 200 with skipped flag
if ( current === 'error' || current === 'ready' ) {
return res . status ( 200 ) . json ( { id , skipped : true } ) ;
}
// Allow update for 'live' or 'processing' (race condition on shutdown)
if ( current === 'live' || current === 'processing' ) {
await pool . query (
` UPDATE assets
SET status = 'error' ,
notes = COALESCE ( notes || E '\\n' , '' ) || 'Recording produced no frames — source never connected.' ,
updated _at = NOW ( )
WHERE id = $1 ` ,
[ id ]
) ;
return res . json ( { id } ) ;
}
// Any other status (e.g. 'archived') is incompatible
return res . status ( 409 ) . json ( {
error : ` Cannot mark-empty an asset with status ' ${ current } ' ` ,
status : current ,
} ) ;
2026-05-22 23:52:30 -04:00
} catch ( err ) { next ( err ) ; }
} ) ;
2026-05-25 19:29:23 -04:00
// POST /:id/generate-proxy
2026-05-22 23:41:03 -04:00
router . post ( '/:id/generate-proxy' , 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 . original _s3 _key ) return res . status ( 400 ) . json ( { error : 'Asset has no hi-res source to proxy from' } ) ;
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 (
2026-05-25 19:29:23 -04:00
` UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = $ 1 RETURNING * ` , [ id ]
2026-05-22 23:41:03 -04:00
) ;
res . json ( updated . rows [ 0 ] ) ;
} catch ( err ) { next ( err ) ; }
} ) ;
2026-05-25 19:29:23 -04:00
// POST /backfill-proxies
2026-05-22 23:41:03 -04:00
router . post ( '/backfill-proxies' , async ( _req , res , next ) => {
try {
const targets = await pool . query (
` SELECT id, original_s3_key FROM assets
2026-05-25 19:29:23 -04:00
WHERE status = 'ready' AND original _s3 _key IS NOT NULL
2026-05-22 23:41:03 -04:00
AND ( proxy _s3 _key IS NULL OR proxy _s3 _key = '' )
ORDER BY created _at DESC `
) ;
for ( const asset of targets . rows ) {
const proxyKey = ` proxies/ ${ asset . id } .mp4 ` ;
2026-05-25 19:29:23 -04:00
await proxyQueue . add ( 'generate' , { assetId : asset . id , inputKey : asset . original _s3 _key , outputKey : proxyKey } ) ;
2026-05-22 23:41:03 -04:00
}
if ( targets . rows . length > 0 ) {
await pool . query (
2026-05-25 19:29:23 -04:00
` UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = ANY( $ 1) ` ,
2026-05-22 23:41:03 -04:00
[ targets . rows . map ( r => r . id ) ]
) ;
}
res . json ( { queued : targets . rows . length } ) ;
} catch ( err ) { next ( err ) ; }
} ) ;
2026-05-26 12:39:44 -04:00
// POST /:id/reprocess?type=proxy|thumbnail|filmstrip
// Force-requeue a processing job regardless of current asset status.
2026-05-26 10:10:44 -04:00
router . post ( '/:id/reprocess' , async ( req , res , next ) => {
try {
const { id } = req . params ;
const type = req . query . type || 'proxy' ;
2026-05-26 12:39:44 -04:00
if ( ! [ 'proxy' , 'thumbnail' , 'filmstrip' ] . includes ( type ) ) {
return res . status ( 400 ) . json ( { error : 'type must be "proxy", "thumbnail", or "filmstrip"' } ) ;
2026-05-26 10:10:44 -04:00
}
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 ( type === 'proxy' ) {
2026-05-26 12:39:44 -04:00
if ( ! asset . original _s3 _key ) return res . status ( 400 ) . json ( { error : 'Asset has no source file' } ) ;
2026-05-26 10:10:44 -04:00
const proxyKey = ` proxies/ ${ id } .mp4 ` ;
await proxyQueue . add ( 'generate' , { assetId : id , inputKey : asset . original _s3 _key , outputKey : proxyKey } ) ;
await pool . query ( ` UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = $ 1 ` , [ id ] ) ;
return res . json ( { queued : 'proxy' , assetId : id } ) ;
}
if ( type === 'thumbnail' ) {
2026-05-26 12:39:44 -04:00
if ( ! asset . proxy _s3 _key ) return res . status ( 400 ) . json ( { error : 'Asset has no proxy' } ) ;
2026-05-26 10:10:44 -04:00
const thumbnailKey = ` thumbnails/ ${ id } .jpg ` ;
2026-05-26 12:39:44 -04:00
await thumbnailQueue . add ( 'generate' , { assetId : id , proxyKey : asset . proxy _s3 _key , outputKey : thumbnailKey } ) ;
2026-05-26 10:10:44 -04:00
return res . json ( { queued : 'thumbnail' , assetId : id } ) ;
}
2026-05-26 12:39:44 -04:00
if ( type === 'filmstrip' ) {
if ( ! asset . proxy _s3 _key ) return res . status ( 400 ) . json ( { error : 'Asset has no proxy — generate proxy first' } ) ;
await filmstripQueue . add ( 'generate' , { assetId : id , proxyKey : asset . proxy _s3 _key } ) ;
return res . json ( { queued : 'filmstrip' , assetId : id } ) ;
}
} catch ( err ) { next ( err ) ; }
} ) ;
// GET /:id/filmstrip — returns signed URL to the pre-built filmstrip JSON
router . get ( '/:id/filmstrip' , async ( req , res , next ) => {
try {
const { id } = req . params ;
const r = await pool . query ( 'SELECT filmstrip_s3_key FROM assets WHERE id = $1' , [ id ] ) ;
if ( r . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
const { filmstrip _s3 _key } = r . rows [ 0 ] ;
if ( ! filmstrip _s3 _key ) return res . json ( { url : null , ready : false } ) ;
const url = await getSignedUrlForObject ( filmstrip _s3 _key ) ;
res . json ( { url , ready : true } ) ;
2026-05-26 10:10:44 -04:00
} catch ( err ) { next ( err ) ; }
} ) ;
2026-05-25 19:29:23 -04:00
// POST /:id/retry
2026-05-19 00:17:00 -04:00
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 ] ;
2026-05-25 19:29:23 -04:00
if ( ! asset . original _s3 _key ) return res . status ( 400 ) . json ( { error : 'Asset has no source file to reprocess' } ) ;
const canRetry = asset . status === 'error' || ! asset . proxy _s3 _key ;
if ( ! canRetry ) return res . status ( 400 ) . json ( { error : ` Nothing to retry — asset is ${ asset . status } and already has a proxy. ` } ) ;
2026-05-19 00:17:00 -04:00
const proxyKey = asset . proxy _s3 _key || ` proxies/ ${ id } .mp4 ` ;
2026-05-20 15:49:40 -04:00
await proxyQueue . add ( 'generate' , { assetId : id , inputKey : asset . original _s3 _key , outputKey : proxyKey } ) ;
2026-05-19 00:17:00 -04:00
const updated = await pool . query (
2026-05-25 19:29:23 -04:00
` UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = $ 1 RETURNING * ` , [ id ]
2026-05-19 00:17:00 -04:00
) ;
res . json ( updated . rows [ 0 ] ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
2026-05-19 00:17:00 -04:00
} ) ;
2026-05-20 15:49:40 -04:00
// DELETE /:id
2026-04-07 21:58:27 -04:00
router . delete ( '/:id' , async ( req , res , next ) => {
try {
const { id } = req . params ;
const { hard } = req . query ;
if ( hard === 'true' ) {
2026-05-20 15:49:40 -04:00
const assetResult = await pool . query ( 'SELECT * FROM assets WHERE id = $1' , [ id ] ) ;
if ( assetResult . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
2026-04-07 21:58:27 -04:00
const asset = assetResult . rows [ 0 ] ;
2026-05-26 10:10:44 -04:00
// Remove any pending/waiting BullMQ jobs for this asset before deleting
// the row — prevents workers from receiving jobs for a non-existent asset.
for ( const queue of [ proxyQueue , thumbnailQueue ] ) {
try {
const waiting = await queue . getJobs ( [ 'waiting' , 'delayed' , 'prioritized' ] ) ;
for ( const job of waiting ) {
if ( job . data ? . assetId === id ) await job . remove ( ) ;
}
} catch ( e ) {
console . warn ( ` [assets] BullMQ cleanup failed for asset ${ id } : ` , e . message ) ;
}
}
2026-05-25 19:29:23 -04:00
const s3Errors = [ ] ;
for ( const key of [ asset . proxy _s3 _key , asset . thumbnail _s3 _key , asset . original _s3 _key ] ) {
if ( ! key ) continue ;
try { await deleteObject ( key ) ; }
catch ( e ) { s3Errors . push ( { key , error : e . message } ) ; console . warn ( ` [assets] s3 delete failed for ${ key } : ` , e . message ) ; }
}
2026-04-07 21:58:27 -04:00
await pool . query ( 'DELETE FROM assets WHERE id = $1' , [ id ] ) ;
2026-05-25 19:29:23 -04:00
res . json ( { message : 'Asset deleted permanently' , ... ( s3Errors . length ? { s3Errors } : { } ) } ) ;
2026-04-07 21:58:27 -04:00
} else {
const result = await pool . query (
2026-05-25 19:29:23 -04:00
` UPDATE assets SET status = 'archived', updated_at = NOW() WHERE id = $ 1 RETURNING * ` , [ id ]
2026-04-07 21:58:27 -04:00
) ;
2026-05-20 15:49:40 -04:00
if ( result . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
2026-04-07 21:58:27 -04:00
res . json ( result . rows [ 0 ] ) ;
}
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
2026-04-07 21:58:27 -04:00
} ) ;
2026-05-20 15:49:40 -04:00
// GET /:id/stream
2026-04-07 21:58:27 -04:00
router . get ( '/:id/stream' , async ( req , res , next ) => {
try {
const { id } = req . params ;
2026-05-18 07:29:50 -04:00
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 a = r . rows [ 0 ] ;
2026-05-20 15:49:40 -04:00
if ( a . status === 'live' ) return res . json ( { url : ` /live/ ${ a . id } /index.m3u8 ` , type : 'hls' , live : true } ) ;
2026-05-26 10:10:44 -04:00
const VIDEO _EXTS = [ '.mp4' , '.mov' , '.mxf' , '.ts' , '.m4v' , '.mkv' , '.avi' , '.webm' ] ;
2026-05-26 12:57:37 -04:00
// Return signed S3 URL directly — browser talks to S3, no Node proxy bottleneck.
// Sign for 4 hours so the player doesn't expire mid-session.
const key = a . proxy _s3 _key ||
( a . original _s3 _key && VIDEO _EXTS . some ( ext => a . original _s3 _key . toLowerCase ( ) . endsWith ( ext ) )
? a . original _s3 _key : null ) ;
if ( key ) {
const signedUrl = await getSignedUrlForObject ( key , 14400 ) ;
return res . json ( { url : signedUrl , type : 'mp4' , source : a . proxy _s3 _key ? 'proxy' : 'original' } ) ;
2026-05-26 10:10:44 -04:00
}
2026-05-25 19:29:23 -04:00
return res . json ( { url : null , type : null , reason : 'no_proxy' , has _source : ! ! a . original _s3 _key } ) ;
} catch ( err ) { next ( err ) ; }
2026-04-07 21:58:27 -04:00
} ) ;
2026-05-19 22:47:33 -04:00
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
// GET /:id/live-path
router . get ( '/:id/live-path' , async ( req , res , next ) => {
try {
const { id } = req . params ;
2026-05-25 19:29:23 -04:00
const a = await pool . query ( 'SELECT id, project_id, display_name, status FROM assets WHERE id = $1' , [ id ] ) ;
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
if ( a . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
const asset = a . rows [ 0 ] ;
2026-05-25 19:29:23 -04:00
if ( asset . status !== 'live' ) return res . status ( 409 ) . json ( { error : 'Asset is not currently growing' , status : asset . status } ) ;
const s = await pool . query ( ` SELECT key, value FROM settings WHERE key IN ('growing_enabled','growing_smb_url') ` ) ;
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
const cfg = { } ;
for ( const { key , value } of s . rows ) cfg [ key ] = value ;
2026-05-25 19:29:23 -04:00
if ( cfg . growing _enabled !== 'true' ) return res . status ( 409 ) . json ( { error : 'Growing-files mode is disabled' } ) ;
if ( ! cfg . growing _smb _url ) return res . status ( 409 ) . json ( { error : 'No SMB URL configured — set growing_smb_url in Settings' } ) ;
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
const rec = await pool . query (
2026-05-25 19:29:23 -04:00
` SELECT recording_container FROM recorders WHERE current_session_id = $ 1 ORDER BY updated_at DESC LIMIT 1 ` ,
[ asset . id ]
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
) ;
const ext = rec . rows [ 0 ] ? . recording _container || 'mov' ;
const smbRoot = cfg . growing _smb _url . replace ( /\/+$/ , '' ) ;
2026-05-25 19:29:23 -04:00
const winPath = smbRoot . replace ( /^smb:\/\// , '\\\\' ) . replace ( /\//g , '\\' ) + ` \\ ${ asset . project _id } \\ ${ asset . display _name } . ${ ext } ` ;
const posix = smbRoot . replace ( /^smb:\/\// , '//' ) + ` / ${ asset . project _id } / ${ asset . display _name } . ${ ext } ` ;
res . json ( { smb _url : ` ${ smbRoot } / ${ asset . project _id } / ${ asset . display _name } . ${ ext } ` , win _path : winPath , posix _path : posix , project _id : asset . project _id , display _name : asset . display _name , ext } ) ;
} catch ( err ) { next ( err ) ; }
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
} ) ;
2026-05-20 15:49:40 -04:00
// GET /:id/video
2026-05-26 12:57:37 -04:00
// Redirects to a signed S3 URL so the browser streams directly from S3.
// This eliminates the Node.js proxy bottleneck and lets S3 handle range
// requests natively — critical for smooth seeking in the player.
2026-05-19 22:47:33 -04:00
router . get ( '/:id/video' , async ( req , res , next ) => {
try {
const { id } = req . params ;
const r = await pool . query ( 'SELECT proxy_s3_key, original_s3_key FROM assets WHERE id = $1' , [ id ] ) ;
if ( r . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
const a = r . rows [ 0 ] ;
2026-05-26 10:10:44 -04:00
const VIDEO _EXTS = [ '.mp4' , '.mov' , '.mxf' , '.ts' , '.m4v' , '.mkv' , '.avi' , '.webm' ] ;
const origIsVideo = a . original _s3 _key && VIDEO _EXTS . some ( ext => a . original _s3 _key . toLowerCase ( ) . endsWith ( ext ) ) ;
const key = a . proxy _s3 _key || ( origIsVideo ? a . original _s3 _key : null ) ;
2026-05-19 22:47:33 -04:00
if ( ! key ) return res . status ( 404 ) . json ( { error : 'No browser-playable source' } ) ;
2026-05-26 12:57:37 -04:00
// Sign for 4 hours — enough for an editing session without frequent re-fetches
const url = await getSignedUrlForObject ( key , 14400 ) ;
res . redirect ( 302 , url ) ;
2026-05-19 22:47:33 -04:00
} catch ( err ) { next ( err ) ; }
} ) ;
2026-05-20 15:49:40 -04:00
// GET /:id/hires
2026-05-20 00:34:18 -04:00
router . get ( '/:id/hires' , async ( req , res , next ) => {
try {
const { id } = req . params ;
2026-05-25 19:29:23 -04:00
const r = await pool . query ( 'SELECT original_s3_key, filename, display_name, file_size FROM assets WHERE id = $1' , [ id ] ) ;
2026-05-20 00:34:18 -04:00
if ( r . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
const a = r . rows [ 0 ] ;
2026-05-20 15:49:40 -04:00
if ( ! a . original _s3 _key ) return res . status ( 404 ) . json ( { error : 'No hi-res source available' } ) ;
2026-05-25 19:29:23 -04:00
const url = await getSignedUrlForObject ( a . original _s3 _key ) ;
2026-05-20 00:34:18 -04:00
const parts = a . original _s3 _key . split ( '.' ) ;
2026-05-25 19:29:23 -04:00
const ext = ( parts . length > 1 ? parts [ parts . length - 1 ] : 'mxf' ) . toLowerCase ( ) ;
const base = ( a . display _name || a . filename || id ) . replace ( /[^\w.-]/g , '_' ) . substring ( 0 , 100 ) ;
2026-05-20 00:34:18 -04:00
res . json ( { url , filename : ` ${ base } . ${ ext } ` , ext , file _size : a . file _size || null , type : 'hires' } ) ;
} catch ( err ) { next ( err ) ; }
} ) ;
2026-05-20 15:49:40 -04:00
// GET /:id/thumbnail
2026-04-07 21:58:27 -04:00
router . get ( '/:id/thumbnail' , async ( req , res , next ) => {
try {
const { id } = req . params ;
2026-05-20 15:49:40 -04:00
const result = await pool . query ( 'SELECT thumbnail_s3_key FROM assets WHERE id = $1' , [ id ] ) ;
if ( result . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Asset not found' } ) ;
2026-04-07 21:58:27 -04:00
const { thumbnail _s3 _key } = result . rows [ 0 ] ;
2026-05-20 15:49:40 -04:00
if ( ! thumbnail _s3 _key ) return res . status ( 404 ) . json ( { error : 'Thumbnail not yet available' } ) ;
2026-04-07 21:58:27 -04:00
const url = await getSignedUrlForObject ( thumbnail _s3 _key ) ;
2026-05-19 23:44:17 -04:00
if ( req . query . redirect === '1' ) return res . redirect ( 302 , url ) ;
2026-04-07 21:58:27 -04:00
res . json ( { url } ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
2026-04-07 21:58:27 -04:00
} ) ;
2026-05-25 19:29:23 -04:00
// POST /batch-trim
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
router . post ( '/batch-trim' , async ( req , res , next ) => {
try {
const { clips } = req . body ;
2026-05-25 19:29:23 -04:00
if ( ! Array . isArray ( clips ) || clips . length === 0 ) return res . status ( 400 ) . json ( { error : 'clips array is required and must be non-empty' } ) ;
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
for ( const c of clips ) {
if ( ! c . assetId || ! c . filename ||
2026-05-25 19:29:23 -04:00
! Number . isFinite ( Number ( c . sourceInFrames ) ) || ! Number . isFinite ( Number ( c . sourceOutFrames ) ) ||
! Number . isFinite ( Number ( c . timelineInFrames ) ) || ! Number . isFinite ( Number ( c . timelineOutFrames ) ) ||
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
! Number . isInteger ( Number ( c . trackIndex ) ) || Number ( c . trackIndex ) < 0 ) {
2026-05-25 19:29:23 -04:00
return res . status ( 400 ) . json ( { error : 'Each clip must have assetId, filename, sourceInFrames, sourceOutFrames, timelineInFrames, timelineOutFrames, and a non-negative integer trackIndex' } ) ;
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
}
}
const jobId = uuidv4 ( ) ;
const expiresAt = new Date ( Date . now ( ) + 24 * 60 * 60 * 1000 ) ;
2026-05-26 10:10:44 -04:00
await pool . query (
` INSERT INTO jobs (id, type, status, payload, expires_at) VALUES ( $ 1, $ 2, $ 3, $ 4, $ 5) ` ,
[ jobId , 'trim' , 'queued' , JSON . stringify ( { clips } ) , expiresAt ]
) ;
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
const clipResults = [ ] ;
for ( const c of clips ) {
const clipInstanceId = uuidv4 ( ) ;
2026-05-25 19:29:23 -04:00
await trimQueue . add ( 'trim-clip' , { jobId , clipInstanceId , assetId : c . assetId , filename : c . filename , sourceInFrames : c . sourceInFrames , sourceOutFrames : c . sourceOutFrames , timelineInFrames : c . timelineInFrames , timelineOutFrames : c . timelineOutFrames , trackIndex : c . trackIndex } ) ;
await pool . query ( ` INSERT INTO temp_segments (job_id, clip_instance_id, asset_id, s3_key, expires_at) VALUES ( $ 1, $ 2, $ 3,'', $ 4) ` , [ jobId , clipInstanceId , c . assetId , expiresAt ] ) ;
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
clipResults . push ( { clipInstanceId , status : 'queued' } ) ;
}
res . status ( 201 ) . json ( { jobId , clips : clipResults } ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
} ) ;
2026-05-25 19:29:23 -04:00
// GET /trim-status/:jobId
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
router . get ( '/trim-status/:jobId' , async ( req , res , next ) => {
try {
const { jobId } = req . params ;
const jobResult = await pool . query ( 'SELECT * FROM jobs WHERE id = $1' , [ jobId ] ) ;
2026-05-25 19:29:23 -04:00
if ( jobResult . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Trim job not found' } ) ;
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
const job = jobResult . rows [ 0 ] ;
2026-05-26 10:10:44 -04:00
// Auto-expire: delete stale jobs rows (and their temp_segments) past TTL
if ( job . expires _at && new Date ( job . expires _at ) < new Date ( ) ) {
await pool . query ( 'DELETE FROM temp_segments WHERE job_id = $1' , [ jobId ] ) ;
await pool . query ( 'DELETE FROM jobs WHERE id = $1' , [ jobId ] ) ;
return res . status ( 404 ) . json ( { error : 'Trim job expired' } ) ;
}
2026-05-25 19:29:23 -04:00
const segResult = await pool . query ( ` SELECT clip_instance_id, asset_id, s3_key, expires_at FROM temp_segments WHERE job_id = $ 1 ORDER BY created_at ` , [ jobId ] ) ;
const clips = segResult . rows . map ( row => ( { clipInstanceId : row . clip _instance _id , assetId : row . asset _id , s3Key : row . s3 _key || null , status : row . s3 _key ? 'completed' : job . status , expiresAt : row . expires _at } ) ) ;
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
res . json ( { jobId , status : job . status , clips } ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
} ) ;
2026-05-25 19:29:23 -04:00
// GET /temp-segment-url/:clipInstanceId
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
router . get ( '/temp-segment-url/:clipInstanceId' , async ( req , res , next ) => {
try {
const { clipInstanceId } = req . params ;
2026-05-25 19:29:23 -04:00
const result = await pool . query ( 'SELECT s3_key FROM temp_segments WHERE clip_instance_id = $1 AND expires_at > NOW()' , [ clipInstanceId ] ) ;
if ( result . rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Temp segment not found or expired' } ) ;
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
const { s3 _key } = result . rows [ 0 ] ;
2026-05-25 19:29:23 -04:00
if ( ! s3 _key ) return res . status ( 404 ) . json ( { error : 'Segment not yet processed' } ) ;
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
const url = await getSignedUrlForObject ( s3 _key ) ;
res . json ( { url , s3Key : s3 _key } ) ;
2026-05-25 19:29:23 -04:00
} catch ( err ) { next ( err ) ; }
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
} ) ;
2026-04-07 21:58:27 -04:00
export default router ;