기존 GhiVideo 저장소 HEAD의 트래킹 소스 362개 파일을 복제. (node_modules·storage·빌드 산출물·대용량 미디어는 .gitignore 규칙대로 제외) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
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;
|