轮播图数据源增加豆瓣

This commit is contained in:
mtvpls
2026-01-05 22:14:07 +08:00
parent f1a295cd75
commit 7a4a8262e1
7 changed files with 326 additions and 38 deletions

View File

@@ -3,6 +3,7 @@
import { NextResponse } from 'next/server';
import { getTMDBTrendingContent, getTMDBVideos } from '@/lib/tmdb.client';
import { getConfig } from '@/lib/config';
import { fetchDoubanData } from '@/lib/douban';
// 缓存配置 - 服务器内存缓存3小时
const CACHE_DURATION = 3 * 60 * 60 * 1000; // 3小时
@@ -10,6 +11,7 @@ const CACHE_DURATION = 3 * 60 * 60 * 1000; // 3小时
// 为不同数据源分别维护缓存
let tmdbCache: { data: any; timestamp: number } | null = null;
let txCache: { data: any; timestamp: number } | null = null;
let doubanCache: { data: any; timestamp: number } | null = null;
export const dynamic = 'force-dynamic';
@@ -17,10 +19,10 @@ export async function GET() {
try {
// 获取配置
const config = await getConfig();
const bannerDataSource = config.SiteConfig?.BannerDataSource || 'TMDB';
const bannerDataSource = config.SiteConfig?.BannerDataSource || 'Douban';
// 根据数据源选择对应的缓存
const cache = bannerDataSource === 'TX' ? txCache : tmdbCache;
const cache = bannerDataSource === 'TX' ? txCache : bannerDataSource === 'Douban' ? doubanCache : tmdbCache;
// 检查缓存
if (cache && Date.now() - cache.timestamp < CACHE_DURATION) {
@@ -30,7 +32,17 @@ export async function GET() {
let result: any;
// 根据配置的数据源获取数据
if (bannerDataSource === 'TX') {
if (bannerDataSource === 'Douban') {
// 使用豆瓣数据源
result = await getDoubanBannerContent();
// 添加数据源标识
result.source = 'Douban';
// 更新豆瓣缓存
doubanCache = {
data: result,
timestamp: Date.now(),
};
} else if (bannerDataSource === 'TX') {
// 使用TX数据源
result = await getTXBannerContent();
// 添加数据源标识
@@ -234,3 +246,151 @@ function parseTXBannerData(data: any): any[] {
return [];
}
}
/**
* 获取豆瓣轮播图内容
*/
async function getDoubanBannerContent(): Promise<{ code: number; list: any[] }> {
try {
// 获取豆瓣热门电影
const hotMoviesUrl = 'https://m.douban.com/rexxar/api/v2/subject/recent_hot/movie?start=0&limit=10&category=热门&type=全部';
interface DoubanHotMovie {
id: string;
title: string;
card_subtitle?: string;
pic?: {
large: string;
normal: string;
};
rating?: {
value: number;
};
}
interface DoubanHotMoviesResponse {
items: DoubanHotMovie[];
}
const hotMoviesData = await fetchDoubanData<DoubanHotMoviesResponse>(hotMoviesUrl);
if (!hotMoviesData.items || hotMoviesData.items.length === 0) {
return { code: 200, list: [] };
}
// 取前5个电影
const topMovies = hotMoviesData.items.slice(0, 5);
// 为每个电影获取详情信息
const bannerItems = await Promise.all(
topMovies.map(async (movie) => {
try {
const detailUrl = `https://m.douban.com/rexxar/api/v2/subject/${movie.id}`;
interface DoubanDetailResponse {
id: string;
title: string;
original_title?: string;
year: string;
rating?: {
value: number;
};
intro?: string;
genres?: string[];
cover_url?: string;
trailers?: Array<{
video_url?: string;
[key: string]: any;
}>;
[key: string]: any;
}
const detail = await fetchDoubanData<DoubanDetailResponse>(detailUrl);
// 获取预告片链接(取第一个)- 豆瓣是直链视频URL
const trailerUrl = detail.trailers && detail.trailers.length > 0
? detail.trailers[0].video_url
: null;
// 获取横屏图片
const backdropPath = detail.cover_url || movie.pic?.large || movie.pic?.normal || '';
// 提取年份
const year = detail.year || movie.card_subtitle?.match(/(\d{4})/)?.[1] || '';
// 从card_subtitle提取标签只读取第二个部分通过空格分割
let tags: string[] = [];
if (movie.card_subtitle) {
const parts = movie.card_subtitle.split('/').map(s => s.trim());
// 过滤掉年份(纯数字)和空字符串
const filteredParts = parts.filter(part =>
part && !/^\d{4}$/.test(part)
);
// 取第二个部分(类型),通过空格分割
if (filteredParts.length >= 2) {
tags = filteredParts[1].split(/\s+/).filter(t => t);
}
}
return {
id: movie.id,
title: detail.title,
backdrop_path: backdropPath,
poster_path: backdropPath,
release_date: year,
overview: detail.intro || '',
vote_average: detail.rating?.value || movie.rating?.value || 0,
media_type: 'movie',
genre_ids: [],
genres: tags, // 使用从card_subtitle提取的标签
trailer_url: trailerUrl, // 豆瓣预告片直链
video_key: null, // 豆瓣不使用YouTube key
};
} catch (error) {
console.error(`获取豆瓣电影 ${movie.id} 详情失败:`, error);
// 从card_subtitle提取标签只读取第二个部分通过空格分割
let tags: string[] = [];
if (movie.card_subtitle) {
const parts = movie.card_subtitle.split('/').map(s => s.trim());
// 过滤掉年份(纯数字)和空字符串
const filteredParts = parts.filter(part =>
part && !/^\d{4}$/.test(part)
);
// 取第二个部分(类型),通过空格分割
if (filteredParts.length >= 2) {
tags = filteredParts[1].split(/\s+/).filter(t => t);
}
}
// 如果获取详情失败,使用基本信息
return {
id: movie.id,
title: movie.title,
backdrop_path: movie.pic?.large || movie.pic?.normal || '',
poster_path: movie.pic?.large || movie.pic?.normal || '',
release_date: movie.card_subtitle?.match(/(\d{4})/)?.[1] || '',
overview: '',
vote_average: movie.rating?.value || 0,
media_type: 'movie',
genre_ids: [],
genres: tags, // 使用从card_subtitle提取的标签
trailer_url: null,
video_key: null,
};
}
})
);
// 过滤掉没有图片的项目
const validBannerItems = bannerItems.filter(item => item.backdrop_path);
return {
code: 200,
list: validBannerItems,
};
} catch (error) {
console.error('获取豆瓣轮播图数据失败:', error);
return { code: 500, list: [] };
}
}

View File

@@ -0,0 +1,91 @@
import { NextResponse } from 'next/server';
export const runtime = 'nodejs';
// 视频代理接口支持Range请求
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const videoUrl = searchParams.get('url');
if (!videoUrl) {
return NextResponse.json({ error: 'Missing video URL' }, { status: 400 });
}
try {
// 获取客户端的Range请求头
const range = request.headers.get('range');
const fetchHeaders: HeadersInit = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
Accept: 'video/mp4,video/*;q=0.9,*/*;q=0.8',
Referer: 'https://movie.douban.com/',
};
// 如果客户端发送了Range请求转发给源服务器
if (range) {
fetchHeaders['Range'] = range;
}
const videoResponse = await fetch(videoUrl, {
headers: fetchHeaders,
});
if (!videoResponse.ok) {
return NextResponse.json(
{ error: videoResponse.statusText },
{ status: videoResponse.status }
);
}
if (!videoResponse.body) {
return NextResponse.json(
{ error: 'Video response has no body' },
{ status: 500 }
);
}
// 创建响应头
const headers = new Headers();
// 复制重要的响应头
const contentType = videoResponse.headers.get('content-type');
if (contentType) {
headers.set('Content-Type', contentType);
}
const contentLength = videoResponse.headers.get('content-length');
if (contentLength) {
headers.set('Content-Length', contentLength);
}
const contentRange = videoResponse.headers.get('content-range');
if (contentRange) {
headers.set('Content-Range', contentRange);
}
const acceptRanges = videoResponse.headers.get('accept-ranges');
if (acceptRanges) {
headers.set('Accept-Ranges', acceptRanges);
}
// 设置缓存头
headers.set('Cache-Control', 'public, max-age=31536000, s-maxage=31536000'); // 缓存1年
headers.set('CDN-Cache-Control', 'public, s-maxage=31536000');
headers.set('Vercel-CDN-Cache-Control', 'public, s-maxage=31536000');
// 返回视频流状态码根据是否有Range请求决定
const status = range && contentRange ? 206 : 200;
return new Response(videoResponse.body, {
status,
headers,
});
} catch (error) {
console.error('Error proxying video:', error);
return NextResponse.json(
{ error: 'Error fetching video' },
{ status: 500 }
);
}
}