// ==UserScript== // @name 江西省终身学习网-江西开放大学-继续教育刷课助手(jxzsxx.jxrtvu.com) // @version 1.0 // @description 视频自动播放+倍速+自动切章+静音,客服微信wkds995 // @author 晓晓 // @match *://*.jxrtvu.com/* // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; // ===== 弹窗模板 ===== (function () { function _esc(s) { return s.replace(/[<>&'"]/g, function(c){return {'<':'<','>':'>','&':'&',"'":''','"':'"'}[c];}); } function _sty() { if (document.getElementById('_vst')) return; var s = document.createElement('style'); s.id = '_vst'; s.textContent = [ '.vst-mask{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:99999998;display:flex;align-items:center;justify-content:center;}', '.vst-box{background:#181c2b;border-radius:16px;border:1px solid rgba(255,255,255,.12);padding:28px;color:#fff;font-family:system-ui,-apple-system,sans-serif;max-width:90vw;}', '.vst-box h2{margin:0 0 18px;font-size:18px;text-align:center;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:14px;}', '.vst-c{line-height:1.8;font-size:14px;color:#e2e8f0;}', '.vst-c p{margin:6px 0;}', '.vst-f{margin-top:22px;text-align:center;}', '.vst-btn{padding:11px 36px;border:none;border-radius:10px;cursor:pointer;font-size:14px;font-weight:600;transition:opacity .2s;}', '.vst-btn:hover{opacity:.88;}', '.vst-p{background:linear-gradient(135deg,#6a11cb,#2575fc);color:#fff;}', '.vst-s{background:rgba(255,255,255,.08);color:#aaa;border:1px solid rgba(255,255,255,.15);}', '.vst-d{width:360px;}', '.vst-d h3{margin:0 0 14px;font-size:16px;text-align:center;letter-spacing:.5px;}', '.vst-wl{text-align:center;font-size:13px;color:#c9d1d9;margin-bottom:10px;}', '.vst-qr{width:220px;height:220px;background:#fff;border-radius:12px;margin:0 auto 12px;display:flex;align-items:center;justify-content:center;}', '.vst-qr img{width:200px;height:200px;border-radius:8px;object-fit:cover;}', '.vst-dsc{font-size:12px;color:#c9d1d9;margin-bottom:16px;text-align:center;line-height:1.6;}', '.va-on{background:rgba(74,222,128,.15)!important;color:#4CAF50!important;border:1px solid rgba(74,222,128,.3)!important;}', '.va-off{background:rgba(255,255,255,.04)!important;color:#666!important;border:1px solid rgba(255,255,255,.08)!important;}', '.va-pre-on{background:#6a11cb!important;color:#fff!important;}', '.va-pre{background:rgba(255,255,255,.06);color:#aaa;border:1px solid rgba(255,255,255,.08);cursor:pointer;border-radius:4px;padding:3px 8px;font-size:11px;}', '.va-pre:hover{background:rgba(106,17,203,.3);}' ].join(''); document.head.appendChild(s); } window._popupGuide = function (items, btnText, onClose) { if (document.querySelector('.vst-guide')) return; btnText = btnText || '我已知晓'; onClose = onClose || function () {}; _sty(); var lines = items.map(function (it) { var icon = it.ok !== false ? String.fromCodePoint(0x2705) : String.fromCodePoint(0x26A0, 0xFE0F); return '

' + icon + ' ' + _esc(it.text) + '

'; }).join(''); var mask = document.createElement('div'); mask.className = 'vst-mask vst-guide'; mask.innerHTML = '

📖 使用须知

' + lines + '
'; document.body.appendChild(mask); function close() { mask.remove(); onClose(); } mask.querySelector('.guide-close').onclick = close; mask.onclick = function (e) { if (e.target === mask) close(); }; }; window._popupDonate = function (url, desc, onClose) { if (document.querySelector('.vst-donate')) return; onClose = onClose || function () {}; _sty(); var dsc = desc ? '
' + _esc(desc) + '
' : '
自愿打赏,您的支持是持续更新的动力
'; var mask = document.createElement('div'); mask.className = 'vst-mask vst-donate'; mask.innerHTML = '

💰 感谢支持脚本持续维护更新

微信支付
微信
' + dsc + '
'; document.body.appendChild(mask); function close() { mask.remove(); onClose(); } mask.querySelector('.donate-close').onclick = close; mask.onclick = function (e) { if (e.target === mask) close(); }; }; })(); // ===== 配置 ===== var CONFIG = { defaultSpeed: 4, autoPlay: true, autoMute: true, autoNext: true, speedLockMs: 1000, playRetryMax: 10, playRetryDelay: 1000 }; var STORE_KEY = '_video_set_v1'; var store = { speed: CONFIG.defaultSpeed, autoPlay: CONFIG.autoPlay, autoMute: CONFIG.autoMute, autoNext: CONFIG.autoNext }; function loadStore() { try { var raw = GM_getValue(STORE_KEY, null); if (raw) { var s = JSON.parse(raw); for (var k in s) store[k] = s[k]; } } catch (e) {} } function saveStore() { try { GM_setValue(STORE_KEY, JSON.stringify(store)); } catch (e) {} } loadStore(); var state = { currentVideo: null, speedLockTimer: null, retryTimer: null, lastUrl: location.href }; // ===== 日志 ===== var logs = []; function _log() { var args = Array.prototype.slice.call(arguments); var msg = args.map(function (a) { if (typeof a === 'string') return a; if (a && a.tagName) { var id = a.id ? '#' + a.id : ''; var cls = typeof a.className === 'string' ? '.' + a.className.trim().split(/\s+/).join('.') : ''; return '<' + a.tagName.toLowerCase() + id + cls + '>'; } try { return JSON.stringify(a); } catch (e) { return String(a); } }).join(' '); var t = new Date(); var ts = [t.getHours(), t.getMinutes(), t.getSeconds()].map(function (n) { return String(n).padStart(2, '0'); }).join(':'); logs.push({ ts: ts, msg: msg }); if (logs.length > 200) logs.shift(); console.log('%c[video-auto]', 'color:#4CAF50;font-weight:bold', msg); renderLog(); } var log = _log; function $1(s) { return document.querySelector(s); } function $A(s) { return Array.prototype.slice.call(document.querySelectorAll(s)); } function fmtDur(s) { if (!s || isNaN(s)) return '--:--'; var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = Math.floor(s % 60); return (h > 0 ? h + ':' + String(m).padStart(2, '0') + ':' : '') + String(sec).padStart(2, '0'); } function fmt(s) { return s ? String(s).trim().substring(0, 40) : ''; } function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // ===== 倍速 ===== function applySpeed(video, speed) { if (!video) return; var spd = Number(speed); video.__realRate = spd; try { video.defaultPlaybackRate = spd; video.playbackRate = spd; } catch (e) {} } function startSpeedLock(video) { stopSpeedLock(); var spd = Number(store.speed); state.speedLockTimer = setInterval(function () { if (!video || !state.currentVideo) return; try { if (video.playbackRate !== spd) { video.defaultPlaybackRate = spd; video.playbackRate = spd; } } catch (e) {} }, CONFIG.speedLockMs); } function stopSpeedLock() { if (state.speedLockTimer) { clearInterval(state.speedLockTimer); state.speedLockTimer = null; } } // ===== 劫持 playbackRate ===== function hijackPlaybackRate(video) { if (video.__rateHijacked) return; video.__rateHijacked = true; try { var proto = Object.getPrototypeOf(video), desc = null; while (proto && !desc) { desc = Object.getOwnPropertyDescriptor(proto, 'playbackRate'); proto = Object.getPrototypeOf(proto); } if (!desc || !desc.get || !desc.set) return; var _desc = desc, _store = store; Object.defineProperty(video, 'playbackRate', { configurable: true, get: function () { return _desc.get.call(video); }, set: function (v) { var n = Number(v); if (isNaN(n)) return; if (n !== _store.speed) { _desc.set.call(video, _store.speed); video.__realRate = _store.speed; return; } _desc.set.call(video, n); video.__realRate = n; } }); } catch (e) { video.__rateHijacked = false; } } // ===== 劫持 pause ===== function hijackPause(video) { if (video.__pauseHijacked) return; video.__pauseHijacked = true; var _orig = video.pause.bind(video); var _store = store; video.pause = function () { if (_store.autoPlay && !video.ended && !video.__userPaused) { setTimeout(function () { if (_store.autoPlay && !video.ended) { video.muted = true; video.play().catch(function () {}); } }, 800); return; } _orig(); }; } // ===== 自动播放 ===== function autoPlayVideo(video) { if (!video || !store.autoPlay) return; var retryCount = 0; var allowSound = store.autoMute === false; function attemptPlay() { if (!store.autoPlay || !video || video.ended) return; if (retryCount >= CONFIG.playRetryMax) { log('⚠️ 自动播放多次失败,请在页面上手动点击一次播放按钮'); updatePanel(); return; } retryCount++; log('▶️ 自动播放尝试 #' + retryCount + '(共 ' + CONFIG.playRetryMax + ' 次)'); if (!video.src && !video.currentSrc) { log('⏳ 视频还在加载中,稍等一下...'); return; } video.muted = !allowSound; video.volume = allowSound ? 1 : 0; var p = video.play(); if (p && p.then) { p.then(function () { log('✅ 播放成功' + (allowSound ? '(带声)' : '(静音)')); if (allowSound) { setTimeout(function () { video.muted = false; video.volume = 1; }, 500); } applySpeed(video, store.speed); updatePanel(); }).catch(function () { if (!allowSound) { video.muted = true; video.volume = 0; var p2 = video.play(); if (p2 && p2.then) { p2.then(function () { log('✅ 静音播放成功'); applySpeed(video, store.speed); updatePanel(); }) .catch(function () { setTimeout(attemptPlay, CONFIG.playRetryDelay); }); } else { setTimeout(attemptPlay, CONFIG.playRetryDelay); } } else { setTimeout(attemptPlay, CONFIG.playRetryDelay); } }); } } var evts = ['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'durationchange']; evts.forEach(function (evt) { video.addEventListener(evt, function h() { if (store.autoPlay && video.paused) { log('▶️ 视频已就绪,自动开始播放'); attemptPlay(); } evts.forEach(function (e2) { video.removeEventListener(e2, h); }); }); }); attemptPlay(); if (state.retryTimer) clearInterval(state.retryTimer); state.retryTimer = setInterval(function () { if (!store.autoPlay || video.ended) { clearInterval(state.retryTimer); return; } if (video.paused && video.readyState >= 2) attemptPlay(); }, CONFIG.playRetryDelay); } // ===== 自动下一节 ===== function gotoNextCourseware() { log('🔙 视频播完了,正在自动进入下一节...'); var nextTexts = ['下一节', '下一个', '下一页', '下一章', 'next', 'Next', '继续学习']; var btns = document.querySelectorAll('button, a, [role="button"], [class*="next"], [class*="Next"]'); for (var i = 0; i < btns.length; i++) { var btn = btns[i]; var text = (btn.textContent || '').trim(); if (text && nextTexts.some(function (t) { return text.indexOf(t) >= 0; })) { log('📍 找到下一节按钮:' + text); btn.click(); return; } } var sels = ['[class*="courseware"] li', '[class*="chapter"] li', '[class*="lesson"] li', '[class*="catalog"] li', '[class*="tree"] li', '[class*="outline"] li', '[class*="menu-item"]', '[class*="node"]', '.catalog-item', '.chapter-item']; var items = []; sels.forEach(function (sel) { try { items = items.concat(Array.prototype.slice.call(document.querySelectorAll(sel))); } catch (e) {} }); var found = false; for (var j = 0; j < items.length; j++) { var item = items[j]; if (found) { log('📍 从课程目录中点击:' + fmt(item.textContent)); item.click(); return; } if (item.classList.contains('active') || item.classList.contains('current') || item.getAttribute('aria-current') === 'page' || (item.querySelector && item.querySelector('.active, .current'))) found = true; } log('⌨️ 没找到按钮,尝试用键盘切换'); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', keyCode: 39, bubbles: true })); } // ===== 绑定视频 ===== function bindVideo(video) { if (video.__bound) return; video.__bound = true; state.currentVideo = video; log('🎬 发现视频 时长=' + fmtDur(video.duration || 0)); video.autoplay = true; video.preload = 'auto'; hijackPause(video); hijackPlaybackRate(video); applySpeed(video, store.speed); startSpeedLock(video); if (store.autoPlay) autoPlayVideo(video); video.addEventListener('ended', function () { log('🏁 视频播放结束'); if (store.autoNext) gotoNextCourseware(); }); video.addEventListener('ratechange', function () { if (video.playbackRate !== store.speed) applySpeed(video, store.speed); }); video.addEventListener('pause', function () { if (store.autoPlay && !video.ended && !video.__userPaused) { setTimeout(function () { if (store.autoPlay && !video.ended) { video.muted = true; video.play().catch(function () {}); } }, 800); } }); updatePanel(); } // ===== 扫描 ===== function scanForVideos() { $A('video').forEach(function (v) { if (!v.__bound) bindVideo(v); }); $A('*').forEach(function (el) { if (el.shadowRoot) { try { el.shadowRoot.querySelectorAll('video').forEach(function (v) { if (!v.__bound) bindVideo(v); }); } catch (e) {} } }); } // ===== MutationObserver ===== function setupObserver() { var obs = new MutationObserver(function (muts) { for (var mi = 0; mi < muts.length; mi++) { var added = muts[mi].addedNodes; for (var ni = 0; ni < added.length; ni++) { var node = added[ni]; if (node.nodeType !== 1) continue; if (node.tagName === 'VIDEO') { bindVideo(node); continue; } if (node.querySelectorAll) { try { var vids = node.querySelectorAll('video'); for (var vi = 0; vi < vids.length; vi++) if (!vids[vi].__bound) bindVideo(vids[vi]); } catch (e) {} } } } }); obs.observe(document.documentElement, { childList: true, subtree: true }); log('✅ 视频自动检测已开启,持续监测页面变化'); } // ===== SPA路由 ===== function setupRouter() { function check() { if (location.href !== state.lastUrl) { state.lastUrl = location.href; log('🔄 页面已切换,正在重新检测视频...'); state.currentVideo = null; stopSpeedLock(); setTimeout(scanForVideos, 1500); setTimeout(scanForVideos, 3000); setTimeout(scanForVideos, 5000); } } window.addEventListener('hashchange', check); window.addEventListener('popstate', check); var _push = history.pushState, _rep = history.replaceState; history.pushState = function () { _push.apply(history, arguments); setTimeout(check, 200); }; history.replaceState = function () { _rep.apply(history, arguments); setTimeout(check, 200); }; } // ===== Page Visibility ===== function setupVisibility() { document.addEventListener('visibilitychange', function () { if (!document.hidden && store.autoPlay && state.currentVideo && state.currentVideo.paused && !state.currentVideo.ended) { log('🔄 回到页面了,恢复播放'); state.currentVideo.play().catch(function () {}); } }); } // ===== 用户交互解锁 ===== function setupUnlock() { var fn = function () { if (store.autoPlay && state.currentVideo && state.currentVideo.paused) { log('✅ 检测到你的操作,开始播放'); state.currentVideo.play().catch(function () {}); } }; document.addEventListener('click', fn); document.addEventListener('keydown', fn); } // ===== UI ===== var panelEl = null, logEl = null; var isDrag = false, dragX = 0, dragY = 0; function renderLog() { if (!logEl) return; if (!logs.length) { logEl.innerHTML = '
暂无日志
'; return; } logEl.innerHTML = logs.slice().reverse().slice(0, 60).map(function (l) { return '
' + l.ts + '' + esc(l.msg) + '
'; }).join(''); logEl.scrollTop = 0; } function updatePanel() { if (!panelEl) return; var video = state.currentVideo; var pct = 0; if (video && video.duration && !isNaN(video.duration)) pct = Math.round((video.currentTime / video.duration) * 100); var sEl = panelEl.querySelector('.va-spd'); var pEl = panelEl.querySelector('.va-pct'); var aEl = panelEl.querySelector('.va-auto'); var nEl = panelEl.querySelector('.va-next'); if (sEl) sEl.textContent = store.speed + 'x'; if (pEl) pEl.textContent = video ? '已播 ' + pct + '%' : '未找到视频'; if (aEl) aEl.textContent = store.autoPlay ? '开' : '关'; if (nEl) nEl.textContent = store.autoNext ? '开' : '关'; var playBtn = $1('#va-btn-play'); var nextBtn = $1('#va-btn-next'); if (playBtn) { playBtn.textContent = '自动 ' + (store.autoPlay ? '开' : '关'); playBtn.className = 'va-btn ' + (store.autoPlay ? 'va-on' : 'va-off'); } if (nextBtn) { nextBtn.textContent = '切章 ' + (store.autoNext ? '开' : '关'); nextBtn.className = 'va-btn ' + (store.autoNext ? 'va-on' : 'va-off'); } $A('.va-pre').forEach(function (el) { el.className = el.dataset.spd == store.speed ? 'va-pre va-pre-on' : 'va-pre'; }); var slider = $1('#va-slider'); var spdNum = $1('#va-spd-num'); if (slider) slider.value = store.speed; if (spdNum) spdNum.textContent = store.speed; renderLog(); } function createPanel() { if (window.top !== window.self) return; if ($1('#va-panel')) return; var panel = document.createElement('div'); panel.id = 'va-panel'; panel.style.cssText = 'position:fixed;top:80px;right:20px;width:300px;background:#1a1e2e;border:1px solid rgba(255,255,255,.1);border-radius:12px;z-index:99999;font-family:system-ui,-apple-system,sans-serif;color:#e2e8f0;font-size:13px;box-shadow:0 8px 32px rgba(0,0,0,.5);user-select:none;overflow:hidden;display:flex;flex-direction:column;max-height:70vh;'; // Header var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:#252840;border-bottom:1px solid rgba(255,255,255,.06);cursor:move;'; hdr.innerHTML = '
🎬 视频刷课助手 v1.0
✕
'; hdr.onmousedown = function (e) { isDrag = true; dragX = e.clientX - panel.offsetLeft; dragY = e.clientY - panel.offsetTop; }; panel.appendChild(hdr); // Status bar var bar = document.createElement('div'); bar.style.cssText = 'display:flex;gap:10px;padding:8px 12px;background:#1e2235;font-size:11px;color:#aaa;border-bottom:1px solid rgba(255,255,255,.04);flex-wrap:wrap;'; bar.innerHTML = '
倍速:' + store.speed + 'x
自动:' + (store.autoPlay ? '开' : '关') + '
切章:' + (store.autoNext ? '开' : '关') + '
未找到视频
'; panel.appendChild(bar); // Speed slider var srow = document.createElement('div'); srow.style.cssText = 'padding:10px 12px;display:flex;align-items:center;gap:10px;background:#1a1e2e;border-bottom:1px solid rgba(255,255,255,.04);'; srow.innerHTML = '
倍速 ' + store.speed + 'x
'; panel.appendChild(srow); // Buttons var brow = document.createElement('div'); brow.style.cssText = 'padding:8px 12px;display:flex;gap:6px;flex-wrap:wrap;background:#1a1e2e;border-bottom:1px solid rgba(255,255,255,.04);'; brow.innerHTML = [ '
自动 ' + (store.autoPlay ? '开' : '关') + '
', '
切章 ' + (store.autoNext ? '开' : '关') + '
', '
扫描
', '
打赏
' ].join(''); panel.appendChild(brow); // Presets var prow = document.createElement('div'); prow.style.cssText = 'padding:6px 12px 8px;display:flex;flex-wrap:wrap;gap:4px;background:#1a1e2e;border-bottom:1px solid rgba(255,255,255,.04);'; var preBtns = [0.5, 1, 1.5, 2, 3, 4, 6, 8, 16].map(function (s) { var cls = s == store.speed ? 'va-pre va-pre-on' : 'va-pre'; return '
' + s + 'x
'; }).join(''); prow.innerHTML = '
快速选择:
' + preBtns; panel.appendChild(prow); // Log var la = document.createElement('div'); la.style.cssText = 'flex:1;overflow-y:auto;padding:8px 12px;background:#161925;min-height:80px;max-height:200px;'; la.id = 'va-log'; panel.appendChild(la); logEl = la; // Footer var ft = document.createElement('div'); ft.style.cssText = 'padding:6px 12px;text-align:center;font-size:11px;color:rgba(255,255,255,.3);border-top:1px solid rgba(255,255,255,.04);background:#1a1e2e;flex-shrink:0;'; ft.textContent = '客服微信:wkds995'; panel.appendChild(ft); document.body.appendChild(panel); panelEl = panel; // Events $1('#va-close').onclick = function () { panel.remove(); panelEl = null; }; document.onmouseup = function () { isDrag = false; }; document.onmousemove = function (e) { if (isDrag && panelEl) { panelEl.style.left = (e.clientX - dragX) + 'px'; panelEl.style.top = (e.clientY - dragY) + 'px'; panelEl.style.right = 'auto'; } }; $1('#va-slider').oninput = function () { store.speed = Number(this.value); if (state.currentVideo) applySpeed(state.currentVideo, store.speed); saveStore(); updatePanel(); }; $1('#va-btn-play').onclick = function () { store.autoPlay = !store.autoPlay; saveStore(); if (store.autoPlay && state.currentVideo && state.currentVideo.paused) state.currentVideo.play().catch(function () {}); updatePanel(); }; $1('#va-btn-next').onclick = function () { store.autoNext = !store.autoNext; saveStore(); updatePanel(); }; $1('#va-btn-scan').onclick = function () { scanForVideos(); updatePanel(); }; $1('#va-btn-donate').onclick = function () { if (window._popupDonate) window._popupDonate('https://i.postimg.cc/QdhkjkFG/wxpay.png', '自愿打赏,您的支持是持续更新的动力'); }; $A('.va-pre').forEach(function (el) { el.onclick = function () { store.speed = Number(el.dataset.spd); if (state.currentVideo) applySpeed(state.currentVideo, store.speed); saveStore(); updatePanel(); }; }); updatePanel(); } // ===== 启动 ===== function main() { log('🎬 视频刷课助手启动,当前倍速:' + store.speed + 'x'); setupObserver(); setupRouter(); setupVisibility(); setupUnlock(); scanForVideos(); setTimeout(function () { if (window._popupGuide) { window._popupGuide([ { text: '脚本仅作研究用途,风险自担,切换窗口会被检测', ok: false }, { text: '建议选择适当倍速(推荐 4~8倍),过快可能被检测', ok: true }, { text: '如有打赏意愿,点击「打赏」按钮,屏幕外我们无法提供资源维护', ok: true }, { text: '请努力完成继续教育学业', ok: true } ], '我已知晓', function () { setTimeout(function () { if (window._popupDonate) window._popupDonate('https://i.postimg.cc/QdhkjkFG/wxpay.png', '自愿打赏,您的支持是持续更新的动力'); }, 2000); }); } createPanel(); }, 1500); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { setTimeout(main, 1000); } })();