Greasy Fork

Greasy Fork is available in English.

SuperMario 国家开放大学自动学习

超级马里奥(SuperMario)国开大学自动脚本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

    // >>>>>>>>>>>>>>>>>>>> 代码执行结束标记 <<<<<<<<<<<<<<<<<<<<

})();