Skip to content

Instantly share code, notes, and snippets.

@lekoOwO
Last active June 10, 2023 12:46
Show Gist options
  • Save lekoOwO/a2d42bb44b651ebc9b2335ddd92111fb to your computer and use it in GitHub Desktop.
Save lekoOwO/a2d42bb44b651ebc9b2335ddd92111fb to your computer and use it in GitHub Desktop.
交大課表 -> Google 日曆
(async() => {
const startDate = prompt("請輸入學期開始日期(YYYY/MM/DD)");
const stopDate = prompt("請輸入學期結束日期(YYYY/MM/DD)"); // https://www.nycu.edu.tw/calendar/
const now = new Date();
const DAYS = ["U", "M", "T", "W", "R", "F", "S"];
const TIMES = {
y: [[6, 0], [6, 50]],
z: [[7, 0], [7, 50]],
1: [[8, 0], [8, 50]],
2: [[9, 0], [9, 50]],
3: [[10, 10], [11, 0]],
4: [[11, 10], [12, 0]],
n: [[12, 20], [13, 10]],
5: [[13, 20], [14, 10]],
6: [[14, 20], [15, 10]],
7: [[15, 30], [16, 20]],
8: [[16, 30], [17, 20]],
9: [[17, 30], [18, 20]],
a: [[18, 30], [19, 20]],
b: [[19, 30], [20, 20]],
c: [[20, 30], [21, 20]],
d: [[21, 30], [22, 20]],
};
async function getSchedule() {
const re = /.*userToken=([^;]+);?.*/;
const jwt = document.cookie.match(re)[1];
const resp = await fetch("https://portal.nycu.edu.tw/portal/api/getClassSchedule", {
"headers": {
"accept": "application/json",
"content-type": "application/x-www-form-urlencoded",
},
"referrer": "https://portal.nycu.edu.tw/",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": "undefined&token=" + jwt,
"method": "POST",
"mode": "cors",
"credentials": "include"
});
const data = await resp.json();
return data.classSchedule.result;
}
function getICSDate(date) {
const month = (date.getUTCMonth() + 1).toString();
const day = date.getUTCDate().toString();
const pre = date.getUTCFullYear().toString() +
((date.getUTCMonth() + 1) < 10 ? "0"+month : month) +
(date.getUTCDate() < 10 ? "0"+day : day);
const hour = date.getUTCHours().toString();
const minute = date.getUTCMinutes().toString();
const second = date.getUTCSeconds().toString();
const post = (date.getUTCHours() < 10 ? "0"+hour : hour) +
(date.getUTCMinutes() < 10 ? "0"+minute : minute) +
(date.getUTCSeconds() < 10 ? "0"+second : second);
return `${pre}T${post}Z`;
}
function courseLocationToChinese(locationStr){
const locationMap = {"YN":"護理館","YE":"實驗大樓","YR":"守仁樓","YS":"醫學二館","YB":"生醫工程館","YX":"知行樓","YD":"牙醫館","YK":"傳統醫學大樓(甲棟)","YT":"教學大樓","YM":"醫學館","YL":"圖書資源暨研究大樓","YA":"活動中心","YH":"致和樓","YC":"生物醫學大樓","AS":"中央研究院","PH":"臺北榮民總醫院","CH":"台中榮民總醫院","KH":"高雄榮民總醫院","C":"竹銘館","E":"教學大樓","LI":"實驗一館","BA":"生科實驗館","BB":"生科實驗二館","BI":"賢齊館","EA":"工程一館","EB":"工程二館","EC":"工程三館","ED":"工程四館","EE":"工程五館","EF":"工程六館","M":"管理館","MB":"管理二館","SA":"科學一館","SB":"科學二館","SC":"科學三館","AC":"學生活動中心","A":"綜合一館","AB":"綜合一館地下室","HA":"人社一館","F":"人社二館","HC":"人社三館","CY":"交映樓","EO":"田家炳光電大樓","EV":"環工館","CS":"資訊技術服務中心","ES":"電子資訊中心","CE":"土木結構實驗室","TA":"會議室","TD":"一般教室","TC":"演講廳","CM":"奇美樓","HK":"客家大樓"};
const classroom = locationStr.match(/^([A-Z]+)\d+\[[A-Z]+\]$/)?.[1];
if (!classroom) return locationStr;
if (!locationMap.hasOwnProperty(classroom)) return locationStr;
return `${locationStr} (${locationMap[classroom]})`
}
function getCourseDates(courseTime){
// coureTime: "W34"
const day = DAYS.findIndex(d => d === courseTime[0]);
let startTime = [23, 59];
let endTime = [0, 0];
for(let i = 1; i < courseTime.length; i++){
const time = TIMES[courseTime[i]];
if(time[0][0] < startTime[0]){
startTime = time[0];
}
if(time[1][0] > endTime[0]){
endTime = time[1];
}
}
const s = new Date(startDate);
s.setDate(s.getDate() + (day + 7 - s.getDay()) % 7);
s.setHours(startTime[0]);
s.setMinutes(startTime[1]);
s.setSeconds(0);
const e = new Date(startDate);
e.setDate(e.getDate() + (day + 7 - e.getDay()) % 7);
e.setHours(endTime[0]);
e.setMinutes(endTime[1]);
e.setSeconds(0);
return [s, e];
}
function createCourseEvent(course) {
let result = "";
for(const courseCombine of course.crstimecombine) {
const [s, e] = getCourseDates(courseCombine.time);
result += `
BEGIN:VEVENT
DTSTART:${getICSDate(s)}
DTEND:${getICSDate(e)}
RRULE:FREQ=WEEKLY;UNTIL=${getICSDate(new Date(stopDate))}
DTSTAMP:${getICSDate(now)}
UID:${course.pno}.${course.stdno}-${course.acy}-${now.getTime()}@nycu.edu.tw
CREATED:${getICSDate(now)}
DESCRIPTION:授課教師: ${course.crsteacher}\\n課程網址: ${course.url}
LAST-MODIFIED:${getICSDate(now)}
LOCATION:${courseLocationToChinese(courseCombine.room)}
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:${course.crsname}
TRANSP:OPAQUE
X-MOZ-GOOGLE-HTML-DESCRIPTION:true
END:VEVENT`;
}
return result;
}
const schedule = await getSchedule();
const ics = `BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:
X-WR-TIMEZONE:Asia/Taipei
${schedule.map(createCourseEvent).join("").trim()}
END:VCALENDAR
`.split("\n").map(x => x.trim()).join("\n");
// download ics as file
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([ics], {type: "text/calendar"}));
a.download = `${schedule[0].acy}交大課表-${schedule[0].stdno}.ics`;
a.click();
a.remove();
})();
// ==UserScript==
// @name 交大課表 -> ICS
// @namespace http://leko.moe
// @version 1.5
// @description
// @author You
// @source https://gist.github.com/lekoOwO/a2d42bb44b651ebc9b2335ddd92111fb
// @downloadURL https://gist.github.com/lekoOwO/a2d42bb44b651ebc9b2335ddd92111fb/raw/nycu_ics.user.js
// @updateURL https://gist.github.com/lekoOwO/a2d42bb44b651ebc9b2335ddd92111fb/raw/nycu_ics.user.js
// @match https://portal.nycu.edu.tw/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
if (document.location.hash !== '#/schedule/index') return;
const SCRIPT_NAME = "交大課表 -> ICS";
function log(...args) {
console.log(`[${SCRIPT_NAME}]`, ...args);
}
const styles = "@import url(' https://cdnjs.cloudflare.com/ajax/libs/tocas/4.1.0/tocas.min.css ');";
const tocas_ui = document.createElement('link');
tocas_ui.rel = 'stylesheet';
tocas_ui.href = 'data:text/css,' + encodeURIComponent(styles);
document.getElementsByTagName("head")[0].appendChild(tocas_ui);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
document.addEventListener('DOMContentLoaded', async () => {
log("Started.");
const getBg = () => document.querySelector(".app-main .bg");
while (!getBg()) {
await sleep(200);
}
getBg().insertAdjacentHTML("afterbegin", `
<div class="ts-row is-end-aligned" style="margin-top: 10px;">
<div class="column">
<button class="ts-button is-start-icon is-light" style="margin-right: 2px;" id="download-ics">
<span class="ts-icon is-download-icon"></span>
下載 ICS
</button>
</div>
</div>
`);
document.querySelector(".app-main .bg .container").style.margin = "10px auto";
document.getElementById("download-ics").addEventListener("click", main);
});
const main = (async () => {
const startDate = prompt("請輸入學期開始日期(YYYY/MM/DD)");
const stopDate = prompt("請輸入學期結束日期(YYYY/MM/DD)"); // https://www.nycu.edu.tw/calendar/
const now = new Date();
const DAYS = ["U", "M", "T", "W", "R", "F", "S"];
const TIMES = {
y: [[6, 0], [6, 50]],
z: [[7, 0], [7, 50]],
1: [[8, 0], [8, 50]],
2: [[9, 0], [9, 50]],
3: [[10, 10], [11, 0]],
4: [[11, 10], [12, 0]],
n: [[12, 20], [13, 10]],
5: [[13, 20], [14, 10]],
6: [[14, 20], [15, 10]],
7: [[15, 30], [16, 20]],
8: [[16, 30], [17, 20]],
9: [[17, 30], [18, 20]],
a: [[18, 30], [19, 20]],
b: [[19, 30], [20, 20]],
c: [[20, 30], [21, 20]],
d: [[21, 30], [22, 20]],
};
async function getSchedule() {
const re = /.*userToken=([^;]+);?.*/;
const jwt = document.cookie.match(re)[1];
const resp = await fetch("https://portal.nycu.edu.tw/portal/api/getClassSchedule", {
"headers": {
"accept": "application/json",
"content-type": "application/x-www-form-urlencoded",
},
"referrer": "https://portal.nycu.edu.tw/",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": "undefined&token=" + jwt,
"method": "POST",
"mode": "cors",
"credentials": "include"
});
const data = await resp.json();
return data.classSchedule.result;
}
function getICSDate(date) {
const month = (date.getUTCMonth() + 1).toString();
const day = date.getUTCDate().toString();
const pre = date.getUTCFullYear().toString() +
((date.getUTCMonth() + 1) < 10 ? "0" + month : month) +
(date.getUTCDate() < 10 ? "0" + day : day);
const hour = date.getUTCHours().toString();
const minute = date.getUTCMinutes().toString();
const second = date.getUTCSeconds().toString();
const post = (date.getUTCHours() < 10 ? "0" + hour : hour) +
(date.getUTCMinutes() < 10 ? "0" + minute : minute) +
(date.getUTCSeconds() < 10 ? "0" + second : second);
return `${pre}T${post}Z`;
}
function courseLocationToChinese(locationStr){
const locationMap = {"YN":"護理館","YE":"實驗大樓","YR":"守仁樓","YS":"醫學二館","YB":"生醫工程館","YX":"知行樓","YD":"牙醫館","YK":"傳統醫學大樓(甲棟)","YT":"教學大樓","YM":"醫學館","YL":"圖書資源暨研究大樓","YA":"活動中心","YH":"致和樓","YC":"生物醫學大樓","AS":"中央研究院","PH":"臺北榮民總醫院","CH":"台中榮民總醫院","KH":"高雄榮民總醫院","C":"竹銘館","E":"教學大樓","LI":"實驗一館","BA":"生科實驗館","BB":"生科實驗二館","BI":"賢齊館","EA":"工程一館","EB":"工程二館","EC":"工程三館","ED":"工程四館","EE":"工程五館","EF":"工程六館","M":"管理館","MB":"管理二館","SA":"科學一館","SB":"科學二館","SC":"科學三館","AC":"學生活動中心","A":"綜合一館","AB":"綜合一館地下室","HA":"人社一館","F":"人社二館","HC":"人社三館","CY":"交映樓","EO":"田家炳光電大樓","EV":"環工館","CS":"資訊技術服務中心","ES":"電子資訊中心","CE":"土木結構實驗室","TA":"會議室","TD":"一般教室","TC":"演講廳","CM":"奇美樓","HK":"客家大樓"};
const classroom = locationStr.match(/^([A-Z]+)\d+\[[A-Z]+\]$/)?.[1];
if (!classroom) return locationStr;
if (!locationMap.hasOwnProperty(classroom)) return locationStr;
return `${locationStr} (${locationMap[classroom]})`
}
function getCourseDates(courseTime) {
// coureTime: "W34"
const day = DAYS.findIndex(d => d === courseTime[0]);
let startTime = [23, 59];
let endTime = [0, 0];
for (let i = 1; i < courseTime.length; i++) {
const time = TIMES[courseTime[i]];
if (time[0][0] < startTime[0]) {
startTime = time[0];
}
if (time[1][0] > endTime[0]) {
endTime = time[1];
}
}
const s = new Date(startDate);
s.setDate(s.getDate() + (day + 7 - s.getDay()) % 7);
s.setHours(startTime[0]);
s.setMinutes(startTime[1]);
s.setSeconds(0);
const e = new Date(startDate);
e.setDate(e.getDate() + (day + 7 - e.getDay()) % 7);
e.setHours(endTime[0]);
e.setMinutes(endTime[1]);
e.setSeconds(0);
return [s, e];
}
function createCourseEvent(course) {
let result = "";
for (const courseCombine of course.crstimecombine) {
const [s, e] = getCourseDates(courseCombine.time);
result += `
BEGIN:VEVENT
DTSTART:${getICSDate(s)}
DTEND:${getICSDate(e)}
RRULE:FREQ=WEEKLY;UNTIL=${getICSDate(new Date(stopDate))}
DTSTAMP:${getICSDate(now)}
UID:${course.pno}.${course.stdno}-${course.acy}-${now.getTime()}@nycu.edu.tw
CREATED:${getICSDate(now)}
DESCRIPTION:授課教師: ${course.crsteacher}\\n課程網址: ${course.url}
LAST-MODIFIED:${getICSDate(now)}
LOCATION:${courseLocationToChinese(courseCombine.room)}
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:${course.crsname}
TRANSP:OPAQUE
X-MOZ-GOOGLE-HTML-DESCRIPTION:true
END:VEVENT`;
}
return result;
}
const schedule = await getSchedule();
const ics = `BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:
X-WR-TIMEZONE:Asia/Taipei
${schedule.map(createCourseEvent).join("").trim()}
END:VCALENDAR
`.split("\n").map(x => x.trim()).join("\n");
// download ics as file
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([ics], { type: "text/calendar" }));
a.download = `${schedule[0].acy}交大課表-${schedule[0].stdno}.ics`;
a.click();
a.remove();
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment