小雅自动刷新链接,过滤空url
This commit is contained in:
@@ -168,6 +168,13 @@ function PlayPageClient() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearRefreshTimer();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 跳过片头片尾配置
|
||||
const [skipConfig, setSkipConfig] = useState<{
|
||||
enable: boolean;
|
||||
@@ -1116,6 +1123,13 @@ function PlayPageClient() {
|
||||
// 视频清晰度列表
|
||||
const [videoQualities, setVideoQualities] = useState<Array<{name: string, url: string}>>([]);
|
||||
|
||||
// Xiaoya链接刷新相关状态
|
||||
const [isRefreshingUrl, setIsRefreshingUrl] = useState(false); // 是否正在刷新链接
|
||||
const retryCountRef = useRef(0); // 重试计数
|
||||
const lastRefreshTimeRef = useRef(0); // 上次刷新时间
|
||||
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null); // 14分钟定时器
|
||||
const currentXiaoyaUrlRef = useRef<string>(''); // 当前xiaoya原始URL(用于刷新)
|
||||
|
||||
// 视频源代理模式状态
|
||||
const [sourceProxyMode, setSourceProxyMode] = useState(false);
|
||||
|
||||
@@ -1469,11 +1483,235 @@ function PlayPageClient() {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新xiaoya链接(静默刷新,不改变videoUrl状态)
|
||||
* @param hls HLS实例
|
||||
* @param video 视频元素
|
||||
* @param isScheduled 是否为定时刷新(true=定时,false=错误触发)
|
||||
*/
|
||||
const refreshXiaoyaUrl = async (
|
||||
hls: any,
|
||||
video: HTMLVideoElement,
|
||||
isScheduled: boolean = false
|
||||
) => {
|
||||
// 防抖:距离上次刷新不足3秒则不刷新
|
||||
const now = Date.now();
|
||||
if (now - lastRefreshTimeRef.current < 3000) {
|
||||
console.log('[链接刷新] 防抖跳过');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 重试次数限制(仅对错误触发的刷新)
|
||||
if (!isScheduled && retryCountRef.current >= 3) {
|
||||
console.error('[链接刷新] 重试次数已达上限');
|
||||
setVideoError('链接已过期且刷新失败,请手动刷新页面');
|
||||
hls.destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有原始URL
|
||||
if (!currentXiaoyaUrlRef.current) {
|
||||
console.warn('[链接刷新] 无原始URL,跳过刷新');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[链接刷新] 开始刷新 (${isScheduled ? '定时' : '错误触发'})`);
|
||||
setIsRefreshingUrl(true);
|
||||
|
||||
if (!isScheduled) {
|
||||
retryCountRef.current++;
|
||||
}
|
||||
lastRefreshTimeRef.current = now;
|
||||
|
||||
try {
|
||||
// 保存当前播放进度
|
||||
const currentTime = video.currentTime;
|
||||
const isPaused = video.paused;
|
||||
|
||||
console.log(`[链接刷新] 开始刷新 (${isScheduled ? '定时' : '错误触发'}), 当前时间:`, currentTime);
|
||||
|
||||
// 重新获取播放链接(添加时间戳避免缓存)
|
||||
const separator = currentXiaoyaUrlRef.current.includes('?') ? '&' : '?';
|
||||
const fetchUrl = `${currentXiaoyaUrlRef.current}${separator}format=json&t=${Date.now()}`;
|
||||
|
||||
const response = await fetch(fetchUrl);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.url) {
|
||||
throw new Error('未获取到有效链接');
|
||||
}
|
||||
|
||||
console.log('[链接刷新] 获取到新链接');
|
||||
|
||||
// 先停止HLS加载
|
||||
hls.stopLoad();
|
||||
|
||||
// 使用HLS的loadSource方法直接加载新链接(不改变videoUrl状态)
|
||||
hls.loadSource(data.url);
|
||||
|
||||
// 监听加载完成事件,恢复播放进度
|
||||
const onManifestParsed = () => {
|
||||
// 从指定位置开始加载
|
||||
hls.startLoad(currentTime);
|
||||
|
||||
// 等待视频可以seek
|
||||
const onLoadedData = () => {
|
||||
video.removeEventListener('loadeddata', onLoadedData);
|
||||
|
||||
// 设置播放位置
|
||||
if (currentTime > 0) {
|
||||
video.currentTime = currentTime;
|
||||
|
||||
// 等待seek完成
|
||||
const onSeeked = () => {
|
||||
console.log('[链接刷新] 刷新完成,恢复到:', video.currentTime);
|
||||
|
||||
video.removeEventListener('seeked', onSeeked);
|
||||
|
||||
// 恢复播放状态
|
||||
if (!isPaused) {
|
||||
video.play().catch(err => {
|
||||
console.warn('[链接刷新] 自动播放失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
setIsRefreshingUrl(false);
|
||||
|
||||
// 显示提示
|
||||
if (artPlayerRef.current) {
|
||||
artPlayerRef.current.notice.show = isScheduled
|
||||
? '链接已自动刷新'
|
||||
: '链接已过期并自动刷新';
|
||||
}
|
||||
|
||||
// 刷新成功,重置重试计数
|
||||
retryCountRef.current = 0;
|
||||
|
||||
// 重新启动14分钟定时器
|
||||
startRefreshTimer(hls, video);
|
||||
};
|
||||
|
||||
video.addEventListener('seeked', onSeeked, { once: true });
|
||||
} else {
|
||||
// 如果是从头开始,直接播放
|
||||
if (!isPaused) {
|
||||
video.play().catch(err => {
|
||||
console.warn('[链接刷新] 自动播放失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
setIsRefreshingUrl(false);
|
||||
|
||||
if (artPlayerRef.current) {
|
||||
artPlayerRef.current.notice.show = isScheduled
|
||||
? '链接已自动刷新'
|
||||
: '链接已过期并自动刷新';
|
||||
}
|
||||
|
||||
retryCountRef.current = 0;
|
||||
startRefreshTimer(hls, video);
|
||||
}
|
||||
};
|
||||
|
||||
video.addEventListener('loadeddata', onLoadedData, { once: true });
|
||||
};
|
||||
|
||||
// 使用 hls.constructor.Events 访问事件常量
|
||||
const HlsEvents = (hls.constructor as any).Events;
|
||||
hls.once(HlsEvents.MANIFEST_PARSED, onManifestParsed);
|
||||
|
||||
// 添加超时保护(10秒内未加载完成则认为失败)
|
||||
setTimeout(() => {
|
||||
hls.off(HlsEvents.MANIFEST_PARSED, onManifestParsed);
|
||||
if (isRefreshingUrl) {
|
||||
console.error('[链接刷新] 加载超时');
|
||||
setIsRefreshingUrl(false);
|
||||
throw new Error('加载超时');
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[链接刷新] 刷新失败:', error);
|
||||
setIsRefreshingUrl(false);
|
||||
|
||||
// 如果是定时刷新失败,不显示错误(继续使用旧链接)
|
||||
if (isScheduled) {
|
||||
console.warn('[链接刷新] 定时刷新失败,继续使用旧链接');
|
||||
// 5分钟后再试一次
|
||||
setTimeout(() => {
|
||||
if (hls && video && currentXiaoyaUrlRef.current) {
|
||||
refreshXiaoyaUrl(hls, video, true);
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 错误触发的刷新失败,如果还有重试次数,延迟后再试
|
||||
if (retryCountRef.current < 3) {
|
||||
console.log(`[链接刷新] 2秒后重试 (${retryCountRef.current}/3)`);
|
||||
setTimeout(() => {
|
||||
if (hls && video && currentXiaoyaUrlRef.current) {
|
||||
refreshXiaoyaUrl(hls, video, false);
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
setVideoError('链接刷新失败,请手动刷新页面');
|
||||
hls.destroy();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 启动14分钟定时刷新器
|
||||
*/
|
||||
const startRefreshTimer = (hls: any, video: HTMLVideoElement) => {
|
||||
// 清除旧定时器
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
refreshTimerRef.current = null;
|
||||
}
|
||||
|
||||
// 只对xiaoya源启动定时器
|
||||
if (!currentXiaoyaUrlRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[定时刷新] 启动14分钟定时器');
|
||||
|
||||
// 14分钟 = 840000毫秒
|
||||
refreshTimerRef.current = setTimeout(() => {
|
||||
console.log('[定时刷新] 14分钟到期,开始刷新');
|
||||
if (hls && video && currentXiaoyaUrlRef.current) {
|
||||
refreshXiaoyaUrl(hls, video, true);
|
||||
}
|
||||
}, 14 * 60 * 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除刷新定时器
|
||||
*/
|
||||
const clearRefreshTimer = () => {
|
||||
if (refreshTimerRef.current) {
|
||||
console.log('[定时刷新] 清除定时器');
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
refreshTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新视频地址
|
||||
const updateVideoUrl = async (
|
||||
detailData: SearchResult | null,
|
||||
episodeIndex: number
|
||||
) => {
|
||||
// 重置刷新相关状态
|
||||
retryCountRef.current = 0;
|
||||
lastRefreshTimeRef.current = 0;
|
||||
currentXiaoyaUrlRef.current = ''; // 清空旧的原始URL
|
||||
clearRefreshTimer(); // 清除旧的定时器
|
||||
|
||||
// 动态设置 referrer policy:只在小雅源时不发送 Referer
|
||||
const existingMeta = document.querySelector('meta[name="referrer"]');
|
||||
if (detailData?.source === 'xiaoya') {
|
||||
@@ -1507,6 +1745,9 @@ function PlayPageClient() {
|
||||
// 如果是小雅或 openlist 接口,先请求获取真实 URL
|
||||
if (newUrl.startsWith('/api/xiaoya/play') || newUrl.startsWith('/api/openlist/play')) {
|
||||
try {
|
||||
// 保存原始URL(用于后续刷新)
|
||||
currentXiaoyaUrlRef.current = newUrl;
|
||||
|
||||
// 添加 format=json 参数
|
||||
const separator = newUrl.includes('?') ? '&' : '?';
|
||||
const fetchUrl = `${newUrl}${separator}format=json`;
|
||||
@@ -1525,6 +1766,7 @@ function PlayPageClient() {
|
||||
} catch (error) {
|
||||
console.error('获取播放链接失败:', error);
|
||||
setVideoQualities([]);
|
||||
currentXiaoyaUrlRef.current = ''; // 获取失败,清空
|
||||
}
|
||||
} else {
|
||||
// 非小雅/openlist 源,清空清晰度列表
|
||||
@@ -1719,6 +1961,9 @@ function PlayPageClient() {
|
||||
|
||||
// 清理播放器资源的统一函数
|
||||
const cleanupPlayer = async () => {
|
||||
// 清除刷新定时器
|
||||
clearRefreshTimer();
|
||||
|
||||
// 先清理Anime4K,避免GPU纹理错误
|
||||
await cleanupAnime4K();
|
||||
|
||||
@@ -4445,6 +4690,16 @@ function PlayPageClient() {
|
||||
(video as any).playsInline = true;
|
||||
(video as any).webkitPlaysInline = true;
|
||||
|
||||
// 监听Manifest加载完成事件,启动xiaoya链接定时刷新
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
console.log('[HLS] Manifest解析完成');
|
||||
|
||||
// 如果是xiaoya源的m3u8,启动14分钟定时刷新
|
||||
if (currentXiaoyaUrlRef.current && url.includes('.m3u8')) {
|
||||
startRefreshTimer(hls, video);
|
||||
}
|
||||
});
|
||||
|
||||
hls.on(Hls.Events.ERROR, function (event: any, data: any) {
|
||||
console.error('HLS Error:', event, data);
|
||||
if (data.fatal) {
|
||||
@@ -4453,9 +4708,21 @@ function PlayPageClient() {
|
||||
// 检查是否是 manifest 加载错误(通常是 403/404/CORS 错误)
|
||||
if (data.details === 'manifestLoadError') {
|
||||
console.log('Manifest 加载失败:可能是 403/404 或 CORS 错误');
|
||||
hls.destroy();
|
||||
// 检查是否有响应码
|
||||
|
||||
const statusCode = data.response?.code || data.response?.status;
|
||||
|
||||
// 如果是403且是xiaoya源的m3u8,尝试自动刷新
|
||||
if (statusCode === 403 && currentXiaoyaUrlRef.current) {
|
||||
const isM3u8 = url.includes('.m3u8') || url.includes('m3u8');
|
||||
if (isM3u8) {
|
||||
console.log('[HLS错误] 检测到403,尝试刷新链接');
|
||||
refreshXiaoyaUrl(hls, video, false);
|
||||
return; // 不执行后续的错误处理
|
||||
}
|
||||
}
|
||||
|
||||
// 原有的错误处理逻辑
|
||||
hls.destroy();
|
||||
if (statusCode === 403) {
|
||||
setVideoError('访问被拒绝 (403)');
|
||||
} else if (statusCode === 404) {
|
||||
@@ -6712,6 +6979,19 @@ function PlayPageClient() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 链接刷新提示 */}
|
||||
{isRefreshingUrl && (
|
||||
<div className='absolute inset-0 flex items-center justify-center bg-black/50 z-50 pointer-events-none'>
|
||||
<div className='bg-black/80 text-white px-6 py-3 rounded-lg flex items-center gap-3 backdrop-blur-sm border border-green-500/30'>
|
||||
<svg className='animate-spin h-5 w-5' viewBox='0 0 24 24'>
|
||||
<circle className='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='4' fill='none'/>
|
||||
<path className='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'/>
|
||||
</svg>
|
||||
<span>正在刷新链接...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 弹幕加载蒙层 */}
|
||||
{danmakuLoading && (
|
||||
<div className='absolute top-0 right-0 m-4 bg-black/80 backdrop-blur-sm rounded-lg px-4 py-2 z-[600] flex items-center gap-2 border border-green-500/30'>
|
||||
|
||||
Reference in New Issue
Block a user