Q学友刷课

Q学友刷课脚本,人脸识别后,点击刷课即可开始

// ==UserScript==
// @name         Q学友刷课
// @namespace    https://www.qxueyou.com/
// @version      2024-01-15
// @description  Q学友刷课脚本,人脸识别后,点击刷课即可开始
// @author       Kane Simmons
// @match        https://www.qxueyou.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @license MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    let interval = null // interval定时器
    let timeout = null // setTimeout定时器
    const courses = new Map(); // 课程Map {courseDom: 课程dom, rateDom: 课程进度条dom, rate: 课程完成度, playTimes: 播放次数}
    let runningTime = 0 // 运行时长(s)
    let processing = false // 正在答题
    let currentPlayingIndex = 0 // 当前播放的课程索引
  
    /** 更新课程进度 */
    const updateCourseRate = (isInit = false) => {
        const courseDoms = document.getElementsByClassName('m-slide slipDel')
        if (isInit) {
            // 初始化课程进度
            for (let i = 0; i < courseDoms.length; i++) {
                const courseDom = courseDoms[i].querySelector('.v-list-item--link');
                const rateDom = courseDoms[i].getElementsByClassName('progress col col-3')[0]
                const rate = parseInt(rateDom.querySelector('div').getAttribute('aria-valuenow'), 10);
                courses.set(i, { index: i + 1, courseDom, rateDom, rate, playTimes: 0 });
            }
            return;
        }
        // 更新进度到courses的map中
        const rateDoms = Array.from(courses.values()).map(item => item.rateDom)
        for (let i = 0; i < rateDoms.length; i++) {
            const rate = parseInt(rateDoms[i].querySelector('div').getAttribute('aria-valuenow'), 10);
            courses.get(i).rate = rate;
        }
    }
  
    /** 更新当前播放课程索引 */
    const updateCurrentPlayingIndex = () => {
        const courseDoms = Array.from(courses.values()).map(item => item.courseDom)
        currentPlayingIndex = courseDoms.findIndex(item => item.classList.contains('grey'))
    }
  
    /** 检测视频是否异常,并切换到未完成的科目 */
    const checkVideoStatus = () => {
        // 检测视频是否异常
        const isLoadingShow = window.getComputedStyle(document.querySelector('.vjs-loading-spinner')).getPropertyValue('display') === 'block'
        if (isLoadingShow) {
            console.warn(`当前视频(第${currentPlayingIndex + 1}集)异常`)
            switchToUnwatchCourse()
        }
        // 检测当前播放课程是否完成
        if (courses.get(currentPlayingIndex).rate === 100) {
            console.log(`第${currentPlayingIndex + 1}集课程已完成`)
            switchToUnwatchCourse()
        }
    }
  
    /** 切换到未完成的科目 */
    const switchToUnwatchCourse = () => {
        let minPlays = Infinity;
        let minPlayIndex = -1;
        let arePlayTimesEqual = true;
        let firstRateUnder100Index = -1;
  
        // 找到 rate < 100 的课程中 playTimes 最少的课程
        courses.forEach((course, index) => {
            if (course.rate < 100) {
                if (firstRateUnder100Index === -1) {
                    firstRateUnder100Index = index;
                }
  
                if (course.playTimes < minPlays) {
                    minPlays = course.playTimes;
                    minPlayIndex = index;
                    arePlayTimesEqual = false;
                } else if (course.playTimes > minPlays) {
                    arePlayTimesEqual = false;
                }
            }
        });
  
        // 如果所有 rate < 100 的课程的 playTimes 都相等,则找到第一个 rate < 100 的课程
        if (arePlayTimesEqual && firstRateUnder100Index !== -1) {
            minPlayIndex = firstRateUnder100Index;
        }
  
        // 点击选定的课程并更新 playTimes
        if (minPlayIndex !== -1) {
            // 如果只剩一项 rate < 100 的课程,则先点击别的项目
            let RateUnder100Subjects = []
            RateUnder100Subjects = Array.from(courses.values()).filter(item => item.rate < 100)
            if (RateUnder100Subjects.length === 1) {
                const courseDoms = Array.from(courses.values()).map(item => item.courseDom)
                let otherIndex = minPlayIndex + 1 >= courseDoms.length ? 0 : minPlayIndex + 1;
                courseDoms[otherIndex].click();
                console.log(`只剩一项,先点击第${otherIndex}集`)
            }
  
            const selectedCourse = courses.get(minPlayIndex);
            selectedCourse.courseDom.click();
            currentPlayingIndex = minPlayIndex;
            selectedCourse.playTimes++;
  
            // 输出播放次数和切换信息
            console.table(Array.from(courses.values()).map(({ index, rate, playTimes }) => ({ '课程序号': index, '课程完成度(%)': rate, '播放次数': playTimes })));
            console.log(`已切换至第${selectedCourse.index}集`)
  
        }
    }
  
    /** 检查问题弹窗是否出现,出现则回答问题 */
    const checkQuestion = () => {
        const modelDom = document.querySelector('.overlay')
        if (!modelDom || processing) return // 未发现问题弹窗或正在处理不继续
  
        console.log('发现问题弹窗!');
        answerQuestions()
    }
  
    /** 回答弹窗的问题 */
    const answerQuestions = () => {
        processing = true
        const modelDom = document.querySelector('.overlay')
        const bodyDoms = modelDom.querySelectorAll('div')
        let randomTime = (Math.floor(Math.random() * 60) + 1); // 生成 60s 的随机数
        if(bodyDoms.length) {
            const questionDom = bodyDoms[1]
            const questionResult = eval(questionDom.innerText.split('=')[0])
            console.log(`答案是:${questionResult}, 将在${randomTime}s后选择答案`)
            timeout = setTimeout(() => {
                const answersDoms = bodyDoms[5].querySelectorAll('.v-radio')
                answersDoms.forEach((item) => {
                    const answer = item.innerText.split('.')[1] * 1
                    const answerDom = item.querySelector('.v-label')
                    if (answer === questionResult) {
                        answerDom && answerDom.click()
                        const confirmDom = document.getElementsByClassName('white--text mb-2 v-btn v-btn--block v-btn--contained theme--light v-size--small')[0]
                        confirmDom && confirmDom.click()
                        processing = false
                        console.log('已选择正确答案');
                    }
                })
            }, randomTime)
        }
    }
  
    /** 检查是否全部课程已完成 */
    const checkAllCompleted = () => {
        const isFinished = Array.from(courses.values()).findIndex(item => item.rate < 100) === -1
        if (isFinished) {
            console.log(`%c全部课程已完成!总用时:${runningTime / 60 / 60}小时`, 'color: green; font-weight: bold;');
            alert(`全部课程已完成!总用时:${(runningTime / 60 / 60).toFixed(2)}小时`)
            clearInterval(interval)
            clearTimeout(timeout)
        } else {
            console.log('正在运行...')
            const startBtn = document.querySelector('#startBtn')
            const restRate = Array.from(courses.values()).reduce((acc, item) => acc + item.rate, 0) / 8
            startBtn.innerText = `刷课中,已运行${runningTime / 60}分钟,已完成${restRate}%`;
            if (restRate > 95) {
              // 整体进度大于95%时,点击学习进度按钮,通过网络请求获取真正进度
              const updateRateDom = document.getElementsByClassName('m-slide__del m-slide__del-red')[0]
              updateRateDom && updateRateDom.click()
              const checkModel = setInterval(() => {
                  const confirmDom = document.getElementsByClassName('v-btn v-btn--contained theme--light v-size--default primary')[0]
                  if (confirmDom) {
                    confirmDom.click()
                    clearInterval(checkModel)
                  }
              }, 100)
            }
        }
    }
  
    /** 每秒执行 */
    const mainProcess = () => {
        interval = setInterval(() => {
            try {
  
                runningTime++
  
                // 每分钟
                if (runningTime % 60 ===  0) {
                    // 更新课程进度
                    updateCourseRate()
                    // 每分钟都检查下视频是否异常
                    checkVideoStatus()
                    // 检查是否全部课程已完成
                    checkAllCompleted()
                }
  
                // 每45分钟
                if ((runningTime % (60 * 45) === 0) && !processing) {
                    console.log('每45分钟切换一次课程')
                    switchToUnwatchCourse()
                }
  
                // 更新当前播放课程索引
                updateCurrentPlayingIndex()
                // 检查问题弹窗
                checkQuestion()
  
            } catch (e) {
                console.error('程序报错了!!已停止检查弹窗', e)
                clearInterval(interval)
                clearTimeout(timeout)
            }
        }, 1000)
    }
  
    /** 生成开始按钮 */
    const domInit = () => {
        // 创建一个按钮元素
        var button = document.createElement("button");
  
        // 设置按钮的文本内容
        button.innerText = "开始刷课";
  
        // 设置按钮的 ID
        button.id = "startBtn"; // 添加 ID
  
        // 设置按钮的点击事件处理程序
        function clickHandler() {
            startProgram()
            button.innerText = "刷课中";
            button.style.backgroundColor = "#409eff";
            button.style.borderColor = "#409eff";
            // 移除按钮的点击事件处理程序
            button.removeEventListener("click", clickHandler);
        };
  
        // 添加按钮的点击事件处理程序
        button.addEventListener("click", clickHandler);
  
  
        // 设置按钮的样式
        button.style.cssText = "position: fixed; bottom: 10%; left: 50%; transform: translateX(-50%); padding: 12px 23px;color: #fff; background-color: #67c23a; border-color: #67c23a;";
  
        // 将按钮直接添加到 body 元素中
        document.body.appendChild(button);
    }
  
    /** 启动程序 */
    const startProgram = () => {
  
        mainProcess()
  
        updateCourseRate(true)
  
        checkAllCompleted()
  
        switchToUnwatchCourse()
    }
  
    domInit()
  })();