小雅自动刷新链接,过滤空url

This commit is contained in:
mtvpls
2026-01-14 16:31:51 +08:00
parent 59e3685e65
commit 6d7a3b551b
3 changed files with 306 additions and 2 deletions

View File

@@ -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'>