西大课表转ics

西南大学课程表转.ics

// ==UserScript==
// @name         西大课表转ics
// @namespace    https://lers.top
// @version      1.1
// @description  西南大学课程表转.ics
// @author       Lers梦魔
// @match        http://jw-swu-edu-cn.sangfor.vpn.swu.edu.cn:8118/jwglxt/kbcx/*
// @match        http://jw.swu.edu.cn/jwglxt/kbcx/*
// @icon         https://img.afqaq.com/images/2022/03/14/favicon-32x32.png
// @license BSD
// @run-at      document-start
// @grant       none
// ==/UserScript==

//---------------------------------静态配置------------------------------------------
// 学期第一周星期一
var TERM_START = "2022-09-05"
// ics文件开头
const ICS_Start = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//lers梦魔//SWU Ics Exporter v1.1//CN\n"
// ics文件时区
const ICS_Timezone1 = "BEGIN:VTIMEZONE\nTZID:Asia/Shanghai\n"
const ICS_Timezone2 = "LAST-MODIFIED:20220816T024022Z\nTZURL:http://tzurl.org/zoneinfo-outlook/Asia/Shanghai\nX-LIC-LOCATION:Asia/Shanghai\n"
const ICS_Timezone3 = "BEGIN:STANDARD\nTZNAME:CST\nTZOFFSETFROM:+0800\nTZOFFSETTO:+0800\nDTSTART:19700101T000000\nEND:STANDARD\nEND:VTIMEZONE\n"
// ics文件结尾
const ICS_End = "END:VCALENDAR"
// 课程时间安排
const Timetable = {
  // 节数:[开始时间,结束时间]
  "1": ["08:00", "08:45"],
  "2": ["08:55", "09:40"],
  "3": ["10:00", "10:45"],
  "4": ["10:55", "11:40"],
  "5": ["12:10", "12:55"],
  "6": ["13:05", "13:50"],
  "7": ["14:00", "14:45"],
  "8": ["14:55", "15:40"],
  "9": ["15:50", "16:35"],
  "10": ["16:55", "17:40"],
  "11": ["17:50", "18:35"],
  "12": ["19:20", "20:05"],
  "13": ["20:15", "21:00"],
  "14": ["21:10", "21:55"]
}
// 总周数
const TOTALWEEK = 20

//---------------------------------全局变量------------------------------------------
// 课表解析载体
var TABLE = {
  "termStart": "", // 学期第一天星期一
  "termEnd": "", // 学期最后一天
  "kbList": []
}
//---------------------------------ics生成函数---------------------------------------
class ICS {
  constructor() {
    this.table = TABLE // 解析后课表内容
    this.dtstamp = TABLE.termStart + "T063157z\n"
    this.ICS = ""
  }
  format_Class (element) {
    var body = "BEGIN:VEVENT\nDTSTAMP:" + this.dtstamp
    body += "UID:" + element.UID + "\n"
    body += "SUMMARY:" + element.SUMMARY + "\n"
    body += "DTSTART;TZID=" + element.DTSTART + "\n"
    body += "DTEND;TZID=" + element.DTEND + "\n"
    if (element.FREQ != "x") {
      body += "RRULE:FREQ=" + element.FREQ + ";UNTIL=" + element.UNTIL + ";INTERVAL=" + element.INTERVAL + "\n"
    }
    body += "LOCATION:" + element.LOCATION + "\n"
    body += "DESCRIPTION:" + element.DESCRIPTION1 + "\n"
    body += "BEGIN:VALARM\nACTION:DISPLAY\nTRIGGER;RELATED=START:-PT20M\n"
    body += "DESCRIPTION:" + element.DESCRIPTION2 + "\n"
    body += "END:VALARM\nEND:VEVENT\n"
    return body
  }
  init_ICS () {
    this.ICS += ICS_Start + ICS_Timezone1 + ICS_Timezone2 + ICS_Timezone3
    this.table.kbList.forEach(element => {
      this.ICS += this.format_Class(element)
    })
    this.ICS += ICS_End
  }
  // 下载函数
  download_ICS (fileName, content) {
    var aTag = document.createElement('a')
    var blob = new Blob([content])
    aTag.download = fileName
    aTag.href = URL.createObjectURL(blob)
    aTag.click()
    URL.revokeObjectURL(blob)
  }

}

//---------------------------------工具函数------------------------------------------
// hook函数
function addXMLRequestCallback (callback) {
  var oldSend, i
  if (XMLHttpRequest.callbacks) {
    //判断XMLHttpRequest对象下是否存在回调列表,存在就push一个回调的函数
    XMLHttpRequest.callbacks.push(callback)
  } else {
    XMLHttpRequest.callbacks = [callback]
    //如果不存在则在xmlhttprequest函数下创建一个回调列表
    oldSend = XMLHttpRequest.prototype.send
    //获取旧xml的send函数,并对其进行劫持
    XMLHttpRequest.prototype.send = function () {
      for (i = 0; i < XMLHttpRequest.callbacks.length; i++) {
        XMLHttpRequest.callbacks[i](this)
      }
      //循环回调xml内的回调函数
      oldSend.apply(this, arguments)
      //由于我们获取了send函数的引用,并且复写了send函数,这样我们在调用原send的函数的时候,需要对其传入引用,而arguments是传入的参数
    }
  }
}
// 课表信息解析函数
function timetableFormat (table) {
  TABLE.termStart = dateLater(TERM_START, 0)
  TABLE.termEnd = dateLater(TERM_START, TOTALWEEK * 7)
  var data = JSON.parse(table).kbList
  // console.log(data)
  var freq, interval, until, dtstart1, dtstart2, dtend1, dtend2
  TABLE.kbList = []
  data.forEach(element => {
    var pushs = function (tar) {
      if (tar.zcd.includes("-")) {
        freq = tar.zcd.split('周')[0].split('-')
        // 单双周判断
        if (tar.zcd.includes("单")) {
          interval = 2
        }
        else if (tar.zcd.includes("双")) {
          interval = 2
        }
        else {
          interval = 1
        }
        dtend1 = dtstart1 = dateLater(TERM_START, ((freq[0] - 1) * 7) + parseInt(element.xqj - 1))
        until = dateLater(TERM_START, ((freq[1] - 1) * 7) + parseInt(element.xqj)) + "T120000Z"
        freq = "WEEKLY"
      }
      else {
        freq = "x"
        interval = 0
        dtend1 = dtstart1 = dateLater(TERM_START, tar.xqj - 1 + (tar.zcd.replace("周", "") - 1) * 7)
        console.log(dtend1)
        until = dateLater(TERM_START, parseInt(tar.xqj) + parseInt(tar.zcd.replace("周", "")) - 1) * 7 + "T120000Z"
      }
      // 课程时间生成
      if (tar.jcs.includes('-')) {
        dtstart2 = Timetable[tar.jcs.split('-')[0]][0].replace(':', '')
        dtend2 = Timetable[tar.jcs.split('-')[1]][1].replace(':', '')
      }
      else {
        dtstart2 = Timetable[tar.jcs][0].replace(':', '')
        dtend2 = Timetable[tar.jcs][1].replace(':', '')
      }
      TABLE.kbList.push(
        {
          "SUMMARY": tar.kcmc,
          "FREQ": freq,
          "UNTIL": until,
          "INTERVAL": interval,
          "DTSTART": "Asia/Shanghai:" + dtstart1 + "T" + dtstart2 + "00",
          "DTEND": "Asia/Shanghai:" + dtend1 + "T" + dtend2 + "00",
          "UID": uuid(),
          "DESCRIPTION1": "第" + tar.jc + "\\n" + tar.xqmc + " " + tar.cdmc + "\\n" + element.xm,
          "DESCRIPTION2": tar.kcmc + "@" + tar.xqmc + " " + tar.cdmc,
          "LOCATION": tar.xqmc + " " + tar.cdmc + " " + tar.xm
        }
      )
    }
    if (element.zcd.includes(",")) {
      var tmp = element
      var zcd = tmp.zcd.split(',')
      zcd.forEach(tar => {
        tmp.zcd = tar
        pushs(tmp)
      })
    }
    else {
      pushs(element)
    }
  })
}
// UUID生成函数
function uuid () {
  function S4 () {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
  }
  return (S4() + S4() + "-" + S4() + "-LersNightmare-" + S4() + "-" + S4() + S4() + S4())
}
// 计算n天后日期(传入 2022-09-05,5)
function dateLater (datatemp, days) {
  days = parseInt(days)
  let nowTime = strToDate(datatemp) //当前时间戳
  let futureTime = Math.abs(nowTime) + (days * 24 * 60 * 60 * 1000) //days天后的时间戳
  let futureDate = new Date(futureTime)
  let year = futureDate.getFullYear()
  let month = futureDate.getMonth() + 1
  if (month < 10) month = "0" + month
  let date = futureDate.getDate()
  if (date < 10) date = "0" + date
  return (year.toString() + month.toString() + date.toString())
}
// 将yyy-MM-dd格式的字符串转换为日期
function strToDate (datestr) {
  var dateStr = datestr.replace(/-/g, "/")//现将yyyy-MM-dd类型转换为yyyy/MM/dd
  var dateTime = Date.parse(dateStr)//将日期字符串转换为表示日期的秒数
  //注意:Date.parse(dateStr)默认情况下只能转换:月/日/年 格式的字符串,但是经测试年/月/日格式的字符串也能被解析
  var data = new Date(dateTime)//将日期秒数转换为日期格式
  return data
}
// 添加下载按键和日期选择器函数
function addDownload (xhr) {
  var addComp = function () {
    // 下载按钮
    let btn = document.createElement("button")
    btn.innerHTML = "下载ICS"//innerText也可以,区别是innerText不会解析html
    btn.id = "lers_btn"
    btn.className = "btn btn-default btn-primary bigger-120 glyphicon glyphicon-arrow-down"
    btn.onclick = function () {
      timetableFormat(xhr.responseText) // 解析
      var ics = new ICS()
      ics.init_ICS()
      ics.download_ICS("timetable.ics", ics.ICS)
    }
    // 日期选择器
    let Datepicker = document.createElement("input")
    Datepicker.type = "date"
    Datepicker.value = TERM_START
    Datepicker.id = "lers_DatePicker"
    Datepicker.className = "btn bigger-120 btn-success"
    Datepicker.onchange = function () {
      TERM_START = document.getElementById("lers_DatePicker").value
    }
    document.getElementById("tb").prepend(btn)
    document.getElementById("tb").prepend(Datepicker)
  }
  if (document.getElementById("lers_btn")) {
    document.getElementById("lers_btn").remove()
    document.getElementById("lers_DatePicker").remove()
    addComp()
  }
  else {
    addComp()
  }
}
//----------------------------------流程------------------------------------------
//加载脚本时就启动函数开始监听
(function () {
  'use strict'
  addXMLRequestCallback(function (xhr) {
    //调用劫持函数,填入一个function的回调函数
    //回调函数监听了对xhr调用了监听load状态,并且在触发的时候再次调用一个function,进行一些数据的劫持以及修改
    xhr.addEventListener("load", function () {
      if (xhr.readyState == 4 && xhr.status == 200) {
        if (xhr.responseURL.indexOf("xskbcx_cxXsgrkb.html?gnmkdm=N253508") >= 0) { // url中包含固定字符串
          // hook xhr请求成功
          console.log("执行")
          addDownload(xhr)
        }
      }
    })
  })
})()