/** * Backfill HLS transcoding for existing videos. * * Finds every Video record that has never been queued for HLS (hlsStatus is * NULL) and is otherwise transcodable, then enqueues a transcode job for * each. Idempotent — re-runs only pick up still-NULL rows. Skips invalid or * zero-duration videos. * * Bypasses the ENABLE_HLS_TRANSCODE flag (calls forceSubmitTranscode) * because the flag is meant to gate the *upload-time* enqueue; once an * operator runs this script they're explicitly asking for transcoding. * * Usage: * docker compose exec api tsx scripts/backfill-hls.ts * # or after building: * docker compose exec api node dist/scripts/backfill-hls.js */ import { prisma } from '../src/config/database'; import { hlsTranscodeQueueService } from '../src/services/hls-transcode-queue.service'; import { logger } from '../src/utils/logger'; async function main() { const candidates = await prisma.video.findMany({ where: { hlsStatus: null, isValid: true, durationSeconds: { gt: 0 }, width: { gt: 0 }, height: { gt: 0 }, }, select: { id: true, filename: true, durationSeconds: true }, orderBy: { id: 'asc' }, }); if (candidates.length === 0) { logger.info('[backfill-hls] No videos require HLS transcoding.'); process.exit(0); } logger.info(`[backfill-hls] Enqueueing ${candidates.length} video(s) for HLS transcoding`); let enqueued = 0; for (const video of candidates) { try { const jobId = await hlsTranscodeQueueService.forceSubmitTranscode(video.id); enqueued++; logger.info(`[backfill-hls] Enqueued video ${video.id} (${video.filename}) → job ${jobId}`); } catch (err) { logger.error(`[backfill-hls] Failed to enqueue video ${video.id}: ${err instanceof Error ? err.message : String(err)}`); } } logger.info(`[backfill-hls] Done. ${enqueued}/${candidates.length} jobs enqueued. Worker concurrency is 1, so total wall time depends on per-video transcode duration (~2 min per 1080p video).`); // Give BullMQ a moment to flush, then exit cleanly. await new Promise((r) => setTimeout(r, 500)); await hlsTranscodeQueueService.close(); await prisma.$disconnect(); process.exit(0); } main().catch(async (err) => { logger.error(`[backfill-hls] Fatal: ${err instanceof Error ? err.stack : String(err)}`); await prisma.$disconnect().catch(() => {}); process.exit(1); });