Fujian Cadre Network College Course Auto Player

Auto play Fujian Cadre Network College course videos, auto switch chapters and subjects, auto save progress, auto handle token

As of 2024-12-17. See the latest version.

// ==UserScript==
// @name         福建网络干部学院课程自动播放
// @name:en      Fujian Cadre Network College Course Auto Player
// @namespace    https://greasyforks.org/users/1410751-liuxing7954
// @version      1.0.0
// @description  自动播放福建网络干部学院课程视频,支持自动切换章节和科目,自动保存进度,自动处理token
// @description:en  Auto play Fujian Cadre Network College course videos, auto switch chapters and subjects, auto save progress, auto handle token
// @author       liuxing7954
// @match        *://www.fsa.gov.cn/video*
// @match        https://www.fsa.gov.cn/video*
// @match        *://www.fsa.gov.cn/videoChoose*
// @match        https://www.fsa.gov.cn/videoChoose*
// @license      MIT
// @icon         https://www.fsa.gov.cn/favicon.ico
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 调试模式配置
    let DEBUG = localStorage.getItem('debugMode') === 'true' || false; // 从localStorage读取调试状态
    const DEBUG_INTERVAL = 5000; // 调试模式下的循环间隔(5秒)
    const NORMAL_INTERVAL = 20000; // 正常模式下的循环间隔(20秒)
    const TARGET_PROGRESS = 100; // 目标进度值

    // 添加初始化标记和上次进度记录
    let isFirstCheck = true;
    let lastProgress = '';

    // 全局变量
    let currentCourseMode = localStorage.getItem('courseMode') || 'elective';

    // 从localStorage获取studentId
    function getStudentId() {
        const studentId = localStorage.getItem('studentId');
        if (DEBUG) {
            log(`获取到studentId: ${studentId}`, true);
        }
        return studentId;
    }

    // 创建模拟控制台
    function createConsole() {
        const consoleDiv = document.createElement('div');
        consoleDiv.id = 'custom-console';
        consoleDiv.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 450px;
            height: 300px;
            background: rgba(0, 0, 0, 0.8);
            color: #fff;
            padding: 10px;
            border-radius: 5px;
            font-family: monospace;
            font-size: 12px;
            overflow-y: auto;
            z-index: 9999;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        `;
        document.body.appendChild(consoleDiv);
        return consoleDiv;
    }

    // 自定义日志函数
    function log(message, isDebugMsg = false) {
        if (isDebugMsg && !DEBUG) return;

        const consoleDiv = document.getElementById('custom-console');
        const logLine = document.createElement('div');
        logLine.style.borderBottom = '1px solid rgba(255,255,255,0.1)';
        logLine.style.padding = '3px 0';

        if (isDebugMsg) {
            logLine.style.color = '#aaffaa';
            message = '[DEBUG] ' + message;
        }

        logLine.textContent = `${new Date().toLocaleTimeString()} - ${message}`;
        consoleDiv.appendChild(logLine);
        consoleDiv.scrollTop = consoleDiv.scrollHeight;

        const maxLines = DEBUG ? 20 : 10;
        while (consoleDiv.children.length > maxLines) {
            consoleDiv.removeChild(consoleDiv.firstChild);
        }

        if (isDebugMsg) {
            console.debug(message);
        } else {
            console.log(message);
        }
    }

    // 创建初始提示
    function showInitialNotification() {
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: #4CAF50;
            color: white;
            padding: 15px;
            border-radius: 5px;
            z-index: 9999;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        `;
        notification.textContent = "脚本已启动";
        document.body.appendChild(notification);

        setTimeout(() => {
            notification.remove();
        }, 2000);
    }

    // 获取选修课程列表
    async function fetchCourseList() {
        const studentId = getStudentId();
        const token = localStorage.getItem('wlxytk');
        const refreshToken = localStorage.getItem('rt');

        if (!studentId || !token || !refreshToken) {
            log('未找到必要信息,无法获取课程列表');
            if (DEBUG) {
                log(`studentId: ${studentId}`, true);
                log(`token: ${token}`, true);
                log(`refreshToken: ${refreshToken}`, true);
            }
            return null;
        }

        const url = getApiUrl();
        const requestBody = {
            "studentId": studentId,
            "size": 30,
            "current": 1
        };

        try {
            if (DEBUG) {
                log('正在请求课程列表...', true);
                log(`请求体: ${JSON.stringify(requestBody)}`, true);
                log(`使用token: ${token}`, true);
                log(`使用refreshToken: ${refreshToken}`, true);
            }

            const response = await fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': token,
                    'RefreshAuthorization': refreshToken
                },
                body: JSON.stringify(requestBody)
            });

            // 检查响应头中的新token
            if (response.status === 200) {
                const newToken = response.headers.get('authorization');
                const newRefreshToken = response.headers.get('refre');

                if (newToken) {
                    localStorage.setItem('wlxytk', newToken);
                    if (DEBUG) {
                        log(`更新token: ${newToken}`, true);
                    }
                }

                if (newRefreshToken) {
                    localStorage.setItem('rt', newRefreshToken);
                    if (DEBUG) {
                        log(`更新refreshToken: ${newRefreshToken}`, true);
                    }
                }
            }

            if (!response.ok) {
                log(`请求失败: ${response.status} ${response.statusText}`);
                return null;
            }

            const data = await response.json();

            if (DEBUG) {
                log(`获取���课程列表响应: ${JSON.stringify(data)}`, true);
            }
            await new Promise(resolve => setTimeout(resolve, 10000));
            return data;
        } catch (error) {
            log(`获取课程列表失败: ${error.message}`);
            return null;
        }
    }

    // 查找下一个未完成的科目
    async function findNextUnfinishedSubject() {
        const courseList = await fetchCourseList();
        if (!courseList || !courseList.data || !courseList.data.records) {
            log('获取课程列表失败或列表为空');
            return null;
        }

        if (DEBUG) {
            log(`开始查找未完成科目,共有 ${courseList.data.records.length} 个科目`, true);
        }

        // 查找第一个未完成的科目
        const nextSubject = courseList.data.records.find(course => {
            const isCompleted = course.creditsEarnedNew === course.credit;
            if (DEBUG) {
                log(`科目: ${course.courseware.coursewareName} - 已学时数: ${course.creditsEarnedNew}/${course.credit} - ${isCompleted ? '已完成' : '未完成'}`, true);
            }
            return !isCompleted;
        });

        return nextSubject;
    }

    // 跳转到指定科目
    function navigateToSubject(courseware) {
        const url = `https://www.fsa.gov.cn/videoChoose?newId=${courseware.id}`;
        if (DEBUG) {
            log(`准备跳转到新科目,URL: ${url}`, true);
            log(`科目信息: ${JSON.stringify(courseware)}`, true);
        }
        window.location.href = url;
    }

    // 修改原有的switchToNextSubject函数
    async function switchToNextSubject() {
        log('所有课程已完成,正在查找下一个未完成的科目...');

        const nextSubject = await findNextUnfinishedSubject();
        if (nextSubject) {
            log(`找到下一个未完成的科目: ${nextSubject.courseware.coursewareName}`);
            log(`当前进度: ${nextSubject.creditsEarnedNew}/${nextSubject.credit}`);
            navigateToSubject(nextSubject.courseware);
        } else {
            log('恭喜!所有科目都已完成!');
        }
    }

    function getCurrentCourseInfo() {
        const currentCourse = document.querySelector('.kc-list li h5[style*="color: rgb(166, 0, 0)"]');
        if (!currentCourse) {
            log('未找到当前课程');
            return null;
        }

        const progressSpan = currentCourse.parentElement.querySelector('.kc-info span:last-child');
        const progressText = progressSpan ? progressSpan.textContent : '进度:0.0%';
        const progress = parseFloat(progressText.replace('进度:', ''));

        if (DEBUG) {
            log(`当前课程状态检查 - 标题: ${currentCourse.textContent.trim()} - 进度: ${progressText}`, true);
        }

        if (isFirstCheck) {
            log('------------------------');
            log('当前课程: ' + currentCourse.textContent.trim());
            log('------------------------');
            isFirstCheck = false;
        }

        if (progressText !== lastProgress) {
            log('当前进度: ' + progressText);
            lastProgress = progressText;
        }

        return {
            currentCourse: currentCourse.parentElement,
            progress: progress
        };
    }

    function saveProgress() {
        const saveButton = document.querySelector('button.el-button.btn span:first-child').closest('button');
        if (saveButton) {
            if (DEBUG) {
                log('正在点击保存进度按钮...', true);
            }
            saveButton.click();
            log('自动保存进度');
        } else {
            log('未找到保存进度按钮');
        }
    }

    // 检查是否所有章节都已完成
    function areAllChaptersCompleted() {
        const chapters = document.querySelectorAll('.kc-list li');
        if (DEBUG) {
            log(`检查所有章节状态,共找到 ${chapters.length} 个章节`, true);
        }

        for (const chapter of chapters) {
            const progressSpan = chapter.querySelector('.kc-info span:last-child');
            const progressText = progressSpan ? progressSpan.textContent : '进度:0.0%';
            const progress = parseFloat(progressText.replace('进度:', ''));

            if (DEBUG) {
                const chapterTitle = chapter.querySelector('h5').textContent.trim();
                log(`章节: ${chapterTitle} - 进度: ${progressText}`, true);
            }

            if (progress < TARGET_PROGRESS) {
                return false;
            }
        }

        return true;
    }

    // 获取视频播放器
    function getPlayer() {
        const video = document.querySelector('video');
        if (!video) {
            if (DEBUG) {
                log('未找到视频元素', true);
            }
            return null;
        }
        return video;
    }

    // 等待视频元素加载
    function waitForVideo(maxAttempts = 10) {
        return new Promise((resolve) => {
            let attempts = 0;

            const checkVideo = () => {
                const video = getPlayer();
                if (video) {
                    if (DEBUG) {
                        log('视频元素已就绪', true);
                    }
                    resolve(video);
                    return;
                }

                attempts++;
                if (attempts >= maxAttempts) {
                    if (DEBUG) {
                        log(`视频加载超时,已尝试 ${attempts} 次`, true);
                    }
                    resolve(null);
                    return;
                }

                if (DEBUG) {
                    log(`等待视频加载,第 ${attempts} 次尝试...`, true);
                }
                setTimeout(checkVideo, 1000);
            };

            checkVideo();
        });
    }

    // 播放视频
    async function playVideo() {
        const video = await waitForVideo();
        if (!video) return false;

        try {
            if (video.paused) {
                // 设置为静音并保持静音状态
                video.muted = true;
                await video.play();

                if (DEBUG) {
                    log('视频已开始播放(音模式)', true);
                }
                return true;
            }
        } catch (error) {
            if (DEBUG) {
                log(`播放视频出错: ${error.message}`, true);
            }
        }
        return false;
    }

    // ���停视频
    function pauseVideo() {
        const video = getPlayer();
        if (!video) return false;

        if (DEBUG) {
            log(`当前视频状态: ${video.paused ? '已暂停' : '播放中'}`, true);
        }

        if (!video.paused) {
            video.pause();
            log('暂停播放视频');
            return true;
        }
        return false;
    }

    // 切换播放状态
    function toggleVideo() {
        const video = getPlayer();
        if (!video) return false;

        if (video.paused) {
            return playVideo();
        } else {
            return pauseVideo();
        }
    }

    // 获取当前播放状态
    function getVideoStatus() {
        const video = getPlayer();
        if (!video) return null;

        return {
            currentTime: video.currentTime,
            duration: video.duration,
            paused: video.paused,
            playbackRate: video.playbackRate,
            volume: video.volume
        };
    }

    async function mainLoop() {
        if (DEBUG) {
            log('开始新一轮检查...', true);
        }

        // 先保存进度
        saveProgress();

        console.log(getVideoStatus());
        if(getVideoStatus().paused == true){
            playVideo();
        }

        // 2秒检查进度
        setTimeout(async () => {
            try {
                const currentInfo = getCurrentCourseInfo();
                if (!currentInfo) return;

                if (currentInfo.progress >= TARGET_PROGRESS) {
                    if (DEBUG) {
                        log('当前章节完成,检查其他章节...', true);
                    }

                    // 检查是否所有章节都完成
                    if (areAllChaptersCompleted()) {
                        log('所有章节已完成,准备切换科目...');
                        await switchToNextSubject();
                    } else {
                        // 查找下一个未完成的章节
                        const chapters = document.querySelectorAll('.kc-list li');
                        for (const chapter of chapters) {
                            const progressSpan = chapter.querySelector('.kc-info span:last-child');
                            const progressText = progressSpan ? progressSpan.textContent : '进度:0.0%';
                            const progress = parseFloat(progressText.replace('进度:', ''));

                            if (progress < TARGET_PROGRESS) {
                                const chapterTitle = chapter.querySelector('h5').textContent.trim();
                                log('------------------------');
                                log('准备切换到章节: ' + chapterTitle);
                                log('当前进度: ' + progressText);
                                log('------------------------');

                                chapter.querySelector('h5').click();
                                isFirstCheck = true;
                                break;
                            }
                        }
                    }
                }
            } catch (error) {
                log(`处理课程切换时发生错误: ${error.message}`);
            }
        }, 2000);
    }

    // 视频播放页面的主要逻辑
    function handleVideoPage() {
        if (DEBUG) {
            log('初始化视频播放页面逻辑...', true);
        }

        // 5秒后开始第一次执行
        setTimeout(() => {
            getCurrentCourseInfo(); // 显示初始课程信息


            mainLoop(); // 开始一次循环

            // 根据调试模式设置不同的循环间隔
            const interval = DEBUG ? DEBUG_INTERVAL : NORMAL_INTERVAL;
            setInterval(mainLoop, interval);

        }, 5000);
    }

    // 课程选择页面的主要逻辑
    function handleVideoChoosePage() {
        if (DEBUG) {
            log('初始化课程选择页面逻辑...', true);
        }

        // 等待页面元素加载完成
        setTimeout(() => {
            // 使用精确的选择器查找第一条课程
            const firstCourse = document.querySelector('#app > div.video-page-main > div.page-content > div > div.choose-body > div:nth-child(2) > span.choose-content');
            if (firstCourse) {
                if (DEBUG) {
                    log(`找到第一条课程: ${firstCourse.textContent}`, true);
                    log(`课程元素: ${firstCourse.outerHTML}`, true);
                }
                // 点击课程
                firstCourse.click();
                log('已选择第一条课程');

                // 等待5秒后刷新页面
                if (DEBUG) {
                    log('5秒后将刷新页面...', true);
                }
                setTimeout(() => {
                    window.location.reload();
                }, 5000);
            } else {
                log('未找到课程选项');
                if (DEBUG) {
                    // 输出页面结构以便调试
                    const pageContent = document.querySelector('.page-content');
                    log(`页面内容: ${pageContent ? pageContent.innerHTML : '未找到page-content'}`, true);
                }
            }
        }, 1000);
    }

    // 根据页面URL执行不同的逻辑
    function initializeBasedOnURL() {
        const currentURL = window.location.href;

        // 创建模拟控制台
        createConsole();
        // 创建配置面板
        createConfigPanel();
        // 显示初始提示
        showInitialNotification();

        if (DEBUG) {
            log(`当前页面URL: ${currentURL}`, true);
        }

        if (currentURL.includes('/video?')) {
            if (DEBUG) {
                log('检测到视频播放页面', true);
            }
            handleVideoPage();
        } else if (currentURL.includes('/videoChoose?')) {
            if (DEBUG) {
                log('检测到课程选择页面', true);
            }
            handleVideoChoosePage();
        } else {
            log('未知页面类型');
        }
    }

    // 等待页面加载完成后初始化
    window.addEventListener('load', initializeBasedOnURL);

    // 添加配置面板
    function createConfigPanel() {
        const panel = document.createElement('div');
        panel.innerHTML = `
            <div id="config-panel" style="
                position: fixed;
                top: 50%;
                right: 20px;
                transform: translateY(-50%);
                z-index: 9999;
                background: white;
                padding: 15px;
                border-radius: 5px;
                box-shadow: 0 2px 10px rgba(0,0,0,0.1);
                font-size: 14px;
            ">
                <div style="margin-bottom:15px;font-weight:bold;border-bottom:1px solid #eee;padding-bottom:5px;">
                    脚本设置
                </div>
                <div style="margin-bottom:15px;">
                    <label style="display:flex;align-items:center;cursor:pointer;">
                        <span id="debugIcon" style="margin-right:8px;width:20px;text-align:center;">
                            ${DEBUG ? '✅' : '❌'}
                        </span>
                        <input
                            type="checkbox"
                            id="debugMode"
                            ${DEBUG ? 'checked' : ''}
                            style="margin-right:8px;"
                        >
                        开启调试日志
                    </label>
                </div>
                <div style="margin-bottom:10px;">
                    <div style="margin-bottom:5px;">课程模式:</div>
                    <select
                        id="courseMode"
                        style="width:100%;padding:5px;border-radius:3px;border:1px solid #ddd;"
                    >
                        <option value="elective" ${currentCourseMode === 'elective' ? 'selected' : ''}>选修课程</option>
                        <option value="required" ${currentCourseMode === 'required' ? 'selected' : ''}>必修课程</option>
                    </select>
                </div>
            </div>
        `;
        document.body.appendChild(panel);

        // 修改调试模式事件监听
        document.getElementById('debugMode').addEventListener('change', e => {
            DEBUG = e.target.checked;
            localStorage.setItem('debugMode', DEBUG);
            document.getElementById('debugIcon').textContent = DEBUG ? '✅' : '❌';
            log(`调试模式已${DEBUG ? '开启' : '关闭'}`);
        });

        // 课程模式事件监听保持不变
        document.getElementById('courseMode').addEventListener('change', e => {
            currentCourseMode = e.target.value;
            localStorage.setItem('courseMode', currentCourseMode);
            log(`已切换到${currentCourseMode === 'required' ? '必修' : '选修'}模式`);
        });
    }

    // 修改 fetchCourseList 函数中的 URL 获取
    function getApiUrl() {
        const urls = {
            elective: 'https://www.fsa.gov.cn/api/study/my/elective/myElectives',
            required: 'https://www.fsa.gov.cn/api/study/years/yearsCourseware/annualPortalCourseList'
        };
        return urls[currentCourseMode];
    }
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。