├── .gitignore ├── README.md ├── Scripts ├── Classes.py ├── Monitor.py └── Utils.py ├── UI ├── Config.py ├── Image │ ├── TsinghuaYKT.jpg │ └── favicon.ico ├── Login.py └── MainWindow.py ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__* 2 | ~$* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 清华大学荷塘雨课堂助手 2 |   基于 *TrickyDeath* 的项目 [RainClassroomAssistant](https://github.com/TrickyDeath/RainClassroomAssitant) 进行修改,以专门适配清华大学的荷塘雨课堂。 3 | 4 | ## 功能 5 | - 自动签到 6 | - ~~自动答题(仅限于上课过程中发布的选择题、多选题、填空题)~~ 7 | - 自动发弹幕(一定时间内收到一定数量的弹幕后,自动跟风发送相同内容的弹幕) 8 | - 点名、收到题目等情况下的语音提醒 9 | - 多线程支持(此脚本支持在有多个正在上课课程的情况下使用) 10 | - 简洁美观的UI 11 | 12 | **经检查,由于荷塘雨课堂更新,前端无法获取题目答案,自动答题功能已失效。** 我们计划加入AI解题功能,敬请期待。 13 | 14 | ### 使用程序 15 |   已更新 Release v1.0.1,下载后双击打开即可使用! -------------------------------------------------------------------------------- /Scripts/Classes.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import threading 3 | import random 4 | import time 5 | import websocket 6 | import json 7 | from Scripts.Utils import get_user_info, dict_result, calculate_waittime 8 | 9 | wss_url = "wss://pro.yuketang.cn/wsapp/" 10 | class Lesson: 11 | def __init__(self,lessonid,lessonname,classroomid,main_ui): 12 | self.classroomid = classroomid 13 | self.lessonid = lessonid 14 | self.lessonname = lessonname 15 | self.sessionid = main_ui.config["sessionid"] 16 | self.headers = { 17 | "Cookie":"sessionid=%s" % self.sessionid, 18 | "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0", 19 | } 20 | self.receive_danmu = {} 21 | self.sent_danmu_dict = {} 22 | self.danmu_dict = {} 23 | self.problems_ls = [] 24 | self.unlocked_problem = [] 25 | self.classmates_ls = [] 26 | self.add_message = main_ui.add_message_signal.emit 27 | self.add_course = main_ui.add_course_signal.emit 28 | self.del_course = main_ui.del_course_signal.emit 29 | self.config = main_ui.config 30 | code, rtn = get_user_info(self.sessionid) 31 | self.user_uid = rtn["id"] 32 | self.user_uname = rtn["name"] 33 | self.main_ui = main_ui 34 | 35 | def _get_ppt(self,presentationid): 36 | # 获取课程各页ppt 37 | r = requests.get(url="https://pro.yuketang.cn/api/v3/lesson/presentation/fetch?presentation_id=%s" % (presentationid),headers=self.headers,proxies={"http": None,"https":None}) 38 | return dict_result(r.text)["data"] 39 | 40 | def get_problems(self,presentationid): 41 | # 获取课程ppt中的题目 42 | data = self._get_ppt(presentationid) 43 | return [problem["problem"] for problem in data["slides"] if "problem" in problem.keys()] 44 | 45 | def answer_questions(self,problemid,problemtype,answer,limit): 46 | # 回答问题 47 | if answer and problemtype != 3: 48 | wait_time = calculate_waittime(limit, self.config["answer_config"]["answer_delay"]["type"], self.config["answer_config"]["answer_delay"]["custom"]["time"]) 49 | if wait_time != 0: 50 | meg = "%s检测到问题,将在%s秒后自动回答,答案为%s" % (self.lessonname,wait_time,answer) 51 | # threading.Thread(target=say_something,args=(meg,)).start() 52 | self.add_message(meg,3) 53 | time.sleep(wait_time) 54 | else: 55 | meg = "%s检测到问题,剩余时间小于15秒,将立即自动回答,答案为%s" % (self.lessonname,answer) 56 | self.add_message(meg,3) 57 | # threading.Thread(target=say_something,args=(meg,)).start() 58 | data = {"problemId":problemid,"problemType":problemtype,"dt":int(time.time()),"result":answer} 59 | r = requests.post(url="https://pro.yuketang.cn/api/v3/lesson/problem/answer",headers=self.headers,data=json.dumps(data),proxies={"http": None,"https":None}) 60 | return_dict = dict_result(r.text) 61 | if return_dict["code"] == 0: 62 | meg = "%s自动回答成功" % self.lessonname 63 | self.add_message(meg,4) 64 | # threading.Thread(target=say_something,args=(meg,)).start() 65 | return True 66 | else: 67 | meg = "%s自动回答失败,原因:%s" % (self.lessonname,return_dict["msg"].replace("_"," ")) 68 | self.add_message(meg,4) 69 | # threading.Thread(target=say_something,args=(meg,)).start() 70 | return False 71 | else: 72 | if limit == -1: 73 | meg = "%s的问题没有找到答案,该题不限时,请尽快前往荷塘雨课堂回答" % (self.lessonname) 74 | else: 75 | meg = "%s的问题没有找到答案,请在%s秒内前往荷塘雨课堂回答" % (self.lessonname,limit) 76 | # threading.Thread(target=say_something,args=(meg,)).start() 77 | self.add_message(meg,4) 78 | return False 79 | 80 | def on_open(self, wsapp): 81 | self.handshake = {"op":"hello","userid":self.user_uid,"role":"student","auth":self.auth,"lessonid":self.lessonid} 82 | wsapp.send(json.dumps(self.handshake)) 83 | 84 | def checkin_class(self): 85 | r = requests.post(url="https://pro.yuketang.cn/api/v3/lesson/checkin",headers=self.headers,data=json.dumps({"source":5,"lessonId":self.lessonid}),proxies={"http": None,"https":None}) 86 | set_auth = r.headers.get("Set-Auth",None) 87 | times = 1 88 | while not set_auth and times <= 3: 89 | set_auth = r.headers.get("Set-Auth",None) 90 | times += 1 91 | time.sleep(1) 92 | self.headers["Authorization"] = "Bearer %s" % set_auth 93 | return dict_result(r.text)["data"]["lessonToken"] 94 | 95 | def on_message(self, wsapp, message): 96 | data = dict_result(message) 97 | op = data["op"] 98 | if op == "hello": 99 | presentations = list(set([slide["pres"] for slide in data["timeline"] if slide["type"]=="slide"])) 100 | current_presentation = data["presentation"] 101 | if current_presentation not in presentations: 102 | presentations.append(current_presentation) 103 | for presentationid in presentations: 104 | self.problems_ls.extend(self.get_problems(presentationid)) 105 | self.unlocked_problem = data["unlockedproblem"] 106 | for problemid in self.unlocked_problem: 107 | self._current_problem(wsapp, problemid) 108 | elif op == "unlockproblem": 109 | self.start_answer(data["problem"]["sid"],data["problem"]["limit"]) 110 | elif op == "lessonfinished": 111 | meg = "%s下课了" % self.lessonname 112 | # threading.Thread(target=say_something,args=(meg,)).start() 113 | self.add_message(meg,7) 114 | wsapp.close() 115 | elif op == "presentationupdated": 116 | self.problems_ls.extend(self.get_problems(data["presentation"])) 117 | elif op == "presentationcreated": 118 | self.problems_ls.extend(self.get_problems(data["presentation"])) 119 | elif op == "newdanmu" and self.config["auto_danmu"]: 120 | current_content = data["danmu"].lower() 121 | uid = data["userid"] 122 | sent_danmu_user = User(uid) 123 | if sent_danmu_user in self.classmates_ls: 124 | for i in self.classmates_ls: 125 | if i == sent_danmu_user: 126 | meg = "%s课程的%s%s发送了弹幕:%s" %(self.lessonname,i.sno,i.name,data["danmu"]) 127 | self.add_message(meg,2) 128 | break 129 | else: 130 | self.classmates_ls.append(sent_danmu_user) 131 | sent_danmu_user.get_userinfo(self.classroomid,self.headers) 132 | meg = "%s课程的%s%s发送了弹幕:%s" %(self.lessonname,sent_danmu_user.sno,sent_danmu_user.name,data["danmu"]) 133 | self.add_message(meg,2) 134 | now = time.time() 135 | # 收到一条弹幕,尝试取出其之前的所有记录的列表,取不到则初始化该内容列表 136 | try: 137 | same_content_ls = self.danmu_dict[current_content] 138 | except KeyError: 139 | self.danmu_dict[current_content] = [] 140 | same_content_ls = self.danmu_dict[current_content] 141 | # 清除超过60秒的弹幕记录 142 | for i in same_content_ls: 143 | if now - i > 60: 144 | same_content_ls.remove(i) 145 | # 如果当前的弹幕没被发过,或者已发送时间超过60秒 146 | if current_content not in self.sent_danmu_dict.keys() or now - self.sent_danmu_dict[current_content] > 60: 147 | if len(same_content_ls) + 1 >= self.config["danmu_config"]["danmu_limit"]: 148 | self.send_danmu(current_content) 149 | same_content_ls = [] 150 | self.sent_danmu_dict[current_content] = now 151 | else: 152 | same_content_ls.append(now) 153 | elif op == "callpaused": 154 | meg = "%s点名了,点到了:%s" % (self.lessonname, data["name"]) 155 | if self.user_uname == data["name"]: 156 | self.add_message(meg,5) 157 | else: 158 | self.add_message(meg,6) 159 | # 程序在上课中途运行,由_current_problem发送的已解锁题目数据,得到的返回值。 160 | # 此处需要筛选未到期的题目进行回答。 161 | elif op == "probleminfo": 162 | if data["limit"] != -1: 163 | time_left = int(data["limit"]-(int(data["now"]) - int(data["dt"]))/1000) 164 | else: 165 | time_left = data["limit"] 166 | # 筛选未到期题目 167 | if time_left > 0 or time_left == -1: 168 | if self.config["auto_answer"]: 169 | self.start_answer(data["problemid"],time_left) 170 | else: 171 | if time_left == -1: 172 | meg = "%s检测到问题,该题不限时,请尽快前往荷塘雨课堂回答" % (self.lessonname) 173 | self.add_message(meg,3) 174 | else: 175 | meg = "%s检测到问题,请在%s秒内前往荷塘雨课堂回答" % (self.lessonname,time_left) 176 | 177 | def start_answer(self, problemid, limit): 178 | for promblem in self.problems_ls: 179 | if promblem["problemId"] == problemid: 180 | if promblem["result"] is not None: 181 | # 如果该题已经作答过,直接跳出函数以忽略该题 182 | # 该情况理论上只会出现在启动监听时 183 | return 184 | blanks = promblem.get("blanks",[]) 185 | answers = [] 186 | if blanks: 187 | for i in blanks: 188 | answers.append(random.choice(i["answers"])) 189 | else: 190 | answers = promblem.get("answers",[]) 191 | threading.Thread(target=self.answer_questions,args=(promblem["problemId"],promblem["problemType"],answers,limit)).start() 192 | break 193 | else: 194 | if limit == -1: 195 | meg = "%s的问题没有找到答案,该题不限时,请尽快前往荷塘雨课堂回答" % (self.lessonname) 196 | else: 197 | meg = "%s的问题没有找到答案,请在%s秒内前往荷塘雨课堂回答" % (self.lessonname,limit) 198 | self.add_message(meg,4) 199 | # threading.Thread(target=say_something,args=(meg,)).start() 200 | 201 | 202 | def _current_problem(self, wsapp, promblemid): 203 | # 为获取已解锁的问题详情信息,向wsapp发送probleminfo 204 | query_problem = {"op":"probleminfo","lessonid":self.lessonid,"problemid":promblemid,"msgid":1} 205 | wsapp.send(json.dumps(query_problem)) 206 | 207 | def start_lesson(self, callback): 208 | self.auth = self.checkin_class() 209 | rtn = self.get_lesson_info() 210 | teacher = rtn["teacher"]["name"] 211 | title = rtn["title"] 212 | timestamp = rtn["startTime"] // 1000 213 | time_str = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(timestamp)) 214 | index = self.main_ui.tableWidget.rowCount() 215 | self.add_course([self.lessonname,title,teacher,time_str],index) 216 | self.wsapp = websocket.WebSocketApp(url=wss_url,header=self.headers,on_open=self.on_open,on_message=self.on_message) 217 | self.wsapp.run_forever() 218 | meg = "%s监听结束" % self.lessonname 219 | self.add_message(meg,7) 220 | self.del_course(index) 221 | # threading.Thread(target=say_something,args=(meg,)).start() 222 | return callback(self) 223 | 224 | def send_danmu(self,content): 225 | url = "https://pro.yuketang.cn/api/v3/lesson/danmu/send" 226 | data = { 227 | "extra": "", 228 | "fromStart": "50", 229 | "lessonId": self.lessonid, 230 | "message": content, 231 | "requiredCensor": False, 232 | "showStatus": True, 233 | "target": "", 234 | "userName": "", 235 | "wordCloud": True 236 | } 237 | r = requests.post(url=url,headers=self.headers,data=json.dumps(data),proxies={"http": None,"https":None}) 238 | if dict_result(r.text)["code"] == 0: 239 | meg = "%s弹幕发送成功!内容:%s" % (self.lessonname,content) 240 | else: 241 | meg = "%s弹幕发送失败!内容:%s" % (self.lessonname,content) 242 | self.add_message(meg,1) 243 | 244 | def get_lesson_info(self): 245 | url = "https://pro.yuketang.cn/api/v3/lesson/basic-info" 246 | r = requests.get(url=url,headers=self.headers,proxies={"http": None,"https":None}) 247 | return dict_result(r.text)["data"] 248 | 249 | 250 | def __eq__(self, other): 251 | return self.lessonid == other.lessonid 252 | 253 | class User: 254 | def __init__(self, uid): 255 | self.uid = uid 256 | 257 | def get_userinfo(self, classroomid, headers): 258 | r = requests.get("https://pro.yuketang.cn/v/course_meta/fetch_user_info_new?query_user_id=%s&classroom_id=%s" % (self.uid,classroomid),headers=headers,proxies={"http": None,"https":None}) 259 | data = dict_result(r.text)["data"] 260 | self.sno = data["school_number"] 261 | self.name = data["name"] -------------------------------------------------------------------------------- /Scripts/Monitor.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import threading 4 | from Scripts.Utils import get_on_lesson, test_network 5 | from Scripts.Classes import Lesson 6 | 7 | def monitor(main_ui): 8 | # 监听器函数 9 | 10 | def del_onclass(lesson_obj): 11 | # 作为回调函数传入start_lesson 12 | on_lesson_list.remove(lesson_obj) 13 | 14 | # 已经签到完成加入监听列表的课程 15 | on_lesson_list = [] 16 | # 检测到的未加入监听列表的课程 17 | lesson_list = [] 18 | network_status = True 19 | sessionid = main_ui.config["sessionid"] 20 | while True: 21 | # 获取课程列表 22 | try: 23 | lesson_list = get_on_lesson(sessionid) 24 | # lesson_list_old = get_on_lesson_old() 25 | except requests.exceptions.ConnectionError: 26 | meg = "网络异常,监听中断" 27 | main_ui.add_message_signal.emit(meg,8) 28 | network_status = False 29 | except Exception: 30 | pass 31 | # 网络异常处理 32 | while not network_status: 33 | ret = test_network() 34 | if ret == True: 35 | try: 36 | lesson_list = get_on_lesson(sessionid) 37 | # lesson_list_old = get_on_lesson_old() 38 | except: 39 | pass 40 | else: 41 | network_status = True 42 | meg = "网络已恢复,监听开始" 43 | main_ui.add_message_signal.emit(meg,8) 44 | break 45 | # 可结束线程的计时器 46 | timer = 0 47 | while timer <= 5: 48 | time.sleep(1) 49 | timer += 1 50 | if not main_ui.is_active: 51 | # 由于on_lesson_list在多线程操作之下,此处必须使用列表复制,以保证列表完整性 52 | for lesson in on_lesson_list.copy(): 53 | lesson.wsapp.close() 54 | return 55 | # 课程列表 56 | for lesson in lesson_list: 57 | lessionid = lesson["lessonId"] 58 | lessonname = lesson["courseName"] 59 | classroomid = lesson["classroomId"] 60 | lesson_obj = Lesson(lessionid,lessonname,classroomid,main_ui) 61 | if lesson_obj not in on_lesson_list: 62 | thread = threading.Thread(target=lesson_obj.start_lesson,args=(del_onclass,),daemon=True) 63 | thread.start() 64 | meg = "检测到课程%s正在上课,已加入监听列表" % lessonname 65 | main_ui.add_message_signal.emit(meg,7) 66 | on_lesson_list.append(lesson_obj) 67 | 68 | # for lesson in lesson_list_old: 69 | # lessionid = lesson["lesson_id"] 70 | # lessonname = lesson["classroom"]["name"] 71 | # classroomid = lesson["classroomId"] 72 | 73 | # 可结束线程的计时器 74 | timer = 0 75 | while timer <= 30: 76 | time.sleep(1) 77 | timer += 1 78 | if not main_ui.is_active: 79 | # 由于on_lesson_list在多线程操作之下,此处必须使用列表复制,以保证列表完整性 80 | for lesson in on_lesson_list.copy(): 81 | lesson.wsapp.close() 82 | return -------------------------------------------------------------------------------- /Scripts/Utils.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import pyttsx3 3 | import json 4 | import urllib3 5 | import requests 6 | import random 7 | import os 8 | import sys 9 | 10 | lock = threading.Lock() 11 | 12 | def say_something(text): 13 | # 带线程锁的语音函数 14 | lock.acquire() 15 | pyttsx3.speak(text) 16 | lock.release() 17 | 18 | def dict_result(text): 19 | # json string 转 dict object 20 | return dict(json.loads(text)) 21 | 22 | def test_network(): 23 | # 网络状态测试 24 | try: 25 | http = urllib3.PoolManager() 26 | http.request('GET', 'https://baidu.com') 27 | return True 28 | except: 29 | return False 30 | 31 | def calculate_waittime(limit, type, custom_time): 32 | # 计算答题等待时间 33 | ''' 34 | type 35 | 1: 随机 36 | 2: 自定义 37 | ''' 38 | def default_calculate(limit): 39 | # 默认的随机答题等待时间算法 40 | if limit == -1: 41 | wait_time = random.randint(5,20) 42 | else: 43 | if limit > 15: 44 | wait_time = random.randint(5,limit-10) 45 | else: 46 | wait_time = 0 47 | return wait_time 48 | 49 | if type == 1: 50 | wait_time = default_calculate(limit) 51 | elif type == 2: 52 | # 如果自定义等待时间超过当前题目的剩余时间,则采用默认算法 53 | if custom_time > limit: 54 | wait_time = default_calculate(limit) 55 | else: 56 | wait_time = custom_time 57 | return wait_time 58 | 59 | def get_initial_data(): 60 | # 默认配置信息 61 | initial_data = \ 62 | { 63 | "sessionid":"", 64 | "auto_danmu":True, 65 | "danmu_config":{ 66 | "danmu_limit":5 67 | }, 68 | "audio_on":True, 69 | "audio_config":{ 70 | "audio_type":{ 71 | "send_danmu":False, 72 | "others_danmu":False, 73 | "receive_problem":True, 74 | "answer_result":True, 75 | "im_called":True, 76 | "others_called":True, 77 | "course_info":True, 78 | "network_info":True 79 | } 80 | }, 81 | "auto_answer":True, 82 | "answer_config":{ 83 | "answer_delay":{ 84 | "type":1, 85 | "custom":{ 86 | "time":0 87 | } 88 | } 89 | } 90 | } 91 | return initial_data 92 | 93 | def get_config_path(): 94 | # 获取配置文件路径 95 | config_route = get_config_dir() + "\\config.json" 96 | return config_route 97 | 98 | def get_config_dir(): 99 | # 获取配置文件所在文件夹 100 | appdata_route = os.environ['APPDATA'] 101 | dir_route = appdata_route + "\\RainClassroomAssistant" 102 | return dir_route 103 | 104 | def get_user_info(sessionid): 105 | # 获取用户信息 106 | headers = { 107 | "Cookie":"sessionid=%s" % sessionid, 108 | "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0", 109 | } 110 | r = requests.get(url="https://pro.yuketang.cn/api/v3/user/basic-info",headers=headers,proxies={"http": None,"https":None}) 111 | rtn = dict_result(r.text) 112 | return (rtn["code"],rtn["data"]) 113 | 114 | def get_on_lesson(sessionid): 115 | # 获取用户当前正在上课列表 116 | headers = { 117 | "Cookie":"sessionid=%s" % sessionid, 118 | "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0", 119 | } 120 | r = requests.get("https://pro.yuketang.cn/api/v3/classroom/on-lesson",headers=headers,proxies={"http": None,"https":None}) 121 | rtn = dict_result(r.text) 122 | return rtn["data"]["onLessonClassrooms"] 123 | 124 | def get_on_lesson_old(sessionid): 125 | # 获取用户当前正在上课的列表(旧版) 126 | headers = { 127 | "Cookie":"sessionid=%s" % sessionid, 128 | "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0", 129 | } 130 | r = requests.get("https://pro.yuketang.cn/v/course_meta/on_lesson_courses",headers=headers,proxies={"http": None,"https":None}) 131 | rtn = dict_result(r.text) 132 | return rtn["on_lessons"] 133 | 134 | def resource_path(relative_path): 135 | # 解决打包exe的图片路径问题 136 | if getattr(sys, 'frozen', False): 137 | base_path = sys._MEIPASS 138 | else: 139 | base_path = os.path.abspath(".") 140 | return os.path.join(base_path, relative_path) -------------------------------------------------------------------------------- /UI/Config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'Config.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.4 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt5 import QtCore, QtWidgets, QtGui 12 | from Scripts.Utils import get_config_path, resource_path 13 | import json 14 | import functools 15 | 16 | class Config_Ui(object): 17 | def setupUi(self, Dialog): 18 | Dialog.setObjectName("Dialog") 19 | Dialog.resize(430, 550) 20 | Dialog.setStyleSheet("background-color: rgb(255, 255, 255);\n" 21 | "font: 9pt \"微软雅黑\";") 22 | Dialog.setWindowIcon(QtGui.QIcon(resource_path("UI\\Image\\favicon.ico"))) 23 | self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) 24 | self.verticalLayout.setObjectName("verticalLayout") 25 | self.scrollArea = QtWidgets.QScrollArea(Dialog) 26 | self.scrollArea.setWidgetResizable(True) 27 | self.scrollArea.setObjectName("scrollArea") 28 | self.scrollAreaWidgetContents = QtWidgets.QWidget() 29 | self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, -56, 393, 580)) 30 | self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") 31 | self.verticalLayout_12 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) 32 | self.verticalLayout_12.setObjectName("verticalLayout_12") 33 | self.danmu_config = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) 34 | self.danmu_config.setObjectName("danmu_config") 35 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.danmu_config) 36 | self.verticalLayout_2.setObjectName("verticalLayout_2") 37 | self.danmu_on = QtWidgets.QCheckBox(self.danmu_config) 38 | self.danmu_on.setObjectName("danmu_on") 39 | self.verticalLayout_2.addWidget(self.danmu_on) 40 | self.when_danmu_on = QtWidgets.QWidget(self.danmu_config) 41 | self.when_danmu_on.setEnabled(False) 42 | self.when_danmu_on.setObjectName("when_danmu_on") 43 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.when_danmu_on) 44 | self.verticalLayout_3.setObjectName("verticalLayout_3") 45 | self.label = QtWidgets.QLabel(self.when_danmu_on) 46 | self.label.setScaledContents(False) 47 | self.label.setWordWrap(True) 48 | self.label.setObjectName("label") 49 | self.verticalLayout_3.addWidget(self.label) 50 | self.danmu_spinBox = QtWidgets.QSpinBox(self.when_danmu_on) 51 | self.danmu_spinBox.setMaximum(32767) 52 | self.danmu_spinBox.setProperty("value", 5) 53 | self.danmu_spinBox.setObjectName("danmu_spinBox") 54 | self.verticalLayout_3.addWidget(self.danmu_spinBox) 55 | self.verticalLayout_2.addWidget(self.when_danmu_on) 56 | self.verticalLayout_12.addWidget(self.danmu_config) 57 | self.audio_config = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) 58 | self.audio_config.setObjectName("audio_config") 59 | self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.audio_config) 60 | self.verticalLayout_4.setObjectName("verticalLayout_4") 61 | self.audio_on = QtWidgets.QCheckBox(self.audio_config) 62 | self.audio_on.setObjectName("audio_on") 63 | self.verticalLayout_4.addWidget(self.audio_on) 64 | self.when_audio_on = QtWidgets.QWidget(self.audio_config) 65 | self.when_audio_on.setEnabled(False) 66 | self.when_audio_on.setObjectName("when_audio_on") 67 | self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.when_audio_on) 68 | self.verticalLayout_10.setContentsMargins(0, 0, 0, 0) 69 | self.verticalLayout_10.setSpacing(0) 70 | self.verticalLayout_10.setObjectName("verticalLayout_10") 71 | self.widget_4 = QtWidgets.QWidget(self.when_audio_on) 72 | self.widget_4.setObjectName("widget_4") 73 | self.verticalLayout_11 = QtWidgets.QVBoxLayout(self.widget_4) 74 | self.verticalLayout_11.setContentsMargins(9, 0, 0, 0) 75 | self.verticalLayout_11.setSpacing(0) 76 | self.verticalLayout_11.setObjectName("verticalLayout_11") 77 | self.label_4 = QtWidgets.QLabel(self.widget_4) 78 | self.label_4.setObjectName("label_4") 79 | self.verticalLayout_11.addWidget(self.label_4) 80 | self.verticalLayout_10.addWidget(self.widget_4) 81 | self.widget_3 = QtWidgets.QWidget(self.when_audio_on) 82 | self.widget_3.setObjectName("widget_3") 83 | self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.widget_3) 84 | self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) 85 | self.horizontalLayout_4.setSpacing(0) 86 | self.horizontalLayout_4.setObjectName("horizontalLayout_4") 87 | self.when_audio_on_1 = QtWidgets.QWidget(self.widget_3) 88 | self.when_audio_on_1.setObjectName("when_audio_on_1") 89 | self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.when_audio_on_1) 90 | self.verticalLayout_7.setObjectName("verticalLayout_7") 91 | self.self_danmu = QtWidgets.QCheckBox(self.when_audio_on_1) 92 | self.self_danmu.setObjectName("self_danmu") 93 | self.verticalLayout_7.addWidget(self.self_danmu) 94 | self.others_danmu = QtWidgets.QCheckBox(self.when_audio_on_1) 95 | self.others_danmu.setObjectName("others_danmu") 96 | self.verticalLayout_7.addWidget(self.others_danmu) 97 | self.receive_problem = QtWidgets.QCheckBox(self.when_audio_on_1) 98 | self.receive_problem.setObjectName("receive_problem") 99 | self.verticalLayout_7.addWidget(self.receive_problem) 100 | self.answer_result = QtWidgets.QCheckBox(self.when_audio_on_1) 101 | self.answer_result.setObjectName("answer_result") 102 | self.verticalLayout_7.addWidget(self.answer_result) 103 | self.horizontalLayout_4.addWidget(self.when_audio_on_1) 104 | self.when_audio_on_2 = QtWidgets.QWidget(self.widget_3) 105 | self.when_audio_on_2.setObjectName("when_audio_on_2") 106 | self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.when_audio_on_2) 107 | self.verticalLayout_6.setObjectName("verticalLayout_6") 108 | self.self_called = QtWidgets.QCheckBox(self.when_audio_on_2) 109 | self.self_called.setObjectName("self_called") 110 | self.verticalLayout_6.addWidget(self.self_called) 111 | self.others_called = QtWidgets.QCheckBox(self.when_audio_on_2) 112 | self.others_called.setObjectName("others_called") 113 | self.verticalLayout_6.addWidget(self.others_called) 114 | self.course = QtWidgets.QCheckBox(self.when_audio_on_2) 115 | self.course.setObjectName("course") 116 | self.verticalLayout_6.addWidget(self.course) 117 | self.network = QtWidgets.QCheckBox(self.when_audio_on_2) 118 | self.network.setObjectName("network") 119 | self.verticalLayout_6.addWidget(self.network) 120 | self.horizontalLayout_4.addWidget(self.when_audio_on_2) 121 | self.verticalLayout_10.addWidget(self.widget_3) 122 | self.verticalLayout_4.addWidget(self.when_audio_on) 123 | self.verticalLayout_12.addWidget(self.audio_config) 124 | self.answer_config = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) 125 | self.answer_config.setObjectName("answer_config") 126 | self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.answer_config) 127 | self.verticalLayout_5.setObjectName("verticalLayout_5") 128 | self.answer_on = QtWidgets.QCheckBox(self.answer_config) 129 | self.answer_on.setObjectName("answer_on") 130 | self.verticalLayout_5.addWidget(self.answer_on) 131 | self.when_answer_on = QtWidgets.QWidget(self.answer_config) 132 | self.when_answer_on.setEnabled(False) 133 | self.when_answer_on.setObjectName("when_answer_on") 134 | self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.when_answer_on) 135 | self.verticalLayout_8.setObjectName("verticalLayout_8") 136 | self.label_3 = QtWidgets.QLabel(self.when_answer_on) 137 | self.label_3.setObjectName("label_3") 138 | self.verticalLayout_8.addWidget(self.label_3) 139 | self.delay_time_radio_1 = QtWidgets.QRadioButton(self.when_answer_on) 140 | self.delay_time_radio_1.setChecked(True) 141 | self.delay_time_radio_1.setObjectName("delay_time_radio_1") 142 | self.verticalLayout_8.addWidget(self.delay_time_radio_1) 143 | self.delay_time_radio_2 = QtWidgets.QRadioButton(self.when_answer_on) 144 | self.delay_time_radio_2.setObjectName("delay_time_radio_2") 145 | self.verticalLayout_8.addWidget(self.delay_time_radio_2) 146 | self.when_delay_time_2 = QtWidgets.QWidget(self.when_answer_on) 147 | self.when_delay_time_2.setEnabled(False) 148 | self.when_delay_time_2.setObjectName("when_delay_time_2") 149 | self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.when_delay_time_2) 150 | self.verticalLayout_9.setContentsMargins(0, 0, 0, 3) 151 | self.verticalLayout_9.setObjectName("verticalLayout_9") 152 | self.delay_time_2_input = QtWidgets.QSpinBox(self.when_delay_time_2) 153 | self.delay_time_2_input.setObjectName("delay_time_2_input") 154 | self.verticalLayout_9.addWidget(self.delay_time_2_input) 155 | self.label_2 = QtWidgets.QLabel(self.when_delay_time_2) 156 | self.label_2.setWordWrap(True) 157 | self.label_2.setObjectName("label_2") 158 | self.verticalLayout_9.addWidget(self.label_2) 159 | self.verticalLayout_8.addWidget(self.when_delay_time_2) 160 | self.verticalLayout_5.addWidget(self.when_answer_on) 161 | self.verticalLayout_12.addWidget(self.answer_config) 162 | self.scrollArea.setWidget(self.scrollAreaWidgetContents) 163 | self.verticalLayout.addWidget(self.scrollArea) 164 | self.btn_wid = QtWidgets.QWidget(Dialog) 165 | self.btn_wid.setObjectName("btn_wid") 166 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.btn_wid) 167 | self.horizontalLayout.setObjectName("horizontalLayout") 168 | self.save = QtWidgets.QPushButton(self.btn_wid) 169 | self.save.setMaximumSize(QtCore.QSize(16777215, 40)) 170 | self.save.setObjectName("save") 171 | self.horizontalLayout.addWidget(self.save) 172 | self.cancel = QtWidgets.QPushButton(self.btn_wid) 173 | self.cancel.setMaximumSize(QtCore.QSize(16777215, 40)) 174 | self.cancel.setObjectName("cancel") 175 | self.horizontalLayout.addWidget(self.cancel) 176 | self.verticalLayout.addWidget(self.btn_wid) 177 | 178 | # 动作绑定 179 | self.cancel.clicked.connect(Dialog.reject) 180 | self.danmu_on.stateChanged.connect(self.enable_danmu_config) 181 | self.audio_on.stateChanged.connect(self.enable_audio_config) 182 | self.answer_on.stateChanged.connect(self.enable_answer_config) 183 | self.delay_time_radio_1.clicked.connect(self.enable_delay_custom) 184 | self.delay_time_radio_2.clicked.connect(self.enable_delay_custom) 185 | self.save.clicked.connect(functools.partial(self.save_config,dialog=Dialog)) 186 | 187 | self.retranslateUi(Dialog) 188 | QtCore.QMetaObject.connectSlotsByName(Dialog) 189 | 190 | def enable_danmu_config(self): 191 | # 启用自动弹幕详细配置Widget 192 | if self.danmu_on.isChecked(): 193 | self.when_danmu_on.setEnabled(True) 194 | else: 195 | self.when_danmu_on.setEnabled(False) 196 | 197 | def enable_audio_config(self): 198 | # 启用语言提醒详细配置Widget 199 | if self.audio_on.isChecked(): 200 | self.when_audio_on.setEnabled(True) 201 | else: 202 | self.when_audio_on.setEnabled(False) 203 | 204 | def enable_answer_config(self): 205 | # 启用自动答题详细配置Widget 206 | if self.answer_on.isChecked(): 207 | self.when_answer_on.setEnabled(True) 208 | else: 209 | self.when_answer_on.setEnabled(False) 210 | 211 | def enable_delay_custom(self): 212 | # 启用自定义延迟详细配置Widget 213 | if self.delay_time_radio_2.isChecked(): 214 | self.when_delay_time_2.setEnabled(True) 215 | else: 216 | self.when_delay_time_2.setEnabled(False) 217 | 218 | def load_config(self, config): 219 | # 弹幕配置 220 | self.danmu_on.setChecked(config["auto_danmu"]) 221 | self.danmu_spinBox.setValue(config["danmu_config"]["danmu_limit"]) 222 | # 语音配置 223 | self.audio_on.setChecked(config["audio_on"]) 224 | self.self_danmu.setChecked(config["audio_config"]["audio_type"]["send_danmu"]) 225 | self.others_danmu.setChecked(config["audio_config"]["audio_type"]["others_danmu"]) 226 | self.receive_problem.setChecked(config["audio_config"]["audio_type"]["receive_problem"]) 227 | self.answer_result.setChecked(config["audio_config"]["audio_type"]["answer_result"]) 228 | self.self_called.setChecked(config["audio_config"]["audio_type"]["im_called"]) 229 | self.others_called.setChecked(config["audio_config"]["audio_type"]["others_called"]) 230 | self.course.setChecked(config["audio_config"]["audio_type"]["course_info"]) 231 | self.network.setChecked(config["audio_config"]["audio_type"]["network_info"]) 232 | # 答题配置 233 | self.answer_on.setChecked(config["auto_answer"]) 234 | if config["answer_config"]["answer_delay"]["type"] == 1: 235 | self.delay_time_radio_1.setChecked(True) 236 | elif config["answer_config"]["answer_delay"]["type"] == 2: 237 | self.delay_time_radio_2.setChecked(True) 238 | self.delay_time_2_input.setValue(config["answer_config"]["answer_delay"]["custom"]["time"]) 239 | self.dialog_config = config 240 | 241 | def save_config(self, dialog): 242 | config = self.dialog_config 243 | # 弹幕配置 244 | config["auto_danmu"] = self.danmu_on.isChecked() 245 | config["danmu_config"]["danmu_limit"] = self.danmu_spinBox.value() 246 | # 语音配置 247 | config["audio_on"] = self.audio_on.isChecked() 248 | config["audio_config"]["audio_type"]["send_danmu"] = self.self_danmu.isChecked() 249 | config["audio_config"]["audio_type"]["others_danmu"] = self.others_danmu.isChecked() 250 | config["audio_config"]["audio_type"]["receive_problem"] = self.receive_problem.isChecked() 251 | config["audio_config"]["audio_type"]["answer_result"] = self.answer_result.isChecked() 252 | config["audio_config"]["audio_type"]["im_called"] = self.self_called.isChecked() 253 | config["audio_config"]["audio_type"]["others_called"] = self.others_called.isChecked() 254 | config["audio_config"]["audio_type"]["course_info"] = self.course.isChecked() 255 | config["audio_config"]["audio_type"]["network_info"] = self.network.isChecked() 256 | # 答题配置 257 | config["auto_answer"] = self.answer_on.isChecked() 258 | if self.delay_time_radio_1.isChecked(): 259 | config["answer_config"]["answer_delay"]["type"] = 1 260 | elif self.delay_time_radio_2.isChecked(): 261 | config["answer_config"]["answer_delay"]["type"] = 2 262 | config["answer_config"]["answer_delay"]["custom"]["time"] = self.delay_time_2_input.value() 263 | # 保存 264 | config_path = get_config_path() 265 | with open(config_path,"w+") as f: 266 | json.dump(config,f) 267 | dialog.accept() 268 | 269 | def retranslateUi(self, Dialog): 270 | _translate = QtCore.QCoreApplication.translate 271 | Dialog.setWindowTitle(_translate("Dialog", "配置")) 272 | self.danmu_config.setTitle(_translate("Dialog", "弹幕配置")) 273 | self.danmu_on.setText(_translate("Dialog", "启用自动发送弹幕")) 274 | self.label.setText(_translate("Dialog", "自动弹幕阈值(每分钟内收到n条弹幕后自动发送相同弹幕)")) 275 | self.audio_config.setTitle(_translate("Dialog", "语音配置")) 276 | self.audio_on.setText(_translate("Dialog", "启用语音提醒")) 277 | self.label_4.setText(_translate("Dialog", "需要语音提醒的内容")) 278 | self.self_danmu.setText(_translate("Dialog", "自动发送弹幕情况提醒")) 279 | self.others_danmu.setText(_translate("Dialog", "他人弹幕发送语音提醒")) 280 | self.receive_problem.setText(_translate("Dialog", "收到题目提醒")) 281 | self.answer_result.setText(_translate("Dialog", "自动答题情况提醒")) 282 | self.self_called.setText(_translate("Dialog", "自己被点名提醒")) 283 | self.others_called.setText(_translate("Dialog", "他人被点名提醒")) 284 | self.course.setText(_translate("Dialog", "课程相关提醒")) 285 | self.network.setText(_translate("Dialog", "网络断开/重连提醒")) 286 | self.answer_config.setTitle(_translate("Dialog", "答题配置")) 287 | self.answer_on.setText(_translate("Dialog", "启用自动答题")) 288 | self.label_3.setText(_translate("Dialog", "答题延迟时长")) 289 | self.delay_time_radio_1.setText(_translate("Dialog", "系统默认(随机决定时间)")) 290 | self.delay_time_radio_2.setText(_translate("Dialog", "自定义(于收到题目n秒后自动回答)")) 291 | self.label_2.setText(_translate("Dialog", "注:如果您采用自定义延迟时长,当延迟时长大于题目所给时限时,将按照系统默认算法重新计算延迟时长。")) 292 | self.save.setText(_translate("Dialog", "保存")) 293 | self.cancel.setText(_translate("Dialog", "取消")) 294 | -------------------------------------------------------------------------------- /UI/Image/TsinghuaYKT.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangchi2004/THU-Yuketang-Helper/6297f4fd14854c930c9b36263a1183d62f3c637d/UI/Image/TsinghuaYKT.jpg -------------------------------------------------------------------------------- /UI/Image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangchi2004/THU-Yuketang-Helper/6297f4fd14854c930c9b36263a1183d62f3c637d/UI/Image/favicon.ico -------------------------------------------------------------------------------- /UI/Login.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'Login.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.4 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt5 import QtCore, QtGui, QtWidgets 12 | from Scripts.Utils import dict_result, get_config_path, resource_path 13 | import websocket 14 | import requests 15 | import json 16 | import threading 17 | import time 18 | 19 | class Login_Ui(object): 20 | def setupUi(self, Dialog): 21 | Dialog.setObjectName("Dialog") 22 | Dialog.resize(350, 500) 23 | Dialog.setStyleSheet("background-color: rgb(255, 255, 255);") 24 | Dialog.setWindowIcon(QtGui.QIcon(resource_path("UI\\Image\\favicon.ico"))) 25 | self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) 26 | self.verticalLayout.setObjectName("verticalLayout") 27 | self.widget = QtWidgets.QWidget(Dialog) 28 | self.widget.setObjectName("widget") 29 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.widget) 30 | self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) 31 | self.verticalLayout_2.setSpacing(0) 32 | self.verticalLayout_2.setObjectName("verticalLayout_2") 33 | self.label = QtWidgets.QLabel(self.widget) 34 | self.label.setLayoutDirection(QtCore.Qt.LeftToRight) 35 | self.label.setStyleSheet("color: rgb(0, 0, 0);\n" 36 | "font: 16pt \"微软雅黑\";") 37 | self.label.setAlignment(QtCore.Qt.AlignCenter) 38 | self.label.setObjectName("label") 39 | self.verticalLayout_2.addWidget(self.label) 40 | self.label_2 = QtWidgets.QLabel(self.widget) 41 | self.label_2.setStyleSheet("font: 8pt \"微软雅黑\";\n" 42 | "color: rgb(255, 0, 0);") 43 | self.label_2.setTextFormat(QtCore.Qt.AutoText) 44 | self.label_2.setAlignment(QtCore.Qt.AlignCenter) 45 | self.label_2.setWordWrap(True) 46 | self.label_2.setObjectName("label_2") 47 | self.verticalLayout_2.addWidget(self.label_2) 48 | self.verticalLayout_2.setStretch(0, 2) 49 | self.verticalLayout_2.setStretch(1, 1) 50 | self.verticalLayout.addWidget(self.widget) 51 | self.widget_2 = QtWidgets.QWidget(Dialog) 52 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) 53 | sizePolicy.setHorizontalStretch(0) 54 | sizePolicy.setVerticalStretch(0) 55 | sizePolicy.setHeightForWidth(self.widget_2.sizePolicy().hasHeightForWidth()) 56 | self.widget_2.setSizePolicy(sizePolicy) 57 | self.widget_2.setObjectName("widget_2") 58 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget_2) 59 | self.horizontalLayout.setContentsMargins(0, 0, 0, 0) 60 | self.horizontalLayout.setSpacing(0) 61 | self.horizontalLayout.setObjectName("horizontalLayout") 62 | self.QRcode = QtWidgets.QLabel(self.widget_2) 63 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) 64 | sizePolicy.setHorizontalStretch(0) 65 | sizePolicy.setVerticalStretch(0) 66 | sizePolicy.setHeightForWidth(self.QRcode.sizePolicy().hasHeightForWidth()) 67 | self.QRcode.setSizePolicy(sizePolicy) 68 | self.QRcode.setMaximumSize(QtCore.QSize(256, 256)) 69 | self.QRcode.setText("") 70 | self.QRcode.setScaledContents(True) 71 | self.QRcode.setObjectName("QRcode") 72 | self.horizontalLayout.addWidget(self.QRcode) 73 | self.horizontalLayout.setStretch(0, 1) 74 | self.verticalLayout.addWidget(self.widget_2) 75 | self.widget_3 = QtWidgets.QWidget(Dialog) 76 | self.widget_3.setObjectName("widget_3") 77 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.widget_3) 78 | self.verticalLayout_3.setObjectName("verticalLayout_3") 79 | self.login_return = QtWidgets.QLabel(self.widget_3) 80 | self.login_return.setText("") 81 | self.login_return.setObjectName("login_return") 82 | self.login_return.setAlignment(QtCore.Qt.AlignCenter) 83 | self.login_return.setStyleSheet("color: rgb(255, 0, 0);") 84 | self.verticalLayout_3.addWidget(self.login_return) 85 | self.verticalLayout.addWidget(self.widget_3) 86 | self.verticalLayout.setStretch(0, 3) 87 | self.verticalLayout.setStretch(1, 10) 88 | self.verticalLayout.setStretch(2, 1) 89 | # 开启扫码登录所需的websocket连接 90 | self.start_wssapp(Dialog) 91 | self.retranslateUi(Dialog) 92 | QtCore.QMetaObject.connectSlotsByName(Dialog) 93 | 94 | def _flush_login_QRcode(self): 95 | # 刷新登录二维码,单独线程运行 96 | count = 0 97 | # 便于退出的sleep 98 | while self.flush_on: 99 | if count == 60: 100 | count = 0 101 | data={"op":"requestlogin","role":"web","version":1.4,"type":"qrcode","from":"web"} 102 | self.wsapp.send(json.dumps(data)) 103 | else: 104 | time.sleep(1) 105 | count += 1 106 | 107 | def close_all(self): 108 | # 关闭websocket连接 109 | self.flush_on = False 110 | self.wsapp.close() 111 | self.flush_t.join() 112 | 113 | def load_config(self, config): 114 | # 载入配置文件 115 | self.config = config 116 | 117 | def save(self, sessionid): 118 | # 保存sessionid 119 | config = self.config 120 | config["sessionid"] = sessionid 121 | config_path = get_config_path() 122 | with open(config_path,"w+") as f: 123 | json.dump(config, f) 124 | 125 | def start_wssapp(self, Dialog): 126 | def on_open(wsapp): 127 | data={"op":"requestlogin","role":"web","version":1.4,"type":"qrcode","from":"web"} 128 | wsapp.send(json.dumps(data)) 129 | 130 | def on_close(wsapp): 131 | print("closed") 132 | 133 | def on_message(wsapp, message): 134 | data = dict_result(message) 135 | # 二维码刷新 136 | if data["op"] == "requestlogin": 137 | img = requests.get(url=data["ticket"],proxies={"http": None,"https":None}).content 138 | img_pixmap = QtGui.QPixmap() 139 | img_pixmap.loadFromData(img) 140 | self.QRcode.setPixmap(img_pixmap) 141 | # 扫码且登录成功 142 | elif data["op"] == "loginsuccess": 143 | web_login_url = "https://pro.yuketang.cn/pc/web_login" 144 | login_data = { 145 | "UserID":data["UserID"], 146 | "Auth":data["Auth"] 147 | } 148 | headers = { 149 | "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" 150 | } 151 | login_data = json.dumps(login_data) 152 | # 使用Auth和UserID正式登录获取sessionid 153 | r = requests.post(url=web_login_url,data=login_data,headers=headers,proxies={"http": None,"https":None}) 154 | sessionid = dict(r.cookies)["sessionid"] 155 | config = self.config 156 | config["sessionid"] = sessionid 157 | self.save(sessionid) 158 | Dialog.accept() 159 | login_wss_url = "wss://pro.yuketang.cn/wsapp/" 160 | # 开启websocket线程和定时刷新二维码线程 161 | self.wsapp = websocket.WebSocketApp(url=login_wss_url,on_open=on_open,on_message=on_message,on_close=on_close) 162 | self.wsapp_t = threading.Thread(target=self.wsapp.run_forever,daemon=True) 163 | self.wsapp_t.start() 164 | self.flush_on = True 165 | self.flush_t = threading.Thread(target=self._flush_login_QRcode,daemon=True) 166 | self.flush_t.start() 167 | 168 | def retranslateUi(self, Dialog): 169 | _translate = QtCore.QCoreApplication.translate 170 | Dialog.setWindowTitle(_translate("Dialog", "登录")) 171 | self.label.setText(_translate("Dialog", "微信扫码登录荷塘雨课堂")) 172 | self.label_2.setText(_translate("Dialog", "注:扫码登录仅用于获取您的登录状态以便软件监听荷塘雨课堂信息。")) 173 | -------------------------------------------------------------------------------- /UI/MainWindow.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'MainWindow.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.4 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt5 import QtCore, QtGui, QtWidgets 12 | from UI.Login import Login_Ui 13 | from UI.Config import Config_Ui 14 | from Scripts.Utils import * 15 | from Scripts.Monitor import monitor 16 | import os 17 | import json 18 | import datetime 19 | import threading 20 | 21 | class MainWindow_Ui(QtCore.QObject): 22 | # 需要建立信号槽,解决无法在线程中修改UI值问题 23 | add_message_signal = QtCore.pyqtSignal(str,int) 24 | add_course_signal = QtCore.pyqtSignal(list,int) 25 | del_course_signal = QtCore.pyqtSignal(int) 26 | 27 | def setupUi(self, MainWindow): 28 | # 对象变量初始化 29 | self.table_index = [] 30 | self.is_active = False 31 | 32 | MainWindow.setObjectName("MainWindow") 33 | MainWindow.resize(800, 700) 34 | MainWindow.setAutoFillBackground(False) 35 | MainWindow.setStyleSheet("background-color: rgb(255, 255, 255);") 36 | MainWindow.setWindowIcon(QtGui.QIcon(resource_path("UI\\Image\\favicon.ico"))) 37 | self.Window = QtWidgets.QWidget(MainWindow) 38 | self.Window.setStyleSheet("background-color: rgb(255, 255, 255);") 39 | self.Window.setObjectName("Window") 40 | self.verticalLayout = QtWidgets.QVBoxLayout(self.Window) 41 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 42 | self.verticalLayout.setObjectName("verticalLayout") 43 | self.Menu = QtWidgets.QWidget(self.Window) 44 | self.Menu.setStyleSheet("background-color: rgb(17, 17, 17);") 45 | self.Menu.setObjectName("Menu") 46 | self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.Menu) 47 | self.horizontalLayout_3.setContentsMargins(9, 9, 9, 9) 48 | self.horizontalLayout_3.setSpacing(6) 49 | self.horizontalLayout_3.setObjectName("horizontalLayout_3") 50 | self.label = QtWidgets.QLabel(self.Menu) 51 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) 52 | sizePolicy.setHorizontalStretch(0) 53 | sizePolicy.setVerticalStretch(0) 54 | sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) 55 | self.label.setSizePolicy(sizePolicy) 56 | self.label.setMaximumSize(QtCore.QSize(32, 32)) 57 | self.label.setStyleSheet("border-radius:10px;\n" 58 | "") 59 | self.label.setText("") 60 | self.label.setPixmap(QtGui.QPixmap(resource_path("UI\\Image\\TinsghuaYKT.jpg"))) 61 | self.label.setScaledContents(True) 62 | self.label.setObjectName("label") 63 | self.horizontalLayout_3.addWidget(self.label) 64 | self.label_2 = QtWidgets.QLabel(self.Menu) 65 | self.label_2.setStyleSheet("color: rgb(255, 255, 255);\n" 66 | "font: 16pt \"黑体\";") 67 | self.label_2.setObjectName("label_2") 68 | self.horizontalLayout_3.addWidget(self.label_2) 69 | spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 70 | self.horizontalLayout_3.addItem(spacerItem) 71 | self.active_btn = QtWidgets.QPushButton(self.Menu) 72 | self.active_btn.setMaximumSize(QtCore.QSize(100, 400)) 73 | self.active_btn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 74 | self.active_btn.setAutoFillBackground(False) 75 | self.active_btn.setStyleSheet("background-color: rgb(255, 255, 255);") 76 | self.active_btn.setObjectName("active_btn") 77 | self.horizontalLayout_3.addWidget(self.active_btn) 78 | self.login_btn = QtWidgets.QPushButton(self.Menu) 79 | self.login_btn.setMaximumSize(QtCore.QSize(100, 400)) 80 | self.login_btn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 81 | self.login_btn.setAutoFillBackground(False) 82 | self.login_btn.setStyleSheet("background-color: rgb(255, 255, 255);") 83 | self.login_btn.setObjectName("login_btn") 84 | self.horizontalLayout_3.addWidget(self.login_btn) 85 | self.config_btn = QtWidgets.QPushButton(self.Menu) 86 | self.config_btn.setMaximumSize(QtCore.QSize(100, 400)) 87 | self.config_btn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 88 | self.config_btn.setAutoFillBackground(False) 89 | self.config_btn.setStyleSheet("background-color: rgb(255, 255, 255);") 90 | self.config_btn.setObjectName("config_btn") 91 | self.horizontalLayout_3.addWidget(self.config_btn) 92 | self.horizontalLayout_3.setStretch(2, 1) 93 | self.horizontalLayout_3.setStretch(3, 1) 94 | self.horizontalLayout_3.setStretch(4, 1) 95 | self.horizontalLayout_3.setStretch(5, 1) 96 | self.verticalLayout.addWidget(self.Menu) 97 | self.Table = QtWidgets.QGroupBox(self.Window) 98 | self.Table.setStyleSheet("color: rgb(209, 209, 209);\n" 99 | "font: 10pt \"微软雅黑\";\n" 100 | "color: rgb(0, 0, 0);") 101 | self.Table.setObjectName("Table") 102 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.Table) 103 | self.verticalLayout_3.setObjectName("verticalLayout_3") 104 | self.tableWidget = QtWidgets.QTableWidget(self.Table) 105 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) 106 | sizePolicy.setHorizontalStretch(0) 107 | sizePolicy.setVerticalStretch(0) 108 | sizePolicy.setHeightForWidth(self.tableWidget.sizePolicy().hasHeightForWidth()) 109 | self.tableWidget.setSizePolicy(sizePolicy) 110 | self.tableWidget.setStyleSheet("font: 9pt \"微软雅黑\";") 111 | self.tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) 112 | self.tableWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 113 | self.tableWidget.setObjectName("tableWidget") 114 | self.tableWidget.setColumnCount(4) 115 | self.tableWidget.setRowCount(0) 116 | item = QtWidgets.QTableWidgetItem() 117 | self.tableWidget.setHorizontalHeaderItem(0, item) 118 | item = QtWidgets.QTableWidgetItem() 119 | self.tableWidget.setHorizontalHeaderItem(1, item) 120 | item = QtWidgets.QTableWidgetItem() 121 | self.tableWidget.setHorizontalHeaderItem(2, item) 122 | item = QtWidgets.QTableWidgetItem() 123 | self.tableWidget.setHorizontalHeaderItem(3, item) 124 | self.tableWidget.horizontalHeader().setHighlightSections(False) 125 | self.tableWidget.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) 126 | self.tableWidget.verticalHeader().setVisible(False) 127 | self.tableWidget.verticalHeader().setHighlightSections(False) 128 | self.verticalLayout_3.addWidget(self.tableWidget) 129 | self.verticalLayout.addWidget(self.Table) 130 | self.Output = QtWidgets.QGroupBox(self.Window) 131 | self.Output.setStyleSheet("color: rgb(209, 209, 209);\n" 132 | "font: 10pt \"微软雅黑\";\n" 133 | "color: rgb(0, 0, 0);") 134 | self.Output.setObjectName("Output") 135 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.Output) 136 | self.verticalLayout_2.setObjectName("verticalLayout_2") 137 | self.output_textarea = QtWidgets.QTextBrowser(self.Output) 138 | self.output_textarea.setStyleSheet("background-color: rgb(100, 100, 100);\n" 139 | "color: rgb(255, 255, 255);\n" 140 | "font: 9pt \"微软雅黑\";") 141 | self.output_textarea.setObjectName("output_textarea") 142 | self.verticalLayout_2.addWidget(self.output_textarea) 143 | self.verticalLayout.addWidget(self.Output) 144 | MainWindow.setCentralWidget(self.Window) 145 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 146 | self.statusbar.setObjectName("statusbar") 147 | MainWindow.setStatusBar(self.statusbar) 148 | 149 | self.retranslateUi(MainWindow) 150 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 151 | 152 | # 按钮事件绑定 153 | self.config_btn.clicked.connect(self.show_config) 154 | self.login_btn.clicked.connect(self.show_login) 155 | self.active_btn.clicked.connect(self.active_clicked) 156 | 157 | # 绑定信号槽 158 | self.add_message_signal.connect(self.add_message) 159 | self.add_course_signal.connect(self.add_course) 160 | self.del_course_signal.connect(self.del_course) 161 | 162 | # 配置文件检查 163 | dir_route = get_config_dir() 164 | config_route = get_config_path() 165 | self.config = self.check_config(dir_route, config_route) 166 | 167 | self.add_message_signal.emit("当前版本:v0.0.4",0) 168 | self.add_message_signal.emit("初始化完成",0) 169 | 170 | # 登录状态检查 171 | status, user_info = self.check_login() 172 | if status: 173 | self.login_btn.setText("重新登录") 174 | self.add_message_signal.emit("登录成功,当前登录用户:"+user_info["name"],0) 175 | else: 176 | self.show_login() 177 | 178 | def retranslateUi(self, MainWindow): 179 | _translate = QtCore.QCoreApplication.translate 180 | MainWindow.setWindowTitle(_translate("MainWindow", "荷塘雨课堂")) 181 | self.label_2.setText(_translate("MainWindow", "荷塘雨课堂")) 182 | self.active_btn.setText(_translate("MainWindow", "启用")) 183 | self.login_btn.setText(_translate("MainWindow", "登录")) 184 | self.config_btn.setText(_translate("MainWindow", "配置")) 185 | self.Table.setTitle(_translate("MainWindow", "监听列表")) 186 | self.Output.setTitle(_translate("MainWindow", "信息")) 187 | item = self.tableWidget.horizontalHeaderItem(0) 188 | item.setText(_translate("MainWindow", "课程名")) 189 | item = self.tableWidget.horizontalHeaderItem(1) 190 | item.setText(_translate("MainWindow", "课程标题")) 191 | item = self.tableWidget.horizontalHeaderItem(2) 192 | item.setText(_translate("MainWindow", "教师")) 193 | item = self.tableWidget.horizontalHeaderItem(3) 194 | item.setText(_translate("MainWindow", "上课时间")) 195 | self.Output.setTitle(_translate("MainWindow", "信息")) 196 | 197 | def add_course(self, row, row_count): 198 | # 添加课程 199 | # 注意:在非UI运行线程中调用该方法请对add_course_signal发送信号 200 | self.tableWidget.insertRow(row_count) 201 | # 装填当前行各列数据 202 | for i in range(len(row)): 203 | content = QtWidgets.QTableWidgetItem(str(row[i])) 204 | self.tableWidget.setItem(row_count,i,content) 205 | 206 | # 存储各行PersistentModelIndex,便于后续删除操作 207 | model_index = self.tableWidget.indexFromItem(content) 208 | per_model_index = QtCore.QPersistentModelIndex(model_index) 209 | self.table_index.append((row_count,per_model_index)) 210 | 211 | def del_course(self, index): 212 | # 删除课程 213 | # 注意:在非UI运行线程中调用该方法请对del_course_signal发送信号 214 | for row_count,per_model_index in self.table_index: 215 | # 搜索传入index所对应的QPersistentModelIndex,并进行删除 216 | # QPersistentModelIndex对象的索引在删除过程中不会被已删除元素影响 217 | if row_count == index: 218 | self.tableWidget.removeRow(per_model_index.row()) 219 | self.table_index.remove((row_count,per_model_index)) 220 | 221 | def show_config(self): 222 | # 展示配置对话框 223 | dialog = QtWidgets.QDialog() 224 | config_ui = Config_Ui() 225 | config_ui.setupUi(dialog) 226 | # 加载配置文件 227 | config_ui.load_config(self.config) 228 | # 这里需要刷新一次自定义延迟部分的Widget 229 | config_ui.enable_delay_custom() 230 | if dialog.exec_(): 231 | config_route = get_config_path() 232 | with open(config_route,"r") as f: 233 | self.config = json.load(f) 234 | 235 | def show_login(self, _bool=False, rtn_message=""): 236 | # 展示登录对话框 237 | # rtn_message用于展示上次扫码登录失败的信息 238 | dialog = QtWidgets.QDialog() 239 | login_ui = Login_Ui() 240 | login_ui.setupUi(dialog) 241 | # 加载配置文件,主要用于加载sessionid 242 | login_ui.load_config(self.config) 243 | 244 | # 将最下方登录返回值栏设置rtn_message 245 | login_ui.login_return.setText(rtn_message) 246 | 247 | # 登录成功返回1,其他情况返回0 248 | success = dialog.exec_() 249 | if success: 250 | config_route = get_config_path() 251 | with open(config_route,"r") as f: 252 | self.config = json.load(f) 253 | # 删除涉及登录的线程 254 | login_ui.close_all() 255 | # 再次检测登录状态 256 | status, user_info = self.check_login() 257 | if status and success: 258 | self.add_message_signal.emit("登录成功,当前登录用户:"+user_info["name"],0) 259 | self.login_btn.setText("重新登录") 260 | if not status: 261 | self.show_login(rtn_message="登录失败,请重新登录") 262 | 263 | def check_config(self, dir_route, config_route): 264 | # 检查配置文件 265 | # 检查目录是否存在 266 | if not os.path.exists(dir_route): 267 | os.makedirs(dir_route) 268 | # 检查配置文件存在性及可用性 269 | if not os.path.exists(config_route): 270 | initial_data = get_initial_data() 271 | f = open(config_route,"w+") 272 | json.dump(initial_data,f) 273 | f.close() 274 | self.add_message_signal.emit("没有检测到配置文件,已自动创建",0) 275 | return initial_data 276 | else: 277 | try: 278 | with open(config_route,"r") as f: 279 | data = json.load(f) 280 | self.add_message_signal.emit("配置文件已读取",0) 281 | return data 282 | except: 283 | with open(config_route,"w+") as f: 284 | initial_data = get_initial_data() 285 | json.dump(initial_data,f) 286 | self.add_message_signal.emit("配置文件读取失败,已重新生成",0) 287 | return initial_data 288 | 289 | def check_login(self): 290 | # 检查登录状态 291 | code, user_info = get_user_info(self.config["sessionid"]) 292 | if code == 50000: 293 | return False,user_info 294 | elif code == 0: 295 | return True,user_info 296 | 297 | def add_message(self, message, type=0): 298 | # 新增输出信息,并尝试语音播报 299 | time = datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S] ") 300 | self.output_textarea.append(time + message) 301 | if not type == 0: 302 | self.audio(message,type) 303 | 304 | def active_clicked(self): 305 | # 启动按钮被点击 306 | if self.is_active: 307 | self.deactive() 308 | else: 309 | self.active() 310 | 311 | def active(self): 312 | # 启动 313 | self.monitor_t = threading.Thread(target=monitor,args=(self,),daemon=True) 314 | self.monitor_t.start() 315 | self.is_active = True 316 | self.active_btn.setText("停止监听") 317 | self.add_message_signal.emit("启动成功",0) 318 | 319 | def deactive(self): 320 | # 停止 321 | self.active_btn.setText("停止中...") 322 | self.active_btn.setEnabled(False) 323 | # 强制刷新UI 324 | QtWidgets.qApp.processEvents() 325 | self.is_active = False 326 | self.monitor_t.join() 327 | self.active_btn.setEnabled(True) 328 | self.active_btn.setText("启动") 329 | self.add_message_signal.emit("停止成功",0) 330 | 331 | def audio(self, message, type): 332 | ''' 333 | type 334 | 0: 默认,未分类音频 335 | 1: 自动发送弹幕成功 336 | 2: 他人弹幕发送 337 | 3: 收到题目 338 | 4: 自动答题情况 339 | 5: 自己被点名 340 | 6: 他人被点名 341 | 7: 课程相关 342 | 8: 网络断开/重连 343 | ''' 344 | # 尝试播放语音提示 345 | audio_on = self.config["audio_on"] 346 | if audio_on: 347 | audio_type = self.config["audio_config"]["audio_type"] 348 | if \ 349 | (type == 1 and audio_type["send_danmu"]) or \ 350 | (type == 2 and audio_type["others_danmu"]) or \ 351 | (type == 3 and audio_type["receive_problem"]) or \ 352 | (type == 4 and audio_type["answer_result"]) or \ 353 | (type == 5 and audio_type["im_called"]) or \ 354 | (type == 6 and audio_type["others_called"]) or \ 355 | (type == 7 and audio_type["course_info"]) or \ 356 | (type == 8 and audio_type["network_info"]) : 357 | threading.Thread(target=say_something,args=(message,),daemon=True).start() -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt5 import QtWidgets 3 | from UI.MainWindow import MainWindow_Ui 4 | 5 | if __name__ == "__main__": 6 | # 初始化 7 | app = QtWidgets.QApplication(sys.argv) 8 | main = QtWidgets.QMainWindow() 9 | ui = MainWindow_Ui() 10 | ui.setupUi(main) 11 | main.show() 12 | # 启动监听 13 | ui.active() 14 | # 主窗体循环 15 | app.exec_() 16 | 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.17.4 2 | certifi==2024.8.30 3 | charset-normalizer==2.0.12 4 | comtypes==1.4.7 5 | idna==3.10 6 | packaging==24.1 7 | pefile==2024.8.26 8 | pillow==10.4.0 9 | pyinstaller==6.10.0 10 | pyinstaller-hooks-contrib==2024.8 11 | pypiwin32==223 12 | PyQt5==5.15.7 13 | PyQt5-Qt5==5.15.2 14 | PyQt5_sip==12.15.0 15 | pyttsx3==2.90 16 | pywin32==306 17 | pywin32-ctypes==0.2.3 18 | requests==2.27.1 19 | setuptools==75.1.0 20 | six==1.16.0 21 | urllib3==2.2.3 22 | websocket-client==1.4.0 23 | wheel==0.44.0 24 | --------------------------------------------------------------------------------