diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3407c8f..ccbd1b6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -93,4 +93,4 @@ body: id: extra attributes: label: 补充说明 - description: 其他您认为有帮助的信息,例如出问题的教材链接(不含 Token)。 + description: 其他您认为有帮助的信息,例如出问题的资源链接(不含 Token)。 diff --git a/README.md b/README.md index 8e4c104..ff20ffe 100644 --- a/README.md +++ b/README.md @@ -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/)。 - 所下载资源的**版权归原平台及相关权利人所有**,请仅用于个人学习与教学参考,**请勿用于商业用途或二次分发**。 - 使用本工具时请遵守该平台的服务条款及您所在地区的法律法规。因使用本工具产生的任何后果由使用者自行承担。 - 本项目与国家中小学智慧教育平台**没有任何隶属或合作关系**。 diff --git a/src/tchmaterial_parser/api.py b/src/tchmaterial_parser/api.py index d9eeb69..bb13214 100644 --- a/src/tchmaterial_parser/api.py +++ b/src/tchmaterial_parser/api.py @@ -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 diff --git a/src/tchmaterial_parser/catalog.py b/src/tchmaterial_parser/catalog.py index 877469a..bc1fbe3 100644 --- a/src/tchmaterial_parser/catalog.py +++ b/src/tchmaterial_parser/catalog.py @@ -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]: # 按完整分类路径筛选资源树 diff --git a/src/tchmaterial_parser/ui/download_panel.py b/src/tchmaterial_parser/ui/download_panel.py index e463c31..0e63a82 100644 --- a/src/tchmaterial_parser/ui/download_panel.py +++ b/src/tchmaterial_parser/ui/download_panel.py @@ -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") # 恢复下载按钮为启用状态 diff --git a/src/tchmaterial_parser/ui/resource_tree.py b/src/tchmaterial_parser/ui/resource_tree.py index f3e3ac7..8069953 100644 --- a/src/tchmaterial_parser/ui/resource_tree.py +++ b/src/tchmaterial_parser/ui/resource_tree.py @@ -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" diff --git a/src/tchmaterial_parser/ui/token_window.py b/src/tchmaterial_parser/ui/token_window.py index 299992f..5f787ab 100644 --- a/src/tchmaterial_parser/ui/token_window.py +++ b/src/tchmaterial_parser/ui/token_window.py @@ -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)并登录账号。