|
|
@@ -89,6 +89,8 @@ export interface WorkItem {
|
|
|
videoId?: string;
|
|
|
title: string;
|
|
|
coverUrl: string;
|
|
|
+ /** 作品播放/详情页 URL,同步到 works.video_url */
|
|
|
+ videoUrl?: string;
|
|
|
duration: string;
|
|
|
publishTime: string;
|
|
|
status: string;
|
|
|
@@ -777,6 +779,7 @@ class HeadlessBrowserService {
|
|
|
work_id: string;
|
|
|
title: string;
|
|
|
cover_url: string;
|
|
|
+ video_url?: string;
|
|
|
duration: number;
|
|
|
publish_time: string;
|
|
|
status: string;
|
|
|
@@ -789,6 +792,7 @@ class HeadlessBrowserService {
|
|
|
videoId: work.work_id,
|
|
|
title: work.title,
|
|
|
coverUrl: work.cover_url,
|
|
|
+ videoUrl: work.video_url || '',
|
|
|
duration: String(work.duration || 0),
|
|
|
publishTime: work.publish_time,
|
|
|
status: work.status || 'published',
|
|
|
@@ -1001,9 +1005,15 @@ class HeadlessBrowserService {
|
|
|
}
|
|
|
|
|
|
// 作品列表为空,尝试用 Playwright 获取账号信息
|
|
|
- logger.info(`[Python API] Got empty works list for ${platform}, trying Playwright`);
|
|
|
+ if (worksTotal > 0 && worksList.length === 0) {
|
|
|
+ logger.warn(`[Python API] Warning: API reported ${worksTotal} works but returned empty list for ${platform}`);
|
|
|
+ logger.warn(`[Python API] This may indicate a bug in Python API or API format change`);
|
|
|
+ } else {
|
|
|
+ logger.info(`[Python API] Got empty works list for ${platform} (total=${worksTotal}), trying Playwright`);
|
|
|
+ }
|
|
|
} catch (pythonError) {
|
|
|
logger.warn(`[Python API] Failed to fetch works for ${platform}:`, pythonError);
|
|
|
+ logger.warn(`[Python API] Error details:`, pythonError instanceof Error ? pythonError.message : String(pythonError));
|
|
|
}
|
|
|
} else {
|
|
|
logger.info(`[Python API] Service not available for ${platform}`);
|
|
|
@@ -1104,7 +1114,7 @@ class HeadlessBrowserService {
|
|
|
coverUrl: string;
|
|
|
duration: number;
|
|
|
createTime: number;
|
|
|
- statistics: { play_count: number; digg_count: number; comment_count: number; share_count: number };
|
|
|
+ statistics: { play_count: number; digg_count: number; comment_count: number; share_count: number; collect_count: number };
|
|
|
}>;
|
|
|
total?: number;
|
|
|
} = {};
|
|
|
@@ -1149,16 +1159,19 @@ class HeadlessBrowserService {
|
|
|
capturedData.total = (capturedData.total || 0) + data.aweme_list.length;
|
|
|
}
|
|
|
}
|
|
|
- // 解析作品列表
|
|
|
+ // 解析作品列表;video_url 使用 video.play_addr.url_list 的第一项
|
|
|
capturedData.worksList = data.aweme_list.map((aweme: Record<string, unknown>) => {
|
|
|
const statistics = aweme.statistics as Record<string, unknown> || {};
|
|
|
const cover = aweme.Cover as { url_list?: string[] } || aweme.video as { cover?: { url_list?: string[] } };
|
|
|
const coverUrl = cover?.url_list?.[0] || (cover as { cover?: { url_list?: string[] } })?.cover?.url_list?.[0] || '';
|
|
|
+ const video = aweme.video as { play_addr?: { url_list?: string[] } } | undefined;
|
|
|
+ const videoUrl = video?.play_addr?.url_list?.[0] || '';
|
|
|
|
|
|
return {
|
|
|
awemeId: String(aweme.aweme_id || ''),
|
|
|
title: String(aweme.item_title || aweme.desc || '').split('\n')[0].slice(0, 50) || '无标题',
|
|
|
coverUrl,
|
|
|
+ videoUrl,
|
|
|
duration: Number(aweme.duration || 0),
|
|
|
createTime: Number(aweme.create_time || 0),
|
|
|
statistics: {
|
|
|
@@ -1166,6 +1179,7 @@ class HeadlessBrowserService {
|
|
|
digg_count: Number(statistics.digg_count || 0),
|
|
|
comment_count: Number(statistics.comment_count || 0),
|
|
|
share_count: Number(statistics.share_count || 0),
|
|
|
+ collect_count: Number((statistics as any).collect_count || 0),
|
|
|
},
|
|
|
};
|
|
|
});
|
|
|
@@ -1417,6 +1431,8 @@ class HeadlessBrowserService {
|
|
|
logger.info('[Douyin] Fetching works via API...');
|
|
|
const apiResult = await this.fetchWorksDirectApi(page);
|
|
|
|
|
|
+ logger.info(`[Douyin] fetchWorksDirectApi returned: works.length=${apiResult.works.length}, total=${apiResult.total}`);
|
|
|
+
|
|
|
if (apiResult.works.length > 0) {
|
|
|
// 使用 items 累计数量作为作品数(apiResult.total 现在是累计的 items.length)
|
|
|
// 如果 total 为 0,则使用 works 列表长度
|
|
|
@@ -1425,22 +1441,28 @@ class HeadlessBrowserService {
|
|
|
videoId: w.awemeId,
|
|
|
title: w.title,
|
|
|
coverUrl: w.coverUrl,
|
|
|
+ videoUrl: (w as { videoUrl?: string }).videoUrl || (w.awemeId ? `https://www.douyin.com/video/${w.awemeId}` : ''),
|
|
|
duration: '00:00',
|
|
|
publishTime: w.createTime ? new Date(w.createTime * 1000).toISOString() : '',
|
|
|
status: 'published',
|
|
|
- playCount: 0,
|
|
|
- likeCount: 0,
|
|
|
+ playCount: w.playCount,
|
|
|
+ likeCount: w.likeCount,
|
|
|
commentCount: w.commentCount,
|
|
|
- shareCount: 0,
|
|
|
+ shareCount: w.shareCount,
|
|
|
+ collectCount: w.collectCount,
|
|
|
}));
|
|
|
logger.info(`[Douyin] Got ${apiResult.works.length} works from API, total count: ${worksCount}`);
|
|
|
- } else if (capturedData.worksList && capturedData.worksList.length > 0) {
|
|
|
- // 如果直接 API 调用失败,使用监听到的数据
|
|
|
- worksCount = capturedData.total || capturedData.worksList.length;
|
|
|
+ } else {
|
|
|
+ logger.warn(`[Douyin] fetchWorksDirectApi returned 0 works`);
|
|
|
+ if (capturedData.worksList && capturedData.worksList.length > 0) {
|
|
|
+ // 如果直接 API 调用失败,使用监听到的数据
|
|
|
+ logger.info(`[Douyin] Falling back to intercepted API data: ${capturedData.worksList.length} works`);
|
|
|
+ worksCount = capturedData.total || capturedData.worksList.length;
|
|
|
worksList = capturedData.worksList.map(w => ({
|
|
|
videoId: w.awemeId,
|
|
|
title: w.title,
|
|
|
coverUrl: w.coverUrl,
|
|
|
+ videoUrl: (w as { videoUrl?: string }).videoUrl || (w.awemeId ? `https://www.douyin.com/video/${w.awemeId}` : ''),
|
|
|
duration: this.formatDuration(w.duration),
|
|
|
publishTime: w.createTime ? new Date(w.createTime * 1000).toISOString() : '',
|
|
|
status: 'published',
|
|
|
@@ -1448,12 +1470,18 @@ class HeadlessBrowserService {
|
|
|
likeCount: w.statistics.digg_count,
|
|
|
commentCount: w.statistics.comment_count,
|
|
|
shareCount: w.statistics.share_count,
|
|
|
+ collectCount: w.statistics.collect_count,
|
|
|
}));
|
|
|
- logger.info(`[Douyin] Got ${worksCount} works from intercepted API data`);
|
|
|
+ logger.info(`[Douyin] Got ${worksCount} works from intercepted API data`);
|
|
|
+ } else {
|
|
|
+ logger.warn(`[Douyin] No works found: fetchWorksDirectApi returned 0, intercepted data also empty`);
|
|
|
+ logger.warn(`[Douyin] This may indicate: cookie expired, API error, or account has no works`);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
- logger.warn('Failed to fetch Douyin account info:', error);
|
|
|
+ logger.error('Failed to fetch Douyin account info:', error);
|
|
|
+ logger.error('Error details:', error instanceof Error ? error.stack : String(error));
|
|
|
}
|
|
|
|
|
|
return { accountId, accountName, avatarUrl, fansCount, worksCount, worksList };
|
|
|
@@ -2287,6 +2315,7 @@ class HeadlessBrowserService {
|
|
|
videoId: note.noteId,
|
|
|
title: note.title || '无标题',
|
|
|
coverUrl: note.coverUrl,
|
|
|
+ videoUrl: note.noteId ? `https://www.xiaohongshu.com/explore/${note.noteId}` : '',
|
|
|
duration: durationStr,
|
|
|
publishTime: note.publishTime,
|
|
|
status: statusStr,
|
|
|
@@ -2653,6 +2682,7 @@ class HeadlessBrowserService {
|
|
|
videoId: item.id || item.article_id || `bjh_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
title: item.title || '',
|
|
|
coverUrl: coverUrl,
|
|
|
+ videoUrl: item.url || item.article_url || '',
|
|
|
duration: '00:00',
|
|
|
publishTime: item.created_at || item.create_time || new Date().toISOString(),
|
|
|
status: item.status || 'published',
|
|
|
@@ -3753,7 +3783,12 @@ class HeadlessBrowserService {
|
|
|
awemeId: string;
|
|
|
title: string;
|
|
|
coverUrl: string;
|
|
|
+ videoUrl?: string;
|
|
|
+ playCount: number;
|
|
|
+ likeCount: number;
|
|
|
commentCount: number;
|
|
|
+ shareCount: number;
|
|
|
+ collectCount: number;
|
|
|
createTime?: number;
|
|
|
}>;
|
|
|
total: number;
|
|
|
@@ -3762,7 +3797,12 @@ class HeadlessBrowserService {
|
|
|
awemeId: string;
|
|
|
title: string;
|
|
|
coverUrl: string;
|
|
|
+ videoUrl?: string;
|
|
|
+ playCount: number;
|
|
|
+ likeCount: number;
|
|
|
commentCount: number;
|
|
|
+ shareCount: number;
|
|
|
+ collectCount: number;
|
|
|
createTime?: number;
|
|
|
}> = [];
|
|
|
let totalCount = 0; // 从 API 获取的总作品数
|
|
|
@@ -3820,12 +3860,24 @@ class HeadlessBrowserService {
|
|
|
// 检查 API 返回状态
|
|
|
if (data?.status_code !== 0 && data?.status_code !== undefined) {
|
|
|
logger.warn(`[DirectAPI] API returned error status_code: ${data.status_code}`);
|
|
|
+ logger.warn(`[DirectAPI] Error message: ${data?.err_msg || data?.errMsg || 'unknown'}`);
|
|
|
// status_code: 8 表示未授权,可能需要重新登录
|
|
|
if (data.status_code === 8) {
|
|
|
logger.warn('[DirectAPI] status_code 8: Not authorized, may need re-login');
|
|
|
}
|
|
|
+ // 如果是第一页就出错,记录更详细的错误信息
|
|
|
+ if (pageCount === 1) {
|
|
|
+ logger.error(`[DirectAPI] First page failed with status_code ${data.status_code}, cannot fetch works`);
|
|
|
+ logger.error(`[DirectAPI] Response data: ${JSON.stringify(data).substring(0, 500)}`);
|
|
|
+ }
|
|
|
break;
|
|
|
}
|
|
|
+
|
|
|
+ // 如果 status_code 是 0 但 aweme_list 为空,记录警告
|
|
|
+ if (data?.status_code === 0 && awemeList.length === 0 && pageCount === 1) {
|
|
|
+ logger.warn(`[DirectAPI] API returned success but aweme_list is empty on first page`);
|
|
|
+ logger.warn(`[DirectAPI] Response data: ${JSON.stringify(data).substring(0, 500)}`);
|
|
|
+ }
|
|
|
|
|
|
// 优先从第一个作品的 author.aweme_count 获取真实作品数(只在第一页获取)
|
|
|
if (pageCount === 1 && awemeList.length > 0) {
|
|
|
@@ -3844,9 +3896,13 @@ class HeadlessBrowserService {
|
|
|
const awemeId = String(aweme.aweme_id || '');
|
|
|
if (!awemeId) continue;
|
|
|
|
|
|
- // 从 statistics 中获取评论数
|
|
|
+ // 从 statistics 中获取所有统计字段
|
|
|
const statistics = aweme.statistics || {};
|
|
|
+ const playCount = parseInt(String(statistics.play_count || '0'), 10);
|
|
|
+ const likeCount = parseInt(String(statistics.digg_count || '0'), 10); // 抖音用 digg_count 表示点赞
|
|
|
const commentCount = parseInt(String(statistics.comment_count || '0'), 10);
|
|
|
+ const shareCount = parseInt(String(statistics.share_count || '0'), 10);
|
|
|
+ const collectCount = parseInt(String(statistics.collect_count || '0'), 10);
|
|
|
|
|
|
// 获取标题:优先使用 item_title,其次使用 desc(描述)
|
|
|
let title = aweme.item_title || '';
|
|
|
@@ -3864,11 +3920,19 @@ class HeadlessBrowserService {
|
|
|
coverUrl = aweme.video.cover.url_list[0];
|
|
|
}
|
|
|
|
|
|
+ // 入库 video_url 使用 play_addr.url_list 的第一项
|
|
|
+ const videoUrl = aweme.video?.play_addr?.url_list?.[0] || '';
|
|
|
+
|
|
|
works.push({
|
|
|
awemeId,
|
|
|
title,
|
|
|
coverUrl,
|
|
|
+ videoUrl,
|
|
|
+ playCount,
|
|
|
+ likeCount,
|
|
|
commentCount,
|
|
|
+ shareCount,
|
|
|
+ collectCount,
|
|
|
createTime: aweme.create_time,
|
|
|
});
|
|
|
}
|
|
|
@@ -3915,8 +3979,15 @@ class HeadlessBrowserService {
|
|
|
}
|
|
|
|
|
|
logger.info(`[DirectAPI] Total fetched ${works.length} works from ${pageCount} pages, items count: ${totalCount}`);
|
|
|
+
|
|
|
+ // 如果总作品数 > 0 但实际获取到的作品数为 0,记录警告
|
|
|
+ if (totalCount > 0 && works.length === 0) {
|
|
|
+ logger.warn(`[DirectAPI] Warning: API reported ${totalCount} works but fetched 0 works`);
|
|
|
+ logger.warn(`[DirectAPI] This may indicate: API error, cookie expired, or permission issue`);
|
|
|
+ }
|
|
|
} catch (e) {
|
|
|
- logger.warn('[DirectAPI] Failed to fetch works:', e);
|
|
|
+ logger.error('[DirectAPI] Failed to fetch works:', e);
|
|
|
+ logger.error('[DirectAPI] Error details:', e instanceof Error ? e.stack : String(e));
|
|
|
}
|
|
|
|
|
|
return { works, total: totalCount };
|