优化动图
This commit is contained in:
@@ -55,3 +55,36 @@ export async function extractFirstFrame(videoBuffer: Buffer): Promise<{ buffer:
|
||||
try { await fs.unlink(outPath); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 ffprobe 获取视频时长(毫秒)
|
||||
*/
|
||||
export async function getVideoDuration(videoBuffer: Buffer): Promise<number | null> {
|
||||
const ffprobeCmd = process.env.FFPROBE_PATH || 'ffprobe';
|
||||
const tmpDir = os.tmpdir();
|
||||
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const inPath = path.join(tmpDir, `${base}.mp4`);
|
||||
|
||||
try {
|
||||
await fs.writeFile(inPath, videoBuffer);
|
||||
const args = [
|
||||
'-v', 'error',
|
||||
'-show_entries', 'format=duration',
|
||||
'-of', 'default=noprint_wrappers=1:nokey=1',
|
||||
inPath,
|
||||
];
|
||||
const { stdout } = await execFileAsync(ffprobeCmd, args, { windowsHide: true });
|
||||
const durationSeconds = parseFloat(stdout.trim());
|
||||
if (isNaN(durationSeconds)) return null;
|
||||
return Math.round(durationSeconds * 1000); // 转换为毫秒
|
||||
} catch (e: any) {
|
||||
if (e && (e.code === 'ENOENT' || /not found|is not recognized/i.test(String(e.message)))) {
|
||||
console.warn('系统未检测到 ffprobe,可安装并配置 PATH 或设置 FFPROBE_PATH 后启用时长提取。');
|
||||
return null;
|
||||
}
|
||||
console.warn(`获取视频时长失败: ${e?.message || e}`);
|
||||
return null;
|
||||
} finally {
|
||||
try { await fs.unlink(inPath); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export async function saveImagePostToDB(
|
||||
context: BrowserContext,
|
||||
aweme: DouyinImageAweme,
|
||||
commentResp: DouyinCommentResponse,
|
||||
uploads: { images: { url: string; width?: number; height?: number, video?: string }[]; musicUrl?: string },
|
||||
uploads: { images: { url: string; width?: number; height?: number; video?: string; duration?: number }[]; musicUrl?: string },
|
||||
rawJson?: any
|
||||
) {
|
||||
if (!aweme?.author?.sec_uid) throw new Error('作者 sec_uid 缺失');
|
||||
@@ -205,7 +205,7 @@ export async function saveImagePostToDB(
|
||||
|
||||
// Upsert ImageFiles(按顺序)
|
||||
for (let i = 0; i < uploads.images.length; i++) {
|
||||
const { url, width, height, video } = uploads.images[i];
|
||||
const { url, width, height, video, duration } = uploads.images[i];
|
||||
await prisma.imageFile.upsert({
|
||||
where: { postId_order: { postId: imagePost.aweme_id, order: i } },
|
||||
create: {
|
||||
@@ -215,12 +215,14 @@ export async function saveImagePostToDB(
|
||||
width: typeof width === 'number' ? width : null,
|
||||
height: typeof height === 'number' ? height : null,
|
||||
animated: video || null,
|
||||
duration: typeof duration === 'number' ? duration : null,
|
||||
},
|
||||
update: {
|
||||
url,
|
||||
width: typeof width === 'number' ? width : null,
|
||||
height: typeof height === 'number' ? height : null,
|
||||
animated: video || null,
|
||||
duration: typeof duration === 'number' ? duration : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { BrowserContext } from 'playwright';
|
||||
import { uploadFile, generateUniqueFileName } from '@/lib/minio';
|
||||
import { downloadBinary } from './network';
|
||||
import { pickFirstUrl } from './utils';
|
||||
import { getVideoDuration } from './media';
|
||||
|
||||
/**
|
||||
* 下载头像并上传到 MinIO,返回外链;失败时回退为原始链接。
|
||||
@@ -29,9 +30,9 @@ export async function uploadAvatarFromUrl(
|
||||
export async function handleImagePost(
|
||||
context: BrowserContext,
|
||||
aweme: DouyinImageAweme
|
||||
): Promise<{ images: { url: string; width?: number; height?: number }[]; musicUrl?: string }> {
|
||||
): Promise<{ images: { url: string; width?: number; height?: number; video?: string; duration?: number }[]; musicUrl?: string }> {
|
||||
const awemeId = aweme.aweme_id;
|
||||
const uploadedImages: { url: string; width?: number; height?: number, video?: string }[] = [];
|
||||
const uploadedImages: { url: string; width?: number; height?: number; video?: string; duration?: number }[] = [];
|
||||
|
||||
// 下载图片(顺序保持)
|
||||
for (let i = 0; i < (aweme.images?.length || 0); i++) {
|
||||
@@ -52,10 +53,25 @@ export async function handleImagePost(
|
||||
const safeVideoExt = videoExt || 'mp4';
|
||||
const videoFileName = generateUniqueFileName(`${awemeId}/${i}_animated.${safeVideoExt}`, 'douyin/images');
|
||||
const uploadedVideo = await uploadFile(videoBuffer, videoFileName, { 'Content-Type': videoContentType });
|
||||
// 将动图的 video URL 也存储起来
|
||||
uploadedImages.push({ url: uploaded, width: img?.width, height: img?.height, video: uploadedVideo });
|
||||
|
||||
// 获取动图时长
|
||||
const duration = await getVideoDuration(videoBuffer);
|
||||
|
||||
// 将动图的 video URL 和 duration 也存储起来
|
||||
uploadedImages.push({
|
||||
url: uploaded,
|
||||
width: img?.width,
|
||||
height: img?.height,
|
||||
video: uploadedVideo,
|
||||
duration: duration ?? undefined
|
||||
});
|
||||
|
||||
if (duration) {
|
||||
console.log(`[image] 动图 ${i} 时长: ${duration}ms`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[image] 动图视频上传失败,跳过:`, (e as Error)?.message || e);
|
||||
uploadedImages.push({ url: uploaded, width: img?.width, height: img?.height });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user