diff --git a/.gitignore b/.gitignore index c9c33cb..6a51811 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ whisper_models/ release_*/ *.json .idea -.DS_Store \ No newline at end of file +.DS_Store +auth.txt diff --git a/README.md b/README.md index 6593c9e..7e3ba46 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,43 @@ ![image-20231018204208066](md/README/image-20231018204208066.png) -### 网页 +### 登陆延河课堂 + +新版的延河课堂要求登陆才能查看课程列表,故需要先自行登陆延河课堂。登陆后,在延河课堂的页面的地址栏输入如下代码(注意,浏览器会自动去掉前缀"javascript:",故直接复制粘贴后需手动补上): + +``` +javascript:alert(JSON.parse(localStorage.auth).token) +``` + +![image-20240809182406184](md/README/image-20240809182406184.png) + +回车后会弹出提示框,复制该身份认证码。 + +![image-20240809182413373](md/README/image-20240809182413373.png) + +或者可以按`F12`键打开”控制台“,在其中输入上述代码,也能得到身份认证码。 + +### 网页GUI交互 双击运行`webui_interface.exe`文件打开网页服务器,会自动弹出浏览器网页。 而后在打开的网页中新建任务即可。 -![image-20240529174739174](md/README/image-20240529174739174.png) +下载类型可选摄像头(即教室后的摄像头录像)或电脑屏幕(即教室电脑的屏幕信号)。 + +可以选择是否下载教室蓝牙话筒信号(该课程有蓝牙话筒信号时有效),若老师未使用蓝牙话筒则该信号没有声音。 + +![image-20240529171709402](md/README/image-20240529171709402.png) + +首次使用或之前的登陆失效时,需要输入上述获取的身份认证码。 + +若之前使用过本工具(包括其他交互方式),登陆未失效,身份认证码会自动保存,无需每次都填写。 + +![image-20240809182420653](md/README/image-20240809182420653.png) + +下载完成的文件在`output/`目录下以`课程名-video/screen`格式命名的文件夹中。若下载了蓝牙音频则保存在和视频同目录同名的`.aac`文件中。 + +![image-20230926124922726](md/README/image-20230926124922726.png) ### 命令行GUI交互 @@ -32,29 +62,29 @@ ![image-20240413001734218](md/README/image-20240413001734218.png) +同样,首次使用或之前的登陆失效时,需要输入上述获取的身份认证码;登陆未失效则不用。 + +![image-20240809183350633](md/README/image-20240809183350633.png) + image-20240413002004628 按键盘上下键移动光标,按空格选择/取消选择,至少需要选择一个视频。选择完成后按回车确认。若想退出按q键即可。 -确认后,选择要下载的信号,可选摄像头(即教室后的摄像头录像)或电脑屏幕(即教室电脑的屏幕信号),同样至少需要选择一个信号,选择完成后按回车确认。 +确认后,选择要下载的信号,同样至少需要选择一个信号,选择完成后按回车确认。 ![image-20240413002242979](md/README/image-20240413002242979.png) -而后选择是否下载教室蓝牙话筒信号,若老师未使用蓝牙话筒则该信号没有声音。选择完成后按回车确认。开始下载。按`ctrl+c`停止。 - -![image-20240529171709402](md/README/image-20240529171709402.png) +而后选择是否下载教室蓝牙话筒信号,选择完成后按回车确认。开始下载。按`ctrl+c`停止。 ![image-20240529171253980](md/README/image-20240529171253980.png) -下载完成的文件在`output/`目录下以`课程名-video/screen`格式命名的文件夹中。若下载了蓝牙音频则保存在和视频同目录同名的`.aac`文件中。 -![image-20230926124922726](md/README/image-20230926124922726.png) ### 原始交互方式 -若使用上述GUI显示有问题,可直接使用原始交互方式。双击运行`main.exe`文件,并输入你想下载的课程编号(40524)。输出课程视频列表: +若使用上述GUI显示有问题,可直接使用原始交互方式。双击运行`main.exe`文件,并输入你想下载的课程编号(40524)和身份认证码(如果需要)。输出课程视频列表: ![image-20240529171540279](md/README/image-20240529171540279.png) @@ -122,7 +152,7 @@ pip install pyinstaller # 打包 pyinstaller -F main.py pyinstaller -F gui.py -pyinstaller -F webui_interface.py --add-data webui:webui +pyinstaller -F webui_interface.py --add-data webui:webui --add-data templates:templates pyinstaller -F gen_caption.py ``` 打包`gen_caption.py`时可能会失败,提示递归过深: diff --git a/gui.py b/gui.py index 59aa5f8..82b9543 100644 --- a/gui.py +++ b/gui.py @@ -120,12 +120,22 @@ def config(stdscr): draw_line(stdscr, f"{url_base}", 1) # 等待用户输入字符串并显示它 - courseID = stdscr.getstr() + courseID = stdscr.getstr().decode("utf-8") if not courseID: sys.exit() - videoList, courseName, professor = utils.get_course_info( - courseID=courseID.decode("utf-8") - ) + + if not utils.read_auth() or not utils.test_auth(courseID=courseID): + stdscr.clear() + for i, line in enumerate(utils.auth_prompt()): + draw_line(stdscr, line, i) + auth = stdscr.getstr().decode("utf-8") + utils.write_auth(auth) + if not utils.test_auth(courseID=courseID): + stdscr.clear() + draw_line(stdscr, "身份验证失败", 0) + stdscr.getch() + sys.exit() + videoList, courseName, professor = utils.get_course_info(courseID=courseID) selected_videos = [] @@ -170,6 +180,7 @@ def get_cmd_window_size(stdscr): return stdscr.getmaxyx() +@utils.print_help def main(): global align align = 25 diff --git a/main.py b/main.py index 66d1551..192e10a 100644 --- a/main.py +++ b/main.py @@ -16,14 +16,23 @@ } +@utils.print_help def main(): if len(sys.argv) == 1: courseID = input("输 入 课 程 ID: ") else: courseID = sys.argv[1] + if not utils.read_auth() or not utils.test_auth(courseID=courseID): + auth = input("。".join(utils.auth_prompt())) + utils.write_auth(auth) + if not utils.test_auth(courseID=courseID): + print("身份验证失败") + sys.exit() videoList, courseName, professor = utils.get_course_info(courseID=courseID) + print(f"课 程 名: {courseName}") + for i, c in enumerate(videoList): print(f"[{i}]: ", c["title"]) @@ -59,16 +68,4 @@ def main(): if __name__ == "__main__": - try: - main() - # cProfile.run('main()', 'output/profile.txt') - except Exception as e: - print(e) - print( - "If the problem is still not solved, you can report an issue in https://github.com/AuYang261/BIT_yanhe_download/issues." - ) - print("Or contact with the author xu_jyang@163.com. Thanks for your report!") - print( - "如果问题仍未解决,您可以在https://github.com/AuYang261/BIT_yanhe_download/issues 中报告问题。" - ) - print("或者联系作者xu_jyang@163.com。感谢您的报告!") + main() diff --git a/md/README/image-20240529174739174.png b/md/README/image-20240529174739174.png deleted file mode 100644 index 46ff7bb..0000000 Binary files a/md/README/image-20240529174739174.png and /dev/null differ diff --git a/md/README/image-20240809182344017.png b/md/README/image-20240809182344017.png new file mode 100644 index 0000000..ecd4fb9 Binary files /dev/null and b/md/README/image-20240809182344017.png differ diff --git a/md/README/image-20240809182406184.png b/md/README/image-20240809182406184.png new file mode 100644 index 0000000..ecd4fb9 Binary files /dev/null and b/md/README/image-20240809182406184.png differ diff --git a/md/README/image-20240809182413373.png b/md/README/image-20240809182413373.png new file mode 100644 index 0000000..8b1ad04 Binary files /dev/null and b/md/README/image-20240809182413373.png differ diff --git a/md/README/image-20240809182420653.png b/md/README/image-20240809182420653.png new file mode 100644 index 0000000..fcd524c Binary files /dev/null and b/md/README/image-20240809182420653.png differ diff --git a/md/README/image-20240809183350633.png b/md/README/image-20240809183350633.png new file mode 100644 index 0000000..b1646a1 Binary files /dev/null and b/md/README/image-20240809183350633.png differ diff --git a/webui/index.html b/templates/index.html similarity index 91% rename from webui/index.html rename to templates/index.html index 723abad..4c21db7 100644 --- a/webui/index.html +++ b/templates/index.html @@ -24,6 +24,9 @@
+ +

{{ auth_prompt }}

+ diff --git a/utils.py b/utils.py index fc728c2..3a227c4 100644 --- a/utils.py +++ b/utils.py @@ -4,6 +4,7 @@ import m3u8dl import time from hashlib import md5 +import os headers = { @@ -11,10 +12,25 @@ "Referer": "https://www.yanhekt.cn/", "xdomain-client": "web_user", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.26", + "Xdomain-Client": "web_user", + "Xclient-Signature": "e6e3bf5851b0e4af888cb4bc1938c568", + "Xclient-Version": "v1", + "Xclient-Timestamp": str(int(time.time())), + "Authorization": "", } magic = "1tJrMwNq3h0yLgx86Rued2J1tFc" +def auth_prompt(code=True): + return [ + "请先在浏览器登陆延河课堂", + "并在延河课堂的地址栏输入 javascript:alert(JSON.parse(localStorage.auth).token)", + '注意粘贴时浏览器会自动去掉"javascript:",需要手动补上', + "或者按F12打开控制台粘贴这段代码", + "然后将弹出的内容粘贴到" + ("这里:" if code else '"身份认证码"栏'), + ] + + def encryptURL(url): url_list = url.split("/") # "a97f12c055a10ee51d60e441e618bfef" @@ -33,6 +49,14 @@ def getToken(): "https://cbiz.yanhekt.cn/v1/auth/video/token?id=0", headers=headers ) data = req.json()["data"] + if not data: + read_auth() + req = requests.get( + "https://cbiz.yanhekt.cn/v1/auth/video/token?id=0", headers=headers + ) + data = req.json()["data"] + if not data: + raise Exception("获取Token失败") return data["token"] @@ -50,6 +74,39 @@ def add_signature_for_url(url, token, timestamp, signature): return url +def read_auth(): + if not os.path.exists("auth.txt"): + return "" + with open("auth.txt", "r") as f: + auth = f.read().strip() + headers["Authorization"] = "Bearer " + auth + return auth + + +def write_auth(auth): + headers["Authorization"] = "Bearer " + auth + with open("auth.txt", "w") as f: + f.write(auth) + + +def remove_auth(): + headers["Authorization"] = "" + if os.path.exists("auth.txt"): + os.remove("auth.txt") + + +def test_auth(courseID): + """ + Test if the auth in headers is valid. + Return True if the auth is valid, otherwise False. + """ + res = requests.get( + f"https://cbiz.yanhekt.cn/v2/course/session/list?course_id={courseID}", + headers=headers, + ) + return bool(res.json()["data"]) + + def get_course_info(courseID): courseID = courseID.strip() @@ -104,3 +161,23 @@ def download_audio(url, path, name): res = requests.get(url, headers=_headers) with open(f"{path}/{name}.aac", "wb") as f: f.write(res.content) + + +def print_help(f: callable): + def wrap(): + try: + f() + except Exception as e: + print(e) + print( + "If the problem is still not solved, you can report an issue in https://github.com/AuYang261/BIT_yanhe_download/issues." + ) + print( + "Or contact with the author xu_jyang@163.com. Thanks for your report!" + ) + print( + "如果问题仍未解决,您可以在https://github.com/AuYang261/BIT_yanhe_download/issues 中报告问题。" + ) + print("或者联系作者xu_jyang@163.com。感谢您的报告!") + + return wrap diff --git a/webui/script.js b/webui/script.js index 85bf823..7b22f49 100644 --- a/webui/script.js +++ b/webui/script.js @@ -8,10 +8,14 @@ document.getElementsByClassName("close")[0].onclick = function () { // Implement the logic to fetch course number and handle form submission function fetchCourseNumber() { - fetch(`/get_course?course_id=${document.getElementById("courseId").value}`) + fetch(`/get_course?course_id=${document.getElementById("courseId").value}&auth=${document.getElementById("auth").value}`) .then((response) => response.json()) .then((data) => { console.log(data); + if (data.code && data.code == 403) { + document.getElementById("auth_prompt").innerHTML = data.msg; + alert(data.msg); + } document.getElementById("courseList").innerHTML = ``; document.getElementById("courseName11").innerHTML = `课程名: ${data.courseName == "" ? "未知" : data.courseName }`; @@ -186,4 +190,4 @@ function selectAll(select) { for (let i = 0; i < list.childNodes.length; i++) { list.childNodes[i].className = select ? "selected" : ""; } -} \ No newline at end of file +} diff --git a/webui_interface.py b/webui_interface.py index 009aa05..9f27069 100644 --- a/webui_interface.py +++ b/webui_interface.py @@ -1,4 +1,12 @@ -from flask import Flask, request, jsonify, send_from_directory +from flask import ( + Flask, + request, + jsonify, + send_from_directory, + render_template, + url_for, + redirect, +) import os import requests import m3u8dl @@ -134,12 +142,23 @@ def execute_tasks(): @app.route("/") def index(): - return send_from_directory(app.static_folder, "index.html") + auth = utils.read_auth() + return render_template( + "index.html", + auth=auth, + auth_prompt="" if auth else "。".join(utils.auth_prompt()), + ) @app.route("/get_course") def get_course(): course_id = request.args.get("course_id") + auth = request.args.get("auth") + if auth: + utils.write_auth(auth) + if not utils.test_auth(courseID=course_id): + utils.remove_auth() + return jsonify({"code": 403, "msg": "。".join(utils.auth_prompt(False))}) try: videoList, courseName, professor = utils.get_course_info(courseID=course_id) except: