Fix #43: 建议添加音频下载

Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
This commit is contained in:
Mr-Neutr0n
2026-08-03 08:47:12 +00:00
parent f70137b552
commit b4e26651e4
2 changed files with 148 additions and 0 deletions
+38
View File
@@ -85,6 +85,33 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
data: dict = response.json()
# 3. 获取资源标题、下载链接及章节目录
def get_audio_info(audio_data: dict, root_title: str | None = None) -> tuple[str, str, str, list[dict]] | None: # 解析教材关联的音频资源(如英语教材听力)
# 音频资源的标题存放在 global_title 字典中(键为语言代码,如 zh-CN)
title_data = audio_data.get("global_title")
audio_title = title_data.get("zh-CN") or title_data.get("en") if isinstance(title_data, dict) else title_data or audio_data.get("title") or audio_data.get("id")
title: str = f"{root_title} - {audio_title}" if root_title else audio_title
resource_url: str | None = None
resource_format = "mp3"
# 优先选择转码后的 MP3 文件(ti_file_flag 为 href),否则回退到源文件
for item in audio_data["ti_items"]:
if item.get("ti_file_flag") not in ("href", "source") or item.get("ti_format") != "mp3":
continue
resource_url = item.get("ti_storage") # 获取并构造资源的 URL
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.get("ti_storages") or [] if url), None)
if resource_url:
resource_format = item.get("ti_format") or "mp3"
break
if not resource_url:
return None
return title, resource_url, resource_format, []
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
@@ -221,6 +248,17 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
if resource_info:
resources_info.append(resource_info)
if content_type == "assets_document": # 教材可能带有配套的音频资源(如英语教材听力)
try:
audios_resp = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/resources/{content_id}/relation_audios.json")
audios_data: list[dict] = audios_resp.json()
for audio in audios_data:
audio_info = get_audio_info(audio, data.get("title"))
if audio_info:
resources_info.append(audio_info)
except Exception: # 音频资源不是必需的,获取失败时直接跳过
pass
return resources_info
except Exception as e:
+110
View File
@@ -0,0 +1,110 @@
import unittest
from src.tchmaterial_parser import api
class FakeResponse:
def __init__(self, json_data: object) -> None:
self._json = json_data
def json(self) -> object:
return self._json
class FakeSession:
def __init__(self, details: dict, audios: list[dict]) -> None:
self.details = details
self.audios = audios
def get(self, url: str, *args: tuple, **kwargs: dict) -> FakeResponse:
if "relation_audios.json" in url:
return FakeResponse(self.audios)
return FakeResponse(self.details)
DETAILS = {
"id": "book-1",
"title": "英语七年级上册",
"ti_items": [
{
"ti_is_source_file": True,
"ti_format": "pdf",
"ti_storage": "cs_path:${ref-path}/edu_product/esp/assets/book-1.pkg/英语七年级上册.pdf",
},
],
}
AUDIOS = [
{
"id": "audio-1",
"global_title": {"zh-CN": "1 Starter Section 2 Activity 2"},
"ti_items": [
{
"ti_file_flag": "href",
"ti_format": "mp3",
"ti_storage": "cs_path:${ref-path}/edu_product/esp/assets/audio-1.t/1.mp3",
},
{
"ti_file_flag": "source",
"ti_format": "wav",
"ti_storage": "cs_path:${ref-path}/edu_product/esp/assets/audio-1.t/1.wav",
},
],
},
{
"id": "audio-2",
"global_title": {"zh-CN": "2 Starter Section 3 Activity 1"},
"ti_items": [
{
"ti_file_flag": "href",
"ti_format": "mp3",
"ti_storage": "cs_path:${ref-path}/edu_product/esp/assets/audio-2.t/2.mp3",
},
],
},
]
class AudioParseTest(unittest.TestCase):
def setUp(self) -> None:
self.addCleanup(setattr, api, "session", api.session)
def parse_book(self) -> list[tuple[str, str, str, list[dict]]] | None:
api.session = FakeSession(DETAILS, AUDIOS)
url = "https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document&contentId=book-1&catalogType=tchMaterial&subCatalog=tchMaterial"
return api.parse(url, False)
def test_textbook_with_audio_returns_pdf_and_mp3s(self) -> None:
results = self.parse_book()
self.assertIsNotNone(results)
self.assertEqual(len(results), 3)
self.assertEqual(results[0][2], "pdf")
self.assertEqual(results[1][1], "https://r1-ndr-private.ykt.cbern.com.cn/edu_product/esp/assets/audio-1.t/1.mp3")
self.assertEqual(results[1][2], "mp3")
self.assertEqual(results[1][0], "英语七年级上册 - 1 Starter Section 2 Activity 2")
self.assertEqual(results[2][0], "英语七年级上册 - 2 Starter Section 3 Activity 1")
def test_textbook_without_audio_returns_only_pdf(self) -> None:
api.session = FakeSession(DETAILS, [])
url = "https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document&contentId=book-1&catalogType=tchMaterial&subCatalog=tchMaterial"
results = api.parse(url, False)
self.assertIsNotNone(results)
self.assertEqual(len(results), 1)
self.assertEqual(results[0][2], "pdf")
def test_audio_fetch_failure_is_ignored(self) -> None:
class FailingAudiosSession(FakeSession):
def get(self, url: str, *args: tuple, **kwargs: dict) -> FakeResponse:
if "relation_audios.json" in url:
raise RuntimeError("network error")
return super().get(url, *args, **kwargs)
api.session = FailingAudiosSession(DETAILS, [])
url = "https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document&contentId=book-1&catalogType=tchMaterial&subCatalog=tchMaterial"
results = api.parse(url, False)
self.assertIsNotNone(results)
self.assertEqual(len(results), 1)
if __name__ == "__main__":
unittest.main()