├── LICENSE ├── README.md ├── img ├── list_classes.png └── select_course.png ├── main.js └── update.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 SeanChao 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 | # SJTU 选课魔咒 2 | 3 | > version: v0.0.3-alpha 4 | 5 | 欢迎所有建议、意见和各位大佬 PR 🦾 6 | 7 | ## 功能✌ 8 | 9 | - 通过魔法让当前筛选条件下完整的课程列表显形 10 | - 在 Console 中显示所有教学班信息 11 | - 魔法抢课 12 | 13 | ## 使用说明 14 | 15 | 1. 在 Chrome 中进入开发者模式 16 | 2. 在 `Sources` 下的 `Snippets` 中,点击 `+ New Snippet`,创建一个代码片段 17 | 3. 将 `main.js` 文件中的全部内容复制进去~ 18 | 4. 根据需要**修改配置🎨** 19 | 5. 心中默念魔咒,运行🍻 20 | 21 | **注意事项👀**: 22 | 23 | - 这个脚本太菜了,只能抢已经在当前页面上列出来的课。可以通过多开几个标签页解决。 24 | - 需要值守 25 | - 还没有异常处理,需要确保选课时间无冲突等意外情况2333 26 | 27 | ## 配置说明 28 | 29 | 配置项在文件开头定义的 `config` JSON对象中。 30 | 31 | | 配置项 | 说明 | 32 | | -------------- | ---------------------------------------------------- | 33 | | listAll | 是否列出所有教学班 | 34 | | auto | 是否自动抢课 | 35 | | times | 运行次数 | 36 | | taskInterval | 每次运行之间的间隔 | 37 | | targetList | 一个数组,包含所有待抢的课。其中的元素为教学班的代码 | 38 | | i_ve_read_this | 改为 `true` 才会运行 | 39 | 40 | ## 截图 41 | 42 | ![课程教学班列表](img/list_classes.png) 43 | 44 | ![魔法选课](img/select_course.png) 45 | 46 | ## 声明 47 | 48 | 当您决定使用此脚本时,您需要自行承担此脚本带来的一切风险,可能包含: 49 | 50 | - 抢不到想要的课 51 | - 退了选到的课 52 | - 向教务处发起了退学申请 53 | - 引起其它同学的不满 54 | - 意外穿越了时空 55 | - 虫子钻进了电脑 56 | 57 | **脚本有风险,运行需谨慎!** 58 | 59 | ## 更新日志 60 | 61 | [🔗link](update.md) 62 | -------------------------------------------------------------------------------- /img/list_classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanChao/iMagicSJTU/eacfd8030d1f4cc8c8d45cd8613a4cccb7893153/img/list_classes.png -------------------------------------------------------------------------------- /img/select_course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanChao/iMagicSJTU/eacfd8030d1f4cc8c8d45cd8613a4cccb7893153/img/select_course.png -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WELCOME TO SJTU of Witchcraft and Wizardry 3 | * Version: v0.0.3-alpha 4 | */ 5 | 6 | var config = { 7 | // list all classes 8 | "listAll": true, 9 | // trigger auto select 10 | "auto": false, 11 | // how many times to run 12 | "times": 10, 13 | // TODO: add support for interval between getClass, ms 14 | "unitInterval": 1000, 15 | // retry interval of the whole task, ms 16 | "taskInterval": 10000, 17 | "semesterPrefix": "(2019-2020-2)-", 18 | // target courses, please follow the format below: 19 | "targetList": ["CH936-1", "BI916-1", "CH902-1", "MU901-3", "WA901-1", "HI948-1"], 20 | // Please change this to `true` 21 | "i_ve_read_this": false, 22 | } 23 | 24 | console.log('WELCOME TO SJTU of Witchcraft and Wizardry~ 😏'); 25 | if (!config['i_ve_read_this']) { 26 | console.log('哦!我向上帝发誓,老伙计,你一定得先修改 `config` 才能和隔壁家的玛丽太太一起去祈祷!'); 27 | } else { 28 | run(config); 29 | } 30 | 31 | async function run(config) { 32 | if (config.listAll) { 33 | document.querySelector("#nav_tab > li.active > a").click(); 34 | await sleep(500); 35 | await showAllCourses(); 36 | await listAllCoursesInfo(); 37 | } 38 | 39 | const targetList = config['targetList']; 40 | for (var count = 0; (config.times == 0) ? true : (count < config.times); count++) { 41 | if (targetList.length == 0 || !config.auto) break; 42 | 43 | // refresh 44 | document.querySelector("#nav_tab > li.active > a").click(); 45 | await sleep(500); 46 | await showAllCourses(false); 47 | await listAllCoursesInfo(false); 48 | //select 49 | if (config.times != 0) 50 | console.log(new Date().toLocaleString() + ' 正在尝试第' + (count + 1) + '/' + config.times + '次'); 51 | else 52 | console.log(new Date().toLocaleString() + ' 正在尝试第' + (count + 1) + '次'); 53 | await runGetClasses(targetList, config.unitInterval); 54 | console.log('让我睡个觉觉,' + config.taskInterval + "ms"); 55 | await sleep(config.taskInterval); 56 | } 57 | console.log('👋Bye!'); 58 | } 59 | 60 | async function showAllCourses(log = true) { 61 | console.debug('begin of showAllCourses'); 62 | let guard = 0; 63 | while (guard++ < 1000 && !(document.getElementById('more').style.display.trim() === 'none')) { 64 | loadCoursesByPaged(); 65 | await sleep(100); 66 | if (log) 67 | console.log('load ' + guard + ' more times '); 68 | } 69 | console.debug('end of showAllCourses') 70 | } 71 | 72 | function sleep(ms) { 73 | console.debug('set timer: ' + ms); 74 | return new Promise(resolve => setTimeout(resolve, ms)); 75 | } 76 | 77 | async function listAllCoursesInfo(log = true) { 78 | console.debug('begin of listAllCoursesInfo'); 79 | // get all of the courses 80 | a = document.querySelector("#contentBox") 81 | allCourses = document.querySelectorAll("#contentBox > div.tjxk_list > div.panel "); 82 | await Promise.all(Array.from(allCourses).map(async course => { 83 | allClasses = course.querySelectorAll("div.panel-body > table > tbody > tr "); 84 | for (tClass of allClasses) { 85 | courseObj = tClass.parentNode.parentNode.parentNode.parentNode.childNodes[0]; 86 | await loadJxbxxZzxk(courseObj); 87 | await sleep(1); // have to just wait a moment for animation to be loaded 88 | } 89 | })) 90 | console.log('loading...'); 91 | console.debug("Promise listAllClasses done"); 92 | // loop for sequential courses display 93 | for (course of allCourses) { 94 | if (!log) break; 95 | const courseName = course.querySelector("div > h3 > span > a").innerText; 96 | const credits = course.querySelector("div > h3 > span > i").innerText; 97 | if (log) console.log(courseName + '\t' + credits); 98 | allClasses = course.querySelectorAll("div.panel-body > table > tbody > tr "); 99 | for (tClass of allClasses) { 100 | courseObj = tClass.parentNode.parentNode.parentNode.parentNode.childNodes[0]; 101 | const classId = tClass.querySelector('td.jxbmc > a').innerText; 102 | const teacher = tClass.querySelector("td.jsxm").innerText; 103 | const prof = tClass.querySelector("td.jszc").innerText; 104 | const time = tClass.querySelector("td.sksj").innerText; 105 | const location = tClass.querySelector("td.jxdd").innerText; 106 | const selected = tClass.querySelector("td.rsxx > .jxbrs").innerText.toInt(); 107 | const capacity = tClass.querySelector("td.rsxx > .jxbrl").innerText.toInt(); 108 | selectBtnId = tClass.querySelector("td > button"); 109 | const ok = selected < capacity; 110 | if (log) 111 | console.log('\t- ' + teacher + ' ' + prof + ' ' + time + ' ' + location + ' ' + selected + '/' + capacity + ' ' + rate(selected, capacity) + '; id:' + classId); 112 | } 113 | } 114 | console.log('successfully load all ' + allCourses.length + ' courses under current conditions'); 115 | } 116 | 117 | async function runGetClasses(targetList, interval) { 118 | const bufferedList = targetList.slice(); 119 | // console.log(bufferedList); 120 | var promises = []; 121 | bufferedList.forEach((tClass) => { 122 | promises.push(getClass(config['semesterPrefix'] + tClass)); 123 | } 124 | ) 125 | await Promise.all(promises).then(async (res) => {// console.log(res); 126 | for (var idx = 0; idx < res.length; idx++) {// console.log(bufferedList[idx]); 127 | var name; 128 | if (res[idx] != 2) { 129 | name = (await getClassById(config['semesterPrefix'] + bufferedList[idx])).courseName; 130 | } 131 | switch (res[idx]) { 132 | case 0: { console.info('👏👏👏 选到「' + name + '」辣!'); targetList.splice(idx, 1); break; } 133 | case 1: { console.warn("😐 「" + name + "」被抢爆啦,稍后试试"); break; } 134 | case 2: { console.warn('🤔 当前页面上没有id为「' + bufferedList[idx] + '」的这门课哦'); break; } 135 | case 3: { console.info('😎 你已经有「' + name + '」啦!'); targetList.splice(idx, 1); break; } 136 | } 137 | } 138 | }, (err) => { console.log(err); }) 139 | // console.log(targetList); 140 | } 141 | 142 | function rate(selected, capacity) { 143 | if (selected >= capacity || capacity === 0) { 144 | return 'full'; 145 | } 146 | const ratio = selected / capacity; 147 | if (capacity - selected <= 3) { 148 | return 'super hot'; 149 | } else if (ratio >= 0.9) { 150 | return 'very hot'; 151 | } else if (ratio >= 0.8) { 152 | return 'hot'; 153 | } else if (ratio >= 0.6) { 154 | return 'normal'; 155 | } else 156 | return 'deserted'; 157 | } 158 | 159 | async function getClassById(tClassId) { 160 | const allClasses = document.querySelectorAll("td.jxbmc"); 161 | for (tClass of allClasses) { 162 | if (tClass.querySelector("a").innerText === tClassId) { 163 | p = tClass.parentNode; 164 | console.debug(p); 165 | const courseName = p.parentNode.parentNode.parentNode.parentNode.querySelector('.panel-title a').innerText; 166 | const tClassId = p.querySelector('td.jxbmc > a').innerText; 167 | const teacher = p.querySelector("td.jsxm").innerText; 168 | const prof = p.querySelector("td.jszc").innerText; 169 | const time = p.querySelector("td.sksj").innerText; 170 | const location = p.querySelector("td.jxdd").innerText; 171 | const numSelected = p.querySelector("td.rsxx > .jxbrs").innerText.toInt(); 172 | const capacity = p.querySelector("td.rsxx > .jxbrl").innerText.toInt(); 173 | const full = numSelected >= capacity; 174 | const selected = p.querySelector("button").innerText === '退选'; 175 | const btn = p.querySelector("button"); 176 | classJson = { 177 | "courseName": courseName, 178 | "tClassId": tClassId, 179 | "teacher": teacher, 180 | "prof": prof, 181 | "time": time, 182 | "location": location, 183 | "numSelected": numSelected, 184 | "capacity": capacity, 185 | "full": full, 186 | "selected": selected, 187 | "btn": btn, 188 | } 189 | console.debug(classJson); 190 | return classJson; 191 | } 192 | } 193 | // console.warn("Class not found in current page. Please check the classId: " + tClassId); 194 | return null; 195 | } 196 | 197 | /** 198 | * return value: 199 | * 0: take the class successfully; 200 | * 1: the class is full 201 | * 2: class doesn't exist in current page 202 | * 3: already had the course 203 | * -1: other reasons leading to failure 204 | * @param {string} classId teaching class id of the class 205 | * @returns {int} state code 206 | */ 207 | async function getClass(classId) { 208 | target = await getClassById(classId); 209 | console.debug(target); 210 | if (target === null) 211 | return 2; 212 | else if (target.selected) { 213 | return 3; 214 | } else if (target.full) { 215 | return 1; 216 | } 217 | console.log('正在用靴子狠狠地抢' + target.courseName); 218 | target.btn.click(); 219 | await sleep(100); 220 | target = await getClassById(classId); 221 | console.debug(target); 222 | if (target.selected) { 223 | return 0; 224 | } else 225 | return -1; 226 | } 227 | -------------------------------------------------------------------------------- /update.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## v0.0.3-alpha 4 | 5 | improvements: 6 | 7 | - 提高了刷新、加载所有教学班列表的效率 8 | 9 | ## v0.0.2-alpha 10 | 11 | fix: 每次抢课时没有刷新信息的严重bug 12 | --------------------------------------------------------------------------------