// ==UserScript== // @name 自动课程播放器(爱上空气自用) // @namespace http://tampermonkey.net/ // @version 3.1 // @description 自动播放视频,拦截原生弹窗(含iframe),智能识别课表并自动切换课程,保持不掉线 // @author Marvis // @match http://ce.esnai.net/cpvcas/studyView.do?courseWareId=* // @run-at document-start // @grant none // ==/UserScript== (function() { 'use strict'; // ============================================================ // 步骤0:最早阶段——Hook 原生弹窗(必须在任何页面代码前完成) // ============================================================ let alertCount = 0; let confirmCount = 0; let hookInstalled = false; function installDialogHooks(targetWindow) { if (!targetWindow || !targetWindow.alert) return false; // 避免重复 hook 同一个 window if (targetWindow.__marvis_hooked) return false; targetWindow.__marvis_hooked = true; try { targetWindow.alert = function(msg) { alertCount++; console.log('[Marvis] 拦截 alert #' + alertCount + ' from', targetWindow.location.href, ':', msg ? ('' + msg).substring(0, 200) : '(无内容)'); // 吞掉弹窗,触发后续流程 triggerPostDialogFlow(); }; targetWindow.confirm = function(msg) { confirmCount++; console.log('[Marvis] 拦截 confirm #' + confirmCount + ' from', targetWindow.location.href, ':', msg ? ('' + msg).substring(0, 200) : '(无内容)'); triggerPostDialogFlow(); return true; // 自动确定 }; targetWindow.prompt = function(msg, defaultText) { console.log('[Marvis] 拦截 prompt from', targetWindow.location.href, ':', msg ? ('' + msg).substring(0, 200) : '(无内容)'); triggerPostDialogFlow(); return defaultText || ''; }; console.log('[Marvis] 已 hook 弹窗,目标 window:', targetWindow.location.href || '主窗口'); return true; } catch (e) { console.warn('[Marvis] hook 失败(可能跨域iframe):', e.message); targetWindow.__marvis_hooked = false; return false; } } // 立即 hook 主窗口 installDialogHooks(window); // ============================================================ // 步骤1:拦截所有 iframe 的创建 // ============================================================ function hookIframeWindow(iframe) { if (!iframe || !iframe.contentWindow) return; try { // 即使跨域也能尝试(跨域会抛异常被 catch) if (iframe.contentWindow && !iframe.contentWindow.__marvis_hooked) { installDialogHooks(iframe.contentWindow); } } catch (e) { // 跨域 iframe 无法访问 contentWindow,忽略 } } // 方案A:监听 DOM 中的 iframe 添加 function startIframeObserver() { const observer = new MutationObserver(function(mutations) { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { // 直接新增的 iframe if (node.tagName === 'IFRAME') { hookIframeWindow(node); // iframe 可能还没加载完,等 load 再试一次 node.addEventListener('load', function() { hookIframeWindow(node); }, { once: true }); } // 新增节点内部的 iframe if (node.querySelectorAll) { const iframes = node.querySelectorAll('iframe'); for (const ifr of iframes) { hookIframeWindow(ifr); ifr.addEventListener('load', function() { hookIframeWindow(ifr); }, { once: true }); } } } } } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } // 方案B:拦截 document.createElement 和 appendChild/insertBefore function hookIframeCreation() { const origCreateElement = document.createElement.bind(document); document.createElement = function(tagName, options) { const element = origCreateElement(tagName, options); if (tagName.toLowerCase() === 'iframe') { element.addEventListener('load', function() { hookIframeWindow(element); }, { once: true }); } return element; }; const origAppendChild = Element.prototype.appendChild; Element.prototype.appendChild = function(child) { const result = origAppendChild.call(this, child); if (child.tagName === 'IFRAME') { hookIframeWindow(child); child.addEventListener('load', function() { hookIframeWindow(child); }, { once: true }); } return result; }; const origInsertBefore = Element.prototype.insertBefore; Element.prototype.insertBefore = function(newNode, referenceNode) { const result = origInsertBefore.call(this, newNode, referenceNode); if (newNode.tagName === 'IFRAME') { hookIframeWindow(newNode); newNode.addEventListener('load', function() { hookIframeWindow(newNode); }, { once: true }); } return result; }; } // ============================================================ // 步骤2:弹窗触发后的流程控制器 // ============================================================ let flowTimer = null; let postDialogExecuted = false; function triggerPostDialogFlow() { if (postDialogExecuted) return; // 防止短时间内重复触发 postDialogExecuted = true; if (flowTimer) clearTimeout(flowTimer); flowTimer = setTimeout(function() { console.log('[Marvis] 开始执行弹窗后流程:点击课表 → 下一个课程'); updateIndicator('已拦截弹窗,切换课程中...', '#FF9800'); tryClickSchedule(3); }, 1500); } function tryClickSchedule(retries) { if (retries <= 0) { console.log('[Marvis] 课表按钮重试耗尽,尝试直接点击下一个课程'); updateIndicator('课表未找到,直接切换...', '#FF5722'); tryClickNextCourse(3); return; } // 尝试点击课表按钮(可能是按钮、标签页、链接等) let clicked = false; // 方法1:直接查找课表按钮 clicked = clickElementByText(['课表', '课程表', '课程目录', '目录', 'schedule', 'timetable']); // 方法2:如果没找到,尝试查找标签页(tab) if (!clicked) { const tabs = document.querySelectorAll('[role="tab"], .tab, .nav-tab, .tab-item, [class*="tab"]'); for (const tab of tabs) { const text = (tab.textContent || '').trim().toLowerCase(); if (text.includes('课程') || text.includes('课表') || text.includes('目录')) { console.log('[Marvis] 找到课表标签页:', text); simulateClick(tab); clicked = true; break; } } } // 方法3:如果还没找到,尝试查找右侧导航 if (!clicked) { const rightElements = document.querySelectorAll('div, aside, nav'); for (const el of rightElements) { const rect = el.getBoundingClientRect(); if (rect.left > window.innerWidth * 0.7) { const text = (el.textContent || '').trim(); if (text.length > 20 && text.includes('课程')) { console.log('[Marvis] 找到右侧导航区域,尝试点击'); // 尝试点击该区域内的第一个可点击元素 const clickable = el.querySelector('a, button, [role="button"], [onclick], [class*="btn"]'); if (clickable) { simulateClick(clickable); clicked = true; break; } } } } } if (clicked) { console.log('[Marvis] 课表已点击'); updateIndicator('课表已点击,等待加载...', '#4CAF50'); // 等待课表加载,然后点击下一个课程 setTimeout(function() { tryClickNextCourse(3); }, 2000); } else { console.log('[Marvis] 课表未找到,剩余重试 ' + (retries - 1) + ' 次'); setTimeout(function() { tryClickSchedule(retries - 1); }, 2000); } } // ============================================================ // 智能查找并点击下一个课程 // ============================================================ function tryClickNextCourse(retries) { if (retries <= 0) { console.log('[Marvis] 下一课程重试耗尽,重置状态'); resetState(); return; } console.log('[Marvis] 开始查找下一个课程,剩余重试 ' + retries + ' 次'); // 策略1:在课表面板中查找当前课程 → 找下一个 const schedulePanel = findSchedulePanel(); if (schedulePanel) { const nextEl = findNextCourseInPanel(schedulePanel); if (nextEl) { console.log('[Marvis] 点击下一个课程:', nextEl.textContent.trim()); simulateClick(nextEl); updateIndicator('已切换课程,等待新视频...', '#4CAF50'); setTimeout(resetState, 3000); return; } } // 策略2:传统文本匹配 const clicked = clickElementByText(['下一个', '下一节', '下一课', '下一条', '继续', '下一章', 'next']); if (clicked) { console.log('[Marvis] 传统方法点击成功'); updateIndicator('课程已切换(传统方法),等待新视频...', '#4CAF50'); setTimeout(resetState, 3000); return; } console.log('[Marvis] 未找到下一课程,剩余重试 ' + (retries - 1) + ' 次'); setTimeout(function() { tryClickNextCourse(retries - 1); }, 2000); } // 查找课表面板(右侧课程清单) function findSchedulePanel() { // 方法1:查找包含特定关键词的容器 const containers = document.querySelectorAll('div, section, aside, nav, ul, ol'); for (const el of containers) { const text = (el.textContent || '').trim(); if (text.length < 20 || text.length > 2000) continue; const lower = text.toLowerCase(); if (lower.includes('课程清单') || lower.includes('课表') || lower.includes('目录')) { const childCount = el.querySelectorAll('li, a, [class*="item"]').length; if (childCount >= 3) { console.log('[Marvis] 找到课表面板:', text.substring(0, 50)); return el; } } } // 方法2:查找右侧1/3区域的列表容器 const viewportWidth = window.innerWidth; const allDivs = document.querySelectorAll('div'); for (const el of allDivs) { const rect = el.getBoundingClientRect(); if (rect.left > viewportWidth * 0.65 && rect.width > 100 && rect.width < viewportWidth * 0.4) { const items = el.querySelectorAll('li, a, [class*="item"], [class*="course"]'); if (items.length >= 3) { const text = (el.textContent || '').trim(); if (text.includes('课程') || text.includes('章') || text.includes('节') || text.includes('讲')) { console.log('[Marvis] 找到右侧课表面板'); return el; } } } } return null; } // 在课表面板中查找当前课程和下一个课程 function findNextCourseInPanel(panel) { // 获取面板中的所有行/条目 const items = panel.querySelectorAll('li, tr, .item, .course-item, [class*="row"], a, div[class*="item"]'); let currentIndex = -1; // 查找当前课程:加粗 + 展开箭头 for (let i = 0; i < items.length; i++) { const el = items[i]; const text = (el.textContent || '').trim(); if (text.length < 2 || text.length > 200) continue; if (!text.includes('课') && !text.includes('章') && !text.includes('节') && !text.includes('讲') && !text.includes('单元')) continue; const style = window.getComputedStyle(el); const isBold = style.fontWeight === 'bold' || parseInt(style.fontWeight) >= 600; // 检查内部是否有展开箭头 const hasArrow = el.querySelector('[class*="arrow"], [class*="expand"], [class*="chevron"], [class*="icon"]') || text.includes('▼') || text.includes('▶') || text.includes('◆'); // 或者查找子元素中的箭头图标 let hasArrowIcon = hasArrow; if (!hasArrowIcon) { const childSpans = el.querySelectorAll('span, i, svg, img'); for (const child of childSpans) { const cls = (child.className || '').toLowerCase(); if (cls.includes('arrow') || cls.includes('expand') || cls.includes('chevron') || cls.includes('icon')) { hasArrowIcon = true; break; } } } // 也可以检测加粗的父元素 const boldChildren = el.querySelectorAll('b, strong, [style*="bold"], [class*="bold"]'); if (boldChildren.length > 0) hasArrowIcon = true; // 有加粗子元素也算标记 if (isBold || hasArrowIcon) { currentIndex = i; console.log('[Marvis] 找到当前课程 (索引 ' + i + '):', text.substring(0, 30)); break; } } // 如果没找到当前课程,尝试根据字号差异判断 if (currentIndex === -1) { let maxFontSize = 0; for (let i = 0; i < items.length; i++) { const el = items[i]; const text = (el.textContent || '').trim(); if (text.length < 2 || text.length > 200) continue; if (!text.includes('课') && !text.includes('章') && !text.includes('节') && !text.includes('讲') && !text.includes('单元')) continue; const style = window.getComputedStyle(el); const fontSize = parseFloat(style.fontSize); if (fontSize > maxFontSize) { maxFontSize = fontSize; currentIndex = i; } } if (currentIndex !== -1) { console.log('[Marvis] 通过字号找到当前课程 (索引 ' + currentIndex + '):', items[currentIndex].textContent.trim().substring(0, 30)); } } // 找到当前课程后,查找下一个课程 if (currentIndex !== -1 && currentIndex + 1 < items.length) { for (let i = currentIndex + 1; i < items.length; i++) { const el = items[i]; const text = (el.textContent || '').trim(); if (text.length < 2 || text.length > 200) continue; // 下一个课程通常是蓝色链接或可点击的 const link = el.querySelector('a'); // 优先找里面的链接 const target = link || el; const style = window.getComputedStyle(target); const isClickable = target.tagName === 'A' || target.tagName === 'BUTTON' || target.hasAttribute('onclick') || style.cursor === 'pointer' || target.getAttribute('role') === 'button'; if (isClickable) { console.log('[Marvis] 找到下一个课程 (索引 ' + i + '):', text.substring(0, 30)); return target; } } // 如果没找到可点击的,直接返回下一个元素 const nextEl = items[currentIndex + 1]; const nextText = (nextEl.textContent || '').trim(); if (nextText.length > 2) { console.log('[Marvis] 返回下一个课程元素:', nextText.substring(0, 30)); return nextEl; } } return null; } function resetState() { postDialogExecuted = false; updateIndicator('等待视频...', '#2196F3'); console.log('[Marvis] 状态已重置,准备下一轮'); } // ============================================================ // 步骤3:通用元素查找和点击 // ============================================================ function clickElementByText(keywords) { // 在所有可见元素中查找匹配关键词的 const candidates = []; const allElements = document.querySelectorAll('a, button, span, div, li, td, p, [role="button"], [class*="btn"], [class*="menu"], [class*="nav"], [class*="tab"], [class*="item"]'); for (const el of allElements) { // 跳过不可见元素 const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue; if (el.offsetParent === null && style.position !== 'fixed') continue; const text = (el.textContent || '').trim().replace(/\s+/g, ' '); if (!text || text.length > 100) continue; for (const keyword of keywords) { if (text === keyword || text.startsWith(keyword) || text.includes(keyword)) { candidates.push({ element: el, text: text, keyword: keyword, score: text.length }); break; } } } if (candidates.length === 0) return false; // 优先选最短文本的(更精确匹配) candidates.sort(function(a, b) { return a.score - b.score; }); const best = candidates[0]; console.log('[Marvis] 点击元素:', best.text, '(匹配关键词: ' + best.keyword + ')'); simulateClick(best.element); return true; } function simulateClick(element) { if (!element) return; // 移除可能的 disabled 属性 if (element.disabled) element.disabled = false; if (element.classList.contains('disabled')) element.classList.remove('disabled'); // 直接触发 click element.click(); // 补充鼠标事件链 ['mousedown', 'mouseup', 'click'].forEach(function(eventType) { element.dispatchEvent(new MouseEvent(eventType, { view: window, bubbles: true, cancelable: true })); }); // 如果元素可聚焦,触发 Enter if (element.tabIndex >= 0 || element.hasAttribute('tabindex')) { element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true })); } } // ============================================================ // 步骤4:自动播放视频 // ============================================================ function autoPlayVideo() { const videos = document.querySelectorAll('video'); for (const video of videos) { if (video.paused && !video.ended) { video.play().then(function() { console.log('[Marvis] 视频开始播放'); updateIndicator('视频播放中...', '#4CAF50'); }).catch(function() { // 浏览器可能禁止自动播放,尝试静音后播放 video.muted = true; video.play().then(function() { console.log('[Marvis] 视频静音播放中'); updateIndicator('视频播放中(静音)...', '#4CAF50'); }).catch(function() {}); }); } } } // ============================================================ // 步骤5:心跳保持连接 // ============================================================ function startHeartbeat() { setInterval(function() { window.dispatchEvent(new MouseEvent('mousemove', { view: window, bubbles: true, cancelable: true })); }, 30000); } // ============================================================ // 步骤6:可视化状态指示器 // ============================================================ let indicator = null; function createIndicator() { indicator = document.createElement('div'); indicator.id = 'marvis-player-indicator'; indicator.style.cssText = 'position:fixed;top:10px;right:10px;background:rgba(0,0,0,0.75);color:#fff;padding:8px 14px;border-radius:4px;font:12px Arial;z-index:2147483647;pointer-events:none;transition:background 0.3s;'; indicator.textContent = '自动播放器: 等待视频...'; } function updateIndicator(text, color) { if (!indicator) return; indicator.textContent = '自动播放器: ' + text; indicator.style.background = color || 'rgba(0,0,0,0.75)'; } // ============================================================ // 步骤7:视频结束检测(兜底:如果弹窗通过其他方式出现) // ============================================================ function monitorVideoEnd() { setInterval(function() { const videos = document.querySelectorAll('video'); for (const video of videos) { if (video.ended && !video.__marvis_ended_handled) { video.__marvis_ended_handled = true; console.log('[Marvis] 检测到视频播放完毕'); // 如果5秒内弹窗没被拦截到,强制触发 setTimeout(function() { if (!postDialogExecuted) { console.log('[Marvis] 5秒后仍未拦截到弹窗,强制触发后续流程'); triggerPostDialogFlow(); } }, 5000); // 重置标记,准备下一轮 setTimeout(function() { video.__marvis_ended_handled = false; }, 60000); } } }, 2000); // 定期重试自动播放 setInterval(autoPlayVideo, 5000); } // ============================================================ // 初始化 // ============================================================ function init() { console.log('[Marvis] 自动课程播放器 v3.0 已启动'); // 等待 body 出现再创建指示器 function waitForBody() { if (document.body) { createIndicator(); document.body.appendChild(indicator); updateIndicator('等待视频...', '#2196F3'); // 启动各项监控 hookIframeCreation(); startIframeObserver(); startHeartbeat(); monitorVideoEnd(); // 初始检查已有 iframe const existingIframes = document.querySelectorAll('iframe'); for (const ifr of existingIframes) { hookIframeWindow(ifr); ifr.addEventListener('load', function() { hookIframeWindow(ifr); }, { once: true }); } // 延迟自动播放 setTimeout(autoPlayVideo, 3000); } else { setTimeout(waitForBody, 50); } } waitForBody(); } // ============================================================ // 启动 // ============================================================ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // 导出控制接口 window.__marvisPlayer = { getStats: function() { return { alertCount: alertCount, confirmCount: confirmCount, postDialogExecuted: postDialogExecuted }; }, forceTrigger: triggerPostDialogFlow, reset: resetState }; })();