diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 610d6b0..de1fa2b 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -327,6 +327,7 @@ interface SiteConfig { DanmakuApiToken: string; TMDBApiKey?: string; TMDBProxy?: string; + BannerDataSource?: string; PansouApiUrl?: string; PansouUsername?: string; PansouPassword?: string; @@ -5414,6 +5415,7 @@ const SiteConfigComponent = ({ DanmakuApiToken: '87654321', TMDBApiKey: '', TMDBProxy: '', + BannerDataSource: 'TMDB', PansouApiUrl: '', PansouUsername: '', PansouPassword: '', @@ -5501,6 +5503,7 @@ const SiteConfigComponent = ({ DanmakuApiToken: config.SiteConfig.DanmakuApiToken || '87654321', TMDBApiKey: config.SiteConfig.TMDBApiKey || '', TMDBProxy: config.SiteConfig.TMDBProxy || '', + BannerDataSource: config.SiteConfig.BannerDataSource || 'TMDB', PansouApiUrl: config.SiteConfig.PansouApiUrl || '', PansouUsername: config.SiteConfig.PansouUsername || '', PansouPassword: config.SiteConfig.PansouPassword || '', @@ -5976,6 +5979,29 @@ const SiteConfigComponent = ({

+ {/* 轮播图数据源 */} +
+ + +

+ 选择首页轮播图的数据来源 +

+
+ {/* 弹幕 API 配置 */}

diff --git a/src/app/api/admin/site/route.ts b/src/app/api/admin/site/route.ts index 285f677..0402b8a 100644 --- a/src/app/api/admin/site/route.ts +++ b/src/app/api/admin/site/route.ts @@ -43,6 +43,7 @@ export async function POST(request: NextRequest) { DanmakuApiToken, TMDBApiKey, TMDBProxy, + BannerDataSource, PansouApiUrl, PansouUsername, PansouPassword, @@ -80,6 +81,7 @@ export async function POST(request: NextRequest) { DanmakuApiToken: string; TMDBApiKey?: string; TMDBProxy?: string; + BannerDataSource?: string; PansouApiUrl?: string; PansouUsername?: string; PansouPassword?: string; @@ -120,6 +122,7 @@ export async function POST(request: NextRequest) { typeof DanmakuApiToken !== 'string' || (TMDBApiKey !== undefined && typeof TMDBApiKey !== 'string') || (TMDBProxy !== undefined && typeof TMDBProxy !== 'string') || + (BannerDataSource !== undefined && typeof BannerDataSource !== 'string') || typeof EnableComments !== 'boolean' || (CustomAdFilterCode !== undefined && typeof CustomAdFilterCode !== 'string') || (CustomAdFilterVersion !== undefined && typeof CustomAdFilterVersion !== 'number') || @@ -172,6 +175,7 @@ export async function POST(request: NextRequest) { DanmakuApiToken, TMDBApiKey, TMDBProxy, + BannerDataSource, PansouApiUrl, PansouUsername, PansouPassword, diff --git a/src/app/api/tmdb/trending/route.ts b/src/app/api/tmdb/trending/route.ts index 6005948..88ed143 100644 --- a/src/app/api/tmdb/trending/route.ts +++ b/src/app/api/tmdb/trending/route.ts @@ -1,47 +1,210 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { NextResponse } from 'next/server'; import { getTMDBTrendingContent } from '@/lib/tmdb.client'; import { getConfig } from '@/lib/config'; // 缓存配置 - 服务器内存缓存3小时 const CACHE_DURATION = 3 * 60 * 60 * 1000; // 3小时 -let cachedData: { data: any; timestamp: number } | null = null; + +// 为不同数据源分别维护缓存 +let tmdbCache: { data: any; timestamp: number } | null = null; +let txCache: { data: any; timestamp: number } | null = null; export const dynamic = 'force-dynamic'; export async function GET() { try { - // 检查缓存 - if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) { - return NextResponse.json(cachedData.data); - } - // 获取配置 const config = await getConfig(); - const apiKey = config.SiteConfig?.TMDBApiKey; - const proxy = config.SiteConfig?.TMDBProxy; + const bannerDataSource = config.SiteConfig?.BannerDataSource || 'TMDB'; - if (!apiKey) { - return NextResponse.json( - { code: 400, message: 'TMDB API Key 未配置' }, - { status: 400 } - ); + // 根据数据源选择对应的缓存 + const cache = bannerDataSource === 'TX' ? txCache : tmdbCache; + + // 检查缓存 + if (cache && Date.now() - cache.timestamp < CACHE_DURATION) { + return NextResponse.json(cache.data); } - // 获取热门内容 - const result = await getTMDBTrendingContent(apiKey, proxy); + let result; - // 更新缓存 - cachedData = { - data: result, - timestamp: Date.now(), - }; + // 根据配置的数据源获取数据 + if (bannerDataSource === 'TX') { + // 使用TX数据源 + result = await getTXBannerContent(); + // 更新TX缓存 + txCache = { + data: result, + timestamp: Date.now(), + }; + } else { + // 使用TMDB数据源(默认) + const apiKey = config.SiteConfig?.TMDBApiKey; + const proxy = config.SiteConfig?.TMDBProxy; + + if (!apiKey) { + return NextResponse.json( + { code: 400, message: 'TMDB API Key 未配置' }, + { status: 400 } + ); + } + + // 获取热门内容 + result = await getTMDBTrendingContent(apiKey, proxy); + // 更新TMDB缓存 + tmdbCache = { + data: result, + timestamp: Date.now(), + }; + } return NextResponse.json(result); } catch (error) { - console.error('获取 TMDB 热门内容失败:', 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 pcShelvesCard = cardList.find((card: any) => card.type === 'pc_shelves'); + if (!pcShelvesCard) { + return []; + } + + // 获取轮播图卡片列表 + const cards = pcShelvesCard?.children_list?.list?.cards; + if (!Array.isArray(cards)) { + return []; + } + + // 转换为统一格式 + const bannerItems = cards + .filter((card: any) => card.params) // 只保留有params的卡片 + .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: [], + }; + }) + .filter((item: any) => { + // 只保留有标题和背景图的项目 + if (!item.title || !item.backdrop_path) return false; + // 剔除标题包含"免费合集"的数据 + if (item.title.includes('免费合集')) return false; + return true; + }); + + return bannerItems; + } catch (error) { + console.error('解析 TX 轮播图数据失败:', error); + return []; + } +} diff --git a/src/components/BannerCarousel.tsx b/src/components/BannerCarousel.tsx index 645524f..3e99893 100644 --- a/src/components/BannerCarousel.tsx +++ b/src/components/BannerCarousel.tsx @@ -10,9 +10,15 @@ interface BannerCarouselProps { autoPlayInterval?: number; // 自动播放间隔(毫秒) } +// 扩展TMDBItem类型以支持TX数据源的额外字段 +interface BannerItem extends TMDBItem { + subtitle?: string; // TX数据源的子标题 + tags?: string[]; // TX数据源的标签 +} + export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarouselProps) { const router = useRouter(); - const [items, setItems] = useState([]); + const [items, setItems] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [isLoading, setIsLoading] = useState(true); const [isPaused, setIsPaused] = useState(false); @@ -30,6 +36,17 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous router.push(`/play?title=${encodeURIComponent(title)}`); }; + // 获取图片URL(处理TX完整URL和TMDB路径) + const getImageUrl = (path: string | null) => { + if (!path) return ''; + // 如果是完整URL(TX数据源),直接返回 + if (path.startsWith('http://') || path.startsWith('https://')) { + return path; + } + // 否则使用TMDB的URL拼接 + return getTMDBImageUrl(path, 'original'); + }; + // 获取热门内容 useEffect(() => { const fetchTrending = async () => { @@ -205,7 +222,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous }`} > {item.title} {currentItem.title}

- +
- - {currentItem.vote_average.toFixed(1)} - - {getGenreNames(currentItem.genre_ids, 3).map(genre => ( - - {genre} + {currentItem.vote_average > 0 && ( + + {currentItem.vote_average.toFixed(1)} - ))} + )} + {/* 显示TX数据源的标签 */} + {currentItem.tags && currentItem.tags.length > 0 ? ( + currentItem.tags.slice(0, 3).map((tag, index) => ( + + {tag} + + )) + ) : ( + /* 显示TMDB数据源的类型标签 */ + getGenreNames(currentItem.genre_ids, 3).map(genre => ( + + {genre} + + )) + )} {currentItem.release_date && ( {currentItem.release_date} )} diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts index 7396d44..a1bd60a 100644 --- a/src/lib/admin.types.ts +++ b/src/lib/admin.types.ts @@ -22,6 +22,7 @@ export interface AdminConfig { // TMDB配置 TMDBApiKey?: string; TMDBProxy?: string; + BannerDataSource?: string; // 轮播图数据源:TMDB 或 TX // Pansou配置 PansouApiUrl?: string; PansouUsername?: string;