// ==UserScript== // @name [YIT]-刷课助手 // @version 2.0.1 // @author adapted // @license MIT // @description 燕京理工学院(yit.haiqikeji.com)在线学习平台刷课助手 — 自动播放视频、自动下一节、进度追踪 // @match *://*.haiqikeji.com/* // @run-at document-end // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; /********************************* 配置 ****************************************/ var CONFIG = { videoRate: 1, autoNext: true, nextDelay: 3, showBox: true, debug: true }; // 从 localStorage 加载 ['videoRate','autoNext','nextDelay','showBox'].forEach(function(k) { var v = localStorage.getItem('yit_' + k); if (v !== null) { if (v === 'true') CONFIG[k] = true; else if (v === 'false') CONFIG[k] = false; else { var n = parseFloat(v); CONFIG[k] = !isNaN(n) ? n : v; } } }); function saveConfig(key, value) { CONFIG[key] = value; localStorage.setItem('yit_' + key, value); } /********************************* 工具 ****************************************/ function formatTime(s) { if (!s || s < 0) return '00:00'; s = Math.floor(s); var h = Math.floor(s/3600), m = Math.floor((s%3600)/60), sec = s%60; var pad = function(n){return n<10?'0'+n:''+n;}; return (h>0?pad(h)+':':'') + pad(m) + ':' + pad(sec); } function dlog(msg, color) { if (!CONFIG.debug && color !== 'red' && color !== 'green') return; var t = new Date().toLocaleTimeString(); console.log('[YIT '+t+']', msg); try { var el = document.getElementById('yit-log'); if (el) { var div = document.createElement('div'); div.innerHTML = '['+t+'] '+msg+''; el.insertBefore(div, el.firstChild); if (el.children.length > 200) el.removeChild(el.lastChild); } } catch(e) {} } /********************************* UI ****************************************/ function buildPanel() { if (!CONFIG.showBox || document.getElementById('yit-box')) return; var html = '\
\
\ YIT 刷课助手 v2\ _\
\
\
\ \ \
\
\ \ \
\
\ \ \
\
\ \ \ \
\
\
\ 等待视频...\ 0%\
\
\
\
\
\ 00:00\ 剩余 --:--\ 00:00\
\
\
\ 就绪 - 等待视频加载...\
\
\
\
'; html = html.replace('__CHECKED__', CONFIG.autoNext ? 'checked' : ''); html = html.replace('__DELAY__', CONFIG.nextDelay); var div = document.createElement('div'); div.innerHTML = html; document.body.appendChild(div.firstElementChild); // 事件绑定 document.getElementById('yit-rate').value = CONFIG.videoRate; document.getElementById('yit-rate').addEventListener('change', function(){ saveConfig('videoRate', parseFloat(this.value)); applyRateToAll(); dlog('倍速 = '+CONFIG.videoRate+'x','blue'); }); document.getElementById('yit-autonext').addEventListener('change', function(){ saveConfig('autoNext', this.checked); dlog('自动下一节: '+(CONFIG.autoNext?'开':'关'),'blue'); }); document.getElementById('yit-delay').addEventListener('change', function(){ saveConfig('nextDelay', parseInt(this.value)||3); dlog('跳转等待: '+CONFIG.nextDelay+'s','blue'); }); document.getElementById('yit-detect').addEventListener('click', function(){ dlog('手动扫描视频...','blue'); scanAllVideos(); }); document.getElementById('yit-next').addEventListener('click', function(){ dlog('手动跳转下一节','blue'); resetProgress(); clickNext(); }); document.getElementById('yit-debug-btn').addEventListener('click', function(){ dlog('运行调试...','blue'); unsafeWindow.yitDebug(); }); // 最小化 var min = false; document.getElementById('yit-min').addEventListener('click', function(){ var b = document.getElementById('yit-body'); min = !min; b.style.display = min ? 'none' : ''; this.textContent = min ? '□' : '_'; }); // 拖拽 (function(){ var hdr = document.getElementById('yit-header'); var box = document.getElementById('yit-box'); var d = false, ox, oy; hdr.addEventListener('mousedown', function(e){ d=true; ox=e.clientX-box.offsetLeft; oy=e.clientY-box.offsetTop; }); document.addEventListener('mousemove', function(e){ if(!d)return; box.style.left=(e.clientX-ox)+'px'; box.style.top=(e.clientY-oy)+'px'; box.style.right='auto'; }); document.addEventListener('mouseup', function(){ d=false; }); })(); } /********************************* 视频处理核心 ****************************************/ // 递归搜索所有 video 元素(包括 iframe 和 shadow DOM) function findAllVideos(root) { root = root || document; var videos = []; // 直接查找 try { var direct = root.querySelectorAll('video'); for (var i = 0; i < direct.length; i++) videos.push(direct[i]); } catch(e) {} // 搜索 iframe try { var iframes = root.querySelectorAll('iframe'); for (var i = 0; i < iframes.length; i++) { try { var doc = iframes[i].contentDocument || iframes[i].contentWindow.document; if (doc) { var fv = doc.querySelectorAll('video'); for (var j = 0; j < fv.length; j++) videos.push(fv[j]); // 递归iframe内iframe var nested = findAllVideos(doc); for (var k = 0; k < nested.length; k++) videos.push(nested[k]); } } catch(e) { // 跨域 iframe 无法访问 } } } catch(e) {} // 搜索 shadow DOM try { var allEls = root.querySelectorAll('*'); for (var m = 0; m < allEls.length; m++) { if (allEls[m].shadowRoot) { var sv = allEls[m].shadowRoot.querySelectorAll('video'); for (var n = 0; n < sv.length; n++) videos.push(sv[n]); } } } catch(e) {} return videos; } function applyRateToAll() { var videos = findAllVideos(); videos.forEach(function(v) { try { v.playbackRate = CONFIG.videoRate; } catch(e) {} }); } function updateStatus(msg) { var el = document.getElementById('yit-status'); if (el) el.textContent = msg; } function resetProgress(videoInfo) { var bar = document.getElementById('yit-bar'); var pctEl = document.getElementById('yit-pct'); var curEl = document.getElementById('yit-cur'); var remainEl = document.getElementById('yit-remain'); var totalEl = document.getElementById('yit-total'); var vnameEl = document.getElementById('yit-vname'); if (bar) bar.style.width = '0%'; if (pctEl) pctEl.textContent = '0%'; if (curEl) curEl.textContent = '00:00'; if (vnameEl) vnameEl.textContent = (videoInfo && videoInfo.name) || '加载中...'; if (totalEl) totalEl.textContent = (videoInfo && videoInfo.durStr) || '--:--'; if (remainEl) remainEl.textContent = (videoInfo && videoInfo.durStr) ? '剩余 ' + videoInfo.durStr : '剩余 --:--'; } // 快速获取新视频信息(不等播放就开始显示时长) function quickVideoInfo() { var videos = findAllVideos(); if (videos.length > 0) { var v = videos[0]; var dur = v.duration; var durStr = (!isNaN(dur) && dur > 0) ? formatTime(dur) : null; var name = ''; try { name = (document.title || '').replace(/\s*[-_|]\s*.+$/, '').substring(0, 40) || '视频'; } catch(e) {} if (durStr || name) { return { name: name, durStr: durStr }; } } return null; } // 记录已处理的视频(用弱引用避免内存问题) var handledVideos = new WeakSet(); var skipMode = false; // 是否在跳过已完成视频的模式中 function processVideo(video) { if (handledVideos.has(video)) return; handledVideos.add(video); // ★ 关键:播放前先检查是否已完成 if (isCurrentItemCompleted()) { dlog('⏭️ 当前视频已完成,跳过播放', 'green'); updateStatus('已完成,跳过 → 下一节'); skipMode = true; if (CONFIG.autoNext) { setTimeout(function() { resetProgress(); clickNext(); }, 1000); } return; } skipMode = false; var dur = video.duration; var durStr = isNaN(dur) ? '未知' : formatTime(dur); dlog('发现未完成视频 | 时长:' + durStr + ' | readyState:' + video.readyState, 'green'); // 等待视频元数据加载 function onLoaded() { dlog('视频元数据就绪 | 时长:' + formatTime(video.duration), 'green'); startPlayback(video); } if (video.readyState >= 2) { startPlayback(video); } else { video.addEventListener('loadedmetadata', onLoaded, { once: true }); video.addEventListener('loadeddata', function(){ if (video.readyState >= 2) startPlayback(video); }, { once: true }); setTimeout(function(){ if (!video.dataset.yitPlaying) startPlayback(video); }, 5000); } } function startPlayback(video) { if (video.dataset.yitPlaying) return; // ★ 再次确认未完成才播放 if (isCurrentItemCompleted()) { dlog('⏭️ 二次确认已完成,跳过播放', 'green'); if (CONFIG.autoNext) { setTimeout(function() { resetProgress(); clickNext(); }, 1000); } return; } video.dataset.yitPlaying = '1'; var myGen = yitGeneration; // 记录此监听器所属世代,防止旧监听器更新UI var durStr = formatTime(video.duration) || '未知'; // 设置倍速 video.playbackRate = CONFIG.videoRate; video.muted = true; // 移除视频上的点击事件阻止 video.style.pointerEvents = 'auto'; // 提取视频标题 var vname = ''; try { var pageTitle = document.title || ''; vname = pageTitle.replace(/\s*[-_|]\s*.+$/, '').substring(0, 40) || '视频'; var vnameEl = document.getElementById('yit-vname'); if (vnameEl) vnameEl.textContent = vname; } catch(e) {} // 尝试播放 var playPromise = video.play(); if (playPromise && playPromise.then) { playPromise.then(function() { dlog('播放成功 ('+CONFIG.videoRate+'x) | 时长:'+durStr, 'green'); updateStatus('播放中 | ' + CONFIG.videoRate + 'x'); }).catch(function(err) { dlog('自动播放失败: '+err.message, 'orange'); updateStatus('等待用户交互...'); // 监听用户交互来触发播放 var resume = function() { video.play().then(function(){ dlog('用户交互后播放成功', 'green'); }).catch(function(){}); document.removeEventListener('click', resume); document.removeEventListener('keydown', resume); }; document.addEventListener('click', resume, { once: true }); document.addEventListener('keydown', resume, { once: true }); // 模拟点击视频区域 try { video.click(); video.dispatchEvent(new MouseEvent('click', { bubbles: true })); } catch(e) {} }); } // 实时进度条更新 video.addEventListener('timeupdate', function() { if (yitGeneration !== myGen) return; // 旧世代监听器,忽略 var cur = video.currentTime; var d = video.duration; // ★ 始终读取实时duration,不捕获闭包变量 if (!d || isNaN(d) || d <= 0) return; var pct = Math.min(100, Math.round(cur / d * 100)); var remain = Math.max(0, d - cur); var bar = document.getElementById('yit-bar'); var pctEl = document.getElementById('yit-pct'); var curEl = document.getElementById('yit-cur'); var remainEl = document.getElementById('yit-remain'); var totalEl = document.getElementById('yit-total'); if (bar) bar.style.width = pct + '%'; if (pctEl) pctEl.textContent = pct + '%'; if (curEl) curEl.textContent = formatTime(cur); if (remainEl) remainEl.textContent = '剩余 ' + formatTime(remain); if (totalEl) totalEl.textContent = formatTime(d); }); // 监听倍速被页面重置 video.addEventListener('ratechange', function() { if (yitGeneration !== myGen) return; if (video.playbackRate !== CONFIG.videoRate && video.dataset.yitPlaying) { dlog('倍速被重置为 '+video.playbackRate+'x,已恢复', 'orange'); video.playbackRate = CONFIG.videoRate; } }); // 监听暂停 video.addEventListener('pause', function() { if (yitGeneration !== myGen) return; if (video.dataset.yitPlaying && !video.ended) { dlog('视频被暂停,尝试恢复...', 'orange'); setTimeout(function(){ if (yitGeneration !== myGen) return; if (video.dataset.yitPlaying && video.paused && !video.ended) { video.play().catch(function(){}); } }, 500); } }); function doJump() { resetProgress(); updateStatus('跳转中...'); clickNext(); } function onVideoComplete() { if (video.dataset.yitCompleted) return; video.dataset.yitCompleted = '1'; video.dataset.yitPlaying = '0'; dlog('视频播放完成!', 'green'); updateStatus('已完成,' + CONFIG.nextDelay + '秒后跳转'); var bar = document.getElementById('yit-bar'); var pctEl = document.getElementById('yit-pct'); var remainEl = document.getElementById('yit-remain'); if (bar) bar.style.width = '100%'; if (pctEl) pctEl.textContent = '100%'; if (remainEl) remainEl.textContent = '剩余 00:00'; if (CONFIG.autoNext) { dlog(CONFIG.nextDelay+'秒后跳转下一节...', 'blue'); setTimeout(doJump, CONFIG.nextDelay * 1000); } else { dlog('自动下一节关闭中,请手动点击', 'orange'); } } // 把完成回调挂到 video 元素上,供外部兜底检测调用 video._yitComplete = onVideoComplete; // 监听结束 video.addEventListener('ended', function() { if (yitGeneration !== myGen) return; dlog('[ended事件触发]', 'green'); onVideoComplete(); }); // 监听卡顿/等待 video.addEventListener('waiting', function() { if (yitGeneration !== myGen) return; dlog('视频缓冲中...', 'orange'); }); video.addEventListener('canplay', function() { if (yitGeneration !== myGen) return; if (video.paused && video.dataset.yitPlaying && !video.ended) { video.play().catch(function(){}); } }); } function scanAllVideos() { var videos = findAllVideos(); dlog('扫描到 ' + videos.length + ' 个 video 元素', videos.length > 0 ? 'green' : 'orange'); videos.forEach(processVideo); // 打印 iframe 信息帮助调试 var iframes = document.querySelectorAll('iframe'); if (iframes.length > 0) { dlog('页面有 ' + iframes.length + ' 个iframe', 'blue'); iframes.forEach(function(f, i) { try { var doc = f.contentDocument || f.contentWindow.document; if (doc) { var vids = doc.querySelectorAll('video'); dlog(' iframe['+i+']: ' + vids.length + '个video, url=' + (f.src||'about:blank').substring(0,80), 'blue'); } } catch(e) { dlog(' iframe['+i+']: 跨域无法访问 (' + (f.src||'about:blank').substring(0,60) + ')', 'orange'); } }); } if (videos.length === 0 && iframes.length === 0) { dlog('未找到视频。页面可能使用canvas/其他方式播放。请截图发给我调试。', 'red'); } } /********************************* 完成状态检测 ****************************************/ // 检测当前视频/课程项是否已标记为完成 // 策略:检查页面 DOM 中各种可能的"已完成"标识 function isCurrentItemCompleted() { // 在section-item结构中查找当前正在播放/激活的节,看它是否已完成 var activeItem = null; var activeClasses = ['active', 'is-active', 'current', 'selected', 'is-playing', 'playing']; for (var c = 0; c < activeClasses.length; c++) { activeItem = document.querySelector('.section-item.' + activeClasses[c]) || document.querySelector('.section-item[class*="' + activeClasses[c] + '"]'); if (activeItem) break; } if (!activeItem) return false; // 检查激活项内部是否有完成标识 // 1. class中含completed/done/finish/checked/success if (/completed|done|finish|checked|success|passed|watched/i.test(activeItem.className)) { dlog('完成检测: section-item class=' + activeItem.className, 'blue'); return true; } // 2. 内部有勾图标 var checkIcon = activeItem.querySelector('[class*="check"], [class*="success"], [class*="finish"], [class*="complete"], [class*="done"], .el-icon-check, .anticon-check'); if (checkIcon) { dlog('完成检测: 找到完成图标', 'blue'); return true; } // 3. 有"已完成""已观看"文字 var text = activeItem.textContent || ''; if (/已完成|已观看|已通过|已学完/.test(text)) { dlog('完成检测: 找到已完成文字', 'blue'); return true; } // 4. 视频已播放到100%且ended var vids = findAllVideos(); for (var v = 0; v < vids.length; v++) { if (vids[v].ended || (vids[v].duration > 0 && vids[v].currentTime >= vids[v].duration - 1)) { dlog('完成检测: video已结束', 'blue'); return true; } } return false; } // 基于页面实际DOM结构的导航 // 结构: .chapter > .chapter-sections > .section-item (多个) > .section-header > .section-title function clickNext() { // 策略1: 找到当前激活的section-item,点下一个 function tryActiveSectionNext() { // 查找带有激活状态的section-item var activeClasses = ['active', 'is-active', 'current', 'selected', 'is-playing', 'playing']; var activeItem = null; for (var c = 0; c < activeClasses.length; c++) { activeItem = document.querySelector('.section-item.' + activeClasses[c]) || document.querySelector('.section-item[class*="' + activeClasses[c] + '"]'); if (activeItem) break; } if (activeItem) { dlog('找到激活节: ' + (activeItem.textContent||'').substring(0,30).replace(/\s/g,''), 'blue'); var nxt = activeItem.nextElementSibling; // 跳过非section-item的兄弟 while (nxt && !nxt.classList.contains('section-item')) { nxt = nxt.nextElementSibling; } if (nxt && nxt.classList.contains('section-item')) { dlog('→ 下一节: ' + (nxt.textContent||'').substring(0,30).replace(/\s/g,''), 'green'); nxt.click(); nxt.dispatchEvent(new MouseEvent('click', { bubbles: true })); return true; } // 当前是章内最后一节,找下一章的第一个section-item var parentChapter = activeItem.closest('.chapter'); if (parentChapter) { var nextChapter = parentChapter.nextElementSibling; while (nextChapter && !nextChapter.classList.contains('chapter')) { nextChapter = nextChapter.nextElementSibling; } if (nextChapter && nextChapter.classList.contains('chapter')) { var firstSection = nextChapter.querySelector('.section-item'); if (firstSection) { dlog('→ 下一章第一节: ' + (firstSection.textContent||'').substring(0,30).replace(/\s/g,''), 'green'); firstSection.click(); firstSection.dispatchEvent(new MouseEvent('click', { bubbles: true })); return true; } } } } return null; } // 策略2: 找到页面上所有section-item,找匹配当前页面标题的,点下一个 function trySectionByTitle() { var pageTitle = (document.title || '').replace(/\s+/g, '').substring(0,30); var items = document.querySelectorAll('.section-item'); for (var i = 0; i < items.length; i++) { var itemTitle = (items[i].textContent || '').replace(/\s+/g, '').substring(0,30); if (itemTitle && pageTitle && pageTitle.indexOf(itemTitle) !== -1 || itemTitle.indexOf(pageTitle) !== -1) { dlog('通过标题匹配到当前节: ' + itemTitle, 'blue'); var nxt = items[i].nextElementSibling; while (nxt && !nxt.classList.contains('section-item')) { nxt = nxt.nextElementSibling; } if (nxt && nxt.classList.contains('section-item')) { dlog('→ 下一节: ' + (nxt.textContent||'').substring(0,30).replace(/\s/g,''), 'green'); nxt.click(); nxt.dispatchEvent(new MouseEvent('click', { bubbles: true })); return true; } // 跨章 var ch = items[i].closest('.chapter'); if (ch) { var nch = ch.nextElementSibling; while (nch && !nch.classList.contains('chapter')) nch = nch.nextElementSibling; if (nch) { var fs = nch.querySelector('.section-item'); if (fs) { dlog('→ 下一章: ' + (fs.textContent||'').substring(0,30).replace(/\s/g,''), 'green'); fs.click(); fs.dispatchEvent(new MouseEvent('click', { bubbles: true })); return true; } } } } } return null; } // 策略3: 文本匹配下一节按钮 function tryTextMatch() { var els = document.querySelectorAll('button, a, span[class*="btn"], div[class*="btn"]'); for (var i = 0; i < els.length; i++) { var b = els[i]; if (!b.offsetParent) continue; var t = (b.textContent || '').replace(/\s/g, ''); if (t && /^下一[节章课讲个步题视]$|^继续学习$|^继续$|^next$/i.test(t)) { return b; } } return null; } // 策略4: 找箭头/下页图标 function tryIcon() { var icons = document.querySelectorAll('[class*="arrow-right"], [class*="ArrowRight"], [class*="arrow_next"], [class*="next-btn"], [class*="NextBtn"]'); for (var i = 0; i < icons.length; i++) { var p = icons[i].closest('button, a') || icons[i].parentElement; if (p && p.offsetParent) return p; } return null; } var strategies = [tryActiveSectionNext, trySectionByTitle, tryTextMatch, tryIcon]; var strategyNames = ['激活节下一项', '标题匹配下一项', '文本匹配按钮', '图标按钮']; for (var i = 0; i < strategies.length; i++) { try { var result = strategies[i](); if (result === true) { dlog('clickNext: 策略'+(i+1)+'('+strategyNames[i]+') 成功(直接跳转)', 'green'); updateStatus('跳转下一节...'); return true; } if (result && result !== true) { var label = (result.textContent || result.getAttribute('aria-label') || result.className || '').substring(0, 40).replace(/\s/g, ''); dlog('clickNext: 策略'+(i+1)+'('+strategyNames[i]+') 点击 ['+result.tagName+'] "' + label + '"', 'green'); updateStatus('跳转下一节...'); result.click(); result.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); return true; } } catch(e) { dlog('clickNext: 策略'+(i+1)+'异常: '+e.message, 'orange'); } } dlog('clickNext: 所有策略均失败! 打印section-item:', 'red'); var items = document.querySelectorAll('.section-item'); items.forEach(function(it, idx) { dlog(' ['+idx+'] ' + (it.textContent||'').replace(/\s/g,'').substring(0,50), 'red'); }); updateStatus('请手动点击下一节'); return false; } /********************************* 监听 DOM 变化 ****************************************/ var scanTimer = null; var observer = new MutationObserver(function() { if (scanTimer) clearTimeout(scanTimer); scanTimer = setTimeout(function() { var videos = findAllVideos(); videos.forEach(processVideo); }, 500); }); /********************************* 视频切换检测 (URL+标题+DOM) ****************************************/ var lastUrl = location.href; var lastTitle = document.title; var lastVideoSrc = ''; var switchLock = false; // 防止重复触发 var yitGeneration = 0; // 切换世代,防止旧监听器更新UI function onVideoSwitched() { if (switchLock) return; switchLock = true; setTimeout(function() { switchLock = false; }, 3000); dlog('检测到视频切换,重置进度并重新扫描...', 'blue'); yitGeneration++; handledVideos = new WeakSet(); // ★ 清除所有视频的播放标记(SPA可能复用video元素) var allVids = findAllVideos(); allVids.forEach(function(v) { v.dataset.yitPlaying = ''; v.dataset.yitCompleted = ''; }); // ★ 立即重置为空(不传旧视频信息,防止显示旧时长) resetProgress(); // 轮询获取新视频信息(等DOM更新和新视频metadata加载) var pollAttempts = 0; function pollNewVideoInfo() { pollAttempts++; var info = quickVideoInfo(); if (info && info.durStr) { resetProgress(info); dlog('新视频: ' + info.name + ' | 时长: ' + info.durStr, 'blue'); return; } if (pollAttempts < 15) { setTimeout(pollNewVideoInfo, 400); } else { dlog('超时:未能获取新视频时长', 'orange'); } } setTimeout(pollNewVideoInfo, 600); // loadedmetadata兜底(如果轮询拿到了就不再触发) setTimeout(function() { var vids = findAllVideos(); if (vids.length > 0) { var v = vids[0]; var handler = function() { var ds = formatTime(v.duration); if (!ds || ds === '00:00') return; var el = document.getElementById('yit-total'); var rel = document.getElementById('yit-remain'); var vnameEl = document.getElementById('yit-vname'); if (el) el.textContent = ds; if (rel) rel.textContent = '剩余 ' + ds; if (vnameEl && vnameEl.textContent === '加载中...') { try { vnameEl.textContent = (document.title || '').replace(/\s*[-_|]\s*.+$/, '').substring(0, 40) || '视频'; } catch(e) {} } dlog('视频时长已加载(metadata): ' + ds, 'blue'); }; if (v.readyState >= 2) { // 已经加载了metadata但quickVideoInfo没拿到,直接读取 handler(); } else { v.addEventListener('loadedmetadata', handler, { once: true }); } } }, 800); // 延迟处理完成检测和扫描 setTimeout(function() { if (isCurrentItemCompleted()) { dlog('⏭️ 当前视频已完成,自动跳过', 'green'); updateStatus('已完成,跳过'); skipMode = true; if (CONFIG.autoNext) clickNext(); } else { skipMode = false; updateStatus('就绪 - 等待视频加载...'); scanAllVideos(); } }, 2000); } // 检测URL变化 setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; lastTitle = document.title; onVideoSwitched(); } }, 800); // 检测标题变化 (SPA内切换课程节时常改标题) setInterval(function() { var curTitle = document.title; if (curTitle !== lastTitle) { lastTitle = curTitle; lastUrl = location.href; onVideoSwitched(); } }, 1500); // 检测video元素src变化 (同一个video元素切源) setInterval(function() { var vids = findAllVideos(); if (vids.length > 0) { var curSrc = vids[0].src || vids[0].currentSrc || ''; if (curSrc && curSrc !== lastVideoSrc) { lastVideoSrc = curSrc; onVideoSwitched(); } } else { lastVideoSrc = ''; } }, 2000); /********************************* 调试导出 ****************************************/ unsafeWindow.yitDebug = function() { var lines = []; lines.push('=== YIT 调试 ==='); lines.push('URL: ' + location.href); lines.push('CONFIG: ' + JSON.stringify(CONFIG)); lines.push(''); var videos = findAllVideos(); lines.push('video: ' + videos.length + '个'); videos.forEach(function(v, i) { lines.push(' ['+i+'] src='+(v.src||'blob')+' dur='+formatTime(v.duration)+' paused='+v.paused+' ended='+v.ended+' cur='+formatTime(v.currentTime)+' yitPlay='+v.dataset.yitPlaying+' yitDone='+v.dataset.yitCompleted); }); lines.push(''); var iframes = document.querySelectorAll('iframe'); lines.push('iframe: ' + iframes.length + '个'); iframes.forEach(function(f, i) { var info = ' ['+i+'] src=' + (f.src||'about:blank').substring(0,80); try { var doc = f.contentDocument||f.contentWindow.document; if(doc){ info += ' video='+doc.querySelectorAll('video').length+'个'; } } catch(e) { info += ' (跨域)'; } lines.push(info); }); lines.push(''); lines.push('=== 完成检测: ' + isCurrentItemCompleted() + ' ==='); lines.push(''); lines.push('=== 可见按钮 ==='); var bc = 0; document.querySelectorAll('button, a, span[class*="btn"], div[class*="btn"]').forEach(function(b) { if (b.offsetParent) { var t = (b.textContent||'').replace(/\s/g,'').substring(0,30); if (t) { lines.push(' ['+b.tagName+'] "'+t+'"'); bc++; } } }); if (!bc) lines.push(' (无)'); lines.push(''); lines.push('=== 激活状态元素 (class含active/current/selected) ==='); var ac = 0; document.querySelectorAll('[class*="active"], [class*="current"], [class*="selected"]').forEach(function(el) { if (el.offsetParent) { var t = (el.textContent||'').replace(/\s/g,'').substring(0,60); var c = (el.className||'').replace(/\s+/g,' ').substring(0,100); lines.push(' ['+(el.tagName||'?')+'] "'+t+'" class="'+c+'"'); ac++; } }); if (!ac) lines.push(' (无! 没有active/current/selected类)'); lines.push(''); lines.push('=== 课程列表项 (前20个) ==='); var lc = 0; document.querySelectorAll('.section-item, .chapter, .chapter-title').forEach(function(el) { if (el.offsetParent && lc < 20) { var t = (el.textContent||'').replace(/\s/g,'').substring(0,60); var c = (el.className||'').replace(/\s+/g,' '); // 检查el或其子元素是否有特殊状态 var status = ''; // 检查自身class if (/completed|done|finish|checked|success|passed|watched/i.test(el.className)) status += '[自身完成]'; // 检查子元素 if (el.querySelector('[class*="check"], [class*="done"], [class*="finish"], [class*="complete"], [class*="success"], .el-icon-check, .anticon-check, svg')) status += '[勾]'; if (el.querySelector('[class*="lock"], [class*="Lock"]')) status += '[锁]'; if (el.querySelector('[class*="play"], [class*="Play"]')) status += '[播放]'; if (status === '') status = '[无标记]'; // 额外:检查data属性 var ds = ''; try { for (var dk in el.dataset) { if (el.dataset[dk]) ds += ' data-'+dk+'='+el.dataset[dk]; } } catch(e) {} lines.push(' ['+(el.tagName||'?')+'] '+status+' "'+t+'" class="'+c+'"'+(ds?' '+ds:'')); lc++; } }); lines.push(''); lines.push('=== 页面标题/URL ==='); lines.push('title: ' + (document.title||'?')); lines.push('url: ' + location.href); lines.push(''); lines.push('=== 视频播放器区域 ==='); var playerArea = document.querySelector('[class*="player"], [class*="Player"], [class*="video"], [class*="Video"], video'); if (playerArea) { var pClass = (playerArea.className||'').replace(/\s+/g,' ').substring(0,80); var pTag = playerArea.tagName; var pParent = playerArea.parentElement; var ppClass = pParent ? (pParent.className||'').replace(/\s+/g,' ').substring(0,80) : ''; lines.push(' 播放器: <'+pTag+' class="'+pClass+'">'); lines.push(' 父元素: <'+ (pParent?pParent.tagName:'?') +' class="'+ppClass+'">'); } else { lines.push(' (未找到播放器区域)'); } lines.push(''); lines.push('=== 跳转测试 ==='); var r = clickNext(); lines.push('clickNext: ' + (r ? 'OK' : 'FAIL')); lines.push('==========================='); var full = lines.join('\n'); console.log(full); // 同时输出到面板日志 lines.forEach(function(l) { dlog(l, '#333'); }); updateStatus('调试完成,看面板日志'); return full; }; /********************************* 初始化 ****************************************/ function init() { dlog('YIT刷课助手 v2 启动', 'green'); buildPanel(); // 开始监听DOM observer.observe(document.body, { childList: true, subtree: true }); // 初始扫描 setTimeout(scanAllVideos, 2000); setTimeout(scanAllVideos, 5000); setTimeout(scanAllVideos, 10000); // 定期维护:倍速保持 + 暂停恢复 + 视频结束检测 + 跳过已完成 setInterval(function() { // ★ 如果在跳过模式,持续尝试跳转到下一个未完成的 if (skipMode) { if (isCurrentItemCompleted()) { clickNext(); } else { skipMode = false; scanAllVideos(); } return; } var videos = findAllVideos(); videos.forEach(function(v) { if (v.playbackRate !== CONFIG.videoRate && v.dataset.yitPlaying) { v.playbackRate = CONFIG.videoRate; } // 兜底检测:视频播到末尾但 ended 事件未触发 var d = v.duration; if (!isNaN(d) && d > 0 && !v.dataset.yitCompleted && v.dataset.yitPlaying) { var remaining = d - v.currentTime; if (remaining <= 2) { dlog('兜底: 视频接近结尾(剩余'+remaining.toFixed(1)+'s),直接触发完成', 'green'); v.currentTime = d; if (v._yitComplete) v._yitComplete(); } } // 恢复暂停的 (但不恢复已完成的) if (v.dataset.yitPlaying && v.paused && !v.ended && !v.dataset.yitCompleted) { v.play().catch(function(){}); } }); }, 3000); // 定期扫描新视频 setInterval(function() { var videos = findAllVideos(); videos.forEach(processVideo); }, 5000); dlog('初始化完成。在控制台运行 yitDebug() 查看调试信息', 'blue'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function(){ setTimeout(init, 1000); }); } else { setTimeout(init, 1000); } })();