176 lines
5.0 KiB
TypeScript
176 lines
5.0 KiB
TypeScript
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<string | undefined> {
|
|
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<string | undefined> {
|
|
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 };
|
|
}
|