UOOC assistant

【使用前先看介绍/有问题可反馈】UOOC 优课联盟助手 (UOOC assistant):可选是否倍速 (若取消勾选则一倍速播放),可选是否静音 (若取消勾选则恢复原音量),可选是否播放 (若取消勾选则暂停播放),可选是否连播 (若取消勾选则循环播放),离开页面保持视频状态,自动回答视频中途弹出问题,可复制已提交测验题目及答案;键盘左右方向键可以控制视频快进/快退,上下方向键可以控制音量增大/减小,空格键可以控制播放/暂停;如果视频标题下面出现 `倍速/静音/播放/连播` 选项说明脚本正常启动运行。

当前为 2021-05-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name UOOC assistant
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.4
  5. // @description 【使用前先看介绍/有问题可反馈】UOOC 优课联盟助手 (UOOC assistant):可选是否倍速 (若取消勾选则一倍速播放),可选是否静音 (若取消勾选则恢复原音量),可选是否播放 (若取消勾选则暂停播放),可选是否连播 (若取消勾选则循环播放),离开页面保持视频状态,自动回答视频中途弹出问题,可复制已提交测验题目及答案;键盘左右方向键可以控制视频快进/快退,上下方向键可以控制音量增大/减小,空格键可以控制播放/暂停;如果视频标题下面出现 `倍速/静音/播放/连播` 选项说明脚本正常启动运行。
  6. // @author cc
  7. // @include http://www.uooc.net.cn/home/learn/index*
  8. // @grant none
  9. // @require https://greasyfork.org/scripts/418193-coder-utils.js
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14. const RECURSION_DURATION = 500;
  15. let recursion = () => {
  16. let extraTime = 0;
  17. try {
  18. let done = false;
  19. let video = document.querySelector('#player_html5_api');
  20. if (video) {
  21. if (document.getElementById('rate').checked)
  22. video.playbackRate = 2;
  23. else
  24. video.playbackRate = 1;
  25. if (document.getElementById('volume').checked)
  26. video.muted = true;
  27. else
  28. video.muted = false;
  29. if (document.getElementById('play').checked && !video.ended)
  30. video.play();
  31. else
  32. video.pause();
  33. if (video.ended)
  34. done = true;
  35. let quizLayer = document.querySelector('#quizLayer');
  36. if (quizLayer && quizLayer.style.display != 'none') {
  37. if (done) {
  38. setTimeout(() => {
  39. document.querySelectorAll('.layui-layer-shade').forEach(e => e.style.display = 'none');
  40. }, RECURSION_DURATION << 1);
  41. };
  42. let source = JSON.parse(document.querySelector('div[uooc-video]').getAttribute('source'));
  43. let quizList = source.quiz;
  44. let quizIndex = 0;
  45. let quizQuestion = document.querySelector('.smallTest-view .ti-q-c').innerHTML;
  46. for (let i = 0; i < quizList.length; i++) {
  47. if (quizList[i].question == quizQuestion) {
  48. quizIndex = i;
  49. break;
  50. };
  51. };
  52. let quizAnswer = eval(quizList[quizIndex].answer);
  53. let quizOptions = quizLayer.querySelector('div.ti-alist');
  54. for (let ans of quizAnswer) {
  55. let labelIndex = ans.charCodeAt() - 'A'.charCodeAt();
  56. quizOptions.children[labelIndex].click();
  57. }; // end for
  58. quizLayer.querySelector('button').click();
  59. extraTime = 1000;
  60. }; // end if
  61. if (!done) {
  62. if (video.paused && document.getElementById('play').checked) {
  63. video.play();
  64. } else {
  65. document.querySelectorAll('.layui-layer-shade, #quizLayer').forEach(e => e.style.display = 'none');
  66. };
  67. };
  68. }; // end if (video)
  69. if (!done) {
  70. setTimeout(recursion, RECURSION_DURATION + extraTime);
  71. } else if (video) {
  72. if (!document.getElementById('continue').checked) {
  73. video.currentTime = 0;
  74. // video.ended = false;
  75. setTimeout(recursion, RECURSION_DURATION + extraTime);
  76. } else {
  77. let current_video = document.querySelector('.basic.active');
  78. let next_part = current_video.parentNode;
  79. let next_video = current_video;
  80. // 定义判断是否视频的函数
  81. let isVideo = node => Boolean(node.querySelector('span.icon-video'));
  82. // 定义是否可返回上一级目录的函数
  83. let canBack = () => {
  84. return Boolean(next_part.parentNode.parentNode.tagName === 'LI');
  85. };
  86. // 定义更新至后续视频的函数
  87. let toNextVideo = () => {
  88. next_video = next_video.nextElementSibling;
  89. while (next_video && !isVideo(next_video)) {
  90. next_video = next_video.nextElementSibling;
  91. };
  92. };
  93. // 定义判断是否存在视频的函数
  94. let isExistsVideo = () => {
  95. let _video = next_part.firstElementChild;
  96. while (_video && !isVideo(_video)) {
  97. _video = _video.nextElementSibling;
  98. };
  99. return Boolean(_video && isVideo(_video));
  100. };
  101. // 定义判断是否存在后续视频的函数
  102. let isExistsNextVideo = () => {
  103. let _video = current_video.nextElementSibling;
  104. while (_video && !isVideo(_video)) {
  105. _video = _video.nextElementSibling;
  106. };
  107. return Boolean(_video && isVideo(_video));
  108. };
  109. // 定义检查文件后是否存在后续目录的函数
  110. let isExistsNextListAfterFile = () => {
  111. let part = next_part.nextElementSibling;
  112. return Boolean(part && part.childElementCount > 0);
  113. };
  114. // 定义更新文件后的后续目录的函数
  115. let toNextListAfterFile = () => {
  116. next_part = next_part.nextElementSibling;
  117. };
  118. // 定义返回上一级的函数
  119. let toOuterList = () => {
  120. next_part = next_part.parentNode.parentNode;
  121. };
  122. // 定义返回主条目的函数
  123. let toOuterItem = () => {
  124. next_part = next_part.parentNode;
  125. };
  126. // 定义检查列表后是否存在后续目录的函数
  127. let isExistsNextListAfterList = () => {
  128. return Boolean(next_part.nextElementSibling);
  129. };
  130. // 定义进入列表后的后续目录的函数
  131. let toNextListAfterList = () => {
  132. next_part = next_part.nextElementSibling;
  133. };
  134. // 定义展开目录的函数
  135. let expandList = () => {
  136. next_part.firstElementChild.click();
  137. };
  138. // 定义进入展开目录的第一个块级元素的函数
  139. let toExpandListFirstElement = () => {
  140. next_part = next_part.firstElementChild.nextElementSibling;
  141. if (next_part.classList.contains('unfoldInfo')) {
  142. next_part = next_part.nextElementSibling;
  143. };
  144. };
  145. // 定义判断块级元素是否目录列表的函数
  146. let isList = () => {
  147. return Boolean(next_part.tagName === 'UL');
  148. };
  149. // 定义目录列表的第一个目录的函数
  150. let toInnerList = () => {
  151. next_part = next_part.firstElementChild;
  152. };
  153. // 定义进入文件列表的第一个视频的函数
  154. let toFirstVideo = () => {
  155. next_video = next_part.firstElementChild;
  156. while (next_video && !isVideo(next_video)) {
  157. next_video = next_video.nextElementSibling;
  158. };
  159. };
  160. // 定义模式
  161. let mode = {
  162. FIRST_VIDEO: 'FIRST_VIDEO',
  163. NEXT_VIDEO: 'NEXT_VIDEO',
  164. LAST_LIST: 'LAST_LIST',
  165. NEXT_LIST: 'NEXT_LIST',
  166. INNER_LIST: 'INNER_LIST',
  167. OUTER_LIST: 'OUTER_LIST',
  168. OUTER_ITEM: 'OUTER_ITEM',
  169. }
  170. // 定义搜索函数
  171. let search = (_mode) => {
  172. switch (_mode) {
  173. case mode.FIRST_VIDEO: // mode = 0
  174. if (isExistsVideo()) {
  175. toFirstVideo();
  176. next_video.click();
  177. setTimeout(recursion, RECURSION_DURATION);
  178. } else if (isExistsNextListAfterFile()) {
  179. search(mode.LAST_LIST);
  180. } else {
  181. // perhaps there is an exam, end recursion
  182. };
  183. break;
  184. case mode.NEXT_VIDEO: // mode == 1
  185. if (isExistsNextVideo()) {
  186. toNextVideo();
  187. next_video.click();
  188. setTimeout(recursion, RECURSION_DURATION);
  189. } else if (isExistsNextListAfterFile()) {
  190. search(mode.LAST_LIST);
  191. } else {
  192. search(mode.OUTER_ITEM);
  193. };
  194. break;
  195. case mode.LAST_LIST: // mode == 2
  196. toNextListAfterFile();
  197. toInnerList();
  198. search(mode.INNER_LIST);
  199. break;
  200. case mode.NEXT_LIST: // mode == 3
  201. toNextListAfterList();
  202. search(mode.INNER_LIST);
  203. break;
  204. case mode.INNER_LIST: // mode == 4
  205. expandList();
  206. (function waitForExpand () {
  207. if (next_part.firstElementChild.nextElementSibling) {
  208. toExpandListFirstElement();
  209. if (isList()) {
  210. toInnerList();
  211. search(mode.INNER_LIST);
  212. } else {
  213. search(mode.FIRST_VIDEO);
  214. };
  215. } else {
  216. setTimeout(waitForExpand, RECURSION_DURATION);
  217. };
  218. })();
  219. break;
  220. case mode.OUTER_LIST: // mode == 5
  221. toOuterList();
  222. if (isExistsNextListAfterList()) {
  223. search(mode.NEXT_LIST);
  224. } else if (canBack()) {
  225. search(mode.OUTER_LIST);
  226. } else {
  227. // perhaps there is no next list
  228. };
  229. break;
  230. case mode.OUTER_ITEM: // mode == 6
  231. toOuterItem();
  232. if (isExistsNextListAfterList()) {
  233. toNextListAfterList();
  234. search(mode.INNER_LIST);
  235. } else if (canBack()){
  236. search(mode.OUTER_LIST);
  237. } else {
  238. // perhaps there is no list
  239. };
  240. break;
  241. default:
  242. break;
  243. };
  244. };
  245. try {
  246. search(mode.NEXT_VIDEO);
  247. } catch (err) {
  248. console.error(err);
  249. };
  250. };
  251. };
  252. } catch (err) {
  253. console.error(err);
  254. };
  255. }; // end recursion
  256. let wait = () => {
  257. if (document.readyState == 'complete') {
  258. let getCheckbox = (name, text) => {
  259. let p = HTMLElement.$mkel('p', {}, {}, {
  260. 'color': '#cccccc',
  261. 'padding-left': '10px',
  262. });
  263. let checkbox = HTMLElement.$mkel('input', {
  264. id: name,
  265. type: 'checkbox',
  266. name: name,
  267. value: name,
  268. }, {
  269. checked: true,
  270. }, {
  271. 'margin-left': '15px',
  272. 'width': '12px',
  273. 'height': '12px',
  274. });
  275. p.append(checkbox);
  276. let label = HTMLElement.$mkel('label', {
  277. for: name,
  278. }, {
  279. innerText: text,
  280. }, {
  281. 'margin-left': '13px',
  282. 'font-size': '12px',
  283. });
  284. p.append(label);
  285. return p;
  286. };
  287. let getContainer = (_id) => {
  288. return HTMLElement.$mkel('div', {id: _id}, {}, {
  289. 'display': 'flex',
  290. 'flex-direction': 'row',
  291. 'align-items': 'center',
  292. });
  293. };
  294. // set checkbox container
  295. let checkboxContainer = getContainer('checkbox-container');
  296. let rateCheckbox = getCheckbox('rate', '倍速');
  297. let volumeCheckbox = getCheckbox('volume', '静音');
  298. let playCheckbox = getCheckbox('play', '播放');
  299. let continueCheckbox = getCheckbox('continue', '连播');
  300. let copyCheckbox = HTMLElement.$mkel('p', {}, {}, {
  301. 'color': '#cccccc',
  302. 'padding-left': '10px',
  303. });
  304. let btn = HTMLElement.$mkel('button', {}, {innerHTML: '复制题目答案'}, {
  305. 'margin-left': '13px',
  306. 'padding': '0 5px 0',
  307. 'font-size': '12px',
  308. 'cursor': 'pointer',
  309. }, {
  310. click: function(event) {
  311. let testPaperTop = frames[0] ? frames[0].document.querySelector('.testPaper-Top') : document.querySelector('.testPaper-Top');
  312. if (!testPaperTop) {
  313. alert('该页面不是测验页面,无法复制内容');
  314. } else {
  315. if (testPaperTop.querySelector('.fl_right')) {
  316. let queItems = frames[0] ? Array.from(frames[0].document.querySelectorAll('.queItems')) : Array.from(document.querySelectorAll('.queItems'));
  317. let content = queItems.map(queType => {
  318. let res = '';
  319. if (queType.querySelector('.queItems-type').innerText.indexOf('选') >= 0) {
  320. let questions = queType.querySelectorAll('.queContainer');
  321. res += Array.from(questions).map(question => {
  322. let que = question.querySelector('.queBox').innerText.replace(/\n{2,}/g, '\n').replace(/(\w\.)\n/g, '$1 ');
  323. let ans = question.querySelector('.answerBox div:first-child').innerText.replace(/\n/g, '');
  324. let right = question.querySelector('.scores').innerText.match(/\d+\.?\d+/g).map(score => eval(score));
  325. right = right[0] === right[1];
  326. return `${que}\n${ans}\n是否正确:${right}\n`;
  327. }).join('\n');
  328. };
  329. return res;
  330. }).join('\n');
  331. content.$copyToClipboard();
  332. alert('题目及答案已复制到剪切板');
  333. } else {
  334. alert('该测验可能还没提交,无法复制');
  335. };
  336. };
  337. },
  338. });
  339. copyCheckbox.appendChild(btn);
  340. let head = document.querySelector('.learn-head');
  341. if (!head) {
  342. setTimeout(wait, RECURSION_DURATION);
  343. return;
  344. };
  345. checkboxContainer.appendChild(rateCheckbox);
  346. checkboxContainer.appendChild(volumeCheckbox);
  347. checkboxContainer.appendChild(playCheckbox);
  348. checkboxContainer.appendChild(continueCheckbox);
  349. checkboxContainer.appendChild(copyCheckbox);
  350. // set prompt container
  351. let promptContainer = getContainer('prompt-container');
  352. let div = HTMLElement.$mkel('div', {}, {
  353. innerHTML: `提示:<u><a href="https://greasyfork.org/zh-CN/scripts/425837-uooc-assistant-beta" target="_blank" style="color: yellow;">开放内测,点此试用</a></u>,键盘的 \u2190 \u2192 可以控制快进/快退,\u2191 \u2193 可以控制音量增大/减小,空格键可以控制播放/暂停`,
  354. }, {
  355. 'color': '#cccccc',
  356. 'height': 'min-height',
  357. 'margin': '0 20px 0',
  358. 'padding': '0 5px',
  359. 'border-radius': '5px',
  360. 'font-size': '12px',
  361. });
  362. promptContainer.appendChild(div);
  363. let appreciationCodeContainer = getContainer('appreciation-code-container');
  364. let a = HTMLElement.$mkel('a', {
  365. href: 'https://s1.ax1x.com/2020/11/08/BTeRqe.png',
  366. target: '_blank',
  367. }, {
  368. innerHTML: '<u>本脚本使用完全免费,您的打赏是作者维护下去的最大动力!点此打赏作者😊</u>',
  369. }, {
  370. 'color': '#cccccc',
  371. 'font-weight': 'bold',
  372. 'height': 'min-height',
  373. 'margin': '0 20px 0',
  374. 'padding': '0 5px',
  375. 'border-radius': '5px',
  376. 'font-size': '11px',
  377. });
  378. appreciationCodeContainer.appendChild(a);
  379. // set head
  380. head.appendChild(checkboxContainer);
  381. head.appendChild(promptContainer);
  382. head.appendChild(appreciationCodeContainer);
  383. head.style.height = `${head.offsetHeight + 30}px`;
  384. // bind key down events
  385. document.onkeydown = (event) => {
  386. let k = event.key;
  387. let complete = false;
  388. let div = document.querySelector('div.basic.active');
  389. if (div && div.classList.contains('complete'))
  390. complete = true;
  391. let video = document.getElementById('player_html5_api');
  392. if (video) {
  393. switch (k) {
  394. case 'ArrowLeft': {
  395. video.currentTime -= 10;
  396. break;
  397. };
  398. case 'ArrowRight': {
  399. if (complete)
  400. video.currentTime += 10;
  401. break;
  402. };
  403. case 'ArrowUp': {
  404. if (video.volume + 0.1 <= 1.0)
  405. video.volume += 0.1;
  406. else
  407. video.volume = 1.0;
  408. break;
  409. }
  410. case 'ArrowDown': {
  411. if (video.volume - 0.1 >= 0.0)
  412. video.volume -= 0.1;
  413. else
  414. video.volume = 0.0;
  415. break;
  416. };
  417. case ' ': {
  418. let continueCheckbox = document.getElementById('play');
  419. continueCheckbox.checked = !continueCheckbox.checked;
  420. break;
  421. };
  422. };
  423. };
  424. };
  425. // information
  426. console.info('UOOC assistant init ok.');
  427. recursion();
  428. } else {
  429. setTimeout(wait, RECURSION_DURATION);
  430. };
  431. }; // end wait
  432. wait();
  433. })();