Greasy Fork

Greasy Fork is available in English.

UOOC 学习助手_szu_v2

自动静音、二倍速、失焦不断播、自动连播、自动答题(视频中非测验)。递归可开关,修复直接跳过首测验视频小节的bug。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         UOOC 学习助手_szu_v2
// @namespace    https://github.com/xiaochai-123
// @version      1.4.2
// @description  自动静音、二倍速、失焦不断播、自动连播、自动答题(视频中非测验)。递归可开关,修复直接跳过首测验视频小节的bug。
// @license      GPL
// @match        *://uooc.net.cn/home/learn/index*
// @match        *://www.uooc.net.cn/home/learn/index*
// @match        *://uooconline.com/home/learn/index*
// @match        *://www.uooconline.com/home/learn/index*
// @grant        none
// ==/UserScript==

(function () {
    "use strict";

    // 配置常量
    const CONFIG = {
        playbackRate: 2,
        interval: {
            player: 500,
            cleaner: 800,
            quiz: 500,
            logic: 800
        }
    };

    // 自动播放和倍速静音
    function keepPlaying() {
        const video = document.getElementById("player_html5_api");
        if (video) {
            // 优化:仅在状态不一致时修改,减少DOM操作开销
            if (!video.muted) video.muted = true;
            if (video.playbackRate !== CONFIG.playbackRate) video.playbackRate = CONFIG.playbackRate;
            if (video.paused && !video.ended) {
                video.play().catch(() => {}); // 捕获在此处可能产生的Promise报错
            }
        }
        const playBtn = document.querySelector(".vjs-big-play-button");
        if (playBtn && playBtn.style.display !== 'none') {
            playBtn.click();
        }
    }
    setInterval(keepPlaying, CONFIG.interval.player);

    // 强力弹窗和遮罩清理函数
    setInterval(() => {
        let hasSmartVerification = false;
        document.querySelectorAll('.layui-layer').forEach(layer => {
            const layerText = layer.innerText || '';
            // 关键词检测
            if (['验证', '智能', '提交'].some(kw => layerText.includes(kw)) || layer.querySelector('.layui-layer-btn')) {
                hasSmartVerification = true;
            }
        });

        if (!hasSmartVerification) {
            document.querySelectorAll('.layui-layer-shade').forEach(e => e.remove());
            document.querySelectorAll('.layui-layer.layui-layer-page').forEach(e => {
                const layerText = e.innerText || '';
                if (!['验证', '智能', '提交'].some(kw => layerText.includes(kw))) {
                    e.remove();
                }
            });
            document.querySelectorAll('.vjs-mask').forEach(e => e.remove());
            
            let quizDom = document.querySelector(".smallTest-view");
            if (quizDom && quizDom.offsetTop > 600) {
                quizDom.style.display = "none";
            }
        }
    }, CONFIG.interval.cleaner);

    // 自动答题
    let lastQuizQuestion = null;
    function autoAnswerQuiz() {
        let quizLayer = document.querySelector("#quizLayer");
        if (quizLayer && quizLayer.style.display !== "none") {
            try {
                let videoDiv = document.querySelector("div[uooc-video]");
                if (!videoDiv) return;
                
                let source = videoDiv.getAttribute("source");
                if (!source) return;
                
                let quizList = JSON.parse(source).quiz || [];
                let quizQuestionElem = document.querySelector(".smallTest-view .ti-q-c");
                if (!quizQuestionElem) return;
                
                let quizQuestion = quizQuestionElem.innerHTML.trim();

                if (lastQuizQuestion === quizQuestion && quizLayer.classList.contains("answered")) {
                    return;
                }

                let quizIndex = quizList.findIndex(q => q.question === quizQuestion);
                if (quizIndex === -1) return;
                
                // 注意:平台数据通常是原始JS字符串,eval在此处必须保留
                let quizAnswer = eval(quizList[quizIndex].answer);
                let quizOptions = quizLayer.querySelector("div.ti-alist");
                
                if (quizOptions) {
                    for (let ans of quizAnswer) {
                        let labelIndex = ans.charCodeAt() - "A".charCodeAt();
                        // 增加安全校验
                        let opt = quizOptions.children[labelIndex];
                        if (opt) opt.click();
                    }
                    let btn = quizLayer.querySelector("button");
                    if (btn) btn.click();
                    
                    quizLayer.classList.add("answered");
                    lastQuizQuestion = quizQuestion;
                    console.log("自动答题已完成:", quizQuestion, quizAnswer.toString());
                }
            } catch (error) {
                console.log("自动答题发生错误:", error);
            }
        } else {
            lastQuizQuestion = null;
            let answeredLayer = document.querySelector("#quizLayer.answered");
            if (answeredLayer) answeredLayer.classList.remove("answered");
        }
    }
    setInterval(autoAnswerQuiz, CONFIG.interval.quiz);

    // ======= 递归连播开关和核心 =======
    let autoRecursiveFlag = false;

    // 创建控制按钮(样式优化)
    function createControlBtn() {
        let btn = document.createElement("button");
        btn.innerText = "递归上课:关闭";
        Object.assign(btn.style, {
            position: "fixed",
            top: "140px",
            right: "30px",
            zIndex: "9999",
            background: "#00796b",
            color: "#fff",
            border: "none",
            padding: "11px 22px",
            borderRadius: "8px",
            cursor: "pointer",
            boxShadow: "0 2px 12px rgba(0,0,0,0.18)",
            userSelect: "none"
        });
        
        btn.onclick = function () {
            autoRecursiveFlag = !autoRecursiveFlag;
            btn.innerText = "递归上课:" + (autoRecursiveFlag ? "开启" : "关闭");
            if (autoRecursiveFlag) {
                startUltimateCourseRush();
            }
        };
        document.body.appendChild(btn);
    }
    createControlBtn();

    function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

    function clearShader() {
        let hasSmartVerification = false;
        document.querySelectorAll('.layui-layer').forEach(layer => {
            const layerText = layer.innerText || '';
            if (['验证', '智能', '提交'].some(kw => layerText.includes(kw)) || layer.querySelector('.layui-layer-btn')) {
                hasSmartVerification = true;
            }
        });

        if (!hasSmartVerification) {
            document.querySelectorAll("div.layui-layer-shade").forEach(shader => shader.remove());
            document.querySelectorAll('.vjs-mask').forEach(e => e.remove());
        }
    }

    function findCurrentVideoNode() {
        return document.querySelector('.basic.active') || 
               document.querySelector('.video-list li.current, .chapter-list li.current, .menu li.current, li.active, li.selected');
    }

    function findNextVideoInSameChapter(currentNode) {
        if (!currentNode) return null;
        let chapterContainer = currentNode.closest('ul, .chapter-list, .video-list');
        if (!chapterContainer) return null;
        
        let allVideoNodes = Array.from(chapterContainer.querySelectorAll('li, .basic'));
        let currentIndex = allVideoNodes.indexOf(currentNode);
        
        if (currentIndex === -1 || currentIndex >= allVideoNodes.length - 1) {
            return null;
        }
        
        for (let i = currentIndex + 1; i < allVideoNodes.length; i++) {
            let nextNode = allVideoNodes[i];
            // 增加空值检查
            if (nextNode.querySelector('.icon-video, span.icon-video') || (nextNode.classList.contains('taskpoint') && !nextNode.innerText.includes("测验"))) {
                return nextNode;
            }
        }
        return null;
    }

    function playNextVideoInChapter() {
        let current = findCurrentVideoNode();
        let next = findNextVideoInSameChapter(current);
        if (next) {
            let clickTarget = next.querySelector('a') || next;
            clickTarget.click();
            setTimeout(() => {
                const video = document.getElementById('player_html5_api');
                if (video) {
                    video.muted = true;
                    video.playbackRate = CONFIG.playbackRate;
                    video.play().catch(() => {});
                }
            }, 1000);
            return true;
        }
        return false;
    }

    function waitForVideoCompletion() {
        return new Promise(resolve => {
            const video = document.getElementById('player_html5_api');
            if (!video) {
                resolve();
                return;
            }
            video._recursiveResolve = resolve;
            video.onended = function() {
                setTimeout(() => {
                    if (video._recursiveResolve) {
                        video._recursiveResolve();
                    }
                }, 1500);
            };
            video.muted = true;
            video.playbackRate = CONFIG.playbackRate;
            
            const playBtn = document.querySelector(".vjs-big-play-button");
            if (playBtn) playBtn.click();
            
            setTimeout(() => {
                if (video.ended && video._recursiveResolve) {
                    video._recursiveResolve();
                }
            }, 500);
        });
    }

    function findCurrentChapter() {
        const currentVideoNode = findCurrentVideoNode();
        if (!currentVideoNode) return null;
        let node = currentVideoNode;
        while (node && node.parentElement) {
            if (node.parentElement.classList.contains('rank-1') || node.classList.contains('rank-1-item')) {
                return node;
            }
            node = node.parentElement;
        }
        return null;
    }

    async function searchUncompleteFromCurrentChapter() {
        const catalogRoot = document.querySelector("ul.rank-1");
        if (!catalogRoot) return;
        
        const chapters = Array.from(catalogRoot.children);
        const currentChapter = findCurrentChapter();
        let startIndex = 0;
        
        if (currentChapter) {
            startIndex = chapters.indexOf(currentChapter);
            if (startIndex === -1) startIndex = 0;
        }
        
        for (let i = startIndex; i < chapters.length; i++) {
            await checkActive(chapters[i]);
        }
    }

    async function checkActive(catalog) {
        if (!catalog) return;
        let children = catalog.children;
        let elem = catalog.firstElementChild;
        
        // 展开章节
        if (elem) {
            let iElement = elem.getElementsByTagName("i")[0];
            if (iElement && iElement.classList.contains("icon-xiangxia")) {
                elem.click();
            }
            await sleep(500);
        }
        
        for (let i = 1; i < children.length; i++) {
            const child = children[i];
            if (child.tagName === "DIV") {
                // 仅选择直接子元素,避免深度查询
                const resources = Array.from(child.querySelectorAll(':scope > .basic'));
                for (let r of resources) {
                    if (r.classList.contains('complete')) continue;
                    
                    let nameSpan = r.querySelector('.tag-source-name');
                    let nameText = nameSpan ? (nameSpan.innerText || '') : '';
                    
                    if (nameSpan && nameSpan.classList.contains('taskpoint') && nameText.includes('测验')) {
                        continue;
                    }
                    
                    if (r.querySelector('.icon-video') || /视频/.test(nameText)) {
                        r.click();
                        clearShader();
                        await waitForVideoCompletion();
                        while (playNextVideoInChapter()) {
                            await waitForVideoCompletion();
                        }
                        return; // 完成一个视频序列后返回上一层继续
                    }
                }
            } else if (child.tagName === "UL") {
                await searchUncomplete(child);
            }
        }
    }

    async function searchUncomplete(query) {
        if (!query) return;
        let catalog = query.children;
        for (let i = 0; i < catalog.length; i++) {
            await checkActive(catalog[i]);
        }
    }

    async function startUltimateCourseRush() {
        if (!autoRecursiveFlag) return;
        await searchUncompleteFromCurrentChapter();
    }

    function bindVideoEnded() {
        const video = document.getElementById('player_html5_api');
        if (video && !video._autoNextBound) {
            video._autoNextBound = true;
            video.onended = function () {
                if (autoRecursiveFlag) {
                    // 如果无法播放同章下一个,则触发递归寻找下一章
                    if (!playNextVideoInChapter()) {
                        setTimeout(() => { startUltimateCourseRush(); }, 1500);
                    }
                }
            };
        }
    }

    // 核心初始化逻辑 - 替换掉了 $(document).ready
    function init() {
        console.log("UOOC 学习助手 v1.4.2 已加载");
        setTimeout(() => { 
            if (autoRecursiveFlag) startUltimateCourseRush(); 
        }, 1000);

        // 递归保护循环
        setInterval(async function () {
            if (autoRecursiveFlag) {
                const video = document.getElementById('player_html5_api');
                // 增加判断:如果当前没有激活的节点,也尝试启动递归(防止卡死)
                if ((!video || video.ended) && !document.querySelector(".basic.active")) {
                    await startUltimateCourseRush();
                }
            }
        }, CONFIG.interval.logic);

        setInterval(bindVideoEnded, CONFIG.interval.logic);
    }

    // 原生 DOM Ready 检测
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        init();
    } else {
        window.addEventListener('load', init);
    }

})();