export const runtime = "nodejs"; import type { BrowserContext } from "playwright"; import { uploadFile, generateUniqueFileName } from "@/lib/minio"; import { downloadBinary } from "./network"; import { pickFirstUrl } from "./utils"; import { getVideoDuration } from "@/app/api/media"; /** * 下载头像并上传到 MinIO,返回外链;失败时回退为原始链接。 */ export async function uploadAvatarFromUrl( context: BrowserContext, srcUrl?: string | null, nameHint?: string, ): Promise { if (!srcUrl) return undefined; try { const { buffer, contentType, ext } = await downloadBinary(context, srcUrl); const safeExt = ext || "jpg"; const baseName = nameHint ? `${nameHint}.${safeExt}` : `avatar.${safeExt}`; const fileName = generateUniqueFileName(baseName, "douyin/avatars"); const uploaded = await uploadFile(buffer, fileName, { "Content-Type": contentType, }); return uploaded; } catch (e) { console.warn( "[avatar] 上传失败,使用原始链接:", (e as Error)?.message || e, ); return srcUrl || undefined; } } /** * 下载任意图片并上传到 MinIO,返回外链;失败时回退为原始链接。 */ export async function uploadImageFromUrl( context: BrowserContext, srcUrl?: string | null, nameHint?: string, ): Promise { if (!srcUrl) return undefined; try { const { buffer, contentType, ext } = await downloadBinary(context, srcUrl); const safeExt = ext || "jpg"; const baseName = nameHint ? `${nameHint}.${safeExt}` : `image.${safeExt}`; const fileName = generateUniqueFileName(baseName, "douyin/comment-images"); const uploaded = await uploadFile(buffer, fileName, { "Content-Type": contentType, }); return uploaded; } catch (e) { console.warn( "[image] 上传失败,使用原始链接:", (e as Error)?.message || e, ); return srcUrl || undefined; } } /** 下载图文作品的图片和音乐并上传到 MinIO */ export async function handleImagePost( context: BrowserContext, aweme: DouyinImageAweme, ): 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; duration?: number; }[] = []; // 下载图片(顺序保持) for (let i = 0; i < (aweme.images?.length || 0); i++) { const img = aweme.images[i]; const url = pickFirstUrl(img?.url_list); if (!url) continue; const { buffer, contentType, ext } = await downloadBinary(context, url); const safeExt = ext || "jpg"; const fileName = generateUniqueFileName( `${awemeId}/${i}.${safeExt}`, "douyin/images", ); const uploaded = await uploadFile(buffer, fileName, { "Content-Type": contentType, }); if (img.video?.play_addr) { // 如果是动图,下载 video 并上传 const videoUrl = img.video.play_addr[0]?.src; if (videoUrl) { try { const { buffer: videoBuffer, contentType: videoContentType, ext: videoExt, } = await downloadBinary(context, videoUrl); const safeVideoExt = videoExt || "mp4"; const videoFileName = generateUniqueFileName( `${awemeId}/${i}_animated.${safeVideoExt}`, "douyin/images", ); const uploadedVideo = await uploadFile(videoBuffer, videoFileName, { "Content-Type": videoContentType, }); // 获取动图时长 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 { uploadedImages.push({ url: uploaded, width: img?.width, height: img?.height, }); } } // 下载音乐(可选) let musicUrl: string | undefined; const audioSrc = pickFirstUrl(aweme.music?.play_url?.url_list); if (audioSrc) { const { buffer, contentType, ext } = await downloadBinary( context, audioSrc, ); const safeExt = ext || "mp3"; const fileName = generateUniqueFileName( `${awemeId}.${safeExt}`, "douyin/audios", ); musicUrl = await uploadFile(buffer, fileName, { "Content-Type": contentType, }); } return { images: uploadedImages, musicUrl }; }