Align preview typography with FFmpeg output, add video/song title split, and ship OSS updates.

Separate YouTube video titles from on-video song/artist fields with Pro gating, serve curated fonts and watermark assets for 1:1 preview parity, and include billing/API/docs/deploy stack for self-hosted release.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Atakan Doğan Özban
2026-07-27 16:26:19 +02:00
co-authored by Cursor
parent 682020ff5a
commit c8015937f9
206 changed files with 37078 additions and 638 deletions
+69 -14
View File
@@ -1,14 +1,15 @@
import { Worker } from "bullmq";
import path from "path";
import { JobItemStatus, JobStatus } from "@prisma/client";
import { QUEUE_NAME } from "./lib/constants";
import { prisma } from "./lib/db";
import { cleanupFiles, encodeVideo } from "./lib/ffmpeg/encode";
import { getRedisConnection } from "./lib/queue/client";
import { incrementQuota } from "./lib/quota";
import { getJobDir } from "./lib/storage";
import type { VideoJobData } from "./lib/types";
import { uploadToYouTube } from "./lib/youtube/upload";
import { QUEUE_NAME } from "../lib/constants";
import { prisma } from "../lib/db";
import { cleanupFiles, encodeVideo, getFfmpegPath } from "../lib/ffmpeg/encode";
import { getRedisConnection } from "../lib/queue/client";
import { releaseReservation } from "../lib/quota";
import { getJobDir } from "../lib/storage";
import type { VideoJobData } from "../lib/types";
import { formatYouTubeErrorForUser } from "../lib/youtube/errors";
import { uploadToYouTube } from "../lib/youtube/upload";
async function updateJobStatus(jobId: string) {
const items = await prisma.jobItem.findMany({ where: { jobId } });
@@ -30,7 +31,6 @@ async function updateJobStatus(jobId: string) {
completedAt: completed + failed === total ? new Date() : null,
},
});
}
async function processJobItem(data: VideoJobData) {
@@ -39,6 +39,18 @@ async function processJobItem(data: VideoJobData) {
include: { job: true },
});
if (item.job.userId !== data.userId || item.jobId !== data.jobId) {
const message = "Job ownership mismatch";
console.error(`[security] ${message} for item ${item.id}`);
await prisma.jobItem.update({
where: { id: item.id },
data: { status: JobItemStatus.FAILED, error: message },
});
await releaseReservation(item.job.userId, item.billingSource, 1).catch(() => {});
await updateJobStatus(item.jobId);
throw new Error(message);
}
const jobDir = getJobDir(data.userId, data.jobId);
const outputPath = path.join(jobDir, `${item.id}.mp4`);
@@ -53,11 +65,51 @@ async function processJobItem(data: VideoJobData) {
});
await encodeVideo({
imagePath: item.job.imagePath,
imagePath: item.itemImagePath || item.job.imagePath,
audioPath: item.audioPath,
outputPath,
resolution: item.resolution,
includeWatermark: item.includeWatermark,
songTitle: item.songTitle || item.title,
artist: item.artist,
layout: item.layoutTemplate
? {
template: item.layoutTemplate as
| "COVER_LEFT_TEXT_RIGHT"
| "COVER_TOP_TEXT_BOTTOM"
| "COVER_RIGHT_TEXT_LEFT"
| "CENTERED_COMPACT",
blurAmount: item.blurAmount ?? 55,
blurOpacity: item.blurOpacity ?? 100,
textPadding: item.textPadding ?? 48,
titleArtistGap: item.titleArtistGap ?? 10,
textOffsetX: item.textOffsetX ?? 0,
textOffsetY: item.textOffsetY ?? 0,
}
: null,
watermark: {
mode: (item.watermarkMode as "none" | "default" | "text" | "logo") || "default",
text: item.watermarkText,
logoPath: item.watermarkLogoPath,
fontKey: (item.watermarkFontKey as
| "system"
| "custom"
| "inter"
| "montserrat"
| "roboto"
| "oswald"
| "playfair") || "system",
fontPath: item.watermarkFontPath,
position:
(item.watermarkPosition as
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right"
| "center") || "bottom-right",
offsetX: item.watermarkOffsetX ?? 20,
offsetY: item.watermarkOffsetY ?? 20,
},
});
await prisma.jobItem.update({
@@ -72,18 +124,21 @@ async function processJobItem(data: VideoJobData) {
data: {
status: JobItemStatus.COMPLETED,
youtubeVideoId,
error: null,
},
});
await incrementQuota(data.userId, 1);
// Quota was reserved at job creation; do not increment again on success.
await cleanupFiles([outputPath]);
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
const message = formatYouTubeErrorForUser(err);
console.error(`[youtube] Job item ${item.id} ("${item.title}") failed: ${message}`);
await prisma.jobItem.update({
where: { id: item.id },
data: { status: JobItemStatus.FAILED, error: message },
});
throw err;
await releaseReservation(data.userId, item.billingSource, 1).catch(() => {});
throw new Error(message);
} finally {
await updateJobStatus(data.jobId);
}
@@ -108,4 +163,4 @@ worker.on("failed", (job, err) => {
console.error(`Job item ${job?.data.jobItemId} failed:`, err.message);
});
console.log("s2yt worker started, waiting for jobs...");
console.log(`Songs2VID worker started (ffmpeg: ${getFfmpegPath()}), waiting for jobs...`);