import express from 'express'; import http from 'http'; import pool from '../db/pool.js'; import { requireAuth } from '../middleware/auth.js'; import { v4 as uuidv4 } from 'uuid'; const router = express.Router(); router.use(requireAuth); // Docker API helper function function dockerApi(method, path, body = null) { return new Promise((resolve, reject) => { const options = { socketPath: '/var/run/docker.sock', path: `/v1.43${path}`, method, headers: { 'Content-Type': 'application/json' }, }; const req = http.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { resolve({ status: res.statusCode, data: data ? JSON.parse(data) : {} }); } catch { resolve({ status: res.statusCode, data }); } }); }); req.on('error', reject); if (body) req.write(JSON.stringify(body)); req.end(); }); } // Helper function to generate clip name with timestamp function generateClipName(recorderName) { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); return `${recorderName}_${year}${month}${day}_${hours}${minutes}${seconds}`; } // GET / - List all recorders router.get('/', async (req, res, next) => { try { const result = await pool.query( 'SELECT * FROM recorders ORDER BY created_at DESC' ); res.json(result.rows); } catch (err) { next(err); } }); // POST / - Create a new recorder router.post('/', async (req, res, next) => { try { const { name, source_type, source_config, recording_codec, recording_resolution, proxy_enabled, proxy_codec, proxy_resolution, project_id, } = req.body; if (!name || !source_type) { return res .status(400) .json({ error: 'Name and source_type are required' }); } const id = uuidv4(); const result = await pool.query( `INSERT INTO recorders ( id, name, source_type, source_config, recording_codec, recording_resolution, proxy_enabled, proxy_codec, proxy_resolution, project_id, status, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), NOW()) RETURNING *`, [ id, name, source_type, source_config || {}, recording_codec || 'prores_hq', recording_resolution || 'native', proxy_enabled !== false, proxy_codec || 'libx264', proxy_resolution || '1920x1080', project_id || null, 'stopped', ] ); res.status(201).json(result.rows[0]); } catch (err) { next(err); } }); // GET /:id - Get single recorder router.get('/:id', async (req, res, next) => { try { const { id } = req.params; const result = await pool.query( 'SELECT * FROM recorders WHERE id = $1', [id] ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Recorder not found' }); } res.json(result.rows[0]); } catch (err) { next(err); } }); // POST /:id/start - Start recording router.post('/:id/start', async (req, res, next) => { try { const { id } = req.params; // Get recorder config from DB const recorderResult = await pool.query( 'SELECT * FROM recorders WHERE id = $1', [id] ); if (recorderResult.rows.length === 0) { return res.status(404).json({ error: 'Recorder not found' }); } const recorder = recorderResult.rows[0]; if (recorder.status === 'recording') { return res.status(400).json({ error: 'Recorder is already recording' }); } // Get S3 config from environment const s3Endpoint = process.env.S3_ENDPOINT; const s3Bucket = process.env.S3_BUCKET; const s3AccessKey = process.env.S3_ACCESS_KEY; const s3SecretKey = process.env.S3_SECRET_KEY; const mamApiUrl = process.env.MAM_API_URL || 'http://mam-api:3000'; const dockerNetwork = process.env.DOCKER_NETWORK || 'wild-dragon_wild-dragon'; // Generate clip name with timestamp const clipName = generateClipName(recorder.name); // Build container config const containerConfig = { Image: 'wild-dragon-capture:latest', Env: [ `S3_ENDPOINT=${s3Endpoint}`, `S3_BUCKET=${s3Bucket}`, `S3_ACCESS_KEY=${s3AccessKey}`, `S3_SECRET_KEY=${s3SecretKey}`, `S3_REGION=${process.env.S3_REGION || 'us-east-1'}`, `MAM_API_URL=${mamApiUrl}`, `RECORDER_ID=${id}`, `SOURCE_TYPE=${recorder.source_type}`, `SOURCE_CONFIG=${JSON.stringify(recorder.source_config)}`, `RECORDING_CODEC=${recorder.recording_codec}`, `RECORDING_RESOLUTION=${recorder.recording_resolution}`, `PROXY_ENABLED=${recorder.proxy_enabled}`, `PROXY_CODEC=${recorder.proxy_codec}`, `PROXY_RESOLUTION=${recorder.proxy_resolution}`, `PROJECT_ID=${recorder.project_id}`, `CLIP_NAME=${clipName}`, ], HostConfig: { Privileged: true, NetworkMode: dockerNetwork, }, Hostname: `recorder-${recorder.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`, }; // Create container const createRes = await dockerApi('POST', '/containers/create', containerConfig); if (createRes.status !== 201) { return res.status(500).json({ error: 'Failed to create container', details: createRes.data, }); } const containerId = createRes.data.Id; // Start container const startRes = await dockerApi('POST', `/containers/${containerId}/start`); if (startRes.status !== 204) { return res.status(500).json({ error: 'Failed to start container', details: startRes.data, }); } // Update recorder in DB const updateResult = await pool.query( `UPDATE recorders SET container_id = $1, status = $2, current_session_id = $3, updated_at = NOW() WHERE id = $4 RETURNING *`, [containerId, 'recording', clipName, id] ); res.json(updateResult.rows[0]); } catch (err) { next(err); } }); // POST /:id/stop - Stop recording router.post('/:id/stop', async (req, res, next) => { try { const { id } = req.params; // Get recorder from DB const recorderResult = await pool.query( 'SELECT * FROM recorders WHERE id = $1', [id] ); if (recorderResult.rows.length === 0) { return res.status(404).json({ error: 'Recorder not found' }); } const recorder = recorderResult.rows[0]; if (!recorder.container_id) { return res.status(400).json({ error: 'No container running' }); } // Stop container const stopRes = await dockerApi( 'POST', `/containers/${recorder.container_id}/stop` ); // 204 = stopped, 304 = already stopped — both are acceptable if (stopRes.status !== 204 && stopRes.status !== 304) { return res.status(500).json({ error: 'Failed to stop container', details: stopRes.data, }); } // Remove container — 204 = removed, 404 = already gone (both acceptable) const removeRes = await dockerApi( 'DELETE', `/containers/${recorder.container_id}` ); if (removeRes.status !== 204 && removeRes.status !== 404) { return res.status(500).json({ error: 'Failed to remove container', details: removeRes.data, }); } // Update recorder in DB const updateResult = await pool.query( `UPDATE recorders SET container_id = NULL, status = $1, updated_at = NOW() WHERE id = $2 RETURNING *`, ['stopped', id] ); res.json(updateResult.rows[0]); } catch (err) { next(err); } }); // GET /:id/status - Get live status router.get('/:id/status', async (req, res, next) => { try { const { id } = req.params; // Get recorder from DB const recorderResult = await pool.query( 'SELECT * FROM recorders WHERE id = $1', [id] ); if (recorderResult.rows.length === 0) { return res.status(404).json({ error: 'Recorder not found' }); } const recorder = recorderResult.rows[0]; if (!recorder.container_id) { return res.json({ status: recorder.status, duration: 0, containerId: null, }); } // Query Docker API for container status const inspectRes = await dockerApi( 'GET', `/containers/${recorder.container_id}/json` ); if (inspectRes.status !== 200) { return res.json({ status: 'unknown', duration: 0, containerId: recorder.container_id, }); } const container = inspectRes.data; const startedAt = new Date(container.State.StartedAt).getTime(); const now = Date.now(); const duration = Math.floor((now - startedAt) / 1000); res.json({ status: container.State.Running ? 'recording' : 'stopped', duration, containerId: recorder.container_id, }); } catch (err) { next(err); } }); // DELETE /:id - Delete recorder router.delete('/:id', async (req, res, next) => { try { const { id } = req.params; // Get recorder from DB const recorderResult = await pool.query( 'SELECT * FROM recorders WHERE id = $1', [id] ); if (recorderResult.rows.length === 0) { return res.status(404).json({ error: 'Recorder not found' }); } const recorder = recorderResult.rows[0]; // If recording, stop the container first if (recorder.container_id) { try { await dockerApi('POST', `/containers/${recorder.container_id}/stop`); await dockerApi('DELETE', `/containers/${recorder.container_id}`); } catch (err) { console.error('Error stopping container during delete:', err); } } // Delete from DB const deleteResult = await pool.query( 'DELETE FROM recorders WHERE id = $1 RETURNING *', [id] ); res.json({ message: 'Recorder deleted', recorder: deleteResult.rows[0] }); } catch (err) { next(err); } }); export default router;