Serve the stage from a 1600px preview instead of the grid thumb.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+53
-4
@@ -1271,6 +1271,10 @@ export function stillThumbFile(owner: string, id: string) {
|
||||
return join(ownerRoot(owner), 'stills', `${id}.thumb.jpg`)
|
||||
}
|
||||
|
||||
export function stillPreviewFile(owner: string, id: string) {
|
||||
return join(ownerRoot(owner), 'stills', `${id}.preview.jpg`)
|
||||
}
|
||||
|
||||
/** Sync check — never kicks ffmpeg. Empty string when thumb is missing or stale. */
|
||||
export function existingStillThumbPath(owner: string, id: string) {
|
||||
const source = stillPath(owner, id)
|
||||
@@ -1282,14 +1286,27 @@ export function existingStillThumbPath(owner: string, id: string) {
|
||||
return ''
|
||||
}
|
||||
|
||||
/** Sync check for stage preview — never kicks ffmpeg. */
|
||||
export function existingStillPreviewPath(owner: string, id: string) {
|
||||
const source = stillPath(owner, id)
|
||||
const dest = stillPreviewFile(owner, id)
|
||||
if (!existsSync(source) || !existsSync(dest)) return ''
|
||||
try {
|
||||
if (statSync(dest).mtimeMs >= statSync(source).mtimeMs) return dest
|
||||
} catch { return '' }
|
||||
return ''
|
||||
}
|
||||
|
||||
function removeStillFiles(owner: string, id: string) {
|
||||
rmSync(stillPath(owner, id), { force: true })
|
||||
rmSync(stillThumbFile(owner, id), { force: true })
|
||||
rmSync(stillPreviewFile(owner, id), { force: true })
|
||||
}
|
||||
|
||||
let thumbSlots = 0
|
||||
const thumbWaiters: Array<() => void> = []
|
||||
const thumbBuilds = new Map<string, Promise<string>>()
|
||||
const previewBuilds = new Map<string, Promise<string>>()
|
||||
|
||||
function withThumbSlot<T>(fn: () => Promise<T>) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
@@ -1306,14 +1323,14 @@ function withThumbSlot<T>(fn: () => Promise<T>) {
|
||||
})
|
||||
}
|
||||
|
||||
function buildStillThumb(source: string, dest: string) {
|
||||
function buildStillJpeg(source: string, dest: string, scale: string, quality: string) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const proc = spawn('ffmpeg', [
|
||||
'-y',
|
||||
'-i', source,
|
||||
'-vf', 'scale=384:-2',
|
||||
'-vf', scale,
|
||||
'-frames:v', '1',
|
||||
'-q:v', '5',
|
||||
'-q:v', quality,
|
||||
dest
|
||||
], { stdio: 'ignore' })
|
||||
const timer = setTimeout(() => {
|
||||
@@ -1331,6 +1348,14 @@ function buildStillThumb(source: string, dest: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function buildStillThumb(source: string, dest: string) {
|
||||
return buildStillJpeg(source, dest, 'scale=384:-2', '5')
|
||||
}
|
||||
|
||||
function buildStillPreview(source: string, dest: string) {
|
||||
return buildStillJpeg(source, dest, 'scale=1600:-2', '3')
|
||||
}
|
||||
|
||||
export async function stillThumbPath(owner: string, id: string) {
|
||||
const source = stillPath(owner, id)
|
||||
const dest = stillThumbFile(owner, id)
|
||||
@@ -1340,7 +1365,7 @@ export async function stillThumbPath(owner: string, id: string) {
|
||||
if (statSync(dest).mtimeMs >= statSync(source).mtimeMs) return dest
|
||||
} catch { /* rebuild */ }
|
||||
}
|
||||
const key = `${owner}:${id}`
|
||||
const key = `${owner}:${id}:thumb`
|
||||
const pending = thumbBuilds.get(key)
|
||||
if (pending) return pending
|
||||
const build = withThumbSlot(async () => {
|
||||
@@ -1354,6 +1379,29 @@ export async function stillThumbPath(owner: string, id: string) {
|
||||
return build
|
||||
}
|
||||
|
||||
export async function stillPreviewPath(owner: string, id: string) {
|
||||
const source = stillPath(owner, id)
|
||||
const dest = stillPreviewFile(owner, id)
|
||||
if (!existsSync(source)) return ''
|
||||
if (existsSync(dest)) {
|
||||
try {
|
||||
if (statSync(dest).mtimeMs >= statSync(source).mtimeMs) return dest
|
||||
} catch { /* rebuild */ }
|
||||
}
|
||||
const key = `${owner}:${id}:preview`
|
||||
const pending = previewBuilds.get(key)
|
||||
if (pending) return pending
|
||||
const build = withThumbSlot(async () => {
|
||||
const ok = await buildStillPreview(source, dest)
|
||||
// Never return the archive master as a stage preview
|
||||
return ok ? dest : ''
|
||||
}).finally(() => {
|
||||
previewBuilds.delete(key)
|
||||
})
|
||||
previewBuilds.set(key, build)
|
||||
return build
|
||||
}
|
||||
|
||||
export async function ensureStillThumbnails(owner: string, opts?: { limit?: number }) {
|
||||
const catalog = readCatalog(owner)
|
||||
const cap = Math.max(0, Math.min(Number(opts?.limit) || 2, 8))
|
||||
@@ -1603,6 +1651,7 @@ export async function saveStill(params: {
|
||||
next.stills.unshift(still)
|
||||
})
|
||||
void stillThumbPath(params.ownerKey, still.id).catch(() => null)
|
||||
void stillPreviewPath(params.ownerKey, still.id).catch(() => null)
|
||||
return still
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user