// ==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);
}
})();