lovart3/lib/storage/video-archiver.ts
2025-12-17 20:15:18 +08:00

190 lines
4.9 KiB
TypeScript
Raw 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.

/**
* 视频归档服务
* 功能:下载临时视频URL,上传到S3,更新数据库记录
*/
import { db } from "@/lib/db";
import { message } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { uploadFile } from "@/lib/storage/service";
import type { VideoMetadata } from "@/types/ai";
interface VideoArchiveOptions {
messageId: string;
temporaryUrl: string;
taskId: string;
metadata?: {
duration?: number;
resolution?: string;
ratio?: string;
};
}
export class VideoArchiver {
/**
* 归档视频:下载 → 上传S3 → 更新数据库
*/
async archiveVideo(options: VideoArchiveOptions): Promise<string> {
const { messageId, temporaryUrl, taskId, metadata } = options;
try {
console.log(`[VideoArchiver] 📹 开始归档视频:${messageId}`);
// 1. 更新状态为"归档中"
await this.updateVideoStatus(messageId, "archiving");
// 2. 下载视频
console.log(`[VideoArchiver] ⬇️ 下载视频:${temporaryUrl}`);
const videoBuffer = await this.downloadVideo(temporaryUrl);
if (!videoBuffer) {
throw new Error("Failed to download video from temporary URL");
}
// 3. 生成文件名
const filename = this.generateVideoFilename(messageId, taskId);
// 4. 上传到S3(使用项目统一存储服务)
console.log(`[VideoArchiver] ⬆️ 上传视频到S3:${filename}`);
const permanentUrl = await this.uploadVideoToS3(videoBuffer, filename);
// 5. 更新数据库
await this.updateVideoUrls(messageId, {
permanentUrl,
fileSize: videoBuffer.length,
...metadata,
});
console.log(`[VideoArchiver] ✅ 视频归档完成:${permanentUrl}`);
return permanentUrl;
} catch (error) {
console.error(`[VideoArchiver] ❌ 归档失败:`, error);
// 更新状态为"失败"
await this.updateVideoStatus(messageId, "failed");
throw error;
}
}
/**
* 下载视频(带重试机制)
*/
private async downloadVideo(
url: string,
retries = 3
): Promise<Buffer | null> {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, {
headers: {
"User-Agent": "AI-Chatbot-Video-Archiver/1.0",
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
} catch (error) {
console.error(
`[VideoArchiver] 下载失败 (尝试 ${i + 1}/${retries}):`,
error
);
if (i === retries - 1) {
return null;
}
// 等待后重试(指数退避)
await new Promise((resolve) =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
}
}
return null;
}
/**
* 上传视频到S3(使用项目统一存储服务)
*/
private async uploadVideoToS3(
buffer: Buffer,
filename: string
): Promise<string> {
// 直接使用项目已有的 uploadFile 方法
// 支持:数据库配置的S3 / 环境变量S3 / Vercel Blob
const result = await uploadFile(filename, buffer as any);
console.log(`[VideoArchiver] 视频已上传到S3:${result.url}`);
return result.url;
}
/**
* 生成视频文件名
* 项目的 uploadFile 会自动生成 uploads/timestamp-random.ext 格式
*/
private generateVideoFilename(messageId: string, taskId: string): string {
// 使用唯一标识作为文件名
return `video-${messageId}-${taskId}.mp4`;
}
/**
* 更新视频状态
*/
private async updateVideoStatus(
messageId: string,
status: "temporary" | "archiving" | "archived" | "failed"
) {
await db
.update(message)
.set({ videoStorageStatus: status })
.where(eq(message.id, messageId));
}
/**
* 更新视频URL和元数据
*/
private async updateVideoUrls(
messageId: string,
data: {
permanentUrl: string;
fileSize: number;
duration?: number;
resolution?: string;
ratio?: string;
}
) {
// 获取现有的 videoMetadata
const existing = await db.query.message.findFirst({
where: eq(message.id, messageId),
});
const existingMetadata = (existing?.videoMetadata as VideoMetadata) || {};
await db
.update(message)
.set({
permanentVideoUrl: data.permanentUrl,
videoStorageStatus: "archived",
archivedAt: new Date(),
videoMetadata: {
...existingMetadata,
fileSize: data.fileSize,
duration: data.duration,
resolution: data.resolution,
ratio: data.ratio,
} as any,
})
.where(eq(message.id, messageId));
}
}
// 单例模式
export const videoArchiver = new VideoArchiver();