wechat-article-parser 0.0.10__tar.gz → 0.0.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/PKG-INFO +11 -7
  2. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/README.md +10 -6
  3. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/pyproject.toml +1 -1
  4. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/src/wechat_article_parser/models.py +5 -2
  5. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/src/wechat_article_parser/parser.py +4 -0
  6. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_models.py +10 -6
  7. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_parser.py +6 -2
  8. wechat_article_parser-0.0.11/tests/test_product_content.py +83 -0
  9. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_type_routing.py +8 -3
  10. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/.gitignore +0 -0
  11. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/LICENSE +0 -0
  12. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/src/wechat_article_parser/__init__.py +0 -0
  13. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/conftest.py +0 -0
  14. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_markdown.py +0 -0
  15. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_paid_content.py +0 -0
  16. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_profile_content.py +0 -0
  17. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_repost_content.py +0 -0
  18. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_svg_content.py +0 -0
  19. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_swiper_content.py +0 -0
  20. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_user_name.py +0 -0
  21. {wechat_article_parser-0.0.10 → wechat_article_parser-0.0.11}/tests/test_video_content.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: wechat-article-parser
3
- Version: 0.0.10
3
+ Version: 0.0.11
4
4
  Summary: WeChat MP article parser - extract metadata and content from WeChat public account articles
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -23,10 +23,11 @@ Description-Content-Type: text/markdown
23
23
  - 转载式文章
24
24
  - 纯文本文章
25
25
  - 视频分享类文章
26
+ - 音频分享类文章
26
27
  - 小红书风格图片轮播文章
27
28
  - 全屏布局短文本文章
28
29
 
29
- 解析时优先读取页面全局的 `item_show_type`:`0` 对应图文、`5` 对应视频分享、
30
+ 解析时优先读取页面全局的 `item_show_type`:`0` 对应图文、`5` 对应视频分享、`7` 对应音频分享、
30
31
  `8` 对应图片轮播、`10` 对应文字消息,再通过 HTML 或脚本数据区分具体模板。
31
32
  类型字段缺失、值未知或对应分支未提取到正文时,会按原有 HTML 特征匹配顺序兜底。
32
33
 
@@ -35,6 +36,8 @@ Description-Content-Type: text/markdown
35
36
  并保留公开元数据;已有的可见预览正文仍正常提取。
36
37
  图文中的公众号推荐卡片按原文顺序转换为 `**公众号名称**:简介`,每张卡片一段;
37
38
  名称或简介缺失时,仅输出已有字段。
39
+ 图文正文仅包含商品广告卡片(`mp-common-cpsad`)时,Markdown 返回“这是一篇商品推荐内容。”;
40
+ 已有的文字和图片仍正常提取。关键元数据完整时占位结果也有效,不表示已获取商品详情。
38
41
 
39
42
  ## 安装
40
43
 
@@ -119,15 +122,15 @@ result = parse(
119
122
  | `article_title` | `str` | 文章标题 |
120
123
  | `article_cover_image` | `str` | 文章封面图链接 |
121
124
  | `article_description` | `str` | 文章摘要 |
122
- | `article_markdown` | `str` | 文章正文的 Markdown 内容;视频分享没有文字附言时可为空 |
125
+ | `article_markdown` | `str` | 文章正文的 Markdown 内容;视频或音频分享没有文字附言时可为空 |
123
126
  | `article_publish_time` | `int` | 发布时间(Unix 时间戳) |
124
- | `article_display_type` | `ArticleDisplayType` | 文章展示类型:`article`、`video`、`image`、`text` 或 `unknown` |
125
- | `article_item_show_type` | `int \| None` | 微信页面中的原始展示类型:`0` 图文、`5` 视频、`8` 图片轮播、`10` 文字;其他已提取的数值原样保留,缺失或无法解析时为 `None` |
127
+ | `article_display_type` | `ArticleDisplayType` | 文章展示类型:`article`、`video`、`audio`、`image`、`text` 或 `unknown` |
128
+ | `article_item_show_type` | `int \| None` | 微信页面中的原始展示类型:`0` 图文、`5` 视频、`7` 音频、`8` 图片轮播、`10` 文字;其他已提取的数值原样保留,缺失或无法解析时为 `None` |
126
129
  | `raw_html` | `str` | 原始 HTML 网页源代码(仅在 `include_raw_html=True` 时填充,否则为空字符串) |
127
130
  | `images` | `list[str]` | 文章中提取的所有图片链接 |
128
131
  | `is_valid` | `bool` | 关键字段是否全部解析成功(属性) |
129
132
 
130
- `is_valid` 为 `True` 的条件:`mp_id`、`mp_name`、`article_id`、`article_msg_id`、`article_idx`、`article_sn`、`article_title`、`article_publish_time` 均不为空/零,且 `article_markdown` 非空或 `article_display_type == "video"`。视频分享允许没有文字正文,仍须满足其他关键字段要求;有效性不表示已获取视频文件。
133
+ `is_valid` 为 `True` 的条件:`mp_id`、`mp_name`、`article_id`、`article_msg_id`、`article_idx`、`article_sn`、`article_title`、`article_publish_time` 均不为空/零,且 `article_markdown` 非空或 `article_display_type in ("video", "audio")`。视频和音频分享允许没有文字正文,仍须满足其他关键字段要求;有效性不表示已获取视频或音频文件。
131
134
 
132
135
  付费受限页面返回提示且关键元数据完整时,`is_valid` 也为 `True`,这不表示已获取付费正文。
133
136
 
@@ -139,6 +142,7 @@ result = parse(
139
142
  |---|---|---|
140
143
  | `ArticleDisplayType.ARTICLE` | `article` | 图文,包括转载式 |
141
144
  | `ArticleDisplayType.VIDEO` | `video` | 视频分享 |
145
+ | `ArticleDisplayType.AUDIO` | `audio` | 音频分享 |
142
146
  | `ArticleDisplayType.IMAGE` | `image` | 图片轮播 |
143
147
  | `ArticleDisplayType.TEXT` | `text` | 文字消息 |
144
148
  | `ArticleDisplayType.UNKNOWN` | `unknown` | 页面类型缺失或未支持 |
@@ -212,7 +216,7 @@ except httpx.HTTPStatusError as e:
212
216
 
213
217
  ### 解析不完整
214
218
 
215
- 缺少标题、发布时间等关键字段,或非视频分享缺少正文时,解析结果的 `is_valid` 会返回 `False`。封面图、摘要等非必填字段为空不影响有效性。建议在业务逻辑中检查:
219
+ 缺少标题、发布时间等关键字段,或非视频/音频分享缺少正文时,解析结果的 `is_valid` 会返回 `False`。封面图、摘要等非必填字段为空不影响有效性。建议在业务逻辑中检查:
216
220
 
217
221
  ```python
218
222
  result = parse("https://mp.weixin.qq.com/s/xxxxx")
@@ -8,10 +8,11 @@
8
8
  - 转载式文章
9
9
  - 纯文本文章
10
10
  - 视频分享类文章
11
+ - 音频分享类文章
11
12
  - 小红书风格图片轮播文章
12
13
  - 全屏布局短文本文章
13
14
 
14
- 解析时优先读取页面全局的 `item_show_type`:`0` 对应图文、`5` 对应视频分享、
15
+ 解析时优先读取页面全局的 `item_show_type`:`0` 对应图文、`5` 对应视频分享、`7` 对应音频分享、
15
16
  `8` 对应图片轮播、`10` 对应文字消息,再通过 HTML 或脚本数据区分具体模板。
16
17
  类型字段缺失、值未知或对应分支未提取到正文时,会按原有 HTML 特征匹配顺序兜底。
17
18
 
@@ -20,6 +21,8 @@
20
21
  并保留公开元数据;已有的可见预览正文仍正常提取。
21
22
  图文中的公众号推荐卡片按原文顺序转换为 `**公众号名称**:简介`,每张卡片一段;
22
23
  名称或简介缺失时,仅输出已有字段。
24
+ 图文正文仅包含商品广告卡片(`mp-common-cpsad`)时,Markdown 返回“这是一篇商品推荐内容。”;
25
+ 已有的文字和图片仍正常提取。关键元数据完整时占位结果也有效,不表示已获取商品详情。
23
26
 
24
27
  ## 安装
25
28
 
@@ -104,15 +107,15 @@ result = parse(
104
107
  | `article_title` | `str` | 文章标题 |
105
108
  | `article_cover_image` | `str` | 文章封面图链接 |
106
109
  | `article_description` | `str` | 文章摘要 |
107
- | `article_markdown` | `str` | 文章正文的 Markdown 内容;视频分享没有文字附言时可为空 |
110
+ | `article_markdown` | `str` | 文章正文的 Markdown 内容;视频或音频分享没有文字附言时可为空 |
108
111
  | `article_publish_time` | `int` | 发布时间(Unix 时间戳) |
109
- | `article_display_type` | `ArticleDisplayType` | 文章展示类型:`article`、`video`、`image`、`text` 或 `unknown` |
110
- | `article_item_show_type` | `int \| None` | 微信页面中的原始展示类型:`0` 图文、`5` 视频、`8` 图片轮播、`10` 文字;其他已提取的数值原样保留,缺失或无法解析时为 `None` |
112
+ | `article_display_type` | `ArticleDisplayType` | 文章展示类型:`article`、`video`、`audio`、`image`、`text` 或 `unknown` |
113
+ | `article_item_show_type` | `int \| None` | 微信页面中的原始展示类型:`0` 图文、`5` 视频、`7` 音频、`8` 图片轮播、`10` 文字;其他已提取的数值原样保留,缺失或无法解析时为 `None` |
111
114
  | `raw_html` | `str` | 原始 HTML 网页源代码(仅在 `include_raw_html=True` 时填充,否则为空字符串) |
112
115
  | `images` | `list[str]` | 文章中提取的所有图片链接 |
113
116
  | `is_valid` | `bool` | 关键字段是否全部解析成功(属性) |
114
117
 
115
- `is_valid` 为 `True` 的条件:`mp_id`、`mp_name`、`article_id`、`article_msg_id`、`article_idx`、`article_sn`、`article_title`、`article_publish_time` 均不为空/零,且 `article_markdown` 非空或 `article_display_type == "video"`。视频分享允许没有文字正文,仍须满足其他关键字段要求;有效性不表示已获取视频文件。
118
+ `is_valid` 为 `True` 的条件:`mp_id`、`mp_name`、`article_id`、`article_msg_id`、`article_idx`、`article_sn`、`article_title`、`article_publish_time` 均不为空/零,且 `article_markdown` 非空或 `article_display_type in ("video", "audio")`。视频和音频分享允许没有文字正文,仍须满足其他关键字段要求;有效性不表示已获取视频或音频文件。
116
119
 
117
120
  付费受限页面返回提示且关键元数据完整时,`is_valid` 也为 `True`,这不表示已获取付费正文。
118
121
 
@@ -124,6 +127,7 @@ result = parse(
124
127
  |---|---|---|
125
128
  | `ArticleDisplayType.ARTICLE` | `article` | 图文,包括转载式 |
126
129
  | `ArticleDisplayType.VIDEO` | `video` | 视频分享 |
130
+ | `ArticleDisplayType.AUDIO` | `audio` | 音频分享 |
127
131
  | `ArticleDisplayType.IMAGE` | `image` | 图片轮播 |
128
132
  | `ArticleDisplayType.TEXT` | `text` | 文字消息 |
129
133
  | `ArticleDisplayType.UNKNOWN` | `unknown` | 页面类型缺失或未支持 |
@@ -197,7 +201,7 @@ except httpx.HTTPStatusError as e:
197
201
 
198
202
  ### 解析不完整
199
203
 
200
- 缺少标题、发布时间等关键字段,或非视频分享缺少正文时,解析结果的 `is_valid` 会返回 `False`。封面图、摘要等非必填字段为空不影响有效性。建议在业务逻辑中检查:
204
+ 缺少标题、发布时间等关键字段,或非视频/音频分享缺少正文时,解析结果的 `is_valid` 会返回 `False`。封面图、摘要等非必填字段为空不影响有效性。建议在业务逻辑中检查:
201
205
 
202
206
  ```python
203
207
  result = parse("https://mp.weixin.qq.com/s/xxxxx")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wechat-article-parser"
7
- version = "0.0.10"
7
+ version = "0.0.11"
8
8
  description = "WeChat MP article parser - extract metadata and content from WeChat public account articles"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -28,6 +28,7 @@ class ArticleDisplayType(str, Enum):
28
28
  UNKNOWN = "unknown"
29
29
  ARTICLE = "article"
30
30
  VIDEO = "video"
31
+ AUDIO = "audio"
31
32
  IMAGE = "image"
32
33
  TEXT = "text"
33
34
 
@@ -77,7 +78,7 @@ class ArticleResult:
77
78
 
78
79
  @property
79
80
  def is_valid(self) -> bool:
80
- """检查关键字段是否解析成功;视频分享允许正文为空。"""
81
+ """检查关键字段是否解析成功;视频和音频分享允许正文为空。"""
81
82
  return bool(
82
83
  self.mp_id
83
84
  and self.mp_name
@@ -88,7 +89,9 @@ class ArticleResult:
88
89
  and self.article_title
89
90
  and (
90
91
  self.article_markdown
91
- or self.article_display_type == ArticleDisplayType.VIDEO
92
+ or self.article_display_type in (
93
+ ArticleDisplayType.VIDEO, ArticleDisplayType.AUDIO,
94
+ )
92
95
  )
93
96
  and self.article_publish_time
94
97
  )
@@ -522,6 +522,8 @@ def _extract_rich_media_content(content_tag: Tag, result: ArticleResult) -> None
522
522
  result.article_markdown = "这是一篇付费内容,正文需付费后查看。"
523
523
  elif has_video:
524
524
  result.article_markdown = "这是一篇纯视频内容。"
525
+ elif soup.find("mp-common-cpsad") is not None:
526
+ result.article_markdown = "这是一篇商品推荐内容。"
525
527
 
526
528
 
527
529
  def _extract_repost_content(
@@ -660,12 +662,14 @@ _CONTENT_SELECTORS = {
660
662
  _ITEM_SHOW_TYPE_BRANCHES = {
661
663
  0: ("rich_text", "repost"),
662
664
  5: ("video",),
665
+ 7: ("video",), # 音频分享复用视频分享的文字附言提取。
663
666
  8: ("swiper",),
664
667
  10: ("plain_text", "fullscreen"),
665
668
  }
666
669
  _ARTICLE_DISPLAY_TYPES = {
667
670
  0: ArticleDisplayType.ARTICLE,
668
671
  5: ArticleDisplayType.VIDEO,
672
+ 7: ArticleDisplayType.AUDIO,
669
673
  8: ArticleDisplayType.IMAGE,
670
674
  10: ArticleDisplayType.TEXT,
671
675
  }
@@ -22,14 +22,16 @@ _COMPLETE = ArticleResult(
22
22
 
23
23
  @pytest.mark.parametrize("display_type", list(ArticleDisplayType))
24
24
  @pytest.mark.parametrize("markdown", ["", "正文"])
25
- def test_only_video_can_be_valid_without_markdown(
25
+ def test_only_video_and_audio_can_be_valid_without_markdown(
26
26
  display_type: ArticleDisplayType, markdown: str,
27
27
  ) -> None:
28
28
  result = replace(
29
29
  _COMPLETE, article_display_type=display_type, article_markdown=markdown,
30
30
  )
31
31
 
32
- assert result.is_valid is (bool(markdown) or display_type == ArticleDisplayType.VIDEO)
32
+ assert result.is_valid is (
33
+ bool(markdown) or display_type in (ArticleDisplayType.VIDEO, ArticleDisplayType.AUDIO)
34
+ )
33
35
 
34
36
 
35
37
  @pytest.mark.parametrize(
@@ -45,13 +47,14 @@ def test_only_video_can_be_valid_without_markdown(
45
47
  ("article_publish_time", 0),
46
48
  ],
47
49
  )
48
- @pytest.mark.parametrize("markdown", ["", "视频附言"])
49
- def test_video_still_requires_all_key_metadata(
50
- field_name: str, empty_value: str | int, markdown: str,
50
+ @pytest.mark.parametrize("markdown", ["", "媒体附言"])
51
+ @pytest.mark.parametrize("display_type", [ArticleDisplayType.VIDEO, ArticleDisplayType.AUDIO])
52
+ def test_video_and_audio_still_require_all_key_metadata(
53
+ field_name: str, empty_value: str | int, markdown: str, display_type: ArticleDisplayType,
51
54
  ) -> None:
52
55
  result = replace(
53
56
  _COMPLETE,
54
- article_display_type=ArticleDisplayType.VIDEO,
57
+ article_display_type=display_type,
55
58
  article_markdown=markdown,
56
59
  **{field_name: empty_value},
57
60
  )
@@ -64,6 +67,7 @@ def test_video_still_requires_all_key_metadata(
64
67
  [
65
68
  "article",
66
69
  "video",
70
+ "audio",
67
71
  "image",
68
72
  "text",
69
73
  "unknown",
@@ -28,6 +28,10 @@ TEST_URLS = [
28
28
  "https://mp.weixin.qq.com/s/3ixeC9whuEgpup2G1yvDZg",
29
29
  # 纯公众号推荐卡片正文。
30
30
  "https://mp.weixin.qq.com/s/PXZuh3fFEi3VXUUI1uBnlw",
31
+ # 无文字正文的音频分享。
32
+ "https://mp.weixin.qq.com/s/y4QxlYs7u0X9TdvZatCfIA",
33
+ # 纯商品广告卡片正文。
34
+ "https://mp.weixin.qq.com/s/CBmoIFPJwV4BkidYGp-l1Q",
31
35
  ]
32
36
 
33
37
 
@@ -102,8 +106,8 @@ def _assert_result(result: ArticleResult, url: str) -> None:
102
106
  assert result.article_idx > 0, "article_idx should be positive"
103
107
  assert result.article_sn, "article_sn should not be empty"
104
108
  assert result.article_title, "article_title should not be empty"
105
- if result.article_display_type != "video":
106
- assert result.article_markdown, "article_markdown should not be empty for non-video articles"
109
+ if result.article_display_type not in ("video", "audio"):
110
+ assert result.article_markdown, "article_markdown should not be empty for non-video/audio articles"
107
111
  assert result.article_publish_time > 0, "article_publish_time should be positive"
108
112
  assert result.is_valid
109
113
 
@@ -0,0 +1,83 @@
1
+ """商品卡片占位提示及混合正文的回归测试。"""
2
+
3
+ import pytest
4
+
5
+ from wechat_article_parser import ArticleDisplayType
6
+ from wechat_article_parser.parser import _parse_html
7
+
8
+
9
+ _URL = "https://mp.weixin.qq.com/s/CBmoIFPJwV4BkidYGp-l1Q"
10
+ _NOTICE = "这是一篇商品推荐内容。"
11
+ _CARD = '<section><mp-common-cpsad data-pid="104_630468802"></mp-common-cpsad></section>'
12
+ _METADATA = """
13
+ var biz = "MzkwNDQyNzY2NQ==";
14
+ var mid = "2247501127";
15
+ var idx = "2";
16
+ var sn = "bc04bcc079a380252791ace32e8a91ba";
17
+ window.__allowLoadResFromMp = true;
18
+ var hd_head_img = "https://example.com/avatar";
19
+ var nickname = htmlDecode("老鱼讲古");
20
+ var oriCreateTime = '1791711648';
21
+ """
22
+
23
+
24
+ def _html(body: str, metadata: str = _METADATA) -> str:
25
+ return (
26
+ '<meta property="og:title" content="推荐几本好书">'
27
+ f'<div class="rich_media_content" id="js_content">{body}</div>'
28
+ '<script type="text/javascript">var item_show_type = "0";</script>'
29
+ f'<script type="text/javascript">{metadata}</script>'
30
+ )
31
+
32
+
33
+ @pytest.mark.parametrize("metadata", [_METADATA, ""])
34
+ def test_product_only_body_returns_one_notice_and_requires_metadata(metadata: str) -> None:
35
+ result = _parse_html(_URL, _html(
36
+ _CARD * 4 + '<section><span><br/></span></section>'
37
+ '<p style="display: none;"><mp-style-type data-value="3"></mp-style-type></p>',
38
+ metadata,
39
+ ))
40
+
41
+ assert result.article_markdown == _NOTICE
42
+ assert result.article_display_type is ArticleDisplayType.ARTICLE
43
+ assert result.article_item_show_type == 0
44
+ assert result.article_title == "推荐几本好书"
45
+ assert result.images == []
46
+ assert result.is_valid is bool(metadata)
47
+ if metadata:
48
+ assert result.mp_name == "老鱼讲古"
49
+ assert result.article_msg_id == 2247501127
50
+
51
+
52
+ @pytest.mark.parametrize(("body", "expected", "images"), [
53
+ ('<p>推荐书单。</p>' + _CARD + '<p>阅读愉快。</p>',
54
+ '推荐书单。\n\n阅读愉快。', []),
55
+ (_CARD + '<img data-src="https://example.com/book.jpg">',
56
+ '![](https://example.com/book.jpg)', ['https://example.com/book.jpg']),
57
+ ])
58
+ def test_product_cards_do_not_replace_visible_content(
59
+ body: str, expected: str, images: list[str],
60
+ ) -> None:
61
+ result = _parse_html(_URL, _html(body))
62
+
63
+ assert result.article_markdown == expected
64
+ assert result.images == images
65
+ assert result.is_valid
66
+
67
+
68
+ def test_product_card_outside_body_does_not_fill_empty_body() -> None:
69
+ result = _parse_html(_URL, _html("") + _CARD)
70
+
71
+ assert result.article_markdown == ""
72
+ assert not result.is_valid
73
+
74
+
75
+ @pytest.mark.parametrize(("embed", "expected"), [
76
+ ('<mp-pay-preview-filter></mp-pay-preview-filter>', '这是一篇付费内容,正文需付费后查看。'),
77
+ ('<iframe class="wx_video_iframe"></iframe>', '这是一篇纯视频内容。'),
78
+ ])
79
+ def test_existing_notices_keep_priority(embed: str, expected: str) -> None:
80
+ result = _parse_html(_URL, _html(_CARD + embed))
81
+
82
+ assert result.article_markdown == expected
83
+ assert result.is_valid
@@ -49,6 +49,7 @@ def _html(body: str = "", script: str = "", title: str = "原标题") -> str:
49
49
  ("window.item_show_type = '8' || '';", 8),
50
50
  ("window.item_show_type = 10;", 10),
51
51
  ("var /* 注释 */ item_show_type = 5;", 5),
52
+ ("window.item_show_type = '7';", 7),
52
53
  ("(function() { window.item_show_type = '10'; })();", 10),
53
54
  ("window.item_show_type = '999';", 999),
54
55
  ("window.item_show_type = '';", None),
@@ -120,6 +121,7 @@ def test_type_zero_uses_html_to_distinguish_rich_text_and_repost(
120
121
  ("item_type", "data", "expected", "display_type"),
121
122
  [
122
123
  (5, _PLAIN_DATA, "文字正文", ArticleDisplayType.VIDEO),
124
+ (7, _PLAIN_DATA, "文字正文", ArticleDisplayType.AUDIO),
123
125
  (8, _SWIPER_DATA, "轮播正文", ArticleDisplayType.IMAGE),
124
126
  (10, _PLAIN_DATA, "文字正文", ArticleDisplayType.TEXT),
125
127
  (10, _FULLSCREEN_DATA, "全屏正文", ArticleDisplayType.TEXT),
@@ -153,7 +155,7 @@ def test_missing_unknown_or_invalid_type_falls_back_to_html(declaration: str) ->
153
155
  assert result.article_display_type is ArticleDisplayType.UNKNOWN
154
156
 
155
157
 
156
- @pytest.mark.parametrize("item_type", [5, 8, 10])
158
+ @pytest.mark.parametrize("item_type", [5, 7, 8, 10])
157
159
  def test_known_type_without_matching_data_falls_back_to_html(item_type: int) -> None:
158
160
  result = parser._parse_html(
159
161
  _URL,
@@ -215,9 +217,10 @@ def test_unrecognized_content_still_returns_metadata() -> None:
215
217
  [
216
218
  ("", ArticleDisplayType.UNKNOWN),
217
219
  ("window.item_show_type = '5';", ArticleDisplayType.VIDEO),
220
+ ("window.item_show_type = '7';", ArticleDisplayType.AUDIO),
218
221
  ],
219
222
  )
220
- def test_video_without_description_keeps_type_and_metadata(
223
+ def test_video_and_audio_without_description_keep_type_and_metadata(
221
224
  declaration: str, expected_type: ArticleDisplayType,
222
225
  ) -> None:
223
226
  metadata = """
@@ -247,4 +250,6 @@ def test_video_without_description_keeps_type_and_metadata(
247
250
  assert result.mp_id == 1
248
251
  assert result.mp_name == "测试公众号"
249
252
  assert result.article_markdown == ""
250
- assert result.is_valid is (expected_type == ArticleDisplayType.VIDEO)
253
+ assert result.is_valid is (
254
+ expected_type in (ArticleDisplayType.VIDEO, ArticleDisplayType.AUDIO)
255
+ )