// ==UserScript==
// @name 英华网课助手 Plus
// @description 自动连播网课
// @author little3022
// @namespace little3022.TM
// @homepageURL https://greasyfork.org/users/782903
// @supportURL https://greasyfork.org/scripts/440261/feedback
// @version 2.2.5
// @license GNU GPL v2.0
// @icon data:image/gif;base64,R0lGODlhQABAALMAAAAAABCWm/vaCPoFE3t+fu3t7AOi6+C8rXV0dJHR7leHifRPVpCQkUa48PTiUQAAACH5BAkAAA8ALAAAAABAAEAAAAT+8MlJq7046827/2B4LWJpVsNwrmVKsjCXpnGNLTNt71Ku8zXfDBgUqogs4xBpwikHL2bouZR+qLOoVYb9bTXd3FfjzC7OZ984MzscLoVDeW1JvTkFHL2yKIDkexJ3IXmBJ36GiYqLjB4FiBaIkIaPD4+TE5WVjH6YkZ9rl5uRohWDQAYYnRqSFAcORA2ppJ6Zlp4FAkQGsxSatRKXFwKwO7y9oBnAArrGvMoby8Q2x8i2wLbDzDWyxwk7DszbMNUGDeDizSvlzzbp4yfs5jXh78Ul8u0w7+Im3fLnWPATdw9EvmMBTdQbqO7DwWoJQyxkWLDDQ4giJjJsyOFiuYiSHDRu5Jjhn0eEIUcOjNGgW8uXDb5dEKmyXyMpASTkxLDTws6fPR/81GkigNGjSCcMJUohaNCmQp+KWNoU6VGlVpNGzWqUhVSrOntKZZozq9CiZqFSxaoVqtIVS8Vy7epzbNoSc51GPXuBLl+3Mfz2/dpVMFuwJwpXyGtXw9gQgq8udmx4K1e8NzNr3sy5s+cMEQAAOw==
// @run-at document-body
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @downloadURL https://update.greasyfork.icu/scripts/440261/%E8%8B%B1%E5%8D%8E%E7%BD%91%E8%AF%BE%E5%8A%A9%E6%89%8B%20Plus.user.js
// @updateURL https://update.greasyfork.icu/scripts/440261/%E8%8B%B1%E5%8D%8E%E7%BD%91%E8%AF%BE%E5%8A%A9%E6%89%8B%20Plus.meta.js
// ==/UserScript==
class YHAssistant {
/**
* 类名: YHAssistant
* 说明: 主类, 统筹管理所有脚本数据.
**/
constructor() {
this.__appname__ = 'YHAssistant';
this.__author__ = 'little3022';
this.__version__ = '2.2.5';
this.__scriptid__ = '440261';
this.regexs = {
originalHostMatch: [
/mooc\.yinghuaonline\.com/,
/mooc\.\S+\.edu\.cn/,
/shixun\.\S+\.edu\.cn/
],
courseId: /(?<=courseId=)\d+/,
nodeId: /(?<=nodeId=)\d+/
};
this.pathnames = [
'/user/node'
];
this.setting = {
video: {
muted: false,
volume: 1,
autoplay: true
},
beep: {
muted: false,
volume: 80,
effect: 0,
customURL: ''
},
captcha: {
enabled: false,
apiURL: ''
},
whitelist: [],
blacklist: [],
data: {
courseId: '',
finished: false,
page: 1,
pageCount: 1
}
};
this.beepURLs = [
'https://cdn2.ear0.com:3321/preview?soundid=34591&type=mp3',
'https://downsc.chinaz.net/Files/DownLoad/sound1/202106/14428.mp3',
'https://downsc.chinaz.net/Files/DownLoad/sound1/202103/14039.mp3'
];
this.finishBeep = 'https://ppt-mp3cdn.hrxz.com/d/file/filemp3/hrxz.com-bjxdrlfq5o143304.mp3';
this.HTML = null;
this._timers = {
mainTimer: {
id: null,
value: 0
}
};
this._videoData = { // 本地视频总时长, 已提交时长, 本次播放有效时长
totalTime: 0,
submitTime: 0,
validTime: 0
};
this._courseData = {
courseId: '',
page: 0,
pageCount: 0,
index: 0,
recordsCount: 0,
nextURL: ''
};
this._tree = {};
this._locker = {
refresh: {
value: false,
timeout: 3000
},
button: {
value: false,
timeout: 300
},
beep: {
value: false,
timeout: 1000
},
request: {
value: false,
timeout:500
}
};
this._page = 0;
this._elBeep = document.createElement('audio');
this._beepCount = 0;
}
_timerLock(locker) {
if(!locker.value) {
locker.value = true;
setTimeout(() => {locker.value = false;}, locker.timeout);
return false;
}
return true;
}
_initGUITree() {
let table = this.HTML.querySelector('table[data-yha="table"]');
this._tree = {
msgbox: this.HTML.querySelector('[data-yha="msgbox"]'),
container: this.HTML.querySelector('[data-yha="container"]'),
labelInfo: this.HTML.querySelector('[data-yha="label-info"]'),
labelSet: this.HTML.querySelector('[data-yha="label-set"]'),
labelCaptcha: this.HTML.querySelector('[data-yha="label-advanced"]'),
btRefresh: this.HTML.querySelector('[yha-action="refresh"]'),
btSet: this.HTML.querySelector('[yha-action="setting"]'),
btBack: this.HTML.querySelector('[yha-action="back"]'),
btReset: this.HTML.querySelector('[yha-action="reset"]'),
btCaptcha: this.HTML.querySelector('[yha-action="advanced"]'),
tip2: this.HTML.querySelector('[data-yha="tip2"]'),
progress: {
table: {
caption: table.caption,
cells: [
table.rows[0].cells[1],
table.rows[1].cells[1],
table.rows[2].cells[1],
table.rows[3].cells[1],
table.rows[4].cells[1],
table.rows[5].cells[1],
table.rows[6].cells[1]
]
}
},
setting: {
video: {
muting: this.HTML.querySelector('[yha-setting="video-muting"]'),
volume: this.HTML.querySelector('[yha-setting="video-volume"]'),
autoplay: this.HTML.querySelector('[yha-setting="video-autoplay"]')
},
beep:{
muting: this.HTML.querySelector('[yha-setting="beep-muting"]'),
volume: this.HTML.querySelector('[yha-setting="beep-volume"]'),
beep1: this.HTML.querySelector('[yha-setting="beep-beep1"]'),
beep2: this.HTML.querySelector('[yha-setting="beep-beep2"]'),
beep3: this.HTML.querySelector('[yha-setting="beep-beep3"]'),
beep4: this.HTML.querySelector('[yha-setting="beep-beep4"]'),
beepURL: this.HTML.querySelector('[yha-setting="beep-beepURL"]'),
test: this.HTML.querySelector('[yha-action="test"]')
},
advanced: {
whitelist: this.HTML.querySelector('[yha-setting="whitelist"]'),
blacklist: this.HTML.querySelector('[yha-setting="blacklist"]'),
btAPIEnabled: this.HTML.querySelector('[yha-setting="api-enabled"]'),
apiURL: this.HTML.querySelector('[yha-setting="captcha-api"]')
}
}
}
}
_bindGUIEvent() {
let elContainer = this._tree.container;
let labelInfo = this._tree.labelInfo;
let labelSet = this._tree.labelSet;
let labelCaptcha = this._tree.labelCaptcha;
let btRefresh = this._tree.btRefresh;
let btSet = this._tree.btSet;
let btBack = this._tree.btBack;
let btReset = this._tree.btReset;
let btCaptcha = this._tree.btCaptcha;
let tip2 = this._tree.tip2;
let vMute = this._tree.setting.video.muting;
let bMute = this._tree.setting.beep.muting;
let vVolume = this._tree.setting.video.volume;
let bVolume = this._tree.setting.beep.volume;
let vAutoplay = this._tree.setting.video.autoplay;
let beep4 = this._tree.setting.beep.beep4;
let txtURL = this._tree.setting.beep.beepURL;
let btTest = this._tree.setting.beep.test;
let whitelist = this._tree.setting.advanced.whitelist;
let blacklist = this._tree.setting.advanced.blacklist;
let btAPIEnabled = this._tree.setting.advanced.btAPIEnabled;
let apiURL = this._tree.setting.advanced.apiURL;
let radioArr = [
this._tree.setting.beep.beep1,
this._tree.setting.beep.beep2,
this._tree.setting.beep.beep3,
this._tree.setting.beep.beep4,
];
let arr = [];
btRefresh.addEventListener('click', () => {
if(this._timerLock(this._locker.refresh)) return;
let tStr = btRefresh.getAttribute('yha-tooltip');
let tTime = this._locker.refresh.timeout;
btRefresh.classList.toggle('action');
this.refreshClick();
let tTimer = setInterval(() => {
btRefresh.setAttribute('yha-tooltip', `${(tTime -= 100) / 1000}s`);
if(tTime <= 0) {
btRefresh.setAttribute('yha-tooltip', tStr);
btRefresh.classList.toggle('action');
tTimer && clearInterval(tTimer);
}
}, 100);
});
btSet.addEventListener('click', () => {
if(this._timerLock(this._locker.button)) return;
labelInfo.style.display = 'none';
labelSet.style.display = 'inline-block';
btRefresh.style.display = 'none';
btSet.style.display = 'none';
btBack.style.display = 'block';
btReset.style.display = 'block';
btCaptcha.style.display = 'block';
this._page = 1;
elContainer.style.left = `-${this._page * 100}%`;
});
btBack.addEventListener('click', () => {
if(this._timerLock(this._locker.button)) return;
labelInfo.style.display = 'none';
labelSet.style.display = 'none';
labelCaptcha.style.display = 'none';
btRefresh.style.display = 'none';
btSet.style.display = 'none';
btBack.style.display = 'none';
btReset.style.display = 'none';
btCaptcha.style.display = 'none';
this._page--;
switch(this._page) {
case 0:
labelInfo.style.display = 'inline-block';
btRefresh.style.display = 'block';
btSet.style.display = 'block';
break;
case 1:
labelSet.style.display = 'inline-block';
btBack.style.display = 'block';
btReset.style.display = 'block';
btCaptcha.style.display = 'block';
break;
case 2:
labelCaptcha.style.display = 'inline-block';
btBack.style.display = 'block';
break;
}
elContainer.style.left = `-${this._page * 100}%`;
});
btReset.addEventListener('click', () => {
if(this._timerLock(this._locker.button)) return;
if(!confirm('确定重置设置吗?')) return;
this.recoverSetting();
this.saveSetting();
this.showSetting();
});
btCaptcha.addEventListener('click', () => {
btReset.style.display = 'none';
btCaptcha.style.display = 'none';
labelSet.style.display = 'none';
labelCaptcha.style.display = 'inline-block';
this._page = 2;
elContainer.style.left = `-${this._page * 100}%`;
});
arr = [
this._tree.progress.table.cells[1],
this._tree.progress.table.cells[2],
this._tree.progress.table.cells[4]
];
arr.forEach(item => {
item.set = function(value) {
if(value < 0) value = 0;
item.innerText = value + ' s';
};
});
vMute.addEventListener('click', () => {
if(this._timerLock(this._locker.button)) return;
if(!this.setting.video.muted) {
vMute.classList.add('muted');
vVolume.value = 0;
}
else {
vMute.classList.remove('muted');
vVolume.value = this.setting.video.volume;
}
this.setting.video.muted = !this.setting.video.muted;
if(this.elVideo) {
this.elVideo.muted = this.setting.video.muted;
}
this.saveSetting();
});
bMute.addEventListener('click', () => {
if(this._timerLock(this._locker.button)) return;
if(!this.setting.beep.muted) {
bMute.classList.add('muted');
bVolume.value = 0;
}
else {
bMute.classList.remove('muted');
bVolume.value = this.setting.beep.volume;
}
this.setting.beep.muted = !this.setting.beep.muted;
this.saveSetting();
});
vVolume.addEventListener('input', () => {
vVolume.setAttribute('value', vVolume.value);
if(vVolume.value == 0) vMute.classList.add('muted');
vMute.classList.remove('muted');
if(this.elVideo) {
this.elVideo.muted = false;
this.elVideo.volume = vVolume.value / 100;
}
});
bVolume.addEventListener('input', () => {
bVolume.setAttribute('value', bVolume.value);
if(bVolume.value == 0) bMute.classList.add('muted');
bMute.classList.remove('muted');
});
vVolume.addEventListener('change', () => {
if(this._timerLock(this._locker.button)) return;
if(vVolume.value == 0) {
vMute.classList.add('muted');
vVolume.setAttribute('value', this.setting.video.volume);
this.setting.video.muted = true;
}
else {
vMute.classList.remove('muted');
this.setting.video.volume = parseInt(vVolume.value);
this.setting.video.muted = false;
}
if(this.elVideo) {
this.elVideo.muted = this.setting.video.muted;
this.elVideo.volume = this.setting.video.volume / 100;
// 同步视频标签操作, 防止音量被重置
}
this.saveSetting();
});
bVolume.addEventListener('change', () => {
if(this._timerLock(this._locker.button)) return;
if(bVolume.value == 0) {
bMute.classList.add('muted');
bVolume.setAttribute('value', this.setting.beep.volume);
this.setting.beep.muted = true;
}
else {
bMute.classList.remove('muted');
this.setting.beep.volume = parseInt(bVolume.value);
this.setting.beep.muted = false;
this.beep();
this.saveSetting();
}
});
vAutoplay.addEventListener('click', () => {
if(this._timerLock(this._locker.button)) return;
this.setting.video.autoplay = !this.setting.video.autoplay;
vAutoplay.checked = this.setting.video.autoplay;
this.saveSetting();
});
arr = [
this._tree.setting.beep.beep1,
this._tree.setting.beep.beep2,
this._tree.setting.beep.beep3
]
arr.forEach(item => {
item.addEventListener('click', () => {
if(this._timerLock(this._locker.beep)) {
radioArr[this.setting.beep.effect].checked = true;
return;
}
txtURL.disabled = true;
this.setting.beep.effect= parseInt(item.value);
this.beep();
this.saveSetting();
});
});
beep4.addEventListener('click', () => {
if(this._timerLock(this._locker.button)) {
radioArr[this.setting.beep.effect].checked = true;
return;
}
txtURL.disabled = false;
});
txtURL.addEventListener('input', () => {
tip2.hidden = false;
});
txtURL.addEventListener('change', () => {
if(this._timerLock(this._locker.button)) return;
tip2.hidden = true;
if(txtURL.value.search(/https:\S+/) !== 0) {
txtURL.value = '';
arr[this.setting.beep.effect].checked = true;
txtURL.disabled = true;
return this.showInfo('无效的 URL');
}
this.setting.beep.effect = 3;
this.setting.beep.customURL = txtURL.value;
this.saveSetting();
});
btTest.addEventListener('click', () => {
if(this._timerLock(this._locker.beep)) return;
if(this.setting.beep.effect == 3 && (this.setting.beep.customURL.search(/https:\S+/) !== 0)) return this.showError('#008', '无效的 URL');
this.beep();
});
let whiteCount = 0, blackCount = 0;
whitelist.addEventListener('keypress', e => {
tip2.hidden = false;
if(e.which === 13) {
let eChange = new Event('change');
whitelist.dispatchEvent(eChange);
}
whiteCount++;
});
whitelist.addEventListener('change', e => {
if(this._timerLock(this._locker.button) && e.isTrusted) return;
if(!whiteCount) return;
// 去除空格并转为数组并去除空值
let list = whitelist.value.replaceAll(' ', '').replaceAll(/\n+/g, '\n').split('\n').filter(x => x);
// 去除重复项
list = [...new Set(list)];
// 判断是否相同
let i = 0;
for(; list.length === this.setting.whitelist.length && i < list.length; i++) {
if(list[i] !== this.setting.whitelist[i]) break;
}
if(i < list.length) { // 不相同
this.setting.whitelist = list;
this.saveSetting();
}
whitelist.value = list.join('\n');
tip2.hidden = true;
});
blacklist.addEventListener('keypress', e => {
tip2.hidden = false;
if(e.which === 13) {
let eChange = new Event('change');
blacklist.dispatchEvent(eChange);
}
else blackCount++;
});
blacklist.addEventListener('change', e => {
if(this._timerLock(this._locker.button) && e.isTrusted) return;
if(!blackCount) return;
// 去除空格转为数组并去除空值
let list = blacklist.value.replaceAll(/\n+/g, '\n').split('\n').filter(x => x);
// 去除重复项
list = [...new Set(list)];
// 判断是否相同
let i = 0;
for(; list.length === this.setting.blacklist.length && i < list.length; i++) {
if(list[i] !== this.setting.blacklist[i]) break;
}
if(i < list.length) { // 不相同
this.setting.blacklist = list;
this.saveSetting();
}
blacklist.value = list.join('\n');
tip2.hidden = true;
});
btAPIEnabled.addEventListener('click', () => {
if(this._timerLock(this._locker.button)) return;
if(this.setting.captcha.enabled) {
apiURL.disabled = true;
}
else {
apiURL.disabled = false;
}
if(this.setting.captcha.apiURL);
this.setting.captcha.enabled = true;
this.saveSetting();
});
apiURL.addEventListener('input', () => {
tip2.hidden = false;
});
apiURL.addEventListener('change', () => {
if(this._timerLock(this._locker.button)) return;
tip2.hidden = true;
if(apiURL.value.search(/http\S+/) !== 0) {
apiURL.value = '';
btAPIEnabled.checked = false;
apiURL.disabled = true;
return this.showInfo('无效的 URL');
}
this.setting.captcha.enabled = true;
this.setting.captcha.apiURL = apiURL.value;
this.saveSetting();
});
}
showGUI() {
function getHTML() {
/*
英华网课助手 Plus 控制台
设置未保存
*/
let lines = new String(getHTML);
lines = lines.substring(lines.indexOf("/*") + 3, lines.indexOf("*/"));
return lines;
}
function getCSS() {
/*
#YHAssistant {
--yha-width: 300px;
--yha-height: 435px;
--yha-color: gray;
z-index: 999;
position: fixed;
top: calc(50vh - var(--yha-height) / 2);
left: calc(20px - var(--yha-width));
margin: 0;
padding: 0;
border: 0;
border-radius: 8px;
box-shadow: 3px 3px 8px #3338;
width: var(--yha-width);
font-size: 16px;
color: var(--yha-color);
background-color: #F5F5DCDD;
transition: all 0.5s ease;
overflow: hidden;
-webkit-user-select: none;
user-select: none;
}
#YHAssistant:hover {
left: 0;
}
#YHAssistant.reveal {
left: 8px;
}
[data-yha="title"] {
margin: 1em auto;
font-size: 1em;
font-weight: bold;
text-align: center;
}
[data-yha="title"] span {
font-size: 0.6em;
font-weight: normal;
vertical-align: super;
}
[data-yha="info"] {
border-bottom: 1px solid var(--yha-color);
padding-bottom: 8px;
font-size: 0.6em;
text-align: center;
}
[data-yha="info"] span {
padding: 0 5px 0 5px;
border-right: 1px solid var(--yha-color);
}
[data-yha="info"] span:last-child {
padding: 0 0 0 5px;
border-right: none;
}
[data-yha="toolbar"] {
z-index: 10;
margin: 8px 12px;
}
[data-yha="toolbar"] [data-yha="icon"] {
float: left;
margin: 0 8px;
}
[data-yha="toolbar"] [data-yha="button"] {
float: right;
margin-left: 8px;
}
[data-yha="icon"] svg,
[data-yha="button"] svg {
width: 20px;
height: 20px;
margin: auto;
pointer-events: none;
}
[data-yha="button"] {
opacity: 0.6;
height: 30px;
width: 30px;
display: block;
cursor: pointer;
line-height: 36px;
text-align: center;
border-radius: 8px;
transition: opacity 0.1s ease;
fill: #333;
}
[data-yha="button"]:hover {
opacity: 1;
background-color: #3331;
}
[data-yha="button"]::after {
display: none;
content: attr(yha-tooltip);
position: relative;
top: -15px;
font-size: 0.8em;
}
[data-yha="button"]:hover::after {
display: block;
}
@keyframes refreshing{
from{transform: rotate(0deg);}
to{transform: rotate(-360deg);}
}
[yha-action="refresh"].action svg {
animation: refreshing 3s linear infinite;
animation-fill-mode: forwards;
}
[data-yha="container"] {
display: flex;
flex-direction: row;
position: relative;
left: 0;
width: 300%;
min-height: 200px;
transition: left 0.5s ease;
}
[data-yha="progress"],
[data-yha="setting"],
[data-yha="advanced"] {
box-sizing: content-box;
padding: 15px 20px;
width: calc(var(--yha-width) - 40px);
}
[data-yha="setting"],
[data-yha="advanced"] {
font-size: 0.85em;
}
[data-yha="table"] {
clear: both;
margin: auto;
margin-top: 12px;
width: 80%;
line-height: 1.5em;
text-align: center;
}
[data-yha="table"] caption {
margin: 8px auto;
width: calc(var(--yha-width) * 0.8);
font-size: 1.1em;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-yha="table"] th, td {
border-bottom: 1px solid #ccc;
text-align: center;
}
[data-yha="table"] th {
font-size: 0.9em;
}
[data-yha="setting"] {
font-size: 0.85em;
}
[data-yha="setting-value"] input[type="range"] {
position: relative;
top: -2px;
width: calc(100% - 75px);
}
[data-yha="setting-value"] input[type="range"]::after {
content: attr(value);
float: right;
width: 0;
}
[data-yha="setting-value"] input[type="checkbox"] {
position: relative;
top: -1.5px;
margin: 0 8px 0 0;
}
[data-yha="setting-value"] input[type="radio"] {
position: relative;
top: 1.5px;
margin: 0 8px 0 0;
}
[data-yha="text"] {
display: inline-block;
margin: 5px auto;
padding-top: 5px;
border-top: 1px var(--yha-color) dashed;
width: 100%;
font-size: 1em;
font-weight: bold;
}
[data-yha="setting-item"] {
margin: 5px auto;
}
[data-yha="setting-key"] {
position: relative;
top: -3px;
}
[data-yha="test"] {
float: right;
position: relative;
top: -35px;
left: -18px;
display: inline-block;
border-radius: 8px;
width: 52px;
height: 28px;
color: #fff;
line-height: 28px;
text-align: center;
background-color: #0005;
box-shadow: 3px 3px 3px #3333;
cursor: pointer;
}
[data-yha="test"]:hover {
background-color: #0008;
}
[data-yha="tip1"] {
display: inline-block;
margin-top: -22px;
line-height: 20px;
color: red;
font-weight: bold;
text-indent: 1.5em;
text-align: justify;
}
[data-yha="tip2"] {
z-index: 10;
position: absolute;
top: 118px;
right: 15px;
color: red;
background-color: pink;
padding: 5px;
border-radius: 5px;
font-size: 0.6em;
box-shadow: 3px 3px 3px gray;
pointer-events: none;
}
input[type="text"].changed,
textarea.changed {
background-color: pink;
}
[data-yha="msgbox"] {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
pointer-events: none;
}
[data-yha="msgbox"] > span {
z-index: 999;
margin: 5px auto;
padding: 8px 20px;
border: 2px dashed royalblue;
border-radius: 8px;
text-align: center;
color: white;
transition: opacity 0.8s linear;
}
.msgbox-info {
background-color: #ef950399;
}
.msgbox-error {
background-color: #ff1e1e99;
}
.yha-input {
opacity: 0;
position: absolute;
left: 0;
cursor: pointer;
}
.yha-icon-muting {
display: inline-block;
width: 18px;
height: 18px;
background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSJncmF5IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxwYXRoIGQ9Ik03NjIuNyA0NjkuN2MwLTkzLjUtNjkuMy0xNzAuMS0xNTkuMi0xODN2NTMuMWM2MC41IDEyLjMgMTA2LjEgNjUuOCAxMDYuMSAxMzBzLTQ1LjYgMTE3LjctMTA2LjEgMTMwdjUzLjFjODkuOS0xMy4yIDE1OS4yLTg5LjggMTU5LjItMTgzLjJ6TTk5LjQgMzYzLjV2MjEyLjJjMCAyOS4zIDIzLjggNTMuMSA1My4xIDUzLjFoNzkuNlYzMTAuNWgtNzkuNmMtMjkuMyAwLTUzLjEgMjMuNy01My4xIDUzeiBtMzcxLjUtMjEyLjJMMjg1LjIgMjc1LjF2Mzg5LjFMNDcwLjkgNzg4YzI5LjMgMCA1My4xLTIzLjggNTMuMS01My4xVjIwNC40Yy0wLjEtMjkuNC0yMy44LTUzLjEtNTMuMS01My4xeiBtMTMyLjYtNTIuN3Y1NC41YzE1NS44IDMwIDI2NS4zIDE1Ny4xIDI2NS4zIDMxNi42IDAgMTU4LjYtMTA2LjEgMjgxLjUtMjY1LjMgMzE2LjZ2NTQuNWMxNzkuOC0yNi4zIDMxOC40LTE4MS42IDMxOC40LTM3MS4xIDAtMTg5LjUtMTM4LjYtMzQ0LjgtMzE4LjQtMzcxLjF6Ij48L3BhdGg+PC9zdmc+);
cursor: pointer;
}
.yha-icon-muting.muted {
background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSJncmF5IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxwYXRoIGQ9Ik0xMDAuMSAzOTl2MjIxYzAgMzAuNSAyNC43IDU1LjMgNTUuMyA1NS4zaDgyLjlWMzQzLjdoLTgyLjljLTMwLjUgMC01NS4zIDI0LjgtNTUuMyA1NS4zek04MDMgNTA5LjVsMTE1LjMtMTE1LjNjMTIuNC0xMi40IDEzLjYtMzEuNSAyLjYtNDIuNXMtMzAtOS45LTQyLjUgMi42TDc2My4xIDQ2OS42IDY0Ny45IDM1NC4zYy0xMi40LTEyLjUtMzEuNS0xMy42LTQyLjUtMi42cy05LjkgMzAgMi42IDQyLjVsMTE1LjMgMTE1LjNMNjA4IDYyNC44Yy0xMi40IDEyLjQtMTMuNiAzMS41LTIuNiA0Mi41czMwIDkuOSA0Mi41LTIuNmwxMTUuMy0xMTUuMyAxMTUuMyAxMTUuM2MxMi40IDEyLjQgMzEuNSAxMy42IDQyLjUgMi42czkuOS0zMC0yLjYtNDIuNUw4MDMgNTA5LjV6TTQ4Ni45IDE3OEwyOTMuNSAzMDYuOXY0MDUuMkw0ODYuOSA4NDFjMzAuNSAwIDU1LjMtMjQuNyA1NS4zLTU1LjNWMjMzLjJjLTAuMS0zMC41LTI0LjgtNTUuMi01NS4zLTU1LjJ6Ij48L3BhdGg+PC9zdmc+);
}
*/
let lines = new String(getCSS);
lines = lines.substring(lines.indexOf("/*") + 3, lines.indexOf("*/"));
return lines;
}
let _css = document.createElement('style');
let _wrapper = document.createElement('div');
_css.type = 'text/css';
_css.innerHTML = getCSS();
_wrapper.id = this.__appname__;
_wrapper.classList.toggle('reveal');
_wrapper.innerHTML = getHTML().replace('__author__', this.__author__).replace('__version__', this.__version__).replace('__scriptid__', this.__scriptid__);
document.head.appendChild(_css);
document.documentElement.insertBefore(_wrapper, document.body);
this.HTML = _wrapper;
this._initGUITree();
this._bindGUIEvent();
this._tree.tip2.hidden = true;
setTimeout(() => {
_wrapper.classList.toggle('reveal');
}, 3000);
}
_setRange(el, value) {
el.value = value;
el.setAttribute('value', value);
}
loadSetting() {
let t1 = GM_getValue(this.__appname__, null);
let badSetting = false;
for(let item in t1) {
if(item == 'whitelist') {
if(Array.isArray(t1[item])) this.setting.whitelist = t1[item];
continue;
}
if(item == 'blacklist') {
if(Array.isArray(t1[item])) this.setting.blacklist = t1[item];
continue;
}
for(let key in t1[item]) {
try {
this.setting[item][key] = t1[item][key];
}
catch(e) {
badSetting = true;
continue;
}
}
}
if(badSetting) {
GM_deleteValue(this.__appname__);
this.showInfo('识别到无效本地设置, 已重置');
}
}
saveSetting(showInfo=true) {
GM_setValue(this.__appname__, this.setting);
if(showInfo) this.showInfo('已存储设置');
}
recoverSetting() {
let effect = 0;
let customURL = '';
let enabledAPI = false;
let apiURL = '';
if(this.setting.beep.effect === 3) effect = 3;
if(this.setting.beep.customURL.search(/https:\S+/) === 0) customURL = this.setting.beep.customURL;
if(this.setting.captcha.enabled) enabledAPI = true;
if(this.setting.captcha.apiURL.search(/http\S+/) === 0) apiURL = this.setting.captcha.apiURL;
this.setting = {
video: {
muted: false,
volume: 1,
autoplay: true
},
beep: {
muted: false,
volume: 80,
effect: 0,
customURL: ''
},
captcha: {
enabled: false,
apiURL: ''
},
whitelist: [],
blacklist: [],
data: {
courseId: '',
finished: false,
page: 1,
pageCount: 1
}
};
this.setting.beep.effect = effect;
this.setting.beep.customURL = customURL;
this.setting.captcha.enabled = enabledAPI;
this.setting.captcha.apiURL = apiURL;
}
showSetting() {
let vMute = this._tree.setting.video.muting;
let bMute = this._tree.setting.beep.muting;
let vVolume = this._tree.setting.video.volume;
let bVolume = this._tree.setting.beep.volume;
let vAutoplay = this._tree.setting.video.autoplay;
let txtURL = this._tree.setting.beep.beepURL;
let whitelist = this._tree.setting.advanced.whitelist;
let blacklist = this._tree.setting.advanced.blacklist;
let btAPIEnabled = this._tree.setting.advanced.btAPIEnabled;
let apiURL = this._tree.setting.advanced.apiURL;
if(this.setting.video.muted) {
vMute.classList.add('muted');
vVolume.setAttribute('value', this.setting.video.volume);
vVolume.value = 0;
}
else {
this._setRange(vVolume, this.setting.video.volume);
}
if(this.setting.video.autoplay) vAutoplay.checked = true;
if(this.setting.beep.muted) {
bMute.classList.add('muted');
vVolume.setAttribute('value', this.setting.beep.volume);
bVolume.value = 0;
}
else {
this._setRange(bVolume, this.setting.beep.volume);
if(this.setting.beep.volume < 50) this.showInfo('提示音音量过低', 6000);
}
switch(this.setting.beep.effect) {
case 0:
this._tree.setting.beep.beep1.checked = true;
txtURL.disabled = true;
break;
case 1:
this._tree.setting.beep.beep2.checked = true;
txtURL.disabled = true;
break;
case 2:
this._tree.setting.beep.beep3.checked = true;
txtURL.disabled = true;
break;
case 3:
this._tree.setting.beep.beep4.checked = true;
txtURL.disabled = false;
}
txtURL.value = this.setting.beep.customURL;
whitelist.value = this.setting.whitelist.join('\n');
blacklist.value = this.setting.blacklist.join('\n');
btAPIEnabled.checked = this.setting.captcha.enabled;
if(this.setting.captcha.enabled) apiURL.disabled = false;
else apiURL.disabled = true;
apiURL.value = this.setting.captcha.apiURL;
}
beep(src='') {
if(this.setting.beep.muted || this.setting.data.finished) return;
if(src) {
this._elBeep.src = src;
}
else if(this.setting.beep.effect == 3) {
this._elBeep.src = this.setting.beep.customURL;
}
else {
this._elBeep.src = this.beepURLs[this.setting.beep.effect];
}
this._elBeep.volume = this.setting.beep.volume / 100;
if(this._beepCount < 3) {
this._elBeep.muted = true;
this._elBeep.play();
setTimeout(() => {
this._elBeep.muted = false;
}, 10);
}
else {
this._elBeep.play();
}
this._beepCount++;
}
loopBeep(times=3, interval=1000) {
let i = 0;
if(times <= 1) {
this.beep();
return;
}
let val = setInterval(() => {
if(i++ < times) this.beep();
else val && clearInterval(val);
}, interval);
return val;
}
beepTip() { // 组合提示音
this._timers.a = this.loopBeep(3, 2000);
this._timers.c = setTimeout(() => {
this._timers.b = this.loopBeep(5, 10000);
}, 30000);
}
clearBeepTip() {
this._timers.a && clearInterval(this._timers.a);
this._timers.b && clearInterval(this._timers.b);
this._timers.c && clearTimeout(this._timers.c);
}
showInfo(msg, timeout=3000) {
let elTip = document.createElement('span');
elTip.className = 'msgbox-info';
elTip.innerText = 'Ⓘ ' + msg;
this._tree.msgbox.appendChild(elTip);
setTimeout(() => {
elTip.style.opacity = 0;
}, timeout - 800);
setTimeout(() => {
elTip.style.display = 'none';
}, timeout);
return true;
}
showError(id, msg, timeout=5000) {
let elTip = document.createElement('span');
elTip.className = 'msgbox-error';
elTip.innerText = `⚠ 错误 ID: ${id}, 描述: ${msg}.`;
this._tree.progress.table.caption.innerText = `⚠ ${msg} ⚠`;
this._tree.progress.table.caption.title = `⚠ 错误 ID: ${id}, 描述: ${msg}.`;
this._tree.progress.table.caption.style.color = 'red';
this._tree.msgbox.appendChild(elTip);
setTimeout(() => {
elTip.style.opacity = 0;
}, timeout - 800);
setTimeout(() => {
elTip.style.display = 'none';
}, timeout);
return false;
}
simulateMouseMove(element) {
}
refreshClick() {
if(!this._courseData && !this._courseData.courseId) return this.showError('#002', '不支持的页面');
this.showInfo('正在检查进度...');
let courseData = this.checkPage();
if(courseData) {
this._courseData = courseData;
this.showCourseData(courseData);
// 刷新计时器
if(this._timers.d) {
clearTimeout(this._timers.d);
this._timers.d = null;
}
let surplusTime = this._videoData.totalTime - this._videoData.submitTime - this._videoData.validTime;
this._timers.d = setTimeout(() => {
}, parseInt(surplusTime > 0? surplusTime + 3: 0) * 1000);
}
else {
this.showInfo('刷新失败');
}
}
furureTime(sec=0) {
/**
* 返回当前时间(hh:mm:ss)加 sec 后的时间
*/
let date = new Date();
let sTime = date.toJSON().match(/\d\d:\d\d:\d\d/)[0];
let time = this.parseSec(sTime);
return this.formatSec(time + sec);
}
parseSec(sTime="00:00:00") {
/**
* 函数名: parseSec()
* 说明: 把时间格式的字符串转换为秒数.*/
let sec = 0;
if(sTime != "" && !isNaN(Date.parse("1970-1-1 " + sTime))) { //判断是否是时间格式
let t = sTime.split(":");
sec += parseInt(t[0]) * 3600 + parseInt(t[1]) * 60 + parseInt(t[2]);
}
else return -1;
return sec;
}
formatSec(sec=0) {
/**
* 函数名: formateSec()
* 说明: 把秒数转换为时间格式的字符串.*/
return (new Date(sec * 1000).toTimeString().slice(0, 8));
}
img2b64(elImg, width=90, height=40) {
let canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
let ctx1 = canvas.getContext("2d");
ctx1.drawImage(elImg, 0, 0, width, height);
return canvas.toDataURL();
}
parseCaptcha(elImg) {
let xhr = new XMLHttpRequest();
let formdata = new FormData();
let code = '';
let b64 = this.img2b64(elImg, elImg.width, elImg.height);
formdata.append('img', b64);
xhr.open('POST', this.setting.captcha.apiURL, false);
xhr.onreadystatechange = () => {
if(xhr.readyState === 4 && xhr.status === 200) {
try {
code = JSON.parse(xhr.responseText).code;
}
catch(e) {}
}
};
try {
xhr.send(formdata);
}
catch(e) {
return '';
}
return code;
}
handleCaptcha() {
this.showInfo('正在识别验证码...', 5000);
let t1 = null, t2 = null;
t1 = setTimeout(() => {
try {
let elImg = document.querySelector('#layui-layer1 > div.layui-layer-content > div > div:nth-child(2) > img:nth-child(4)');
let elInput = document.querySelector('#layui-layer1 > div.layui-layer-content > div > div:nth-child(2) > input[type=text]:nth-child(2)');
let elSubmit = document.querySelector('#layui-layer1 > div.layui-layer-btn.layui-layer-btn- > a.layui-layer-btn0');
let code = this.parseCaptcha(elImg);
let eMousedown = new MouseEvent('mousedown');
if(!code) {
this.showError('#009', '验证码识别失败');
return false;
}
this.showInfo('验证码识别成功');
// 模拟触发 mousedown 事件, 执行其内部代码, 防止被服务器检测
elInput.dispatchEvent(eMousedown);
elInput.value = code;
elSubmit.click();
setTimeout(() => {
if(this.elVideo.paused) this.elVideo.play();
}, 2500);
}
catch(e) {
this.showError('#007', '验证码识别失败');
this.beepTip();
}
}, 5000);
t2 = setTimeout(() => {
if(this.elVideo.paused) {
window.location.reload();
}
}, 10000);
}
getCourseID() {
/**
* 爬取网页源码中的课程 ID, 用于请求课程数据
*/
let courseId = '';
try {
courseId = document.querySelector('#wrapper > div.curPlace > div.center > a:last-child').href.match(/(?<=courseId=)\d+/)[0];
}
catch(err) {}
return courseId;
}
request(courseId, page=1) {
/**
* 描述: 从服务器获取课程数据(JSON)
* 请求模式: 同步
*/
let xhttp = new XMLHttpRequest();
let url = `/user/study_record.json?courseId=${courseId}&page=${page}&_=${(new Date()).valueOf()}`;
let result = null;
xhttp.onreadystatechange = () => {
if(xhttp.readyState == 4 && xhttp.status == 200) {
result = JSON.parse(xhttp.responseText);
}
};
xhttp.open('get', url, false);
xhttp.send();
return result;
}
checkPage(page=1, pageCount=1) {
/**
* 跳转未完成页面, 返回对应课程数据
*/
let courseId = this.getCourseID();
if(!courseId) return this.showError('#003', 'courseId 获取失败');
if(this.setting.data.courseId == courseId) {
page = this.setting.data.page;
pageCount = this.setting.data.pageCount;
}
for(; page <= pageCount; page++) {
this.showInfo(`请求数据, courseId: ${courseId}, page: ${page}`);
let json = this.request(courseId, page);
if(json && json.status) {
this.showInfo('数据请求成功');
pageCount = json.pageInfo.pageCount;
for(let courseData of json.list) { // 查找未完成页面
if(courseData.state.match(/(未学完|未学)/)) {
let currentNodeID = document.location.href.match(this.regexs.nodeId)[0];
let newNodeID = courseData.url.match(this.regexs.nodeId)[0];
let index = json.list.indexOf(courseData);
let nextCourse = json.list[index + 1];
if(currentNodeID != newNodeID) { // 本节已完成 跳转未完成页面
window.open(courseData.url, '_self');
// 立即退出循环, 防止 window.open() 重复请求
return this.showInfo('正在跳转新页面...');
}
// 当前页面为未完成, 添加页面信息方便后续处理
courseData.page = json.pageInfo.page;
courseData.pageCount = json.pageInfo.pageCount;
courseData.index = json.pageInfo.page * 20 - 20 + index;
courseData.recordsCount = json.pageInfo.recordsCount;
if(nextCourse) courseData.nextURL = nextCourse.url;
// 保存页码减少重复请求
this.setting.data.courseId = courseId;
this.setting.data.finished = false;
this.setting.data.page = page;
this.setting.data.pageCount = json.pageInfo.pageCount;
this.saveSetting();
return courseData;
}
}
}
else return this.showError('#004', '数据请求失败');
}
// 课程进度 100%
this.HTML.style.color = 'black';
this.HTML.style.backgroundColor = 'mediumseagreen';
this.showInfo('该课程观看进度 100%');
this._tree.progress.table.caption.innerText = '(已完成) ' + this._tree.progress.table.caption.innerText;
this._tree.progress.table.caption.title = this._tree.progress.table.caption.innerText;
this.beep(this.finishBeep);
this.setting.data.finished = true;
this.saveSetting();
return false;
}
showCourseData() {
this._tree.progress.table.caption.innerText = this._courseData.name;
this._tree.progress.table.caption.title = this._courseData.name;
this._tree.progress.table.cells[0].innerText = parseInt(this._courseData.viewCount) + 1;
this._tree.progress.table.cells[1].set(this.parseSec(this._courseData.videoDuration));
this._tree.progress.table.cells[2].set(this.parseSec(this._courseData.videoDuration) - this._courseData.duration);
this._tree.progress.table.cells[3].innerText = this._courseData.state.match(/(未学完|未学|已学)/)[0];
this._tree.progress.table.cells[4].set(0);
this._tree.progress.table.cells[5].innerText = this.furureTime(this.parseSec(this._courseData.videoDuration) - this._courseData.duration);
this._tree.progress.table.cells[6].innerText = `${this._courseData.index}/${this._courseData.recordsCount} (${(this._courseData.index / this._courseData.recordsCount * 100).toFixed(2)}%)`;
}
listeningVideo() {
let elVideo = document.querySelector('video');
if(this.setting.data.finished) return;
if(!elVideo) return this.showError('#005', '未获取视频对象');
// if(elVideo.src != this._courseData.localFile) return this.showError('#006', '视频 URL 不匹配');
this.showInfo('开始监听视频对象');
// 初始化数据
this._videoData.totalTime = this.parseSec(this._courseData.videoDuration);
this._videoData.submitTime = parseInt(this._courseData.duration);
this._timers.d = null; // 超时查询进度
// 绑定事件
let pos1 = parseInt(elVideo.currentTime), pos2 = 0;
elVideo.addEventListener('timeupdate', () => {
let pos2 = parseInt(elVideo.currentTime);
if(pos2 >= pos1 + 1) { // 经过 1s
pos1 = pos2;
this._tree.progress.table.cells[2].set(this._videoData.totalTime - this._videoData.submitTime - this._videoData.validTime++);
}
});
elVideo.addEventListener('play', () => {
if(!this._timers.d) { // 超时查询进度
let surplusTime = this._videoData.totalTime - this._videoData.submitTime - this._videoData.validTime;
this._timers.d = setTimeout(() => {
this.refreshClick();
}, parseInt(surplusTime + 3) * 1000);
// 刷新预计完成时间
this._tree.progress.table.cells[5].innerText = this.furureTime(surplusTime + 3);
}
this.clearBeepTip();
});
elVideo.addEventListener('pause', () => {
if(this._timers.d) {
clearTimeout(this._timers.d);
this._timers.d = null;
}
let t1 = false, t2 = document.querySelector('div.layui-layer > div.layui-layer-content'), t3 = 0;
if(elVideo.ended) { // 视频播放结束
this.refreshClick();
}
else if(t2 && t2.innerText == '提交学习时长失败,请检查网络状态是否正常!') { // 网络故障提交失败
if(++t3 >= 3) {
this.beepTip();
this.showInfo('网络情况不佳, 脚本已暂停', 15000);
setTimeout(() => {
if(!elVideo.paused) t3 =0;
}, 300000);
}
else {
document.querySelector('div.layui-layer > div.layui-layer-btn > a').click();
this.elVideo.play();
}
}
else if(!t1 && elVideo.currentTime < 3) { // 处理验证码
t1 = true;
if(this.setting.captcha.enabled) this.handleCaptcha();
}
else { // 未知错误
this.beepTip();
}
});
// 监听鼠标移动事件
document.body.onmousemove = () => {
this.clearBeepTip();
};
this.elVideo = elVideo;
this.showCourseData();
}
_matching() {
let matched = false;
if(this.setting.blacklist.indexOf(document.location.hostname) >= 0) {
matched = false;
}
else if(this.setting.whitelist.indexOf(document.location.hostname) >= 0) {
matched = true;
}
else {
for(let href of this.regexs.originalHostMatch) {
if(document.location.hostname.search(href) === 0) matched = true;
}
}
if(matched) { // 元匹配或白名单
if(this.setting.whitelist.indexOf(document.location.hostname) >= 0) { // 白名单
GM_registerMenuCommand('❌ 将此网站移出白名单', () => {
this.setting.whitelist = this.setting.whitelist.filter(x => {return x != document.location.hostname});
this.saveSetting();
window.location.reload();
});
}
else { // 元匹配
GM_registerMenuCommand('❌ 将此网站加入黑名单', () => {
this.setting.blacklist.push(document.location.hostname);
this.saveSetting();
window.location.reload();
});
}
}
else { // 未识别或黑名单
if(this.setting.blacklist.indexOf(document.location.hostname) >= 0) { // 黑名单
GM_registerMenuCommand('✅ 将此网站移出黑名单', () => {
this.setting.blacklist = this.setting.blacklist.filter(x => {return x != document.location.hostname});
this.saveSetting(false);
window.location.reload();
});
}
else { // 未识别页面
GM_registerMenuCommand('✅ 将此网站加入白名单', () => {
this.setting.whitelist.push(document.location.hostname);
this.saveSetting(false);
window.location.reload();
});
}
}
return matched;
}
exec() {
this.loadSetting();
if(!this._matching()) return;
this.showGUI();
this.showSetting();
if(this.pathnames.indexOf(document.location.pathname) > -1) {
let courseData = this.checkPage();
if(courseData) {
this._courseData = courseData;
// 设置主计时器
let t1 = false, t2 = false;
this._timers.mainTimer.id = setInterval(() => {
if(!t2 && document.querySelector('video')) { // 视频加载
t2 = true;
this.listeningVideo();
}
if(!t1 && this.elVideo && this.elVideo.readyState === 4) { // 视频就绪
t1 = true;
// 应用视频设置
this.elVideo.currentTime = 0;
this.elVideo.volume = this.setting.video.volume / 100;
if(this.setting.video.autoplay) {
// 静音状态下播放视频, 防止浏览器报错
this.elVideo.muted = true;
this.elVideo.play();
}
this.elVideo.muted = this.setting.video.muted;
this.elVideo.playbackRate = 1;
}
this._tree.progress.table.cells[4].set(++this._timers.mainTimer.value);
}, 1000);
}
}
else {
this.showError('#001', '不支持的页面');
}
}
}
(function() {
'use strict';
let app = new YHAssistant();
app.exec();
})();