feat: 添加对课程资源、精品课的下载支持

Signed-off-by: 晨叶梦春 <wuziqian211@126.com>
This commit is contained in:
晨叶梦春
2026-08-03 00:58:26 +08:00
parent 1bf83d4bba
commit 7bdd2030da
7 changed files with 116 additions and 39 deletions
+1 -1
View File
@@ -93,4 +93,4 @@ body:
id: extra
attributes:
label: 补充说明
description: 其他您认为有帮助的信息,例如出问题的教材链接(不含 Token)。
description: 其他您认为有帮助的信息,例如出问题的资源链接(不含 Token)。
+2 -2
View File
@@ -53,7 +53,7 @@
- 📂 **自动命名文件**:工具会自动使用电子课本的名称作为默认文件名,方便管理下载的课本文件。
- 🔖 **自动添加书签**:若开启 “添加 PDF 书签”,则会在下载完成后为电子课本添加书签,在查看 PDF 时可更方便地跳转到指定位置。
- 🔑 **支持 Access Token**:支持用户[手动输入 Access Token](#2--设置-access-token可选) 并自动保存,下次启动可自动加载。
- 🔎 **教材快速搜索**:可按教材名称或 “学段、学科、年级” 等分类组合搜索,结果会自动展开;长名称支持横向滚动,悬停时可查看完整信息和大尺寸封面。
- 🔎 **资源快速搜索**:可按资源名称或 “学段、学科、年级” 等分类组合搜索,结果会自动展开;长名称支持横向滚动,悬停时可查看完整信息和大尺寸封面。
- 🖥️ **高 DPI 适配**:优化 UI 以适配高分辨率屏幕,避免界面模糊问题。
- 🌗 **深色模式**:启动时自动跟随系统的浅色/深色模式,也可点击右上角的按钮手动切换,切换结果会被记住。
- 💻 **跨平台支持**:支持 Windows、Linux、macOS 等操作系统(需要图形界面)。
@@ -250,7 +250,7 @@ https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document&content
## ⚖️ 免责声明
- 本工具**仅提供下载上的便利**,不存储、不托管、不分发任何教材内容,所有资源均直接来自[国家中小学智慧教育平台](https://basic.smartedu.cn/)。
- 本工具**仅提供下载上的便利**,不存储、不托管、不分发任何资源内容,所有资源均直接来自[国家中小学智慧教育平台](https://basic.smartedu.cn/)。
- 所下载资源的**版权归原平台及相关权利人所有**,请仅用于个人学习与教学参考,**请勿用于商业用途或二次分发**。
- 使用本工具时请遵守该平台的服务条款及您所在地区的法律法规。因使用本工具产生的任何后果由使用者自行承担。
- 本项目与国家中小学智慧教育平台**没有任何隶属或合作关系**。
+59 -17
View File
@@ -19,13 +19,26 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
if "contentId" in params:
content_id = params["contentId"][0]
elif re.search(r"^https?://([^/]+)/syncClassroom/classActivity", url): # 课程资源
content_type = "national_lesson"
if "activityId" in params:
content_id = params["activityId"][0]
else:
return None
elif re.search(r"^https?://([^/]+)/qualityCourse", url): # 精品课
content_type = "quality_course"
if "courseId" in params:
content_id = params["courseId"][0]
else:
return None
else:
return None
if "contentType" in params:
content_type = params["contentType"][0]
else:
content_type = "assets_document"
if not content_type:
if "contentType" in params:
content_type = params["contentType"][0]
else:
content_type = "assets_document"
# 2. 获取资源的信息
# 返回数据示例:
@@ -60,19 +73,20 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
"""
# 其中 $.ti_items 的每一项对应一个资源
if re.search(r"^https?://([^/]+)/syncClassroom/basicWork/detail", url): # 对基础性作业的解析
if re.search(r"^https?://([^/]+)/tchMaterial/detail", url) and content_type == "assets_document": # 对普通电子课本的解析
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrv2/resources/tch_material/details/{content_id}.json")
elif content_type == "national_lesson": # 对课程资源的解析
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrv2/national_lesson/resources/details/{content_id}.json")
elif content_type == "quality_course": # 对精品课的解析
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrv2/resources/{content_id}.json")
else: # 对专题课程(含电子课本、视频等)、其他类型资源的解析
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/special_edu/resources/details/{content_id}.json")
else: # 对课本的解析
if content_type == "thematic_course": # 对专题课程(含电子课本、视频等)的解析
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/special_edu/resources/details/{content_id}.json")
else: # 对普通电子课本的解析
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrv2/resources/tch_material/details/{content_id}.json")
data: dict = response.json()
# 3. 获取资源标题、下载链接及章节目录
def get_resource_info(resource_data: dict) -> tuple[str, str, str, list[dict]] | None:
title: str = resource_data.get("title")
def get_resource_info(resource_data: dict, root_title: str | None = None) -> tuple[str, str, str, list[dict]] | None:
title: str = f"{root_title} - {resource_data.get('title') or resource_data.get('id')}" if root_title else resource_data.get("title") or resource_data.get("id")
resource_url: str | None = None
resource_format = "pdf"
@@ -93,6 +107,24 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
continue
break
if not resource_url: # 使用不同的判断条件寻找源文件
for item in resource_data["ti_items"]:
if not item["ti_file_flag"] in ("source", "pdf"):
continue
resource_format = item.get("ti_format") or "pdf"
if resource_format == "folder":
continue
resource_url = item.get("ti_storage")
if resource_url:
resource_url = resource_url.replace("cs_path:${ref-path}", "https://r1-ndr-private.ykt.cbern.com.cn")
else:
resource_url = next((url for url in item["ti_storages"] if url), None)
if not resource_url:
continue
break
if not resource_url:
return None
@@ -167,17 +199,27 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
return title, resource_url, resource_format, chapters
resource_info = get_resource_info(data)
if resource_info:
resources_info.append(resource_info)
if content_type == "thematic_course": # 专题课程
resources_resp = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/special_edu/thematic_course/{content_id}/resources/list.json")
resources_data: list[dict] = resources_resp.json()
for resource in resources_data:
resource_info = get_resource_info(resource)
resource_info = get_resource_info(resource, data["title"])
if resource_info:
resources_info.append(resource_info)
elif content_type == "national_lesson": # 课程资源
for resource in data["relations"]["national_course_resource"]:
resource_info = get_resource_info(resource, data["title"])
if resource_info:
resources_info.append(resource_info)
elif content_type == "quality_course": # 精品课
for resource in data["relations"]["course_resource"]:
resource_info = get_resource_info(resource, data["title"])
if resource_info:
resources_info.append(resource_info)
else: # 其他类型资源
resource_info = get_resource_info(data)
if resource_info:
resources_info.append(resource_info)
return resources_info
+50 -16
View File
@@ -31,18 +31,15 @@ class ResourceHelper: # 获取网站上资源的数据
for book in book_data:
if book.get("tag_paths"): # 某些非课本资料的 tag_paths 属性为空数组
# 解析课本层级数据
tag_paths: list[str] = book["tag_paths"][0].split("/")[2:] # 电子课本 tag_paths 的前两项为“教材”、“电子教材”
# 如果课本层级数据不在层级数据中,跳过
temp_hier = parsed_hier[book["tag_paths"][0].split("/")[1]]
if not tag_paths[0] in temp_hier["children"]:
continue
tag_paths: list[str] = book["tag_paths"][0].split("/")
# 分别解析课本层级
for p in tag_paths:
if temp_hier["children"] and temp_hier["children"].get(p):
temp_hier = temp_hier["children"].get(p)
if not temp_hier["children"]:
temp_hier = parsed_hier[tag_paths[1]]
for p in tag_paths[2:]: # 电子课本 tag_paths 的前两项为 “教材”、“电子教材”
if temp_hier.get("children") and temp_hier["children"].get(p):
temp_hier = temp_hier["children"][p]
if not temp_hier.get("children"):
temp_hier["children"] = {}
book["display_name"] = book["title"] if "title" in book else book["name"] if "name" in book else f"(未知电子课本 {book['id']})"
@@ -51,11 +48,11 @@ class ResourceHelper: # 获取网站上资源的数据
return parsed_hier
def fetch_lesson_list(self) -> dict: # 获取课件列表
def fetch_national_lesson_list(self) -> dict: # 获取自学课件列表
# 获取课件层级数据
tags_resp = session.get("https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/tags/national_lesson_tag.json")
tags_data: dict = tags_resp.json()
parsed_hier = self.parse_hierarchy([{ "children": [{ "tag_id": "__internal_national_lesson", "hierarchies": tags_data["hierarchies"], "tag_name": "课件资源" }] }])
parsed_hier = self.parse_hierarchy([{ "children": [{ "tag_id": "__internal_national_lesson", "hierarchies": tags_data["hierarchies"], "tag_name": "学生自主学习课件" }] }])
# 获取课件 URL 列表
list_resp = session.get("https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/national_lesson/teachingmaterials/version/data_version.json")
@@ -73,12 +70,48 @@ class ResourceHelper: # 获取网站上资源的数据
# 分别解析课件层级(tag_paths 为乱序)
def parse_tag_path(hier: dict) -> dict:
for p in tag_paths:
if hier["children"] and hier["children"].get(p):
return parse_tag_path(hier["children"].get(p))
if hier.get("children") and hier["children"].get(p):
return parse_tag_path(hier["children"][p])
return hier
hier = parse_tag_path(parsed_hier["__internal_national_lesson"])
if not hier["children"]:
if not hier.get("children"):
hier["children"] = {}
lesson["display_name"] = lesson["title"] if "title" in lesson else lesson["name"] if "name" in lesson else f"(未知课件 {lesson['id']})"
hier["children"][lesson["id"]] = lesson
return parsed_hier
def fetch_prepare_lesson_list(self) -> dict: # 获取备课课件列表
# 获取课件层级数据
tags_resp = session.get("https://s-file-2.ykt.cbern.com.cn/zxx/ndrs/tags/k12.json")
tags_data: dict = tags_resp.json()
parsed_hier = self.parse_hierarchy([{ "children": [{ "tag_id": "__internal_prepare_lesson", "hierarchies": tags_data["hierarchies"], "tag_name": "教师备课授课课件" }] }])
# 获取课件 URL 列表
list_resp = session.get("https://s-file-2.ykt.cbern.com.cn/zxx/ndrs/prepare_lesson/teachingmaterials/parts.json")
list_data: list[str] = list_resp.json()
# 获取课件列表
for url in list_data:
lesson_resp = session.get(url)
lesson_data: list[dict] = lesson_resp.json()
for lesson in lesson_data:
if lesson.get("tag_list"):
# 解析课件层级数据
tag_paths: list[str] = [tag["tag_id"] for tag in sorted(lesson["tag_list"], key=lambda tag: tag["order_num"])]
# 分别解析课件层级(tag_paths 为乱序)
def parse_tag_path(hier: dict) -> dict:
for p in tag_paths:
if hier.get("children") and hier["children"].get(p):
return parse_tag_path(hier["children"][p])
return hier
hier = parse_tag_path(parsed_hier["__internal_prepare_lesson"])
if not hier.get("children"):
hier["children"] = {}
lesson["display_name"] = lesson["title"] if "title" in lesson else lesson["name"] if "name" in lesson else f"(未知课件 {lesson['id']})"
@@ -89,7 +122,8 @@ class ResourceHelper: # 获取网站上资源的数据
def fetch_resource_list(self) -> dict: # 获取资源列表
book_hier = self.fetch_book_list()
# lesson_hier = self.fetch_lesson_list()
# national_lesson_hier = self.fetch_national_lesson_list()
# prepare_lesson_hier = self.fetch_prepare_lesson_list()
return { **book_hier }
def filter_resource_items(items: dict[str, dict], query: str) -> dict[str, dict]: # 按完整分类路径筛选资源树
+1 -1
View File
@@ -62,7 +62,7 @@ def download() -> None: # 下载资源文件
resource_urls.add(resource_url)
if len(resources_info_list) > 1:
messagebox.showinfo("提示", "您将下载多个文件,请选择要下载文件的位置,本程序将在选定的文件夹中使用教材名称作为文件名进行下载。")
messagebox.showinfo("提示", "您将下载多个文件,请选择要下载文件的位置,本程序将在选定的文件夹中使用资源名称作为文件名进行下载。")
dir_path = filedialog.askdirectory() # 选择文件夹
if not dir_path: # 用户取消或关闭对话框
download_btn.config(state="normal") # 恢复下载按钮为启用状态
+2 -1
View File
@@ -166,8 +166,9 @@ def build_resource_tree(pane: ttk.Frame, resource_list: dict[str, dict], url_tex
resource_type = resource_data.get("resource_type_code") or "assets_document"
content_id = resource_data.get("content_id") or item.split(":")[-1]
root_id = item.split(":")[0]
if resource_type == "teachingmaterials":
url = f"https://basic.smartedu.cn/syncClassroom?defaultTag={'%2F'.join(item.split(':')[1:])}"
url = f"https://basic.smartedu.cn/syncClassroom{'/prepare' if root_id == '__internal_prepare_lesson' else ''}?defaultTag={'%2F'.join(item.split(':')[1:])}"
else:
url = f"https://basic.smartedu.cn/tchMaterial/detail?contentType={resource_type}&contentId={content_id}&catalogType=tchMaterial&subCatalog=tchMaterial"
+1 -1
View File
@@ -72,7 +72,7 @@ def show_access_token_window() -> None: # 打开输入 Access Token 的窗口
help_frame.pack(fill="both", expand=True)
help_text = """\
国家中小学智慧教育平台需要登录后才可获取教材,因此要使用本程序下载教材,您需要在平台内登录账号(如没有需注册),然后获得登录凭据(Access Token)。本程序仅保存该凭据至本地。
国家中小学智慧教育平台需要登录后才可获取资源,因此要使用本程序下载资源,您需要在平台内登录账号(如没有需注册),然后获得登录凭据(Access Token)。本程序仅保存该凭据至本地。
获取方法如下:
1. 打开浏览器,访问国家中小学智慧教育平台(https://auth.smartedu.cn/uias/login)并登录账号。