轮播图数据源增加豆瓣
This commit is contained in:
@@ -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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
91
src/app/api/video-proxy/route.ts
Normal file
91
src/app/api/video-proxy/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user