GhiVideo 소스 복제 — v4 작업 시작 기준
기존 GhiVideo 저장소 HEAD의 트래킹 소스 362개 파일을 복제. (node_modules·storage·빌드 산출물·대용량 미디어는 .gitignore 규칙대로 제외) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user