2026-04-07 21:58:18 -04:00
|
|
|
import 'dotenv/config';
|
|
|
|
|
import { Worker } from 'bullmq';
|
|
|
|
|
import { proxyWorker } from './workers/proxy.js';
|
|
|
|
|
import { thumbnailWorker } from './workers/thumbnail.js';
|
|
|
|
|
import { conformWorker } from './workers/conform.js';
|
2026-05-23 16:05:41 -04:00
|
|
|
import { youtubeImportWorker } from './workers/youtube-import.js';
|
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
|
|
|
import { trimWorker } from './workers/trimWorker.js';
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
import { startPromotionWorker } from './workers/promotion.js';
|
2026-04-07 21:58:18 -04:00
|
|
|
|
|
|
|
|
const parseRedisUrl = (url) => {
|
|
|
|
|
const parsed = new URL(url);
|
|
|
|
|
return {
|
|
|
|
|
host: parsed.hostname,
|
|
|
|
|
port: parseInt(parsed.port, 10),
|
|
|
|
|
password: parsed.password || undefined,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const redisOptions = parseRedisUrl(process.env.REDIS_URL || 'redis://localhost:6379');
|
|
|
|
|
|
2026-05-23 16:05:41 -04:00
|
|
|
const createWorker = (queueName, handler, overrides = {}) => {
|
2026-05-17 19:10:08 -04:00
|
|
|
const worker = new Worker(queueName, handler, {
|
|
|
|
|
connection: redisOptions,
|
|
|
|
|
// Stall detection: if a worker dies mid-job, BullMQ moves it back to wait
|
|
|
|
|
// after stalledInterval. Without this a crashed run sits in active forever.
|
|
|
|
|
stalledInterval: 30000,
|
|
|
|
|
maxStalledCount: 1,
|
|
|
|
|
lockDuration: 60000,
|
|
|
|
|
lockRenewTime: 15000,
|
2026-05-23 16:05:41 -04:00
|
|
|
...overrides,
|
2026-05-17 19:10:08 -04:00
|
|
|
});
|
2026-04-07 21:58:18 -04:00
|
|
|
|
|
|
|
|
worker.on('completed', (job) => {
|
|
|
|
|
console.log(`[${queueName}] Job ${job.id} completed`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
worker.on('failed', (job, err) => {
|
|
|
|
|
console.error(`[${queueName}] Job ${job.id} failed:`, err.message);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-17 19:10:08 -04:00
|
|
|
worker.on('stalled', (jobId) => {
|
|
|
|
|
console.warn(`[${queueName}] Job ${jobId} stalled — reclaimed`);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-16 00:46:53 -04:00
|
|
|
worker.on('progress', (job, progress) => {
|
|
|
|
|
console.log(`[${queueName}] Job ${job.id} progress:`, progress);
|
2026-04-07 21:58:18 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return worker;
|
|
|
|
|
};
|
|
|
|
|
|
fix(jobs): real cancel for active jobs + multi-threaded thumbnail worker
DELETE /jobs/:id was throwing "404 not found" when the operator tried to
cancel a running job. BullMQ refuses job.remove() while a job is in the
active state; the route caught that error and fell through to the
404 branch, which was misleading because the job actually exists — the
queue was just refusing to drop it from under the worker.
Fix:
- Detect 'active' state explicitly and call moveToFailed(err, '0', false)
first. Token '0' bypasses the per-worker lock check (the operator-side
cancel doesn't hold the worker lock). That transitions active -> failed
and frees the queue's concurrency slot.
- If moveToFailed itself fails (lock owned by a live worker), fall back
to job.discard() so at least the result is thrown away.
- If remove() then fails (stalled, broken state), drop the job's Redis
key directly via queue.client. Last-resort obliteration.
- Stop swallowing getJob() errors — if Redis is sad, surface it via
next(err) instead of returning a misleading 404.
- Return { cancelled: true } when the job was active, so the client
can show "Cancelled" rather than "Removed" in any future toast.
While here: thumbnail jobs now run with concurrency 4 by default
(proxy 2, conform 1, import 1 unchanged). Every queue defaulted to
concurrency 1 before, so a single stalled job blocked the entire queue.
All three are overridable via PROXY_CONCURRENCY / THUMBNAIL_CONCURRENCY
/ CONFORM_CONCURRENCY env vars for nodes with more headroom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:23:07 -04:00
|
|
|
// Per-queue concurrency. Defaults to 1, which serialises every job in a
|
|
|
|
|
// queue — meaning a single stalled job blocks every other one. We want
|
|
|
|
|
// thumbnails (cheap, parallel-safe) to run several at a time so a slow
|
|
|
|
|
// outlier doesn't back the rest of the catalog up. Proxy + conform are
|
|
|
|
|
// heavier (ffmpeg transcode) so we keep them lower to avoid trashing
|
|
|
|
|
// the box; tune via env if a node has more headroom.
|
|
|
|
|
const PROXY_CONCURRENCY = parseInt(process.env.PROXY_CONCURRENCY || '2', 10);
|
|
|
|
|
const THUMBNAIL_CONCURRENCY = parseInt(process.env.THUMBNAIL_CONCURRENCY || '4', 10);
|
|
|
|
|
const CONFORM_CONCURRENCY = parseInt(process.env.CONFORM_CONCURRENCY || '1', 10);
|
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
|
|
|
const TRIM_CONCURRENCY = parseInt(process.env.TRIM_CONCURRENCY || '4', 10);
|
fix(jobs): real cancel for active jobs + multi-threaded thumbnail worker
DELETE /jobs/:id was throwing "404 not found" when the operator tried to
cancel a running job. BullMQ refuses job.remove() while a job is in the
active state; the route caught that error and fell through to the
404 branch, which was misleading because the job actually exists — the
queue was just refusing to drop it from under the worker.
Fix:
- Detect 'active' state explicitly and call moveToFailed(err, '0', false)
first. Token '0' bypasses the per-worker lock check (the operator-side
cancel doesn't hold the worker lock). That transitions active -> failed
and frees the queue's concurrency slot.
- If moveToFailed itself fails (lock owned by a live worker), fall back
to job.discard() so at least the result is thrown away.
- If remove() then fails (stalled, broken state), drop the job's Redis
key directly via queue.client. Last-resort obliteration.
- Stop swallowing getJob() errors — if Redis is sad, surface it via
next(err) instead of returning a misleading 404.
- Return { cancelled: true } when the job was active, so the client
can show "Cancelled" rather than "Removed" in any future toast.
While here: thumbnail jobs now run with concurrency 4 by default
(proxy 2, conform 1, import 1 unchanged). Every queue defaulted to
concurrency 1 before, so a single stalled job blocked the entire queue.
All three are overridable via PROXY_CONCURRENCY / THUMBNAIL_CONCURRENCY
/ CONFORM_CONCURRENCY env vars for nodes with more headroom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:23:07 -04:00
|
|
|
|
2026-04-07 21:58:18 -04:00
|
|
|
const workers = [
|
fix(jobs): real cancel for active jobs + multi-threaded thumbnail worker
DELETE /jobs/:id was throwing "404 not found" when the operator tried to
cancel a running job. BullMQ refuses job.remove() while a job is in the
active state; the route caught that error and fell through to the
404 branch, which was misleading because the job actually exists — the
queue was just refusing to drop it from under the worker.
Fix:
- Detect 'active' state explicitly and call moveToFailed(err, '0', false)
first. Token '0' bypasses the per-worker lock check (the operator-side
cancel doesn't hold the worker lock). That transitions active -> failed
and frees the queue's concurrency slot.
- If moveToFailed itself fails (lock owned by a live worker), fall back
to job.discard() so at least the result is thrown away.
- If remove() then fails (stalled, broken state), drop the job's Redis
key directly via queue.client. Last-resort obliteration.
- Stop swallowing getJob() errors — if Redis is sad, surface it via
next(err) instead of returning a misleading 404.
- Return { cancelled: true } when the job was active, so the client
can show "Cancelled" rather than "Removed" in any future toast.
While here: thumbnail jobs now run with concurrency 4 by default
(proxy 2, conform 1, import 1 unchanged). Every queue defaulted to
concurrency 1 before, so a single stalled job blocked the entire queue.
All three are overridable via PROXY_CONCURRENCY / THUMBNAIL_CONCURRENCY
/ CONFORM_CONCURRENCY env vars for nodes with more headroom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:23:07 -04:00
|
|
|
createWorker('proxy', proxyWorker, { concurrency: PROXY_CONCURRENCY }),
|
|
|
|
|
createWorker('thumbnail', thumbnailWorker, { concurrency: THUMBNAIL_CONCURRENCY }),
|
|
|
|
|
createWorker('conform', conformWorker, { concurrency: CONFORM_CONCURRENCY }),
|
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
|
|
|
createWorker('trim', trimWorker, { concurrency: TRIM_CONCURRENCY }),
|
2026-05-23 16:05:41 -04:00
|
|
|
// YouTube imports: keep concurrency at 1 so we don't burn through rate
|
|
|
|
|
// limits when several jobs land back-to-back. Lock window is longer than
|
|
|
|
|
// the default because a long video download can run for minutes.
|
|
|
|
|
createWorker('import', youtubeImportWorker, {
|
|
|
|
|
concurrency: 1,
|
|
|
|
|
lockDuration: 10 * 60 * 1000,
|
|
|
|
|
lockRenewTime: 60000,
|
|
|
|
|
}),
|
2026-04-07 21:58:18 -04:00
|
|
|
];
|
|
|
|
|
|
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
|
|
|
console.log(`Concurrency: proxy=${PROXY_CONCURRENCY} thumbnail=${THUMBNAIL_CONCURRENCY} conform=${CONFORM_CONCURRENCY} trim=${TRIM_CONCURRENCY} import=1`);
|
fix(jobs): real cancel for active jobs + multi-threaded thumbnail worker
DELETE /jobs/:id was throwing "404 not found" when the operator tried to
cancel a running job. BullMQ refuses job.remove() while a job is in the
active state; the route caught that error and fell through to the
404 branch, which was misleading because the job actually exists — the
queue was just refusing to drop it from under the worker.
Fix:
- Detect 'active' state explicitly and call moveToFailed(err, '0', false)
first. Token '0' bypasses the per-worker lock check (the operator-side
cancel doesn't hold the worker lock). That transitions active -> failed
and frees the queue's concurrency slot.
- If moveToFailed itself fails (lock owned by a live worker), fall back
to job.discard() so at least the result is thrown away.
- If remove() then fails (stalled, broken state), drop the job's Redis
key directly via queue.client. Last-resort obliteration.
- Stop swallowing getJob() errors — if Redis is sad, surface it via
next(err) instead of returning a misleading 404.
- Return { cancelled: true } when the job was active, so the client
can show "Cancelled" rather than "Removed" in any future toast.
While here: thumbnail jobs now run with concurrency 4 by default
(proxy 2, conform 1, import 1 unchanged). Every queue defaulted to
concurrency 1 before, so a single stalled job blocked the entire queue.
All three are overridable via PROXY_CONCURRENCY / THUMBNAIL_CONCURRENCY
/ CONFORM_CONCURRENCY env vars for nodes with more headroom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:23:07 -04:00
|
|
|
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
startPromotionWorker();
|
|
|
|
|
|
2026-04-07 21:58:18 -04:00
|
|
|
console.log('Wild Dragon Worker Service started');
|
|
|
|
|
console.log(`Redis: ${redisOptions.host}:${redisOptions.port}`);
|
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
|
|
|
console.log('Active queues: proxy, thumbnail, conform, trim, import');
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
console.log('Background scans: promotion (growing-files → S3)');
|
2026-04-07 21:58:18 -04:00
|
|
|
|
|
|
|
|
process.on('SIGTERM', async () => {
|
|
|
|
|
console.log('SIGTERM received, shutting down...');
|
|
|
|
|
await Promise.all(workers.map(w => w.close()));
|
|
|
|
|
process.exit(0);
|
|
|
|
|
});
|