Greasy Fork is available in English.
支持自动播放,自动换集、延长登陆时间等功能
// ==UserScript==// @name 湖南开放大学刷课(旧版界面)// @namespace Violentmonkey Scripts// @match *://www.hnsydwpx.cn/center.html*// @match *://www.hnsydwpx.cn/getcourseDetails.html*// @match *://www.hnsydwpx.cn/play.html*// @match *://www.hnsydwpx.cn/template/*// @version 2.2// @author n1nja88888// @description 支持自动播放,自动换集、延长登陆时间等功能// @run-at document-start// @frames// @require https://lib.baomitu.com/axios/0.27.2/axios.min.js// @require https://lib.baomitu.com/crypto-js/3.3.0/crypto-js.min.js// @grant GM_getValue// @grant GM_setValue// @license AGPL License// ==/UserScript==// http://www.hnsydwpx.cn/center.html 课程// http://www.hnsydwpx.cn/play.html 课程概览// http://www.hnsydwpx.cn/getcourseDetails.html 视频'use strict'console.log('n1nja88888 creates this world!')const key = 'easyweb'const checkedKey = 'checked'const accountKey = 'account'const passwordKey = 'password'main()async function main() {// 对于内嵌标签页的处理if (window.top !== window) {await getEleAsync('script[src*="lay-config"]')$.ajaxPrefilter(function(options, originalOptions, jqXHR) {const error = options.erroroptions.error = function(...args) {if (args[0].status != 401) {if ($.isFunction(error))return error.apply(this, args)}}})const temp = unsafeWindow.$Object.defineProperty(unsafeWindow, '$', {get() { return temp },set(val) { }})Object.defineProperty(unsafeWindow.layui, 'jquery', {get() { return temp },set(val) { }})return}const token = JSON.parse(localStorage.getItem(key)).tokenif (!token)returnaxios.defaults.baseURL = 'https://www.hnsydwpx.cn'axios.defaults.headers.common['Authorization'] = 'Bearer ' + JSON.parse(token)// 重写media标签的play函数const _play = HTMLMediaElement.prototype.playHTMLMediaElement.prototype.play = function() {this.muted = true // 默认静音return _play.apply(this)}// 插入时机await getEleAsync('script[src*="public"]')loginPanel()// 重写layui.jsunsafeWindow.layui._use = unsafeWindow.layui.useunsafeWindow.layui.use = (...args) => {if (!!args[0] && Array.isArray(args[0]) && args[0].length === 6&& location.href.includes('www.hnsydwpx.cn/getcourseDetails.html'))returnreturn unsafeWindow.layui._use.apply(unsafeWindow.layui, args)}window.addEventListener('DOMContentLoaded', () => {if (location.href.includes('www.hnsydwpx.cn/center.html'))centerPage()else if (location.href.includes('www.hnsydwpx.cn/play.html'))getEleAsync('.classItem a').then(course => course.click())else if (location.href.includes('www.hnsydwpx.cn/getcourseDetails.html'))videoPage()})}// 获取网页元素function getEleAsync(selector, isCollection = false, context = null) {return new Promise(res => {context = context ? context.document : documentconst ret = get(context)if (ret) {res(ret)return}const observer = new MutationObserver((records, observer) => {const ret = get(context)if (ret) {res(ret)observer.disconnect()}})observer.observe(context, {childList: true,subtree: true})})function get(context) {const ret = isCollection ? context.querySelectorAll(selector) : context.querySelector(selector)if ((!isCollection && !!ret) || (isCollection && ret.length > 0))return retelsereturn null}}// 添加登陆面板function loginPanel() {const isChecked = GM_getValue(checkedKey, '')const css = `<style>.fixed-form {position: fixed;bottom: 20px;right: 20px;width: 400px;background-color: #fff;border: 1px solid #ddd;border-radius: 5px;box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);z-index: 9999;}.form-content {margin-top: 20px;}.form-item {margin-bottom: 20px;}.layui-input {width: 220px;}.layui-form-label {width: 90px;}</style>`const panel = `<div class="fixed-form" id="loginPanel"><div class="form-content layui-container"><form class="layui-form" lay-filter="form"><div class="form-item"><label class="layui-form-label">账号</label><div class="layui-input-block"><input type="text" name="account" placeholder="请输入账号" class="layui-input" autocomplete="on"></div></div><div class="form-item"><label class="layui-form-label">密码</label><div class="layui-input-block"><input type="password" name="password" placeholder="请输入密码" class="layui-input" autocomplete="current-password"></div></div><div class="form-item"><label class="layui-form-label">延长登录</label><div class="layui-input-block"><input type="checkbox" name="switch" lay-skin="switch" lay-text="ON|OFF" id="switch" ${isChecked}></div></div></form></div></div>`$('head').append(css)$('body').append(panel)layui.use('form', () => {const form = layui.formform.render()if (isChecked)isCorrect()// 监听开关按钮的状态改变事件form.on('switch', (data) => {if (!data.elem.checked)GM_setValue(checkedKey, '')else {GM_setValue(checkedKey, 'checked')const account = form.val('form').accountconst password = form.val('form').passwordif (!isCorrect(account))returnif (account == '' || password == '') {print('不能填写为空,已自动关闭延长登录')toggle(false)}else {GM_setValue(accountKey, account)GM_setValue(passwordKey, password)GM_setValue(checkedKey, 'checked')}}})})// 拦截所有401错误$.ajaxPrefilter(function(options, originalOptions, jqXHR) {const error = options.erroroptions.error = function(...args) {if (args[0].status == 401) {if (GM_getValue(checkedKey))extendSession()}else {if ($.isFunction(error))return error.apply(this, args)}}})const temp = unsafeWindow.$Object.defineProperty(unsafeWindow, '$', {get() { return temp },set(val) { }})Object.defineProperty(unsafeWindow.layui, 'jquery', {get() { return temp },set(val) { }})function print(msg) {layer.open({title: '刷课脚本提示',content: msg})}function toggle(isChecked) {if (isChecked)$('#switch').next().addClass('layui-form-onswitch')else$('#switch').next().removeClass('layui-form-onswitch')$('#switch').next().children().eq(0).text(isChecked ? 'ON' : 'OFF')GM_setValue(checkedKey, isChecked ? 'checked' : '')}function extendSession() {if (!isCorrect())returnconst username = GM_getValue(accountKey)const password = CryptoJS.AES.encrypt(GM_getValue(passwordKey), CryptoJS.enc.Latin1.parse(layui.webconfig.cryptoJSKey), {iv: CryptoJS.enc.Latin1.parse(layui.webconfig.cryptoJSKey),mode: CryptoJS.mode.CFB,padding: CryptoJS.pad.NoPadding}).toString()axios.post('/auth/oauth/token?grant_type=password',`username=${username}&password=${password}&scope=server&clientId=trainee`,{headers: {'Authorization': 'Basic dGVzdDp0ZXN0'}}).then(res => {const data = res.datalayui.webconfig.putToken(data.access_token)layui.webconfig.putUser(data.user_info)location.reload()}).catch(err => {print('账号或密码错误,延长登录失败,已自动关闭延长登录功能')toggle(false)})}function isCorrect(account = null) {account = account ? account : GM_getValue(accountKey)const username = JSON.parse(JSON.parse(localStorage.getItem('easyweb')).login_user).usernameif (account == username)return trueelse {print('检测到保存的账号与当前登录账号不符,已自动关闭延长登录功能')toggle(false)return false}}}async function centerPage() {// 获取iframe的上下文const iframe = await getEleAsync('#iframe2')const context = iframe.contentWindow// 判断是否全部学完const ret = await isFinished()if (ret)return// 获取课程列表const list = $(await getEleAsync('#LearnInCompleteArr li', true, context))// 记录要学习的点位 默认从0 即第一个视频开始let index = 0// 获取第一个未学习课let curLesson = list.eq(index)// 检查视频学习进度let text = curLesson.find('.percent').text()// 定位到未学状态的视频while (text === '进度:100%') {curLesson = list.eq(++index)text = curLesson.find('.classItemInfo p').eq(2).text()}curLesson.find('button').click()async function isFinished() {const customerId = JSON.parse(JSON.parse(localStorage.getItem(key)).login_user).idconst res = await axios.get(`/classes/sydwpxxclassescustomercourse/selectChangeNum?customerId=${customerId}`)return res.data.data.noNum <= 0}}async function videoPage() {// 重写发生heartbeat的时间间隔const res = await axios.get('/js/getcourseDetails.js')const data = res.data.replace(/layui\.use/, 'layui._use').replace(/player\.on\(\'pause\'[\s\S]*?player\.on\(\'error\'/, `let curDate = Date.now()player.on('pause', function() {playing = false//======添加的代码const temp = Date.now()const interval = Math.ceil((temp - curDate) / 1e3)curDate = tempplayTime += interval //本次播放时长(不是播放器的时长)submitTime = interval //提交时长(循环清空)//======添加的代码var tmpSubmitTime = submitTime //暂存待提交时长clearInterval(playtimer) //清空定时器var restLen = parseInt(currentDuration) - parseInt(playTime) - parseInt(lastTime)//console.log("pause", restLen, tmpSubmitTime, parseInt(playTime), parseInt(lastTime), parseInt(currentDuration));if (errorFlag) { //错误不做提交//console.log("error pause not submit");errorFlag = false} else {submitTime = 0 //重置提交缓冲时长//console.log("pause ok", learningToken);if (learnStatus != 2) { //没有看完的//没有错误if (tmpSubmitTime > 0) { //待提交时长大于1秒if (restLen < 3) {//视频最后一次提交playTime++ //最后加1秒tmpSubmitTime++ //最后加1秒var slIdx = submitLoading(true)setTimeout(function() {sendheartbeat(chapterId, tmpSubmitTime, learnStatus, function() {$("#" + slIdx).remove() //移除遮罩//console.log('最后提交数据ok');//效验数据 此方法会自动判断标记是否已真实学完handleEnded(player, learnStatus)})}, 2000)} else {sendheartbeat(chapterId, tmpSubmitTime, learnStatus, function() {//回调learningToken = "" //清空token//console.log('数据提交ok');})}} else {learningToken = "" //清空token}} else {//已看完的if (parseInt(player.currentTime) >= parseInt(player.duration)) { //到进度条最后了handleEnded(player, learnStatus)}if (tmpSubmitTime > 1) {sendheartbeat(chapterId, tmpSubmitTime, learnStatus)} else {learningToken = "" //清空token}}}})player.on('play', function() {//console.log('play', learningToken);videoMsk(false) //隐藏缓冲遮罩learningToken = "" //开始播放清空tokensendheartbeat(chapterId, 0) //发送初次请求playtimer = setInterval(function() {// ========添加的代码const temp = Date.now()const interval = Math.ceil((temp - curDate) / 1e3)curDate = tempplayTime += interval //本次播放时长(不是播放器的时长)submitTime = interval //提交时长(循环清空)// ========添加的代码var lookedLen = parseInt(playTime) + parseInt(lastTime)//console.log("计算时长:" + submitTime, "本次播放时长:" + playTime, "已学习时长:" + lookedLen,"当前播放器进度:" + player.currentTime, "当前视频时长:" + currentDuration);//最新版火狐101.0.1 (64 位)出现播放完成不触发结束暂停事件,手动判断是否播放完毕//当前播放大于等于视频时长// 剩余时间var restNodeId = "#shengyu" + jidvar restLen = parseInt(currentDuration) - lookedLenrestLen = restLen < 0 ? 0 : restLenif (learnStatus == 2) {restLen = parseInt(currentDuration) - parseInt(player.currentTime)}$(restNodeId).removeClass("hide")$(restNodeId).addClass("redborder")$(restNodeId).text("剩余" + formatSeconds2(restLen))$(restNodeId).prev().css("width", "55%")//没有学完且播放器计时比定时器快了,将进度条拉回来if (learnStatus != 2 && parseInt(player.currentTime) > lookedLen) {player.currentTime = lookedLen //拉回来//console.log('矫正');}if (isFirefox && !player.ended && !player.paused && parseInt(player.currentTime) >= parseInt(player.duration)) {if (submitTime > 1) {//console.log("firefox 播放结束");player.pause() //交给播放暂停去提交数据}} else {if (learningToken == "") {//网速太慢上次请求还没完成 这种情况发生在网络极端不好的情况下errorFlag = true //标识此变量,后续暂停将不请求心跳player.pause() //暂停player.currentTime = parseInt(playTime) + parseInt(lastTime) //将进度条拖回上一次提交时长playTime = playTime - submitTime //本次播放时长回退//currentPlayTime = player.currentTime;//开启数据提交中遮罩var slIdx = submitLoading(true)//console.log('网络太慢提交数据等待中...', chapterId, submitTime, learnStatus);sendheartbeat(chapterId, submitTime, learnStatus, function() {learningToken = "" //清空token$("#" + slIdx).remove() //移除遮罩//console.log('网络太慢提交数据成功..', chapterId, submitTime, learnStatus);//成功后重新开始播放if (!playing || player.paused) {player.play()}})submitTime = 0} else {//console.log("定时器提交数据:", parseInt(player.currentTime), parseInt(player.duration));if ((parseInt(player.currentTime) + 1) >= parseInt(player.duration)) {handleEnded(player, learnStatus)}sendheartbeat(chapterId, submitTime, learnStatus, function() { }) //提交数据submitTime = 0}}}, 30e3)})// 播放错误时,点击刷新player.on('error'`)new Function(data).apply(unsafeWindow)// 监听是否卡顿,以及是否添加videoconst observer = new MutationObserver(records => {let retry = 10, timer = nullrecords.forEach(record => {const type = record.typeif ('childList' === type) {for (const node of record.addedNodes.values()) {if ('VIDEO' === node.tagName)videoOps(record)}}else if ('attribute' === type)refreshOps(record)})function videoOps(record) {const exit = $('.list-header').find('button')const video = $('#studyVideo video')[0]video.addEventListener('play', async () => {const videoItems = await getEleAsync('.item-list', true)if (!video.muted)video.muted = true// 找到第一个未看完的元素,且不是当前播放的元素const videoItem = $(videoItems).find('.item-list-progress:not(:contains("100%"))').first().parent()if (videoItem.length === 0)exit.click()if (!videoItem.hasClass('item-list-redClass'))videoItem.click()})video.addEventListener('pause', async function() {const videoItems = await getEleAsync('.item-list', true)if ($('.item-list-redClass').index('.item-list') === videoItems.length - 1) {isFinished().then(res => {if (res)exit.click()})}// 停顿5s再播放,因为原视频页存在卡顿时也会停止播放,要留给原视频页对视频卡顿进行处理setTimeout(() => video.play(), 5e3)})video.addEventListener('canplay', e => e.play())}async function refreshOps(record) {const refresh = await getEleAsync('.xgplayer-error-refresh')if (record.target.classList.contains('xgplayer-is-error')) {if (!timer) {if (retry-- <= 0)location.reload()timer = setTimeout(() => {refresh.click()timer = null}, 1.5e3)}}}})observer.observe($('#studyVideo')[0], {attributes: true,attributeFilter: ['class'],childList: true})// 每1.5min检查一次是否卡住,因为heartbeat被修改为30s发送一次// 记录上次进度let lastProgress = $('.item-list-redClass .item-list-progress').text()setInterval(() => {const curProgress = $('.item-list-redClass .item-list-progress').text()if (lastProgress === curProgress)location.reload()lastProgress = curProgress}, 3 * 30e3)async function isFinished() {const playInfo = JSON.parse(localStorage.getItem(key)).playinfoconst res = await axios.post('/learning/coursenode/getCourseNodeProgressRedis', {chapterId: playInfo.chapterId,classesId: playInfo.classesId,courseId: playInfo.courseid,nodeId: playInfo.jid})return parseInt(res.data.data.demandLength) <= parseInt(res.data.data.learnLength)}}