北京大学医学部继续教育平台(北大医学堂)——课程自动化脚本 (V5.0 自动刷新-防拦截版)

自动开始|25分钟定时刷新|移除弹窗监测|禁用刷新拦截(beforeunload),实现真·全自动挂机

// ==UserScript==
// @name         北京大学医学部继续教育平台(北大医学堂)——课程自动化脚本 (V5.0 自动刷新-防拦截版)
// @namespace    http://tampermonkey.net/
// @version      5.0
// @description  自动开始|25分钟定时刷新|移除弹窗监测|禁用刷新拦截(beforeunload),实现真·全自动挂机
// @author       BdyyfxkBjmu
// @match        *://*.webtrn.cn/learnspace/learn/learn/templateeight/*
// @grant        none
// @run-at       document-start
// @license      GPL-3.0-or-later
// ==/UserScript==

'use strict';

// 【V5.0 核心】拦截 beforeunload 事件,防止页面刷新时弹窗确认
// 必须在 @run-at document-start 模式下,在页面自身脚本执行前运行
const originalAddEventListener = window.EventTarget.prototype.addEventListener;
window.EventTarget.prototype.addEventListener = function (type, listener, options) {
    if (type === 'beforeunload') {
        console.log('[脚本拦截] 已成功阻止页面的 "beforeunload" 事件,可无提示刷新。');
        return; // 直接返回,不添加该事件监听器
    }
    originalAddEventListener.call(this, type, listener, options);
};
// 同时覆盖 onbeforeunload 属性,双重保险
Object.defineProperty(window, 'onbeforeunload', {
    value: null,
    writable: true,
});


// 主逻辑需要等待DOM加载完成
window.addEventListener('DOMContentLoaded', () => {

    // 全局配置
    const CONFIG = {
        playbackRate: 2.0,
        volume: 0,
        progressThreshold: 0.98,
        checkInterval: 15000,
        playerInitDelay: 3000,
        resumeDelay: 1500,
        retryTimes: 5,
        speedControlRetry: 8,
        // 页面自动刷新时间(分钟)
        refreshIntervalMinutes: 25
    };

    // 全局状态
    let isScriptRunning = true; // 默认启动

    // 确保SweetAlert2可用
    function ensureSwalLoaded() {
        if (typeof Swal === 'undefined') {
            const swalScript = document.createElement('script');
            swalScript.src = 'https://cdn.bootcdn.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.all.js';
            document.head.appendChild(swalScript);
            return false;
        }
        return true;
    }

    // ==================== 主框架页面逻辑 ====================
    function initMainFrame() {
        console.log("[主框架] 脚本初始化 (V5.0 自动刷新-防拦截版)...");

        // 25分钟后自动刷新页面
        if (CONFIG.refreshIntervalMinutes > 0) {
            const refreshTimeMs = CONFIG.refreshIntervalMinutes * 60 * 1000;
            setTimeout(() => {
                console.log(`[自动刷新] ${CONFIG.refreshIntervalMinutes}分钟已到,正在无提示刷新页面...`);
                window.onbeforeunload = null; // 刷新前再次确保拦截已移除
                location.reload();
            }, refreshTimeMs);
            console.log(`[自动刷新] 页面刷新已设定在 ${CONFIG.refreshIntervalMinutes} 分钟后。`);
        }

        // 创建控制面板
        const ctrlPanel = document.createElement('div');
        ctrlPanel.style.cssText = `
            position: fixed; top: 150px; left: 20px; z-index: 99999;
            display: flex; flex-direction: column; gap: 10px;
        `;

        const statusIndicator = document.createElement('div');
        statusIndicator.textContent = '🔄 自动化运行中...';
        statusIndicator.style.cssText = `
            background-color: #2196F3; color: white; padding: 10px 20px;
            border: none; border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            font-size: 14px; font-weight: bold; text-align: center;
        `;

        const toggleBtn = document.createElement('button');
        toggleBtn.textContent = '⏸️ 暂停脚本';
        toggleBtn.style.cssText = `
            background-color: #f44336; color: white; padding: 10px 20px;
            border: none; border-radius: 5px; cursor: pointer;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            font-size: 14px; font-weight: bold;
        `;

        ctrlPanel.appendChild(statusIndicator);
        ctrlPanel.appendChild(toggleBtn);
        document.body.appendChild(ctrlPanel);

        toggleBtn.addEventListener('click', function() {
            isScriptRunning = !isScriptRunning;
            if (isScriptRunning) {
                statusIndicator.textContent = '🔄 自动化运行中...';
                toggleBtn.textContent = '⏸️ 暂停脚本';
                toggleBtn.style.backgroundColor = '#f44336';
                if (ensureSwalLoaded()) Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: '自动化学习已恢复!', showConfirmButton: false, timer: 3000 });
            } else {
                statusIndicator.textContent = '⏹️ 脚本已暂停';
                toggleBtn.textContent = '▶️ 恢复运行';
                toggleBtn.style.backgroundColor = '#4CAF50';
                if (ensureSwalLoaded()) Swal.fire({ toast: true, position: 'top-end', icon: 'info', title: '自动化学习已暂停!', showConfirmButton: false, timer: 3000 });
            }
        });

        function autoStartLearning() {
            if (!isScriptRunning) {
                console.log("[自动开始] 脚本已暂停,不执行自动开始。");
                return;
            }
            console.log("[自动开始] 正在启动自动化流程...");
            const courseFrame = document.getElementById('mainContent');
            if (courseFrame?.contentWindow) {
                setTimeout(() => {
                    console.log("[自动开始] 向课程框架发送 'startAutoPlay' 消息");
                    courseFrame.contentWindow.postMessage({action: 'startAutoPlay'}, '*');
                }, 2000);

                if (ensureSwalLoaded()) {
                    Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: '自动化学习已自动启动!', showConfirmButton: false, timer: 3000 });
                }
            } else {
                console.error('[框架错误] 找不到课程内容框架,自动开始失败。');
            }
        }

        // 初始启动
        autoStartLearning();
    }

    // ==================== 课程列表页面逻辑 ====================
    function initCourseListPage() {
        console.log("[课程列表] 初始化自动化监听...");
        localStorage.setItem('videoAutoNext_isEnd', 'false');

        window.addEventListener('message', function(event) {
            if (event.data.action === 'startAutoPlay') {
                playNextUnfinishedVideo();
            }
        });

        setTimeout(() => {
            const currentPlaying = document.querySelector('.s_point.s_point_cur');
            if (!currentPlaying) {
                 console.log("[课程列表] 未发现当前播放项,主动开始寻找下一个视频...");
                 playNextUnfinishedVideo();
            }
        }, 3000);

        function playNextUnfinishedVideo() {
            console.log("[课程切换] 寻找下一个未完成视频...");
            const lessons = document.querySelectorAll('.s_point');
            for (let lesson of lessons) {
                const isCompleted = lesson.getAttribute('completestate') === '1';
                const isVideo = lesson.getAttribute('itemtype') === 'video';
                if (!isCompleted && isVideo) {
                    console.log(`[课程切换] 找到未播放视频: ${lesson.title}`);
                    lesson.click();
                    return;
                }
            }
            console.log("[进度报告] 所有视频任务已完成");
            if (ensureSwalLoaded()) {
                Swal.fire({ title: '课程完成', text: '所有视频任务点已完成!', icon: 'success', confirmButtonText: '好的' });
            }
        }

        setInterval(function() {
            // 通过isScriptRunning变量来决定是否检查,但此变量在iframe中无法直接访问,
            // 所以依赖主框架的逻辑,这里的暂停功能主要体现在不会自动播放下一个。
            if (localStorage.getItem('videoAutoNext_isEnd') === 'true') {
                console.log("[进度监控] 检测到视频完成,准备下一节");
                localStorage.setItem('videoAutoNext_isEnd', 'false');
                setTimeout(playNextUnfinishedVideo, 2000);
            }
        }, 2000);
    }

    // ==================== 视频播放页面逻辑 ====================
    function initVideoPage() {
        console.log("[播放页面] 初始化播放控制器...");

        let currentPlayer = null;
        let retryCount = 0;

        function setSpeedByDOM(speed) {
            let attempts = 0;
            const maxAttempts = CONFIG.speedControlRetry;
            function trySetSpeed() {
                attempts++;
                const speedElements = document.querySelectorAll('.choose-items-cell[name="speed"]');
                if (speedElements.length > 0) {
                    for (let elem of speedElements) {
                        const speedVal = parseFloat(elem.getAttribute('speedval'));
                        if (Math.abs(speedVal - speed) < 0.01) {
                            elem.querySelector('a').click();
                            console.log(`[速度控制] 已通过DOM设置速度: ${speed}x`);
                            return true;
                        }
                    }
                }
                if (attempts < maxAttempts) setTimeout(trySetSpeed, 1000 * attempts);
                else console.warn(`[速度控制] 无法找到速度控制元素,已尝试${maxAttempts}次`);
                return false;
            }
            return trySetSpeed();
        }

        function detectPlayer() {
            if (currentPlayer?.instance?.play) return currentPlayer;
            currentPlayer = null;
            const playerTypes = [
                { name: 'WhatyMediaPlayer', test: () => typeof WhatyMediaPlayer !== 'undefined', instance: () => WhatyMediaPlayer, methods: { play: (p) => p.play || p.start, setRate: (p) => p.setRate || p.setPlaybackRate, mute: (p) => p.mute || p.setMute } },
                { name: 'AliPlayer', test: () => typeof player !== 'undefined', instance: () => player, methods: { play: (p) => p.play || p.start, setRate: (p) => p.setPlaybackRate, mute: (p) => p.setMute || p.setVolume } },
                { name: 'JWPlayer', test: () => typeof jwplayer === 'function', instance: () => jwplayer(), methods: { play: (p) => p.play, setRate: (p) => p.setPlaybackRate, mute: (p) => p.setMute } },
                { name: 'HTML5 Video', test: () => document.querySelector('video') !== null, instance: () => document.querySelector('video'), methods: { play: (p) => () => p.play(), setRate: (p) => (rate) => { p.playbackRate = rate; }, mute: (p) => (mute) => { p.muted = mute; } } }
            ];
            for (const type of playerTypes) {
                try {
                    if (type.test()) {
                        const instance = type.instance();
                        if (instance) {
                            currentPlayer = { type: type.name.toLowerCase(), instance: instance, play: type.methods.play(instance), setRate: type.methods.setRate(instance), mute: type.methods.mute(instance) };
                            console.log(`[播放器检测] 发现并初始化 ${type.name}`);
                            return currentPlayer;
                        }
                    }
                } catch(e) { console.warn(`[播放器检测] ${type.name}检测失败:`, e); }
            }
            return null;
        }

        function configurePlayer() {
            const player = detectPlayer();
            if (!player) {
                if (retryCount < CONFIG.retryTimes) {
                    retryCount++;
                    setTimeout(configurePlayer, 3000);
                }
                return false;
            }
            try {
                if (player.setRate) player.setRate(CONFIG.playbackRate);
                else if (player.instance?.setPlaybackRate) player.instance.setPlaybackRate(CONFIG.playbackRate);
                setSpeedByDOM(CONFIG.playbackRate);

                if (player.mute) player.mute(true);
                else if (player.instance?.setMute) player.instance.setMute(true);
                else if (player.instance?.setVolume) player.instance.setVolume(CONFIG.volume);

                // 找到播放器就尝试播放
                if(player.play) {
                    player.play();
                }

                return true;
            } catch(e) {
                console.error("[播放设置] 配置失败:", e);
                return false;
            }
        }

        function initPlayer() {
            if (!configurePlayer()) return;
            const progressCheck = setInterval(() => {
                try {
                    const current = document.getElementById('screen_player_time_1')?.textContent;
                    const total = document.getElementById('screen_player_time_2')?.textContent;
                    if (current && total) {
                        const currentSec = timeToSeconds(current);
                        const totalSec = timeToSeconds(total);
                        if (currentSec > 0 && totalSec > 0 && (currentSec / totalSec) >= CONFIG.progressThreshold) {
                            console.log("[进度完成] 视频即将结束");
                            localStorage.setItem('videoAutoNext_isEnd', 'true');
                            window.parent.postMessage({action: 'videoEnded'}, '*');
                            clearInterval(progressCheck);
                        }
                    }
                } catch(e) { console.error("[进度监控] 检测异常:", e); }
            }, CONFIG.checkInterval);
        }

        function timeToSeconds(timeStr) {
            if (!timeStr) return 0;
            const parts = timeStr.split(':').map(Number);
            if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
            if (parts.length === 2) return parts[0] * 60 + parts[1];
            return parts[0] || 0;
        }

        setTimeout(initPlayer, CONFIG.playerInitDelay);
    }

    // ==================== 页面路由 ====================
    const path = window.location.pathname;
    console.log(`[路由] 当前路径: ${path}`);
    if (path.includes('/index.action')) {
        initMainFrame();
    } else if (path.includes('/courseware_index.action')) {
        initCourseListPage();
    } else if (path.includes('/content_video.action')) {
        initVideoPage();
    }

});