// ==UserScript== // @name 广西网课 - 自动播放 & 下一节 v4 // @namespace http://tampermonkey.net/ // @version 4.0 // @description 可视状态面板 + 多重兜底 + HLS兼容 + 刷新回补 // @author Marvis // @match http://gxgs.study.gspxonline.com/* // @match https://gxgs.study.gspxonline.com/* // @match http://gxgspxpt.gspxonline.com/* // @match https://gxgspxpt.gspxonline.com/* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; // ========== 状态面板(页面右上角浮动小窗) ========== const panel = document.createElement('div'); panel.id = '__auto_study_panel'; panel.innerHTML = '⏳ 初始化...'; Object.assign(panel.style, { position: 'fixed', top: '10px', right: '10px', zIndex: '99999', background: 'rgba(0,0,0,0.8)', color: '#0f0', padding: '6px 12px', borderRadius: '6px', fontSize: '13px', fontFamily: 'monospace', pointerEvents: 'none', maxWidth: '300px', lineHeight: '1.5', }); document.body.appendChild(panel); function setStatus(text, color) { panel.textContent = text; panel.style.color = color || '#0f0'; } // ========== 配置 ========== const TICK = 1500; // ========== DOM ========== const $v = () => document.querySelector('#aliplayer video'); const $allLi = () => document.querySelectorAll('li[id^="res_"]'); const $active = () => document.querySelector('li.active[id^="res_"]'); const $sidebar = () => document.querySelector('.sidebar'); function findNext() { const act = $active(); if (!act) return null; const all = $allLi(); for (let i = 0; i < all.length - 1; i++) { if (all[i].id === act.id) return all[i + 1]; } return null; } function findIndex() { const act = $active(); if (!act) return -1; const all = $allLi(); for (let i = 0; i < all.length; i++) { if (all[i].id === act.id) return i; } return -1; } // ========== 点击跳转 ========== function clickNext(next) { if (!next) return false; // 滚动到可见区域 const sb = $sidebar(); if (sb) next.scrollIntoView({ block: 'center', behavior: 'auto' }); // 点击 next.click(); // 如果 li 内部有 a 标签,也点一下 const a = next.querySelector('a'); if (a) setTimeout(() => a.click(), 100); return true; } function jumpNext() { const next = findNext(); if (!next) return false; const idx = findIndex(); setStatus('跳转: ' + (idx + 2) + '/' + $allLi().length + ' ' + (next.textContent || '').trim().slice(0, 20), '#ff0'); return clickNext(next); } // ========== 播放 ========== function doPlay(v) { if (!v) return; try { v.muted = true; v.play().then(() => { setTimeout(() => { v.muted = false; }, 1000); }).catch(() => {}); } catch (e) {} } // ========== 核心状态机 ========== let phase = 'init'; // init | playing | done let triggered = false; let stuckTimer = null; let stuckStart = 0; let stuckLast = 0; let initRetry = 0; function resetForNewVideo() { phase = 'init'; triggered = false; stuckTimer = null; stuckStart = 0; stuckLast = 0; initRetry = 0; } function loop() { if (!location.href.includes('/study/learn/')) { setStatus('非学习页面', '#888'); return; } const v = $v(); const all = $allLi(); const act = $active(); const idx = findIndex(); const total = all.length; // ---- 无视频:等待 ---- if (!v) { initRetry++; if (initRetry > 20) { // 等了 30 秒还没有视频,刷新 setStatus('无视频,刷新...', '#f80'); setTimeout(() => location.reload(), 2000); return; } setStatus('等待播放器... (' + initRetry + ')', '#888'); return; } initRetry = 0; const dur = v.duration || 0; const cur = v.currentTime || 0; // HLS 流 duration 可能为 Infinity,用 0 兜底 const hasDur = isFinite(dur) && dur > 1; const prog = hasDur ? cur / dur : 0; const pct = (prog * 100).toFixed(0); // ---- 刚初始化 ---- if (phase === 'init') { phase = 'playing'; stuckStart = Date.now(); stuckLast = cur; v.addEventListener('ended', onVideoEnded); if (v.paused) doPlay(v); } // ---- 状态文字 ---- const vidLabel = hasDur ? pct + '% | ' + fmt(cur) + '/' + fmt(dur) : fmt(cur) + ' (HLS)'; setStatus('第' + (idx + 1) + '/' + total + '节 | ' + vidLabel + ' | ' + (v.paused ? '暂停' : '播放中'), '#0f0'); // ---- 暂停自动恢复 ---- if (v.paused && !v.ended) { doPlay(v); } // ---- 兜底 1: ended 属性 ---- if (v.ended && !triggered) { setStatus('ended=true,跳转', '#0ff'); onVideoEnded(); return; } // ---- 兜底 2: 进度 >= 98% ---- if (hasDur && prog >= 0.98 && !triggered) { setStatus('进度 ' + pct + '%,跳转', '#0ff'); onVideoEnded(); return; } // ---- 兜底 3: 进度卡死 ---- if (hasDur && !v.paused) { const delta = cur - stuckLast; const elapsed = Date.now() - stuckStart; if (delta < 1 && elapsed > 120000) { // 2 分钟进度不动 setStatus('卡死 ' + (elapsed / 1000).toFixed(0) + 's,恢复中', '#f44'); doPlay(v); // 如果卡了超过 5 分钟,直接当播完处理 if (elapsed > 300000 && prog > 0.5 && !triggered) { setStatus('卡死过久,强制跳转', '#f44'); onVideoEnded(); return; } stuckStart = Date.now(); stuckLast = cur; } if (delta >= 1) { stuckStart = Date.now(); stuckLast = cur; } } // ---- 兜底 4: 超时(duration 不可靠时的保底) ---- // 如果 hasDur 且已经过了 duration+30秒还没跳转,强制跳 if (hasDur && cur > dur + 30 && !triggered) { setStatus('超时,强制跳转', '#f44'); onVideoEnded(); return; } } function onVideoEnded() { if (triggered) return; triggered = true; phase = 'done'; setStatus('播放完毕,跳转中...', '#ff0'); setTimeout(() => { const ok = jumpNext(); if (ok) { // 等待页面切换 waitTransition(); } else { setStatus('已是最后一节 ✓', '#0f0'); } }, 1000); } function waitTransition() { const oldId = $active() ? $active().id : ''; let waitCount = 0; const check = setInterval(() => { waitCount++; const act = $active(); const newId = act ? act.id : ''; if (newId && newId !== oldId) { clearInterval(check); setStatus('已切换: ' + (act.textContent || '').trim().slice(0, 20), '#0f0'); waitForVideo(); return; } if (waitCount > 30) { // 15 秒超时 clearInterval(check); setStatus('切换超时,刷新页面', '#f44'); setTimeout(() => location.reload(), 500); } }, 500); } function waitForVideo() { let count = 0; const check = setInterval(() => { count++; const v = $v(); if (v) { clearInterval(check); resetForNewVideo(); phase = 'playing'; stuckStart = Date.now(); stuckLast = v.currentTime || 0; v.addEventListener('ended', onVideoEnded); doPlay(v); setStatus('新视频就绪', '#0f0'); return; } if (count > 40) { // 20 秒超时 clearInterval(check); setStatus('视频加载超时,刷新', '#f44'); setTimeout(() => location.reload(), 500); } }, 500); } function fmt(s) { const m = Math.floor(s / 60); const sec = Math.floor(s % 60); return m + ':' + (sec < 10 ? '0' : '') + sec; } // ========== SPA 路由监听 ========== function setupRouter() { let last = location.href; const h = history; const _ps = h.pushState; h.pushState = function () { _ps.apply(this, arguments); check(); }; const _rs = h.replaceState; h.replaceState = function () { _rs.apply(this, arguments); check(); }; window.addEventListener('popstate', check); function check() { if (location.href !== last) { last = location.href; if (last.includes('/study/learn/')) { resetForNewVideo(); setTimeout(() => { const v = $v(); if (v) { v.addEventListener('ended', onVideoEnded); doPlay(v); phase = 'playing'; } }, 1500); } } } } // ========== 保活 ========== function keepAlive() { requestAnimationFrame(function loop() { const v = $v(); if (v && !v.ended) void v.currentTime; requestAnimationFrame(loop); }); setInterval(() => { const v = $v(); if (v && !v.ended) void v.currentTime; }, 30000); } document.addEventListener('visibilitychange', () => { if (!document.hidden) { const v = $v(); if (v && v.paused && !v.ended && phase === 'playing') { v.muted = true; v.play().catch(() => {}); setTimeout(() => { v.muted = false; }, 1000); } } }); // ========== 启动 ========== setupRouter(); keepAlive(); if (location.href.includes('/study/learn/')) { setTimeout(() => { const v = $v(); if (v) { v.addEventListener('ended', onVideoEnded); doPlay(v); phase = 'playing'; stuckStart = Date.now(); stuckLast = v.currentTime || 0; } }, 1500); } setInterval(loop, TICK); })();