Greasy Fork

来自缓存

Greasy Fork is available in English.

同步深大课表到小爱

Sync your SZU course table to Mi AI

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         同步深大课表到小爱
// @namespace    sync-szu-ct-to-mi-ai
// @version      1.0
// @description  Sync your SZU course table to Mi AI
// @author       Strick Chan
// @match        *://ehall.szu.edu.cn/jwapp/sys/wdkb/*
// @grant        GM_xmlhttpRequest
// @connect      https://i.ai.mi.com
// ==/UserScript==

/**
 * @typedef {{
 *   name: string;
 *   teacher: string;
 *   sections: string;
 *   weeks: string;
 *   day: number;
 *   position: string;
 *   style: {
 *     color: string;
 *     background: string;
 *   };
 * }} CourseInfo
 */

/**
 * @typedef {{
 *   userId: number;
 *   deviceid: string;
 *   ctId: number;
 * }} UserInfo
 */

/**
 * @typedef {{
 *   desc: string;
 *   data: any;
 *   code: number;
 * }} Result
 */

(function () {
  "use strict";

  const button = document.createElement("button");
  button.id = "sync_button";
  button.className = "bh-btn bh-btn-small bh-btn-default";
  button.textContent = "同步到小爱";
  button.addEventListener("click", async () => {
    const link = prompt("请输入小爱课程表的「分享课表」链接:");
    const user = parseUserInfo(link);

    if (confirm("该操作会清空小爱课程表中已有的内容,是否继续?")) {
      try {
        const courseIds = await getCourseIds(user);
        await Promise.all(courseIds.map((id) => delCourse(user, id)));

        const courses = parseCourses();
        await Promise.all(courses.map((course) => addCourse(user, course)));

        alert("操作成功");
      } catch (_) {
        alert("操作失败,请检查控制台日志并联系开发者");
      }
    }
  });

  const checkExist = setInterval(() => {
    if ($(".bh-buttons").length) {
      $(".bh-buttons").append(button);
      clearInterval(checkExist);
    }
  }, 100);
})();

const headers = {
  "Accept": "application/json",
  "Content-Type": "application/json",
  "Origin": "https://i.ai.mi.com",
  "Host": "i.ai.mi.com",
};

/**
 * 获得所有已有的课程ID
 * @param {UserInfo} userInfo
 */
function getCourseIds(userInfo) {
  return new Promise((resolve, reject) => {
    const request = { ...userInfo };
    GM_xmlhttpRequest({
      url: `https://i.ai.mi.com/course-multi/table?${$.param(request)}`,
      method: "GET",
      headers,
      onload: (xhr) => {
        /** @type {Result} */
        const response = JSON.parse(xhr.responseText);
        console.log({ action: "getCourseIds", request, response });

        /** @type {number[]} */
        const courseIds = response.data.courses.map((item) => item.id);
        resolve(courseIds);
      },
      onerror: (err) => console.error(err),
    });
  });
}

/**
 * 删除一个课程记录
 * @param {UserInfo} userInfo
 * @param {number} courseId
 */
function delCourse(userInfo, courseId) {
  return new Promise((resolve, reject) => {
    const request = { ...userInfo, cId: courseId };
    GM_xmlhttpRequest({
      url: "https://i.ai.mi.com/course-multi/courseInfo",
      method: "DELETE",
      headers,
      data: JSON.stringify(request),
      onload: (xhr) => {
        /** @type {Result} */
        const response = JSON.parse(xhr.responseText);
        console.log({ action: "delCourse", request, response });

        /** @type {boolean} */
        const data = response.data;
        resolve(data);
      },
      onerror: (err) => console.error(err),
    });
  });
}

/**
 * 添加一个课程记录
 * @param {UserInfo} userInfo
 * @param {CourseInfo} courseInfo
 */
function addCourse(userInfo, courseInfo) {
  return new Promise((resolve, reject) => {
    const request = { ...userInfo, course: courseInfo };
    GM_xmlhttpRequest({
      url: "https://i.ai.mi.com/course-multi/courseInfo",
      method: "POST",
      headers,
      data: JSON.stringify(request),
      onload: (xhr) => {
        /** @type {Result} */
        const response = JSON.parse(xhr.responseText);
        console.log({ action: "addCourse", request, response });

        resolve(response.data);
      },
      onerror: (err) => console.error(err),
    });
  });
}

/**
 * 生成一个区间数组
 * @param {number} start
 * @param {number} end
 * @returns 区间数组 [start, end]
 */
function range(start, end) {
  return [...Array(end - start + 1).keys()].map((i) => i + start);
}

/**
 * 解析用户信息
 * @param {string} link
 */
function parseUserInfo(link) {
  const { searchParams } = new URL(
    link.replace("/#/", "/"),
  );

  /** @type {UserInfo} */
  const userInfo = {
    userId: parseInt(searchParams.get("userId")),
    deviceId: searchParams.get("deviceId"),
    ctId: parseInt(searchParams.get("ctId")),
  };

  return userInfo;
}

/**
 * 从网页解析课表信息
 */
function parseCourses() {
  /** @type {CourseInfo[]} */
  const courses = [];

  $(".mtt_arrange_item").each((_, card) => {
    /** @type {string} */
    const background = $(card)
      .attr("style").split(";")
      .filter((item) => item.includes("background-color"))[0]
      .split("background-color:")[1];

    /** @type {string[]} */
    const lines = [];

    $("div", card).each((_, line) => {
      lines.push($(line).text());
    });

    /** @type {CourseInfo} */
    const course = {
      name: lines[1],
      teacher: lines[2],
      sections: "",
      weeks: "",
      day: 0,
      position: "",
      style: JSON.stringify({ background, color: "#000000" }),
    };

    /** @type {string[]} */
    const tempWeeks = [];

    lines[3].split(",").forEach((item) => {
      if (item.includes("周")) {
        tempWeeks.push(item);
      } else if (item.includes("星期")) {
        course.day = parseInt(item.charAt(item.length - 1));
      } else if (item.includes("节")) {
        const [start, end] = item.replace("节", "").split("-");
        course.sections = range(parseInt(start), parseInt(end)).toString();
      } else {
        course.position = item;
      }
    });

    tempWeeks.forEach((item) => {
      if (item.includes("-")) {
        const filter = (() => {
          if (item.includes("单")) return (n) => n % 2 === 1;
          if (item.includes("双")) return (n) => n % 2 === 0;
          return (_) => true;
        })();
        const [start, end] = item.replace("周", "").split("-");
        const array = range(parseInt(start), parseInt(end)).filter(filter);
        course.weeks += (course.weeks !== "" ? "," : "") + array.toString();
      } else {
        course.weeks += (course.weeks !== "" ? "," : "") +
          item.replace("周", "");
      }
    });

    courses.push(course);
  });

  return courses;
}