diff --git a/src/app/api/tmdb/trending/route.ts b/src/app/api/tmdb/trending/route.ts
index a9ec969..997a635 100644
--- a/src/app/api/tmdb/trending/route.ts
+++ b/src/app/api/tmdb/trending/route.ts
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextResponse } from 'next/server';
-import { getTMDBTrendingContent } from '@/lib/tmdb.client';
+import { getTMDBTrendingContent, getTMDBVideos } from '@/lib/tmdb.client';
import { getConfig } from '@/lib/config';
// 缓存配置 - 服务器内存缓存3小时
@@ -54,6 +54,18 @@ export async function GET() {
// 获取热门内容
result = await getTMDBTrendingContent(apiKey, proxy);
+
+ // 为每个项目获取视频数据
+ 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);
+ return { ...item, video_key: videoKey };
+ })
+ );
+ result.list = itemsWithVideos;
+ }
+
// 添加数据源标识
result.source = 'TMDB';
// 更新TMDB缓存
diff --git a/src/components/BannerCarousel.tsx b/src/components/BannerCarousel.tsx
index 652ccbb..349bd62 100644
--- a/src/components/BannerCarousel.tsx
+++ b/src/components/BannerCarousel.tsx
@@ -23,6 +23,8 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
const [isLoading, setIsLoading] = useState(true);
const [isPaused, setIsPaused] = useState(false);
const [skipNextAutoPlay, setSkipNextAutoPlay] = useState(false); // 跳过下一次自动播放
+ const [isYouTubeAccessible, setIsYouTubeAccessible] = useState(false); // YouTube连通性(默认false,检查后再决定)
+ const [enableTrailers, setEnableTrailers] = useState(false); // 是否启用预告片(默认关闭)
const touchStartX = useRef(0);
const touchEndX = useRef(0);
const isManualChange = useRef(false); // 标记是否为手动切换
@@ -51,6 +53,39 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
return getTMDBImageUrl(path, 'original');
};
+ // 读取本地设置
+ useEffect(() => {
+ const setting = localStorage.getItem('enableTrailers');
+ if (setting !== null) {
+ setEnableTrailers(setting === 'true');
+ }
+ }, []);
+
+ // 检测YouTube连通性
+ useEffect(() => {
+ const checkYouTubeAccess = () => {
+ const img = document.createElement('img');
+ const timeout = setTimeout(() => {
+ img.src = '';
+ setIsYouTubeAccessible(false);
+ }, 3000);
+
+ img.onload = () => {
+ clearTimeout(timeout);
+ setIsYouTubeAccessible(true);
+ };
+
+ img.onerror = () => {
+ clearTimeout(timeout);
+ setIsYouTubeAccessible(false);
+ };
+
+ img.src = 'https://i.ytimg.com/vi/dQw4w9WgXcQ/default.jpg';
+ };
+
+ checkYouTubeAccess();
+ }, []);
+
// 获取热门内容
useEffect(() => {
const fetchTrending = async () => {
@@ -234,7 +269,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
}
}}
>
- {/* 背景图片 */}
+ {/* 背景图片或视频 */}
{items.map((item, index) => (
-
+ {item.video_key && isYouTubeAccessible && enableTrailers ? (
+ /* 显示YouTube视频 */
+
+
+
+ ) : (
+ /* 显示图片 */
+
+ )}
{/* 渐变遮罩 */}
diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx
index 3b861b1..0562d5a 100644
--- a/src/components/UserMenu.tsx
+++ b/src/components/UserMenu.tsx
@@ -91,6 +91,7 @@ export const UserMenu: React.FC = () => {
const [liveDirectConnect, setLiveDirectConnect] = useState(false);
const [danmakuHeatmapDisabled, setDanmakuHeatmapDisabled] = useState(false);
const [tmdbBackdropDisabled, setTmdbBackdropDisabled] = useState(false);
+ const [enableTrailers, setEnableTrailers] = useState(false);
const [doubanDataSource, setDoubanDataSource] = useState('cmliussss-cdn-tencent');
const [doubanImageProxyType, setDoubanImageProxyType] = useState('cmliussss-cdn-tencent');
const [doubanImageProxyUrl, setDoubanImageProxyUrl] = useState('');
@@ -327,6 +328,11 @@ export const UserMenu: React.FC = () => {
setTmdbBackdropDisabled(savedTmdbBackdropDisabled === 'true');
}
+ const savedEnableTrailers = localStorage.getItem('enableTrailers');
+ if (savedEnableTrailers !== null) {
+ setEnableTrailers(savedEnableTrailers === 'true');
+ }
+
const savedBufferStrategy = localStorage.getItem('bufferStrategy');
if (savedBufferStrategy !== null) {
setBufferStrategy(savedBufferStrategy);
@@ -564,6 +570,13 @@ export const UserMenu: React.FC = () => {
}
};
+ const handleEnableTrailersToggle = (value: boolean) => {
+ setEnableTrailers(value);
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('enableTrailers', String(value));
+ }
+ };
+
const handleDoubanDataSourceChange = (value: string) => {
setDoubanDataSource(value);
if (typeof window !== 'undefined') {
@@ -635,6 +648,8 @@ export const UserMenu: React.FC = () => {
setFluidSearch(defaultFluidSearch);
setLiveDirectConnect(false);
setDanmakuHeatmapDisabled(false);
+ setTmdbBackdropDisabled(false);
+ setEnableTrailers(false);
setDoubanProxyUrl(defaultDoubanProxy);
setDoubanDataSource(defaultDoubanProxyType);
setDoubanImageProxyType(defaultDoubanImageProxyType);
@@ -648,6 +663,8 @@ export const UserMenu: React.FC = () => {
localStorage.setItem('fluidSearch', JSON.stringify(defaultFluidSearch));
localStorage.setItem('liveDirectConnect', JSON.stringify(false));
localStorage.setItem('danmaku_heatmap_disabled', 'false');
+ localStorage.setItem('tmdb_backdrop_disabled', 'false');
+ localStorage.setItem('enableTrailers', 'false');
localStorage.setItem('doubanProxyUrl', defaultDoubanProxy);
localStorage.setItem('doubanDataSource', defaultDoubanProxyType);
localStorage.setItem('doubanImageProxyType', defaultDoubanImageProxyType);
@@ -1297,6 +1314,30 @@ export const UserMenu: React.FC = () => {
+ {/* 启用预告片 */}
+
+
+
+ 首页预告片
+
+
+ 在首页轮播图中显示视频预告片(需刷新页面生效)
+
+
+
+
+
{/* 分割线 */}
diff --git a/src/lib/tmdb.client.ts b/src/lib/tmdb.client.ts
index 26174fe..404940a 100644
--- a/src/lib/tmdb.client.ts
+++ b/src/lib/tmdb.client.ts
@@ -54,6 +54,7 @@ export interface TMDBItem {
vote_average: number;
media_type: 'movie' | 'tv';
genre_ids?: number[]; // 类型ID列表
+ video_key?: string; // YouTube视频key
}
interface TMDBUpcomingResponse {
@@ -255,6 +256,60 @@ export async function getTMDBUpcomingContent(
}
}
+/**
+ * 获取视频(预告片)
+ * @param apiKey - TMDB API Key
+ * @param mediaType - 媒体类型 (movie 或 tv)
+ * @param mediaId - 媒体ID
+ * @param proxy - 代理服务器地址
+ * @returns YouTube视频key(只返回预告片)
+ */
+export async function getTMDBVideos(
+ apiKey: string,
+ mediaType: 'movie' | 'tv',
+ mediaId: number,
+ proxy?: string
+): Promise
{
+ try {
+ const actualKey = getNextApiKey(apiKey);
+ if (!actualKey) {
+ return null;
+ }
+
+ const url = `https://api.themoviedb.org/3/${mediaType}/${mediaId}/videos?api_key=${actualKey}`;
+ const fetchOptions: any = proxy
+ ? {
+ agent: new HttpsProxyAgent(proxy, {
+ timeout: 30000,
+ keepAlive: false,
+ }),
+ signal: AbortSignal.timeout(30000),
+ }
+ : {
+ signal: AbortSignal.timeout(15000),
+ };
+
+ const response = await nodeFetch(url, fetchOptions);
+
+ if (!response.ok) {
+ return null;
+ }
+
+ const data: any = await response.json();
+ const videos = data.results || [];
+
+ // 只查找YouTube预告片
+ const trailer = videos.find((v: any) =>
+ v.site === 'YouTube' && v.type === 'Trailer'
+ );
+
+ return trailer?.key || null;
+ } catch (error) {
+ console.error('获取 TMDB 视频失败:', error);
+ return null;
+ }
+}
+
/**
* 获取热门内容(电影+电视剧)
* @param apiKey - TMDB API Key