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.

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

})();