FDU_Class_Schedule_Generator

Generate an .ics format document from eHall(https://my.fudan.edu.cn/list/bks_xx_kcb)

当前为 2020-07-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name FDU_Class_Schedule_Generator
  3. // @name:zh 复旦大学课表生成器
  4. // @description Generate an .ics format document from eHall(https://my.fudan.edu.cn/list/bks_xx_kcb)
  5. // @description:zh 从eHall个人信息(https://my.fudan.edu.cn/list/bks_xx_kcb)生成ics格式的课表
  6. // @version 0.1.7
  7. // @include https://my.fudan.edu.cn/list/bks_xx_kcb
  8. // @supportURL qsliu2017@outlook.com
  9. // @namespace https://greasyfork.org/users/666994
  10. // ==/UserScript==
  11.  
  12.  
  13. function chooseSemester() {
  14. //get semesters id
  15. const nav = document.getElementsByClassName("nav")[0];
  16. semesters = {};
  17. for (const child of nav.children) {
  18. const semesterNavReg = /nav-(\d{10})/;
  19. if (semesterNavReg.test(child.id))
  20. semesters[RegExp.$1] = child.firstElementChild.innerHTML;
  21. }
  22.  
  23. //generate choose form
  24. let formHTML = "";
  25. for (const semestersId in semesters) {
  26. formHTML += `
  27. <div class="radio">
  28. <label>
  29. <input type="radio" name="semesterOption" id="${semestersId}" value="${semestersId}">
  30. ${semesters[semestersId]}
  31. </label>
  32. </div>
  33. `
  34. }
  35.  
  36. //include bootstrap
  37. let headElement = document.getElementsByTagName('head')[0];
  38. headElement.innerHTML += `
  39. <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
  40. <script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
  41. <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  42. `;
  43.  
  44. //generate choose modal
  45. let bodyElement = document.getElementsByTagName('body')[0];
  46. bodyElement.innerHTML += `
  47. <!-- Modal -->
  48. <div class="modal fade" id="chooseModal" tabindex="-1" role="dialog" aria-labelledby="chooseModalLabel">
  49. <div class="modal-dialog" role="document">
  50. <div class="modal-content">
  51. <div class="modal-header">
  52. <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
  53. <h4 class="modal-title" id="chooseModalLabel">选择学期</h4>
  54. </div>
  55. <form name="chooseForm" class="modal-body">
  56. ${formHTML}
  57. </form>
  58. <div class="modal-footer">
  59. <button class="btn btn-primary" id="chooseModalSubmit" data-dismiss="modal">生成课表</button>
  60. </div>
  61. </div>
  62. </div>
  63. </div>
  64. `
  65. document.getElementById("chooseModalSubmit").addEventListener("click",
  66. () => {
  67. // $('#chooseModal').modal('hide');
  68. generateClassSchedule();
  69. }
  70. )
  71. $('#chooseModal').modal('show');
  72. }
  73.  
  74. function chooseFirstDate(semesterId) {
  75. let bodyElement = document.getElementsByTagName('body')[0];
  76. bodyElement.innerHTML += `
  77. <!-- Modal -->
  78. <div class="modal fade" id="chooseFirstDateModal" tabindex="-1" role="dialog" aria-labelledby="chooseFirstDateModalLabel">
  79. <div class="modal-dialog" role="document">
  80. <div class="modal-content">
  81. <div class="modal-header">
  82. <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
  83. <h4 class="modal-title" id="chooseFirstDateModalLabel">选择这个学期的起始</h4>
  84. </div>
  85. <div class="modal-body">
  86. <p>请从<a href="http://www.jwc.fudan.edu.cn/">教务处</a>找到这个学期的起始,即第0周的星期日</p>
  87. <form name="chooseFirstDateForm">
  88. <input type="datetime-local" name="fisrtDate" id="firstDate">
  89. </form>
  90. </div>
  91. <div class="modal-footer">
  92. <button class="btn btn-primary" id="chooseFirstDateModalSubmit" data-dismiss="modal">选择</button>
  93. </div>
  94. </div>
  95. </div>
  96. </div>
  97. `
  98. document.getElementById("#chooseFirstDateModalSubmit").addEventListener("click",
  99. () => {
  100. // $('#chooseFirstDateModal').modal('hide');
  101. getFirstDate(semesterId);
  102. }
  103. )
  104. $('#chooseFirstDateModal').modal('show');
  105. }
  106.  
  107. function getFirstDate(semesterId) {
  108. const firstDate = document.chooseFirstDateForm.fisrtDate.value;
  109. FirstdateOfSemester = firstDate.slice(0, 10);
  110. includeFileSaver();
  111. let blob = new Blob([iCalendarFormatter.ClassesCalendar(getClasses(semesterId))], { type: "text/plain;charset=utf-8" });
  112. saveAs(blob, semesters[semesterId] + ".ics");
  113. }
  114.  
  115. function generateClassSchedule() {
  116. const semesterId = document.chooseForm.semesterOption.value;
  117. PublicClassTime = getPublicClassTime(semesterId);
  118. chooseFirstDate(semesterId);
  119. }
  120.  
  121. class ClassTimeAndPlace {
  122.  
  123. //教室、上课时间、上课周
  124. constructor(classroom, time, weeks) {
  125. this.classroom = classroom;
  126. this.parseTime(time);
  127. this.parseWeeks(weeks);
  128. }
  129.  
  130. //把上课时间转化为Datetime格式
  131. parseTime(time) {
  132. const timeReg = /星期(.) 第(\d*)-(\d*)节/;
  133. if (timeReg.exec(time)) {
  134. this.weekday = ClassTimeAndPlace.WEEKDAYS()[RegExp.$1];
  135. this.starttime = PublicClassTime[parseInt(RegExp.$2) - 1][0];
  136. this.endtime = PublicClassTime[parseInt(RegExp.$3) - 1][1];
  137. }
  138. }
  139.  
  140. //把上课周转化为数组
  141. parseWeeks(weeks) {
  142. const weeksReg = /(\d+-\d+|\d)/g,
  143. weekReg = /(\d+)/,
  144. durationReg = /(\d+)-(\d+)/;
  145. this.weeks = [];
  146. let weekDurations = [];
  147. while (weeksReg.test(weeks)) {
  148. weekDurations.push(RegExp.$1);
  149. }
  150. for (const duration of weekDurations) {
  151. if (durationReg.test(duration)) {
  152. this.weeks.push(
  153. [firstDatetimeOfWeek(parseInt(RegExp.$1)),
  154. lastDatetimeOfWeek(parseInt(RegExp.$2))]);
  155. }
  156. else if (weekReg.test(duration)) {
  157. this.weeks.push(
  158. [firstDatetimeOfWeek(parseInt(RegExp.$1)),
  159. lastDatetimeOfWeek(parseInt(RegExp.$1))])
  160. }
  161. }
  162. }
  163.  
  164. //获取第一次课的开始时间
  165. getFirstStartDatetime() {
  166. const firstWeek = this.weeks[0][0],
  167. firstDate = dateToDatetime(new Date(datetimeToDate(firstWeek).getTime() + ClassTimeAndPlace.WEEKDAYS_TO_NUM()[this.weekday] * 24 * 60 * 60 * 1000));
  168. return firstDate.slice(0, -6) + this.starttime;
  169. }
  170.  
  171. //获取第一次课的结束时间
  172. getFirstEndDatetime() {
  173. const firstWeek = this.weeks[0][0],
  174. firstDate = dateToDatetime(new Date(datetimeToDate(firstWeek).getTime() + ClassTimeAndPlace.WEEKDAYS_TO_NUM()[this.weekday] * 24 * 60 * 60 * 1000));
  175. return firstDate.slice(0, -6) + this.endtime;
  176. }
  177. static WEEKDAYS() {
  178. return {
  179. "日": "SU",
  180. "一": "MO",
  181. "二": "TU",
  182. "三": "WE",
  183. "四": "TH",
  184. "五": "FR",
  185. "六": "SA"
  186. }
  187. }
  188. static WEEKDAYS_TO_NUM() {
  189. return {
  190. "SU": 0,
  191. "MO": 1,
  192. "TU": 2,
  193. "WE": 3,
  194. "TH": 4,
  195. "FR": 5,
  196. "SA": 6
  197. }
  198. }
  199. }
  200.  
  201. class Class {
  202. constructor(id, name) {
  203. this.id = id;
  204. this.name = name;
  205. this.timeAndPlace = [];
  206. }
  207. AddTimeAndPlace(classroom, time, weeks) {
  208. this.timeAndPlace.push(new ClassTimeAndPlace(classroom, time, weeks));
  209. }
  210. }
  211.  
  212. function getClasses(semesterId) {
  213. let listId = "list-" + semesterId,
  214. list = document.getElementById(listId),
  215. table = list
  216. .getElementsByTagName("table")[0]
  217. .getElementsByTagName("tbody")[0],
  218. rowSpan = 0,
  219. currentClass,
  220. Classes = [];
  221. for (const record of table.children) {
  222. if (rowSpan <= 0) {
  223. rowSpan = record.children[0].rowSpan;
  224. currentClass = new Class(
  225. record.children[0].innerHTML,
  226. record.children[1].innerHTML);
  227. currentClass.AddTimeAndPlace(
  228. record.children[2].innerHTML,
  229. record.children[3].innerHTML,
  230. record.children[4].innerHTML);
  231. }
  232. else {
  233. currentClass.AddTimeAndPlace(
  234. record.children[0].innerHTML,
  235. record.children[1].innerHTML,
  236. record.children[2].innerHTML);
  237. }
  238. rowSpan--;
  239. if (rowSpan <= 0) {
  240. Classes.push(currentClass);
  241. }
  242. }
  243. return Classes;
  244. }
  245.  
  246. function getPublicClassTime(semesterId) {
  247. const tableId = "table-" + semesterId + "-1",
  248. table = document.getElementById(tableId).getElementsByTagName("tbody")[0],
  249. timeReg = /(\d+):(\d+)-(\d+):(\d+)/;
  250. let times = [];
  251. for (const row of table.children) {
  252. const record = row.firstElementChild.rowSpan == 1 ? row.firstElementChild : row.firstElementChild.nextElementSibling;
  253. if (timeReg.exec(record.innerHTML)) {
  254. let begintime = (RegExp.$1.length == 1 ? "0" : "") + RegExp.$1 + RegExp.$2 + "00",
  255. endtime = (RegExp.$3.length == 1 ? "0" : "") + RegExp.$3 + RegExp.$4 + "00";
  256. times.push([begintime, endtime]);
  257. }
  258. }
  259. return times;
  260. }
  261.  
  262. function firstDatetimeOfWeek(week) {
  263. const FirstDateOfSemester = new Date(FirstdateOfSemester),
  264. FirstDateOfWeek = new Date(FirstDateOfSemester.getTime() + week * 7 * 24 * 60 * 60 * 1000);
  265. return dateToDatetime(FirstDateOfWeek);
  266. }
  267.  
  268. function lastDatetimeOfWeek(week) {
  269. const FirstDatetimeOfSemester = new Date(FirstdateOfSemester),
  270. LastDatetimeOfWeek = new Date(FirstDatetimeOfSemester.getTime() + (week + 1) * 7 * 24 * 60 * 60 * 1000 - 1);
  271. return dateToDatetime(LastDatetimeOfWeek)
  272. }
  273.  
  274. function dateToDatetime(date) {
  275. return ""
  276. + date.getFullYear()
  277. + ("0" + (date.getMonth() + 1)).slice(-2)
  278. + ("0" + date.getDate()).slice(-2)
  279. + "T"
  280. + ("0" + date.getHours()).slice(-2)
  281. + ("0" + date.getMinutes()).slice(-2)
  282. + ("0" + date.getSeconds()).slice(-2);
  283. }
  284.  
  285. function datetimeToDate(datetime) {
  286. let date = datetime.slice(0, 4) + "/";
  287. date += datetime.slice(4, 6) + "/";
  288. date += datetime.slice(6, 8) + " ";
  289. date += datetime.slice(9, 11) + ":";
  290. date += datetime.slice(11, 13) + ":";
  291. date += datetime.slice(13, 15);
  292. return new Date(date);
  293. }
  294.  
  295. class iCalendarFormatter {
  296. static ClassEvent(ClassRecord) {
  297. let event = "";
  298. for (const Class of ClassRecord.timeAndPlace) {
  299. event += "BEGIN:VEVENT\n";
  300. event += "SUMMARY:" + ClassRecord.name + "\n";
  301. event += "LOCATION:" + Class.classroom + "\n";
  302. event += "DTSTART:" + Class.getFirstStartDatetime() + "\n";
  303. event += "DTEND:" + Class.getFirstEndDatetime() + "\n";
  304. event += "RRULE:FREQ=WEEKLY;"
  305. + "BYDAY=" + Class.weekday + "\n";
  306. event += "RDATE;VALUE=PERIOD:"
  307. for (const week of Class.weeks) {
  308. event += week[0] + "/" + week[1] + ",";
  309. } event = event.slice(0, -1) + "\n";
  310. event += "END:VEVENT\n";
  311. }
  312. return event;
  313. }
  314. static ClassesCalendar(Classes) {
  315. let calendar = "BEGIN:VCALENDAR\nVERSION:2.0\n";
  316. for (const Class of Classes) {
  317. calendar += iCalendarFormatter.ClassEvent(Class);
  318. }
  319. calendar += "END:VCALENDAR\n";
  320. return calendar;
  321. }
  322. }
  323.  
  324. function includeFileSaver() {
  325. (function (global, factory) {
  326. if (typeof define === "function" && define.amd) {
  327. define([], factory);
  328. } else if (typeof exports !== "undefined") {
  329. factory();
  330. } else {
  331. var mod = {
  332. exports: {}
  333. };
  334. factory();
  335. global.FileSaver = mod.exports;
  336. }
  337. })(this, function () {
  338. "use strict";
  339.  
  340. /*
  341. * FileSaver.js
  342. * A saveAs() FileSaver implementation.
  343. *
  344. * By Eli Grey, http://eligrey.com
  345. *
  346. * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
  347. * source : http://purl.eligrey.com/github/FileSaver.js
  348. */
  349. // The one and only way of getting global scope in all environments
  350. // https://stackoverflow.com/q/3277182/1008999
  351. var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0;
  352.  
  353. function bom(blob, opts) {
  354. if (typeof opts === 'undefined') opts = {
  355. autoBom: false
  356. }; else if (typeof opts !== 'object') {
  357. console.warn('Deprecated: Expected third argument to be a object');
  358. opts = {
  359. autoBom: !opts
  360. };
  361. } // prepend BOM for UTF-8 XML and text/* types (including HTML)
  362. // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
  363.  
  364. if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
  365. return new Blob([String.fromCharCode(0xFEFF), blob], {
  366. type: blob.type
  367. });
  368. }
  369.  
  370. return blob;
  371. }
  372.  
  373. function download(url, name, opts) {
  374. var xhr = new XMLHttpRequest();
  375. xhr.open('GET', url);
  376. xhr.responseType = 'blob';
  377.  
  378. xhr.onload = function () {
  379. saveAs(xhr.response, name, opts);
  380. };
  381.  
  382. xhr.onerror = function () {
  383. console.error('could not download file');
  384. };
  385.  
  386. xhr.send();
  387. }
  388.  
  389. function corsEnabled(url) {
  390. var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker
  391.  
  392. xhr.open('HEAD', url, false);
  393.  
  394. try {
  395. xhr.send();
  396. } catch (e) { }
  397.  
  398. return xhr.status >= 200 && xhr.status <= 299;
  399. } // `a.click()` doesn't work for all browsers (#465)
  400.  
  401.  
  402. function click(node) {
  403. try {
  404. node.dispatchEvent(new MouseEvent('click'));
  405. } catch (e) {
  406. var evt = document.createEvent('MouseEvents');
  407. evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
  408. node.dispatchEvent(evt);
  409. }
  410. }
  411.  
  412. var saveAs = _global.saveAs || ( // probably in some web worker
  413. typeof window !== 'object' || window !== _global ? function saveAs() { }
  414. /* noop */
  415. // Use download attribute first if possible (#193 Lumia mobile)
  416. : 'download' in HTMLAnchorElement.prototype ? function saveAs(blob, name, opts) {
  417. var URL = _global.URL || _global.webkitURL;
  418. var a = document.createElement('a');
  419. name = name || blob.name || 'download';
  420. a.download = name;
  421. a.rel = 'noopener'; // tabnabbing
  422. // TODO: detect chrome extensions & packaged apps
  423. // a.target = '_blank'
  424.  
  425. if (typeof blob === 'string') {
  426. // Support regular links
  427. a.href = blob;
  428.  
  429. if (a.origin !== location.origin) {
  430. corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank');
  431. } else {
  432. click(a);
  433. }
  434. } else {
  435. // Support blobs
  436. a.href = URL.createObjectURL(blob);
  437. setTimeout(function () {
  438. URL.revokeObjectURL(a.href);
  439. }, 4E4); // 40s
  440.  
  441. setTimeout(function () {
  442. click(a);
  443. }, 0);
  444. }
  445. } // Use msSaveOrOpenBlob as a second approach
  446. : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) {
  447. name = name || blob.name || 'download';
  448.  
  449. if (typeof blob === 'string') {
  450. if (corsEnabled(blob)) {
  451. download(blob, name, opts);
  452. } else {
  453. var a = document.createElement('a');
  454. a.href = blob;
  455. a.target = '_blank';
  456. setTimeout(function () {
  457. click(a);
  458. });
  459. }
  460. } else {
  461. navigator.msSaveOrOpenBlob(bom(blob, opts), name);
  462. }
  463. } // Fallback to using FileReader and a popup
  464. : function saveAs(blob, name, opts, popup) {
  465. // Open a popup immediately do go around popup blocker
  466. // Mostly only available on user interaction and the fileReader is async so...
  467. popup = popup || open('', '_blank');
  468.  
  469. if (popup) {
  470. popup.document.title = popup.document.body.innerText = 'downloading...';
  471. }
  472.  
  473. if (typeof blob === 'string') return download(blob, name, opts);
  474. var force = blob.type === 'application/octet-stream';
  475.  
  476. var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;
  477.  
  478. var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);
  479.  
  480. if ((isChromeIOS || force && isSafari) && typeof FileReader === 'object') {
  481. // Safari doesn't allow downloading of blob URLs
  482. var reader = new FileReader();
  483.  
  484. reader.onloadend = function () {
  485. var url = reader.result;
  486. url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;');
  487. if (popup) popup.location.href = url; else location = url;
  488. popup = null; // reverse-tabnabbing #460
  489. };
  490.  
  491. reader.readAsDataURL(blob);
  492. } else {
  493. var URL = _global.URL || _global.webkitURL;
  494. var url = URL.createObjectURL(blob);
  495. if (popup) popup.location = url; else location.href = url;
  496. popup = null; // reverse-tabnabbing #460
  497.  
  498. setTimeout(function () {
  499. URL.revokeObjectURL(url);
  500. }, 4E4); // 40s
  501. }
  502. });
  503. _global.saveAs = saveAs.saveAs = saveAs;
  504.  
  505. if (typeof module !== 'undefined') {
  506. module.exports = saveAs;
  507. }
  508. });
  509. }
  510.  
  511. chooseSemester()