├── .gitignore ├── LICENSE ├── README.md ├── fetch └── fetch.go ├── generate └── generate.go ├── login └── login.go ├── main.go └── screenshots ├── kde.png ├── mac.png └── win10.png /.gitignore: -------------------------------------------------------------------------------- 1 | getMyCourses 2 | myCourses.ics -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 念 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # getMyCourses 2 | 从NEU新版教务系统获取自己的课程表,并生成可导入日历的.ics文件。 3 | 4 | ## 使用方法 5 | * 下载[Release](https://github.com/whoisnian/getMyCourses/releases/latest)后解压,在命令行环境下运行。 6 | * 按提示通过树维教务系统或统一认证登录,程序自动抓取课程并在当前目录生成`myCourses.ics`。 7 | * 打开.ics文件,系统会自动调用关联程序进行导入。 8 | 9 | ## 参考信息 10 | * 通过树维教务系统登录获取课程表的具体请求过程:[NEU 新版教务处课程表.md](https://gist.github.com/whoisnian/32b832bd55978fefa042d7c76f9d76c3) 11 | * iCalendar格式介绍:[维基百科:ICalendar](https://en.wikipedia.org/wiki/ICalendar) 12 | * Google日历帮助:[创建或编辑ICAL文件](https://support.google.com/calendar/answer/37118#format_ical) 13 | 14 | ## 注意 15 | * 生成.ics文件过程中会在命令行输出获取到的课程,请检查无误后再进行导入。 16 | * 当前已测试可成功导入.ics文件的日历:Google 日历,Outlook 日历,Mac 系统日历,Win10 UWP 系统日历。 17 | * Win10 UWP 日历缺少批量删除功能,可以关联 Outlook 帐户后使用 Outlook 帐户新建一个日历单独用于存储课程表。 18 | 19 | ## 效果图 20 | ![kde](screenshots/kde.png) 21 | ![win10](screenshots/win10.png) 22 | ![mac](screenshots/mac.png) 23 | -------------------------------------------------------------------------------- /fetch/fetch.go: -------------------------------------------------------------------------------- 1 | package fetch 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/cookiejar" 9 | "net/url" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // 获取课程表所在页面源代码 17 | func FetchCourses(cookieJar *cookiejar.Jar) (string, error) { 18 | fmt.Println("\n获取课表详情中。。。") 19 | 20 | // http 请求客户端 21 | var client http.Client 22 | client.Jar = cookieJar 23 | 24 | // 第一次请求 25 | time.Sleep(1 * time.Second) 26 | req, err := http.NewRequest(http.MethodGet, "http://219.216.96.4/eams/courseTableForStd.action", nil) 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | // 发送 32 | resp1, err := client.Do(req) 33 | if err != nil { 34 | return "", err 35 | } 36 | defer resp1.Body.Close() 37 | 38 | // 读取 39 | content, err := ioutil.ReadAll(resp1.Body) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | // 检查 45 | temp := string(content) 46 | if !strings.Contains(temp, "bg.form.addInput(form,\"ids\",\"") { 47 | return "", errors.New("获取ids失败") 48 | } 49 | temp = temp[strings.Index(temp, "bg.form.addInput(form,\"ids\",\"")+29 : strings.Index(temp, "bg.form.addInput(form,\"ids\",\"")+50] 50 | ids := temp[:strings.Index(temp, "\");")] 51 | semesterId := resp1.Cookies()[0].Value 52 | 53 | // 第二次请求 54 | time.Sleep(1 * time.Second) 55 | formValues := make(url.Values) 56 | formValues.Set("ignoreHead", "1") 57 | formValues.Set("showPrintAndExport", "1") 58 | formValues.Set("setting.kind", "std") 59 | formValues.Set("startWeek", "") 60 | formValues.Set("semester.id", semesterId) 61 | formValues.Set("ids", ids) 62 | 63 | req, err = http.NewRequest(http.MethodPost, "http://219.216.96.4/eams/courseTableForStd!courseTable.action", strings.NewReader(formValues.Encode())) 64 | if err != nil { 65 | return "", err 66 | } 67 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 68 | req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0") 69 | 70 | // 发送 71 | resp2, err := client.Do(req) 72 | if err != nil { 73 | return "", err 74 | } 75 | defer resp2.Body.Close() 76 | 77 | // 读取 78 | content, err = ioutil.ReadAll(resp2.Body) 79 | if err != nil { 80 | return "", err 81 | } 82 | 83 | // 检查 84 | temp = string(content) 85 | if !strings.Contains(temp, "课表格式说明") { 86 | return "", errors.New("获取课表失败") 87 | } 88 | 89 | fmt.Println("获取课表详情完成。") 90 | return temp, nil 91 | } 92 | 93 | // 获取当前教学周 94 | func FetchLearnWeek(cookieJar *cookiejar.Jar) (int, error) { 95 | fmt.Println("\n获取当前教学周中。。。") 96 | 97 | // http 请求客户端 98 | var client http.Client 99 | client.Jar = cookieJar 100 | 101 | req, err := http.NewRequest(http.MethodGet, "http://219.216.96.4/eams/homeExt.action", nil) 102 | if err != nil { 103 | return 0, err 104 | } 105 | 106 | // 发送 107 | resp1, err := client.Do(req) 108 | if err != nil { 109 | return 0, err 110 | } 111 | defer resp1.Body.Close() 112 | 113 | // 读取 114 | content, err := ioutil.ReadAll(resp1.Body) 115 | if err != nil { 116 | return 0, err 117 | } 118 | 119 | // 检查 120 | temp := string(content) 121 | if !strings.Contains(temp, "教学周") { 122 | return 0, errors.New("获取教学周失败") 123 | } 124 | temp = temp[strings.Index(temp, "id=\"teach-week\">") : strings.Index(temp, "教学周")+10] 125 | 126 | reg := regexp.MustCompile(`学期\s*(\d+)<\/font>\s*教学周`) 127 | res := reg.FindStringSubmatch(temp) 128 | if len(res) < 2 { 129 | return 0, errors.New(temp + " 中未匹配到教学周") 130 | } 131 | 132 | fmt.Println("获取当前教学周完成。") 133 | return strconv.Atoi(res[1]) 134 | } 135 | -------------------------------------------------------------------------------- /generate/generate.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/uuid" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // 课程具体时间,周几第几节 13 | type CourseTime struct { 14 | dayOfTheWeek int 15 | timeOfTheDay int 16 | } 17 | 18 | // 课程信息 19 | type Course struct { 20 | courseID string 21 | courseName string 22 | roomID string 23 | roomName string 24 | weeks string 25 | courseTimes []CourseTime 26 | } 27 | 28 | // 作息时间表,浑南上课时间 29 | var ClassStartTimeHunnan = []string{ 30 | "083000", 31 | "093000", 32 | "104000", 33 | "114000", 34 | "140000", 35 | "150000", 36 | "161000", 37 | "171000", 38 | "183000", 39 | "193000", 40 | "203000", 41 | "213000", 42 | } 43 | 44 | // 作息时间表,浑南下课时间 45 | var classEndTimeHunnan = []string{ 46 | "092000", 47 | "102000", 48 | "113000", 49 | "123000", 50 | "145000", 51 | "155000", 52 | "170000", 53 | "180000", 54 | "192000", 55 | "202000", 56 | "212000", 57 | "222000", 58 | } 59 | 60 | // 作息时间表,南湖上课时间 61 | var ClassStartTimeNanhu = []string{ 62 | "080000", 63 | "090000", 64 | "101000", 65 | "111000", 66 | "140000", 67 | "150000", 68 | "161000", 69 | "171000", 70 | "183000", 71 | "193000", 72 | "203000", 73 | "213000", 74 | } 75 | 76 | // 作息时间表,南湖下课时间 77 | var classEndTimeNanhu = []string{ 78 | "085000", 79 | "095000", 80 | "110000", 81 | "120000", 82 | "145000", 83 | "155000", 84 | "170000", 85 | "180000", 86 | "192000", 87 | "202000", 88 | "212000", 89 | "222000", 90 | } 91 | 92 | // ics文件用到的星期几简称 93 | var dayOfWeek = []string{ 94 | "MO", 95 | "TU", 96 | "WE", 97 | "TH", 98 | "FR", 99 | "SA", 100 | "SU", 101 | } 102 | 103 | // 从html源码生成ics文件内容 104 | func GenerateIcs(html string, schoolStartDay time.Time) (string, error) { 105 | fmt.Println("\n生成ics文件中。。。") 106 | // 利用正则匹配有效信息 107 | var myCourses []Course 108 | reg1 := regexp.MustCompile(`TaskActivity\(actTeacherId.join\(','\),actTeacherName.join\(','\),"(.*)","(.*)\(.*\)","(.*)","(.*)","(.*)",null,null,assistantName,"",""\);((?:\s*index =\d+\*unitCount\+\d+;\s*.*\s)+)`) 109 | reg2 := regexp.MustCompile(`\s*index =(\d+)\*unitCount\+(\d+);\s*`) 110 | coursesStr := reg1.FindAllStringSubmatch(html, -1) 111 | for _, courseStr := range coursesStr { 112 | var course Course 113 | course.courseID = courseStr[1] 114 | course.courseName = courseStr[2] 115 | course.roomID = courseStr[3] 116 | course.roomName = courseStr[4] 117 | course.weeks = courseStr[5] 118 | for _, indexStr := range strings.Split(courseStr[6], "table0.activities[index][table0.activities[index].length]=activity;") { 119 | if !strings.Contains(indexStr, "unitCount") { 120 | continue 121 | } 122 | var courseTime CourseTime 123 | courseTime.dayOfTheWeek, _ = strconv.Atoi(reg2.FindStringSubmatch(indexStr)[1]) 124 | courseTime.timeOfTheDay, _ = strconv.Atoi(reg2.FindStringSubmatch(indexStr)[2]) 125 | course.courseTimes = append(course.courseTimes, courseTime) 126 | } 127 | myCourses = append(myCourses, course) 128 | } 129 | 130 | // 生成ics文件头 131 | var icsData string 132 | icsData = `BEGIN:VCALENDAR 133 | PRODID:-//nian//getMyCourses 20190522//EN 134 | VERSION:2.0 135 | CALSCALE:GREGORIAN 136 | METHOD:PUBLISH 137 | X-WR-CALNAME:myCourses 138 | X-WR-TIMEZONE:Asia/Shanghai 139 | BEGIN:VTIMEZONE 140 | TZID:Asia/Shanghai 141 | X-LIC-LOCATION:Asia/Shanghai 142 | BEGIN:STANDARD 143 | TZOFFSETFROM:+0800 144 | TZOFFSETTO:+0800 145 | TZNAME:CST 146 | DTSTART:19700101T000000 147 | END:STANDARD 148 | END:VTIMEZONE` + "\n" 149 | 150 | num := 0 151 | for _, course := range myCourses { 152 | var weekDay, st, en int 153 | weekDay = course.courseTimes[0].dayOfTheWeek 154 | st = 12 155 | en = -1 156 | // 课程上下课时间 157 | for _, courseTime := range course.courseTimes { 158 | if st > courseTime.timeOfTheDay { 159 | st = courseTime.timeOfTheDay 160 | } 161 | if en < courseTime.timeOfTheDay { 162 | en = courseTime.timeOfTheDay 163 | } 164 | } 165 | 166 | // debug信息 167 | num++ 168 | fmt.Println("") 169 | fmt.Println(num) 170 | fmt.Println(course.courseName) 171 | fmt.Println("周" + strconv.Itoa(weekDay+1) + " 第" + strconv.Itoa(st+1) + "-" + strconv.Itoa(en+1) + "节") 172 | 173 | // 统计要上课的周 174 | var periods []string 175 | var startWeek []int 176 | byday := dayOfWeek[weekDay] 177 | for i := 0; i < 53; i++ { 178 | if course.weeks[i] != '1' { 179 | continue 180 | } 181 | if i+1 >= 53 { 182 | startWeek = append(startWeek, i) 183 | periods = append(periods, "RRULE:FREQ=WEEKLY;WKST=SU;COUNT=1;INTERVAL=1;BYDAY="+byday) 184 | // debug信息 185 | fmt.Println("第" + strconv.Itoa(i) + "周") 186 | continue 187 | } 188 | if course.weeks[i+1] == '1' { 189 | // 连续周合并 190 | var j int 191 | for j = i + 1; j < 53; j++ { 192 | if course.weeks[j] != '1' { 193 | break 194 | } 195 | } 196 | startWeek = append(startWeek, i) 197 | periods = append(periods, "RRULE:FREQ=WEEKLY;WKST=SU;COUNT="+strconv.Itoa(j-i)+";INTERVAL=1;BYDAY="+byday) 198 | // debug信息 199 | fmt.Println("第" + strconv.Itoa(i) + "-" + strconv.Itoa(j-1) + "周") 200 | i = j - 1 201 | } else { 202 | // 单双周合并 203 | var j int 204 | for j = i + 1; j+1 < 53; j += 2 { 205 | if course.weeks[j] == '1' || course.weeks[j+1] == '0' { 206 | break 207 | } 208 | } 209 | startWeek = append(startWeek, i) 210 | periods = append(periods, "RRULE:FREQ=WEEKLY;WKST=SU;COUNT="+strconv.Itoa((j+1-i)/2)+";INTERVAL=2;BYDAY="+byday) 211 | // debug信息 212 | if i%2 == 0 { 213 | fmt.Printf("双") 214 | } else { 215 | fmt.Printf("单") 216 | } 217 | fmt.Println(strconv.Itoa(i) + "-" + strconv.Itoa(j-1) + "周") 218 | i = j - 1 219 | } 220 | } 221 | 222 | // 生成ics文件中的EVENT 223 | for i := 0; i < len(periods); i++ { 224 | var eventData string 225 | eventData = `BEGIN:VEVENT` + "\n" 226 | startDate := schoolStartDay.AddDate(0, 0, (startWeek[i]-1)*7+weekDay+1) 227 | 228 | if strings.Contains(course.roomName, "浑南") { 229 | eventData = eventData + `DTSTART;TZID=Asia/Shanghai:` + startDate.Format("20060102T") + ClassStartTimeHunnan[st] + "\n" 230 | eventData = eventData + `DTEND;TZID=Asia/Shanghai:` + startDate.Format("20060102T") + classEndTimeHunnan[en] + "\n" 231 | } else { 232 | eventData = eventData + `DTSTART;TZID=Asia/Shanghai:` + startDate.Format("20060102T") + ClassStartTimeNanhu[st] + "\n" 233 | eventData = eventData + `DTEND;TZID=Asia/Shanghai:` + startDate.Format("20060102T") + classEndTimeNanhu[en] + "\n" 234 | } 235 | eventData = eventData + periods[i] + "\n" 236 | eventData = eventData + `DTSTAMP:` + time.Now().Format("20060102T150405Z") + "\n" 237 | eventData = eventData + `UID:` + uuid.New().String() + "\n" 238 | eventData = eventData + `CREATED:` + time.Now().Format("20060102T150405Z") + "\n" 239 | eventData = eventData + `DESCRIPTION:` + "\n" 240 | eventData = eventData + `LAST-MODIFIED:` + time.Now().Format("20060102T150405Z") + "\n" 241 | eventData = eventData + `LOCATION:` + course.roomName + "\n" 242 | eventData = eventData + `SEQUENCE:0 243 | STATUS:CONFIRMED` + "\n" 244 | eventData = eventData + `SUMMARY:` + course.courseName + "\n" 245 | 246 | eventData = eventData + `TRANSP:OPAQUE 247 | END:VEVENT` + "\n" 248 | icsData = icsData + eventData 249 | } 250 | } 251 | icsData = icsData + `END:VCALENDAR` 252 | 253 | fmt.Println("\n生成ics文件完成。") 254 | return icsData, nil 255 | } 256 | -------------------------------------------------------------------------------- /login/login.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/cookiejar" 11 | "net/url" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // 树维教务系统登录:http://219.216.96.4/eams/loginExt.action 19 | func LoginViaSupwisdom(username string, password string) (*cookiejar.Jar, error) { 20 | fmt.Println("\n树维教务系统登录中。。。") 21 | 22 | // Cookie 自动维护 23 | cookieJar, err := cookiejar.New(nil) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | // http 请求客户端 29 | var client http.Client 30 | client.Jar = cookieJar 31 | 32 | // 第一次请求 33 | req, err := http.NewRequest(http.MethodGet, "http://219.216.96.4/eams/loginExt.action", nil) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | // 发送 39 | resp1, err := client.Do(req) 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer resp1.Body.Close() 44 | 45 | // 读取 46 | content, err := ioutil.ReadAll(resp1.Body) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // 检查 52 | temp := string(content) 53 | if !strings.Contains(temp, "CryptoJS.SHA1(") { 54 | return nil, errors.New("登录页面打开失败,请检查 http://219.216.96.4/eams/loginExt.action") 55 | } 56 | 57 | // 对密码进行SHA1哈希 58 | temp = temp[strings.Index(temp, "CryptoJS.SHA1(")+15 : strings.Index(temp, "CryptoJS.SHA1(")+52] 59 | password = temp + password 60 | bytes := sha1.Sum([]byte(password)) 61 | password = hex.EncodeToString(bytes[:]) 62 | 63 | // 第二次请求 64 | time.Sleep(1 * time.Second) 65 | formValues := make(url.Values) 66 | formValues.Set("username", username) 67 | formValues.Set("password", password) 68 | formValues.Set("session_locale", "zh_CN") 69 | 70 | req, err = http.NewRequest(http.MethodPost, "http://219.216.96.4/eams/loginExt.action", strings.NewReader(formValues.Encode())) 71 | if err != nil { 72 | return nil, err 73 | } 74 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 75 | req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0") 76 | 77 | // 发送 78 | resp2, err := client.Do(req) 79 | if err != nil { 80 | return nil, err 81 | } 82 | defer resp2.Body.Close() 83 | 84 | // 读取 85 | content, err = ioutil.ReadAll(resp2.Body) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | // 检查 91 | temp = string(content) 92 | if !strings.Contains(temp, "personal-name") { 93 | return nil, errors.New("登录失败,请检查用户名和密码") 94 | } 95 | 96 | temp = temp[strings.Index(temp, "class=\"personal-name\">")+23 : strings.Index(temp, "class=\"personal-name\">")+60] 97 | fmt.Println(temp[:strings.Index(temp, ")")+1]) 98 | 99 | fmt.Println("树维教务系统登录完成。") 100 | return cookieJar, nil 101 | } 102 | 103 | // 统一身份认证:https://pass.neu.edu.cn 104 | func LoginViaTpass(username string, password string) (*cookiejar.Jar, error) { 105 | fmt.Println("\n统一身份认证登录中。。。") 106 | 107 | // Cookie 自动维护 108 | cookieJar, err := cookiejar.New(nil) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | // http 请求客户端 114 | var client http.Client 115 | client.Jar = cookieJar 116 | 117 | // 第一次请求 118 | req, err := http.NewRequest(http.MethodGet, "http://219.216.96.4/eams/localLogin!tip.action", nil) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | // 发送 124 | resp1, err := client.Do(req) 125 | if err != nil { 126 | return nil, err 127 | } 128 | defer resp1.Body.Close() 129 | 130 | // 第二次请求 131 | req, err = http.NewRequest(http.MethodGet, "https://pass.neu.edu.cn/tpass/login?service=http%3A%2F%2F219.216.96.4%2Feams%2FhomeExt.action", nil) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | // 发送 137 | resp2, err := client.Do(req) 138 | if err != nil { 139 | return nil, err 140 | } 141 | defer resp2.Body.Close() 142 | 143 | // 读取 144 | content, err := ioutil.ReadAll(resp2.Body) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | // 检查 150 | temp := string(content) 151 | if !strings.Contains(temp, "
")+23 : strings.Index(temp, "class=\"personal-name\">")+60] 205 | fmt.Println(temp[:strings.Index(temp, ")")+1]) 206 | 207 | fmt.Println("统一身份登录认证登录完成。") 208 | return cookieJar, nil 209 | } 210 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/whoisnian/getMyCourses/fetch" 6 | "github.com/whoisnian/getMyCourses/generate" 7 | "github.com/whoisnian/getMyCourses/login" 8 | "io/ioutil" 9 | "net/http/cookiejar" 10 | "path/filepath" 11 | "time" 12 | ) 13 | 14 | func main() { 15 | // 选择登录方式 16 | var choice int 17 | fmt.Println("1.树维教务系统登录: http://219.216.96.4/eams/loginExt.action") 18 | fmt.Println("2.东大统一身份认证: https://pass.neu.edu.cn") 19 | fmt.Printf("\n请选择登录方式(1 或 2):") 20 | _, err := fmt.Scanln(&choice) 21 | fmt.Printf("\n") 22 | if err != nil { 23 | fmt.Println(err.Error()) 24 | return 25 | } 26 | 27 | // 获取帐号和密码 28 | var username, password string 29 | fmt.Printf("帐号: ") 30 | fmt.Scanln(&username) 31 | fmt.Printf("密码: ") 32 | fmt.Scanln(&password) 33 | 34 | // 登录 35 | var cookieJar *cookiejar.Jar 36 | if choice == 1 { 37 | cookieJar, err = login.LoginViaSupwisdom(username, password) 38 | } else { 39 | cookieJar, err = login.LoginViaTpass(username, password) 40 | } 41 | 42 | if err != nil { 43 | fmt.Println(err.Error()) 44 | return 45 | } 46 | 47 | // 获取包含课程表的html源码 48 | html, err := fetch.FetchCourses(cookieJar) 49 | if err != nil { 50 | fmt.Println(err.Error()) 51 | return 52 | } 53 | 54 | // 获取当前教学周 55 | learnWeek, err := fetch.FetchLearnWeek(cookieJar) 56 | if err != nil { 57 | fmt.Println(err.Error()) 58 | return 59 | } 60 | 61 | // 计算校历第一周周日 62 | now := time.Now() 63 | location := time.FixedZone("UTC+8", 8*60*60) 64 | daySum := int(now.Weekday()) + learnWeek*7 - 7 65 | schoolStartDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location).AddDate(0, 0, -daySum) 66 | 67 | fmt.Println("\n当前为第", learnWeek, "教学周。") 68 | fmt.Println("计算得到本学期开始于", schoolStartDay.Format("2006-01-02")) 69 | fmt.Println("官方校历 http://www.neu.edu.cn/xl/list.htm") 70 | 71 | // 从html源码生成ics文件内容 72 | ics, err := generate.GenerateIcs(html, schoolStartDay) 73 | if err != nil { 74 | fmt.Println(err.Error()) 75 | return 76 | } 77 | 78 | // 保存到文件 79 | err = ioutil.WriteFile("myCourses.ics", []byte(ics), 0644) 80 | if err != nil { 81 | fmt.Println(err.Error()) 82 | return 83 | } 84 | 85 | // 提示文件路径 86 | path, err := filepath.Abs("myCourses.ics") 87 | if err != nil { 88 | fmt.Println(err.Error()) 89 | return 90 | } 91 | fmt.Println("\n已保存为:", path) 92 | } 93 | -------------------------------------------------------------------------------- /screenshots/kde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisnian/getMyCourses/d838f331c7dc3336e02805cf41dfb84ade902478/screenshots/kde.png -------------------------------------------------------------------------------- /screenshots/mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisnian/getMyCourses/d838f331c7dc3336e02805cf41dfb84ade902478/screenshots/mac.png -------------------------------------------------------------------------------- /screenshots/win10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisnian/getMyCourses/d838f331c7dc3336e02805cf41dfb84ade902478/screenshots/win10.png --------------------------------------------------------------------------------