// ==UserScript== // @name 上海开放大学/上开自动刷视频【可加速及自动下一页】 // @namespace https://l.shou.org.cn/ // @namespace 一心向善 // @version 1.7 // @description 上海开放大学相关视频自动学习自动看视频程序【可加速及自动下一页】【需要答题及合作联系群主】。 // @author user // @match https://l.shou.org.cn/study/learnCatalogNew.aspx* // @match https://l.shou.org.cn/study/directory.aspx* // @match https://l.shou.org.cn/study/activity-preview.aspx* // @match https://l.shou.org.cn/activity-middle/Playback* // @match https://l.shou.org.cn/study/play.aspx* // @run-at document-idle // @noframes // @icon https://www.google.com/s2/favicons?sz=64&domain=shou.org.cn // @grant none // @license All Rights Reserved // ==/UserScript== /* * ==================== 平台接口(已实测抓取) ==================== * A. 学习目录视频(directory.aspx,DPlayer / HTML5 video) * 1) 单元与播放地址(返回 ldoc.shou.org.cn 带 token 的 mp4): * POST /study/action-directory-getCell * 2) 每 60 秒学习进度 + 完播上报: * POST /study/action-directory-CellLeaveLog * { logId, cellId, videoEndTime, videoLengthTime, playbackSpeed } * 3) 完成标记: * POST /study/action-student-signLearned { cellId, videoLength } * * B. 学习活动-直播课回看(activity-middle/Playback,videojs) * 1) 开始:POST /activity-middle/api/course-activity-process/play-back-view-log-start * { activityId, videoStartTime, timestamp, playRate } -> r.result 即 logId * 2) 每 60 秒 / 结束:POST .../play-back-view-log-end * { logId, videoEndTime, timestamp }(需请求头 Authorization: Bearer ) * 3) 播放地址:GET ./api/resource-process/transfer-file/ * * C. 学习活动-录播课回放(study/play.aspx?url=...) * 每 60 秒:POST /study/action-preview-recordJoinInfoForUrl { url } * 注:部分旧录像服务端解密失败,页面视频源为 “Decrypt Failed!”,脚本自动跳过。 * * 脚本不伪造任何进度:所有计时/上报仍由平台页面自身的 JS 完成。 * ================================================================ */ (function () { 'use strict'; if (window.__SHOU_AUTO_DASH__) return; window.__SHOU_AUTO_DASH__ = true; var STATE_KEY = 'SHOU_AUTO_STUDY_V2'; var CFG_KEY = 'SHOU_AUTO_STUDY_CFG_V1'; var DEF_CFG = { muted: true, dwellSec: 60, embed: true }; var path = location.pathname; var isCatalog = /learnCatalogNew\.aspx$/i.test(path); var isDirectory = /directory\.aspx$/i.test(path); var isActivityPreview = /activity-preview\.aspx$/i.test(path); var isPlayback = /\/activity-middle\/Playback/i.test(path); var isPlayAs = /play\.aspx$/i.test(path); /* ---------------- 基础工具 ---------------- */ function loadState() { try { return JSON.parse(localStorage.getItem(STATE_KEY) || 'null'); } catch (e) { return null; } } function saveStateNow() { try { localStorage.setItem(STATE_KEY, JSON.stringify(ENGINE.st)); } catch (e) {} } var saveTimer = null; function saveState() { if (saveTimer) return; saveTimer = setTimeout(function () { saveTimer = null; saveStateNow(); }, 600); } function loadCfg() { var c = {}; try { c = JSON.parse(localStorage.getItem(CFG_KEY) || '{}'); } catch (e) {} var out = {}; for (var k in DEF_CFG) out[k] = DEF_CFG[k]; for (var k2 in c) out[k2] = c[k2]; return out; } function saveCfg(c) { try { localStorage.setItem(CFG_KEY, JSON.stringify(c)); } catch (e) {} } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (m) { return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[m]; }); } function qs() { return new URLSearchParams(location.search); } function courseKey() { var p = qs(); return p.get('CourseOpenId') || p.get('courseOpenId') || p.get('minorCourseOpenId') || p.get('minorcourseopenid') || ''; } function ts() { var d = new Date(); function p(n) { return n < 10 ? '0' + n : '' + n; } return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()); } var ENGINE = { st: null, stage: null, poll: null, listeners: [], token: 0 }; /* ---------------- 队列构建 ---------------- */ function buildQueue() { var items = []; var seen = {}; var coid = courseKey(); var cont = document.getElementById('process_container'); if (cont) { var rows = cont.querySelectorAll('tr.dynamic_topic'); for (var i = 0; i < rows.length; i++) { var resList = rows[i].querySelectorAll('.sh-res'); for (var j = 0; j < resList.length; j++) { var res = resList[j]; var a = res.querySelector('a'); if (!a) continue; var type = ((res.querySelector('.sh-res-b') || {}).textContent || '').replace(/[\s()]/g, ''); var href = a.getAttribute('href') || ''; var cellId = a.getAttribute('data-id') || ''; if (!cellId) { try { var q = new URLSearchParams(href.split('?')[1] || ''); cellId = q.get('cellId') || q.get('cellid') || ''; } catch (e) {} } if (!cellId || seen[cellId]) continue; seen[cellId] = true; var url; if (/\/study\/directory\.aspx/i.test(href)) { url = new URL(href, location.origin).href; } else { url = location.origin + '/study/directory.aspx?CourseOpenId=' + encodeURIComponent(coid) + '&minorcourseopenid=' + encodeURIComponent(coid) + '&cellId=' + encodeURIComponent(cellId); } items.push({ kind: 'cell', type: type || '资源', name: a.getAttribute('title') || a.textContent.trim(), key: cellId, url: url }); } } } var actTb = document.getElementById('tb_activities'); if (actTb) { var trs = actTb.querySelectorAll('tr'); for (var k = 0; k < trs.length; k++) { var alink = trs[k].querySelector('a'); if (!alink) continue; var actUrl; try { actUrl = new URL(alink.getAttribute('href'), location.origin); } catch (e) { continue; } var activityId = actUrl.searchParams.get('activityId'); if (!activityId || seen[activityId]) continue; seen[activityId] = true; items.push({ kind: 'activity', type: '学习活动', name: alink.textContent.trim(), key: activityId, url: actUrl.href }); } } return items; } function newState(items) { var status = []; var progress = []; for (var i = 0; i < items.length; i++) { status.push('pending'); progress.push(0); } return { v: 2, catalogUrl: location.href, courseKey: courseKey(), items: items, idx: 0, running: false, status: status, progress: progress, logs: [] }; } /* ---------------- 控制台 UI ---------------- */ function $(s, r) { return (r || document).querySelector(s); } function addLog(msg) { var st = ENGINE.st; if (!st) return; st.logs.push('[' + ts() + '] ' + msg); if (st.logs.length > 150) st.logs = st.logs.slice(st.logs.length - 150); var box = $('#shou-log'); if (box) { box.textContent = st.logs.join('\n'); box.scrollTop = box.scrollHeight; } saveState(); } function setStatus(i, s) { if (ENGINE.st) ENGINE.st.status[i] = s; saveState(); updateSummary(); } function setProgress(i, pct) { pct = Math.max(0, Math.min(100, Math.round(pct))); if (ENGINE.st) ENGINE.st.progress[i] = pct; var el = document.getElementById('shou-pct-' + i); if (el) el.textContent = pct + '%'; saveState(); } function statusIcon(s) { return s === 'done' ? '✔' : s === 'running' ? '▶' : s === 'skipped' ? '✖' : '·'; } function statusColor(s) { return s === 'done' ? '#2e7d32' : s === 'running' ? '#1876d2' : s === 'skipped' ? '#c62828' : '#999'; } function renderList() { var box = $('#shou-list'); var st = ENGINE.st; if (!box || !st) return; var html = ''; for (var i = 0; i < st.items.length; i++) { var it = st.items[i]; var s = st.status[i] || 'pending'; var bg = s === 'running' ? '#eaf3fd' : (i % 2 ? '#fff' : '#fafbfc'); html += '
' + '' + statusIcon(s) + '' + '' + (i + 1) + '' + '' + esc(it.type) + '' + '' + esc(it.name) + '' + '' + ((st.progress[i] || 0) ? st.progress[i] + '%' : '') + '
'; } box.innerHTML = html; scrollCurrent(); } function scrollCurrent() { var st = ENGINE.st; if (!st) return; var box = $('#shou-list'); if (!box) return; var row = $('.shou-row[data-i="' + st.idx + '"]', box); if (!row) return; var rb = row.getBoundingClientRect(); var bb = box.getBoundingClientRect(); if (rb.top < bb.top || rb.bottom > bb.bottom) { box.scrollTop += (rb.top - bb.top) - 8; } } function updateSummary() { var st = ENGINE.st; if (!st) return; var n = st.items.length; var done = 0, skipped = 0; for (var i = 0; i < n; i++) { if (st.status[i] === 'done') done++; else if (st.status[i] === 'skipped') skipped++; } var sum = $('#shou-sum'); if (sum) sum.textContent = '总进度 ' + (done + skipped) + '/' + n + '(完成 ' + done + ',跳过 ' + skipped + ')'; var bar = $('#shou-bar-i'); if (bar) bar.style.width = (n ? ((done + skipped) / n * 100) : 0) + '%'; var cur = $('#shou-cur'); if (cur) { if (!n) cur.textContent = '正在识别课程目录…'; else if (st.idx >= n) cur.textContent = '全部任务已结束'; else cur.textContent = (st.running ? '正在执行 ' : '待执行 ') + (st.idx + 1) + '/' + n + ' · ' + st.items[st.idx].name; } var startBtn = $('#shou-b-start'); if (startBtn) startBtn.textContent = st.running ? '执行中…' : '开始/继续'; } function buildDash() { if (document.getElementById('shou-dash')) return; var d = document.createElement('div'); d.id = 'shou-dash'; d.style.cssText = 'position:fixed;right:16px;top:16px;z-index:2147483645;width:390px;' + 'max-height:calc(100vh - 32px);display:flex;flex-direction:column;background:#fff;' + 'border:1px solid #dfe3e8;border-radius:12px;box-shadow:0 10px 28px rgba(0,0,0,.18);' + 'font:13px/1.55 -apple-system,BlinkMacSystemFont,"Microsoft YaHei",Arial,sans-serif;color:#333;overflow:hidden;'; d.innerHTML = '
' + '上开自动学习【答题及合作联系群主】' + '收起
' + '
' + '
总进度 0/0
' + '
' + '
' + '
识别中…
' + '
' + '' + '' + '' + '
' + '
' + '' + '' + '' + '
' + '
任务列表(自动滚动;双击任意项从该项开始自动连播)
' + '
' + '
播放日志(自动滚动)
' + '
' + '
答题或批量合作,联系Q:640105435
' + 'Q群1:949193546|Q群2:756253160
' + '
'; document.body.appendChild(d); var hs = document.createElement('style'); hs.textContent = '.shou-row:hover{background:#f2f7fd !important;}'; document.head.appendChild(hs); var cfg = loadCfg(); $('#shou-c-embed').checked = cfg.embed; $('#shou-c-mute').checked = cfg.muted; $('#shou-c-dwell').value = cfg.dwellSec; $('#shou-c-embed').addEventListener('change', function () { cfg.embed = this.checked; saveCfg(cfg); addLog(cfg.embed ? '已切换:内嵌播放' : '已切换:跳转模式(下个任务生效)'); }); $('#shou-c-mute').addEventListener('change', function () { cfg.muted = this.checked; saveCfg(cfg); }); $('#shou-c-dwell').addEventListener('change', function () { cfg.dwellSec = Math.max(5, parseInt(this.value, 10) || 60); this.value = cfg.dwellSec; saveCfg(cfg); }); $('#shou-min').addEventListener('click', function () { var body = $('#shou-body'); var hidden = body.style.display === 'none'; body.style.display = hidden ? 'flex' : 'none'; this.textContent = hidden ? '收起' : '展开'; }); $('#shou-b-start').addEventListener('click', onStart); $('#shou-b-pause').addEventListener('click', onPause); $('#shou-b-reset').addEventListener('click', onReset); $('#shou-list').addEventListener('dblclick', function (e) { var row = e.target && e.target.closest ? e.target.closest('.shou-row') : null; if (!row) return; var i = parseInt(row.getAttribute('data-i'), 10); if (isNaN(i)) return; jumpTo(i); }); } function renderAll() { renderList(); updateSummary(); var log = $('#shou-log'); if (log && ENGINE.st) log.textContent = (ENGINE.st.logs || []).join('\n'); if (log) log.scrollTop = log.scrollHeight; } /* ---------------- 内嵌播放器 ---------------- */ function ensureStage() { if (ENGINE.stage && ENGINE.stage.parentNode) return ENGINE.stage; var s = document.createElement('iframe'); s.id = 'shou-stage'; s.setAttribute('allow', 'autoplay; fullscreen'); s.style.cssText = 'position:fixed;left:16px;bottom:16px;width:900px;height:600px;transform:scale(.5);' + 'transform-origin:bottom left;border:2px solid #1876d2;border-radius:8px;background:#000;' + 'z-index:2147483644;box-shadow:0 8px 24px rgba(0,0,0,.28);'; document.body.appendChild(s); ENGINE.stage = s; return s; } function clearWatch() { if (ENGINE.poll) { clearInterval(ENGINE.poll); ENGINE.poll = null; } ENGINE.hook = null; for (var i = 0; i < ENGINE.listeners.length; i++) { var l = ENGINE.listeners[i]; try { l[0].removeEventListener(l[1], l[2]); } catch (e) {} } ENGINE.listeners = []; } function listen(t, f, o) { try { t.addEventListener(f, o); ENGINE.listeners.push([t, f, o]); } catch (e) {} } function neutralizeFrame() { try { var w = ENGINE.stage && ENGINE.stage.contentWindow; if (w) { w.onbeforeunload = null; w.onunload = null; try { w.onbeforeunload_handler = null; w.onunload_handler = null; } catch (e) {} } } catch (e) {} } function hardenFrame() { try { var w = ENGINE.stage && ENGINE.stage.contentWindow; if (!w || !w.document) return false; w.alert = function (m) { addLog('[站内提示] ' + String(m)); }; w.confirm = function () { return true; }; w.open = function () { return null; }; w.onbeforeunload = null; w.onunload = null; return true; } catch (e) { return false; } } function navigateStage(url, cb) { var token = ++ENGINE.token; neutralizeFrame(); ENGINE.stage.onload = function () { if (token !== ENGINE.token) return; hardenFrame(); cb(); }; ENGINE.stage.src = url; } function iframeVideoSrc(v) { if (!v) return ''; var s = v.querySelector('source'); var src = (s && s.getAttribute('src')) || v.currentSrc || v.src || ''; return src; } function getFrameDoc() { try { var d = ENGINE.stage && ENGINE.stage.contentDocument; return d || null; } catch (e) { return null; } } function embedRejected(i) { var cfg = loadCfg(); cfg.embed = false; saveCfg(cfg); addLog('⚠ 站点拒绝内嵌加载,已自动切换为跳转模式'); jumpGo(ENGINE.st, i); } /* ---------------- 后台保活 ---------------- */ function activeVideo() { try { var d = getFrameDoc(); if (d) { var v = d.querySelector('video'); if (v) return v; } } catch (e) {} try { return document.querySelector('video'); } catch (e) { return null; } } function isPageHidden() { if (document.hidden) return true; try { var d = getFrameDoc(); return !!(d && d.hidden); } catch (e) { return false; } } function runSoon(fn, delay) { if (isPageHidden()) { try { fn(); } catch (e) {} return; } setTimeout(fn, delay); } /* 页面在后台时直接把流程转到下一项,避免被浏览器节流的 setTimeout 拖慢 */ function goNext(i, delay) { runSoon(function () { processItem(i + 1); }, delay); } function jumpNextSoon(st, delay) { runSoon(function () { jumpNext(st); }, delay); } var HEART = null; function startHeartbeat() { if (HEART) return; try { var code = 'setInterval(function(){postMessage(1)},2000);'; var burl = URL.createObjectURL(new Blob([code], { type: 'text/javascript' })); HEART = new Worker(burl); HEART.onmessage = function () { if (!ENGINE.st || !ENGINE.st.running) return; if (ENGINE.hook) { try { ENGINE.hook(); } catch (e) {} } var v = activeVideo(); if (!v || v.ended || v.readyState < 1) return; if (Math.abs(v.playbackRate - 1) > 0.01) { try { v.playbackRate = 1; } catch (e) {} } if (v.paused) { try { v.play(); } catch (e) {} addLog('检测到后台被暂停,已自动恢复播放'); } }; } catch (e) { HEART = null; } } document.addEventListener('visibilitychange', function () { var st = ENGINE.st; if (!st || !st.running) return; var v = activeVideo(); if (v && !v.ended) { try { v.playbackRate = 1; } catch (e) {} if (v.paused) { try { v.play(); } catch (e) {} } } if (document.hidden) addLog('已切到后台:继续 1 倍速播放,播完自动下一项'); else addLog('已回到前台:继续跟随当前任务'); }); /* ---------------- 播放 ---------------- */ function playVideo(v, i, label) { var cfg = loadCfg(); clearWatch(); startHeartbeat(); try { v.playbackRate = 1; } catch (e) {} if (cfg.muted) { try { v.muted = true; } catch (e) {} } var lockRate = function () { if (Math.abs(v.playbackRate - 1) > 0.01) { try { v.playbackRate = 1; } catch (e) {} } }; listen(v, 'ratechange', lockRate); var w = ENGINE.stage.contentWindow; try { if (w.dp && typeof w.dp.play === 'function') w.dp.play(); } catch (e) {} try { if (w.player && typeof w.player.play === 'function') w.player.play(); } catch (e) {} try { v.play(); } catch (e) {} var done = false; var finish = function () { if (done) return; done = true; clearWatch(); setStatus(i, 'done'); setProgress(i, 100); addLog('完成(' + (i + 1) + ')' + label); scrollCurrent(); goNext(i, 2200); }; var tick = function () { if (done) return; var d = v.duration; if (d && isFinite(d) && d > 0) { setProgress(i, Math.min(99, v.currentTime / d * 100)); if (v.currentTime >= d - 1.2) finish(); } }; listen(v, 'ended', finish); listen(v, 'timeupdate', tick); listen(v, 'error', function () { if (done) return; addLog('⚠ 视频加载失败,10 秒后跳过'); setTimeout(function () { if (done) return; done = true; clearWatch(); setStatus(i, 'skipped'); goNext(i, 600); }, 10000); }); ENGINE.poll = setInterval(function () { if (!ENGINE.st || !ENGINE.st.running) return; if (done) return; if (v.paused && !v.ended) { try { v.play(); } catch (e) {} } tick(); }, 1000); addLog('播放中(1 倍速):' + label); updateSummary(); } function dwellThenNext(i, it) { var cfg = loadCfg(); var total = cfg.dwellSec; var start = Date.now(); addLog('[' + it.type + '] 无视频,停留 ' + total + ' 秒计时后自动下一个:' + it.name); ENGINE.poll = setInterval(function () { if (!ENGINE.st || !ENGINE.st.running) return; var elapsed = (Date.now() - start) / 1000; setProgress(i, Math.min(99, elapsed / total * 100)); if (elapsed >= total) { clearWatch(); setStatus(i, 'done'); setProgress(i, 100); addLog('完成(' + (i + 1) + ')' + it.name); goNext(i, 800); } }, 1000); } /* ---------------- 内嵌引擎 ---------------- */ function processItem(i) { var st = ENGINE.st; if (!st) return; if (i >= st.items.length) { st.running = false; st.idx = i; saveStateNow(); clearWatch(); neutralizeFrame(); if (ENGINE.stage) ENGINE.stage.src = 'about:blank'; addLog('全部任务结束(' + st.items.length + '/' + st.items.length + ')'); renderAll(); return; } st.idx = i; st.status[i] = 'running'; saveStateNow(); renderList(); updateSummary(); var it = st.items[i]; addLog('开始(' + (i + 1) + '/' + st.items.length + ')[' + it.type + '] ' + it.name); if (!loadCfg().embed) { jumpGo(st, i); return; } stopPoll(); ensureStage(); startHeartbeat(); if (it.kind === 'cell') openCell(it, i); else openActivity(it, i); } function stopPoll() { clearWatch(); } function openCell(it, i) { navigateStage(it.url, function () { var start = Date.now(), nulls = 0, stopped = false; var step = function () { if (stopped || !ENGINE.st || !ENGINE.st.running) return; var d = getFrameDoc(); if (!d) { if (++nulls >= 16) { stop(); embedRejected(i); } return; } nulls = 0; var v = d.querySelector('video'); var src = iframeVideoSrc(v); if (v && src && !/Decrypt Failed/i.test(src)) { stop(); playVideo(v, i, '目录视频:' + it.name); return; } if (Date.now() - start >= 10000) { stop(); dwellThenNext(i, it); } }; var stop = function () { stopped = true; clearInterval(ENGINE.poll); ENGINE.poll = null; if (ENGINE.hook === step) ENGINE.hook = null; }; ENGINE.hook = step; ENGINE.poll = setInterval(step, 500); }); } function openActivity(it, i) { navigateStage(it.url, function () { var start = Date.now(), nulls = 0, stopped = false; var step = function () { if (stopped || !ENGINE.st || !ENGINE.st.running) return; var d = getFrameDoc(); if (!d) { if (++nulls >= 16) { stop(); embedRejected(i); } return; } nulls = 0; var w = ENGINE.stage.contentWindow; var links = d.querySelectorAll('a[href]'); var target = null; for (var j = 0; j < links.length; j++) { if (/\/activity-middle\/Playback/i.test(links[j].getAttribute('href') || '')) { target = links[j]; break; } } if (!target) { for (var k = 0; k < links.length; k++) { var h = links[k].getAttribute('href') || ''; if (/\/study\/play\.aspx/i.test(h) && /回放|回看/.test(links[k].textContent)) { target = links[k]; break; } } } if (target) { stop(); var url = new URL(target.getAttribute('href'), (w && w.location ? w.location.origin : location.origin)).href; addLog('进入回看:' + it.name); openPlayback(url, i, it); return; } if (Date.now() - start >= 10000) { stop(); addLog('该活动暂无可用回看,跳过'); setStatus(i, 'skipped'); runSoon(function () { processItem(i + 1); }, 800); } }; var stop = function () { stopped = true; clearInterval(ENGINE.poll); ENGINE.poll = null; if (ENGINE.hook === step) ENGINE.hook = null; }; ENGINE.hook = step; ENGINE.poll = setInterval(step, 500); }); } function openPlayback(url, i, it) { navigateStage(url, function () { var start = Date.now(), nulls = 0, stopped = false; var step = function () { if (stopped || !ENGINE.st || !ENGINE.st.running) return; var d = getFrameDoc(); if (!d) { if (++nulls >= 16) { stop(); embedRejected(i); } return; } nulls = 0; var v = d.querySelector('video'); var src = iframeVideoSrc(v); if (v && src && !/Decrypt Failed/i.test(src)) { stop(); playVideo(v, i, '活动回看:' + it.name); return; } if (Date.now() - start >= 12000) { stop(); addLog('回放无可用视频或加密源解密失败(Decrypt Failed),跳过'); setStatus(i, 'skipped'); runSoon(function () { processItem(i + 1); }, 800); } }; var stop = function () { stopped = true; clearInterval(ENGINE.poll); ENGINE.poll = null; if (ENGINE.hook === step) ENGINE.hook = null; }; ENGINE.hook = step; ENGINE.poll = setInterval(step, 500); }); } /* ---------------- 跳转模式(备用) ---------------- */ function jumpGo(st, idx) { st.idx = idx; if (idx >= st.items.length) { st.running = false; saveStateNow(); location.href = st.catalogUrl; return; } saveStateNow(); location.href = st.items[idx].url; } function jumpNext(st) { jumpGo(st, st.idx + 1); } function getJumpItem(st) { if (!st || !st.running || !st.items) return null; var it = st.items[st.idx]; if (!it) return null; var p = qs(); if (isDirectory) return (it.kind === 'cell' && p.get('cellId') === it.key) ? it : null; if (isActivityPreview) return (it.kind === 'activity' && p.get('activityId') === it.key) ? it : null; if (isPlayback) { var aid = p.get('activityId'); return (it.kind === 'activity' && (!aid || aid === it.key)) ? it : null; } if (isPlayAs) return (it.kind === 'activity') ? it : null; return null; } function jumpWireVideo(v, i, st) { var cfg = loadCfg(); startHeartbeat(); var done = false; var lock = function () { if (Math.abs(v.playbackRate - 1) > 0.01) { try { v.playbackRate = 1; } catch (e) {} } }; lock(); v.addEventListener('ratechange', lock); if (cfg.muted) { try { v.muted = true; } catch (e) {} } var finish = function () { if (done) return; done = true; setStatus(i, 'done'); setProgress(i, 100); addLog('完成(' + (i + 1) + ')' + st.items[i].name); jumpNextSoon(st, 2200); }; var check = function () { if (done) return; if (v.duration && isFinite(v.duration) && v.currentTime >= v.duration - 1.2) finish(); }; v.addEventListener('ended', finish); v.addEventListener('timeupdate', check); var tick = setInterval(function () { if (done) return; if (v.paused && !v.ended) { try { v.play(); } catch (e) {} } check(); }, 1000); try { v.play(); } catch (e) {} } function runJumpPage() { var st = loadState(); if (!st || !st.running || loadCfg().embed) return; var it = getJumpItem(st); if (!it) return; ENGINE.st = st; var i = st.idx; startHeartbeat(); if (isDirectory) { var startDir = Date.now(); var iv = setInterval(function () { var v = document.querySelector('video'); var src = iframeVideoSrc(v); if (v && src && !/Decrypt Failed/i.test(src)) { clearInterval(iv); jumpWireVideo(v, i, st); return; } if (Date.now() - startDir >= 8000) { clearInterval(iv); var cfg = loadCfg(); addLog('[' + it.type + '] 停留 ' + cfg.dwellSec + ' 秒后下一个:' + it.name); var dwellStart = Date.now(); var dw = setInterval(function () { if (Date.now() - dwellStart >= cfg.dwellSec * 1000) { clearInterval(dw); setStatus(i, 'done'); jumpNext(st); } }, 1000); } }, 1000); } else if (isActivityPreview) { var links = document.querySelectorAll('a[href]'); var target = null; for (var j = 0; j < links.length; j++) { if (/\/activity-middle\/Playback/i.test(links[j].getAttribute('href') || '')) { target = links[j]; break; } } if (!target) { for (var k = 0; k < links.length; k++) { var h = links[k].getAttribute('href') || ''; if (/\/study\/play\.aspx/i.test(h) && /回放|回看/.test(links[k].textContent)) { target = links[k]; break; } } } if (target) runSoon(function () { location.href = new URL(target.getAttribute('href'), location.origin).href; }, 1200); else runSoon(function () { setStatus(i, 'skipped'); jumpNext(st); }, 8000); } else if (isPlayback || isPlayAs) { var startPb = Date.now(); var iv2 = setInterval(function () { var v = document.querySelector('video'); var src = iframeVideoSrc(v); if (v && src && !/Decrypt Failed/i.test(src)) { clearInterval(iv2); jumpWireVideo(v, i, st); return; } if (Date.now() - startPb >= 15000) { clearInterval(iv2); addLog('回放无可用视频/解密失败,跳过'); setStatus(i, 'skipped'); runSoon(function () { jumpNext(st); }, 800); } }, 1000); } } /* ---------------- 控制 ---------------- */ function jumpTo(i) { var st = ENGINE.st; if (!st || !st.items || !st.items.length) return; if (i < 0 || i >= st.items.length) return; clearWatch(); ENGINE.token++; st.idx = i; st.running = true; saveStateNow(); renderList(); updateSummary(); addLog('↪ 双击跳转,从第 ' + (i + 1) + ' 项开始自动连播:' + st.items[i].name); scrollCurrent(); if (!loadCfg().embed) { jumpGo(st, i); return; } processItem(i); } function onStart() { var st = ENGINE.st; if (!st) return; if (!st.items.length) { addLog('未识别到学习目录资源或学习活动,请确认已进入“学习目录”页'); return; } if (st.idx >= st.items.length) { ENGINE.st = newState(buildQueue()); ENGINE.st.running = true; saveStateNow(); renderAll(); processItem(0); return; } st.running = true; saveStateNow(); updateSummary(); addLog('▶ 开始/继续自动学习'); if (!loadCfg().embed) { jumpGo(st, st.idx); return; } var w = ENGINE.stage && ENGINE.stage.contentWindow; var v = w && w.document.querySelector('video'); var src = iframeVideoSrc(v); if (v && src && !/Decrypt Failed/i.test(src) && st.status[st.idx] === 'running') { playVideo(v, st.idx, '继续:' + st.items[st.idx].name); } else { processItem(st.idx); } } function onPause() { var st = ENGINE.st; if (!st) return; st.running = false; saveStateNow(); clearWatch(); try { var w = ENGINE.stage && ENGINE.stage.contentWindow; var v = w && w.document.querySelector('video'); if (v) v.pause(); } catch (e) {} addLog('⏸ 已暂停'); updateSummary(); } function onReset() { clearWatch(); ENGINE.token++; neutralizeFrame(); if (ENGINE.stage) { try { ENGINE.stage.src = 'about:blank'; } catch (e) {} } ENGINE.st = newState(buildQueue()); saveStateNow(); renderAll(); addLog('已重置,全部任务归零'); } /* ---------------- 入口 ---------------- */ function initCatalog() { buildDash(); var q = buildQueue(); var st = loadState(); if (!st || st.v !== 2 || !st.items || st.items.length !== q.length || st.courseKey !== courseKey()) { st = newState(q); } else { st.items = q; // 刷新链接 if (!st.status || st.status.length !== q.length) st.status = newState(q).status; if (!st.progress || st.progress.length !== q.length) st.progress = newState(q).progress; if (!st.logs) st.logs = []; } ENGINE.st = st; saveStateNow(); renderAll(); if (st.running) { addLog('检测到未完成任务,自动继续…'); runSoon(function () { processItem(st.idx); }, 800); } } if (isCatalog) { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initCatalog); else initCatalog(); } else { var runOnReady = function () { runJumpPage(); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', runOnReady); else runOnReady(); } })();