diff --git a/EXT_API_REFERENCE.md b/EXT_API_REFERENCE.md new file mode 100644 index 0000000..cb484e8 --- /dev/null +++ b/EXT_API_REFERENCE.md @@ -0,0 +1,515 @@ +# VFXReview External API Reference + +Base URL: `https://review.twotalesvfx.com` + +All requests require an `Authorization` header. Two formats are accepted: + +``` +Authorization: Bearer +X-Api-Key: +``` + +--- + +## Endpoints Overview + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/ext/projects` | List all projects with showIds | +| `GET` | `/api/ext/shots/lookup` | Look up a shot by code — primary pipeline endpoint | +| `GET` | `/api/ext/projects/{projectCode}/episodes` | List episodes for a project | +| `GET` | `/api/ext/projects/{projectCode}/shots` | List shots for a project by code | +| `GET` | `/api/ext/shots` | List shots by database project ID | +| `GET` | `/api/ext/shots/{shotId}` | Get a single shot by DB id or shot code | +| `POST` | `/api/ext/shots` | Create a new shot | +| `PATCH` | `/api/ext/shots/{shotId}` | Update shot fields (e.g. increment version) | + +--- + +## Shot Status Values + +| Value | Description | +|---|---| +| `WAITING` | Not yet started | +| `IN_PROGRESS` | Currently being worked on | +| `IN_REVIEW` | Submitted for review | +| `REVISIONS` | Changes requested | +| `COMPLETE` | Approved and complete | + +--- + +## Shot Priority Values + +| Value | Description | +|---|---| +| `LOW` | Low priority | +| `NORMAL` | Default | +| `HIGH` | High priority | +| `URGENT` | Urgent | + +--- + +## Shot Code Format + +``` +{showId}_{episode}_{scene}_{shotNumber} + +e.g. UNG_106_010_020 + ^^^ ^^^ ^^^ ^^^ + | | | shot number (zero-padded) + | | scene + | episode + show ID +``` + +--- + +## 1. List Projects + +``` +GET /api/ext/projects +``` + +Returns all projects with their `showId`, `code`, and metadata. Useful for populating project selectors in pipeline tools or resolving a showId → projectCode mapping. + +### Query Parameters + +| Parameter | Required | Default | Description | +|---|---|---|---| +| `status` | No | `ACTIVE` | `ACTIVE`, `ARCHIVED`, or `ALL` | + +### Response `200 OK` + +```json +{ + "projects": [ + { + "id": "cmp6l5mzq0001ua0gz07bk72f", + "name": "Ungo Season 1", + "code": "UNG_S1", + "showId": "UNG", + "projectType": "EPISODIC", + "status": "ACTIVE", + "startDate": "2026-01-01T00:00:00.000Z", + "dueDate": "2026-12-01T00:00:00.000Z", + "_count": { "shots": 312 } + } + ], + "total": 1 +} +``` + +### Project Type Values + +| Value | Description | +|---|---| +| `STANDARD` | Non-episodic project | +| `EPISODIC` | Series with episode numbers | + +### Project Status Values + +| Value | Description | +|---|---| +| `ACTIVE` | Currently in production | +| `ARCHIVED` | Completed or archived | + +--- + +## 2. Look Up Shot by Code + +**Primary endpoint used by pipeline tools (After Effects, Nuke, etc.)** + +``` +GET /api/ext/shots/lookup?shotCode={shotCode}&projectCode={projectCode} +``` + +### Query Parameters + +| Parameter | Required | Description | +|---|---|---| +| `shotCode` | Yes | Human-readable shot code, e.g. `UNG_106_010_020` | +| `projectCode` | Recommended | Project code, e.g. `UNG_S1` — disambiguates if same shot code appears in multiple projects | + +### Response `200 OK` + +```json +{ + "shot": { + "id": "clxxxxxxxxxxxxxx", + "shotCode": "UNG_106_010_020", + "scene": "010", + "episode": "106", + "sequence": "010", + "shotNumber": 20, + "description": "Hero establishing wide", + "notes": "Keep sky practical", + "status": "IN_PROGRESS", + "priority": "NORMAL", + "frameStart": 1001, + "frameEnd": 1120, + "fps": 24, + "dueDate": "2026-08-01T00:00:00.000Z", + "thumbnailUrl": "/api/files/thumb_abc123", + "createdAt": "2026-06-01T10:00:00.000Z", + "updatedAt": "2026-07-15T14:30:00.000Z", + + "shotVersion": "v003", + "exrOutput": "UNG_106_010_020_cmp_TT_v003", + + "sourceClip": "A106_C020_0601AB", + "timecodeStart": "01:00:10:00", + "timecodeEnd": "01:00:15:00", + "clipDuration": "00:00:05:00", + "seqTimecodeStart": "00:59:49:12", + "seqTimecodeEnd": "00:59:55:00", + + "project": { + "id": "cmp6l5mzq0001ua0gz07bk72f", + "name": "Ungo Season 1", + "code": "UNG_S1", + "showId": "UNG" + }, + "artist": { + "id": "clxxxxxxxxxxxxxx", + "name": "Jane Smith", + "email": "jane@studio.com" + }, + "tasks": [ + { + "id": "clxxxxxxxxxxxxxx", + "title": "Comp", + "type": "COMP", + "status": "IN_PROGRESS" + } + ], + "versions": [ + { + "id": "clxxxxxxxxxxxxxx", + "versionNumber": 3, + "approvalStatus": "PENDING", + "reviewStatus": "PENDING", + "fileUrl": "/api/files/vid_abc123", + "thumbnailUrl": "/api/files/thumb_abc123", + "createdAt": "2026-07-15T14:30:00.000Z" + } + ] + } +} +``` + +### Pipeline-Specific Fields + +| Field | Type | Description | +|---|---|---| +| `shotVersion` | `string` | Current version string, e.g. `v003`. Used for output naming. | +| `exrOutput` | `string` | Base filename for EXR renders, e.g. `UNG_106_010_020_cmp_TT_v003` | +| `seqTimecodeStart` | `string` | Shot in-point in the locked picture sequence (HH:MM:SS:FF) | +| `seqTimecodeEnd` | `string` | Shot out-point in the locked picture sequence (HH:MM:SS:FF) | +| `sourceClip` | `string` | Original camera roll/clip name from the EDL | +| `timecodeStart` | `string` | Source clip timecode in (HH:MM:SS:FF) | +| `timecodeEnd` | `string` | Source clip timecode out (HH:MM:SS:FF) | +| `clipDuration` | `string` | Clip duration (HH:MM:SS:FF) | + +--- + +## 3. List Episodes + +``` +GET /api/ext/projects/{projectCode}/episodes +``` + +### Path Parameters + +| Parameter | Description | +|---|---| +| `projectCode` | Human-readable project code, e.g. `UNG_S1` | + +### Query Parameters + +| Parameter | Required | Description | +|---|---|---| +| `shotNames` | No | Pass `1` to include full per-episode shot list (default off) | + +### Response `200 OK` + +```json +{ + "project": { + "id": "cmp6l5mzq0001ua0gz07bk72f", + "name": "Ungo Season 1", + "code": "UNG_S1", + "showId": "UNG" + }, + "episodes": [ + { + "episode": "106", + "shotCount": 42, + "sequences": ["010", "020", "030"] + } + ] +} +``` + +When `shotNames=1`, each episode entry also includes a `shots` array with: +`id`, `shotCode`, `episode`, `sequence`, `status`, `exrOutput`, `seqTimecodeStart`, `seqTimecodeEnd`, `sourceClip`, `timecodeStart`, `timecodeEnd`, `clipDuration`. + +--- + +## 4. List Shots by Project Code + +``` +GET /api/ext/projects/{projectCode}/shots +``` + +### Path Parameters + +| Parameter | Description | +|---|---| +| `projectCode` | Human-readable project code, e.g. `UNG_S1` | + +### Query Parameters + +| Parameter | Required | Default | Description | +|---|---|---|---| +| `episode` | No | — | Filter by episode, e.g. `106` | +| `sequence` | No | — | Filter by sequence, e.g. `010` | +| `status` | No | — | Filter by shot status | +| `page` | No | `1` | Page number | +| `limit` | No | `200` | Results per page (max `500`) | + +### Response `200 OK` + +```json +{ + "project": { "id": "...", "name": "...", "code": "UNG_S1", "showId": "UNG" }, + "pagination": { "page": 1, "limit": 200, "total": 42, "pages": 1 }, + "shots": [ + { + "id": "clxxxxxxxxxxxxxx", + "shotCode": "UNG_106_010_020", + "scene": "010", + "episode": "106", + "sequence": "010", + "shotNumber": 20, + "description": "Hero establishing wide", + "status": "IN_PROGRESS", + "priority": "NORMAL", + "frameStart": 1001, + "frameEnd": 1120, + "fps": 24, + "dueDate": "2026-08-01T00:00:00.000Z", + "thumbnailUrl": null, + "exrOutput": "UNG_106_010_020_cmp_TT_v003", + "seqTimecodeStart": "00:59:49:12", + "seqTimecodeEnd": "00:59:55:00", + "sourceClip": "A106_C020_0601AB", + "timecodeStart": "01:00:10:00", + "timecodeEnd": "01:00:15:00", + "clipDuration": "00:00:05:00", + "artist": { "id": "...", "name": "Jane Smith", "email": "jane@studio.com" }, + "_count": { "versions": 3, "tasks": 2 } + } + ] +} +``` + +> **Note:** `shotVersion` is not included in this response. Use the lookup endpoint to get it. + +--- + +## 5. List Shots by Project ID + +``` +GET /api/ext/shots?projectId={projectId} +``` + +Legacy endpoint. Prefer `/api/ext/projects/{projectCode}/shots` for new integrations. + +### Query Parameters + +| Parameter | Required | Description | +|---|---|---| +| `projectId` | Yes | Database CUID of the project | +| `episode` | No | Filter by episode | +| `status` | No | Filter by shot status | +| `shotCode` | No | Return only the shot matching this exact code | + +### Response `200 OK` + +```json +{ + "shots": [ /* array of shot objects */ ], + "total": 42 +} +``` + +Shot objects include: `id`, `shotCode`, `scene`, `episode`, `shotNumber`, `description`, `status`, `priority`, `frameStart`, `frameEnd`, `fps`, `dueDate`, `createdAt`, `updatedAt`, `artist`, `_count`. + +> **Note:** Pipeline-specific fields (`exrOutput`, `shotVersion`, `seqTimecodes`, etc.) are **not** included. Use the lookup endpoint. + +--- + +## 6. Get Single Shot + +``` +GET /api/ext/shots/{shotId} +``` + +### By Database ID + +```http +GET /api/ext/shots/clxxxxxxxxxxxxxx +``` + +### By Shot Code + +```http +GET /api/ext/shots/UNG_106_010_020?byCode=1&projectId=cmp6l5mzq0001ua0gz07bk72f +``` + +| Query Param | Description | +|---|---| +| `byCode` | Set to `1` to treat the path segment as a shot code | +| `projectId` | Required when `byCode=1` to scope the lookup | + +### Response `200 OK` + +Returns the same full shot object as the lookup endpoint, including tasks and latest version. Pipeline-specific fields (`exrOutput`, `shotVersion`, `seqTimecodeStart`, `seqTimecodeEnd`, `sourceClip`, etc.) are included. + +--- + +## 7. Create Shot + +``` +POST /api/ext/shots +Content-Type: application/json +``` + +Accepts either `application/json` or `multipart/form-data` (when uploading a thumbnail file). + +### Request Body + +| Field | Type | Required | Description | +|---|---|---|---| +| `projectId` | string (CUID) | Yes | Database project ID | +| `scene` | string | Yes | Scene identifier, alphanumeric + underscore, e.g. `010` | +| `episode` | string | No | Episode number. Required for `EPISODIC` projects | +| `description` | string | No | Shot description | +| `artistId` | string (CUID) | No | Assign to an artist by their DB id | +| `priority` | string | No | `LOW`, `NORMAL`, `HIGH`, or `URGENT` (default `NORMAL`) | +| `fps` | number | No | Frame rate (default `24`) | +| `frameStart` | integer | No | First frame number | +| `frameEnd` | integer | No | Last frame number | +| `dueDate` | string (ISO 8601) | No | Due date | +| `thumbnailUrl` | string (URL) | No | Pre-hosted thumbnail URL | +| `thumbnail` | File | No | Thumbnail image upload (multipart only, max 50 MB) | +| `shotGroupName` | string | No | Shot group name — created automatically if it doesn't exist | +| `shotCode` | string | No | Explicit shot code — bypasses auto-generation, must be unique in the project | + +### Response `201 Created` + +```json +{ + "shot": { + "id": "clxxxxxxxxxxxxxx", + "shotCode": "UNG_106_010_0010", + "scene": "010", + "episode": "106", + "shotNumber": 10, + "description": null, + "status": "WAITING", + "priority": "NORMAL", + "fps": 24, + "frameStart": null, + "frameEnd": null, + "dueDate": null, + "thumbnailUrl": null, + "projectId": "cmp6l5mzq0001ua0gz07bk72f", + "artistId": null, + "shotGroupId": null, + "createdAt": "2026-07-21T09:00:00.000Z" + } +} +``` + +### Error `409 Conflict` + +Returned when an explicit `shotCode` already exists in the project: + +```json +{ "error": "A shot with that code already exists in this project", "shotCode": "UNG_106_010_020" } +``` + +--- + +## 8. Update Shot + +``` +PATCH /api/ext/shots/{shotId} +Content-Type: application/json +``` + +Used by pipeline tools to write back metadata after a render/upload. + +### Path Parameters + +| Parameter | Description | +|---|---| +| `shotId` | Database ID of the shot (from the lookup response `shot.id`) | + +### Request Body + +| Field | Type | Required | Validation | Description | +|---|---|---|---|---| +| `shotVersion` | string | Yes* | Must match `v###`, e.g. `v004` | Sets the current version string | + +*At least one field must be provided; currently only `shotVersion` is supported. + +### Example Request + +```http +PATCH /api/ext/shots/clxxxxxxxxxxxxxx +Authorization: Bearer your-api-key +Content-Type: application/json + +{ "shotVersion": "v004" } +``` + +### Response `200 OK` + +```json +{ "success": true, "shot": { "id": "clxxxxxxxxxxxxxx", "shotVersion": "v004" } } +``` + +--- + +## Error Responses + +| Status | Meaning | +|---|---| +| `400` | Missing required parameter | +| `401` | Missing or invalid API key | +| `403` | Forbidden (session-auth routes only) | +| `404` | Resource not found | +| `409` | Conflict (duplicate shot code) | +| `413` | Payload too large (thumbnail > 50 MB) | +| `422` | Validation error | +| `500` | Internal server error | + +```json +{ "error": "shotCode query param is required" } +{ "error": "Validation error", "details": [ ... ] } +``` + +--- + +## After Effects Connector — Endpoints Used + +For reference, the endpoints called by `VFXReviewConnector.jsx`: + +| Action | Endpoint | +|---|---| +| Populate Episode dropdown | `GET /api/ext/projects/{PROJECT_CODE}/episodes` | +| Populate Shot dropdown | `GET /api/ext/projects/{PROJECT_CODE}/shots?episode={value}` | +| All shot operations (overlay, queue, preview, delivery) | `GET /api/ext/shots/lookup?shotCode={code}&projectCode={PROJECT_CODE}` | +| Increment Shot Version | `PATCH /api/ext/shots/{shot.id}` with `{ "shotVersion": "v00X" }` | diff --git a/app/api/ext/projects/route.ts b/app/api/ext/projects/route.ts new file mode 100644 index 0000000..36d4498 --- /dev/null +++ b/app/api/ext/projects/route.ts @@ -0,0 +1,60 @@ +import { NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; + +// ── Auth ───────────────────────────────────────────────────────────────────── + +function isAuthorized(req: NextRequest): boolean { + const apiKey = process.env.API_SECRET_KEY; + if (!apiKey) return false; + const authHeader = req.headers.get("authorization") ?? ""; + if (authHeader.startsWith("Bearer ")) return authHeader.slice(7) === apiKey; + return (req.headers.get("x-api-key") ?? "") === apiKey; +} + +// ── GET /api/ext/projects ───────────────────────────────────────────────────── +// +// Returns all projects with their showIds. Useful for pipeline tools that need +// to enumerate available projects and resolve a showId → projectCode mapping. +// +// Query params (all optional): +// status Filter by project status: ACTIVE (default) | ARCHIVED | ALL +// +// Example: +// GET /api/ext/projects +// GET /api/ext/projects?status=ALL + +export async function GET(req: NextRequest) { + if (!isAuthorized(req)) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(req.url); + const statusParam = searchParams.get("status")?.toUpperCase() ?? "ACTIVE"; + + const whereStatus = + statusParam === "ALL" + ? undefined + : statusParam === "ARCHIVED" + ? { status: "ARCHIVED" as const } + : { status: "ACTIVE" as const }; + + const projects = await db.project.findMany({ + where: whereStatus, + orderBy: [{ showId: "asc" }, { code: "asc" }], + select: { + id: true, + name: true, + code: true, + showId: true, + projectType: true, + status: true, + startDate: true, + dueDate: true, + _count: { + select: { shots: true }, + }, + }, + }); + + return NextResponse.json({ projects, total: projects.length }); +}