轮播图数据源增加豆瓣
This commit is contained in:
@@ -6006,7 +6006,7 @@ const SiteConfigComponent = ({
|
|||||||
DanmakuApiToken: '87654321',
|
DanmakuApiToken: '87654321',
|
||||||
TMDBApiKey: '',
|
TMDBApiKey: '',
|
||||||
TMDBProxy: '',
|
TMDBProxy: '',
|
||||||
BannerDataSource: 'TMDB',
|
BannerDataSource: 'Douban',
|
||||||
RecommendationDataSource: 'Mixed',
|
RecommendationDataSource: 'Mixed',
|
||||||
PansouApiUrl: '',
|
PansouApiUrl: '',
|
||||||
PansouUsername: '',
|
PansouUsername: '',
|
||||||
@@ -6095,7 +6095,7 @@ const SiteConfigComponent = ({
|
|||||||
DanmakuApiToken: config.SiteConfig.DanmakuApiToken || '87654321',
|
DanmakuApiToken: config.SiteConfig.DanmakuApiToken || '87654321',
|
||||||
TMDBApiKey: config.SiteConfig.TMDBApiKey || '',
|
TMDBApiKey: config.SiteConfig.TMDBApiKey || '',
|
||||||
TMDBProxy: config.SiteConfig.TMDBProxy || '',
|
TMDBProxy: config.SiteConfig.TMDBProxy || '',
|
||||||
BannerDataSource: config.SiteConfig.BannerDataSource || 'TMDB',
|
BannerDataSource: config.SiteConfig.BannerDataSource || 'Douban',
|
||||||
PansouApiUrl: config.SiteConfig.PansouApiUrl || '',
|
PansouApiUrl: config.SiteConfig.PansouApiUrl || '',
|
||||||
PansouUsername: config.SiteConfig.PansouUsername || '',
|
PansouUsername: config.SiteConfig.PansouUsername || '',
|
||||||
PansouPassword: config.SiteConfig.PansouPassword || '',
|
PansouPassword: config.SiteConfig.PansouPassword || '',
|
||||||
@@ -6577,7 +6577,7 @@ const SiteConfigComponent = ({
|
|||||||
轮播图数据源
|
轮播图数据源
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={siteSettings.BannerDataSource || 'TMDB'}
|
value={siteSettings.BannerDataSource || 'Douban'}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSiteSettings((prev) => ({
|
setSiteSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -6586,6 +6586,7 @@ const SiteConfigComponent = ({
|
|||||||
}
|
}
|
||||||
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'
|
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='Douban'>豆瓣</option>
|
||||||
<option value='TMDB'>TMDB</option>
|
<option value='TMDB'>TMDB</option>
|
||||||
<option value='TX'>TX</option>
|
<option value='TX'>TX</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getTMDBTrendingContent, getTMDBVideos } from '@/lib/tmdb.client';
|
import { getTMDBTrendingContent, getTMDBVideos } from '@/lib/tmdb.client';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { fetchDoubanData } from '@/lib/douban';
|
||||||
|
|
||||||
// 缓存配置 - 服务器内存缓存3小时
|
// 缓存配置 - 服务器内存缓存3小时
|
||||||
const CACHE_DURATION = 3 * 60 * 60 * 1000; // 3小时
|
const CACHE_DURATION = 3 * 60 * 60 * 1000; // 3小时
|
||||||
@@ -10,6 +11,7 @@ const CACHE_DURATION = 3 * 60 * 60 * 1000; // 3小时
|
|||||||
// 为不同数据源分别维护缓存
|
// 为不同数据源分别维护缓存
|
||||||
let tmdbCache: { data: any; timestamp: number } | null = null;
|
let tmdbCache: { data: any; timestamp: number } | null = null;
|
||||||
let txCache: { 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 const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -17,10 +19,10 @@ export async function GET() {
|
|||||||
try {
|
try {
|
||||||
// 获取配置
|
// 获取配置
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const bannerDataSource = config.SiteConfig?.BannerDataSource || 'TMDB';
|
const bannerDataSource = config.SiteConfig?.BannerDataSource || 'Douban';
|
||||||
|
|
||||||
// 根据数据源选择对应的缓存
|
// 根据数据源选择对应的缓存
|
||||||
const cache = bannerDataSource === 'TX' ? txCache : tmdbCache;
|
const cache = bannerDataSource === 'TX' ? txCache : bannerDataSource === 'Douban' ? doubanCache : tmdbCache;
|
||||||
|
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
if (cache && Date.now() - cache.timestamp < CACHE_DURATION) {
|
if (cache && Date.now() - cache.timestamp < CACHE_DURATION) {
|
||||||
@@ -30,7 +32,17 @@ export async function GET() {
|
|||||||
let result: any;
|
let result: any;
|
||||||
|
|
||||||
// 根据配置的数据源获取数据
|
// 根据配置的数据源获取数据
|
||||||
if (bannerDataSource === 'TX') {
|
if (bannerDataSource === 'Douban') {
|
||||||
|
// 使用豆瓣数据源
|
||||||
|
result = await getDoubanBannerContent();
|
||||||
|
// 添加数据源标识
|
||||||
|
result.source = 'Douban';
|
||||||
|
// 更新豆瓣缓存
|
||||||
|
doubanCache = {
|
||||||
|
data: result,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
} else if (bannerDataSource === 'TX') {
|
||||||
// 使用TX数据源
|
// 使用TX数据源
|
||||||
result = await getTXBannerContent();
|
result = await getTXBannerContent();
|
||||||
// 添加数据源标识
|
// 添加数据源标识
|
||||||
@@ -234,3 +246,151 @@ function parseTXBannerData(data: any): any[] {
|
|||||||
return [];
|
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);
|
||||||
|
|
||||||
|
// 获取预告片链接(取第一个)- 豆瓣是直链视频URL
|
||||||
|
const trailerUrl = detail.trailers && detail.trailers.length > 0
|
||||||
|
? detail.trailers[0].video_url
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 获取横屏图片
|
||||||
|
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提取的标签
|
||||||
|
trailer_url: trailerUrl, // 豆瓣预告片直链
|
||||||
|
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提取的标签
|
||||||
|
trailer_url: null,
|
||||||
|
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: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
// 视频代理接口,支持Range请求
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const videoUrl = searchParams.get('url');
|
||||||
|
|
||||||
|
if (!videoUrl) {
|
||||||
|
return NextResponse.json({ error: 'Missing video URL' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取客户端的Range请求头
|
||||||
|
const range = request.headers.get('range');
|
||||||
|
|
||||||
|
const fetchHeaders: HeadersInit = {
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
||||||
|
Accept: 'video/mp4,video/*;q=0.9,*/*;q=0.8',
|
||||||
|
Referer: 'https://movie.douban.com/',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果客户端发送了Range请求,转发给源服务器
|
||||||
|
if (range) {
|
||||||
|
fetchHeaders['Range'] = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoResponse = await fetch(videoUrl, {
|
||||||
|
headers: fetchHeaders,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!videoResponse.ok) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: videoResponse.statusText },
|
||||||
|
{ status: videoResponse.status }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!videoResponse.body) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Video response has no body' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建响应头
|
||||||
|
const headers = new Headers();
|
||||||
|
|
||||||
|
// 复制重要的响应头
|
||||||
|
const contentType = videoResponse.headers.get('content-type');
|
||||||
|
if (contentType) {
|
||||||
|
headers.set('Content-Type', contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLength = videoResponse.headers.get('content-length');
|
||||||
|
if (contentLength) {
|
||||||
|
headers.set('Content-Length', contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentRange = videoResponse.headers.get('content-range');
|
||||||
|
if (contentRange) {
|
||||||
|
headers.set('Content-Range', contentRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptRanges = videoResponse.headers.get('accept-ranges');
|
||||||
|
if (acceptRanges) {
|
||||||
|
headers.set('Accept-Ranges', acceptRanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置缓存头
|
||||||
|
headers.set('Cache-Control', 'public, max-age=31536000, s-maxage=31536000'); // 缓存1年
|
||||||
|
headers.set('CDN-Cache-Control', 'public, s-maxage=31536000');
|
||||||
|
headers.set('Vercel-CDN-Cache-Control', 'public, s-maxage=31536000');
|
||||||
|
|
||||||
|
// 返回视频流,状态码根据是否有Range请求决定
|
||||||
|
const status = range && contentRange ? 206 : 200;
|
||||||
|
|
||||||
|
return new Response(videoResponse.body, {
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error proxying video:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Error fetching video' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import Image from 'next/image';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { getTMDBImageUrl, getGenreNames, type TMDBItem } from '@/lib/tmdb.client';
|
import { getTMDBImageUrl, getGenreNames, type TMDBItem } from '@/lib/tmdb.client';
|
||||||
import { ChevronLeft, ChevronRight, Play } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Play } from 'lucide-react';
|
||||||
|
import { processImageUrl, processVideoUrl } from '@/lib/utils';
|
||||||
|
|
||||||
interface BannerCarouselProps {
|
interface BannerCarouselProps {
|
||||||
autoPlayInterval?: number; // 自动播放间隔(毫秒)
|
autoPlayInterval?: number; // 自动播放间隔(毫秒)
|
||||||
@@ -14,6 +15,8 @@ interface BannerCarouselProps {
|
|||||||
interface BannerItem extends TMDBItem {
|
interface BannerItem extends TMDBItem {
|
||||||
subtitle?: string; // TX数据源的子标题
|
subtitle?: string; // TX数据源的子标题
|
||||||
tags?: string[]; // TX数据源的标签
|
tags?: string[]; // TX数据源的标签
|
||||||
|
trailer_url?: string | null; // 豆瓣预告片直链
|
||||||
|
genres?: string[]; // 豆瓣数据源的类型标签
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarouselProps) {
|
export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarouselProps) {
|
||||||
@@ -25,6 +28,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
|
|||||||
const [skipNextAutoPlay, setSkipNextAutoPlay] = useState(false); // 跳过下一次自动播放
|
const [skipNextAutoPlay, setSkipNextAutoPlay] = useState(false); // 跳过下一次自动播放
|
||||||
const [isYouTubeAccessible, setIsYouTubeAccessible] = useState(false); // YouTube连通性(默认false,检查后再决定)
|
const [isYouTubeAccessible, setIsYouTubeAccessible] = useState(false); // YouTube连通性(默认false,检查后再决定)
|
||||||
const [enableTrailers, setEnableTrailers] = useState(false); // 是否启用预告片(默认关闭)
|
const [enableTrailers, setEnableTrailers] = useState(false); // 是否启用预告片(默认关闭)
|
||||||
|
const [dataSource, setDataSource] = useState<string>(''); // 当前数据源
|
||||||
const touchStartX = useRef(0);
|
const touchStartX = useRef(0);
|
||||||
const touchEndX = useRef(0);
|
const touchEndX = useRef(0);
|
||||||
const isManualChange = useRef(false); // 标记是否为手动切换
|
const isManualChange = useRef(false); // 标记是否为手动切换
|
||||||
@@ -45,14 +49,29 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
|
|||||||
// 获取图片URL(处理TX完整URL和TMDB路径)
|
// 获取图片URL(处理TX完整URL和TMDB路径)
|
||||||
const getImageUrl = (path: string | null) => {
|
const getImageUrl = (path: string | null) => {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
// 如果是完整URL(TX数据源),直接返回
|
// 如果是完整URL(TX数据源或豆瓣),需要判断是否需要代理
|
||||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||||
|
// 豆瓣图片需要通过代理
|
||||||
|
if (path.includes('doubanio.com')) {
|
||||||
|
return processImageUrl(path);
|
||||||
|
}
|
||||||
|
// TX等其他完整URL直接返回
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
// 否则使用TMDB的URL拼接
|
// 否则使用TMDB的URL拼接
|
||||||
return getTMDBImageUrl(path, 'original');
|
return getTMDBImageUrl(path, 'original');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取视频URL(处理豆瓣视频代理)
|
||||||
|
const getVideoUrl = (url: string | null) => {
|
||||||
|
if (!url) return null;
|
||||||
|
// 豆瓣视频需要通过域名替换代理
|
||||||
|
if (url.includes('doubanio.com')) {
|
||||||
|
return processVideoUrl(url);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
// 读取本地设置
|
// 读取本地设置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setting = localStorage.getItem('enableTrailers');
|
const setting = localStorage.getItem('enableTrailers');
|
||||||
@@ -61,10 +80,10 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 检测YouTube连通性 - 仅在启用预告片时检测
|
// 检测YouTube连通性 - 仅在启用预告片且数据源为TMDB时检测
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 如果未启用预告片,不进行检测
|
// 如果未启用预告片或数据源不是TMDB,不进行检测
|
||||||
if (!enableTrailers) {
|
if (!enableTrailers || dataSource !== 'TMDB') {
|
||||||
setIsYouTubeAccessible(false);
|
setIsYouTubeAccessible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -91,14 +110,14 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
|
|||||||
};
|
};
|
||||||
|
|
||||||
checkYouTubeAccess();
|
checkYouTubeAccess();
|
||||||
}, [enableTrailers]);
|
}, [enableTrailers, dataSource]);
|
||||||
|
|
||||||
// 获取热门内容
|
// 获取热门内容
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTrending = async () => {
|
const fetchTrending = async () => {
|
||||||
try {
|
try {
|
||||||
// 先尝试从所有可能的数据源缓存中读取
|
// 先尝试从所有可能的数据源缓存中读取
|
||||||
const sources = ['TMDB', 'TX'];
|
const sources = ['TMDB', 'TX', 'Douban'];
|
||||||
let cachedData = null;
|
let cachedData = null;
|
||||||
let validSource = null;
|
let validSource = null;
|
||||||
|
|
||||||
@@ -126,6 +145,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
|
|||||||
// 如果有有效的缓存,直接使用,不请求API
|
// 如果有有效的缓存,直接使用,不请求API
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
setItems(cachedData);
|
setItems(cachedData);
|
||||||
|
setDataSource(validSource || ''); // 设置数据源
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,6 +159,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
|
|||||||
const cacheKey = getLocalStorageKey(dataSource);
|
const cacheKey = getLocalStorageKey(dataSource);
|
||||||
|
|
||||||
setItems(result.list);
|
setItems(result.list);
|
||||||
|
setDataSource(dataSource); // 设置数据源
|
||||||
|
|
||||||
// 保存到 localStorage(使用数据源特定的key)
|
// 保存到 localStorage(使用数据源特定的key)
|
||||||
try {
|
try {
|
||||||
@@ -285,7 +306,20 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
|
|||||||
index === currentIndex ? 'opacity-100' : 'opacity-0'
|
index === currentIndex ? 'opacity-100' : 'opacity-0'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.video_key && isYouTubeAccessible && enableTrailers ? (
|
{item.trailer_url && enableTrailers ? (
|
||||||
|
/* 显示豆瓣直链视频 */
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<video
|
||||||
|
src={getVideoUrl(item.trailer_url) || undefined}
|
||||||
|
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 min-w-full min-h-full w-auto h-auto object-cover"
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
preload="metadata"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : item.video_key && isYouTubeAccessible && enableTrailers ? (
|
||||||
/* 显示YouTube视频 */
|
/* 显示YouTube视频 */
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
<iframe
|
<iframe
|
||||||
@@ -333,13 +367,20 @@ export default function BannerCarousel({ autoPlayInterval = 5000 }: BannerCarous
|
|||||||
{currentItem.vote_average.toFixed(1)}
|
{currentItem.vote_average.toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* 显示TX数据源的标签 */}
|
{/* 显示标签:优先TX的tags,其次豆瓣的genres,最后TMDB的genre_ids */}
|
||||||
{currentItem.tags && currentItem.tags.length > 0 ? (
|
{currentItem.tags && currentItem.tags.length > 0 ? (
|
||||||
currentItem.tags.slice(0, 3).map((tag, index) => (
|
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">
|
<span key={index} className="px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm">
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))
|
))
|
||||||
|
) : currentItem.genres && Array.isArray(currentItem.genres) && currentItem.genres.length > 0 ? (
|
||||||
|
/* 显示豆瓣数据源的标签 */
|
||||||
|
currentItem.genres.slice(0, 3).map((genre, index) => (
|
||||||
|
<span key={index} className="px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm">
|
||||||
|
{genre}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
) : (
|
) : (
|
||||||
/* 显示TMDB数据源的类型标签 */
|
/* 显示TMDB数据源的类型标签 */
|
||||||
getGenreNames(currentItem.genre_ids, 3).map(genre => (
|
getGenreNames(currentItem.genre_ids, 3).map(genre => (
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export interface AdminConfig {
|
|||||||
// TMDB配置
|
// TMDB配置
|
||||||
TMDBApiKey?: string;
|
TMDBApiKey?: string;
|
||||||
TMDBProxy?: string;
|
TMDBProxy?: string;
|
||||||
BannerDataSource?: string; // 轮播图数据源:TMDB 或 TX
|
BannerDataSource?: string; // 轮播图数据源:TMDB、TX 或 Douban
|
||||||
RecommendationDataSource?: string; // 更多推荐数据源:Douban、TMDB、Mixed、MixedSmart
|
RecommendationDataSource?: string; // 更多推荐数据源:Douban、TMDB、Mixed、MixedSmart
|
||||||
// Pansou配置
|
// Pansou配置
|
||||||
PansouApiUrl?: string;
|
PansouApiUrl?: string;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface ChangelogEntry {
|
|||||||
export const changelog: ChangelogEntry[] = [
|
export const changelog: ChangelogEntry[] = [
|
||||||
{
|
{
|
||||||
version: '206.1.0',
|
version: '206.1.0',
|
||||||
date: '2026-01-04',
|
date: '2026-01-05',
|
||||||
added: [
|
added: [
|
||||||
"新增手动上传弹幕功能"
|
"新增手动上传弹幕功能"
|
||||||
],
|
],
|
||||||
|
|||||||
+17
-22
@@ -27,7 +27,7 @@ function getDoubanImageProxyConfig(): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理图片 URL,如果设置了图片代理则使用代理
|
* 处理图片 URL,统一使用服务器代理
|
||||||
*/
|
*/
|
||||||
export function processImageUrl(originalUrl: string): string {
|
export function processImageUrl(originalUrl: string): string {
|
||||||
if (!originalUrl) return originalUrl;
|
if (!originalUrl) return originalUrl;
|
||||||
@@ -42,28 +42,23 @@ export function processImageUrl(originalUrl: string): string {
|
|||||||
return originalUrl;
|
return originalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { proxyType, proxyUrl } = getDoubanImageProxyConfig();
|
// 统一使用服务器代理
|
||||||
switch (proxyType) {
|
return `/api/image-proxy?url=${encodeURIComponent(originalUrl)}`;
|
||||||
case 'server':
|
}
|
||||||
return `/api/image-proxy?url=${encodeURIComponent(originalUrl)}`;
|
|
||||||
case 'img3':
|
/**
|
||||||
return originalUrl.replace(/img\d+\.doubanio\.com/g, 'img3.doubanio.com');
|
* 处理视频 URL,如果���置了代理则使用代理(与图片使用相同的代理配置)
|
||||||
case 'cmliussss-cdn-tencent':
|
*/
|
||||||
return originalUrl.replace(
|
export function processVideoUrl(originalUrl: string): string {
|
||||||
/img\d+\.doubanio\.com/g,
|
if (!originalUrl) return originalUrl;
|
||||||
'img.doubanio.cmliussss.net'
|
|
||||||
);
|
// 仅处理豆瓣视频代理
|
||||||
case 'cmliussss-cdn-ali':
|
if (!originalUrl.includes('doubanio.com')) {
|
||||||
return originalUrl.replace(
|
return originalUrl;
|
||||||
/img\d+\.doubanio\.com/g,
|
|
||||||
'img.doubanio.cmliussss.com'
|
|
||||||
);
|
|
||||||
case 'custom':
|
|
||||||
return `${proxyUrl}${encodeURIComponent(originalUrl)}`;
|
|
||||||
case 'direct':
|
|
||||||
default:
|
|
||||||
return originalUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一使用服务器代理
|
||||||
|
return `/api/video-proxy?url=${encodeURIComponent(originalUrl)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user