fix(video): make publishing transitions safe
This commit is contained in:
+78
-102
@@ -6,8 +6,13 @@ import {
|
||||
type TiktokSettings,
|
||||
type YoutubeSettings,
|
||||
} from "@/db/schema";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
||||
import { sse } from "@/lib/realtime/sse";
|
||||
import {
|
||||
createVideoPublishingModule,
|
||||
type VideoPublishJobRecord,
|
||||
type VideoPublishUploadRecord,
|
||||
} from "@/lib/video/video-publishing";
|
||||
|
||||
const VIDEO_TYPES = new Set(["video/mp4", "video/quicktime", "video/webm"]);
|
||||
|
||||
@@ -330,105 +335,76 @@ async function publishTiktok(upload: typeof videoUploads.$inferSelect) {
|
||||
return { id: initData.data.publish_id, url: "https://www.tiktok.com/" };
|
||||
}
|
||||
|
||||
export async function publishVideoJob(jobId: string) {
|
||||
const job = await db.query.videoUploadJobs.findFirst({
|
||||
where: (table, { eq }) => eq(table.id, jobId),
|
||||
});
|
||||
if (!job || job.status === "completed") return;
|
||||
const upload = await db.query.videoUploads.findFirst({
|
||||
where: (table, { eq }) => eq(table.id, job.uploadId),
|
||||
});
|
||||
if (!upload) throw new Error("Upload record not found.");
|
||||
await db
|
||||
.update(videoUploadJobs)
|
||||
.set({
|
||||
status: "processing",
|
||||
attempts: job.attempts + 1,
|
||||
startedAt: new Date(),
|
||||
error: null,
|
||||
})
|
||||
.where(eq(videoUploadJobs.id, job.id));
|
||||
await sse.uploads.pub("update", {
|
||||
uploadId: upload.id,
|
||||
jobId: job.id,
|
||||
platform: job.platform,
|
||||
status: "processing",
|
||||
});
|
||||
try {
|
||||
const result =
|
||||
job.platform === "youtube"
|
||||
? await publishYoutube(upload)
|
||||
: await publishTiktok(upload);
|
||||
await db
|
||||
.update(videoUploadJobs)
|
||||
.set({
|
||||
status: "completed",
|
||||
externalId: result.id,
|
||||
externalUrl: result.url,
|
||||
completedAt: new Date(),
|
||||
})
|
||||
.where(eq(videoUploadJobs.id, job.id));
|
||||
await sse.uploads.pub("update", {
|
||||
uploadId: upload.id,
|
||||
jobId: job.id,
|
||||
platform: job.platform,
|
||||
status: "completed",
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Platform upload failed.";
|
||||
await db
|
||||
.update(videoUploadJobs)
|
||||
.set({ status: "failed", error: message })
|
||||
.where(eq(videoUploadJobs.id, job.id));
|
||||
await sse.uploads.pub("update", {
|
||||
uploadId: upload.id,
|
||||
jobId: job.id,
|
||||
platform: job.platform,
|
||||
status: "failed",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
type DatabaseVideoUpload = typeof videoUploads.$inferSelect;
|
||||
type DatabaseVideoJob = typeof videoUploadJobs.$inferSelect;
|
||||
|
||||
export async function processDueVideoUploads() {
|
||||
const due = await db.query.videoUploads.findMany({
|
||||
where: (table, { eq, lte, or, isNull }) =>
|
||||
and(
|
||||
or(eq(table.status, "scheduled"), eq(table.status, "partial")),
|
||||
or(isNull(table.scheduledAt), lte(table.scheduledAt, new Date())),
|
||||
),
|
||||
orderBy: [desc(videoUploads.createdAt)],
|
||||
});
|
||||
for (const upload of due) {
|
||||
const jobs = await db.query.videoUploadJobs.findMany({
|
||||
where: (table, { eq, and, ne }) =>
|
||||
and(eq(table.uploadId, upload.id), ne(table.status, "completed")),
|
||||
});
|
||||
for (const job of jobs) {
|
||||
await publishVideoJob(job.id).catch((error) => {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Platform upload failed.";
|
||||
console.error(`[${job.platform}] ${job.id}: ${message}`);
|
||||
});
|
||||
}
|
||||
const remaining = await db.query.videoUploadJobs.findMany({
|
||||
where: (table, { eq, and, ne }) =>
|
||||
and(eq(table.uploadId, upload.id), ne(table.status, "completed")),
|
||||
});
|
||||
const nextStatus =
|
||||
remaining.length === 0
|
||||
? "completed"
|
||||
: remaining.some((job) => job.status === "completed")
|
||||
? "partial"
|
||||
: "failed";
|
||||
await db
|
||||
.update(videoUploads)
|
||||
.set({ status: nextStatus, updatedAt: new Date() })
|
||||
.where(eq(videoUploads.id, upload.id));
|
||||
await sse.uploads.pub("update", {
|
||||
uploadId: upload.id,
|
||||
status: nextStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
const videoPublishing = createVideoPublishingModule<
|
||||
DatabaseVideoUpload & VideoPublishUploadRecord,
|
||||
DatabaseVideoJob & VideoPublishJobRecord
|
||||
>({
|
||||
store: {
|
||||
claimJob: async (jobId, startedAt) => {
|
||||
const [job] = await db
|
||||
.update(videoUploadJobs)
|
||||
.set({
|
||||
status: "processing",
|
||||
attempts: sql`${videoUploadJobs.attempts} + 1`,
|
||||
startedAt,
|
||||
error: null,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(videoUploadJobs.id, jobId),
|
||||
inArray(videoUploadJobs.status, ["pending", "failed"]),
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
return job ?? null;
|
||||
},
|
||||
findUpload: async (uploadId) => (await db.query.videoUploads.findFirst({
|
||||
where: (table, { eq }) => eq(table.id, uploadId),
|
||||
})) ?? null,
|
||||
markCompleted: async (job, result, completedAt) => {
|
||||
await db
|
||||
.update(videoUploadJobs)
|
||||
.set({
|
||||
status: "completed",
|
||||
externalId: result.id,
|
||||
externalUrl: result.url,
|
||||
completedAt,
|
||||
})
|
||||
.where(eq(videoUploadJobs.id, job.id));
|
||||
},
|
||||
markFailed: async (job, message) => {
|
||||
await db
|
||||
.update(videoUploadJobs)
|
||||
.set({ status: "failed", error: message })
|
||||
.where(eq(videoUploadJobs.id, job.id));
|
||||
},
|
||||
findDueUploads: (now) => db.query.videoUploads.findMany({
|
||||
where: (table, { eq, lte, or, isNull }) =>
|
||||
and(
|
||||
or(eq(table.status, "scheduled"), eq(table.status, "partial")),
|
||||
or(isNull(table.scheduledAt), lte(table.scheduledAt, now)),
|
||||
),
|
||||
orderBy: [desc(videoUploads.createdAt)],
|
||||
}),
|
||||
findJobs: (uploadId) => db.query.videoUploadJobs.findMany({
|
||||
where: (table, { eq }) => eq(table.uploadId, uploadId),
|
||||
}),
|
||||
updateUploadStatus: async (upload, status, updatedAt) => {
|
||||
await db
|
||||
.update(videoUploads)
|
||||
.set({ status, updatedAt })
|
||||
.where(eq(videoUploads.id, upload.id));
|
||||
},
|
||||
},
|
||||
publishers: {
|
||||
youtube: publishYoutube,
|
||||
tiktok: publishTiktok,
|
||||
},
|
||||
notify: (event) => sse.uploads.pub("update", event),
|
||||
});
|
||||
|
||||
export const publishVideoJob = videoPublishing.publishJob;
|
||||
export const processDueVideoUploads = videoPublishing.processDue;
|
||||
|
||||
Reference in New Issue
Block a user