优化图文页

This commit is contained in:
2026-04-28 17:11:42 +08:00
parent 674c202264
commit 040273586b
69 changed files with 3939 additions and 2302 deletions
+150 -92
View File
@@ -1,117 +1,175 @@
export const runtime = 'nodejs'
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';
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,
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;
}
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,
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;
}
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 }[] = [];
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 });
// 下载图片(顺序保持)
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 });
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 });
}
// 下载音乐(可选)
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 };
return { images: uploadedImages, musicUrl };
}