// ==UserScript==
// @name 华师网络教育学习助手
// @version 0.0.5
// @namespace http://tampermonkey.net/
// @description 华师网络教育学习助手,自动学习、获取题库
// @author 4Ark
// @match *https://gdou.scnu.edu.cn/learnspace/learn/learn/blue/index.action*
// @match *https://scnu-exam.webtrn.cn/platformwebapi/student/exam/studentExam_queryExamInfo.action*
// @match *https://gdou.scnu.edu.cn/learnspace/learn/learn/blue/exam_checkPaperToexam.action*
// @match *https://scnu-exam.webtrn.cn/student/exam/studentExam_studentInfo.action*
// @match *https://scnu-exam.webtrn.cn/exam/examflow_index.action*
// @run-at document-end
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_info
// @grant GM_setClipboard
// @antifeature ads
// @license MIT
// ==/UserScript==
;(function () {
'use strict'
const DEFAULT_AUTO_GET_EXAM_COUNT = 15
const PLAY_SPEED_STRATEGY = {
25: 16,
15: 12,
10: 8,
3: 4,
2: 2,
1: 1
}
const EXAM_IDS_KEY = 'huashi-exam-ids'
const QUESTION_TYPE_SORT = {
单项选择题: 1,
多项选择题: 2,
判断题: 3,
填空题: 4
}
const UTILS_TYPE = {
LEARN: '自动学习',
GET_EXAM: '获取题库'
}
let util_type = UTILS_TYPE.LEARN
const CHAPTER_TYPE = {
VIDEO: 'video',
TEXT: 'text'
}
const GET_EXAM_BASE_URL =
'https://scnu-exam.webtrn.cn/platformwebapi/student/exam/studentExam_queryExamInfo.action'
const GET_EXAM_BASE_URL_2 =
'https://gdou.scnu.edu.cn/learnspace/learn/learn/blue/exam_checkPaperToexam.action'
const SURE_EXAM_BASE_URL =
'https://scnu-exam.webtrn.cn/student/exam/studentExam_studentInfo.action'
const EXAM_BASE_URL = 'https://scnu-exam.webtrn.cn/exam/examflow_index.action'
let currentPage
let currentFrame
const delay = (time) =>
new Promise((resolve) => {
setTimeout(() => resolve(), time)
})
function main() {
const getType = function () {
const module = getCurrentModule()
if (module.includes('教学视频')) {
return UTILS_TYPE.LEARN
}
if (module.includes('在线练习')) {
return UTILS_TYPE.GET_EXAM
}
}
$('.nav .nav_li').click(function () {
var tab = $(this).find('a').text()
if (tab === '课程内容') {
initToolbar()
awaitPageLoaded(function () {
setMessage('准备中...')
awaitFrameLoaded(() => {
const type = getType()
const actions = {
[UTILS_TYPE.LEARN]: startLearn,
[UTILS_TYPE.GET_EXAM]: getExam
}
setUtilType(type)
actions[type] && actions[type]()
})
})
return
}
$('#huashi-exam-toolbar').remove()
})
if (location.href.includes(GET_EXAM_BASE_URL)) {
openExam()
}
if (location.href.includes(GET_EXAM_BASE_URL_2)) {
getExam()
}
if (location.href.includes(SURE_EXAM_BASE_URL)) {
sureExam()
}
if (location.href.includes(EXAM_BASE_URL)) {
submitExam()
}
}
function initToolbar() {
if ($('#huashi-exam-toolbar').length) return
const messageBox = $(
`<div id="huashi-exam-toolbar">
<p style="color:red">华师网络教育学习助手</p>
<div id="huashi-exam-message"></div>
<div id="huashi-exam-util-type">当前功能:<span>${util_type}</span></div>
</div>`
)
const css = `
#huashi-exam-toolbar {
position: absolute;
width: 100%;
height: 50px;
padding: 0px 20px;
background: rgb(255, 255, 255);
top: 0px; left: 0px;
color: red;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
}
#huashi-exam-message {
color: red;
}
`
$('body').append(messageBox)
$('head').append($('<style type="text/css">').html(css))
}
function getCurrentModule() {
return $page('#nav .vtitle span.v2').text()
}
function $page(selector, context = currentPage) {
return $(context).contents().find(selector)
}
function setMessage(message) {
$('#huashi-exam-message').text(message)
}
function setUtilType(type) {
util_type = type
$('#huashi-exam-util-type span').text(util_type)
setMessage('')
}
function awaitPageLoaded(cb) {
const page = $('#mainContent')
$(page).one('load', function () {
currentPage = $(this)
cb()
})
}
function awaitFrameLoaded(cb) {
$(currentPage)
.contents()
.find('#mainFrame')
.on('load', function () {
currentFrame = $(this)
cb()
})
}
function fmtMSS(s) {
s = parseInt(s)
if (s < 0 || isNaN(s)) return ''
return (s - (s %= 60)) / 60 + (s > 9 ? ':' : ':0') + s
}
function fmtM(s) {
s = parseInt(s)
return (s - (s %= 60)) / 60
}
function startLearn() {
const getSpeed = function (minute) {
const key = Object.keys(PLAY_SPEED_STRATEGY)
.sort((a, b) => b - a)
.find(function (m) {
return m <= minute
})
return PLAY_SPEED_STRATEGY[key] || 1
}
const passVideo = function (video, next) {
const duration = fmtMSS(video.duration)
const minute = fmtM(video.duration)
const speed = getSpeed(minute)
setMessage(
'当前视频时长:' +
duration +
',将采用' +
speed +
'倍速播放,预计需要' +
(minute * (speed / 100)).toFixed(1) +
'分钟。'
)
video.muted = true
video.playbackRate = speed
const timer = setInterval(function () {
if (video && video.playbackRate !== speed) {
video.playbackRate = speed
}
}, 5000)
$(video).on('ended', function () {
clearInterval(timer)
next()
})
}
const passChapterByType = function (type, context) {
const next = getNext()
if (type === CHAPTER_TYPE.TEXT) {
return next()
}
if (type === CHAPTER_TYPE.VIDEO) {
return passVideo(context, next)
}
}
const getNext = function () {
const menus = $page('.menuct .menub')
const menu = $page('.menuct .menubu')
const index = menus.index(menu)
const nextChapter = function () {
const chapters = $page('.vcon:visible .vconlist > li')
const chapter = $page('.vcon:visible .vconlist > li.select')
const index = chapters.index(chapter)
if (index === chapters.length - 1) {
return () => {
setMessage('学习完毕')
}
}
return function () {
setMessage('正在加载...')
setTimeout(() => {
$(chapters[index + 1]).click()
$(chapters[index + 1])
.find('a')
.click()
}, 500)
}
}
if (index === menus.length - 1) {
return nextChapter()
}
return function () {
setMessage('正在加载...')
$page('#rtarr').click()
}
}
const awaitVideoLoaded = function (cb) {
var target = $(currentFrame).contents().find('body')[0]
const textContent = $(target).find('#textContent')
if (textContent.length >= 1) {
cb(CHAPTER_TYPE.TEXT, textContent)
return
}
let loadedVideo
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function () {
if (loadedVideo) return
const video = $(target).find('.cont video')
if (video.attr('src')) {
loadedVideo = true
video.on('loadedmetadata', function () {
cb(CHAPTER_TYPE.VIDEO, video[0])
})
observer.disconnect()
}
})
})
observer.observe(target, { subtree: true, childList: true })
}
awaitVideoLoaded((type, context) => {
setTimeout(() => {
passChapterByType(type, context)
}, 500)
})
}
function getExam() {
setUtilType(UTILS_TYPE.GET_EXAM)
const isSolo = location.href.includes(GET_EXAM_BASE_URL_2)
if (!isSolo) {
const isConfirm = window.confirm('是否需要自动获取题库?')
if (!isConfirm) return
}
const autoGetExamCount = window.prompt(
'请输入自动获取题库次数:',
DEFAULT_AUTO_GET_EXAM_COUNT
)
GM_setValue('autoGetExamCount', autoGetExamCount)
GM_setValue(EXAM_IDS_KEY, [])
if (isSolo) {
window.open($('#examIframe', currentFrame).attr('src'))
} else {
window.open($page('#examIframe', currentFrame).attr('src'))
}
}
async function openExam() {
if (self != top) {
return
}
const ids = GM_getValue(EXAM_IDS_KEY) || []
console.log('4ark ids -->', ids)
const count = GM_getValue('autoGetExamCount') || DEFAULT_AUTO_GET_EXAM_COUNT
console.log('4ark count -->', count)
if (ids.length >= count) {
initToolbar()
setUtilType(UTILS_TYPE.GET_EXAM)
setMessage('正在下载题库...')
return downloadExam(ids)
}
await delay(500)
console.log(`4ark $('#viewRecordBtn').length`, $('#viewRecordBtn').length)
if ($('#viewRecordBtn').length) {
GM_setValue(EXAM_IDS_KEY, [
...ids,
$('#viewRecordBtn').attr('href').split('=').pop()
])
}
;``
await delay(500)
$('#goBtn').click()
await delay(500)
$('.TB_command_btn ').eq(1).click()
}
async function sureExam() {
const isExercise = $('.exam-primTit').text().includes('在线练习')
if (!isExercise) return
await delay(500)
console.log('4ark', $('.submit_solid'))
$('.submit_solid').click()
await delay(500)
$('.TB_command_btn ').eq(1).click()
}
async function submitExam() {
const isExercise = $('.paper_name').text().includes('在线练习')
if (!isExercise) return
await delay(500)
$('.paper_submit').click()
await delay(500)
$('.win_btn1').eq(1).click()
await delay(500)
$('.TB_command_btn').click()
}
async function downloadExam(ids) {
const tasks = ids.map((id) => requestExam(id))
const result = await Promise.all(tasks)
const html = getHTML(result)
const frag = document.createDocumentFragment()
const div = $(`<div id="huashi-exam-download-div">${html}</div>`)
div.css({
display: 'none'
})
frag.appendChild(div[0])
$('body').append(frag)
const download = function (content, filename) {
var eleLink = document.createElement('a')
eleLink.download = filename
eleLink.style.display = 'none'
// 字符内容转变成blob地址
var blob = new Blob([content])
eleLink.href = URL.createObjectURL(blob)
// 触发点击
document.body.appendChild(eleLink)
eleLink.click()
// 然后移除
document.body.removeChild(eleLink)
}
const downloadHTML = function (questions) {
const html = questions
.map((question, index) => {
return `<body style="padding: 32px;"><div class="question" style="margin-bottom: 20px;">
<div class="question-content" style="display: flex;">${
index + 1
}、${
question.content?.src
? `<img src="${question.content.src}" style="margin-bottom: 20px;" />`
: question.content
}</div>
<div class="question-options" style="padding-left: 20px;">${question.options
.map((option) => `<li>${option}</li>`)
.join('')}</div>
<div class="question-answer" style="padding-left: 20px; margin-top: 10px;">参考答案:${
question.answer?.src
? `<img src="${question.answer.src}" style="margin-bottom: 20px;" />`
: question.answer
}</div>
</div></body>`
})
.join('')
download(html, $('.mod_tit h2').text().trim() + '题库.html')
GM_setValue(EXAM_IDS_KEY, [])
setMessage('下载完成')
}
const getQuestions = function () {
let questions = $('.q_content')
.map((_, el) => {
let content = $(el)
.find('.divQuestionTitle')
.text()
.replace(/\d+、/, '')
const img = $(el).find('.divQuestionTitle img').attr('src')
if (img) {
content = {
src: img.startsWith('https')
? img
: `https://scnu-exam.webtrn.cn${img}`
}
}
const options = $(el)
.find('.q_option_readonly')
.map(function () {
return $(this).text()
})
.get()
let answer =
$(el).find('.exam_rightAnswer span[name=rightAnswer]').text() ||
$(el).find('.exam_rightAnswer .has_standard_answer').text()
const answerImg = $(el)
.find('.exam_rightAnswer .has_standard_answer img')
.attr('src')
if (answerImg) {
answer = {
src: answerImg.startsWith('https')
? answerImg
: `https://scnu-exam.webtrn.cn${answerImg}`
}
}
return {
content,
options,
answer
}
})
.get()
const arrayUniqueByKey = (array, key) => {
return [...new Map(array.map((item) => [item[key], item])).values()]
}
questions = arrayUniqueByKey(questions, 'content')
downloadHTML(questions)
}
getQuestions()
}
function requestExam(id) {
return new Promise((resolve) => {
$.ajax({
url: 'https://scnu-exam.webtrn.cn/student/exam/examrecord_getRecordPaperStructure.action',
type: 'POST',
headers: {
Authority: 'scnu-exam.webtrn.cn',
Pragma: 'no-cache',
'Cache-Control': 'no-cache',
Accept: '*/*',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47',
Origin: 'https://scnu-exam.webtrn.cn',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
Referer: `https://scnu-exam.webtrn.cn/student/exam/examrecord_recordDetail.action?recordId=${id}`,
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
Cookie: document.cookie,
'Accept-Encoding': 'gzip'
},
contentType: 'application/x-www-form-urlencoded',
data: {
recordId: id
}
}).done(function (data) {
data = JSON.parse(data)
const { contentList, examBatchId } = data.data
const contentIds = contentList.map(({ id }) => id).join()
return $.ajax({
type: 'POST',
url: 'https://scnu-exam.webtrn.cn/student/exam/examrecord_getRecordContent.action',
headers: {
Authority: 'scnu-exam.webtrn.cn',
Pragma: 'no-cache',
'Cache-Control': 'no-cache',
Accept: '*/*',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47',
Origin: 'https://scnu-exam.webtrn.cn',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
Referer: `https://scnu-exam.webtrn.cn/student/exam/examrecord_recordDetail.action?recordId=${id}`,
'Accept-Language':
'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
Cookie: document.cookie,
'Accept-Encoding': 'gzip'
},
contentType: 'application/x-www-form-urlencoded',
data: {
recordId: id,
examBatchId: examBatchId,
contentIds: contentIds,
'params.monitor': '',
'params.isRandomQuestion': '0'
}
}).then((data) => {
data = JSON.parse(data)
contentList.forEach((item) => {
if (data?.data?.[item.id]) {
data.data[item.id].type = item.name
}
})
if (data.data) {
resolve(data.data)
} else {
resolve({})
}
})
})
})
}
function getHTML(data) {
return data
.map((item) => {
return Object.values(item)
.map((val) => {
if (val.contentHtml) return val
})
.filter((s) => s)
})
.flat()
.sort((a, b) =>
QUESTION_TYPE_SORT[a.type] > QUESTION_TYPE_SORT[b.type] ? 1 : -1
)
.map((val) => val.contentHtml)
.join('')
}
main()
})()