diff --git a/src/components/EpisodeSelector.tsx b/src/components/EpisodeSelector.tsx index e9bdd34..e536f10 100644 --- a/src/components/EpisodeSelector.tsx +++ b/src/components/EpisodeSelector.tsx @@ -140,6 +140,32 @@ const EpisodeSelector: React.FC = ({ // 集数过滤设置弹窗状态 const [showFilterSettings, setShowFilterSettings] = useState(false); + // 读取本地"优选和测速"开关,默认开启 + const [optimizationEnabled] = useState(() => { + if (typeof window !== 'undefined') { + const saved = localStorage.getItem('enableOptimization'); + if (saved !== null) { + try { + return JSON.parse(saved); + } catch { + /* ignore */ + } + } + } + return true; + }); + + // 读取测速超时设置,默认4秒 + const [speedTestTimeout] = useState(() => { + if (typeof window !== 'undefined') { + const saved = localStorage.getItem('speedTestTimeout'); + if (saved !== null) { + return Number(saved); + } + } + return 4000; + }); + // 集数过滤逻辑 const isEpisodeFiltered = useCallback( (episodeNumber: number): boolean => { @@ -205,7 +231,7 @@ const EpisodeSelector: React.FC = ({ setAttemptedSources((prev) => new Set(prev).add(sourceKey)); try { - const info = await getVideoResolutionFromM3u8(episodeUrl); + const info = await getVideoResolutionFromM3u8(episodeUrl, speedTestTimeout); setVideoInfoMap((prev) => new Map(prev).set(sourceKey, info)); } catch (error) { // 失败时保存错误状态 @@ -219,7 +245,7 @@ const EpisodeSelector: React.FC = ({ }) ); } - }, []); + }, [speedTestTimeout]); // 当有预计算结果时,先合并到videoInfoMap中 useEffect(() => { @@ -252,21 +278,6 @@ const EpisodeSelector: React.FC = ({ } }, [precomputedVideoInfo]); - // 读取本地"优选和测速"开关,默认开启 - const [optimizationEnabled] = useState(() => { - if (typeof window !== 'undefined') { - const saved = localStorage.getItem('enableOptimization'); - if (saved !== null) { - try { - return JSON.parse(saved); - } catch { - /* ignore */ - } - } - } - return true; - }); - // 当切换到换源tab并且有源数据时,异步获取视频信息 - 移除 attemptedSources 依赖避免循环触发 useEffect(() => { const fetchVideoInfosInBatches = async () => { diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index 6adf98c..f4a221a 100644 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -95,6 +95,7 @@ export const UserMenu: React.FC = () => { const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true); const [doubanProxyUrl, setDoubanProxyUrl] = useState(''); const [enableOptimization, setEnableOptimization] = useState(true); + const [speedTestTimeout, setSpeedTestTimeout] = useState(4000); // 测速超时时间(毫秒) const [fluidSearch, setFluidSearch] = useState(true); const [liveDirectConnect, setLiveDirectConnect] = useState(false); const [tmdbBackdropDisabled, setTmdbBackdropDisabled] = useState(false); @@ -344,6 +345,11 @@ export const UserMenu: React.FC = () => { setEnableOptimization(JSON.parse(savedEnableOptimization)); } + const savedSpeedTestTimeout = localStorage.getItem('speedTestTimeout'); + if (savedSpeedTestTimeout !== null) { + setSpeedTestTimeout(Number(savedSpeedTestTimeout)); + } + const savedFluidSearch = localStorage.getItem('fluidSearch'); const defaultFluidSearch = (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; @@ -642,6 +648,13 @@ export const UserMenu: React.FC = () => { } }; + const handleSpeedTestTimeoutChange = (value: number) => { + setSpeedTestTimeout(value); + if (typeof window !== 'undefined') { + localStorage.setItem('speedTestTimeout', String(value)); + } + }; + const handleFluidSearchToggle = (value: boolean) => { setFluidSearch(value); if (typeof window !== 'undefined') { @@ -1439,6 +1452,63 @@ export const UserMenu: React.FC = () => { + {/* 测速超时设置 */} + {enableOptimization && ( +
+
+ + 换源面板测速超时 + + + {speedTestTimeout / 1000}秒 + +
+
+ handleSpeedTestTimeoutChange(Number(e.target.value))} + className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700' + style={{ + background: `linear-gradient(to right, #10b981 0%, #10b981 ${((speedTestTimeout - 4000) / (30000 - 4000)) * 100}%, #e5e7eb ${((speedTestTimeout - 4000) / (30000 - 4000)) * 100}%, #e5e7eb 100%)` + }} + /> +
+
+ + + + +
+

+ 注:此设置仅对换源面板测速生效,优选播放源时仍使用4秒超时 +

+
+ )} + {/* 流式搜索 */}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8e85fb9..37dfa1b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -138,7 +138,10 @@ export function processVideoUrl(originalUrl: string): string { * @param m3u8Url m3u8播放列表的URL * @returns Promise<{quality: string, loadSpeed: string, pingTime: number, bitrate: string}> 视频质量等级和网络信息 */ -export async function getVideoResolutionFromM3u8(m3u8Url: string): Promise<{ +export async function getVideoResolutionFromM3u8( + m3u8Url: string, + timeoutMs: number = 4000 +): Promise<{ quality: string; // 如720p、1080p等 loadSpeed: string; // 自动转换为KB/s或MB/s pingTime: number; // 网络延迟(毫秒) @@ -167,12 +170,12 @@ export async function getVideoResolutionFromM3u8(m3u8Url: string): Promise<{ // 固定使用hls.js加载 const hls = new Hls(); - // 设置超时处理 + // 设置超时处理 - 使用传入的超时时间 const timeout = setTimeout(() => { hls.destroy(); video.remove(); reject(new Error('Timeout loading video metadata')); - }, 4000); + }, timeoutMs); video.onerror = () => { clearTimeout(timeout);