Show video posters in the library by building a first-frame thumb when one is missing.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+27
-18
@@ -520,7 +520,7 @@ export function publicLibrary(event: H3Event, opts?: { light?: boolean }) {
|
||||
clips: catalog.clips.filter(clip => unlockedIds.has(clip.folderId)).map(clip => ({
|
||||
...clip,
|
||||
name: clipTitle(clip),
|
||||
hasThumbnail: light ? !clip.hideThumbnail : !clip.hideThumbnail && existsSync(clipThumbPath(owner, clip.id)),
|
||||
hasThumbnail: !clip.hideThumbnail,
|
||||
hasVideo: light ? true : existsSync(clipVideoPath(owner, clip.id))
|
||||
})),
|
||||
stills: catalog.stills
|
||||
@@ -837,27 +837,36 @@ export function clipThumbPath(owner: string, id: string) {
|
||||
const dir = clipDir(owner, id)
|
||||
for (const name of ['thumb.jpg', 'thumb.png', 'thumb']) {
|
||||
const path = join(dir, name)
|
||||
if (existsSync(path)) return path
|
||||
if (existsSync(path) && statSync(path).size >= 64) return path
|
||||
}
|
||||
return join(dir, 'thumb.jpg')
|
||||
}
|
||||
|
||||
function extractVideoThumb(videoPath: string, destPath: string) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const proc = spawn('ffmpeg', ['-y', '-ss', '0.2', '-i', videoPath, '-frames:v', '1', '-update', '1', '-q:v', '4', destPath], { stdio: 'ignore' })
|
||||
const timer = setTimeout(() => {
|
||||
try { proc.kill('SIGKILL') } catch { /* ignore */ }
|
||||
resolve(false)
|
||||
}, 20000)
|
||||
proc.on('close', (code) => {
|
||||
clearTimeout(timer)
|
||||
resolve(code === 0 && existsSync(destPath))
|
||||
})
|
||||
proc.on('error', () => {
|
||||
clearTimeout(timer)
|
||||
resolve(false)
|
||||
})
|
||||
async function extractVideoThumb(videoPath: string, destPath: string) {
|
||||
try {
|
||||
await extractFirstFrame(videoPath, destPath)
|
||||
return existsSync(destPath) && statSync(destPath).size >= 64
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureClipThumb(owner: string, id: string) {
|
||||
const existing = clipThumbPath(owner, id)
|
||||
if (existsSync(existing) && statSync(existing).size >= 64) return existing
|
||||
const video = clipVideoPath(owner, id)
|
||||
if (!existsSync(video)) return ''
|
||||
const dest = join(clipDir(owner, id), 'thumb.jpg')
|
||||
const pending = thumbBuilds.get(dest)
|
||||
if (pending) return pending
|
||||
const build = withThumbSlot(async () => {
|
||||
const ok = await extractVideoThumb(video, dest)
|
||||
return ok ? dest : ''
|
||||
}).finally(() => {
|
||||
thumbBuilds.delete(dest)
|
||||
})
|
||||
thumbBuilds.set(dest, build)
|
||||
return build
|
||||
}
|
||||
|
||||
function probeVideoSize(videoPath: string) {
|
||||
@@ -913,7 +922,7 @@ export async function ensureClipThumbnails(owner: string, opts?: { skipHistory?:
|
||||
const thumb = clipThumbPath(owner, clip.id)
|
||||
const video = clipVideoPath(owner, clip.id)
|
||||
if (!existsSync(video)) continue
|
||||
if (!existsSync(thumb)) {
|
||||
if (!existsSync(thumb) || statSync(thumb).size < 64) {
|
||||
await extractVideoThumb(video, join(clipDir(owner, clip.id), 'thumb.jpg'))
|
||||
}
|
||||
if (!clip.width || !clip.height) {
|
||||
|
||||
Reference in New Issue
Block a user