新增评论抓取,修改更新日志
This commit is contained in:
174
src/app/api/douban-comments/route.ts
Normal file
174
src/app/api/douban-comments/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user