@@ -38,6 +38,8 @@ const createShotSchema = z.object({
|
|||||||
dueDate: z.string().optional(),
|
dueDate: z.string().optional(),
|
||||||
thumbnailUrl: z.string().url().optional(),
|
thumbnailUrl: z.string().url().optional(),
|
||||||
shotGroupName: z.string().max(100).optional(),
|
shotGroupName: z.string().max(100).optional(),
|
||||||
|
// Optional explicit shot code — bypasses auto-generation
|
||||||
|
shotCode: z.string().max(100).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── POST /api/ext/shots ───────────────────────────────────────────────────────
|
// ── POST /api/ext/shots ───────────────────────────────────────────────────────
|
||||||
@@ -109,6 +111,25 @@ export async function POST(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use explicit shotCode if provided, otherwise auto-increment
|
||||||
|
let shotCode: string;
|
||||||
|
let shotNumber: number;
|
||||||
|
|
||||||
|
if (parsed.shotCode) {
|
||||||
|
// Check uniqueness manually since we bypass auto-generation
|
||||||
|
const existing = await db.shot.findUnique({
|
||||||
|
where: { projectId_shotCode: { projectId: parsed.projectId, shotCode: parsed.shotCode } },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "A shot with that code already exists in this project", shotCode: parsed.shotCode },
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
shotCode = parsed.shotCode;
|
||||||
|
shotNumber = 0;
|
||||||
|
} else {
|
||||||
// Auto-increment shot number within scene (+ episode for episodic)
|
// Auto-increment shot number within scene (+ episode for episodic)
|
||||||
const scopeWhere = {
|
const scopeWhere = {
|
||||||
projectId: parsed.projectId,
|
projectId: parsed.projectId,
|
||||||
@@ -122,13 +143,14 @@ export async function POST(req: NextRequest) {
|
|||||||
select: { shotNumber: true },
|
select: { shotNumber: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const shotNumber = (maxShot?.shotNumber ?? 0) + 10;
|
shotNumber = (maxShot?.shotNumber ?? 0) + 10;
|
||||||
const paddedNumber = shotNumber.toString().padStart(4, "0");
|
const paddedNumber = shotNumber.toString().padStart(4, "0");
|
||||||
|
|
||||||
const shotCode =
|
shotCode =
|
||||||
project.projectType === "EPISODIC" && episode
|
project.projectType === "EPISODIC" && episode
|
||||||
? `${project.showId}_${episode}_${scene}_${paddedNumber}`
|
? `${project.showId}_${episode}_${scene}_${paddedNumber}`
|
||||||
: `${project.showId}_${scene}_${paddedNumber}`;
|
: `${project.showId}_${scene}_${paddedNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Upload thumbnail if provided as a file
|
// Upload thumbnail if provided as a file
|
||||||
let thumbnailUrl = parsed.thumbnailUrl;
|
let thumbnailUrl = parsed.thumbnailUrl;
|
||||||
@@ -214,3 +236,25 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── GET /api/ext/shots?projectId=... ─────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
if (!isAuthorized(req)) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const projectId = searchParams.get("projectId");
|
||||||
|
if (!projectId) {
|
||||||
|
return NextResponse.json({ error: "projectId is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const shots = await db.shot.findMany({
|
||||||
|
where: { projectId },
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
select: { id: true, shotCode: true, scene: true, episode: true, status: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ shots });
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# ==============================================================
|
||||||
|
# import-shots.ps1
|
||||||
|
#
|
||||||
|
# Place this script inside a folder whose SIBLINGS are the
|
||||||
|
# render directories you want to scan, e.g.:
|
||||||
|
#
|
||||||
|
# /renders/
|
||||||
|
# import-shots.ps1 <- this file
|
||||||
|
# UNG_103_010_010_BG01_TT_v001/
|
||||||
|
# UNG_103_010_010_FG01_TT_v001/
|
||||||
|
# UNG_103_020_010_BG01_TT_v001/
|
||||||
|
#
|
||||||
|
# Folder naming convention expected:
|
||||||
|
# {showId}_{episode}_{scene}_{shot}_{...anything...}
|
||||||
|
#
|
||||||
|
# The first four underscore-delimited segments form the shot
|
||||||
|
# code, e.g. UNG_103_010_010.
|
||||||
|
# ==============================================================
|
||||||
|
|
||||||
|
# -- CONFIG ----------------------------------------------------
|
||||||
|
$BaseUrl = "https://review.twotalesvfx.com"
|
||||||
|
$ApiKey = "am3O0PWUtqMJkAqsZ+bO7lho4cxQItxgukF6FHteAx4="
|
||||||
|
$ProjectId = "cmp6l5mzq0001ua0gz07bk72f"
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$headers = @{
|
||||||
|
"Authorization" = "Bearer $ApiKey"
|
||||||
|
"Content-Type" = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- 1. Fetch existing shots for the project -------------------
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Fetching existing shots for project $ProjectId ..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$existingUrl = "$BaseUrl/api/ext/shots?projectId=$ProjectId"
|
||||||
|
try {
|
||||||
|
$response = Invoke-RestMethod -Uri $existingUrl -Method GET -Headers $headers
|
||||||
|
$existingCodes = [System.Collections.Generic.HashSet[string]]($response.shots | ForEach-Object { $_.shotCode })
|
||||||
|
Write-Host " Found $($existingCodes.Count) existing shot(s)." -ForegroundColor DarkGray
|
||||||
|
} catch {
|
||||||
|
Write-Error "Failed to fetch existing shots: $_"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- 2. Scan sibling directories for EXR files ----------------
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$parentDir = Split-Path -Parent $scriptDir
|
||||||
|
$candidates = Get-ChildItem -Path $parentDir -Directory
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Scanning '$parentDir' for directories containing EXR files ..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$shotMap = [ordered]@{}
|
||||||
|
|
||||||
|
foreach ($dir in $candidates) {
|
||||||
|
if ($dir.FullName -eq $scriptDir) { continue }
|
||||||
|
|
||||||
|
$hasExr = Get-ChildItem -Path $dir.FullName -Filter "*.exr" -Recurse -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $hasExr) { continue }
|
||||||
|
|
||||||
|
$parts = $dir.Name -split "_"
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
Write-Warning " Skipping '$($dir.Name)' - fewer than 4 underscore-delimited segments."
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$shotCode = "$($parts[0])_$($parts[1])_$($parts[2])_$($parts[3])"
|
||||||
|
$episode = $parts[1]
|
||||||
|
$scene = $parts[2]
|
||||||
|
|
||||||
|
if (-not $shotMap.Contains($shotCode)) {
|
||||||
|
$shotMap[$shotCode] = @{ Episode = $episode; Scene = $scene }
|
||||||
|
Write-Host " Found EXR dir: $($dir.Name) -> shot code: $shotCode" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shotMap.Count -eq 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "No directories containing EXR files found. Nothing to import." -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Unique shot codes to process: $($shotMap.Count)" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# -- 3. Create shots, skipping existing ones ------------------
|
||||||
|
$created = 0
|
||||||
|
$skipped = 0
|
||||||
|
$failed = 0
|
||||||
|
|
||||||
|
foreach ($shotCode in $shotMap.Keys) {
|
||||||
|
$info = $shotMap[$shotCode]
|
||||||
|
|
||||||
|
if ($existingCodes.Contains($shotCode)) {
|
||||||
|
Write-Host " [SKIP] $shotCode (already exists)" -ForegroundColor DarkGray
|
||||||
|
$skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
projectId = $ProjectId
|
||||||
|
scene = $info.Scene
|
||||||
|
episode = $info.Episode
|
||||||
|
shotCode = $shotCode
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = Invoke-RestMethod -Uri "$BaseUrl/api/ext/shots" -Method POST -Headers $headers -Body $body
|
||||||
|
Write-Host " [CREATED] $shotCode (id: $($result.shot.id))" -ForegroundColor Green
|
||||||
|
$created++
|
||||||
|
} catch {
|
||||||
|
$statusCode = $null
|
||||||
|
if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode }
|
||||||
|
if ($statusCode -eq 409) {
|
||||||
|
Write-Host " [SKIP] $shotCode (conflict - already exists)" -ForegroundColor DarkGray
|
||||||
|
$skipped++
|
||||||
|
} else {
|
||||||
|
Write-Host " [ERROR] $shotCode - $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
$failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- 4. Summary -----------------------------------------------
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Done." -ForegroundColor Cyan
|
||||||
|
Write-Host " Created : $created" -ForegroundColor Green
|
||||||
|
Write-Host " Skipped : $skipped" -ForegroundColor DarkGray
|
||||||
|
if ($failed -gt 0) {
|
||||||
|
Write-Host " Failed : $failed" -ForegroundColor Red
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user