apps/api/src/media-control/edl.ts: the EdlProject schema (closed enums, integer ms, 3dB steps, <=50 items, <=15 min, filtergraph-metacharacter blocklist on card text) + validateEdlProject — the mirror-assert owner for the worker's edl-plan port. 043_edl_projects: RLS tenant table. EdlController: POST /video/edl, PATCH autosave, GET (project + latest assemble job), POST :id/render (queues kind 'assemble' with the validated project). Tests 130 passing across media-control.
70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
import { BadRequestException, Body, Controller, Get, Inject, Param, Patch, Post, Req, UseGuards } from "@nestjs/common";
|
|
import type { Request } from "express";
|
|
import { parseId, requireObject, requireSession } from "../http/validation.js";
|
|
import { RbacGuard, RequirePermission, type AuthenticatedRequest } from "../rbac/rbac.guard.js";
|
|
import { validateEdlProject } from "./edl.js";
|
|
import { EdlService, type EdlLane, type EdlProjectRecord, type EdlProjectView } from "./edl.service.js";
|
|
|
|
/**
|
|
* EDL projects (editor-v2 intake): the timeline is the resource, the render is
|
|
* a queued assemble job. Bodies are parsed with validateEdlProject — closed
|
|
* enums, integer ms, unknown keys rejected — before anything touches the DB.
|
|
*/
|
|
@Controller("video/edl")
|
|
@UseGuards(RbacGuard)
|
|
export class EdlController {
|
|
constructor(@Inject(EdlService) private readonly edl: EdlService) {}
|
|
|
|
@Post()
|
|
@RequirePermission("media.manage")
|
|
create(
|
|
@Req() request: Request & AuthenticatedRequest,
|
|
@Body() body: unknown,
|
|
): Promise<EdlProjectRecord> {
|
|
const session = requireSession(request);
|
|
const project = validateEdlProject(requireObject(body, "body").project);
|
|
return this.edl.create(session.workspaceId, project, session.userId);
|
|
}
|
|
|
|
@Patch(":id")
|
|
@RequirePermission("media.manage")
|
|
update(
|
|
@Req() request: Request & AuthenticatedRequest,
|
|
@Param("id") id: string,
|
|
@Body() body: unknown,
|
|
): Promise<EdlProjectRecord> {
|
|
const session = requireSession(request);
|
|
const project = validateEdlProject(requireObject(body, "body").project);
|
|
return this.edl.update(session.workspaceId, parseId(id, "id"), project, session.userId);
|
|
}
|
|
|
|
@Get(":id")
|
|
@RequirePermission("media.view")
|
|
get(
|
|
@Req() request: Request & AuthenticatedRequest,
|
|
@Param("id") id: string,
|
|
): Promise<EdlProjectView> {
|
|
return this.edl.get(requireSession(request).workspaceId, parseId(id, "id"));
|
|
}
|
|
|
|
@Post(":id/render")
|
|
@RequirePermission("media.manage")
|
|
render(
|
|
@Req() request: Request & AuthenticatedRequest,
|
|
@Param("id") id: string,
|
|
@Body() body: unknown,
|
|
): Promise<{ jobId: string }> {
|
|
const session = requireSession(request);
|
|
const input = requireObject(body, "body");
|
|
const lane: EdlLane | undefined =
|
|
input.lane === undefined || input.lane === null ? undefined : parseLane(input.lane);
|
|
return this.edl.render(session.workspaceId, parseId(id, "id"), lane, session.userId);
|
|
}
|
|
}
|
|
|
|
function parseLane(value: unknown): EdlLane {
|
|
if (value !== "local" && value !== "remote") {
|
|
throw new BadRequestException("lane must be one of local, remote");
|
|
}
|
|
return value;
|
|
}
|