源名字中显示分辨率,换源增加全部重试
This commit is contained in:
+52
-3
@@ -1271,6 +1271,14 @@ function PlayPageClient() {
|
|||||||
Map<string, { quality: string; loadSpeed: string; pingTime: number; bitrate: string }>
|
Map<string, { quality: string; loadSpeed: string; pingTime: number; bitrate: string }>
|
||||||
>(new Map());
|
>(new Map());
|
||||||
|
|
||||||
|
// 当前源的视频信息(用于标题旁边显示)
|
||||||
|
const [currentSourceVideoInfo, setCurrentSourceVideoInfo] = useState<{
|
||||||
|
quality: string;
|
||||||
|
loadSpeed: string;
|
||||||
|
pingTime: number;
|
||||||
|
bitrate: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// 折叠状态(仅在 lg 及以上屏幕有效)
|
// 折叠状态(仅在 lg 及以上屏幕有效)
|
||||||
const [isEpisodeSelectorCollapsed, setIsEpisodeSelectorCollapsed] =
|
const [isEpisodeSelectorCollapsed, setIsEpisodeSelectorCollapsed] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@@ -1357,6 +1365,27 @@ function PlayPageClient() {
|
|||||||
return 'unknown';
|
return 'unknown';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取当前源的视频信息(分辨率和码率)
|
||||||
|
const fetchCurrentSourceVideoInfo = async () => {
|
||||||
|
if (!detail || !detail.episodes || detail.episodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前集数的播放地址
|
||||||
|
const episodeUrl = detail.episodes[currentEpisodeIndex];
|
||||||
|
if (!episodeUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = await getVideoResolutionFromM3u8(episodeUrl, 4000);
|
||||||
|
setCurrentSourceVideoInfo(info);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取视频信息失败:', error);
|
||||||
|
setCurrentSourceVideoInfo(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 播放源优选函数
|
// 播放源优选函数
|
||||||
const preferBestSource = async (
|
const preferBestSource = async (
|
||||||
sources: SearchResult[]
|
sources: SearchResult[]
|
||||||
@@ -3480,6 +3509,13 @@ function PlayPageClient() {
|
|||||||
}
|
}
|
||||||
}, [searchParams, currentSource, currentId, availableSources, currentEpisodeIndex]);
|
}, [searchParams, currentSource, currentId, availableSources, currentEpisodeIndex]);
|
||||||
|
|
||||||
|
// 监听 detail 和 currentEpisodeIndex 变化,自动获取视频信息
|
||||||
|
useEffect(() => {
|
||||||
|
if (detail && detail.episodes && detail.episodes.length > 0) {
|
||||||
|
fetchCurrentSourceVideoInfo();
|
||||||
|
}
|
||||||
|
}, [detail, currentEpisodeIndex]);
|
||||||
|
|
||||||
// 监听 detail 和 currentEpisodeIndex 变化,动态更新字幕
|
// 监听 detail 和 currentEpisodeIndex 变化,动态更新字幕
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!artPlayerRef.current || !detail) return;
|
if (!artPlayerRef.current || !detail) return;
|
||||||
@@ -8193,10 +8229,23 @@ function PlayPageClient() {
|
|||||||
<span>{doubanYear || detail?.year || videoYear}</span>
|
<span>{doubanYear || detail?.year || videoYear}</span>
|
||||||
)}
|
)}
|
||||||
{detail?.source_name && (
|
{detail?.source_name && (
|
||||||
<span className={`border px-2 py-[1px] rounded ${
|
<span
|
||||||
detail.source === 'xiaoya' ? 'border-blue-500' : detail.source === 'openlist' || detail.source === 'emby' || detail.source?.startsWith('emby_') ? 'border-yellow-500' : 'border-gray-500/60'
|
className={`relative group cursor-pointer border px-2 py-[1px] rounded ${
|
||||||
}`}>
|
detail.source === 'xiaoya' ? 'border-blue-500' : detail.source === 'openlist' || detail.source === 'emby' || detail.source?.startsWith('emby_') ? 'border-yellow-500' : 'border-gray-500/60'
|
||||||
|
}`}
|
||||||
|
onClick={fetchCurrentSourceVideoInfo}
|
||||||
|
>
|
||||||
{detail.source_name}
|
{detail.source_name}
|
||||||
|
{/* 视频信息悬浮提示 */}
|
||||||
|
{currentSourceVideoInfo && (
|
||||||
|
<div className='absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-gray-800 dark:bg-gray-900 text-white text-sm rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 ease-out whitespace-nowrap z-[100] pointer-events-none'>
|
||||||
|
<div className='text-sm'>
|
||||||
|
<div>分辨率: {currentSourceVideoInfo.quality}</div>
|
||||||
|
<div>码率: {currentSourceVideoInfo.bitrate}</div>
|
||||||
|
</div>
|
||||||
|
<div className='absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-800 dark:border-t-gray-900'></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{detail?.type_name && <span>{detail.type_name}</span>}
|
{detail?.type_name && <span>{detail.type_name}</span>}
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
);
|
);
|
||||||
// 标记初始测速是否已完成
|
// 标记初始测速是否已完成
|
||||||
const [initialTestingCompleted, setInitialTestingCompleted] = useState(false);
|
const [initialTestingCompleted, setInitialTestingCompleted] = useState(false);
|
||||||
|
// 标记是否正在进行全部重测
|
||||||
|
const [isRetestingAll, setIsRetestingAll] = useState(false);
|
||||||
|
// 标记是否正在进行初始测速
|
||||||
|
const [isInitialTesting, setIsInitialTesting] = useState(false);
|
||||||
|
|
||||||
// 使用 ref 来避免闭包问题
|
// 使用 ref 来避免闭包问题
|
||||||
const attemptedSourcesRef = useRef<Set<string>>(new Set());
|
const attemptedSourcesRef = useRef<Set<string>>(new Set());
|
||||||
@@ -248,6 +252,36 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
}
|
}
|
||||||
}, [speedTestTimeout]);
|
}, [speedTestTimeout]);
|
||||||
|
|
||||||
|
// 重测所有源的函数
|
||||||
|
const retestAllSources = useCallback(async () => {
|
||||||
|
if (!availableSources || availableSources.length === 0) return;
|
||||||
|
|
||||||
|
setIsRetestingAll(true);
|
||||||
|
|
||||||
|
// 清空之前的测速结果
|
||||||
|
setVideoInfoMap(new Map());
|
||||||
|
setAttemptedSources(new Set());
|
||||||
|
attemptedSourcesRef.current = new Set();
|
||||||
|
videoInfoMapRef.current = new Map();
|
||||||
|
|
||||||
|
// 筛选需要测速的源(排除 openlist/emby/xiaoya)
|
||||||
|
const sourcesToTest = availableSources.filter((source) => {
|
||||||
|
if (source.source === 'openlist' || source.source === 'emby' || source.source.startsWith('emby_') || source.source === 'xiaoya') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分批测速,每批最多5个
|
||||||
|
const batchSize = 5;
|
||||||
|
for (let i = 0; i < sourcesToTest.length; i += batchSize) {
|
||||||
|
const batch = sourcesToTest.slice(i, i + batchSize);
|
||||||
|
await Promise.all(batch.map(source => getVideoInfo(source)));
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRetestingAll(false);
|
||||||
|
}, [availableSources, getVideoInfo]);
|
||||||
|
|
||||||
// 当有预计算结果时,先合并到videoInfoMap中
|
// 当有预计算结果时,先合并到videoInfoMap中
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (precomputedVideoInfo && precomputedVideoInfo.size > 0) {
|
if (precomputedVideoInfo && precomputedVideoInfo.size > 0) {
|
||||||
@@ -301,6 +335,9 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
|
|
||||||
if (pendingSources.length === 0) return;
|
if (pendingSources.length === 0) return;
|
||||||
|
|
||||||
|
// 标记开始初始测速
|
||||||
|
setIsInitialTesting(true);
|
||||||
|
|
||||||
const batchSize = Math.ceil(pendingSources.length / 2);
|
const batchSize = Math.ceil(pendingSources.length / 2);
|
||||||
|
|
||||||
for (let start = 0; start < pendingSources.length; start += batchSize) {
|
for (let start = 0; start < pendingSources.length; start += batchSize) {
|
||||||
@@ -309,6 +346,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始测速完成后,标记为已完成
|
// 初始测速完成后,标记为已完成
|
||||||
|
setIsInitialTesting(false);
|
||||||
if (!initialTestingCompleted) {
|
if (!initialTestingCompleted) {
|
||||||
setInitialTestingCompleted(true);
|
setInitialTestingCompleted(true);
|
||||||
}
|
}
|
||||||
@@ -727,7 +765,24 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
|
|
||||||
{/* 换源 Tab 内容 */}
|
{/* 换源 Tab 内容 */}
|
||||||
{activeTab === 'sources' && (
|
{activeTab === 'sources' && (
|
||||||
<div className='flex flex-col h-full mt-4'>
|
<div className='flex flex-col h-full mt-2'>
|
||||||
|
{/* 全部重测按钮 - 右上角 */}
|
||||||
|
{!sourceSearchLoading && !sourceSearchError && availableSources.length > 0 && (
|
||||||
|
<div className='flex justify-end mb-2 px-2 pb-2 border-b border-gray-300 dark:border-gray-700'>
|
||||||
|
<button
|
||||||
|
onClick={retestAllSources}
|
||||||
|
disabled={isRetestingAll || retestingSources.size > 0 || isInitialTesting}
|
||||||
|
className={`text-xs font-medium transition-colors ${
|
||||||
|
isRetestingAll || retestingSources.size > 0 || isInitialTesting
|
||||||
|
? 'text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
||||||
|
: 'text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isRetestingAll ? '重测中...' : isInitialTesting ? '测速中...' : '全部重测'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{sourceSearchLoading && (
|
{sourceSearchLoading && (
|
||||||
<div className='flex items-center justify-center py-8'>
|
<div className='flex items-center justify-center py-8'>
|
||||||
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-green-500'></div>
|
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-green-500'></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user