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
}`}
>
{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;