392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||
|
||
import { NextResponse } from 'next/server';
|
||
|
||
import { getConfig } from '@/lib/config';
|
||
import { fetchDoubanData } from '@/lib/douban';
|
||
import { getTMDBTrendingContent, getTMDBVideos } from '@/lib/tmdb.client';
|
||
|
||
// 缓存配置 - 服务器内存缓存3小时
|
||
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';
|
||
|
||
export async function GET() {
|
||
try {
|
||
// 获取配置
|
||
const config = await getConfig();
|
||
const bannerDataSource = config.SiteConfig?.BannerDataSource || 'Douban';
|
||
|
||
// 根据数据源选择对应的缓存
|
||
const cache = bannerDataSource === 'TX' ? txCache : bannerDataSource === 'Douban' ? doubanCache : tmdbCache;
|
||
|
||
// 检查缓存
|
||
if (cache && Date.now() - cache.timestamp < CACHE_DURATION) {
|
||
return NextResponse.json(cache.data);
|
||
}
|
||
|
||
let result: any;
|
||
|
||
// 根据配置的数据源获取数据
|
||
if (bannerDataSource === 'Douban') {
|
||
// 使用豆瓣数据源
|
||
result = await getDoubanBannerContent();
|
||
// 添加数据源标识
|
||
result.source = 'Douban';
|
||
// 更新豆瓣缓存
|
||
doubanCache = {
|
||
data: result,
|
||
timestamp: Date.now(),
|
||
};
|
||
} else if (bannerDataSource === 'TX') {
|
||
// 使用TX数据源
|
||
result = await getTXBannerContent();
|
||
// 添加数据源标识
|
||
result.source = 'TX';
|
||
// 更新TX缓存
|
||
txCache = {
|
||
data: result,
|
||
timestamp: Date.now(),
|
||
};
|
||
} else {
|
||
// 使用TMDB数据源(默认)
|
||
const apiKey = config.SiteConfig?.TMDBApiKey;
|
||
const proxy = config.SiteConfig?.TMDBProxy;
|
||
const reverseProxy = config.SiteConfig?.TMDBReverseProxy;
|
||
|
||
if (!apiKey) {
|
||
return NextResponse.json(
|
||
{ code: 400, message: 'TMDB API Key 未配置' },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// 获取热门内容
|
||
result = await getTMDBTrendingContent(apiKey, proxy, reverseProxy);
|
||
|
||
// 为每个项目获取视频数据
|
||
if (result.code === 200 && result.list) {
|
||
const itemsWithVideos = await Promise.all(
|
||
result.list.map(async (item: any) => {
|
||
const videoKey = await getTMDBVideos(apiKey, item.media_type, item.id, proxy, reverseProxy);
|
||
return { ...item, video_key: videoKey };
|
||
})
|
||
);
|
||
result.list = itemsWithVideos;
|
||
}
|
||
|
||
// 添加数据源标识
|
||
result.source = 'TMDB';
|
||
// 更新TMDB缓存
|
||
tmdbCache = {
|
||
data: result,
|
||
timestamp: Date.now(),
|
||
};
|
||
}
|
||
|
||
return NextResponse.json(result);
|
||
} catch (error) {
|
||
console.error('获取热门内容失败:', error);
|
||
return NextResponse.json(
|
||
{ code: 500, message: '获取热门内容失败' },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取TX轮播图内容
|
||
*/
|
||
async function getTXBannerContent(): Promise<{ code: number; list: any[] }> {
|
||
try {
|
||
// TX API 配置
|
||
const txApiUrl = 'https://pbaccess.video.qq.com/trpc.vector_layout.page_view.PageService/getPage?video_appid=3000010&vversion_platform=2&vdevice_guid=a458b2024f8d6f14';
|
||
const requestBody = {
|
||
page_params: {
|
||
page_type: 'channel',
|
||
page_id: '100101',
|
||
scene: 'channel',
|
||
new_mark_label_enabled: '1',
|
||
vl_to_mvl: '',
|
||
free_watch_trans_info: '{"ad_frequency_control_time_list":{}}',
|
||
ad_exp_ids: '',
|
||
ams_cookies: 'lv_play_index=33; o_minduid=PBUiqKSklDHZsTs2JqmXhTsczQfz5uzY; appuser=CC19AC2067F39B71',
|
||
ad_trans_data: '{"ad_request_id":"uglfjd6-26n6yw4-gs9tlvy-k19l366","game_sessions":[]}',
|
||
skip_privacy_types: '0',
|
||
support_click_scan: '1',
|
||
},
|
||
page_bypass_params: {
|
||
params: {
|
||
platform_id: '2',
|
||
caller_id: '3000010',
|
||
data_mode: 'default',
|
||
user_mode: 'default',
|
||
specified_strategy: '',
|
||
page_type: 'channel',
|
||
page_id: '100101',
|
||
scene: 'channel',
|
||
new_mark_label_enabled: '1',
|
||
},
|
||
scene: 'channel',
|
||
app_version: '',
|
||
abtest_bypass_id: 'a458b2024f8d6f14',
|
||
},
|
||
page_context: null,
|
||
};
|
||
|
||
// 发送请求到TX API
|
||
const response = await fetch(txApiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(requestBody),
|
||
signal: AbortSignal.timeout(15000),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error('TX API 请求失败:', response.status, response.statusText);
|
||
return { code: response.status, list: [] };
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// 解析响应数据
|
||
const bannerItems = parseTXBannerData(data);
|
||
|
||
return {
|
||
code: 200,
|
||
list: bannerItems,
|
||
};
|
||
} catch (error) {
|
||
console.error('获取 TX 轮播图数据失败:', error);
|
||
return { code: 500, list: [] };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析TX API响应数据,提取轮播图信息
|
||
*/
|
||
function parseTXBannerData(data: any): any[] {
|
||
try {
|
||
const cardList = data?.data?.CardList;
|
||
if (!Array.isArray(cardList)) {
|
||
return [];
|
||
}
|
||
|
||
// 找到所有类型为 pc_shelves 的卡片
|
||
const pcShelvesCards = cardList.filter((card: any) => card.type === 'pc_shelves');
|
||
if (pcShelvesCards.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
// 尝试每个 pc_shelves 卡片,直到找到有效数据
|
||
for (let i = 0; i < pcShelvesCards.length; i++) {
|
||
const pcShelvesCard = pcShelvesCards[i];
|
||
|
||
const cards = pcShelvesCard?.children_list?.list?.cards;
|
||
if (!Array.isArray(cards) || cards.length === 0) {
|
||
continue;
|
||
}
|
||
|
||
// 转换为统一格式
|
||
const cardsWithParams = cards.filter((card: any) => card.params);
|
||
|
||
const mappedItems = cardsWithParams.map((card: any, index: number) => {
|
||
const params = card.params;
|
||
|
||
// 获取标题(优先使用title)
|
||
const title = params.title || '';
|
||
|
||
// 获取子标题(优先使用priority_sub_title,其次rec_normal_reason)
|
||
const subtitle = params.priority_sub_title || params.rec_normal_reason || '';
|
||
|
||
// 获取标签(用"|"分割)
|
||
const topicLabel = params.topic_label || '';
|
||
const tags = topicLabel ? topicLabel.split('|').filter(Boolean) : [];
|
||
|
||
// 获取背景图
|
||
const backdropPath = params.priority_image_url || '';
|
||
|
||
return {
|
||
id: index + 1, // 使用索引作为ID
|
||
title,
|
||
subtitle,
|
||
tags,
|
||
backdrop_path: backdropPath,
|
||
poster_path: backdropPath, // 使用相同的图片
|
||
release_date: '',
|
||
overview: subtitle,
|
||
vote_average: 0,
|
||
media_type: 'tv',
|
||
genre_ids: [],
|
||
};
|
||
});
|
||
|
||
const bannerItems = mappedItems.filter((item: any) => {
|
||
// 只保留有标题和背景图的项目
|
||
if (!item.title || !item.backdrop_path) return false;
|
||
// 剔除标题包含"免费合集"的数据
|
||
if (item.title.includes('免费合集')) return false;
|
||
return true;
|
||
});
|
||
|
||
if (bannerItems.length > 0) {
|
||
return bannerItems;
|
||
}
|
||
}
|
||
|
||
// 所有 pc_shelves 卡片都没有有效数据
|
||
return [];
|
||
} catch (error) {
|
||
console.error('解析 TX 轮播图数据失败:', error);
|
||
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);
|
||
|
||
// 获取横屏图片
|
||
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提取的标签
|
||
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提取的标签
|
||
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: [] };
|
||
}
|
||
}
|