diff --git a/src/app/api/douban/detail/route.ts b/src/app/api/douban/detail/route.ts new file mode 100644 index 0000000..a2b6f07 --- /dev/null +++ b/src/app/api/douban/detail/route.ts @@ -0,0 +1,77 @@ +import { NextResponse } from 'next/server'; + +import { getCacheTime } from '@/lib/config'; +import { fetchDoubanData } from '@/lib/douban'; + +interface DoubanDetailApiResponse { + id: string; + title: string; + original_title?: string; + year: string; + type: 'movie' | 'tv'; + subtype?: string; + is_tv?: boolean; + pic?: { + large: string; + normal: string; + }; + rating?: { + value: number; + count: number; + star_count: number; + }; + card_subtitle?: string; + intro?: string; + genres?: string[]; + directors?: Array<{ name: string; id?: string }>; + actors?: Array<{ name: string; id?: string }>; + countries?: string[]; + languages?: string[]; + pubdate?: string[]; + durations?: string[]; + aka?: string[]; + episodes_count?: number; + episodes_info?: string; + cover_url?: string; + url?: string; + [key: string]: any; // 允许其他字段 +} + +export const runtime = 'nodejs'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + + // 获取参数 + const id = searchParams.get('id'); + + // 验证参数 + if (!id) { + return NextResponse.json( + { error: '缺少必要参数: id' }, + { status: 400 } + ); + } + + const target = `https://m.douban.com/rexxar/api/v2/subject/${id}`; + + try { + // 调用豆瓣 API + const doubanData = await fetchDoubanData(target); + + const cacheTime = await getCacheTime(); + return NextResponse.json(doubanData, { + headers: { + 'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`, + 'CDN-Cache-Control': `public, s-maxage=${cacheTime}`, + 'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`, + 'Netlify-Vary': 'query', + }, + }); + } catch (error) { + return NextResponse.json( + { error: '获取豆瓣详情失败', details: (error as Error).message }, + { status: 500 } + ); + } +} diff --git a/src/lib/douban.client.ts b/src/lib/douban.client.ts index e4608c9..399510d 100644 --- a/src/lib/douban.client.ts +++ b/src/lib/douban.client.ts @@ -54,6 +54,40 @@ interface DoubanRecommendApiResponse { }>; } +interface DoubanDetailApiResponse { + id: string; + title: string; + original_title?: string; + year: string; + type: 'movie' | 'tv'; + subtype?: string; + is_tv?: boolean; + pic?: { + large: string; + normal: string; + }; + rating?: { + value: number; + count: number; + star_count: number; + }; + card_subtitle?: string; + intro?: string; + genres?: string[]; + directors?: Array<{ name: string; id?: string }>; + actors?: Array<{ name: string; id?: string }>; + countries?: string[]; + languages?: string[]; + pubdate?: string[]; + durations?: string[]; + aka?: string[]; + episodes_count?: number; + episodes_info?: string; + cover_url?: string; + url?: string; + [key: string]: any; // 允许其他字段 +} + /** * 带超时的 fetch 请求 */ @@ -477,3 +511,75 @@ async function fetchDoubanRecommends( throw new Error(`获取豆瓣推荐数据失败: ${(error as Error).message}`); } } + +/** + * 浏览器端豆瓣详情数据获取函数 + */ +export async function fetchDoubanDetail( + id: string, + proxyUrl: string, + useTencentCDN = false, + useAliCDN = false +): Promise { + if (!id) { + throw new Error('id 参数不能为空'); + } + + const target = useTencentCDN + ? `https://m.douban.cmliussss.net/rexxar/api/v2/subject/${id}` + : useAliCDN + ? `https://m.douban.cmliussss.com/rexxar/api/v2/subject/${id}` + : `https://m.douban.com/rexxar/api/v2/subject/${id}`; + + try { + const response = await fetchWithTimeout( + target, + useTencentCDN || useAliCDN ? '' : proxyUrl + ); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const doubanData: DoubanDetailApiResponse = await response.json(); + return doubanData; + } catch (error) { + // 触发全局错误提示 + if (typeof window !== 'undefined') { + window.dispatchEvent( + new CustomEvent('globalError', { + detail: { message: '获取豆瓣详情数据失败' }, + }) + ); + } + throw new Error(`获取豆瓣详情数据失败: ${(error as Error).message}`); + } +} + +/** + * 统一的豆瓣详情数据获取函数,根据代理设置选择使用服务端 API 或客户端代理获取 + */ +export async function getDoubanDetail( + id: string +): Promise { + const { proxyType, proxyUrl } = getDoubanProxyConfig(); + switch (proxyType) { + case 'cors-proxy-zwei': + return fetchDoubanDetail(id, 'https://ciao-cors.is-an.org/'); + case 'cmliussss-cdn-tencent': + return fetchDoubanDetail(id, '', true, false); + case 'cmliussss-cdn-ali': + return fetchDoubanDetail(id, '', false, true); + case 'cors-anywhere': + return fetchDoubanDetail(id, 'https://cors-anywhere.com/'); + case 'custom': + return fetchDoubanDetail(id, proxyUrl); + case 'direct': + default: + const response = await fetch(`/api/douban/detail?id=${id}`); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + } +}