您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
【强力防暂停 + 适配新版CC播放器】通过直接控制HTML5 video元素,实现后台挂机、自动静音、自动播放、处理弹窗、播放完自动跳转下一集。全自动建立学习队列,可视化面板显示进度。
// ==UserScript== // @name 华医网自动看课助手 (V2.0.0 CC播放器适配版) // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description 【强力防暂停 + 适配新版CC播放器】通过直接控制HTML5 video元素,实现后台挂机、自动静音、自动播放、处理弹窗、播放完自动跳转下一集。全自动建立学习队列,可视化面板显示进度。 // @author Gemini & Your Name // @match *://*.91huayi.com/pages/course_ware.aspx* // @match *://*.91huayi.com/course_ware/course_ware_polyv.aspx* // @match *://*.91huayi.com/course_ware/course_ware_cc.aspx* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // --- 配置 --- const CHECK_INTERVAL = 2000; // 每2秒检查一次播放状态 const END_THRESHOLD = 2.5; // 视频剩余多少秒时算作播放完毕 // --- 数据存储 --- const db = { load: (key, def) => JSON.parse(GM_getValue(key, JSON.stringify(def))), save: (key, val) => GM_setValue(key, JSON.stringify(val)), }; const learningQueueDB = { key: 'learning_queue_v2_0', // 使用新版本key避免旧数据冲突 get: () => db.load(learningQueueDB.key, []), set: (data) => db.save(learningQueueDB.key, data), clear: () => GM_deleteValue(learningQueueDB.key) }; // --- 状态管理 --- let scriptState = { isRunning: false, mainIntervalId: null, isProcessingEnd: false, // 是否正在处理视频结束逻辑 videoElement: null, // 缓存video元素 }; // --- 辅助函数 --- const formatTime = (s) => (s = Math.floor(s || 0), `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`); const log = (message) => console.log(`[华医网助手] ${message}`); // --- 核心功能 --- /** * V2.0 改造核心:扫描课程列表,建立待看队列 * 此函数逻辑与旧版兼容,无需修改。 */ function buildAndStoreCourseList() { if (learningQueueDB.get().length > 0) return; log("扫描页面,建立学习队列..."); const allLessons = document.querySelectorAll('.lis-inside-content'); const currentLessonMarker = document.getElementById('top_play'); if (!currentLessonMarker) { log("错误:未找到当前播放的课程标记(#top_play),无法建立队列。"); return; } const currentLessonLi = currentLessonMarker.closest('.lis-inside-content'); if (!currentLessonLi) return; let foundCurrent = false; const queue = []; for (const lesson of allLessons) { // 定位到当前课程,从下一个开始处理 if (lesson === currentLessonLi) { foundCurrent = true; continue; } if (foundCurrent) { const statusButton = lesson.querySelector('button'); // 只添加未完成的课程 if (!statusButton || (statusButton.innerText.trim() !== '已完成' && statusButton.innerText.trim() !== '待考试')) { const h2 = lesson.querySelector('h2'); const onclickAttr = h2 ? h2.getAttribute('onclick') : null; const urlMatch = onclickAttr ? onclickAttr.match(/'(.*?)'/) : null; if (urlMatch && urlMatch[1]) { queue.push({ name: h2.innerText.trim().replace(/^\d+、\s*/, ''), url: urlMatch[1] }); } } } } learningQueueDB.set(queue); log(`队列建立完毕,发现 ${queue.length} 个待看课程。`); updatePanel(); } /** * V2.0 改造核心:处理各种弹窗 * 选择器兼容新旧页面结构。 */ function findAndClickPopups() { // 涵盖了“温馨提示”、“继续学习”、“绑定提示”等各类弹窗的确认按钮 const selectors = [ '#div_preview1 #btnPay', // "继续学习" 按钮 '#div_preview2 .rig_btn', // 试看结束后的 "继续学习" 按钮 '#div_tip .rig_btn', // "提示" 弹窗的 "是" 按钮 '.study_diaog .btn_sign', // 旧版可能存在的签到按钮 '#div_processbar_tip .rig_btn' // 旧版可能存在的进度提示按钮 ]; for (const selector of selectors) { const button = document.querySelector(selector); // 确保按钮可见且可点击 if (button && button.offsetParent !== null) { log(`发现弹窗,点击 -> ${button.value || button.innerText}`); button.click(); } } } /** * V2.0 改造核心:主循环任务 * 完全基于 HTML5 <video> 元素进行操作,不再依赖特定播放器API。 */ function mainLoop() { if (!scriptState.isRunning || scriptState.isProcessingEnd) return; findAndClickPopups(); // 查找并缓存 video 元素 if (!scriptState.videoElement) { // 这个选择器能同时兼容新旧页面的视频播放器位置 scriptState.videoElement = document.querySelector('#video video, .video video'); if (!scriptState.videoElement) { log("等待播放器加载..."); updateProgressUI('等待播放器...'); return; } log("播放器 video 元素已找到!"); } const video = scriptState.videoElement; try { // 1. 强制静音 (更可靠的方式) if (!video.muted) { video.muted = true; log("已强制静音。"); } // 2. 检查是否暂停,如果是则播放 if (video.paused && !video.ended) { video.play().catch(e => log(`尝试播放失败: ${e.message}`)); log("检测到视频暂停,已尝试恢复播放。"); } const currentTime = video.currentTime; const duration = video.duration; // 3. 检查是否播放完毕 // (条件:有有效时长,且没结束,且快要结束了) if (duration > 0 && !video.ended && (duration - currentTime < END_THRESHOLD)) { log(`检测到视频播放完毕 (进度: ${formatTime(currentTime)} / ${formatTime(duration)}),准备切换。`); scriptState.isProcessingEnd = true; // 设置标志,防止重复触发 processNextInQueue(); return; // 立即退出本次循环,等待页面跳转 } // 4. 更新UI进度 updateProgressUI(); } catch (e) { log(`主循环发生错误: ${e.message}`); // 重置video元素,以便下次循环重新查找 scriptState.videoElement = null; } } /** * 更新控制面板上的进度显示 */ function updateProgressUI(customMessage = '') { const progressEl = document.getElementById('video-progress-status'); if (!progressEl) return; if (customMessage) { progressEl.textContent = customMessage; return; } if (scriptState.videoElement) { const video = scriptState.videoElement; const currentTime = video.currentTime; const duration = video.duration; if (duration > 0) { progressEl.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`; } else { progressEl.textContent = '加载中...'; } } else { progressEl.textContent = '未开始'; } } /** * 处理队列中的下一个课程 * 此函数无需修改。 */ function processNextInQueue() { log("处理队列中的下一个课程..."); let queue = learningQueueDB.get(); if (queue.length > 0) { const nextCourse = queue.shift(); learningQueueDB.set(queue); log(`将在3秒后跳转到 -> ${nextCourse.name}`); setTimeout(() => { window.location.href = nextCourse.url; }, 3000); } else { log("恭喜!学习队列已清空,所有课程均已完成。"); learningQueueDB.clear(); stopScript(); alert("恭喜!所有课程均已完成,脚本自动停止。"); } } // --- 脚本生命周期与UI控制 (无需修改) --- function startScript() { if (scriptState.isRunning) return; scriptState.isRunning = true; if (learningQueueDB.get().length === 0) { buildAndStoreCourseList(); } scriptState.mainIntervalId = setInterval(mainLoop, CHECK_INTERVAL); updatePanel(); log("脚本已启动。"); mainLoop(); // 立即执行一次 } function stopScript() { if (!scriptState.isRunning) return; clearInterval(scriptState.mainIntervalId); scriptState.mainIntervalId = null; scriptState.isRunning = false; scriptState.isProcessingEnd = false; scriptState.videoElement = null; updatePanel(); log("脚本已停止。"); } function setupUI() { const panel = document.createElement('div'); panel.id = 'auto-video-panel'; document.body.appendChild(panel); GM_addStyle(` #auto-video-panel { position: fixed; bottom: 20px; right: 20px; background-color: #e6f7ff; border: 2px solid #1890ff; border-radius: 8px; padding: 15px; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,0.15); width: 280px; } #auto-video-panel h3 { margin: 0 0 12px 0; color: #1890ff; text-align: center; font-size: 16px; } #auto-video-panel button { width: 100%; padding: 10px; border: none; border-radius: 5px; color: white; cursor: pointer; font-size: 14px; transition: background-color 0.2s; margin-bottom: 10px; } #auto-video-panel button.start { background-color: #52c41a; } #auto-video-panel button.stop { background-color: #f5222d; } #auto-video-panel p { margin: 8px 0 0 0; font-size: 12px; } #auto-video-panel strong { color: #333; } #script-status-text, #video-progress-status { font-weight: bold; } #queue-list-container { background-color: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 5px 10px; margin-top: 10px; max-height: 120px; overflow-y: auto; font-size: 12px; } #queue-list-container ol { margin: 0; padding-left: 20px; } #queue-list-container li { margin-bottom: 5px; color: #555; } .empty-queue { color: #888; text-align: center; font-style: italic; padding: 10px 0; } `); panel.addEventListener('click', e => { if (e.target.tagName === 'BUTTON') { scriptState.isRunning ? stopScript() : startScript(); } }); updatePanel(); } function updatePanel() { const panel = document.getElementById('auto-video-panel'); if (!panel) return; const isRunning = scriptState.isRunning; const queue = learningQueueDB.get(); let queueHtml = '<p class="empty-queue">队列为空或已学完</p>'; if (queue.length > 0) { queueHtml = '<ol>' + queue.map(course => `<li>${course.name}</li>`).join('') + '</ol>'; } panel.innerHTML = ` <h3>自动看课助手 (V2.0.0)</h3> <button class="${isRunning ? 'stop' : 'start'}">${isRunning ? '暂停自动看课' : '开始自动看课'}</button> <p><strong>状态:</strong> <span id="script-status-text" style="color: ${isRunning ? '#52c41a' : '#f5222d'};">${isRunning ? '运行中...' : '已停止'}</span></p> <p><strong>进度:</strong> <span id="video-progress-status" style="color:#1890ff;">未开始</span></p> <strong>待看列表 (${queue.length}个):</strong> <div id="queue-list-container">${queueHtml}</div> `; updateProgressUI(); } function init() { log("脚本(V2.0.0 CC播放器适配版)已加载。"); setupUI(); // 如果是从一个课程跳转过来的,并且队列里还有课,则自动开始 if (window.location.search.includes('cwid=') && learningQueueDB.get().length > 0) { log("检测到从上一课程跳转而来,将在3秒后自动开始。"); setTimeout(startScript, 3000); // 延迟启动,等待页面和播放器充分加载 } } // 使用 load 事件确保页面所有资源(包括播放器脚本)都加载完毕 window.addEventListener('load', init); })();