增加轮播图数据源
This commit is contained in:
@@ -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 = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 轮播图数据源 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
轮播图数据源
|
||||
</label>
|
||||
<select
|
||||
value={siteSettings.BannerDataSource || 'TMDB'}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
BannerDataSource: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
>
|
||||
<option value='TMDB'>TMDB</option>
|
||||
<option value='TX'>TX</option>
|
||||
</select>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
选择首页轮播图的数据来源
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 弹幕 API 配置 */}
|
||||
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TMDBItem[]>([]);
|
||||
const [items, setItems] = useState<BannerItem[]>([]);
|
||||
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
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={getTMDBImageUrl(item.backdrop_path || item.poster_path, 'original')}
|
||||
src={getImageUrl(item.backdrop_path || item.poster_path)}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
@@ -225,16 +242,28 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-white drop-shadow-lg">
|
||||
{currentItem.title}
|
||||
</h2>
|
||||
|
||||
|
||||
<div className="flex items-center gap-2 md:gap-3 text-sm md:text-base text-white/90 flex-wrap">
|
||||
<span className="px-2 py-1 bg-yellow-500 text-black font-semibold rounded">
|
||||
{currentItem.vote_average.toFixed(1)}
|
||||
</span>
|
||||
{getGenreNames(currentItem.genre_ids, 3).map(genre => (
|
||||
<span key={genre} className="px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm">
|
||||
{genre}
|
||||
{currentItem.vote_average > 0 && (
|
||||
<span className="px-2 py-1 bg-yellow-500 text-black font-semibold rounded">
|
||||
{currentItem.vote_average.toFixed(1)}
|
||||
</span>
|
||||
))}
|
||||
)}
|
||||
{/* 显示TX数据源的标签 */}
|
||||
{currentItem.tags && currentItem.tags.length > 0 ? (
|
||||
currentItem.tags.slice(0, 3).map((tag, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm">
|
||||
{tag}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
/* 显示TMDB数据源的类型标签 */
|
||||
getGenreNames(currentItem.genre_ids, 3).map(genre => (
|
||||
<span key={genre} className="px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm">
|
||||
{genre}
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
{currentItem.release_date && (
|
||||
<span>{currentItem.release_date}</span>
|
||||
)}
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface AdminConfig {
|
||||
// TMDB配置
|
||||
TMDBApiKey?: string;
|
||||
TMDBProxy?: string;
|
||||
BannerDataSource?: string; // 轮播图数据源:TMDB 或 TX
|
||||
// Pansou配置
|
||||
PansouApiUrl?: string;
|
||||
PansouUsername?: string;
|
||||
|
||||
Reference in New Issue
Block a user