NUIST TimeTable Export

南信大课表导出为 iCal 格式

  1. // ==UserScript==
  2. // @name NUIST TimeTable Export
  3. // @version 0.1
  4. // @description 南信大课表导出为 iCal 格式
  5. // @author 凌莞
  6. // @license MIT
  7. // @match http://bkxk.nuist.edu.cn/*/student/mykebiaoall1.aspx
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=nuist.edu.cn
  9. // @grant none
  10. // @namespace https://gf.qytechs.cn/users/874549
  11. // ==/UserScript==
  12.  
  13. ((neko, nya) => {
  14. const INFO_SEPERATOR = '◇';
  15. const TIMETABLE = [
  16. // [begin, end]
  17. ['080000', '094000'],
  18. ['101000', '115000'],
  19. ['134500', '152500'],
  20. ['155500', '173500'],
  21. ['184500', '202500'],
  22. ];
  23. const WEEKS = 16;
  24. // 开学第一周的星期一
  25. const FIRST_SCHOOL_DAY = new Date('2022-02-21');
  26.  
  27. const ONE_DAY = 24 * 60 * 60 * 1000;
  28. const lastOf = (iterable) => iterable[iterable.length - 1];
  29. const $ = neko
  30. Date.prototype.format = function (fmt) {
  31. var o = {
  32. 'M+': this.getMonth() + 1, //月份
  33. 'd+': this.getDate(), //日
  34. 'h+': this.getHours(), //小时
  35. 'm+': this.getMinutes(), //分
  36. 's+': this.getSeconds(), //秒
  37. 'q+': Math.floor((this.getMonth() + 3) / 3), //季度
  38. S: this.getMilliseconds(), //毫秒
  39. }
  40. if (/(y+)/.test(fmt)) {
  41. fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length))
  42. }
  43. for (var k in o) {
  44. if (new RegExp('(' + k + ')').test(fmt)) {
  45. fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
  46. }
  47. }
  48. return fmt
  49. }
  50.  
  51. /**
  52. * 将课表上的课程文本转换成对象
  53. * @param {string} lesson 课表上的课程文本,暂不支持黑色菱形(多节课)
  54. *
  55. * 如:数字图像处理◇范春年(1-16)(软工合作20(2)班;软工合作20(1)班;)◇(西苑)揽江楼N505◇多媒体教室◇{12节}
  56. */
  57. function parselesson(lesson) {
  58. if (!lesson.trim()) return null
  59. const info = lesson.split(INFO_SEPERATOR);
  60. const name = info[0].trim();
  61. let place = info.length === 5 ? info[2].trim() : '';
  62. if (['(西苑)', '(东苑)', '(中苑)'].some(it => place.startsWith(it))) {
  63. place = place.substring(4);
  64. }
  65. const infoPart2 = parseInfo(info[1].trim());
  66. const teacher = infoPart2[0];
  67. // 一起上课的班级
  68. const coClass = infoPart2.length === 3 ? infoPart2[2] : '';
  69. let weeks = [1, WEEKS];
  70. if (infoPart2.length === 3) {
  71. const weeksExec = /(\d+)-(\d+)/.exec(infoPart2[1]);
  72. weeks = [parseInt(weeksExec[1]), parseInt(weeksExec[2])];
  73. }
  74. let weekSpec = 'all';
  75. lastOf(info).startsWith('单周') && (weekSpec = 'odd');
  76. lastOf(info).startsWith('双周') && (weekSpec = 'even');
  77. return { name, place, teacher, coClass, weeks, weekSpec, raw: lesson };
  78. }
  79.  
  80. /**
  81. * 解析课程文本的第二串
  82. * @param {string} info 第二串信息,大概包含老师姓名,周次,班级
  83. *
  84. * 如:范春年(1-16)(软工合作20(2)班;软工合作20(1)班;)
  85. */
  86. function parseInfo(info) {
  87. let lastBrackletLength = 0;
  88. const infoArray = [''];
  89. for (const i of info) {
  90. if (i === '(') {
  91. lastBrackletLength++;
  92. if (lastBrackletLength === 1)
  93. lastOf(infoArray) !== '' && infoArray.push('');
  94. else
  95. infoArray[infoArray.length - 1] += i;
  96. }
  97. else if (i === ')') {
  98. lastBrackletLength--;
  99. if (lastBrackletLength === 0)
  100. lastOf(infoArray) !== '' && infoArray.push('');
  101. else
  102. infoArray[infoArray.length - 1] += i;
  103. }
  104. else {
  105. infoArray[infoArray.length - 1] += i;
  106. }
  107.  
  108. }
  109. if (lastOf(infoArray) === '')
  110. infoArray.pop();
  111. return infoArray;
  112. }
  113.  
  114. function getSessionTimeWrapper(beginEnd) {
  115. return (weekday, session, isEvenWeek, firstWeek) => {
  116. weekday += firstWeek - 1;
  117. isEvenWeek && (weekday += 7);
  118. const firstLessonDay = new Date(FIRST_SCHOOL_DAY.getTime() + weekday * ONE_DAY);
  119. return `${firstLessonDay.format('yyyyMMdd')}T${TIMETABLE[session][beginEnd]}`;
  120. }
  121. }
  122.  
  123. const getSessionBeginTime = getSessionTimeWrapper(0);
  124. const getSessionEndTime = getSessionTimeWrapper(1);
  125.  
  126. /**
  127. * 根据网页获取课程列表
  128. */
  129. function getLessonInfos() {
  130. const $rows = $('table#TABLE1>tbody>tr');
  131. const weekdays = [];
  132. for (let i = 1; i < 7; i++) {
  133. const $row = $rows.eq(i);
  134. const sessions = [];
  135. for (let j = 1; j < 6; j++) {
  136. sessions.push(parselesson($row.children().eq(j).text()));
  137. }
  138. weekdays.push(sessions);
  139. }
  140. return weekdays;
  141. }
  142.  
  143. function getPageInfo() {
  144. const schoolYear = $('select#DropDownList1').val()
  145. const semester = $('select#DropDownList2').val()
  146. return `${schoolYear} 学年第 ${semester} 学期课表`
  147. }
  148.  
  149. function convertTimeTableToRfc5545(title, timeTable) {
  150. let icsData = `BEGIN:VCALENDAR
  151. PRODID:-//Clansty//NUIST TimeTable Export 1.0//EN
  152. VERSION:2.0
  153. CALSCALE:GREGORIAN
  154. X-WR-CALNAME:${title}
  155. X-WR-TIMEZONE:Asia/Shanghai
  156. BEGIN:VTIMEZONE
  157. TZID:Asia/Shanghai
  158. X-LIC-LOCATION:Asia/Shanghai
  159. BEGIN:STANDARD
  160. TZOFFSETFROM:+0800
  161. TZOFFSETTO:+0800
  162. TZNAME:CST
  163. DTSTART:19700101T000000
  164. END:STANDARD
  165. END:VTIMEZONE`
  166.  
  167. let idCount = 1000;
  168. const id = () => ++idCount;
  169.  
  170. /**
  171. * 把课程写入 ics
  172. * @param {object} lesson 课程对象
  173. * @param {number} weekday 星期几
  174. * @param {number} session 第几节
  175. */
  176. const writeLesson = (lesson, weekday, session) => {
  177. if (!lesson) return;
  178. const WEEKDAYS = ['MO', 'TU', 'WE', 'TH', 'FR'];
  179. const { name, place, teacher, coClass, weeks, weekSpec, raw } = lesson;
  180. const lessonWeeks = weeks[1] - weeks[0] + 1;
  181. icsData += `
  182. BEGIN:VEVENT
  183. DTSTART;TZID=Asia/Shanghai:${getSessionBeginTime(weekday, session, weekSpec === 'even', weeks[0])}
  184. DTEND;TZID=Asia/Shanghai:${getSessionEndTime(weekday, session, weekSpec === 'even', weeks[0])}
  185. DTSTAMP:${new Date().format('yyyyMMddThhmmss')}
  186. UID:${id()}@clansty.com
  187. SUMMARY:${name}
  188. DESCRIPTION:${teacher}\\n${coClass}\\n\\n${raw}
  189. LOCATION:${place}
  190. RRULE:FREQ=WEEKLY;WKST=MO;INTERVAL=${weekSpec === 'all' ? 1 : 2};BYDAY=${WEEKDAYS[weekday]
  191. };COUNT=${Math.floor(weekSpec === 'all' ? lessonWeeks : lessonWeeks / 2)}
  192. END:VEVENT`
  193. }
  194.  
  195. // 节次
  196. for (let session = 0; session < timeTable.length; session++) {
  197. // 星期几
  198. for (let weekday = 0; weekday < timeTable[session].length; weekday++) {
  199. const lesson = timeTable[session][weekday];
  200. lesson && writeLesson(lesson, weekday, session);
  201. }
  202. }
  203.  
  204. icsData += '\nEND:VCALENDAR';
  205. return icsData;
  206. }
  207.  
  208. function downloadText(content, filename) {
  209. const blob = new Blob([content]);
  210. const url = URL.createObjectURL(blob);
  211. const link = document.createElement('a');
  212. link.href = url;
  213. link.download = filename;
  214. link.click();
  215. }
  216.  
  217. function doGetIcs() {
  218. const title = getPageInfo();
  219. const icsContent = convertTimeTableToRfc5545(title, getLessonInfos());
  220. downloadText(icsContent, `${title}-${new Date().format('yyyyMMddhhmmss')}.ics`);
  221. }
  222.  
  223. function injectExportButton() {
  224. const btn = document.createElement('input');
  225. btn.type = 'button';
  226. btn.value = '导出为 iCal';
  227. btn.onclick = doGetIcs;
  228.  
  229. $('input#Button1').after(btn);
  230. }
  231.  
  232. injectExportButton();
  233. })(jQuery)

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址