XXXXXX / SCU eCourse 自动连续播放

// ==UserScript==
// @name         SCU eCourse 自动连续播放
// @namespace    scu-ecourse-autoplay
// @version      1.0.0
// @description  四川大学 eCourse:自动播放当前视频,播放结束后自动进入下一学习内容
// @author       ChatGPT
// @license      MIT
// @match        https://ecourse.scu.edu.cn/learn/course/detail/mooc/courseWare/*
// @match        https://ecourse.scu.edu.cn/learn/course/mooc/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    let enabled = GM_getValue('scu_autoplay_enabled', true);

    let currentVideo = null;
    let observerTimer = null;
    let nextClickLock = false;

    console.log('[SCU自动连播] 脚本已加载');
    console.log('[SCU自动连播] 当前地址:', location.href);

    // ===============================
    // 后台播放辅助
    // ===============================

    function enableBackgroundPlayback() {
        try {
            Object.defineProperty(document, 'hidden', {
                configurable: true,
                get: () => false
            });

            Object.defineProperty(document, 'visibilityState', {
                configurable: true,
                get: () => 'visible'
            });

            console.log('[SCU自动连播] 后台播放辅助已启用');
        } catch (e) {
            console.log('[SCU自动连播] 无法修改页面可见状态:', e);
        }
    }


    // ===============================
    // 查找视频
    // ===============================

    function findVideo() {
        const videos = Array.from(document.querySelectorAll('video'));

        if (!videos.length) {
            return null;
        }

        // 优先寻找当前可见的视频
        const visibleVideo = videos.find(video => {
            const rect = video.getBoundingClientRect();

            return (
                rect.width > 0 &&
                rect.height > 0
            );
        });

        return visibleVideo || videos[0];
    }


    // ===============================
    // 自动播放
    // ===============================

    async function tryPlay(video) {

        if (!enabled || !video) {
            return;
        }

        if (!video.paused) {
            updateStatus('正在播放');
            return;
        }

        try {

            // 首先尝试正常播放,尽量保留声音
            await video.play();

            console.log('[SCU自动连播] 自动播放成功');

            updateStatus('正在播放');

        } catch (e) {

            console.log(
                '[SCU自动连播] 浏览器阻止有声自动播放,尝试静音播放'
            );

            try {

                video.muted = true;

                await video.play();

                console.log(
                    '[SCU自动连播] 静音自动播放成功'
                );

                updateStatus('播放中(静音)');

            } catch (e2) {

                console.warn(
                    '[SCU自动连播] 自动播放失败,需要手动点击一次播放',
                    e2
                );

                updateStatus('请手动点一次播放');
            }
        }
    }


    // ===============================
    // 查找“下一节”
    // ===============================

    function findNextButton() {

        // 四川大学平台原脚本使用的按钮
        const selectors = [

            '.video_btn.next_video_btn',

            '.next_video_btn',

            'button.next_video_btn',

            '[class*="next_video"]',

            '[title*="下一个"]',

            '[title*="下一节"]',

            '[aria-label*="下一个"]',

            '[aria-label*="下一节"]'

        ];

        for (const selector of selectors) {

            const elements =
                document.querySelectorAll(selector);

            for (const el of elements) {

                if (isUsableButton(el)) {
                    return el;
                }

            }

        }


        // 如果平台修改了 class,
        // 再通过按钮文字寻找
        const candidates =
            document.querySelectorAll(
                'button, a, [role="button"], .el-button'
            );

        for (const el of candidates) {

            const text =
                (el.innerText || el.textContent || '')
                    .trim()
                    .replace(/\s+/g, '');

            if (
                text === '下一个' ||
                text === '下一节' ||
                text.includes('下一个学习内容') ||
                text.includes('下一节')
            ) {

                if (isUsableButton(el)) {
                    return el;
                }

            }

        }

        return null;
    }


    function isUsableButton(el) {

        if (!el) return false;

        const style =
            window.getComputedStyle(el);

        if (
            style.display === 'none' ||
            style.visibility === 'hidden'
        ) {
            return false;
        }

        if (
            el.disabled ||
            el.getAttribute('aria-disabled') === 'true' ||
            el.classList.contains('disabled') ||
            el.classList.contains('is-disabled')
        ) {
            return false;
        }

        return true;
    }


    // ===============================
    // 视频结束
    // ===============================

    function onVideoEnded() {

        if (!enabled || nextClickLock) {
            return;
        }

        console.log(
            '[SCU自动连播] 视频播放结束'
        );

        updateStatus('视频结束,寻找下一节');

        const button =
            findNextButton();

        if (!button) {

            console.log(
                '[SCU自动连播] 没找到下一节按钮'
            );

            updateStatus('未找到下一节');

            return;
        }

        nextClickLock = true;

        console.log(
            '[SCU自动连播] 找到下一节按钮:',
            button
        );

        updateStatus('正在进入下一节');

        setTimeout(() => {

            button.click();

            console.log(
                '[SCU自动连播] 已点击下一节'
            );

            // 防止连续点击
            setTimeout(() => {

                nextClickLock = false;

                scanPage();

            }, 2000);

        }, 800);
    }


    // ===============================
    // 绑定视频
    // ===============================

    function bindVideo(video) {

        if (!video) {
            return;
        }

        if (currentVideo !== video) {

            if (currentVideo) {

                currentVideo.removeEventListener(
                    'ended',
                    onVideoEnded
                );

            }

            currentVideo = video;

            video.addEventListener(
                'ended',
                onVideoEnded
            );

            console.log(
                '[SCU自动连播] 已识别新播放器',
                video
            );
        }

        tryPlay(video);
    }


    // ===============================
    // 页面扫描
    // ===============================

    function scanPage() {

        if (!enabled) {
            updateStatus('已关闭');
            return;
        }

        const video =
            findVideo();

        if (!video) {

            console.log(
                '[SCU自动连播] 当前页面暂未发现 video'
            );

            updateStatus('寻找视频…');

            return;
        }

        bindVideo(video);
    }


    // ===============================
    // 监听页面动态变化
    // ===============================

    function startObserver() {

        const observer =
            new MutationObserver(() => {

                clearTimeout(observerTimer);

                observerTimer =
                    setTimeout(() => {

                        scanPage();

                    }, 700);

            });

        observer.observe(
            document.documentElement,
            {
                childList: true,
                subtree: true
            }
        );
    }


    // ===============================
    // UI
    // ===============================

    let statusText;
    let toggleButton;

    function createUI() {

        const box =
            document.createElement('div');

        box.style.cssText = `
            position: fixed;
            right: 20px;
            bottom: 20px;
            z-index: 2147483647;
            background: rgba(30,30,30,.90);
            color: white;
            padding: 10px 12px;
            border-radius: 8px;
            font-size: 13px;
            font-family: Arial, sans-serif;
            box-shadow: 0 3px 12px rgba(0,0,0,.25);
        `;


        toggleButton =
            document.createElement('button');

        toggleButton.style.cssText = `
            border: none;
            padding: 6px 10px;
            border-radius: 5px;
            cursor: pointer;
            margin-bottom: 5px;
        `;


        statusText =
            document.createElement('div');

        statusText.style.cssText = `
            font-size: 12px;
            opacity: .85;
            min-width: 120px;
        `;


        toggleButton.onclick = () => {

            enabled = !enabled;

            GM_setValue(
                'scu_autoplay_enabled',
                enabled
            );

            updateToggle();

            if (enabled) {
                scanPage();
            } else {
                updateStatus('已关闭');
            }

        };


        box.appendChild(toggleButton);

        box.appendChild(statusText);

        document.body.appendChild(box);

        updateToggle();
    }


    function updateToggle() {

        if (!toggleButton) return;

        if (enabled) {

            toggleButton.innerText =
                'SCU 连播:开';

            toggleButton.style.background =
                '#38a169';

            toggleButton.style.color =
                'white';

        } else {

            toggleButton.innerText =
                'SCU 连播:关';

            toggleButton.style.background =
                '#e53e3e';

            toggleButton.style.color =
                'white';

        }
    }


    function updateStatus(text) {

        if (statusText) {

            statusText.innerText =
                '状态:' + text;

        }
    }


    // ===============================
    // 启动
    // ===============================

    function main() {

        console.log(
            '[SCU自动连播] 初始化'
        );

        createUI();

        enableBackgroundPlayback();

        startObserver();

        // 页面刚打开时多检测几次
        scanPage();

        setTimeout(scanPage, 1000);
        setTimeout(scanPage, 3000);
        setTimeout(scanPage, 6000);

    }


    if (
        document.readyState === 'loading'
    ) {

        document.addEventListener(
            'DOMContentLoaded',
            main,
            { once: true }
        );

    } else {

        main();

    }

})();