FDU_Class_Schedule_Generator

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

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