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:
+67
-10
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user