// ==UserScript== // @name 云南开放大学自动化刷视频【做题功能找群主】 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 自动刷云南开放大学课程资源,自动识别视频/PDF并切换到下一资源 // @author UselessWater // @match https://teach.ynou.edu.cn/play/playVideo* // @match https://teach.ynou.edu.cn/eduCourseBaseinfo/courseCata.action* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // 配置项 /* * 可以根据个人情况自行更改下面的配置项。 * * */ const config = { pdfWaitTime: 1500, // PDF/Word资源等待时间(毫秒) videoCheckInterval: 3000, // 视频进度检测间隔(毫秒) maxWaitTime: 10800000, // 单个视频最大等待时间(3小时),防止视频卡住时无限等待。若视频超过3小时请自行改大 resourceClickDelay: 3000, // 点击资源后等待页面加载的时间(毫秒) expandDelay: 300 // 展开目录时的点击间隔(毫秒) }; // 状态变量 let isRunning = false; let currentResourceIndex = -1; let resources = []; let videoCheckTimer = null; let startTime = null; // 日志记录 const logger = { info: (msg) => { console.log(`[YNOU学习] ${new Date().toLocaleTimeString()} - ${msg}`); addLog(msg, 'info'); }, success: (msg) => { console.log(`[YNOU学习] ${msg}`); addLog(msg, 'success'); }, warning: (msg) => { console.warn(`[YNOU学习] ${msg}`); addLog(msg, 'warning'); }, error: (msg) => { console.error(`[YNOU学习] ${msg}`); addLog(msg, 'error'); } }; // 创建控制面板 function createControlPanel() { const style = document.createElement('style'); style.textContent = ` #ynou-automation-panel { position: fixed; top: 20px; right: 20px; width: 380px; background: #fff; border: 2px solid #4CAF50; border-radius: 8px; padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 999999; font-family: 'Microsoft YaHei', Arial, sans-serif; font-size: 14px; transition: all 0.3s ease; } #ynou-automation-panel:hover { box-shadow: 0 6px 16px rgba(0,0,0,0.2); } #ynou-automation-panel h3 { margin: -5px -5px 10px -5px; padding: 8px 10px; color: #4CAF50; font-size: 16px; font-weight: bold; text-align: center; cursor: move; background: #f0f8f0; border-radius: 6px 6px 0 0; user-select: none; } #ynou-automation-panel h3:active { cursor: grabbing; } #ynou-automation-panel .status { margin: 8px 0; padding: 8px; background: #f5f5f5; border-radius: 4px; font-size: 12px; line-height: 1.4; } #ynou-automation-panel .log-container { max-height: 200px; overflow-y: auto; background: #f9f9f9; border-radius: 4px; padding: 8px; font-size: 11px; line-height: 1.3; margin: 8px 0; } #ynou-automation-panel .log-entry { margin: 2px 0; padding: 2px 0; border-bottom: 1px dashed #eee; word-break: break-all; } #ynou-automation-panel .log-info { color: #2196F3; } #ynou-automation-panel .log-success { color: #4CAF50; font-weight: bold; } #ynou-automation-panel .log-warning { color: #FF9800; } #ynou-automation-panel .log-error { color: #F44336; font-weight: bold; } #ynou-automation-panel button { width: 100%; padding: 10px; margin: 5px 0; border: none; border-radius: 4px; background: #4CAF50; color: white; font-size: 14px; font-weight: bold; cursor: pointer; transition: background 0.2s; } #ynou-automation-panel button:hover { background: #45a049; } #ynou-automation-panel button:active { background: #3d8b40; } #ynou-automation-panel button:disabled { background: #ccc; cursor: not-allowed; } #ynou-automation-panel .stop-btn { background: #f44336; } #ynou-automation-panel .stop-btn:hover { background: #da190b; } #ynou-automation-panel .progress-bar { width: 100%; height: 16px; background: #e0e0e0; border-radius: 8px; overflow: hidden; margin: 8px 0; } #ynou-automation-panel .progress-fill { height: 100%; background: #4CAF50; width: 0%; transition: width 0.3s ease; } #ynou-automation-panel .hint { background: #FFF3CD; border: 1px solid #FFEAA7; padding: 8px; border-radius: 4px; font-size: 12px; line-height: 1.3; color: #856404; margin-bottom: 8px; } `; document.head.appendChild(style); const panel = document.createElement('div'); panel.id = 'ynou-automation-panel'; panel.innerHTML = `

自动学习助手 v3.0

提示:请先点击"一键展开所有目录",再点击"开始学习"!
状态:已就绪,等待开始...
资源总数:0
当前进度:-
当前资源:等待开始
`; document.body.appendChild(panel); document.getElementById('expand-btn').addEventListener('click', expandAllDirectories); document.getElementById('start-btn').addEventListener('click', startAutomation); document.getElementById('stop-btn').addEventListener('click', stopAutomation); // 添加拖动功能 const header = panel.querySelector('h3'); let isDragging = false; let dragOffsetX = 0; let dragOffsetY = 0; header.addEventListener('mousedown', (e) => { isDragging = true; const rect = panel.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; panel.style.transition = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; let newLeft = e.clientX - dragOffsetX; let newTop = e.clientY - dragOffsetY; // 限制在窗口范围内 const maxLeft = window.innerWidth - panel.offsetWidth; const maxTop = window.innerHeight - panel.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, maxLeft)); newTop = Math.max(0, Math.min(newTop, maxTop)); panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; panel.style.transition = 'all 0.3s ease'; } }); return panel; } function addLog(message, type = 'info') { const logContainer = document.getElementById('log-container'); if (logContainer) { const logEntry = document.createElement('div'); logEntry.className = `log-entry log-${type}`; logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logContainer.appendChild(logEntry); logContainer.scrollTop = logContainer.scrollHeight; } } function updateStatus(text) { const statusElement = document.getElementById('status-text'); if (statusElement) { statusElement.textContent = `状态:${text}`; } } function updateProgress() { const progressElement = document.getElementById('progress-fill'); const currentIndexElement = document.getElementById('current-index'); const resourceElement = document.getElementById('current-resource'); if (resources.length > 0 && currentResourceIndex >= 0) { const progress = ((currentResourceIndex + 1) / resources.length) * 100; if (progressElement) progressElement.style.width = `${progress}%`; if (currentIndexElement) currentIndexElement.textContent = `${currentResourceIndex + 1}/${resources.length}`; if (resourceElement && resources[currentResourceIndex]) { resourceElement.textContent = resources[currentResourceIndex].title; } } } // 查找当前选中的资源 - 修复:使用 class="selected" function findCurrentlySelectedResource() { const selectedLink = document.querySelector('a.selected'); if (selectedLink) { return selectedLink; } // 备用方案:查找高亮或激活的资源链接 const activeSelectors = [ 'a.selected', 'a.active', 'a.current', 'a.highlighted', 'a[style*="background"]', 'a[style*="color: red"]', 'a[style*="color: #ff0000"]', '.active > a', '.current > a', '.highlighted > a', 'a.font-bold', 'a.text-bold', 'a.fw-bold' ]; for (const selector of activeSelectors) { const activeLinks = document.querySelectorAll(selector); for (const link of activeLinks) { const onclick = link.getAttribute('onclick') || ''; if (onclick.includes('playFile') || onclick.includes('viewRes')) { return link; } } } return null; } // 获取所有资源 - 修复:只获取 onclick 包含 playFile 或 viewRes 的链接 function getAllResources() { const links = document.querySelectorAll('a'); const resources = []; links.forEach((link, index) => { const onclick = link.getAttribute('onclick') || ''; const text = link.textContent.trim(); const title = link.getAttribute('title') || text; // 只保留 onclick 包含 playFile 或 viewRes 的链接 if ((onclick.includes('playFile') || onclick.includes('viewRes')) && text && text.length > 0) { resources.push({ id: index, element: link, title: title || text, text: text }); } }); logger.success(`找到 ${resources.length} 个有效资源`); return resources; } // 一键展开所有目录 - 修复:使用 .parent_li 和 .hitarea async function expandAllDirectories() { updateStatus('正在展开所有目录,可加QQ群:756253160'); logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); logger.info('开始一键展开所有目录,可加QQ群:756253160'); logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); // 方法1:点击所有 .parent_li 元素 const parentLis = document.querySelectorAll('.parent_li'); logger.info(`找到 ${parentLis.length} 个目录项`); let clickedCount = 0; for (let i = 0; i < parentLis.length; i++) { const li = parentLis[i]; // 尝试点击展开图标 const hitarea = li.querySelector('.hitarea, .expandable-hitarea'); if (hitarea) { hitarea.click(); clickedCount++; } else { // 尝试点击 li 内部的第一个子元素(通常是展开图标) const firstChild = li.querySelector('img, span:first-child'); if (firstChild) { firstChild.click(); clickedCount++; } else { li.click(); clickedCount++; } } await sleep(config.expandDelay); } // 方法2:备用方案 - 点击所有树形图标 const treeIcons = document.querySelectorAll( 'img[src*="tree"], .tree-icon, .folder-icon, .expand-icon, img[alt="展开"], img[alt="收起"]' ); if (treeIcons.length > clickedCount) { for (let i = 0; i < treeIcons.length; i++) { treeIcons[i].click(); await sleep(config.expandDelay); } clickedCount = treeIcons.length; } logger.success(`成功展开 ${clickedCount} 个目录`); updateStatus('目录展开完成'); await sleep(2000); // 重新获取资源数量 const tempResources = getAllResources(); document.getElementById('resource-count').textContent = tempResources.length; logger.info(`扫描到 ${tempResources.length} 个可学习资源`); logger.info('请确认左侧目录树已全部展开,然后点击"开始学习"'); } // 检测当前页面类型 function detectPageType() { const hasVideo = document.querySelector('video'); const hasPDF = document.querySelector('iframe[src*="pdfjs"]'); if (hasVideo) return 'video'; if (hasPDF) return 'pdf'; return 'unknown'; } // 获取视频进度 function getVideoProgress() { const video = document.querySelector('video'); if (video) { return { current: video.currentTime || 0, total: video.duration || 0, percent: video.duration ? (video.currentTime / video.duration) * 100 : 0 }; } const progressBar = document.querySelector('.vjs-progress-bar, .video-progress-bar'); if (progressBar) { const current = parseFloat(progressBar.getAttribute('aria-valuenow')) || 0; const total = parseFloat(progressBar.getAttribute('aria-valuemax')) || 0; return { current: current, total: total, percent: total ? (current / total) * 100 : 0 }; } return null; } // 等待视频完成 async function waitForVideoComplete() { logger.info('正在监控视频播放进度...'); updateStatus('正在观看视频...'); return new Promise((resolve, reject) => { let checkCount = 0; const maxChecks = config.maxWaitTime / config.videoCheckInterval; let lastLoggedPercent = -1; videoCheckTimer = setInterval(() => { checkCount++; if (!isRunning) { clearInterval(videoCheckTimer); reject(new Error('用户停止了自动化')); return; } const progress = getVideoProgress(); if (progress && progress.total > 0) { const currentPercent = Math.floor(progress.percent); if (currentPercent !== lastLoggedPercent && currentPercent % 10 === 0) { logger.info(`视频播放进度: ${currentPercent}%`); updateStatus(`正在观看视频... (${currentPercent}%)`); lastLoggedPercent = currentPercent; } if (progress.percent >= 98) { logger.success('视频播放完成!(100%)'); clearInterval(videoCheckTimer); resolve(); } } if (checkCount >= maxChecks) { clearInterval(videoCheckTimer); logger.warning('超时!强制切换到下一个资源'); resolve(); } }, config.videoCheckInterval); }); } // 等待PDF async function waitForPDF() { logger.info(`PDF资源,等待 ${config.pdfWaitTime/1000} 秒...`); updateStatus('正在浏览PDF...'); await sleep(config.pdfWaitTime); logger.success('PDF浏览完成!'); } // 点击资源 - 修复:使用 playFile 的 onclick async function clickResource(resource) { logger.info(`点击资源: ${resource.title}`); updateStatus(`正在加载: ${resource.title}`); resource.element.scrollIntoView({ behavior: 'smooth', block: 'center' }); await sleep(500); return new Promise((resolve) => { // 直接点击元素,触发其 onclick 事件 resource.element.click(); setTimeout(resolve, config.resourceClickDelay); }); } // 主自动化流程 async function startAutomation() { isRunning = true; startTime = new Date(); currentResourceIndex = -1; logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); logger.info('开始学习流程,可加QQ群:756253160'); logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); updateStatus('正在初始化,可加QQ群:756253160'); // 获取所有资源 resources = getAllResources(); document.getElementById('resource-count').textContent = resources.length; updateProgress(); if (resources.length === 0) { logger.error('未找到任何学习资源!'); logger.error('请确保已展开所有目录树'); updateStatus('错误:未找到资源'); isRunning = false; document.getElementById('start-btn').style.display = 'block'; document.getElementById('stop-btn').style.display = 'none'; return; } // 检查是否已经有选中的资源(从中间开始) const currentlySelected = findCurrentlySelectedResource(); if (currentlySelected) { const foundIndex = resources.findIndex(r => r.element === currentlySelected); if (foundIndex !== -1) { currentResourceIndex = foundIndex - 1; logger.info(`检测到已选中的资源: "${resources[foundIndex].title}"`); logger.info(`将从第 ${foundIndex + 1} 个资源开始继续学习`); logger.info(''); } } logger.info(`准备学习 ${resources.length} 个资源`); logger.info(''); document.getElementById('start-btn').style.display = 'none'; document.getElementById('stop-btn').style.display = 'block'; while (isRunning && currentResourceIndex < resources.length - 1) { currentResourceIndex++; const resource = resources[currentResourceIndex]; logger.info(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); logger.info(`学习资源 [${currentResourceIndex + 1}/${resources.length}]`); logger.info(`标题: ${resource.title}`); updateProgress(); await clickResource(resource); const pageType = detectPageType(); logger.info(`类型: ${pageType}`); if (pageType === 'video') { logger.info('等待视频播放完成...'); await waitForVideoComplete(); } else if (pageType === 'pdf') { logger.info('等待PDF浏览...'); await waitForPDF(); } else { logger.warning('未知类型,等待5秒...'); await sleep(5000); } logger.success('完成!'); } if (isRunning) { logger.info(''); logger.success('所有资源学习完成!【可加QQ群:756253160】'); updateStatus('学习完成!【可加QQ群:756253160】'); const endTime = new Date(); const duration = Math.floor((endTime - startTime) / 1000 / 60); logger.success(`总用时: ${duration} 分钟`); } stopAutomation(); } function stopAutomation() { isRunning = false; if (videoCheckTimer) { clearInterval(videoCheckTimer); videoCheckTimer = null; } document.getElementById('start-btn').style.display = 'block'; document.getElementById('stop-btn').style.display = 'none'; updateStatus('已停止'); logger.info('学习流程已停止'); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function init() { logger.info('脚本已加载,等待页面准备完成...'); // 监听用户手动点击资源事件 document.addEventListener('click', (event) => { const clickedElement = event.target; let resourceLink = null; if (clickedElement.tagName === 'A') { const onclick = clickedElement.getAttribute('onclick') || ''; if (onclick.includes('playFile') || onclick.includes('viewRes')) { resourceLink = clickedElement; } } else if (clickedElement.closest) { const closestLink = clickedElement.closest('a'); if (closestLink) { const onclick = closestLink.getAttribute('onclick') || ''; if (onclick.includes('playFile') || onclick.includes('viewRes')) { resourceLink = closestLink; } } } if (resourceLink && isRunning && resources && resources.length > 0) { const foundIndex = resources.findIndex(r => r.element === resourceLink); if (foundIndex !== -1 && foundIndex !== currentResourceIndex) { currentResourceIndex = foundIndex - 1; logger.info(''); logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); logger.warning(`检测到手动点击: "${resources[foundIndex].title}"`); logger.warning(`已从第 ${foundIndex + 1} 个资源继续学习`); logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); logger.info(''); } } }); setTimeout(() => { createControlPanel(); logger.success('控制面板已创建!'); logger.info('请点击"一键展开所有目录",然后点击"开始学习"'); }, 2000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.addEventListener('beforeunload', (e) => { if (isRunning) { e.preventDefault(); e.returnValue = '学习正在进行中,确定要离开吗?【可加QQ群:756253160】'; } }); window.ynouAutomation = { start: startAutomation, stop: stopAutomation, getResources: () => resources, getStatus: () => ({ isRunning, currentIndex: currentResourceIndex, total: resources.length }) }; })();