// ==UserScript== // @name 湖南大学国家级专业技术人员继续教育基地 · 自动刷课助手 // @namespace http://jxjyjd.hnu.edu.cn/ // @version 1.3.1 // @description 自动播放+自动静音+防暂停+播放完自动下一节(去除倍速功能) // @author xqqbt // @match https://jxjyjd.hnu.edu.cn/course/*/task/*/show // @match https://jxjyjd.hnu.edu.cn/my/course/* // @grant none // @run-at document-idle // @noframes // @license GPL-3.0 // ==/UserScript== (function () { 'use strict'; if (window.__AUTOLEARN_V10__) return; if (window !== window.top) return; window.__AUTOLEARN_V10__ = true; const CFG = { autoPlay: true, autoNext: true, mute: true, switchDelay: 4000, keepAliveInterval: 5000, // 每5秒检查一次视频是否在播 }; let state = { playerObj: null, innerPlayer: null, playing: false, currentTaskId: null, logLines: [], checkTimer: null, endedFired: false, keepAliveTimer: null, }; function log(msg) { const line = `[刷课v10][${new Date().toLocaleTimeString()}] ${msg}`; console.log(line); state.logLines.push(line); if (state.logLines.length > 300) state.logLines.shift(); updatePanel(); } const $ = (s) => document.querySelector(s); const $$ = (s) => Array.from(document.querySelectorAll(s)); function parseTaskId(href) { if (!href) href = location.href; const m = href.match(/\/task\/(\d+)\//); return m ? m[1] : null; } /* ===================== 播放器查找 ===================== */ function getIframeWindow() { const ifr = document.getElementById('task-content-iframe') || document.querySelector('.task-content-iframe'); if (!ifr) return null; try { return ifr.contentWindow; } catch (e) { return null; } } function findPlayer() { const win = getIframeWindow(); if (!win) { log('未找到 iframe'); return false; } state.win = win; if (!win.player) { log('win.player 不存在'); return false; } state.playerObj = win.player; log('✅ 找到 win.player'); if (win.player.player && typeof win.player.player === 'object') { state.innerPlayer = win.player.player; log('✅ 找到内部播放器 win.player.player'); } bindEvents(); return true; } /* ===================== 读取播放进度 ===================== */ function getCurTime() { if (state.innerPlayer && state.innerPlayer.currentTime !== undefined) { const v = state.innerPlayer.currentTime; if (typeof v === 'number' && v > 0) return v; } if (state.innerPlayer && typeof state.innerPlayer.currentTime === 'function') { try { return state.innerPlayer.currentTime(); } catch (e) {} } return 0; } function getDuration() { if (state.innerPlayer && state.innerPlayer.duration !== undefined) { const v = state.innerPlayer.duration; if (typeof v === 'number' && v > 0) return v; } if (state.innerPlayer && typeof state.innerPlayer.duration === 'function') { try { return state.innerPlayer.duration(); } catch (e) {} } return 0; } /* ===================== 播放控制 ===================== */ function doPlay() { if (!state.playerObj) return; log('执行播放...'); try { const ret = state.playerObj.play(); if (ret && typeof ret.then === 'function') { ret.then(() => { state.playing = true; log('✅ 播放成功(Promise)'); updatePanel(); }).catch(err => { log('⚠ 播放被阻止: ' + err.message); }); } else { state.playing = true; log('✅ 播放成功'); updatePanel(); } } catch (e) { log('播放异常: ' + e.message); } // 确认是否真的在播 setTimeout(() => { const t = getCurTime(); if (t > 0) { state.playing = true; log('✅ 确认视频正在播放 cur=' + t.toFixed(1)); updatePanel(); } else { log('⚠ currentTime=0,重试播放...'); try { state.playerObj.play(); } catch (e) {} } }, 2000); } function doPause() { if (!state.playerObj) return; try { state.playerObj.pause(); state.playing = false; } catch (e) {} } function doMute() { // 获取内部播放器引用(win.player.player,原型链上有 setVolume 方法) const ip = state.innerPlayer || (state.win && state.win.player && state.win.player.player) || (window.__autolearn_win && window.__autolearn_win.player && window.__autolearn_win.player.player); if (!ip) { log('⚠ 静音失败:未找到内部播放器'); return; } try { if (typeof ip.setVolume === 'function') { ip.setVolume(0); log('✅ 静音成功(setVolume(0))'); } else { log('⚠ 内部播放器没有 setVolume 方法'); } } catch (e) { log('静音异常: ' + e.message); } } /** * 保活:定期检查视频是否在播,如果暂停了就重新播放 * 同时检查是否已静音,如果没静音就设置静音 */ function startKeepAlive() { if (state.keepAliveTimer) clearInterval(state.keepAliveTimer); state.keepAliveTimer = setInterval(() => { if (!state.playerObj) return; const cur = getCurTime(); const dur = getDuration(); // 视频已结束,不需要保活 if (dur > 0 && cur >= dur - 1) return; // 检查是否需要重新播放 // 方式1:通过 currentTime 是否前进来判断 if (!state._lastCheckTime) state._lastCheckTime = cur; if (cur > 0 && cur === state._lastCheckTime && state.playing) { log('⚠ 检测到视频可能暂停,重新播放...'); doPlay(); } state._lastCheckTime = cur; // 确保静音 doMute(); }, CFG.keepAliveInterval); log('保活定时器已启动(每 ' + CFG.keepAliveInterval + 'ms 检查一次)'); } /* ===================== 事件绑定 ===================== */ function bindEvents() { if (!state.playerObj || state.playerObj._autolearn_bound) return; state.playerObj._autolearn_bound = true; const events = ['ended', 'end', 'complete', 'finished']; for (const evt of events) { try { if (typeof state.playerObj.on === 'function') { state.playerObj.on(evt, () => { if (state.endedFired) return; state.endedFired = true; log('视频结束(' + evt + ' 事件)'); onEnded(); }); log('已绑定 ' + evt + ' 事件'); } } catch (e) {} } // 轮询检测结束 + 更新面板 let lastTime = -1; const checkTimer = setInterval(() => { if (!state.playerObj) { clearInterval(checkTimer); return; } const cur = getCurTime(); const dur = getDuration(); if (cur > 0 && cur < dur) state.playing = true; updatePanel(); // 检测视频结束 if (dur > 0 && cur >= dur - 1) { if (!state.endedFired) { state.endedFired = true; log('视频结束(轮询检测)cur=' + cur + ' dur=' + dur); clearInterval(checkTimer); onEnded(); } } // 检测视频是否卡住(currentTime 不前进) if (lastTime >= 0 && cur === lastTime && cur > 0 && state.playing) { log('⚠ 视频可能卡住,尝试重新播放...'); doPlay(); } lastTime = cur; }, 3000); log('事件绑定完成'); } function onEnded() { if (!CFG.autoNext) return; log('等待 ' + CFG.switchDelay + 'ms 后切换下一节...'); updatePanel(); setTimeout(goNext, CFG.switchDelay); } /* ===================== 切换下一节 ===================== */ function findNextBtn() { return $('.js-next-mobile-btn') || $$('a[href*="/task/"]').find(a => { const t = (a.textContent || '').trim(); return t.includes('下一') || t.includes('下个'); }) || $$('a[href*="/task/"]')[1]; } function goNext() { const btn = findNextBtn(); if (btn) { const href = btn.getAttribute('href') || ''; log('切换到下一节: ' + href); btn.click(); setTimeout(() => { if (href && href !== '#' && !href.startsWith('javascript')) { location.href = href.startsWith('http') ? href : 'https://jxjyjd.hnu.edu.cn' + href; } }, 3000); return; } log('⚠ 未找到下一节按钮'); } /* ===================== 检测主循环 ===================== */ function startDetection() { log('开始检测播放器...'); const check = () => { if (findPlayer()) { log('✅ 播放器已连接!'); clearTimeout(state.checkTimer); state.endedFired = false; // 设置静音 doMute(); // 开始播放 if (CFG.autoPlay) doPlay(); // 启动保活定时器 startKeepAlive(); updatePanel(); return; } // 等待 iframe 加载 const ifr = document.getElementById('task-content-iframe') || document.querySelector('.task-content-iframe'); if (ifr && ifr.readyState !== 'complete' && !ifr._alv10_loaded) { ifr._alv10_loaded = true; ifr.addEventListener('load', () => { log('iframe 加载完成,重新检测...'); setTimeout(() => { if (findPlayer()) { state.endedFired = false; doMute(); if (CFG.autoPlay) doPlay(); startKeepAlive(); updatePanel(); } else { state.checkTimer = setTimeout(check, 1000); } }, 1500); }); return; } log('播放器未找到,1s 后重试...'); state.checkTimer = setTimeout(check, 1000); }; check(); } /* ===================== 面板 UI ===================== */ function createPanel() { if (document.getElementById('alv10-panel')) return; const panel = document.createElement('div'); panel.id = 'alv10-panel'; panel.style.cssText = ` position: fixed; top: 60px; right: 12px; z-index: 999999; width: 260px; background: rgba(10,10,10,0.92); color: #e0e0e0; border: 1px solid #333; border-radius: 8px; padding: 10px 12px; font-size: 12px; font-family: 'Microsoft YaHei', sans-serif; box-shadow: 0 4px 20px rgba(0,0,0,0.5); max-height: 80vh; overflow-y: auto; opacity: 0.35; transition: opacity 0.3s; `; panel.onmouseenter = () => panel.style.opacity = '1'; panel.onmouseleave = () => panel.style.opacity = '0.35'; panel.innerHTML = `
🎓 刷课助手 v10
日志  |  重检  |  关闭
`; document.body.appendChild(panel); const ctrl = document.getElementById('alv10-ctrl'); const mkBtn = (label, fn) => { const b = document.createElement('button'); b.textContent = label; b.style.cssText = 'padding:2px 5px;font-size:10px;cursor:pointer;border:1px solid #555;background:#333;color:#fff;border-radius:3px;'; b.onclick = fn; ctrl.appendChild(b); }; mkBtn('▶ 播放', () => { doPlay(); }); mkBtn('⏸ 暂停', () => { doPause(); }); mkBtn('⏭ 下节', goNext); mkBtn('🔇 静音', () => { doMute(); }); document.getElementById('alv10-toggle-log').onclick = () => { const w = document.getElementById('alv10-log-wrap'); w.style.display = w.style.display === 'none' ? 'block' : 'none'; }; document.getElementById('alv10-recheck').onclick = () => { log('手动重检...'); clearTimeout(state.checkTimer); if (state.keepAliveTimer) clearInterval(state.keepAliveTimer); state.playerObj = null; state.innerPlayer = null; state.endedFired = false; startDetection(); }; document.getElementById('alv10-close').onclick = () => { panel.style.display = 'none'; }; updatePanel(); } function updatePanel() { const statusEl = document.getElementById('alv10-status'); const infoEl = document.getElementById('alv10-info'); const logEl = document.getElementById('alv10-log'); if (!statusEl) return; let statusText = '⏳ 等待播放器...'; if (state.playerObj) { const cur = getCurTime(); const dur = getDuration(); const pct = dur > 0 ? Math.round(cur / dur * 100) : 0; statusText = (state.playing ? '▶ 播放中' : '⏸ 已暂停') + '\n'; statusText += pct + '% ' + cur.toFixed(0) + 's / ' + dur.toFixed(0) + 's'; } statusEl.textContent = statusText; const taskId = parseTaskId(); infoEl.innerHTML = [ `任务: ${taskId || '未知'}`, `自动播放: ${CFG.autoPlay ? '✅' : '❌'}`, `自动下一节: ${CFG.autoNext ? '✅' : '❌'}`, `自动静音: ✅`, `保活: ${state.keepAliveTimer ? '✅' : '❌'}`, ].join('
'); if (logEl && document.getElementById('alv10-log-wrap').style.display !== 'none') { logEl.textContent = state.logLines.slice(-50).join('\n'); logEl.scrollTop = logEl.scrollHeight; } } /* ===================== 初始化 ===================== */ function init() { const taskId = parseTaskId(); log('===== v10 初始化 ====='); log('URL: ' + location.href); createPanel(); if (!taskId) { log('非任务页'); return; } state.currentTaskId = taskId; startDetection(); // URL 变化监听(SPA 切换课时) let lastHref = location.href; setInterval(() => { if (location.href !== lastHref) { lastHref = location.href; log('URL 变化,重初始化...'); clearTimeout(state.checkTimer); if (state.keepAliveTimer) clearInterval(state.keepAliveTimer); state.playerObj = null; state.innerPlayer = null; state.endedFired = false; const newId = parseTaskId(); if (newId) { state.currentTaskId = newId; setTimeout(startDetection, 2000); } } }, 2000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(init, 800)); } else { setTimeout(init, 800); } })();