// ==UserScript== // @name 巴中继续教育自动切课(跨大节修复版) // @namespace http://tampermonkey.net/ // @version 0.5.0 // @description 修复跨大科目跳转失败,自动展开折叠章节,支持完整二级目录遍历 // @author 自用辅助 // @match *://bzys.jjyxt.cn/* // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // ========== 配置区 ========== const CONFIG = { targetPlaybackRate: 2.0, // 目标倍速,建议1.2-1.5更安全 rateFluctuation: 0.2, // 倍速随机波动幅度 rateStep: 0.1, skipSec: 10, autoResume: true, autoMuted: true, autoSkipPopup: true, // 随机暂停间隔(毫秒):6-15分钟 pauseIntervalMin: 360000, pauseIntervalMax: 900000, // 随机暂停时长(毫秒):1-3秒 pauseDurationMin: 1000, pauseDurationMax: 3000, // 切课延迟随机范围(毫秒) nextDelayMin: 1000, nextDelayMax: 4000, // 下一课按钮匹配关键词 nextBtnKeywords: ['下一节', '下一课', '下一章节', '下一章', '继续学习', '下一个'], // ========== 目录选择器(不生效可按下方教程修改) ========== // 大节标题选择器:点击可展开/折叠的科目标题栏 chapterTitleSelector: '.chapter-title, .catalog-group-title, .section-header, .el-collapse-item__header', // 大节容器选择器:每个大节整体包裹元素 chapterGroupSelector: '.chapter-group, .catalog-group, .el-collapse-item', // 小节项选择器:每个具体课时的条目 sectionItemSelector: '.section-item, .course-item, .chapter-item, li.course, .list-item', // 当前播放项标记类名 activeClass: 'active, current, playing, now, on' }; // ================================================= let videoDom = null; let domObserver = null; let isJumping = false; let toastTimer = null; let pauseTimer = null; // ========== 前置:劫持playbackRate隐藏真实倍速 ========== (function hidePlaybackRate() { const proto = HTMLMediaElement.prototype; const originalDesc = Object.getOwnPropertyDescriptor(proto, 'playbackRate'); if (!originalDesc) return; Object.defineProperty(proto, 'playbackRate', { get: function() { return 1.0; }, set: function(value) { originalDesc.set.call(this, value); }, configurable: true, enumerable: true }); })(); // 悬浮提示 function showToast(text) { let toast = document.getElementById('helper-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'helper-toast'; toast.style.cssText = ` position: fixed; top: 80px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.75); color: #fff; padding: 8px 16px; border-radius: 6px; font-size: 14px; z-index: 99999; pointer-events: none; transition: opacity 0.3s; `; document.body.appendChild(toast); } toast.textContent = text; toast.style.opacity = '1'; clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.style.opacity = '0', 1200); } // 右下角状态面板 function createStatusPanel() { let panel = document.getElementById('study-status-panel'); if (panel) return panel; panel = document.createElement('div'); panel.id = 'study-status-panel'; panel.style.cssText = ` position: fixed; right: 10px; bottom: 10px; background: rgba(0,0,0,0.65); color: #fff; padding: 10px; border-radius: 8px; font-size:12px; z-index:99998; min-width:140px; line-height:1.6; `; panel.innerHTML = `
【学习辅助状态】
目标倍速:${CONFIG.targetPlaybackRate}x
播放:00:00/00:00
静音:否
H=快捷键说明
`; document.body.appendChild(panel); return panel; } function updateStatusPanel() { if (!videoDom) return; const panel = createStatusPanel(); panel.querySelector('#rate-line').innerText = `目标倍速:${CONFIG.targetPlaybackRate}x`; panel.querySelector('#mute-line').innerText = `静音:${videoDom.muted ? "是" : "否"}`; const cur = formatTime(videoDom.currentTime); const total = formatTime(videoDom.duration || 0); panel.querySelector('#time-line').innerText = `播放:${cur}/${total}`; } function formatTime(s) { if (isNaN(s)) return "00:00"; let m = Math.floor(s / 60); let sec = Math.floor(s % 60); return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; } function showHelpInfo() { const info = ` 快捷键列表: C:加速 +0.1 | X:减速 -0.1 Z:重置1倍速 | N:手动切下一课 ←→:进退${CONFIG.skipSec}秒 | 空格:播放/暂停 M:切换静音 | Ctrl↑↓:调音量 F:全屏 | H:关闭此提示 `.trim(); alert(info); } // 随机数工具 function random(min, max) { return Math.random() * (max - min) + min; } // ========== 核心:二级目录解析与跨大节跳转 ========== /** * 获取所有大节列表,每个大节包含:标题元素、小节列表 */ function getAllChapterGroups() { const groups = Array.from(document.querySelectorAll(CONFIG.chapterGroupSelector)); if (groups.length === 0) { // 降级:找不到大节容器时,直接扁平处理所有小节 const items = Array.from(document.querySelectorAll(CONFIG.sectionItemSelector)); return [{ titleEl: null, sections: items }]; } return groups.map(group => { const titleEl = group.querySelector(CONFIG.chapterTitleSelector) || group.firstElementChild; const sections = Array.from(group.querySelectorAll(CONFIG.sectionItemSelector)); return { groupEl: group, titleEl, sections }; }); } /** * 定位当前播放的小节位置 * 返回 { groupIndex, sectionIndex } 或 null */ function getCurrentPosition() { const groups = getAllChapterGroups(); const activeClasses = CONFIG.activeClass.split(',').map(c => c.trim()); for (let g = 0; g < groups.length; g++) { const group = groups[g]; for (let s = 0; s < group.sections.length; s++) { const section = group.sections[s]; // 匹配类名 const hasActiveClass = activeClasses.some(cls => section.classList.contains(cls.trim())); // 匹配文字特征 const hasActiveText = section.innerText.includes('正在播放') || section.innerText.includes('学习中'); if (hasActiveClass || hasActiveText) { return { groupIndex: g, sectionIndex: s, groups }; } } } return null; } /** * 等待大节展开,小节渲染完成 */ function waitForSections(groupEl, timeout = 3000) { return new Promise((resolve) => { const startTime = Date.now(); const check = () => { const sections = groupEl.querySelectorAll(CONFIG.sectionItemSelector); if (sections.length > 0 || Date.now() - startTime > timeout) { resolve(sections.length > 0); } else { requestAnimationFrame(check); } }; check(); }); } /** * 二级目录兜底切课(支持跨大节跳转) */ async function clickNextByCatalog() { const pos = getCurrentPosition(); if (!pos) { console.log('[自动切课] 无法定位当前课时'); return false; } const { groupIndex, sectionIndex, groups } = pos; const currentGroup = groups[groupIndex]; const isLastInGroup = sectionIndex >= currentGroup.sections.length - 1; const isLastGroup = groupIndex >= groups.length - 1; // 1. 当前大节还有下一小节:直接跳转 if (!isLastInGroup) { const nextSection = currentGroup.sections[sectionIndex + 1]; console.log(`[自动切课] 同大节跳转:第${sectionIndex+2}小节`); nextSection.click(); return true; } // 2. 当前是大节最后一节,且还有下一个大节:展开下一大节,跳转其首小节 if (isLastInGroup && !isLastGroup) { const nextGroup = groups[groupIndex + 1]; console.log('[自动切课] 到达当前大节末尾,展开下一个大科目'); showToast('正在展开下一科目...'); // 点击大节标题展开 if (nextGroup.titleEl) nextGroup.titleEl.click(); // 等待小节渲染 const success = await waitForSections(nextGroup.groupEl || nextGroup.titleEl?.parentElement); if (success) { // 重新获取该大节的小节列表 const newSections = Array.from((nextGroup.groupEl || nextGroup.titleEl.parentElement).querySelectorAll(CONFIG.sectionItemSelector)); if (newSections.length > 0) { console.log('[自动切课] 跨大节跳转成功,播放下一科目第1小节'); newSections[0].click(); return true; } } console.log('[自动切课] 展开大节失败'); return false; } // 3. 已经是最后一个大节的最后一节:学习完成 if (isLastInGroup && isLastGroup) { showToast('🎉 全部课程学习完毕'); return true; } return false; } // 路径1:点击页面原生下一节按钮 function clickNextByButton() { const els = document.querySelectorAll('a, button, div[role="button"], span'); for (const el of els) { const txt = el.innerText?.trim() || ''; const match = CONFIG.nextBtnKeywords.some(k => txt.includes(k)); const vis = el.offsetParent !== null; const dis = el.disabled || el.classList.contains('disabled'); if (match && vis && !dis) { el.click(); return true; } } return false; } // 统一切课入口 function goToNextChapter() { if (isJumping) return; isJumping = true; const delay = random(CONFIG.nextDelayMin, CONFIG.nextDelayMax); showToast(`${(delay/1000).toFixed(1)}秒后自动跳转下一课`); setTimeout(async () => { const btnOk = clickNextByButton(); if (!btnOk) { await clickNextByCatalog(); } setTimeout(() => { isJumping = false; }, 3000); }, delay); } // ========== 倍速与播放控制 ========== function setFluctuationRate() { if (!videoDom) return; const base = CONFIG.targetPlaybackRate; const fluct = CONFIG.rateFluctuation; const rate = base + random(-fluct, fluct); const finalRate = Math.max(0.5, Math.min(3, rate)); const originalDesc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); originalDesc.set.call(videoDom, finalRate); } function adjustRate(delta) { if (!videoDom) return; CONFIG.targetPlaybackRate = Math.max(0.5, Math.min(3, CONFIG.targetPlaybackRate + delta)); setFluctuationRate(); showToast(`目标倍速:${CONFIG.targetPlaybackRate.toFixed(1)}x`); } function videoSkip(sec) { if (!videoDom) return; videoDom.currentTime = Math.max(0, Math.min(videoDom.duration, videoDom.currentTime + sec)); showToast(sec>0 ? `前进${sec}秒` : `后退${Math.abs(sec)}秒`); } function togglePlay() { if (!videoDom) return; if(videoDom.paused) videoDom.play(); else videoDom.pause(); showToast(videoDom.paused ? "已暂停" : "开始播放"); } function toggleMute() { if (!videoDom) return; videoDom.muted = !videoDom.muted; showToast(videoDom.muted ? "已静音" : "取消静音"); } function volumeChange(delta) { if (!videoDom) return; videoDom.volume = Math.max(0, Math.min(1, videoDom.volume + delta)); showToast(`音量:${(videoDom.volume*100).toFixed(0)}%`); } function toggleFullscreen() { if (!videoDom) return; if(document.fullscreenElement) document.exitFullscreen(); else videoDom.requestFullscreen(); } // 随机暂停模拟真人 function startRandomPause() { clearTimeout(pauseTimer); const nextPause = random(CONFIG.pauseIntervalMin, CONFIG.pauseIntervalMax); pauseTimer = setTimeout(() => { if (!videoDom || videoDom.paused || isJumping) { startRandomPause(); return; } videoDom.pause(); const duration = random(CONFIG.pauseDurationMin, CONFIG.pauseDurationMax); setTimeout(() => { videoDom.play().catch(() => {}); startRandomPause(); }, duration); }, nextPause); } // 视频绑定 function bindVideoEvent() { const videos = document.querySelectorAll('video'); if (!videos.length) return false; videoDom = videos[0]; videoDom.removeEventListener('ended', handleVideoEnd); videoDom.addEventListener('ended', handleVideoEnd); // 初始先1倍速播放5秒,再升到目标倍速 const originalDesc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); originalDesc.set.call(videoDom, 1.0); setTimeout(() => { setFluctuationRate(); setInterval(setFluctuationRate, 240000); }, 5000); videoDom.muted = CONFIG.autoMuted; videoDom.autoplay = true; if (CONFIG.autoResume) { videoDom.removeEventListener('pause', handleVideoPause); videoDom.addEventListener('pause', handleVideoPause); } setInterval(updateStatusPanel, 1000); startRandomPause(); console.log(`[视频绑定完成] 目标倍速${CONFIG.targetPlaybackRate}x`); return true; } function handleVideoEnd() { goToNextChapter(); } function handleVideoPause() { if (isJumping) return; setTimeout(() => videoDom?.play().catch(() => {}), 800); } // 全局快捷键 function initHotkeys() { document.addEventListener('keydown', e => { const tag = document.activeElement?.tagName; if (['INPUT','TEXTAREA','SELECT'].includes(tag)) return; const key = e.key.toLowerCase(); if(e.ctrlKey){ if(key === 'arrowup') {e.preventDefault();volumeChange(0.1);return;} if(key === 'arrowdown') {e.preventDefault();volumeChange(-0.1);return;} } switch(key){ case 'c': e.preventDefault(); adjustRate(CONFIG.rateStep); break; case 'x': e.preventDefault(); adjustRate(-CONFIG.rateStep); break; case 'z': e.preventDefault(); CONFIG.targetPlaybackRate = 1; setFluctuationRate(); showToast("已重置为1倍速"); break; case 'n': e.preventDefault(); goToNextChapter(); break; case 'arrowleft': e.preventDefault(); videoSkip(-CONFIG.skipSec); break; case 'arrowright': e.preventDefault(); videoSkip(CONFIG.skipSec); break; case ' ': e.preventDefault(); togglePlay(); break; case 'm': e.preventDefault(); toggleMute(); break; case 'f': e.preventDefault(); toggleFullscreen(); break; case 'h': e.preventDefault(); showHelpInfo(); break; } }) } // 自动弹窗关闭 function autoSkipPopup() { if (!CONFIG.autoSkipPopup) return; const btns = document.querySelectorAll('button,a,.el-button,.confirm-btn'); btns.forEach(b=>{ const t = b.innerText?.trim()||''; if(['确定','知道了','继续','确认'].includes(t) && b.offsetParent) b.click(); }) } // DOM监听 function initObserver() { domObserver = new MutationObserver(()=>{ if(!videoDom || !document.body.contains(videoDom)) bindVideoEvent(); autoSkipPopup(); }) domObserver.observe(document.body,{childList:true,subtree:true}) } function init() { bindVideoEvent() || initObserver(); initHotkeys(); autoSkipPopup(); createStatusPanel(); console.log('[跨大节修复版脚本加载完成,按H查看快捷键]'); } window.addEventListener('load', ()=>setTimeout(init, 1200)); if(['complete','interactive'].includes(document.readyState)) setTimeout(init, 1200); })();