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 @@

-### 网页
+### 登陆延河课堂
+
+新版的延河课堂要求登陆才能查看课程列表,故需要先自行登陆延河课堂。登陆后,在延河课堂的页面的地址栏输入如下代码(注意,浏览器会自动去掉前缀"javascript:",故直接复制粘贴后需手动补上):
+
+```
+javascript:alert(JSON.parse(localStorage.auth).token)
+```
+
+
+
+回车后会弹出提示框,复制该身份认证码。
+
+
+
+或者可以按`F12`键打开”控制台“,在其中输入上述代码,也能得到身份认证码。
+
+### 网页GUI交互
双击运行`webui_interface.exe`文件打开网页服务器,会自动弹出浏览器网页。
而后在打开的网页中新建任务即可。
-
+下载类型可选摄像头(即教室后的摄像头录像)或电脑屏幕(即教室电脑的屏幕信号)。
+
+可以选择是否下载教室蓝牙话筒信号(该课程有蓝牙话筒信号时有效),若老师未使用蓝牙话筒则该信号没有声音。
+
+
+
+首次使用或之前的登陆失效时,需要输入上述获取的身份认证码。
+
+若之前使用过本工具(包括其他交互方式),登陆未失效,身份认证码会自动保存,无需每次都填写。
+
+
+
+下载完成的文件在`output/`目录下以`课程名-video/screen`格式命名的文件夹中。若下载了蓝牙音频则保存在和视频同目录同名的`.aac`文件中。
+
+
### 命令行GUI交互
@@ -32,29 +62,29 @@

+同样,首次使用或之前的登陆失效时,需要输入上述获取的身份认证码;登陆未失效则不用。
+
+
+
按键盘上下键移动光标,按空格选择/取消选择,至少需要选择一个视频。选择完成后按回车确认。若想退出按q键即可。
-确认后,选择要下载的信号,可选摄像头(即教室后的摄像头录像)或电脑屏幕(即教室电脑的屏幕信号),同样至少需要选择一个信号,选择完成后按回车确认。
+确认后,选择要下载的信号,同样至少需要选择一个信号,选择完成后按回车确认。

-而后选择是否下载教室蓝牙话筒信号,若老师未使用蓝牙话筒则该信号没有声音。选择完成后按回车确认。开始下载。按`ctrl+c`停止。
-
-
+而后选择是否下载教室蓝牙话筒信号,选择完成后按回车确认。开始下载。按`ctrl+c`停止。

-下载完成的文件在`output/`目录下以`课程名-video/screen`格式命名的文件夹中。若下载了蓝牙音频则保存在和视频同目录同名的`.aac`文件中。
-
### 原始交互方式
-若使用上述GUI显示有问题,可直接使用原始交互方式。双击运行`main.exe`文件,并输入你想下载的课程编号(40524)。输出课程视频列表:
+若使用上述GUI显示有问题,可直接使用原始交互方式。双击运行`main.exe`文件,并输入你想下载的课程编号(40524)和身份认证码(如果需要)。输出课程视频列表:

@@ -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: