-
-
Save lekoOwO/a2d42bb44b651ebc9b2335ddd92111fb to your computer and use it in GitHub Desktop.
交大課表 -> Google 日曆
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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(); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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