21tb new UI AutoPlay Script

21tb Auto-Play Script,Only effective on the new version of the course interface. Automatically jumps after course viewing completion. Automatically selects unviewed chapters. Use in conjunction with other scripts if necessary.

当前为 2025-10-31 提交的版本,查看 最新版本

// ==UserScript==
// @name         21tb new UI AutoPlay Script
// @namespace    http://tampermonkey.net/
// @version      2025-10-28
// @description  21tb Auto-Play Script,Only effective on the new version of the course interface. Automatically jumps after course viewing completion. Automatically selects unviewed chapters. Use in conjunction with other scripts if necessary.
// @author       code support by Gemini
// @match        https://*.21tb.com/courseSetting/courseLearning/play?courseType=*&courseId=*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=21tb.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===================================================================================================
    // ⭐️ 全局状态变量 (供所有函数共享)
    // ===================================================================================================
    let allFinished = false;
    let intervalId = null;
    let initialCheckDone = false;

    // ===================================================================================================
    // ⭐️ 配置项
    // ===================================================================================================
    const VIDEO_ITEM_SELECTOR = '.section-item';
    const ACTIVE_CLASS = 'section-item-active';
    const FINISHED_CLASS = 'finish';

    // --- 【函数 1】模拟低级别点击事件 (为跳转做准备) ---
    function simulateClick(element) {
        if (!element) return;
        const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
        element.dispatchEvent(clickEvent);
    }


    // ⚠️ 占位符:心跳监控函数
    function checkVideoProgress(videoElement) {
        console.log("... [Heartbeat Placeholder] 心跳监控启动 ...");
        // 确保在启动新监控前,清除旧的定时器
        if (intervalId) { clearInterval(intervalId); }

        const initialActiveItem = document.querySelector(`.${ACTIVE_CLASS}`);

        // 启动时的播放尝试 (解决浏览器限制)
        // --- 【新增函数】安全调用播放方法 ---
        const safePlay = (element, isMutedAttempt = false) => {
            const playPromise = element.play();

            // 检查返回值是否为 Promise (即是否为 undefined)
            if (playPromise !== undefined) {
                playPromise.then(() => {
                    // 播放成功
                }).catch(error => {
                    // 播放被阻止 (通常是静音问题)
                    if (!isMutedAttempt) {
                        console.warn('⚠️ [Heartbeat] 自动播放被阻止,尝试静音回退。');
                        element.muted = true;
                        // 递归调用,尝试静音播放
                        safePlay(element, true);
                    } else {
                        console.error('❌ [Heartbeat] 静音回退失败,需要用户交互。', error);
                    }
                });
            } else {
                // 非 Promise 返回值 (假设播放成功或在后台处理,或者被静默阻止)
                console.log('💡 [Heartbeat] play() 未返回 Promise (旧环境/自定义播放器)。');
            }
        };

        let checkCount = 0;

        intervalId = setInterval(() => {
            checkCount++;
            const duration = videoElement.duration;
            const currentTime = videoElement.currentTime;

            // 🎯 验证点 1:每次执行都打印输出,检查定时器是否持续
            console.log(`[Polling Check] 运行中... 检查次数: ${checkCount}`);

            // ---------------------------------------------
            // 🎯 关键修正:检测是否被手动切换
            // ---------------------------------------------
            const currentActiveItem = document.querySelector(`.${ACTIVE_CLASS}`);

            // 条件:
            // 1. 当前 DOM 中没有活跃项,或
            // 2. 活跃项仍然存在,但它已经不是我们启动心跳时锁定的那个元素
            if (currentActiveItem !== initialActiveItem) {
                // 确认是用户手动切换导致的非正常退出
                console.log("↩️ [Heartbeat] 检测到目录活跃项变更或丢失,判定为用户手动切换。停止心跳,返回主循环。");
                clearInterval(intervalId);
                supervisorLoop(); // 将控制权交回主循环
                return;
            }

            // ---------------------------------------------
            // 🎯 边界检查:视频元素丢失
            // ---------------------------------------------
            if (!videoElement || videoElement.nodeType === 3) {
                console.error("❌ [Heartbeat] 视频元素已丢失或被移除,停止心跳。");
                clearInterval(intervalId);
                // 重新启动主循环,让它重新评估页面状态
                supervisorLoop();
                return;
            }


            // 🎯 验证点 2:检查关键变量的值,用于调试判断条件
            console.log(`[Polling Data] Duration: ${duration.toFixed(1)}s, CurrentTime: ${currentTime.toFixed(1)}s, Paused: ${videoElement.paused}`);

            // ---------------------------------------------
            // 🎯 自动播放 (心跳) 逻辑
            // ---------------------------------------------
            // 如果视频暂停了,并且还没到最后 1 秒,就尝试启动播放
            if (videoElement.paused && currentTime < duration - 1.0 && duration > 0) {
                safePlay(videoElement);
                // .catch(() => {
                //     videoElement.muted = true;
                //     videoElement.play();
                // });
                console.log('💓 [Heartbeat] 视频暂停被检测到,尝试重新启动播放。');
            }

            // ---------------------------------------------
            // 🎯 结束判定逻辑
            // ---------------------------------------------
            // 条件:时长有效 AND 当前时间接近末尾 (0.5秒内) AND 视频已暂停
            if (duration > 0 && currentTime >= duration - 0.5 && videoElement.paused) {

                clearInterval(intervalId); // 停止轮询

                console.log("🎥 [AutoSkip] 视频播放结束事件触发(通过轮询判定)。");

                // 触发跳转逻辑,并将控制权交回给主循环
                handleNextEpisode();
            }

        }, 5000); // 每 500 毫秒检查一次

        safePlay(videoElement);
        console.log("🔄 [AutoSkip] 已启动 500ms 轮询检查视频播放进度。");

    }

    // ⚠️ 占位符:下一集跳转函数
    function handleNextEpisode() {
        console.log("... [Next Episode Placeholder] 触发跳转 ...");
        // 1. 即时查找所有视频列表项
        const allItemsNodeList = document.querySelectorAll(VIDEO_ITEM_SELECTOR);
        if (allItemsNodeList.length === 0) {
            console.error("⚠️ [Next] 目录元素丢失,无法执行跳转。");
            // 尝试重启 supervisorLoop,看目录是否能再次加载
            setTimeout(supervisorLoop, 1000);
            return;
        }
        const allItems = Array.from(allItemsNodeList);

        // 2. 查找第一个未完成的视频项 (无论当前是哪个视频)
        let nextItem = null;
        let nextIndex = -1;
        // 查找第一个不包含 FINISHED_CLASS 的项
        nextIndex = allItems.findIndex(item => !item.classList.contains(FINISHED_CLASS));
        if (nextIndex !== -1) {
            nextItem = allItems[nextIndex];
        }

        // 3. 执行点击跳转
        if (nextItem) {
            // 检查目标是否已经是活跃项
            if (nextItem.classList.contains(ACTIVE_CLASS)) {
                console.log(`✅ [Next] 目标视频(索引 ${nextIndex + 1})已是活跃项,无需点击。`);
                // 直接将控制权交回给主循环,让监控器启动
                setTimeout(supervisorLoop, 1000);
                return;
            }

            // 模拟点击操作
            simulateClick(nextItem);

            console.log(`🚀 [Next] 视频结束,跳转到目录中第一个未完成视频(索引 ${nextIndex + 1})。`);

            // 4. 将控制权交回给主循环 (通过延迟调用 supervisorLoop)
            setTimeout(supervisorLoop, 1000); // 极短延迟后立即启动主循环,评估新状态

        } else {
            console.log("🏁 [Next] 播放列表已完全结束,所有后续视频均已完成。");
            allFinished = true; // 标记所有已完成,循环将终止
        }
        // 🚨 跳转后,递归函数应该再次启动!
    }


    // --- 核心函数:递归检查、状态判断和流程控制 ---
    function supervisorLoop() {

        // 如果全局标记所有已完成,则停止整个脚本
        if (allFinished) {
            console.log("🏁 [Supervisor] 所有视频已完成,停止监控循环。");
            return;
        }

        const activeItem = document.querySelector(`.${ACTIVE_CLASS}`);

        // 1. 检查目录是否加载
        if (!activeItem) {
            console.log("⏳ [Supervisor] 目录尚未加载,1秒后重试...");
            setTimeout(supervisorLoop, 1000);
            return;
        }

        // 2. 检查当前活跃项的状态
        if (activeItem) {
            // const directoryArray = Array.from(document.querySelectorAll(VIDEO_ITEM_SELECTOR));
            // console.log("✅ [Loader] 目录加载成功!共找到 ${directoryArray.length} 个视频。");
            const videoElement = document.querySelector('video');

            if (activeItem.classList.contains(FINISHED_CLASS)) {
                // 状态 A:当前视频已完成 (包含 active 和 finish)
                console.log("⏭️ [Supervisor] 当前活跃项已标记为完成,触发跳转。");
                // 立即跳转,不需要心跳监控
                handleNextEpisode();
                return; // 跳转后,在跳转函数内会启动下一次循环或监控

            } else if (videoElement) {
                // 状态 B:当前视频未完成,且 video 元素已找到
                console.log("▶️ [Supervisor] 当前视频未完成,5秒后启动心跳监控。");
                // 启动监控,监控器会负责在视频结束时调用 handleNextEpisode
                    checkVideoProgress(videoElement);
                return; // 监控器启动后,不需要继续递归循环

            } else {
                 // 状态 C:目录已加载,但视频播放器未加载 (常见于单页应用切换)
                console.log("⏳ [Supervisor] 目录就绪但播放器未加载,1秒后重试...");
                setTimeout(supervisorLoop, 1000);
                return;
            }
        }

        // 默认:如果流程未返回,继续递归等待
        console.log("⏳ [Supervisor] 脚本启动失败,正在尝试重启");
        setTimeout(supervisorLoop, 1000);
    }

    // ===================================================================================================
    // 脚本入口
    // ===================================================================================================
    supervisorLoop();

})();