fix(assets): replace static S3_BUCKET with getS3Bucket() for dynamic config
This commit is contained in:
parent
737e69d72f
commit
02cfa68b92
1 changed files with 48 additions and 175 deletions
|
|
@ -2,7 +2,7 @@ import express from 'express';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import pool from '../db/pool.js';
|
import pool from '../db/pool.js';
|
||||||
import { getSignedUrlForObject, deleteObject, s3Client, S3_BUCKET } from '../s3/client.js';
|
import { getSignedUrlForObject, deleteObject, s3Client, getS3Bucket } from '../s3/client.js';
|
||||||
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
||||||
import { requireAuth } from '../middleware/auth.js';
|
import { requireAuth } from '../middleware/auth.js';
|
||||||
|
|
||||||
|
|
@ -47,8 +47,6 @@ router.get('/', async (req, res, next) => {
|
||||||
const params = [];
|
const params = [];
|
||||||
let paramCount = 1;
|
let paramCount = 1;
|
||||||
|
|
||||||
// Hide archived rows unless explicitly asked for, or unless the caller is
|
|
||||||
// already filtering by status=archived.
|
|
||||||
if (!status && include_archived !== 'true') {
|
if (!status && include_archived !== 'true') {
|
||||||
query += ` AND a.status <> 'archived'`;
|
query += ` AND a.status <> 'archived'`;
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +72,6 @@ router.get('/', async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
// Search display_name, filename, and notes — filename was previously omitted
|
|
||||||
query += ` AND (a.display_name ILIKE $${paramCount} OR a.filename ILIKE $${paramCount} OR a.notes ILIKE $${paramCount})`;
|
query += ` AND (a.display_name ILIKE $${paramCount} OR a.filename ILIKE $${paramCount} OR a.notes ILIKE $${paramCount})`;
|
||||||
params.push(`%${search}%`);
|
params.push(`%${search}%`);
|
||||||
paramCount++;
|
paramCount++;
|
||||||
|
|
@ -97,11 +94,6 @@ router.get('/', async (req, res, next) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST / - Register a new asset from a completed capture session
|
// 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) => {
|
router.post('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
|
|
@ -110,8 +102,8 @@ router.post('/', async (req, res, next) => {
|
||||||
clipName,
|
clipName,
|
||||||
hiresKey,
|
hiresKey,
|
||||||
proxyKey,
|
proxyKey,
|
||||||
duration, // seconds (integer)
|
duration,
|
||||||
capturedAt, // ISO 8601 string
|
capturedAt,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!projectId || !clipName) {
|
if (!projectId || !clipName) {
|
||||||
|
|
@ -120,10 +112,6 @@ router.post('/', async (req, res, next) => {
|
||||||
|
|
||||||
const durationMs = duration ? Math.round(duration * 1000) : null;
|
const durationMs = duration ? Math.round(duration * 1000) : null;
|
||||||
|
|
||||||
// Phase 1 growing-files: an asset row may already exist in status='live'
|
|
||||||
// (pre-created at recorder start so the library shows the recording while
|
|
||||||
// it is happening). If so we UPDATE that row instead of inserting a new
|
|
||||||
// one -- otherwise we would have two rows per recording.
|
|
||||||
const existing = await pool.query(
|
const existing = await pool.query(
|
||||||
`SELECT * FROM assets
|
`SELECT * FROM assets
|
||||||
WHERE project_id = $1 AND display_name = $2 AND status = 'live'
|
WHERE project_id = $1 AND display_name = $2 AND status = 'live'
|
||||||
|
|
@ -181,8 +169,6 @@ router.post('/', async (req, res, next) => {
|
||||||
|
|
||||||
const thumbnailKey = `thumbnails/${id}.jpg`;
|
const thumbnailKey = `thumbnails/${id}.jpg`;
|
||||||
|
|
||||||
|
|
||||||
// Dispatch thumbnail job — proxy already in S3 from capture
|
|
||||||
if (proxyKey) {
|
if (proxyKey) {
|
||||||
await thumbnailQueue.add('generate', {
|
await thumbnailQueue.add('generate', {
|
||||||
assetId: id,
|
assetId: id,
|
||||||
|
|
@ -190,7 +176,6 @@ router.post('/', async (req, res, next) => {
|
||||||
outputKey: thumbnailKey,
|
outputKey: thumbnailKey,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// No proxy yet — mark ready immediately (e.g. audio-only or test mode)
|
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE assets SET status = 'ready', updated_at = NOW() WHERE id = $1`,
|
`UPDATE assets SET status = 'ready', updated_at = NOW() WHERE id = $1`,
|
||||||
[id]
|
[id]
|
||||||
|
|
@ -204,11 +189,7 @@ router.post('/', async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /cleanup-live – mark stuck 'live' assets as 'error'
|
// POST /cleanup-live
|
||||||
//
|
|
||||||
// Recorder containers that crash without calling the stop callback leave
|
|
||||||
// assets permanently in 'live' status. This endpoint recovers them.
|
|
||||||
// Default age threshold: 4 hours. Accepts ?max_age_hours=N override.
|
|
||||||
router.post('/cleanup-live', async (req, res, next) => {
|
router.post('/cleanup-live', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const maxAgeHours = Math.max(1, parseInt(req.query.max_age_hours || '4', 10));
|
const maxAgeHours = Math.max(1, parseInt(req.query.max_age_hours || '4', 10));
|
||||||
|
|
@ -226,23 +207,19 @@ router.post('/cleanup-live', async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /:id - Single asset
|
// GET /:id
|
||||||
router.get('/:id', async (req, res, next) => {
|
router.get('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const result = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
|
const result = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
|
||||||
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Asset not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PATCH /:id - Update asset metadata
|
// PATCH /:id
|
||||||
router.patch('/:id', async (req, res, next) => {
|
router.patch('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
@ -252,66 +229,35 @@ router.patch('/:id', async (req, res, next) => {
|
||||||
const params = [];
|
const params = [];
|
||||||
let paramCount = 1;
|
let paramCount = 1;
|
||||||
|
|
||||||
if (display_name !== undefined) {
|
if (display_name !== undefined) { updates.push(`display_name = $${paramCount++}`); params.push(display_name); }
|
||||||
updates.push(`display_name = $${paramCount++}`);
|
if (tags !== undefined) { updates.push(`tags = $${paramCount++}`); params.push(tags); }
|
||||||
params.push(display_name);
|
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 (tags !== undefined) {
|
if (updates.length === 0) return res.status(400).json({ error: 'No fields to update' });
|
||||||
updates.push(`tags = $${paramCount++}`);
|
|
||||||
params.push(tags);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notes !== undefined) {
|
|
||||||
updates.push(`notes = $${paramCount++}`);
|
|
||||||
params.push(notes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bin_id !== undefined) {
|
|
||||||
// Accept null to move the asset back to the project root
|
|
||||||
updates.push(`bin_id = $${paramCount++}`);
|
|
||||||
params.push(bin_id || null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.length === 0) {
|
|
||||||
return res.status(400).json({ error: 'No fields to update' });
|
|
||||||
}
|
|
||||||
|
|
||||||
updates.push(`updated_at = NOW()`);
|
updates.push(`updated_at = NOW()`);
|
||||||
params.push(id);
|
params.push(id);
|
||||||
|
|
||||||
const query = `
|
const result = await pool.query(
|
||||||
UPDATE assets
|
`UPDATE assets SET ${updates.join(', ')} WHERE id = $${paramCount} RETURNING *`,
|
||||||
SET ${updates.join(', ')}
|
params
|
||||||
WHERE id = $${paramCount}
|
);
|
||||||
RETURNING *
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await pool.query(query, params);
|
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Asset not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /:id/copy - Reference-copy an asset into another bin (or project)
|
// POST /:id/copy
|
||||||
//
|
|
||||||
// Same S3 keys, new asset row. Mirrors filename + metadata. Useful for
|
|
||||||
// multi-binning a single piece of media without duplicating storage.
|
|
||||||
router.post('/:id/copy', async (req, res, next) => {
|
router.post('/:id/copy', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { binId, projectId } = req.body;
|
const { binId, projectId } = req.body;
|
||||||
|
|
||||||
const r = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
|
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' });
|
if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
const src = r.rows[0];
|
const src = r.rows[0];
|
||||||
|
|
||||||
const newId = uuidv4();
|
const newId = uuidv4();
|
||||||
const ins = await pool.query(
|
const ins = await pool.query(
|
||||||
`INSERT INTO assets (
|
`INSERT INTO assets (
|
||||||
|
|
@ -329,21 +275,11 @@ router.post('/:id/copy', async (req, res, next) => {
|
||||||
newId,
|
newId,
|
||||||
projectId || src.project_id,
|
projectId || src.project_id,
|
||||||
binId === undefined ? src.bin_id : (binId || null),
|
binId === undefined ? src.bin_id : (binId || null),
|
||||||
src.filename,
|
src.filename, src.display_name,
|
||||||
src.display_name,
|
src.status, src.media_type,
|
||||||
src.status,
|
src.original_s3_key, src.proxy_s3_key, src.thumbnail_s3_key,
|
||||||
src.media_type,
|
src.codec, src.resolution, src.fps, src.duration_ms, src.start_tc,
|
||||||
src.original_s3_key,
|
src.file_size, src.tags, src.notes,
|
||||||
src.proxy_s3_key,
|
|
||||||
src.thumbnail_s3_key,
|
|
||||||
src.codec,
|
|
||||||
src.resolution,
|
|
||||||
src.fps,
|
|
||||||
src.duration_ms,
|
|
||||||
src.start_tc,
|
|
||||||
src.file_size,
|
|
||||||
src.tags,
|
|
||||||
src.notes,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
res.status(201).json(ins.rows[0]);
|
res.status(201).json(ins.rows[0]);
|
||||||
|
|
@ -352,81 +288,47 @@ router.post('/:id/copy', async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /:id/retry - Re-queue proxy generation for an asset stuck in error state
|
// POST /:id/retry
|
||||||
//
|
|
||||||
// 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) => {
|
router.post('/:id/retry', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const r = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
|
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' });
|
if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
const asset = r.rows[0];
|
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.status !== 'error') {
|
if (!asset.original_s3_key) return res.status(400).json({ error: 'Asset has no source file to reprocess' });
|
||||||
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`;
|
const proxyKey = asset.proxy_s3_key || `proxies/${id}.mp4`;
|
||||||
|
await proxyQueue.add('generate', { assetId: id, inputKey: asset.original_s3_key, outputKey: proxyKey });
|
||||||
await proxyQueue.add('generate', {
|
|
||||||
assetId: id,
|
|
||||||
inputKey: asset.original_s3_key,
|
|
||||||
outputKey: proxyKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updated = await pool.query(
|
const updated = await pool.query(
|
||||||
`UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = $1 RETURNING *`,
|
`UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = $1 RETURNING *`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json(updated.rows[0]);
|
res.json(updated.rows[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /:id - Soft or hard delete
|
// DELETE /:id
|
||||||
router.delete('/:id', async (req, res, next) => {
|
router.delete('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { hard } = req.query;
|
const { hard } = req.query;
|
||||||
|
|
||||||
if (hard === 'true') {
|
if (hard === 'true') {
|
||||||
const assetResult = await pool.query(
|
const assetResult = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
|
||||||
'SELECT * FROM assets WHERE id = $1',
|
if (assetResult.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
[id]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (assetResult.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Asset not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset = assetResult.rows[0];
|
const asset = assetResult.rows[0];
|
||||||
|
if (asset.proxy_s3_key) await deleteObject(asset.proxy_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.thumbnail_s3_key) await deleteObject(asset.thumbnail_s3_key);
|
||||||
if (asset.original_s3_key) await deleteObject(asset.original_s3_key);
|
if (asset.original_s3_key) await deleteObject(asset.original_s3_key);
|
||||||
|
|
||||||
await pool.query('DELETE FROM assets WHERE id = $1', [id]);
|
await pool.query('DELETE FROM assets WHERE id = $1', [id]);
|
||||||
res.json({ message: 'Asset deleted permanently' });
|
res.json({ message: 'Asset deleted permanently' });
|
||||||
} else {
|
} else {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`UPDATE assets SET status = 'archived', updated_at = NOW()
|
`UPDATE assets SET status = 'archived', updated_at = NOW() WHERE id = $1 RETURNING *`,
|
||||||
WHERE id = $1 RETURNING *`,
|
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Asset not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -434,30 +336,24 @@ router.delete('/:id', async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /:id/stream - URL for proxy playback (relative path, for browser use)
|
// GET /:id/stream
|
||||||
router.get('/:id/stream', async (req, res, next) => {
|
router.get('/:id/stream', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const r = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
|
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' });
|
if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
const a = r.rows[0];
|
const a = r.rows[0];
|
||||||
if (a.status === 'live') {
|
if (a.status === 'live') return res.json({ url: `/live/${a.id}/index.m3u8`, type: 'hls', live: true });
|
||||||
return res.json({ url: `/live/${a.id}/index.m3u8`, type: 'hls', live: true });
|
if (a.proxy_s3_key) return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
|
||||||
}
|
|
||||||
if (a.proxy_s3_key) {
|
|
||||||
return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
|
|
||||||
}
|
|
||||||
const orig = a.original_s3_key;
|
const orig = a.original_s3_key;
|
||||||
if (orig && orig.toLowerCase().endsWith('.mp4')) {
|
if (orig && orig.toLowerCase().endsWith('.mp4')) return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
|
||||||
return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
|
|
||||||
}
|
|
||||||
return res.json({ url: null, type: null, reason: 'no_proxy' });
|
return res.json({ url: null, type: null, reason: 'no_proxy' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /:id/video - Stream proxy for browser video playback (bypasses S3 direct)
|
// GET /:id/video
|
||||||
router.get('/:id/video', async (req, res, next) => {
|
router.get('/:id/video', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
@ -466,28 +362,20 @@ router.get('/:id/video', async (req, res, next) => {
|
||||||
const a = r.rows[0];
|
const a = r.rows[0];
|
||||||
const key = a.proxy_s3_key || (a.original_s3_key?.toLowerCase().endsWith('.mp4') ? a.original_s3_key : null);
|
const key = a.proxy_s3_key || (a.original_s3_key?.toLowerCase().endsWith('.mp4') ? a.original_s3_key : null);
|
||||||
if (!key) return res.status(404).json({ error: 'No browser-playable source' });
|
if (!key) return res.status(404).json({ error: 'No browser-playable source' });
|
||||||
const params = { Bucket: S3_BUCKET, Key: key };
|
const params = { Bucket: getS3Bucket(), Key: key };
|
||||||
const rangeHeader = req.headers.range;
|
const rangeHeader = req.headers.range;
|
||||||
if (rangeHeader) params.Range = rangeHeader;
|
if (rangeHeader) params.Range = rangeHeader;
|
||||||
const s3Res = await s3Client.send(new GetObjectCommand(params));
|
const s3Res = await s3Client.send(new GetObjectCommand(params));
|
||||||
const status = rangeHeader ? 206 : 200;
|
const status = rangeHeader ? 206 : 200;
|
||||||
const headers = {
|
const headers = { 'Content-Type': 'video/mp4', 'Accept-Ranges': 'bytes', 'Cache-Control': 'no-store' };
|
||||||
'Content-Type': 'video/mp4',
|
|
||||||
'Accept-Ranges': 'bytes',
|
|
||||||
'Cache-Control': 'no-store'
|
|
||||||
};
|
|
||||||
if (s3Res.ContentLength) headers['Content-Length'] = String(s3Res.ContentLength);
|
if (s3Res.ContentLength) headers['Content-Length'] = String(s3Res.ContentLength);
|
||||||
if (s3Res.ContentRange) headers['Content-Range'] = s3Res.ContentRange;
|
if (s3Res.ContentRange) headers['Content-Range'] = s3Res.ContentRange;
|
||||||
res.writeHead(status, headers);
|
res.writeHead(status, headers);
|
||||||
s3Res.Body.pipe(res);
|
s3Res.Body.pipe(res);
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /:id/hires - Presigned S3 URL for the original hi-res source file
|
// GET /:id/hires
|
||||||
//
|
|
||||||
// Returns a short-lived presigned URL that the Premiere plugin can use to
|
|
||||||
// download the full-resolution original directly from S3. Also includes
|
|
||||||
// the derived filename and file_size so the client can show a size warning.
|
|
||||||
router.get('/:id/hires', async (req, res, next) => {
|
router.get('/:id/hires', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
@ -497,9 +385,7 @@ router.get('/:id/hires', async (req, res, next) => {
|
||||||
);
|
);
|
||||||
if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
const a = r.rows[0];
|
const a = r.rows[0];
|
||||||
if (!a.original_s3_key) {
|
if (!a.original_s3_key) return res.status(404).json({ error: 'No hi-res source available' });
|
||||||
return res.status(404).json({ error: 'No hi-res source available for this asset' });
|
|
||||||
}
|
|
||||||
const url = await getSignedUrlForObject(a.original_s3_key);
|
const url = await getSignedUrlForObject(a.original_s3_key);
|
||||||
const parts = a.original_s3_key.split('.');
|
const parts = a.original_s3_key.split('.');
|
||||||
const ext = (parts.length > 1 ? parts[parts.length - 1] : 'mxf').toLowerCase();
|
const ext = (parts.length > 1 ? parts[parts.length - 1] : 'mxf').toLowerCase();
|
||||||
|
|
@ -508,28 +394,15 @@ router.get('/:id/hires', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /:id/thumbnail - Signed URL (JSON) or 302 redirect (?redirect=1) for thumbnail
|
// GET /:id/thumbnail
|
||||||
router.get('/:id/thumbnail', async (req, res, next) => {
|
router.get('/:id/thumbnail', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const result = await pool.query(
|
const result = await pool.query('SELECT thumbnail_s3_key FROM assets WHERE id = $1', [id]);
|
||||||
'SELECT thumbnail_s3_key FROM assets WHERE id = $1',
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
[id]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Asset not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { thumbnail_s3_key } = result.rows[0];
|
const { thumbnail_s3_key } = result.rows[0];
|
||||||
|
if (!thumbnail_s3_key) return res.status(404).json({ error: 'Thumbnail not yet available' });
|
||||||
if (!thumbnail_s3_key) {
|
|
||||||
return res.status(404).json({ error: 'Thumbnail not yet available' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await getSignedUrlForObject(thumbnail_s3_key);
|
const url = await getSignedUrlForObject(thumbnail_s3_key);
|
||||||
|
|
||||||
// ?redirect=1 lets <img src="...thumbnail?redirect=1"> work directly in the browser
|
|
||||||
if (req.query.redirect === '1') return res.redirect(302, url);
|
if (req.query.redirect === '1') return res.redirect(302, url);
|
||||||
res.json({ url });
|
res.json({ url });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue