Add a Director section to save and reload multi-shot video scripts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { ownerRoot } from '~/server/utils/library'
|
||||
import {
|
||||
normalizeDirectorScriptName,
|
||||
normalizeDirectorScriptText,
|
||||
type DirectorScript
|
||||
} from '~/utils/directorScripts'
|
||||
|
||||
const writeChains = new Map<string, Promise<unknown>>()
|
||||
|
||||
function scriptsPath(owner: string) {
|
||||
return join(ownerRoot(owner), 'director-scripts.json')
|
||||
}
|
||||
|
||||
function ensureOwner(owner: string) {
|
||||
mkdirSync(ownerRoot(owner), { recursive: true })
|
||||
}
|
||||
|
||||
function normalizeStored(raw: unknown): DirectorScript | null {
|
||||
if (!raw || typeof raw !== 'object') return null
|
||||
const rec = raw as Record<string, unknown>
|
||||
const name = normalizeDirectorScriptName(rec.name)
|
||||
const text = normalizeDirectorScriptText(rec.text)
|
||||
if (!name || !text.trim()) return null
|
||||
const createdAt = Number(rec.createdAt) || Date.now()
|
||||
return {
|
||||
id: String(rec.id || '').trim() || crypto.randomUUID(),
|
||||
name,
|
||||
text,
|
||||
createdAt,
|
||||
updatedAt: Number(rec.updatedAt) || createdAt
|
||||
}
|
||||
}
|
||||
|
||||
function readScripts(owner: string): DirectorScript[] {
|
||||
ensureOwner(owner)
|
||||
const path = scriptsPath(owner)
|
||||
if (!existsSync(path)) return []
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(path, 'utf8'))
|
||||
return Array.isArray(parsed)
|
||||
? parsed.map(normalizeStored).filter((item): item is DirectorScript => Boolean(item))
|
||||
: []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function writeScripts(owner: string, scripts: DirectorScript[]) {
|
||||
ensureOwner(owner)
|
||||
const path = scriptsPath(owner)
|
||||
const tmp = `${path}.tmp`
|
||||
writeFileSync(tmp, JSON.stringify(scripts, null, 2))
|
||||
renameSync(tmp, path)
|
||||
}
|
||||
|
||||
function mutate<T>(owner: string, fn: (scripts: DirectorScript[]) => T): Promise<T> {
|
||||
const prev = writeChains.get(owner) || Promise.resolve()
|
||||
const run = prev.then(() => {
|
||||
const scripts = readScripts(owner)
|
||||
const result = fn(scripts)
|
||||
writeScripts(owner, scripts)
|
||||
return result
|
||||
})
|
||||
writeChains.set(owner, run.then(() => undefined, () => undefined))
|
||||
return run
|
||||
}
|
||||
|
||||
export function listDirectorScripts(owner: string) {
|
||||
return readScripts(owner).sort((a, b) => b.updatedAt - a.updatedAt || a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
export function getDirectorScript(owner: string, id: string) {
|
||||
const wanted = String(id || '').trim()
|
||||
const script = readScripts(owner).find(item => item.id === wanted)
|
||||
if (!script) throw createError({ statusCode: 404, statusMessage: 'Script not found' })
|
||||
return script
|
||||
}
|
||||
|
||||
export async function saveDirectorScript(owner: string, input: {
|
||||
name: string
|
||||
text: string
|
||||
overwrite?: boolean
|
||||
}) {
|
||||
const name = normalizeDirectorScriptName(input.name)
|
||||
const text = normalizeDirectorScriptText(input.text)
|
||||
if (!name) throw createError({ statusCode: 400, statusMessage: 'A script name is required' })
|
||||
if (!text.trim()) throw createError({ statusCode: 400, statusMessage: 'A shot script is required' })
|
||||
const now = Date.now()
|
||||
return mutate(owner, (scripts) => {
|
||||
const existing = scripts.find(item => item.name.toLowerCase() === name.toLowerCase())
|
||||
if (existing && !input.overwrite) {
|
||||
throw createError({ statusCode: 409, statusMessage: `Overwrite script “${existing.name}”?` })
|
||||
}
|
||||
if (existing) {
|
||||
existing.text = text
|
||||
existing.updatedAt = now
|
||||
return existing
|
||||
}
|
||||
const created: DirectorScript = {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
text,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
scripts.unshift(created)
|
||||
return created
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateDirectorScript(owner: string, id: string, input: {
|
||||
name?: string
|
||||
text?: string
|
||||
}) {
|
||||
const wanted = String(id || '').trim()
|
||||
if (!wanted) throw createError({ statusCode: 400, statusMessage: 'Missing script id' })
|
||||
return mutate(owner, (scripts) => {
|
||||
const existing = scripts.find(item => item.id === wanted)
|
||||
if (!existing) throw createError({ statusCode: 404, statusMessage: 'Script not found' })
|
||||
if (input.name !== undefined) {
|
||||
const name = normalizeDirectorScriptName(input.name)
|
||||
if (!name) throw createError({ statusCode: 400, statusMessage: 'A script name is required' })
|
||||
const clash = scripts.find(item => item.id !== wanted && item.name.toLowerCase() === name.toLowerCase())
|
||||
if (clash) throw createError({ statusCode: 409, statusMessage: `A script named “${clash.name}” already exists` })
|
||||
existing.name = name
|
||||
}
|
||||
if (input.text !== undefined) {
|
||||
const text = normalizeDirectorScriptText(input.text)
|
||||
if (!text.trim()) throw createError({ statusCode: 400, statusMessage: 'A shot script is required' })
|
||||
existing.text = text
|
||||
}
|
||||
existing.updatedAt = Date.now()
|
||||
return existing
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteDirectorScript(owner: string, id: string) {
|
||||
const wanted = String(id || '').trim()
|
||||
if (!wanted) throw createError({ statusCode: 400, statusMessage: 'Missing script id' })
|
||||
return mutate(owner, (scripts) => {
|
||||
const index = scripts.findIndex(item => item.id === wanted)
|
||||
if (index < 0) throw createError({ statusCode: 404, statusMessage: 'Script not found' })
|
||||
const [removed] = scripts.splice(index, 1)
|
||||
return removed
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user