import { Router } from 'express'; import { getDb } from '../db'; import { randomUUID } from 'crypto'; import fs from 'fs/promises'; import fss from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..'); export const assetsRouter = Router(); // List assets with optional filters assetsRouter.get('/', (req, res) => { const { accountId, type } = req.query; let sql = 'SELECT * FROM assets WHERE 1=1'; const params: unknown[] = []; if (accountId) { sql += ' AND account_id = ?'; params.push(accountId); } if (type) { sql += ' AND type = ?'; params.push(type); } sql += ' ORDER BY created_at DESC LIMIT 200'; const rows = getDb().prepare(sql).all(...params); res.json(rows); }); // List manifests with metadata assetsRouter.get('/manifests', async (_req, res) => { const rows = getDb().prepare(` SELECT manifest_path, account_id, COUNT(*) as asset_count, SUM(CASE WHEN type = 'image' THEN 1 ELSE 0 END) as image_count, SUM(CASE WHEN type = 'video' THEN 1 ELSE 0 END) as video_count, MAX(created_at) as latest_at FROM assets GROUP BY manifest_path ORDER BY latest_at DESC `).all(); res.json(rows); }); // Delete asset and its file assetsRouter.delete('/:id', async (req, res) => { const asset = getDb().prepare('SELECT * FROM assets WHERE id = ?').get(req.params.id) as { file_path: string; } | undefined; if (!asset) return res.status(404).json({ error: 'Asset not found' }); const filePath = path.join(PROJECT_ROOT, asset.file_path); try { await fs.unlink(filePath); } catch { /* ignore */ } getDb().prepare('DELETE FROM assets WHERE id = ?').run(req.params.id); res.status(204).send(); }); // Scan output directories and index assets assetsRouter.post('/scan', async (_req, res) => { const outputDir = path.join(PROJECT_ROOT, 'output'); try { await fs.access(outputDir); } catch { return res.json({ indexed: 0 }); } const dirs = await fs.readdir(outputDir, { withFileTypes: true }); const dirEntries = dirs.filter((d) => d.isDirectory()); let indexed = 0; for (const dir of dirEntries) { const manifestPath = path.join(outputDir, dir.name, 'manifest.json'); let manifest; try { manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8')); } catch { continue; } const accountId = manifest.account?.id || ''; // Scan images const imagesDir = path.join(outputDir, dir.name, 'images'); try { const files = await fs.readdir(imagesDir); for (const file of files) { if (!/\.(jpe?g|png|webp)$/i.test(file)) continue; const id = randomUUID(); const relPath = path.relative(PROJECT_ROOT, path.join(imagesDir, file)); const match = file.match(/scene_(\d+)/); const shotIndex = match ? parseInt(match[1]) : null; const exists = getDb().prepare('SELECT id FROM assets WHERE file_path = ?').get(relPath); if (exists) continue; getDb().prepare( 'INSERT INTO assets (id, account_id, manifest_path, type, file_path, shot_index) VALUES (?,?,?,?,?,?)' ).run(id, accountId, path.relative(PROJECT_ROOT, manifestPath), 'image', relPath, shotIndex); indexed++; } } catch { /* images dir may not exist */ } // Scan videos const videosDir = path.join(outputDir, dir.name, 'videos'); try { const files = await fs.readdir(videosDir); for (const file of files) { if (!/\.mp4$/i.test(file)) continue; const id = randomUUID(); const relPath = path.relative(PROJECT_ROOT, path.join(videosDir, file)); const match = file.match(/scene_(\d+)/); const shotIndex = match ? parseInt(match[1]) : null; const exists = getDb().prepare('SELECT id FROM assets WHERE file_path = ?').get(relPath); if (exists) continue; getDb().prepare( 'INSERT INTO assets (id, account_id, manifest_path, type, file_path, shot_index) VALUES (?,?,?,?,?,?)' ).run(id, accountId, path.relative(PROJECT_ROOT, manifestPath), 'video', relPath, shotIndex); indexed++; } } catch { /* videos dir may not exist */ } } res.json({ indexed }); }); // Serve asset files assetsRouter.get('/file', (req, res) => { const filePath = req.query.path as string; if (!filePath) return res.status(400).send('Missing path'); const fullPath = path.resolve(PROJECT_ROOT, filePath); if (!fullPath.startsWith(PROJECT_ROOT)) return res.status(403).send('Forbidden'); if (!fss.existsSync(fullPath)) return res.status(404).send('Not found'); res.sendFile(fullPath); });