API UPdate
Deploy / deploy (push) Successful in 2m38s

This commit is contained in:
twotalesanimation
2026-06-12 19:20:58 +02:00
parent 66f1da203f
commit 1bdb147d24
4 changed files with 325 additions and 3 deletions
@@ -0,0 +1,103 @@
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/[projectCode]/episodes ──────────────────────────────
//
// Returns the list of distinct episodes in a project along with their shot
// codes, sequences, and EDL data. Useful for populating episode selectors in
// pipeline tools.
//
// Query params (optional):
// shotNames Pass "1" to include the full shot code list per episode (default off)
//
// Example:
// GET /api/ext/projects/UNG_108/episodes
// GET /api/ext/projects/UNG_108/episodes?shotNames=1
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ projectCode: string }> }
) {
if (!isAuthorized(req)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { projectCode } = await params;
const { searchParams } = new URL(req.url);
const includeShotNames = searchParams.get("shotNames") === "1";
const project = await db.project.findUnique({
where: { code: projectCode },
select: { id: true, name: true, code: true, showId: true },
});
if (!project) {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
// Fetch all shots grouped by episode
const shots = await db.shot.findMany({
where: { projectId: project.id },
orderBy: [{ episode: "asc" }, { shotCode: "asc" }],
select: {
id: true,
shotCode: true,
episode: true,
sequence: true,
status: true,
exrOutput: true,
sourceClip: true,
timecodeStart: true,
timecodeEnd: true,
clipDuration: true,
},
});
// Group by episode
const episodeMap = new Map<
string,
{
episode: string;
shotCount: number;
sequences: string[];
shots?: typeof shots;
}
>();
for (const shot of shots) {
const ep = shot.episode ?? "(none)";
if (!episodeMap.has(ep)) {
episodeMap.set(ep, { episode: ep, shotCount: 0, sequences: [], shots: [] });
}
const entry = episodeMap.get(ep)!;
entry.shotCount++;
const seq = shot.sequence ?? "";
if (seq && !entry.sequences.includes(seq)) {
entry.sequences.push(seq);
}
if (includeShotNames) {
entry.shots!.push(shot);
}
}
const episodes = Array.from(episodeMap.values()).map((e) => {
if (!includeShotNames) {
const { shots: _omit, ...rest } = e;
return rest;
}
return e;
});
return NextResponse.json({ project, episodes });
}
@@ -0,0 +1,110 @@
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/[projectCode]/shots ─────────────────────────────────
//
// Returns all shots for a project, optionally filtered by episode and/or sequence.
// Uses the human-readable project code (e.g. UNG_108) not the DB id.
//
// Query params (all optional):
// episode e.g. 108
// sequence e.g. 004
// status e.g. IN_PROGRESS
// page (default 1)
// limit (default 200, max 500)
//
// Example:
// GET /api/ext/projects/UNG_108/shots?episode=108&sequence=004
// GET /api/ext/projects/UNG_108/shots
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ projectCode: string }> }
) {
if (!isAuthorized(req)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { projectCode } = await params;
const { searchParams } = new URL(req.url);
const episode = searchParams.get("episode")?.trim() || undefined;
const sequence = searchParams.get("sequence")?.trim() || undefined;
const status = searchParams.get("status")?.trim() || undefined;
const page = Math.max(1, parseInt(searchParams.get("page") ?? "1", 10));
const limit = Math.min(500, Math.max(1, parseInt(searchParams.get("limit") ?? "200", 10)));
const skip = (page - 1) * limit;
const project = await db.project.findUnique({
where: { code: projectCode },
select: { id: true, name: true, code: true, showId: true },
});
if (!project) {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
const [shots, total] = await Promise.all([
db.shot.findMany({
where: {
projectId: project.id,
...(episode ? { episode } : {}),
...(sequence ? { sequence } : {}),
...(status ? { status: status as never } : {}),
},
orderBy: [{ episode: "asc" }, { shotCode: "asc" }],
skip,
take: limit,
select: {
id: true,
shotCode: true,
scene: true,
episode: true,
sequence: true,
shotNumber: true,
description: true,
status: true,
priority: true,
frameStart: true,
frameEnd: true,
fps: true,
dueDate: true,
// EDL / pull CSV fields
sourceClip: true,
timecodeStart: true,
timecodeEnd: true,
clipDuration: true,
exrOutput: true,
thumbnailUrl: true,
artist: {
select: { id: true, name: true, email: true },
},
_count: { select: { versions: true, tasks: true } },
},
}),
db.shot.count({
where: {
projectId: project.id,
...(episode ? { episode } : {}),
...(sequence ? { sequence } : {}),
...(status ? { status: status as never } : {}),
},
}),
]);
return NextResponse.json({
project,
pagination: { page, limit, total, pages: Math.ceil(total / limit) },
shots,
});
}
+5
View File
@@ -50,6 +50,11 @@ export async function GET(
frameEnd: true, frameEnd: true,
fps: true, fps: true,
dueDate: true, dueDate: true,
sourceClip: true,
timecodeStart: true,
timecodeEnd: true,
clipDuration: true,
exrOutput: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
project: { project: {
+104
View File
@@ -0,0 +1,104 @@
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/shots/lookup ─────────────────────────────────────────────────
//
// Look up a single shot by its shot code and (optionally) project code.
// Designed for use in Nuke/After Effects/Blender pipeline tools.
//
// Query params:
// shotCode (required) e.g. UNG_108_004_010
// projectCode (recommended) e.g. UNG_108 disambiguates if same code appears across projects
//
// Example:
// GET /api/ext/shots/lookup?shotCode=UNG_108_004_010&projectCode=UNG_108
// Authorization: Bearer <API_SECRET_KEY>
export async function GET(req: NextRequest) {
if (!isAuthorized(req)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(req.url);
const shotCode = searchParams.get("shotCode")?.trim();
const projectCode = searchParams.get("projectCode")?.trim();
if (!shotCode) {
return NextResponse.json({ error: "shotCode query param is required" }, { status: 400 });
}
const shot = await db.shot.findFirst({
where: {
shotCode,
...(projectCode ? { project: { code: projectCode } } : {}),
},
select: {
id: true,
shotCode: true,
scene: true,
episode: true,
sequence: true,
shotNumber: true,
description: true,
notes: true,
status: true,
priority: true,
frameStart: true,
frameEnd: true,
fps: true,
dueDate: true,
// EDL / pull CSV fields
sourceClip: true,
timecodeStart: true,
timecodeEnd: true,
clipDuration: true,
exrOutput: true,
thumbnailUrl: true,
createdAt: true,
updatedAt: true,
project: {
select: { id: true, name: true, code: true, showId: true },
},
artist: {
select: { id: true, name: true, email: true },
},
tasks: {
orderBy: { sortOrder: "asc" },
select: {
id: true,
title: true,
type: true,
status: true,
},
},
versions: {
where: { isLatest: true },
take: 1,
select: {
id: true,
versionNumber: true,
approvalStatus: true,
reviewStatus: true,
fileUrl: true,
createdAt: true,
},
},
},
});
if (!shot) {
return NextResponse.json({ error: "Shot not found" }, { status: 404 });
}
return NextResponse.json({ shot });
}