// ==UserScript== // @name 庆阳继续教育自动看视频 // @namespace https://qingyang.zgzjzj.com/ // @version 4.3 // @description 自动播放庆阳专技继续教育平台视频,支持多页小节、后台播放、静音、自动小节切换、弹窗处理、防误跳 // @author AutoScript // @match https://qingyang.zgzjzj.com/* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ==================== 后台播放限制解除 ==================== // @run-at document-start 确保在页面脚本之前执行 function bypassBackgroundRestriction() { // 1. 覆盖 document.hidden / visibilityState const overrides = { hidden: { get: () => false }, visibilityState: { get: () => 'visible' }, webkitHidden: { get: () => false }, webkitVisibilityState: { get: () => 'visible' }, }; for (const [prop, desc] of Object.entries(overrides)) { try { Object.defineProperty(document, prop, { ...desc, configurable: true }); } catch (e) { /* 只读属性 */ } } // 2. 捕获阶段拦截所有可见性/焦点事件 const block = ['visibilitychange', 'webkitvisibilitychange', 'blur', 'pagehide']; block.forEach(evt => { window.addEventListener(evt, function (e) { e.stopImmediatePropagation(); e.stopPropagation(); }, true); document.addEventListener(evt, function (e) { e.stopImmediatePropagation(); e.stopPropagation(); }, true); }); } bypassBackgroundRestriction(); // ==================== 全局静音 ==================== let globalMuted = true; function muteAll(mute) { globalMuted = mute; document.querySelectorAll('video, audio').forEach(el => { el.muted = mute; if (mute) el.volume = 0; }); // videojs 实例 try { document.querySelectorAll('.video-js').forEach(el => { if (el.player && el.player.muted) el.player.muted(mute); }); } catch (e) { /* ignore */ } } // 监听新增的 media 元素 new MutationObserver(() => { if (globalMuted) { document.querySelectorAll('video:not([muted="true"]), audio:not([muted="true"])').forEach(el => { el.muted = true; el.volume = 0; }); } }).observe(document.documentElement, { childList: true, subtree: true }); // ==================== 配置 ==================== const CONFIG = { stepDelay: 2000, pollInterval: 5000, sectionTimeout: 20 * 60 * 1000, minPlayRatio: 0.5, // 小节至少播放视频的50%才认为完成 minPlaySeconds: 60, // 或者至少播放60秒 autoStartPlay: true, debug: true, }; let isRunning = false; let shouldStop = false; function log(...args) { if (CONFIG.debug) console.log('[学习助手]', ...args); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function getPageType() { const url = location.href; if (url.includes('/my-all-course')) return 'courseList'; if (url.includes('/learncenter/play') || url.includes('/learncenter/study')) return 'playPage'; // DOM 兜底检测(URL 变体时更可靠) if (document.querySelector('.class-catlog') || document.querySelector('#playcourse')) return 'playPage'; if (document.querySelector('.buyCourse_classAble')) return 'courseList'; return 'unknown'; } // ==================== UI ==================== function createPanel(pageType) { const old = document.getElementById('auto-study-panel'); if (old) old.remove(); const panel = document.createElement('div'); panel.id = 'auto-study-panel'; panel.innerHTML = `
🎓 自动学习助手
${pageType === 'courseList' ? '课程列表—点开始' : '就绪,即将自动开始'}
后台播放已启用
`; document.body.appendChild(panel); document.getElementById('as-start').onclick = function () { shouldStop = false; isRunning = false; pageType === 'playPage' ? startPlayPage() : startCourseList(); }; document.getElementById('as-stop').onclick = function () { shouldStop = true; isRunning = false; setStatus('已停止'); }; document.getElementById('as-mute').onclick = function () { muteAll(!globalMuted); this.textContent = globalMuted ? '🔇 静音中' : '🔊 有声音'; }; // 初始静音 setTimeout(() => muteAll(true), 1000); } function setStatus(msg) { var el = document.getElementById('as-status'); if (el) el.textContent = msg; log(msg); } function setInfo(msg) { var el = document.getElementById('as-info'); if (el) el.textContent = msg; } // ==================== 视频控制 ==================== function getVideoEl() { return document.querySelector('#playcourse video, .video-js video, video.vjs-tech'); } function playVideo() { var v = getVideoEl(); if (!v) return; try { if (v.paused || v.ended) { v.play().catch(function () { var btn = document.querySelector('.vjs-big-play-button'); if (btn) btn.click(); }); } } catch (e) { /* ignore */ } } // ==================== 小节解析 ==================== function getAllSections() { var all = []; var chapters = document.querySelectorAll('.class-catlog > ul > li'); chapters.forEach(function (ch) { var sections = ch.querySelectorAll(':scope > ul > li'); sections.forEach(function (s) { all.push(s); }); }); return all; } function getSectionElement(index) { var all = getAllSections(); return all[index] || null; } function getSectionName(li) { var name = li.querySelector('.classname'); var num = li.querySelector('.sectionNum'); return (num ? num.textContent.trim() : '') + ' ' + (name ? name.textContent.trim() : ''); } function isSectionFinished(li) { return li.classList.contains('finished'); } // ==================== 分页控制 ==================== function getPaginationInfo() { // 优先在 .pagination 内查找(视频播放页),否则全局查找(课程列表页) var pager = document.querySelector('.pagination .el-pagination') || document.querySelector('.el-pagination'); if (!pager) return null; var pagerItems = pager.querySelectorAll('.el-pager .number'); var totalPages = pagerItems.length; if (totalPages <= 0) return null; var currentPage = 1; pagerItems.forEach(function (item) { if (item.classList.contains('active')) { currentPage = parseInt(item.textContent.trim()); } }); var nextBtn = pager.querySelector('.btn-next'); var hasNext = nextBtn && !nextBtn.disabled; return { currentPage: currentPage, totalPages: totalPages, hasNext: hasNext }; } async function goToNextPage() { var pager = document.querySelector('.pagination .el-pagination') || document.querySelector('.el-pagination'); if (!pager) { log('未找到分页组件'); return false; } var nextBtn = pager.querySelector('.btn-next'); if (!nextBtn || nextBtn.disabled) { log('没有下一页或按钮禁用'); return false; } // 记录当前页码和小节数量,用于判断页面是否真正切换 var oldPage = getPaginationInfo().currentPage; var oldSectionCount = getAllSections().length; nextBtn.click(); log('点击下一页 (从第' + oldPage + '页)'); // 等待分页状态变化(active 页码改变)或超时 var changed = false; for (var w = 0; w < 30; w++) { // 最多等30秒 await sleep(1000); var newPag = getPaginationInfo(); if (newPag && newPag.currentPage !== oldPage) { changed = true; log('页码已切换到第' + newPag.currentPage + '页'); break; } } if (!changed) { log('分页切换超时,尝试继续'); } // 等待小节内容刷新(DOM 变化) await sleep(2000); try { await waitForEl('.class-catlog > ul > li', 15000); } catch (e) { log('等待小节元素超时'); } // 再等2秒让视频播放器初始化 await sleep(2000); log('页面加载完成,准备播放'); return true; } // ==================== 时间解析 ==================== function parseRemainingMinutes() { var el = document.querySelector('.course-progress'); if (!el) return -1; var text = el.textContent.replace(/\s+/g, ' '); // 把换行符等统一为空格 var m = text.match(/还需观看\s*(\d+)\s*分钟/); if (m) return parseInt(m[1]); // 备选:尝试匹配"还需观看 XX 分钟"格式 var m2 = text.match(/还需观看\s*(\d+)/); if (m2) return parseInt(m2[1]); return -1; } function parseWatchedMinutes() { var el = document.querySelector('.course-progress'); if (!el) return 0; var text = el.textContent.replace(/\s+/g, ' '); var m = text.match(/已累计观看\s*(\d+)\s*分钟/); if (m) return parseInt(m[1]); var m2 = text.match(/已累计观看\s*(\d+)/); if (m2) return parseInt(m2[1]); return 0; } function parseMaxTime() { var els = document.querySelectorAll('.timelength'); for (var i = 0; i < els.length; i++) { var t = els[i].textContent; if (t.indexOf('最大观看') >= 0 || t.indexOf('观看时长') >= 0) { var m = t.match(/(\d+)/); if (m) return parseInt(m[1]); } } return -1; } // ==================== 课程内容反馈浮窗 ==================== function handleFeedbackBox() { var fb = document.querySelector('.feedBackBox'); if (!fb) return false; if (fb.offsetHeight < 5) return false; // 点击关闭图标 h3 img[src*="cancel"] var closeImg = fb.querySelector('h3 img[src*="cancel"]'); if (closeImg) { closeImg.click(); log('已关闭反馈浮窗'); return true; } return false; } // ==================== 弹窗处理 ==================== async function handleDialogs() { var dialogs = document.querySelectorAll('.el-dialog__wrapper'); for (var d = 0; d < dialogs.length; d++) { var dlg = dialogs[d]; if (dlg.offsetHeight < 5) continue; var style = window.getComputedStyle(dlg); if (style.display === 'none') continue; var title = dlg.querySelector('.el-dialog__title'); var titleText = title ? title.textContent.trim() : ''; // 视频抓拍/验证码 if (titleText.indexOf('视频抓拍') >= 0 || titleText.indexOf('验证码') >= 0) { setStatus('⚠ 需要手动验证码!'); await sleep(30000); continue; } // 随堂练习 if (titleText.indexOf('随堂练习') >= 0 || titleText.indexOf('练习') >= 0) { await closeQuiz(dlg); } // 评价 → "以后再评" if (titleText.indexOf('评价') >= 0 || titleText.indexOf('反馈') >= 0 || titleText.indexOf('感受') >= 0) { var later = dlg.querySelector('.el-button.normal, .dialog-footer .el-button--default'); if (later && later.textContent.indexOf('以后再评') >= 0) { later.click(); await sleep(800); } else { var cls = dlg.querySelector('.el-dialog__close, .el-icon-close'); if (cls) { cls.click(); await sleep(500); } } } // 提示/通知 if (titleText.indexOf('提示') >= 0 || titleText.indexOf('通知') >= 0) { var closeBtn = dlg.querySelector('.el-dialog__close, .el-icon-close'); if (closeBtn) { closeBtn.click(); await sleep(500); } } // 证书 if (titleText.indexOf('证书') >= 0) { var cb = dlg.querySelector('.el-dialog__close, .el-icon-close'); if (cb) { cb.click(); await sleep(500); } } // 问卷/调研 if (titleText.indexOf('问卷') >= 0 || titleText.indexOf('调研') >= 0) { var cancel = dlg.querySelector('.el-button.cancel, .dialog-footer .el-button--default'); if (cancel) { cancel.click(); await sleep(800); } } } } async function closeQuiz(dlg) { try { var cls = dlg.querySelector('.el-dialog__headerbtn, .el-icon-close'); if (cls) { cls.click(); await sleep(500); return; } var opts = dlg.querySelectorAll('.el-radio, .el-checkbox'); if (opts.length > 0) { opts[0].click(); await sleep(300); } var confirm = dlg.querySelector('.el-button--primary'); if (confirm) { confirm.click(); await sleep(500); } } catch (e) { /* ignore */ } } // ==================== 播放小节 ==================== async function playSection(sectionIndex) { var sec = getSectionElement(sectionIndex); if (!sec) return; var name = getSectionName(sec); log('--- 小节 ' + (sectionIndex + 1) + ': ' + name + ' ---'); setInfo('播放: ' + name); var playBtn = sec.querySelector('.play-btn'); if (playBtn) playBtn.click(); else sec.click(); await sleep(CONFIG.stepDelay); await waitForVideo(); playVideo(); var startTime = Date.now(); while (!shouldStop) { await sleep(CONFIG.pollInterval); handleFeedbackBox(); await handleDialogs(); var v = getVideoEl(); if (v && v.paused) playVideo(); // 每次循环都读取"还需观看"时间 var rem = parseRemainingMinutes(); var w = parseWatchedMinutes(); var playPct = 0; if (v && v.duration > 0) { playPct = v.currentTime / v.duration * 100; } // 更新状态显示 setStatus('小节' + (sectionIndex + 1) + ' | 已看' + w + '分 剩' + rem + '分'); setInfo('播放: ' + name + ' | ' + Math.floor(playPct) + '%'); // ---- 完成判断 ---- var completed = false; var reason = ''; // 最优先:还需观看时间为0 → 直接完成 if (rem === 0) { completed = true; reason = '所需时间已满足(还需0分钟)'; } // 视频真正播放完毕 if (!completed && v && v.duration > 0) { if (v.ended) { completed = true; reason = '视频已播放完毕(ended)'; } else if (v.currentTime >= v.duration - 2) { completed = true; reason = '视频即将播完'; } else if (playPct >= 95) { completed = true; reason = '播放进度已达' + Math.floor(playPct) + '%'; } } if (completed) { log('小节 ' + (sectionIndex + 1) + ' 完成! 原因: ' + reason); break; } // 超时保护 if (Date.now() - startTime > CONFIG.sectionTimeout) { log('小节 ' + (sectionIndex + 1) + ' 超时,跳过'); break; } } setInfo('等待...'); await sleep(CONFIG.stepDelay); } async function waitForVideo() { return new Promise(function (resolve) { var start = Date.now(); var check = function () { var v = getVideoEl(); if (v && v.duration > 0 && v.readyState >= 2) return resolve(); if (Date.now() - start > 20000) return resolve(); setTimeout(check, 500); }; check(); }); } // ==================== 播放页主逻辑 ==================== async function startPlayPage() { if (isRunning) return; isRunning = true; shouldStop = false; log('========================================'); log('开始自动学习'); log('========================================'); try { await sleep(3000); await waitForEl('.class-catlog', 20000); await sleep(2000); var pag = getPaginationInfo(); var totalPages = pag ? pag.totalPages : 1; log('分页信息: 共' + totalPages + '页'); // 第一轮:按顺序播放所有页的所有小节 log('--- 第一轮:遍历所有页 ---'); for (var pg = 1; pg <= totalPages; pg++) { if (shouldStop) break; // 如果不在目标页,先导航过去 if (pg > 1) { // 到了新的一页后重新获取分页状态 pag = getPaginationInfo(); } var allSections = getAllSections(); var sectionCount = allSections.length; log('第' + pg + '页: 共' + sectionCount + '个小节'); setStatus('第' + pg + '/' + totalPages + '页 | 共' + sectionCount + '小节'); if (sectionCount === 0) { log('第' + pg + '页无小节,跳过'); continue; } for (var i = 0; i < sectionCount; i++) { log(' [P' + pg + '-' + (i + 1) + '] ' + getSectionName(allSections[i]) + ' - ' + (isSectionFinished(allSections[i]) ? '已完成' : '待播放')); } // 播放当前页的所有小节 for (var i = 0; i < sectionCount; i++) { if (shouldStop) break; var rem = parseRemainingMinutes(); var w = parseWatchedMinutes(); setStatus('第' + pg + '页 播放 ' + (i + 1) + '/' + sectionCount + ' | 已看' + w + '分 剩' + rem + '分'); await playSection(i); } // 当前页播放完毕,尝试翻到下一页 if (!shouldStop && pg < totalPages) { log('第' + pg + '页完成,翻到第' + (pg + 1) + '页'); setStatus('翻页: 第' + pg + '→' + (pg + 1) + '页'); var success = await goToNextPage(); if (!success) { log('翻页失败,跳过后续页面'); break; } } } if (shouldStop) { setStatus('已停止'); return; } // 第二轮:补时长,直到"还需观看"时间为0 var remaining = parseRemainingMinutes(); log('第一轮完成: 还需' + remaining + '分'); if (remaining > 0) { log('--- 第二轮:补时长,从第1页第1节开始 ---'); // 先回到第1页 await goToPage1(); // 从第一节开始循环播放,直到时间满足 while (!shouldStop && remaining > 0) { setStatus('补时长: 还需' + remaining + '分 | 从第1节重播'); // 点击第一节 var firstSec = getSectionElement(0); if (firstSec) { var playBtn = firstSec.querySelector('.play-btn'); if (playBtn) playBtn.click(); else firstSec.click(); } await sleep(2000); await waitForVideo(); playVideo(); // 等待播放直到时间满足或视频播放完毕 var retryStart = Date.now(); while (!shouldStop) { await sleep(CONFIG.pollInterval); handleFeedbackBox(); await handleDialogs(); var v = getVideoEl(); if (v && v.paused) playVideo(); // 检查"还需观看"时间 remaining = parseRemainingMinutes(); setStatus('补时长中 | 还需' + remaining + '分'); // 时间满足了就停 if (remaining <= 0) { log('时间已满足! 还需' + remaining + '分'); break; } // 视频播放完毕了就换下一个视频继续 if (v && (v.ended || v.currentTime >= v.duration - 2)) { log('视频播放完毕,继续播放'); break; } // 超时保护 if (Date.now() - retryStart > CONFIG.sectionTimeout) { log('单个视频补时长超时'); break; } } } } setStatus('✅ 本课程完成!即将跳转下一门...'); setInfo('本课程完成,3秒后返回列表'); await sleep(3000); await handleDialogs(); // 自动跳回列表页,列表页会重新扫描找到下一个未完成的课程 if (!shouldStop) { window.location.href = '/learncenter/my-all-course'; } } catch (e) { log('出错:', e.message); setStatus('❌ ' + e.message); } finally { isRunning = false; } } // 回到第1页(补时长用) async function goToPage1() { var pag = getPaginationInfo(); if (!pag || pag.currentPage === 1) return; // 通过点击页码跳转到第1页 var pager = document.querySelector('.pagination .el-pagination') || document.querySelector('.el-pagination'); if (!pager) return; var page1Btn = pager.querySelector('.el-pager .number'); if (page1Btn && !page1Btn.classList.contains('active')) { var oldPage = pag.currentPage; page1Btn.click(); log('跳回第1页 (从第' + oldPage + '页)'); // 等待页码切换 for (var w = 0; w < 20; w++) { await sleep(1000); var newPag = getPaginationInfo(); if (newPag && newPag.currentPage === 1) break; } await sleep(2000); try { await waitForEl('.class-catlog > ul > li', 15000); } catch (e) {} await sleep(2000); } } // ==================== 课程列表页 ==================== async function startCourseList() { setStatus('扫描课程...'); await sleep(2000); var pag = getPaginationInfo(); var totalPages = pag ? pag.totalPages : 1; log('课程列表: 共' + totalPages + '页'); // 逐页扫描,找到第一个未完成的课程并进入 for (var pg = 1; pg <= totalPages; pg++) { if (shouldStop) break; if (pg > 1) { // 翻到下一页 var pager = document.querySelector('.pagination .el-pagination') || document.querySelector('.el-pagination'); if (pager) { var nextBtn = pager.querySelector('.btn-next'); if (!nextBtn || nextBtn.disabled) { log('课程列表没有下一页了'); break; } var oldPage = getPaginationInfo().currentPage; nextBtn.click(); log('课程列表翻页: ' + oldPage + ' → ' + (pg)); // 等待页码切换 for (var w = 0; w < 20; w++) { await sleep(1000); var newPag = getPaginationInfo(); if (newPag && newPag.currentPage !== oldPage) break; } await sleep(2000); try { await waitForEl('.buyCourse_listItem', 10000); } catch (e) {} await sleep(1000); } } // 扫描当前页的课程 var allItems = document.querySelectorAll('.buyCourse_listItem'); log('第' + pg + '页: 找到 ' + allItems.length + ' 个课程卡片'); var courses = []; allItems.forEach(function (item, idx) { var btns = item.querySelectorAll('.buyCourse_classAble .el-button'); var studyBtn = null; btns.forEach(function (btn) { var txt = btn.textContent.trim(); var isStudy = (txt.indexOf('学习') >= 0 || txt.indexOf('学 习') >= 0); var isExam = (txt.indexOf('考试') >= 0); if (isStudy && !isExam) { studyBtn = btn; } }); var nameEl = item.querySelector('.buyCourse_itemTitle'); var name = nameEl ? nameEl.textContent.trim().substring(0, 25) : '课程'; var progress = 0; var progressbar = item.querySelector('[role="progressbar"]'); if (progressbar) { var val = progressbar.getAttribute('aria-valuenow'); if (val) progress = parseInt(val); } if (progress === 0) { var progressText = item.querySelector('.progress_text'); if (progressText) { var match = progressText.textContent.match(/(\d+)/); if (match) progress = parseInt(match[1]); } } if (progress === 0) { var barInner = item.querySelector('.el-progress-bar__inner'); if (barInner) { var style = barInner.getAttribute('style'); if (style) { var match = style.match(/width:\s*(\d+)%/); if (match) progress = parseInt(match[1]); } } } log('课程[P' + pg + '-' + (idx + 1) + '] ' + name + ' 进度:' + progress + '% 有学习按钮:' + (studyBtn ? '是' : '否')); if (studyBtn) { courses.push({ btn: studyBtn, name: name, progress: progress }); } }); // 过滤未完成的 var todoCourses = courses.filter(function (c) { return c.progress < 100; }); if (todoCourses.length > 0) { log('第' + pg + '页: ' + todoCourses.length + '门待学习'); var target = todoCourses[0]; setInfo('进入: ' + target.name + ' (' + target.progress + '%)'); setStatus('进入: ' + target.name); target.btn.click(); await sleep(5000); if (getPageType() === 'courseList') { log('还在列表页,重新扫描'); } else { log('已进入播放页,脚本自动学习中...'); setStatus('播放中...完成后自动回列表'); } return; } log('第' + pg + '页课程全部已完成'); } setStatus('所有课程已完成'); } // ==================== 工具 ==================== function waitForEl(selector, timeout) { return new Promise(function (resolve, reject) { var el = document.querySelector(selector); if (el) return resolve(el); var obs = new MutationObserver(function () { var el = document.querySelector(selector); if (el) { obs.disconnect(); resolve(el); } }); obs.observe(document.body, { childList: true, subtree: true }); setTimeout(function () { obs.disconnect(); reject(new Error('timeout')); }, timeout); }); } // ==================== 初始化 ==================== function init() { var pt = getPageType(); log('页面: ' + pt + ', URL: ' + location.href); createPanel(pt); if (pt === 'playPage' && CONFIG.autoStartPlay) { setTimeout(function () { if (!isRunning && !shouldStop) startPlayPage(); }, 5000); } // 列表页:自动开始扫描(从播放页跳回时会触发) if (pt === 'courseList') { setTimeout(function () { if (!isRunning && !shouldStop) startCourseList(); }, 3000); } } // Vue SPA路由监听 var lastUrl = location.href; setInterval(function () { if (location.href !== lastUrl) { lastUrl = location.href; log('路由变化: ' + lastUrl); isRunning = false; shouldStop = false; setTimeout(init, 3000); } }, 2000); if (document.readyState === 'complete') init(); else window.addEventListener('load', init); })();