厦门大学课程表导出助手-ICS

导出厦门大学课表为 ICS 文件

// ==UserScript==
// @name         厦门大学课程表导出助手-ICS
// @version      2024-01-4
// @description  导出厦门大学课表为 ICS 文件
// @author       yangqian
// @match        https://jw.xmu.edu.cn/gsapp/sys/wdkbapp/*
// @grant        none
// @namespace https://greasyfork.org/users/1392448
// ==/UserScript==

(function() {
    'use strict';
    const START_DATE = '2025-02-17' //第一个周一

    const waitUntilElementPresent = (cssLocator, callback) => {
        const checkExist = setInterval(() => {
            if (document.querySelector(cssLocator)) {
                clearInterval(checkExist);
                callback();
                return;
            }
        }, 100);
    };

    waitUntilElementPresent(".arrage", () => {
        const tab = document.getElementById("xsXx").firstElementChild;
        const getButton = document.createElement('a');
        getButton.innerHTML = "获取ICS";
        getButton.classList.add("bh-btn-default", "bh-btn");
        tab.appendChild(getButton);
        getButton.addEventListener('click', main);
    });

    async function main() {
        const HEADERS = [
            "BEGIN:VCALENDAR",
            "METHOD:PUBLISH",
            "VERSION:2.0",
            "X-WR-CALNAME:课表",
            "X-WR-TIMEZONE:Asia/Shanghai",
            "CALSCALE:GREGORIAN",
            "BEGIN:VTIMEZONE",
            "TZID:Asia/Shanghai",
            "END:VTIMEZONE"
        ];

        const FOOTERS = ["END:VCALENDAR"];

        const timetable = [
            [8, 0],
            [10, 10],
            [14, 30],
            [16, 40],
            [19, 10]
        ];

        function getStartTime(jc) {
            const time = timetable[jc - 1];
            return `${time[0].toString().padStart(2, '0')}${time[1].toString().padStart(2, '0')}00`;
        }

        function getEndTime(jc) {
            const time = timetable[jc - 1].map((num, index) => num + [1, 40][index]);
            if (time[1] >= 60) {
                time[0] += 1;
                time[1] -= 60;
            }
            return `${time[0].toString().padStart(2, '0')}${time[1].toString().padStart(2, '0')}00`;
        }

        function getDates(day, week) {
            const startDate = new Date(START_DATE);
            startDate.setDate(startDate.getDate() + day + 7 * week);
            return `${startDate.getFullYear()}${(startDate.getMonth() + 1).toString().padStart(2, '0')}${startDate.getDate().toString().padStart(2, '0')}`;
        }

        function getDT(jc, day, week) {
            const date = getDates(day, week);
            const start = getStartTime(jc);
            const end = getEndTime(jc);
            return `DTSTART;TZID=Asia/Shanghai:${date}T${start}\nDTEND;TZID=Asia/Shanghai:${date}T${end}`;
        }

        const tds = document.getElementsByTagName("td");
        const classes = [];

        for (const td of tds) {
            if (td.textContent && td.getAttribute("jc") && td.getAttribute("jc") % 2 !== 0) {
                const weeksText = td.children[0].children[1].textContent.trim();
                const weeksMatch = weeksText.match(/(\d+)-(\d+)(单|双)?周/);
                console.log(weeksMatch);
                if (!weeksMatch) continue;

                const [_, startWeek, endWeek, weekType] = weeksMatch;
                let weekCount = endWeek - startWeek + 1;
                if (weekType === "单" || weekType === "双") {
                    weekCount = (endWeek - startWeek) / 2 + 1
                }
                const interval = weekType === "单" || weekType === "双" ? ";INTERVAL=2" : "";
                const jc = (parseInt(td.getAttribute("jc"), 10) + 1) / 2;
                const day = parseInt(td.getAttribute("xq"), 10) - 1;

                const event = [
                    "BEGIN:VEVENT",
                    `SUMMARY:${td.children[0].children[2].textContent.replace(/\(.*?\)/g, '')}`,
                    `DESCRIPTION:${td.children[0].children[3].textContent}`,
                    getDT(jc, day, startWeek - 1),
                    `LOCATION:${td.children[0].children[4].textContent.replace(/(.*?)/g, '')}`,
                    `RRULE:FREQ=WEEKLY;COUNT=${weekCount}${interval}`,
                    "END:VEVENT"
                ];

                classes.push(event.join('\n'));
            }
        }

        const textContent = [...HEADERS, ...classes, ...FOOTERS].join('\n');
        const blob = new Blob([textContent], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = `schedule_${new Date().toISOString().slice(0, 10)}.ics`;
        document.body.appendChild(a);
        a.click();

        URL.revokeObjectURL(url);
        document.body.removeChild(a);
    }
})();