新增评论抓取,修改更新日志

This commit is contained in:
mtvpls
2025-12-02 21:36:59 +08:00
parent 1c058c1774
commit 73be7f6ec4
9 changed files with 567 additions and 2 deletions

View File

@@ -0,0 +1,174 @@
import { NextRequest, NextResponse } from 'next/server';
import * as cheerio from 'cheerio';
export const runtime = 'nodejs';
interface DoubanComment {
id: string;
userName: string;
userAvatar: string;
userUrl: string;
rating: number | null; // 1-5 星null 表示未评分
content: string;
time: string;
votes: number;
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const doubanId = searchParams.get('id');
const start = searchParams.get('start') || '0';
const limit = searchParams.get('limit') || '20';
if (!doubanId) {
return NextResponse.json({ error: 'Missing douban ID' }, { status: 400 });
}
try {
// 请求豆瓣短评页面
const url = `https://movie.douban.com/subject/${doubanId}/comments?start=${start}&limit=${limit}&status=P&sort=new_score`;
const response = await fetch(url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
Referer: 'https://movie.douban.com/',
},
});
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch douban page' },
{ status: response.status }
);
}
const html = await response.text();
const $ = cheerio.load(html);
const comments: DoubanComment[] = [];
console.log('开始解析豆瓣评论start:', start, 'limit:', limit);
// 解析每条短评
$('.comment-item').each((index, element) => {
const $comment = $(element);
// 提取评论 ID
const commentId = $comment.attr('data-cid') || '';
// 提取用户信息
const $avatar = $comment.find('.avatar');
const userUrl = $avatar.find('a').attr('href') || '';
const userAvatar = $avatar.find('img').attr('src') || '';
const userName = $avatar.find('a').attr('title') || '';
// 提取评分(星级)
const ratingClass = $comment.find('.rating').attr('class') || '';
let rating: number | null = null;
const ratingMatch = ratingClass.match(/allstar(\d)0/);
if (ratingMatch) {
rating = parseInt(ratingMatch[1]);
}
// 提取短评内容
const $content = $comment.find('.short');
const content = $content.text().trim();
// 提取时间
const $commentInfo = $comment.find('.comment-info');
const time = $commentInfo.find('.comment-time').attr('title') || '';
// 提取有用数
const votesText = $comment.find('.votes.vote-count').text().trim();
const votes = parseInt(votesText) || 0;
if (commentId && content) {
comments.push({
id: commentId,
userName,
userAvatar,
userUrl,
rating,
content,
time,
votes,
});
}
});
console.log('解析到评论数:', comments.length);
// 获取总评论数 - 尝试多种方式
let total = 0;
// 方式1: 从标题获取 "全部 XXX 条"
const titleText = $('.mod-hd h2, h2, .section-title').text();
const titleMatch = titleText.match(/全部\s*(\d+)\s*条/);
if (titleMatch) {
total = parseInt(titleMatch[1]);
}
// 方式2: 从导航标签获取 "看过(XXX)"
if (total === 0) {
const navText = $('.tabs, .nav-tabs, a').text();
const navMatch = navText.match(/看过\s*\((\d+)\)/);
if (navMatch) {
total = parseInt(navMatch[1]);
}
}
// 方式3: 从页面所有文本查找
if (total === 0) {
const bodyText = $('body').text();
const bodyMatch = bodyText.match(/全部\s*(\d+)\s*条|看过\s*\((\d+)\)/);
if (bodyMatch) {
total = parseInt(bodyMatch[1] || bodyMatch[2]);
}
}
// 方式4: 如果有评论但 total 为 0至少设置为当前评论数并假设有更多
if (total === 0 && comments.length > 0) {
total = parseInt(start) + comments.length;
// 如果本次获取了完整的 limit 数量,可能还有更多
if (comments.length >= parseInt(limit)) {
total += 1; // 暂定有更多
}
}
console.log('豆瓣评论统计:', {
total,
commentsCount: comments.length,
start,
limit,
hasMore: parseInt(start) + comments.length < total || (total === 0 && comments.length >= parseInt(limit)),
});
return NextResponse.json(
{
comments,
total,
start: parseInt(start),
limit: parseInt(limit),
// 如果知道总数,就用总数判断;否则如果获取了完整页,假设还有更多
hasMore: total > 0
? parseInt(start) + comments.length < total
: comments.length >= parseInt(limit),
},
{
headers: {
'Cache-Control': 'public, max-age=600, s-maxage=600',
},
}
);
} catch (error) {
console.error('Douban comments fetch error:', error);
return NextResponse.json(
{ error: 'Failed to parse douban comments' },
{ status: 500 }
);
}
}

View File

@@ -40,6 +40,7 @@ import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
import EpisodeSelector from '@/components/EpisodeSelector';
import PageLayout from '@/components/PageLayout';
import DoubanComments from '@/components/DoubanComments';
// 扩展 HTMLVideoElement 类型以支持 hls 属性
declare global {
@@ -3280,6 +3281,28 @@ function PlayPageClient() {
</div>
</div>
</div>
{/* 豆瓣评论区域 */}
{videoDoubanId !== 0 && (
<div className='mt-6 px-4'>
<div className='bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm rounded-xl border border-gray-200/50 dark:border-gray-700/50 overflow-hidden'>
{/* 标题 */}
<div className='px-6 py-4 border-b border-gray-200 dark:border-gray-700'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2'>
<svg className='w-5 h-5' fill='currentColor' viewBox='0 0 24 24'>
<path d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/>
</svg>
</h3>
</div>
{/* 评论内容 */}
<div className='p-6'>
<DoubanComments doubanId={videoDoubanId} />
</div>
</div>
</div>
)}
</div>
</PageLayout>
);