// ==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
})
};
})();