diff --git a/pages/index.vue b/pages/index.vue index 56254d1..6536906 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -487,6 +487,7 @@ let timer: ReturnType | null = null let source: EventSource | null = null let pollTimer: ReturnType | null = null let settledUi = false +let listenGen = 0 const elapsedLabel = computed(() => { const seconds = Math.floor(elapsedMs.value / 1000) @@ -739,7 +740,7 @@ async function resumeActiveJob() { jobId.value = id concealOutput.value = hidden settledUi = false - applyJobEvent(snap, hidden, locked) + applyJobEvent(snap, hidden, locked, id) const running = snap.status === 'queued' || snap.status === 'uploading' || snap.status === 'running' || snap.type === 'snapshot' if (running && !settledUi) { busy.value = true @@ -828,10 +829,14 @@ async function lockFolder(id: string) { } } +function clipVideoUrl(clipId: string) { + return `/api/library/clips/${clipId}/video?t=${Date.now()}` +} + function openClip(item: LibraryClip) { lockedSave.value = false concealOutput.value = item.hideThumbnail - videoUrl.value = `/api/library/clips/${item.id}/video` + videoUrl.value = clipVideoUrl(item.id) awaitingReveal.value = item.hideThumbnail playerSize.value = { width: item.width || 1344, height: item.height || 768 } browseFolderId.value = item.folderId @@ -874,6 +879,8 @@ async function generate() { } statusBusy.value = false busy.value = true + listenGen += 1 + stopListen() settledUi = false progress.value = 1 videoUrl.value = '' @@ -905,6 +912,7 @@ async function generate() { jobId.value = started.jobId maxStep.value = started.steps playerSize.value = { width: started.width, height: started.height } + settledUi = false persistActiveJob(started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true) listen(started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true) } catch (error: any) { @@ -927,7 +935,10 @@ function stopListen() { } } -function applyJobEvent(payload: Record, hidden: boolean, folderLocked: boolean) { +function applyJobEvent(payload: Record, hidden: boolean, folderLocked: boolean, expectedJobId: string) { + if (expectedJobId && jobId.value && expectedJobId !== jobId.value) return + if (payload.jobId && expectedJobId && payload.jobId !== expectedJobId) return + if (payload.message) statusMessage.value = payload.message statusBusy.value = payload.type === 'busy' || payload.busy === true if (typeof payload.progress === 'number') progress.value = payload.progress @@ -952,12 +963,12 @@ function applyJobEvent(payload: Record, hidden: boolean, folderLock browseFolderId.value = folderId.value statusMessage.value = 'Saved to the locked folder. Unlock it to view.' } else if (payload.clipId) { - videoUrl.value = `/api/library/clips/${payload.clipId}/video` + videoUrl.value = clipVideoUrl(payload.clipId) awaitingReveal.value = Boolean(payload.hideThumbnail || hidden) concealOutput.value = awaitingReveal.value lockedSave.value = false } else { - videoUrl.value = `/api/media?filename=${encodeURIComponent(payload.filename)}&subfolder=${encodeURIComponent(payload.subfolder || 'video')}&type=${encodeURIComponent(payload.mediaType || 'output')}` + videoUrl.value = `/api/media?filename=${encodeURIComponent(payload.filename)}&subfolder=${encodeURIComponent(payload.subfolder || 'video')}&type=${encodeURIComponent(payload.mediaType || 'output')}&t=${Date.now()}` awaitingReveal.value = Boolean(payload.hideThumbnail || hidden) concealOutput.value = awaitingReveal.value lockedSave.value = false @@ -983,26 +994,31 @@ function applyJobEvent(payload: Record, hidden: boolean, folderLock } function listen(id: string, hidden: boolean, folderLocked = false) { + const gen = ++listenGen stopListen() source = new EventSource(`/api/generate/${id}/stream`) source.onmessage = (event) => { - applyJobEvent(JSON.parse(event.data), hidden, folderLocked) + if (gen !== listenGen) return + applyJobEvent(JSON.parse(event.data), hidden, folderLocked, id) } const tick = async () => { - if (settledUi) { - stopListen() + if (gen !== listenGen || settledUi) { + if (gen === listenGen && settledUi) stopListen() return } try { const snap = await $fetch>(`/api/generate/${id}`) - applyJobEvent(snap, hidden, folderLocked) + if (gen !== listenGen) return + applyJobEvent(snap, hidden, folderLocked, id) } catch { + if (gen !== listenGen) return try { const recovered = await $fetch>('/api/generate/recover', { method: 'POST', body: { jobId: id } }) - applyJobEvent(recovered, hidden, folderLocked) + if (gen !== listenGen) return + applyJobEvent(recovered, hidden, folderLocked, id) } catch { // Job snapshot can 404 briefly after a restart; keep polling while busy. } diff --git a/server/api/generate/recover.post.ts b/server/api/generate/recover.post.ts index 88afefc..bc15581 100644 --- a/server/api/generate/recover.post.ts +++ b/server/api/generate/recover.post.ts @@ -12,12 +12,16 @@ export default defineEventHandler(async (event) => { return { type: 'snapshot', status: 'running', + jobId, message: 'Reconnecting to ComfyUI...', progress: 90, promptId: pending.promptId, - elapsedMs: Date.now() - pending.startedAt + elapsedMs: Date.now() - pending.startedAt, + hideThumbnail: pending.hideThumbnail, + folderLocked: pending.folderLocked } } + throw createError({ statusCode: 404, statusMessage: 'Job not found' }) } const { owner } = assertLibraryOwner(event) diff --git a/server/api/library/clips/[id]/video.get.ts b/server/api/library/clips/[id]/video.get.ts index 32ff56e..ea64e6a 100644 --- a/server/api/library/clips/[id]/video.get.ts +++ b/server/api/library/clips/[id]/video.get.ts @@ -11,6 +11,6 @@ export default defineEventHandler((event) => { } setHeader(event, 'Content-Type', 'video/mp4') setHeader(event, 'Content-Disposition', `inline; filename="${safeDownloadName(clipTitle(clip))}.mp4"`) - setHeader(event, 'Cache-Control', 'private, max-age=3600') + setHeader(event, 'Cache-Control', 'private, no-store') return readFileSync(path) }) diff --git a/server/utils/jobs.ts b/server/utils/jobs.ts index 1383f8b..f35ef21 100644 --- a/server/utils/jobs.ts +++ b/server/utils/jobs.ts @@ -18,6 +18,7 @@ export interface JobEvent { queueRunning?: number queuePending?: number folderLocked?: boolean + jobId?: string } export interface Job { @@ -89,6 +90,7 @@ export function listJobs() { export function jobSnapshot(job: Job) { return { + jobId: job.id, type: 'snapshot' as const, status: job.status, message: job.message, @@ -108,6 +110,7 @@ export function jobSnapshot(job: Job) { } export function emitJob(job: Job, event: JobEvent) { + event.jobId = job.id if (event.message) job.message = event.message if (typeof event.progress === 'number') job.progress = event.progress if (typeof event.step === 'number') job.step = event.step diff --git a/server/utils/library.ts b/server/utils/library.ts index b891f18..a15936b 100644 --- a/server/utils/library.ts +++ b/server/utils/library.ts @@ -637,9 +637,6 @@ export async function saveClip(params: { const catalog = readCatalog(params.ownerKey) const folder = catalog.folders.find(item => item.id === params.folderId) || catalog.folders[0] if (!folder) throw createError({ statusCode: 400, statusMessage: 'No library folder available' }) - if (params.comfyFilename && catalog.clips.some(clip => clip.comfyFilename === params.comfyFilename)) { - return catalog.clips.find(clip => clip.comfyFilename === params.comfyFilename) as LibraryClip - } const clip: LibraryClip = { id: crypto.randomUUID(), folderId: folder.id, diff --git a/server/utils/pending.ts b/server/utils/pending.ts index 980d6e1..6304011 100644 --- a/server/utils/pending.ts +++ b/server/utils/pending.ts @@ -91,6 +91,7 @@ export async function completePendingIfReady(pending: PendingJob) { status: 'complete' as const, message: pending.folderLocked ? 'Saved to the locked folder. Unlock it to view.' : 'Video ready', progress: 100, + jobId: pending.jobId, clipId: clip.id, filename: pending.folderLocked ? undefined : video.filename, subfolder: pending.folderLocked ? undefined : video.subfolder,