USTC Courses to ICS

Convert schedule to ICS file

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         USTC Courses to ICS
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Convert schedule to ICS file
// @match        https://jw.ustc.edu.cn/for-std/course-select/*/select
// @grant        none
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // 提取页面信息的函数
    function extractPageInfo() {
        const pageHTML = document.body.innerHTML;
        const extractValue = (regex) => {
            const match = pageHTML.match(regex);
            return match && match[1];
        };

        return {
            studentId: extractValue(/studentId:\s*(\d+)/),
            turnId: extractValue(/turnId:\s*(\d+)/),
            fetchSelectedCoursesUrl: extractValue(/fetchSelectedCourses:\s*"([^"]*)"/)
        };
    }

    // 获取选项的函数
    async function getOptions() {
        const pageInfo = extractPageInfo();
        return {
            studentId: pageInfo.studentId,
            turnId: pageInfo.turnId,
            url: {
                fetchSelectedCourses: pageInfo.fetchSelectedCoursesUrl
            }
        };
    }

    // 获取已选课程的函数
    async function fetchSelectedLessons(options) {
        return new Promise((resolve, reject) => {
            $.ajax({
                url: options.url.fetchSelectedCourses,
                type: 'post',
                data: {
                    studentId: options.studentId,
                    turnId: options.turnId
                },
                success: resolve,
                error: reject
            });
        });
    }

    // 获取课程表的函数
    async function getSchedule() {
        try {
            const options = await getOptions();
            const selectedLessons = await fetchSelectedLessons(options);

            const data = {
                lessonIds: selectedLessons.map(lesson => lesson.id),
                studentId: options.studentId
            };

            return new Promise((resolve, reject) => {
                $.ajax({
                    url: `${window.CONTEXT_PATH}/ws/schedule-table/datum`,
                    type: 'post',
                    contentType: 'application/json',
                    data: JSON.stringify(data),
                    success: (res) => resolve(res),
                    error: reject
                });
            });
        } catch (error) {
            console.error("Error fetching schedule:", error);
            throw error;
        }
    }

    function createICS(data) {
        let icsContent = [
            'BEGIN:VCALENDAR',
            'VERSION:2.0',
            'PRODID:-//[email protected]//Schedule to ICS Converter//EN',
            'CALSCALE:GREGORIAN',
            'METHOD:PUBLISH',
            'X-WR-TIMEZONE:Asia/Shanghai'
        ];

        for (const schedule of data.result.scheduleList) {
            const event = ['BEGIN:VEVENT'];

            // Set event summary
            const lesson = data.result.lessonList.find(l => l.id === schedule.lessonId);
            if (lesson) {
                event.push(`SUMMARY:${lesson.courseName} - ${schedule.personName}`);
            } else {
                event.push(`SUMMARY:课程 - ${schedule.personName}`);
            }

            // Set event location
            if (schedule.room) { // Not online teaching
                const location = schedule.room.nameZh;
                const building = schedule.room.building.nameZh;
                const campus = schedule.room.building.campus.nameZh;
                event.push(`LOCATION:${campus} ${building} ${location}`);
            } else { // Online teaching
                event.push(`LOCATION:${schedule.customPlace}`);
            }

            // Set event start and end time
            const date = new Date(schedule.date);
            const startHour = Math.floor(parseInt(schedule.startTime) / 100);
            const startMinute = parseInt(schedule.startTime) % 100;
            const endHour = Math.floor(parseInt(schedule.endTime) / 100);
            const endMinute = parseInt(schedule.endTime) % 100;

            const startTime = new Date(date.setHours(startHour, startMinute));
            const endTime = new Date(date.setHours(endHour, endMinute));

            event.push(`DTSTART:${formatDate(startTime)}`);
            event.push(`DTEND:${formatDate(endTime)}`);

            // Add description
            const description = `教师: ${schedule.personName}\\n课程ID: ${schedule.lessonId}`;
            event.push(`DESCRIPTION:${description}`);

            event.push('END:VEVENT');
            icsContent = icsContent.concat(event);
        }

        icsContent.push('END:VCALENDAR');
        return icsContent.join('\r\n');
    }

    function formatDate(date) {
        const pad = (n) => n.toString().padStart(2, '0');
        return [
            date.getFullYear(),
            pad(date.getMonth() + 1),
            pad(date.getDate()),
            'T',
            pad(date.getHours()),
            pad(date.getMinutes()),
            pad(date.getSeconds()),
        ].join('');
    }

    async function generateICS() {
        try {
            const schedule = await getSchedule();
            const icsData = createICS(schedule);

            if (!icsData) {
                throw new Error("Failed to create ICS data");
            }

            const blob = new Blob([icsData], { type: 'text/calendar;charset=utf-8' });
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = 'schedule.ics';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        } catch (error) {
            console.error("Error generating ICS file:", error);
        }
    }

    // 创建一个按钮来触发 ICS 生成
    function createButton() {
        const button = document.createElement('button');
        button.textContent = '导出ICS';
        button.className = 'btn btn-primary'
        button.addEventListener('click', generateICS);

        // 创建一个观察器实例
        const observer = new MutationObserver((mutations) => {
            for (let mutation of mutations) {
                if (mutation.type === 'childList') {
                    const container = document.querySelector('.col.col-sm-3.text-right');
                    if (container) {
                        container.appendChild(button);
                        observer.disconnect(); // 停止观察
                        return;
                    }
                }
            }
        });

        // 配置观察选项
        const config = { childList: true, subtree: true };

        // 开始观察目标节点的变化
        observer.observe(document.body, config);

        // 设置一个超时,以防容器never出现
        setTimeout(() => {
            observer.disconnect();
            if (!button.parentNode) {
                document.body.appendChild(button);
            }
        }, 10000); // 10秒后超时
    }

    window.addEventListener('load', createButton);

})();