import { Router, Request, Response } from 'express'; import { getAnnotations, createAnnotation, getAnnotation, updateAnnotation, deleteAnnotation, } from '../services/storage'; import type { CreateAnnotationInput, UpdateAnnotationInput } from '@abcvideo/shared'; const router = Router({ mergeParams: true }); // GET /api/annotations/:videoId router.get('/', (req: Request, res: Response) => { const annotations = getAnnotations(req.params.videoId); res.json(annotations); }); // POST /api/annotations/:videoId router.post('/', (req: Request, res: Response) => { const input: CreateAnnotationInput = { ...req.body, videoId: req.params.videoId }; if (!input.type || input.timeStart === undefined || input.timeEnd === undefined) { res.status(400).json({ error: 'Missing required fields: type, timeStart, timeEnd' }); return; } const annotation = createAnnotation(input); res.status(201).json(annotation); }); // GET /api/annotations/:videoId/export?format=vtt|srt|json|csv // NOTE: this must be registered before /:id to avoid "export" matching as an id router.get('/export', (req: Request, res: Response) => { const { format = 'json' } = req.query as { format?: string }; const annotations = getAnnotations(req.params.videoId); const toTimecode = (s: number, sep = '.') => { const h = Math.floor(s / 3600).toString().padStart(2, '0'); const m = Math.floor((s % 3600) / 60).toString().padStart(2, '0'); const sec = Math.floor(s % 60).toString().padStart(2, '0'); const ms = Math.round((s % 1) * 1000).toString().padStart(3, '0'); return `${h}:${m}:${sec}${sep}${ms}`; }; if (format === 'vtt') { const lines = ['WEBVTT', '']; annotations .filter(a => a.type === 'subtitle') .forEach((a, i) => { lines.push(`${i + 1}`); lines.push(`${toTimecode(a.timeStart)} --> ${toTimecode(a.timeEnd)}`); lines.push(a.text, ''); }); res.setHeader('Content-Type', 'text/vtt'); res.setHeader('Content-Disposition', `attachment; filename="annotations.vtt"`); res.send(lines.join('\n')); } else if (format === 'srt') { const lines: string[] = []; annotations .filter(a => a.type === 'subtitle') .forEach((a, i) => { lines.push(`${i + 1}`); lines.push(`${toTimecode(a.timeStart, ',')} --> ${toTimecode(a.timeEnd, ',')}`); lines.push(a.text, ''); }); res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Disposition', `attachment; filename="annotations.srt"`); res.send(lines.join('\n')); } else if (format === 'csv') { const header = 'id,type,timeStart,timeEnd,text,posX,posY\n'; const rows = annotations.map(a => `${a.id},${a.type},${a.timeStart},${a.timeEnd},"${a.text.replace(/"/g, '""')}",${a.position.x},${a.position.y}` ).join('\n'); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename="annotations.csv"`); res.send(header + rows); } else { res.setHeader('Content-Disposition', `attachment; filename="annotations.json"`); res.json(annotations); } }); // PUT /api/annotations/:videoId/:id router.put('/:id', (req: Request, res: Response) => { const input: UpdateAnnotationInput = req.body; const updated = updateAnnotation(req.params.id, input); if (!updated) { res.status(404).json({ error: 'Annotation not found' }); return; } res.json(updated); }); // DELETE /api/annotations/:videoId/:id router.delete('/:id', (req: Request, res: Response) => { const success = deleteAnnotation(req.params.id); if (!success) { res.status(404).json({ error: 'Annotation not found' }); return; } res.json({ success: true }); }); export default router;