Keep pause across Coolify restarts, apply the chosen still from the image picker, and remux clips with Range/faststart so finished video can be scrubbed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 08:15:52 -05:00
co-authored by Cursor
parent 870150f052
commit 7d783bd97e
12 changed files with 338 additions and 49 deletions
+67 -10
View File
@@ -1,6 +1,8 @@
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { getJob, listJobs, type Job } from '~/server/utils/jobs'
import { listPendingJobs, patchPendingJob, readPendingJob } from '~/server/utils/pending'
import { getShotQueue } from '~/server/utils/shotQueue'
import { parseVideoWorkflow, type VideoWorkflowId } from '~/utils/videoModels'
import type { PermanenceRef } from '~/utils/globalLocks'
@@ -181,7 +183,29 @@ export function summarizeStudioJob(job: StudioJob) {
}
export function videoJobsBusy() {
return listJobs().some(job => job.kind !== 'edit' && (job.status === 'queued' || job.status === 'uploading' || job.status === 'running'))
if (listJobs().some(job => job.kind !== 'edit' && (job.status === 'queued' || job.status === 'uploading' || job.status === 'running'))) {
return true
}
return listPendingJobs().some(job => Boolean(job.promptId))
}
export function applyPersistedPauseToJob(job: Job) {
const library = job.library
if (!library) return false
const store = readStore(library.ownerKey)
const row = store.jobs.find(item => item.liveJobId === job.id)
|| (library.queueId ? store.jobs.find(item => item.shotQueueId === library.queueId) : undefined)
const queue = library.queueId ? getShotQueue(library.ownerKey, library.queueId) : null
const paused = store.paused === true
|| row?.pauseAfterCurrent === true
|| row?.pausedByUser === true
|| queue?.stopAfterCurrent === true
|| queue?.status === 'paused'
if (paused) {
library.stopAfterCurrent = true
library.queueAutoRun = false
}
return paused
}
export async function addStudioJob(params: {
@@ -370,21 +394,42 @@ export function kickStudioQueue() {
return run
}
function repairStaleJobs(jobs: StudioJob[]) {
function pendingAlive(job: StudioJob) {
if (job.liveJobId && readPendingJob(job.liveJobId)?.promptId) return true
if (!job.shotQueueId) return false
return listPendingJobs().some(pending => (
pending.ownerKey === job.ownerKey
&& pending.queueId === job.shotQueueId
&& Boolean(pending.promptId)
))
}
function repairStaleJobs(jobs: StudioJob[], paused: boolean) {
for (const job of jobs) {
if (job.status !== 'running') continue
const live = job.liveJobId ? getJob(job.liveJobId) : undefined
const liveBusy = live && (live.status === 'queued' || live.status === 'uploading' || live.status === 'running')
if (liveBusy) continue
if (liveBusy || pendingAlive(job)) continue
const holdPause = paused || job.pauseAfterCurrent === true || job.pausedByUser === true
if (job.shotQueueId) {
job.status = 'held'
job.liveJobId = undefined
job.holdForCutIn = true
job.resumeAutoRun = job.payload.queueAutoRun === true || job.resumeAutoRun === true
if (holdPause) {
job.pausedByUser = true
job.pauseAfterCurrent = false
job.holdForCutIn = false
} else {
job.holdForCutIn = true
job.resumeAutoRun = job.payload.queueAutoRun === true || job.resumeAutoRun === true
}
job.updatedAt = Date.now()
} else {
job.status = 'complete'
job.status = holdPause ? 'held' : 'complete'
job.liveJobId = undefined
if (holdPause) {
job.pausedByUser = true
job.pauseAfterCurrent = false
}
job.updatedAt = Date.now()
}
}
@@ -394,7 +439,7 @@ async function dispatchStudioQueue() {
if (videoJobsBusy()) return
for (const owner of listOwnersWithStudioQueues()) {
const store = await mutateStore(owner, (current) => {
repairStaleJobs(current.jobs)
repairStaleJobs(current.jobs, current.paused === true)
const next = pruneDone(current.jobs)
current.jobs.splice(0, current.jobs.length, ...next)
return {
@@ -402,12 +447,12 @@ async function dispatchStudioQueue() {
jobs: current.jobs.map(item => structuredClone(item))
}
})
if (store.paused) return
const cutIn = pickCutIn(store.jobs)
if (cutIn) {
await startStudioJob(cutIn)
return
}
if (store.paused) return
const held = pickHeld(store.jobs)
if (held?.shotQueueId) {
await resumeHeldStudioJob(held)
@@ -606,7 +651,9 @@ export async function onLiveVideoSettled(job: Job) {
: 0
const failed = job.status === 'error' || job.status === 'cancelled'
const snapshot = readStore(owner)
const rowNow = snapshot.jobs.find(item => item.liveJobId === job.id) || snapshot.jobs.find(item => item.status === 'running')
const rowNow = snapshot.jobs.find(item => item.liveJobId === job.id)
|| snapshot.jobs.find(item => item.status === 'running' && item.shotQueueId && item.shotQueueId === job.library?.queueId)
|| snapshot.jobs.find(item => item.status === 'running' && !job.library?.queueId)
const userPause = !failed && (
rowNow?.pauseAfterCurrent === true
|| (snapshot.paused && !rowNow?.holdForCutIn)
@@ -674,6 +721,10 @@ function applyLiveJobPause(live: Job | undefined, pause: boolean, restoreAutoRun
live.library.stopAfterCurrent = pause
if (pause) live.library.queueAutoRun = false
else if (restoreAutoRun) live.library.queueAutoRun = true
patchPendingJob(live.id, {
stopAfterCurrent: pause,
queueAutoRun: live.library.queueAutoRun
})
}
export async function toggleStudioQueuePause(owner: string, options: {
@@ -754,10 +805,16 @@ export async function toggleStudioQueuePause(owner: string, options: {
}
})
const queueId = updated.job?.shotQueueId || options.queueId
if (updated.job?.liveJobId) {
applyLiveJobPause(getJob(updated.job.liveJobId), true)
patchPendingJob(updated.job.liveJobId, { stopAfterCurrent: true, queueAutoRun: false })
}
for (const pending of listPendingJobs()) {
if (pending.ownerKey !== owner) continue
if (queueId && pending.queueId && pending.queueId !== queueId) continue
patchPendingJob(pending.jobId, { stopAfterCurrent: true, queueAutoRun: false })
}
const queueId = updated.job?.shotQueueId || options.queueId
const queued = queueId ? getShotQueue(owner, queueId) : null
if (updated.job && queued?.autoRun) {
await patchStudioJob(owner, updated.job.id, (job) => {