您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支持自动播放,自动换集、延长登陆时间等功能
// ==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.error options.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)).token if (!token) return axios.defaults.baseURL = 'https://www.hnsydwpx.cn' axios.defaults.headers.common['Authorization'] = 'Bearer ' + JSON.parse(token) // 重写media标签的play函数 const _play = HTMLMediaElement.prototype.play HTMLMediaElement.prototype.play = function() { this.muted = true // 默认静音 return _play.apply(this) } // 插入时机 await getEleAsync('script[src*="public"]') loginPanel() // 重写layui.js unsafeWindow.layui._use = unsafeWindow.layui.use unsafeWindow.layui.use = (...args) => { if (!!args[0] && Array.isArray(args[0]) && args[0].length === 6 && location.href.includes('www.hnsydwpx.cn/getcourseDetails.html')) return return 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 : document const 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 ret else return 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.form form.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').account const password = form.val('form').password if (!isCorrect(account)) return if (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.error options.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()) return const 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.data layui.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).username if (account == username) return true else { 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).id const 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 = temp playTime += 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 = "" //开始播放清空token sendheartbeat(chapterId, 0) //发送初次请求 playtimer = setInterval(function() { // ========添加的代码 const temp = Date.now() const interval = Math.ceil((temp - curDate) / 1e3) curDate = temp playTime += interval //本次播放时长(不是播放器的时长) submitTime = interval //提交时长(循环清空) // ========添加的代码 var lookedLen = parseInt(playTime) + parseInt(lastTime) //console.log("计算时长:" + submitTime, "本次播放时长:" + playTime, "已学习时长:" + lookedLen,"当前播放器进度:" + player.currentTime, "当前视频时长:" + currentDuration); //最新版火狐101.0.1 (64 位)出现播放完成不触发结束暂停事件,手动判断是否播放完毕 //当前播放大于等于视频时长 // 剩余时间 var restNodeId = "#shengyu" + jid var restLen = parseInt(currentDuration) - lookedLen restLen = restLen < 0 ? 0 : restLen if (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) // 监听是否卡顿,以及是否添加video const observer = new MutationObserver(records => { let retry = 10, timer = null records.forEach(record => { const type = record.type if ('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)).playinfo const 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) } }