import type { BrowserContext } from 'playwright'; import { prisma } from '@/lib/prisma'; import { uploadAvatarFromUrl } from './uploader'; import { firstUrl } from './utils'; export async function saveToDB( context: BrowserContext, detailResp: DouyinVideoDetailResponse, commentResp: DouyinCommentResponse, videoUrl?: string, width?: number, height?: number, coverUrl?: string, fps?: number ) { if (!detailResp?.aweme_detail) throw new Error('视频详情为空'); const d = detailResp.aweme_detail; // 1) Upsert Author const authorAvatarSrc = firstUrl(d.author.avatar_thumb?.url_list); const authorAvatarUploaded = await uploadAvatarFromUrl(context, authorAvatarSrc, `authors/${d.author.sec_uid}`); const author = await prisma.author.upsert({ where: { sec_uid: d.author.sec_uid }, create: { sec_uid: d.author.sec_uid, uid: d.author.uid, nickname: d.author.nickname, signature: d.author.signature ?? null, avatar_url: authorAvatarUploaded ?? null, follower_count: BigInt(d.author.follower_count || 0), total_favorited: BigInt(d.author.total_favorited || 0), unique_id: d.author.unique_id ?? null, short_id: d.author.short_id ?? null, }, update: { uid: d.author.uid, nickname: d.author.nickname, signature: d.author.signature ?? null, avatar_url: authorAvatarUploaded ?? null, follower_count: BigInt(d.author.follower_count || 0), total_favorited: BigInt(d.author.total_favorited || 0), unique_id: d.author.unique_id ?? null, short_id: d.author.short_id ?? null, }, }); // 2) Upsert Video const video = await prisma.video.upsert({ where: { aweme_id: d.aweme_id }, create: { aweme_id: d.aweme_id, desc: d.desc, preview_title: d.preview_title ?? null, duration_ms: d.duration, created_at: new Date((d.create_time || 0) * 1000), share_url: d.share_url, digg_count: BigInt(d.statistics?.digg_count || 0), comment_count: BigInt(d.statistics?.comment_count || 0), share_count: BigInt(d.statistics?.share_count || 0), collect_count: BigInt(d.statistics?.collect_count || 0), authorId: author.sec_uid, tags: (d.tags?.map(t => t.tag_name) ?? []), video_url: videoUrl ?? '', width: width ?? null, height: height ?? null, cover_url: coverUrl ?? null, fps: fps ?? null, raw_json: detailResp as any, // 保存完整接口 JSON }, update: { desc: d.desc, preview_title: d.preview_title ?? null, duration_ms: d.duration, created_at: new Date((d.create_time || 0) * 1000), share_url: d.share_url, digg_count: BigInt(d.statistics?.digg_count || 0), comment_count: BigInt(d.statistics?.comment_count || 0), share_count: BigInt(d.statistics?.share_count || 0), collect_count: BigInt(d.statistics?.collect_count || 0), authorId: author.sec_uid, ...(videoUrl ? { video_url: videoUrl } : {}), ...(width ? { width } : {}), ...(height ? { height } : {}), ...(coverUrl ? { cover_url: coverUrl } : {}), ...(fps ? { fps } : {}), raw_json: detailResp as any, // 更新完整接口 JSON }, }); // 3) Upsert Comments + CommentUser const comments = commentResp?.comments ?? []; for (const c of comments) { const origAvatar: string | null = firstUrl(c.user?.avatar_thumb?.url_list) ?? null; const nameHint = `comment-users/${(c.user?.nickname || 'unknown').replace(/\s+/g, '_')}-${c.cid}`; const uploadedAvatar = await uploadAvatarFromUrl(context, origAvatar ?? undefined, nameHint); const finalAvatar = uploadedAvatar ?? origAvatar; // string | null const finalAvatarKey = finalAvatar ?? ''; const cu = await prisma.commentUser.upsert({ where: { nickname_avatar_url: { nickname: c.user?.nickname || '未知用户', avatar_url: finalAvatarKey, }, }, create: { nickname: c.user?.nickname || '未知用户', avatar_url: finalAvatar ?? null, }, update: { avatar_url: finalAvatar ?? null, }, }); await prisma.comment.upsert({ where: { cid: c.cid }, create: { cid: c.cid, text: c.text, digg_count: BigInt(c.digg_count || 0), created_at: new Date((c.create_time || 0) * 1000), videoId: video.aweme_id, userId: cu.id, }, update: { text: c.text, digg_count: BigInt(c.digg_count || 0), created_at: new Date((c.create_time || 0) * 1000), videoId: video.aweme_id, userId: cu.id, }, }); } return { aweme_id: video.aweme_id, author_sec_uid: author.sec_uid, comment_count: comments.length }; } export async function saveImagePostToDB( context: BrowserContext, aweme: DouyinImageAweme, commentResp: DouyinCommentResponse, uploads: { images: { url: string; width?: number; height?: number }[]; musicUrl?: string }, rawJson?: any ) { if (!aweme?.author?.sec_uid) throw new Error('作者 sec_uid 缺失'); // Upsert Author(与视频一致) const authorAvatarSrc = firstUrl(aweme.author.avatar_thumb?.url_list); const authorAvatarUploaded = await uploadAvatarFromUrl(context, authorAvatarSrc, `authors/${aweme.author.sec_uid}`); const author = await prisma.author.upsert({ where: { sec_uid: aweme.author.sec_uid }, create: { sec_uid: aweme.author.sec_uid, uid: aweme.author.uid, nickname: aweme.author.nickname, signature: aweme.author.signature ?? null, avatar_url: authorAvatarUploaded ?? null, follower_count: BigInt((aweme.author as any).follower_count || 0), total_favorited: BigInt((aweme.author as any).total_favorited || 0), unique_id: (aweme.author as any).unique_id ?? null, short_id: (aweme.author as any).short_id ?? null, }, update: { uid: aweme.author.uid, nickname: aweme.author.nickname, signature: aweme.author.signature ?? null, avatar_url: authorAvatarUploaded ?? null, follower_count: BigInt((aweme.author as any).follower_count || 0), total_favorited: BigInt((aweme.author as any).total_favorited || 0), unique_id: (aweme.author as any).unique_id ?? null, short_id: (aweme.author as any).short_id ?? null, }, }); // Upsert ImagePost const imagePost = await prisma.imagePost.upsert({ where: { aweme_id: aweme.aweme_id }, create: { aweme_id: aweme.aweme_id, desc: aweme.desc, created_at: new Date((aweme.create_time || 0) * 1000), share_url: aweme.share_url || '', digg_count: BigInt(aweme.statistics?.digg_count || 0), comment_count: BigInt(aweme.statistics?.comment_count || 0), share_count: BigInt(aweme.statistics?.share_count || 0), collect_count: BigInt(aweme.statistics?.collect_count || 0), authorId: author.sec_uid, tags: (aweme.video_tag?.map(t => t.tag_name) ?? []), music_url: uploads.musicUrl ?? null, raw_json: rawJson ?? null, // 保存完整接口 JSON }, update: { desc: aweme.desc, created_at: new Date((aweme.create_time || 0) * 1000), share_url: aweme.share_url, digg_count: BigInt(aweme.statistics?.digg_count || 0), comment_count: BigInt(aweme.statistics?.comment_count || 0), share_count: BigInt(aweme.statistics?.share_count || 0), collect_count: BigInt(aweme.statistics?.collect_count || 0), authorId: author.sec_uid, tags: (aweme.video_tag?.map(t => t.tag_name) ?? []), music_url: uploads.musicUrl ?? undefined, raw_json: rawJson ?? undefined, // 更新完整接口 JSON }, }); // Upsert ImageFiles(按顺序) for (let i = 0; i < uploads.images.length; i++) { const { url, width, height } = uploads.images[i]; await prisma.imageFile.upsert({ where: { postId_order: { postId: imagePost.aweme_id, order: i } }, create: { postId: imagePost.aweme_id, order: i, url, width: typeof width === 'number' ? width : null, height: typeof height === 'number' ? height : null, }, update: { url, width: typeof width === 'number' ? width : null, height: typeof height === 'number' ? height : null, }, }); } // 评论入库:关联到 ImagePost const comments = commentResp?.comments ?? []; for (const c of comments) { const origAvatar: string | null = firstUrl(c.user?.avatar_thumb?.url_list) ?? null; const nameHint = `comment-users/${(c.user?.nickname || 'unknown').replace(/\s+/g, '_')}-${c.cid}`; const uploadedAvatar = await uploadAvatarFromUrl(context, origAvatar ?? undefined, nameHint); const finalAvatar = uploadedAvatar ?? origAvatar; // string | null const finalAvatarKey = finalAvatar ?? ''; const cu = await prisma.commentUser.upsert({ where: { nickname_avatar_url: { nickname: c.user?.nickname || '未知用户', avatar_url: finalAvatarKey, }, }, create: { nickname: c.user?.nickname || '未知用户', avatar_url: finalAvatar ?? null, }, update: { avatar_url: finalAvatar ?? null, }, }); await prisma.comment.upsert({ where: { cid: c.cid }, create: { cid: c.cid, text: c.text, digg_count: BigInt(c.digg_count || 0), created_at: new Date((c.create_time || 0) * 1000), imagePostId: imagePost.aweme_id, userId: cu.id, }, update: { text: c.text, digg_count: BigInt(c.digg_count || 0), created_at: new Date((c.create_time || 0) * 1000), imagePostId: imagePost.aweme_id, userId: cu.id, }, }); } return { aweme_id: imagePost.aweme_id, author_sec_uid: author.sec_uid, image_count: uploads.images.length, comment_count: comments.length }; }