@@ -10,7 +10,7 @@ from tkinter import ttk, messagebox, filedialog
1010import os , platform
1111import sys
1212from functools import partial
13- import base64 , tempfile
13+ import base64 , tempfile , pyperclip
1414import threading , requests , psutil
1515import json , re
1616
@@ -61,7 +61,7 @@ def parse(url: str) -> tuple[str, str, str] | tuple[None, None, None]: # 解析
6161 """
6262 # 其中 $.ti_items 的每一项对应一个资源
6363
64- if re .search (r"^https?://([^/]+)/syncClassroom/basicWork/detail" , url ): # 对于“基础性作业”的解析
64+ if re .search (r"^https?://([^/]+)/syncClassroom/basicWork/detail" , url ): # 对于 “基础性作业” 的解析
6565 response = session .get (f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/special_edu/resources/details/{ content_id } .json" )
6666 else : # 对于课本的解析
6767 if content_type == "thematic_course" : # 对专题课程(含电子课本、视频等)的解析
@@ -105,8 +105,8 @@ def download_file(url: str, save_path: str) -> None: # 下载文件
105105 # 服务器返回 401 或 403 状态码
106106 if response .status_code == 401 or response .status_code == 403 :
107107 messagebox .showerror ("授权失败" , "Access Token 可能已过期或无效,请重新设置后再试!" )
108- open_access_token_window ()
109- download_btn .config (state = "normal" ) # 当弹出 “设置 token ” 窗口后,恢复下载按钮
108+ show_access_token_window ()
109+ download_btn .config (state = "normal" ) # 当弹出 “设置 Token ” 窗口后,恢复下载按钮
110110 return
111111 if response .status_code >= 400 :
112112 messagebox .showerror ("下载失败" , f"下载失败,服务器返回状态码:{ response .status_code } " )
@@ -158,6 +158,25 @@ def format_bytes(size: float) -> str: # 将数据单位进行格式化,返回
158158 size /= 1024.0
159159 return f"{ size :3.1f} PB"
160160
161+ def parse_and_copy () -> None : # 解析并复制链接
162+ urls = [line .strip () for line in url_text .get ("1.0" , tk .END ).splitlines () if line .strip ()] # 获取所有非空行
163+ resource_links = []
164+ failed_links = []
165+
166+ for url in urls :
167+ resource_url = parse (url )[0 ]
168+ if not resource_url :
169+ failed_links .append (url ) # 添加到失败链接
170+ continue
171+ resource_links .append (resource_url )
172+
173+ if failed_links :
174+ messagebox .showwarning ("警告" , "以下 “行” 无法解析:\n " + "\n " .join (failed_links )) # 显示警告对话框
175+
176+ if resource_links :
177+ pyperclip .copy ("\n " .join (resource_links )) # 将链接复制到剪贴板
178+ messagebox .showinfo ("提示" , "资源链接已复制到剪贴板" )
179+
161180def download () -> None : # 下载资源文件
162181 global download_states
163182 download_btn .config (state = "disabled" ) # 设置下载按钮为禁用状态
@@ -197,18 +216,20 @@ def download() -> None: # 下载资源文件
197216 thread_it (download_file , (resource_url , save_path )) # 开始下载(多线程,防止窗口卡死)
198217
199218 if failed_links :
200- messagebox .showwarning ("警告" , "以下“行”无法解析:\n " + "\n " .join (failed_links )) # 显示警告对话框
219+ messagebox .showwarning ("警告" , "以下 “行” 无法解析:\n " + "\n " .join (failed_links )) # 显示警告对话框
201220 download_btn .config (state = "normal" ) # 设置下载按钮为启用状态
202221
203222 if not urls and not failed_links :
204223 download_btn .config (state = "normal" ) # 设置下载按钮为启用状态
205224
206- def open_access_token_window () -> None : # 打开输入 Access Token 的窗口
225+ def show_access_token_window () -> None : # 打开输入 Access Token 的窗口
207226 token_window = tk .Toplevel (root )
208227 token_window .title ("设置 Access Token" )
209228 # 让窗口自动根据控件自适应尺寸;如需最小尺寸可用 token_window.minsize(...)
210229
211- token_window .protocol ("WM_DELETE_WINDOW" , lambda : token_window .destroy ())
230+ token_window .focus_force () # 自动获得焦点
231+ token_window .grab_set () # 阻止主窗口操作
232+ token_window .bind ("<Escape>" , lambda event : token_window .destroy ()) # 绑定 Esc 键关闭窗口
212233
213234 # 设置一个 Frame 用于留白、布局更美观
214235 frame = ttk .Frame (token_window , padding = 20 )
@@ -252,7 +273,7 @@ def open_access_token_window() -> None: # 打开输入 Access Token 的窗口
252273 def save_token ():
253274 user_token = token_text .get ("1.0" , tk .END ).strip ()
254275 tip_info = set_access_token (user_token )
255- # 重新启用“下载”按钮 ,并提示用户
276+ # 重新启用下载按钮 ,并提示用户
256277 download_btn .config (state = "normal" )
257278 # 显示提示
258279 messagebox .showinfo ("提示" , tip_info )
@@ -266,14 +287,21 @@ def open_access_token_window() -> None: # 打开输入 Access Token 的窗口
266287 def show_token_help ():
267288 help_win = tk .Toplevel (token_window )
268289 help_win .title ("获取 Access Token 方法" )
290+
291+ help_win .focus_force () # 自动获得焦点
292+ help_win .grab_set () # 阻止主窗口操作
293+ help_win .bind ("<Escape>" , lambda event : help_win .destroy ()) # 绑定 Esc 键关闭窗口
294+
269295 help_frame = ttk .Frame (help_win , padding = 20 )
270296 help_frame .pack (fill = "both" , expand = True )
271297
272298 help_text = """\
273- 自 2025 年 2 月起, 国家中小学智慧教育平台需要登录后才可获取教材,因此要使用本程序下载教材,您需要在平台内登录账号(如没有需注册),然后获得登录凭据(Access Token)。本程序仅保存该凭据至本地。
299+ 国家中小学智慧教育平台需要登录后才可获取教材,因此要使用本程序下载教材,您需要在平台内登录账号(如没有需注册),然后获得登录凭据(Access Token)。本程序仅保存该凭据至本地。
274300
275301获取方法如下:
276- 请先在浏览器登录国家中小学智慧教育平台(https://auth.smartedu.cn/uias/login),然后按 F12 或 Ctrl+Shift+I 或 右键-检查(审查元素),打开开发人员工具,点击“控制台(Console)”选项卡,在里面粘贴以下代码后回车(Enter):
302+ 1. 打开浏览器,访问国家中小学智慧教育平台(https://auth.smartedu.cn/uias/login)并登录账号。
303+ 2. 按下 F12 或 Ctrl+Shift+I,或右键——检查(审查元素)打开开发者工具,选择控制台(Console)。
304+ 3. 在控制台粘贴以下代码后回车(Enter):
277305---------------------------------------------------------
278306(function() {
279307 const authKey = Object.keys(localStorage).find(key => key.startsWith("ND_UC_AUTH"));
@@ -307,7 +335,7 @@ def open_access_token_window() -> None: # 打开输入 Access Token 的窗口
307335 help_btn = ttk .Button (frame , text = "如何获取?" , command = show_token_help )
308336 help_btn .pack (pady = 5 )
309337
310- # 让弹窗大致居中
338+ # 让弹窗居中
311339 token_window .update_idletasks ()
312340 w = token_window .winfo_width ()
313341 h = token_window .winfo_height ()
@@ -412,6 +440,8 @@ def thread_it(func, args: tuple = ()) -> None: # args 为元组,且默认值
412440
413441# 初始化请求
414442session = requests .Session ()
443+ # 初始化下载状态
444+ download_states = []
415445# 设置请求头部,包含认证信息
416446access_token = None
417447headers = { "X-ND-AUTH" : 'MAC id="0",nonce="0",mac="0"' } # “MAC id”等同于“access_token”,“nonce”和“mac”不可缺省但无需有效
@@ -532,6 +562,10 @@ def set_icon() -> None: # 设置窗口图标
532562thread_it (set_icon ) # 设置窗口图标耗时较长,为不影响窗口绘制,采用多线程
533563
534564def on_closing () -> None : # 处理窗口关闭事件
565+ if not all (state ["finished" ] for state in download_states ): # 当正在下载时,询问用户
566+ if not messagebox .askokcancel ("提示" , "下载任务未完成,是否退出?" ):
567+ return
568+
535569 current_process = psutil .Process (os .getpid ()) # 获取自身的进程 ID
536570 child_processes = current_process .children (recursive = True ) # 获取自身的所有子进程
537571
@@ -687,13 +721,17 @@ for i in range(8):
687721 drops .append (drop )
688722
689723# 按钮:设置 Token
690- token_btn = ttk .Button (container_frame , text = "设置 Token" , command = open_access_token_window )
724+ token_btn = ttk .Button (container_frame , text = "设置 Token" , command = show_access_token_window )
691725token_btn .pack (side = "left" , padx = int (5 * scale ), pady = int (5 * scale ), ipady = int (5 * scale ))
692726
693727# 按钮:下载
694728download_btn = ttk .Button (container_frame , text = "下载" , command = download )
695729download_btn .pack (side = "right" , padx = int (5 * scale ), pady = int (5 * scale ), ipady = int (5 * scale ))
696730
731+ # 按钮:解析并复制
732+ copy_btn = ttk .Button (container_frame , text = "解析并复制" , command = parse_and_copy )
733+ copy_btn .pack (side = "right" , padx = int (5 * scale ), pady = int (5 * scale ), ipady = int (5 * scale ))
734+
697735# 下载进度条
698736download_progress_bar = ttk .Progressbar (container_frame , length = (125 * scale ), mode = "determinate" ) # 添加下载进度条
699737download_progress_bar .pack (side = "bottom" , padx = int (40 * scale ), pady = int (10 * scale ), ipady = int (5 * scale )) # 设置水平外边距、垂直外边距(跟随缩放),设置进度条高度(跟随缩放)
0 commit comments