lovart3/lib/jobs/video-archive-job.ts
2025-12-17 20:15:18 +08:00

152 lines
4.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 视频归档定时任务
* 功能:定期查询需要归档的视频并执行归档操作
* 触发条件:创建时间超过20分钟且状态为 temporary
*/
import { db } from "@/lib/db";
import { message } from "@/lib/db/schema";
import { and, lte, isNull, eq } from "drizzle-orm";
import { videoArchiver } from "@/lib/storage/video-archiver";
import type { VideoMetadata } from "@/types/ai";
/**
* 查询需要归档的视频消息
* 条件:
* 1. 视频状态为 'temporary'(临时)
* 2. 创建时间超过20分钟
* 3. 还没有永久URL
*/
export async function getVideosToArchive() {
const twentyMinutesAgo = new Date(Date.now() - 20 * 60 * 1000);
return await db.query.message.findMany({
where: and(
eq(message.videoStorageStatus, "temporary"),
lte(message.createdAt, twentyMinutesAgo),
isNull(message.permanentVideoUrl)
),
limit: 20, // 每次处理20个,20分钟周期可以处理更多
});
}
/**
* 从消息中提取视频数据
*/
function extractVideoDataFromMessage(msg: any) {
try {
if (msg.temporaryVideoUrl) {
const metadata = msg.videoMetadata as VideoMetadata;
return {
temporaryUrl: msg.temporaryVideoUrl,
taskId: metadata?.taskId || msg.id,
metadata: {
duration: metadata?.duration,
resolution: metadata?.resolution,
ratio: metadata?.ratio,
},
};
}
return null;
} catch (error) {
console.error("[VideoArchiveJob] 提取视频数据失败:", error);
return null;
}
}
/**
* 执行视频归档任务
*/
export async function runVideoArchiveJob() {
console.log("[VideoArchiveJob] ⏰ 开始执行视频归档任务...");
try {
// 1. 查询待归档的视频
const videos = await getVideosToArchive();
if (videos.length === 0) {
console.log("[VideoArchiveJob] 📭 没有需要归档的视频");
return {
success: true,
message: "No videos to archive",
total: 0,
succeeded: 0,
failed: 0,
};
}
console.log(`[VideoArchiveJob] 📹 发现 ${videos.length} 个待归档视频`);
// 2. 并发归档(控制并发数为3)
const concurrency = 3;
const results = [];
for (let i = 0; i < videos.length; i += concurrency) {
const batch = videos.slice(i, i + concurrency);
const batchPromises = batch.map(async (video) => {
try {
// 从消息内容中提取视频URL和元数据
const videoData = extractVideoDataFromMessage(video);
if (!videoData) {
console.warn(
`[VideoArchiveJob] ⚠️ 消息 ${video.id} 没有视频数据`
);
return { success: false, messageId: video.id };
}
// 执行归档
const permanentUrl = await videoArchiver.archiveVideo({
messageId: video.id,
temporaryUrl: videoData.temporaryUrl,
taskId: videoData.taskId,
metadata: videoData.metadata,
});
return { success: true, messageId: video.id, permanentUrl };
} catch (error) {
console.error(
`[VideoArchiveJob] ❌ 归档失败 (${video.id}):`,
error
);
return { success: false, messageId: video.id, error };
}
});
const batchResults = await Promise.allSettled(batchPromises);
results.push(...batchResults);
}
// 3. 统计结果
const succeeded = results.filter(
(r) => r.status === "fulfilled" && r.value.success
).length;
const failed = results.length - succeeded;
console.log(
`[VideoArchiveJob] ✅ 完成:成功 ${succeeded},失败 ${failed}`
);
return {
success: true,
message: "Video archive job completed",
total: results.length,
succeeded,
failed,
};
} catch (error) {
console.error("[VideoArchiveJob] ❌ 任务执行出错:", error);
return {
success: false,
message: `Job failed: ${error}`,
total: 0,
succeeded: 0,
failed: 0,
};
}
}