Greasy Fork is available in English.
超级马里奥(SuperMario)国开大学自动脚本
// ==UserScript==
// @name SuperMario 国家开放大学自动学习
// @namespace https://smartadmin.cn
// @version 1.1
// @description 超级马里奥(SuperMario)国开大学自动脚本
// @author 小马哥(mawd86)
// @match *://*.ouchn.cn/*
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @grant none
// @license MIT
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// ----------------------------------------------------
// 【版本号与配置区域】
// ----------------------------------------------------
// 版本号与脚本头部同步
const SCRIPT_VERSION = '1.1'; // <<< 版本号更新
const PRIMARY_SELECTOR = '.next-btn';
const SECONDARY_SELECTORS = ['#next-page-btn', '.next-button', '.button.medium.button-green'];
const NEXT_BUTTON_TEXTS = ['下一个', '下一步', '完成', '继续'];
const CLICK_DELAY_MS = 5000; // 默认回退延迟 (5 秒)
const TARGET_PLAYBACK_RATE = 2.0; // 目标播放速度
const VIDEO_CHECK_INTERVAL_MS = 1000; // 视频检测频率 (1 秒)
// 播放器控制按钮选择器。
const VIDEO_CONTROL_SELECTORS = {
// mvp-fonts-play 代表“未播放”,是我们想要点击的状态
PLAY_BUTTON: ['.mvp-fonts-play', '.vjs-play-control', '.play-button', '.prism-play-btn', '.art-control-play'],
MUTE_BUTTON: ['.vjs-mute-control', '.mute-button', '.prism-volume-btn', '.art-control-volume']
};
// 全局控制变量
let observer = null;
let clickTimer = null;
let videoCheckTimer = null;
let processingVideoElement = null;
let initialScrollPerformed = false; // 控制首次滚动是否已执行
// >>>>>>>>>>>>>>>>>>>> 代码执行开始标记 <<<<<<<<<<<<<<<<<<<<
console.log(`[SuperMario] 脚本执行开始 V${SCRIPT_VERSION} (滚动日志增强版)。`);
// ----------------------------------------------------
// 通用函数 (查找、循环控制、Observer)
// ----------------------------------------------------
/** 查找下一页按钮,三重查找机制。*/
function findNextButton(doc) {
let nextButton = null;
const allSelectors = [PRIMARY_SELECTOR, ...SECONDARY_SELECTORS];
for (const selector of allSelectors) {
nextButton = doc.querySelector(selector);
if (nextButton) return nextButton;
}
for (const text of NEXT_BUTTON_TEXTS) {
const xpath = `//*[self::button or self::a or self::div][contains(., '${text}') and not(ancestor::script) and not(ancestor::style) and not(ancestor::textarea)]`;
const result = doc.evaluate(xpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
if (result.singleNodeValue) return result.singleNodeValue;
}
return null;
}
/** 页面滚动到底部。*/
function scrollToBottom(doc) {
// 确保 doc.body 和 doc.defaultView 存在
if (doc.documentElement && doc.body && doc.defaultView) {
const scrollTarget = doc.body.scrollHeight;
const currentPosition = doc.defaultView.scrollY;
// 增加日志输出,方便调试
console.log(`[SuperMario] 滚动日志:当前位置 Y=${currentPosition},目标滚动高度:${scrollTarget}。`);
// 使用 smooth 行为,模拟用户滚动,并滚动到文档最大高度
doc.defaultView.scrollTo({
top: scrollTarget,
behavior: 'smooth'
});
console.log('[SuperMario] 动作:已触发页面自动滚动到底部的动作。');
} else {
console.warn('[SuperMario] 警告:无法执行页面滚动操作,文档或窗口对象不可用。');
}
}
/** 启动/重启整个观察和点击流程 */
function restartObservation(isImmediate = false, delayMs = CLICK_DELAY_MS) {
if (clickTimer) clearTimeout(clickTimer);
if (videoCheckTimer) clearInterval(videoCheckTimer);
const finalDelay = isImmediate ? 100 : delayMs;
if (isImmediate) {
console.log('[SuperMario] 流程控制:视频播放完毕,等待 100ms 立即检查点击。');
} else {
console.log(`[SuperMario] 流程控制:启动 ${finalDelay / 1000} 秒倒计时...`);
}
clickTimer = setTimeout(() => startClickProcess(isImmediate), finalDelay);
}
/** 设置 MutationObserver 监听页面的 DOM 变化 */
function setupObserver() {
// 由于 @run-at document-start,DOM可能尚未完全加载,但我们先注册观察器
restartObservation();
if (observer) return;
observer = new MutationObserver(function(mutations) {
// 每次DOM变化后,重启流程计时器
restartObservation();
});
const config = { childList: true, subtree: true, attributes: true };
observer.observe(document.body, config);
console.log('[SuperMario] 状态:启动 DOM 观察器,脚本将持续运行。');
}
// ----------------------------------------------------
// 【视频检测与播放模块 V1.1 核心】
// ----------------------------------------------------
/**
* 查找并模拟点击播放器控制按钮,并输出日志。
*/
function simulateControlClick(doc, selectors, type) {
let clicked = false;
// 确保输出日志,表明意图
console.log(`[SuperMario] 动作:尝试模拟点击 ${type} 按钮 (使用鼠标事件序列模拟)...`);
for (const selector of selectors) {
const button = doc.querySelector(selector);
// 确保元素存在、可见且尚未点击
if (button && button.offsetWidth > 0 && button.offsetHeight > 0 && !clicked) {
// *** 模拟鼠标点击事件序列 ***
// 发送 mousedown 和 mouseup 模拟真实用户交互
const clickEvents = ['mousedown', 'mouseup'];
clickEvents.forEach(eventType => {
const event = new MouseEvent(eventType, {
bubbles: true,
cancelable: true,
view: doc.defaultView
});
button.dispatchEvent(event);
});
// 确保事件处理器被触发 (作为兼容性兜底)
button.click();
// 成功日志
console.log(`[SuperMario] 日志:成功执行 模拟点击 ${type} 按钮的动作 (${selector})。`);
clicked = true;
break;
}
}
if (!clicked) {
// 失败日志
console.warn(`[SuperMario] 警告:未找到 ${type} 按钮或按钮不可见,模拟点击失败。`);
}
return clicked;
}
/**
* 每秒执行一次,检查视频是否播放完毕。
*/
function checkVideoStatus() {
const video = processingVideoElement;
if (!video || video.readyState < 3 || isNaN(video.duration) || video.duration <= 0) {
return;
}
// 视频播放完成的判断逻辑:currentTime 接近 duration (预留 0.5 秒误差)
const isFinished = video.currentTime >= video.duration - 0.5;
if (isFinished) {
console.log('[SuperMario] 日志:视频播放完毕。');
// 清理轮询状态
if (videoCheckTimer) clearInterval(videoCheckTimer);
videoCheckTimer = null;
processingVideoElement = null;
restartObservation(true);
} else {
const current = Math.floor(video.currentTime);
const duration = Math.floor(video.duration);
console.log(`[SuperMario] 进度:${current}s / ${duration}s。`);
}
}
/**
* 核心视频处理逻辑:模拟点击、强制属性、启动轮询。
*/
function processVideo(doc) {
const videoElement = doc.querySelector('video');
if (videoElement) {
console.log('[SuperMario] 日志:检测到视频网页。');
if (videoElement.ended) {
console.log('[SuperMario] 状态:视频已处于播放完毕状态,无需处理。');
return false;
}
if (processingVideoElement === videoElement) {
console.log('[SuperMario] 状态:视频正在轮询检测中,无需重复处理。');
return true;
}
// --- 阶段 1: 模拟点击 (优化逻辑顺序:先静音,后播放) ---
// 1. 模拟点击静音按钮 (避免出声,优先执行)
simulateControlClick(doc, VIDEO_CONTROL_SELECTORS.MUTE_BUTTON, '静音');
// 2. 模拟点击播放按钮 (使用精准状态判断)
// 检查是否存在 mvp-fonts-play (未播放状态)
const playIcon = doc.querySelector('.mvp-fonts-play');
const pauseIcon = doc.querySelector('.mvp-fonts-pause');
// ** 精准判断逻辑 **
if (playIcon) {
// 如果找到 mvp-fonts-play (未播放图标),说明播放器处于暂停状态,需要点击。
console.log('[SuperMario] 状态:检测到 mvp-fonts-play 图标,执行播放点击。');
simulateControlClick(doc, VIDEO_CONTROL_SELECTORS.PLAY_BUTTON, '播放');
} else if (pauseIcon) {
// 如果找到 mvp-fonts-pause (正在播放图标),说明正在播放,跳过点击。
console.log('[SuperMario] 状态:检测到 mvp-fonts-pause 图标,跳过播放点击。');
} else if (videoElement.paused) {
// 如果找不到特定图标,则回退到原生 video 标签的 paused 属性来判断。
console.warn('[SuperMario] 警告:未找到 mvp 播放图标,回退到 videoElement.paused 判断。');
simulateControlClick(doc, VIDEO_CONTROL_SELECTORS.PLAY_BUTTON, '播放');
} else {
console.log('[SuperMario] 状态:视频正在播放中,跳过播放点击。');
}
// --- 阶段 2: 强制属性设置 (确保功能生效) ---
// 1. 强制静音 (作为兜底)
videoElement.muted = true;
console.log('[SuperMario] 日志:已强制执行静音功能。');
// 2. 强制倍速
if (typeof videoElement.playbackRate !== 'undefined') {
videoElement.playbackRate = TARGET_PLAYBACK_RATE;
console.log(`[SuperMario] 日志:已强制执行 ${TARGET_PLAYBACK_RATE} 倍速播放。`);
} else {
console.warn('[SuperMario] 警告:视频播放器不支持倍速播放。');
}
// --- 阶段 3: 播放和启动轮询 ---
// 尝试调用 play() (作为属性回退和最终确认)
console.log('[SuperMario] 日志:尝试调用 video.play() (最终播放确认)...');
videoElement.play().then(() => {
console.log('[SuperMario] 状态:视频开始自动播放。');
}).catch(e => {
console.warn('[SuperMario] 警告:video.play() 失败 (模拟点击或属性设置可能已生效)。');
});
// 启动 1 秒轮询
if (videoCheckTimer) clearInterval(videoCheckTimer);
processingVideoElement = videoElement;
videoCheckTimer = setInterval(checkVideoStatus, VIDEO_CHECK_INTERVAL_MS);
return true;
}
return false;
}
// ----------------------------------------------------
// 【核心流程控制】
// ----------------------------------------------------
/** 核心流程控制:遍历 IFrame,处理视频或点击按钮。 */
function startClickProcess(isImmediateCheck = false) {
if (clickTimer) clearTimeout(clickTimer);
let actionHandled = false;
const iframes = document.querySelectorAll('iframe');
// --- 阶段 1: 视频检测和播放 ---
[document, ...Array.from(iframes)].forEach(el => {
if (actionHandled) return;
try {
// 如果是 iframe,尝试获取其内容文档
const doc = (el === document) ? document : (el.contentDocument || el.contentWindow.document);
if (doc && processVideo(doc)) {
actionHandled = true;
}
} catch (e) { /* 忽略跨域错误 */ }
});
// --- 阶段 1.5: 视频页面滚动到底部 (加载完并延迟 5 秒后,且只执行一次) ---
if (actionHandled && !initialScrollPerformed) {
scrollToBottom(document);
initialScrollPerformed = true;
}
// --- 阶段 2: 按钮点击 ---
// 如果视频处理已完成,并且不是即时检查点击(即不是视频结束触发),则等待下一次轮询
if (actionHandled && !isImmediateCheck) {
return;
}
let buttonClicked = false;
[document, ...Array.from(iframes)].forEach(el => {
if (buttonClicked) return;
try {
const doc = (el === document) ? document : (el.contentDocument || el.contentWindow.document);
const nextButton = findNextButton(doc);
if (nextButton && !nextButton.disabled && nextButton.getAttribute('disabled') === null) {
// 模拟点击下一页按钮
nextButton.click();
buttonClicked = true;
}
} catch (e) { /* 忽略 */ }
});
// --- 结果处理 ---
if (buttonClicked) {
console.log('[SuperMario] 状态:成功点击了下一页按钮。');
restartObservation();
} else if (isImmediateCheck) {
console.warn('[SuperMario] 警告:视频播放完毕后,立即点击下一页失败,恢复 5 秒轮询。');
restartObservation();
} else {
console.warn(`[SuperMario] 状态:${CLICK_DELAY_MS / 1000}秒延迟后,未找到或无法点击下一页按钮。`);
}
}
// ----------------------------------------------------
// 启动逻辑
// 由于 @run-at document-start,我们先设置观察器,让它在 DOM 变化时检查
setupObserver();
// >>>>>>>>>>>>>>>>>>>> 代码执行结束标记 <<<<<<<<<<<<<<<<<<<<
})();