b596c97faf
Deploy / deploy (push) Successful in 3m12s
Check shots on the Playlist page and "Play selection" plays them back to back without slate or handles. The cut comes from the version's handle and slate metadata (new cutSpan in lib/handles); the media is never changed. SequencePlayer double-buffers two videos: the next clip is parked a few frames before its cut-in and pre-rolled hidden, the outgoing clip pauses on its last cut frame, and the cut fires on the incoming clip's cut-in frame (requestVideoFrameCallback), with the start time calibrated at each cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import { shotThumbnail } from "@/lib/shot-thumbnail";
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const session = await auth();
|
|
if (!session?.user) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
const projectId = req.nextUrl.searchParams.get("projectId");
|
|
if (!projectId) {
|
|
return NextResponse.json({ error: "projectId required" }, { status: 400 });
|
|
}
|
|
|
|
// All versions are linked to tasks (taskId), not directly to shots.
|
|
// We query shots with their tasks, and for each task get the latest version.
|
|
const shots = await db.shot.findMany({
|
|
where: {
|
|
projectId,
|
|
tasks: {
|
|
some: {
|
|
versions: { some: { isLatest: true } },
|
|
},
|
|
},
|
|
},
|
|
orderBy: [{ episode: "asc" }, { scene: "asc" }, { shotNumber: "asc" }],
|
|
select: {
|
|
id: true,
|
|
shotCode: true,
|
|
episode: true,
|
|
status: true,
|
|
shotApprovalStatus: true,
|
|
sharedWithClient: true,
|
|
thumbnailUrl: true,
|
|
generatedThumbnailUrl: true,
|
|
tasks: {
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
versions: {
|
|
where: { isLatest: true },
|
|
take: 1,
|
|
orderBy: { createdAt: "desc" },
|
|
select: {
|
|
id: true,
|
|
versionNumber: true,
|
|
fileUrl: true,
|
|
thumbnailUrl: true,
|
|
posterUrl: true,
|
|
fps: true,
|
|
duration: true,
|
|
frameCount: true,
|
|
hqFileUrl: true,
|
|
handleHead: true,
|
|
handleTail: true,
|
|
pictureStartFrame: true,
|
|
approvalStatus: true,
|
|
mimeType: true,
|
|
createdAt: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// For each shot, pick the most recently uploaded latest version across all tasks
|
|
const playlist = shots
|
|
.map((shot) => {
|
|
const versions = shot.tasks
|
|
.flatMap((t) => t.versions.map((v) => ({ ...v, taskTitle: t.title })))
|
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
|
|
const latest = versions[0] ?? null;
|
|
if (!latest) return null;
|
|
|
|
return {
|
|
id: shot.id,
|
|
shotCode: shot.shotCode,
|
|
episode: shot.episode,
|
|
status: shot.status,
|
|
shotApprovalStatus: shot.shotApprovalStatus,
|
|
sharedWithClient: shot.sharedWithClient,
|
|
thumbnailUrl: shotThumbnail(shot),
|
|
latestVersion: {
|
|
id: latest.id,
|
|
versionNumber: latest.versionNumber,
|
|
fileUrl: latest.fileUrl,
|
|
thumbnailUrl: latest.thumbnailUrl,
|
|
posterUrl: latest.posterUrl,
|
|
fps: latest.fps,
|
|
duration: latest.duration,
|
|
frameCount: latest.frameCount,
|
|
hqFileUrl: latest.hqFileUrl,
|
|
handleHead: latest.handleHead,
|
|
handleTail: latest.handleTail,
|
|
pictureStartFrame: latest.pictureStartFrame,
|
|
approvalStatus: latest.approvalStatus,
|
|
taskTitle: latest.taskTitle,
|
|
},
|
|
};
|
|
})
|
|
.filter((s): s is NonNullable<typeof s> => s !== null);
|
|
|
|
return NextResponse.json({ shots: playlist });
|
|
}
|