parsehub 2.1.1__tar.gz → 2.1.3__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 (73) hide show
  1. {parsehub-2.1.1/src/parsehub.egg-info → parsehub-2.1.3}/PKG-INFO +18 -3
  2. {parsehub-2.1.1 → parsehub-2.1.3}/README.md +16 -1
  3. {parsehub-2.1.1 → parsehub-2.1.3}/pyproject.toml +3 -2
  4. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/base/ytdlp.py +2 -2
  5. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/bilibili.py +6 -5
  6. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/douyin.py +26 -13
  7. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/bilibili.py +4 -4
  8. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/coolapk.py +6 -4
  9. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/douyin.py +1 -1
  10. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/instagram.py +5 -5
  11. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/twitter.py +1 -1
  12. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/xhs.py +2 -2
  13. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/zhihu.py +4 -4
  14. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/types/result.py +13 -13
  15. {parsehub-2.1.1 → parsehub-2.1.3/src/parsehub.egg-info}/PKG-INFO +18 -3
  16. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub.egg-info/requires.txt +1 -1
  17. {parsehub-2.1.1 → parsehub-2.1.3}/LICENSE +0 -0
  18. {parsehub-2.1.1 → parsehub-2.1.3}/setup.cfg +0 -0
  19. {parsehub-2.1.1 → parsehub-2.1.3}/src/__init__.py +0 -0
  20. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/__init__.py +0 -0
  21. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/cli.py +0 -0
  22. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/cli_config.py +0 -0
  23. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/config/__init__.py +0 -0
  24. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/config/config.py +0 -0
  25. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/errors.py +0 -0
  26. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/__init__.py +0 -0
  27. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/base/__init__.py +0 -0
  28. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/base/base.py +0 -0
  29. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/__init__.py +0 -0
  30. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/coolapk.py +0 -0
  31. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/facebook.py +0 -0
  32. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/instagram.py +0 -0
  33. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/kuaishou.py +0 -0
  34. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/pipix.py +0 -0
  35. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/snapchat.py +0 -0
  36. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/threads.py +0 -0
  37. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/tieba.py +0 -0
  38. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/tiktok.py +0 -0
  39. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/twitter.py +0 -0
  40. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/weibo.py +0 -0
  41. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/weixin.py +0 -0
  42. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/xhs.py +0 -0
  43. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
  44. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/youtube.py +0 -0
  45. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/zhihu.py +0 -0
  46. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/parsers/parser/zuiyou.py +0 -0
  47. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/__init__.py +0 -0
  48. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/kuaishou.py +0 -0
  49. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/pipix.py +0 -0
  50. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/threads.py +0 -0
  51. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/tieba.py +0 -0
  52. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/tiktok.py +0 -0
  53. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/weibo.py +0 -0
  54. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/weixin.py +0 -0
  55. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/xiaoheihe.py +0 -0
  56. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/provider_api/zuiyou.py +0 -0
  57. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/types/__init__.py +0 -0
  58. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/types/callback.py +0 -0
  59. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/types/media_file.py +0 -0
  60. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/types/media_ref.py +0 -0
  61. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/types/platform.py +0 -0
  62. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/types/post.py +0 -0
  63. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/utils/downloader.py +0 -0
  64. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/utils/helpers.py +0 -0
  65. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub/utils/media_info.py +0 -0
  66. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub.egg-info/SOURCES.txt +0 -0
  67. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub.egg-info/dependency_links.txt +0 -0
  68. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub.egg-info/entry_points.txt +0 -0
  69. {parsehub-2.1.1 → parsehub-2.1.3}/src/parsehub.egg-info/top_level.txt +0 -0
  70. {parsehub-2.1.1 → parsehub-2.1.3}/test/test_cli.py +0 -0
  71. {parsehub-2.1.1 → parsehub-2.1.3}/test/test_cli_config.py +0 -0
  72. {parsehub-2.1.1 → parsehub-2.1.3}/test/test_core_offline.py +0 -0
  73. {parsehub-2.1.1 → parsehub-2.1.3}/test/test_downloader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
@@ -34,7 +34,7 @@ Requires-Dist: python-slugify[unidecode]>=8.0.4
34
34
  Requires-Dist: opencv-python-headless>=4.13.0.92
35
35
  Requires-Dist: cryptography>=49.0.0
36
36
  Requires-Dist: gmssl>=3.2.2
37
- Requires-Dist: SignerPy>=0.12.0
37
+ Requires-Dist: signerpy>=0.12.0
38
38
  Provides-Extra: cli
39
39
  Requires-Dist: argcomplete>=3.6.3; extra == "cli"
40
40
  Requires-Dist: platformdirs>=4.5.1; extra == "cli"
@@ -310,6 +310,21 @@ except ParseError as exc:
310
310
 
311
311
  ---
312
312
 
313
+ ## 🤝 参与贡献
314
+
315
+ 欢迎提交 Pull Request 或 Issue!
316
+
317
+ - Bug 反馈请附上相关 URL 和日志信息
318
+
319
+ ### 开发规范
320
+
321
+ 提交代码前请至少执行:
322
+
323
+ ```bash
324
+ ruff format && ruff check --fix && uv run mypy
325
+ uv run pytest
326
+ ```
327
+
313
328
  ## 🤝 参考项目
314
329
 
315
330
  - [Evil0ctal/Douyin_TikTok_Download_API](https://github.com/Evil0ctal/Douyin_TikTok_Download_API)
@@ -321,7 +336,7 @@ except ParseError as exc:
321
336
 
322
337
  ## 📜 开源协议
323
338
 
324
- 本项目基于 [MIT License](LICENSE) 开源。
339
+ 本项目基于 [MIT License](LICENSE) 开源
325
340
 
326
341
  ---
327
342
 
@@ -268,6 +268,21 @@ except ParseError as exc:
268
268
 
269
269
  ---
270
270
 
271
+ ## 🤝 参与贡献
272
+
273
+ 欢迎提交 Pull Request 或 Issue!
274
+
275
+ - Bug 反馈请附上相关 URL 和日志信息
276
+
277
+ ### 开发规范
278
+
279
+ 提交代码前请至少执行:
280
+
281
+ ```bash
282
+ ruff format && ruff check --fix && uv run mypy
283
+ uv run pytest
284
+ ```
285
+
271
286
  ## 🤝 参考项目
272
287
 
273
288
  - [Evil0ctal/Douyin_TikTok_Download_API](https://github.com/Evil0ctal/Douyin_TikTok_Download_API)
@@ -279,7 +294,7 @@ except ParseError as exc:
279
294
 
280
295
  ## 📜 开源协议
281
296
 
282
- 本项目基于 [MIT License](LICENSE) 开源。
297
+ 本项目基于 [MIT License](LICENSE) 开源
283
298
 
284
299
  ---
285
300
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "parsehub"
3
- version = "2.1.1"
3
+ version = "2.1.3"
4
4
  description = "轻量、异步、开箱即用的社交媒体聚合解析库"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12.0"
@@ -37,7 +37,7 @@ dependencies = [
37
37
  "opencv-python-headless>=4.13.0.92",
38
38
  "cryptography>=49.0.0",
39
39
  "gmssl>=3.2.2",
40
- "SignerPy>=0.12.0",
40
+ "signerpy>=0.12.0",
41
41
  ]
42
42
 
43
43
  [project.scripts]
@@ -66,6 +66,7 @@ Issues = "https://github.com/z-mio/parsehub/issues"
66
66
 
67
67
  [tool.ruff]
68
68
  line-length = 120
69
+ exclude = ["test/_*.py"]
69
70
 
70
71
  [tool.ruff.lint]
71
72
  select = [
@@ -445,9 +445,9 @@ class YtVideoParseResult(VideoParseResult):
445
445
  def __init__(
446
446
  self,
447
447
  dl: "YtVideoInfo",
448
- title: str | None,
448
+ title: str = "",
449
449
  video: VideoRef | None = None,
450
- content: str | None = None,
450
+ content: str = "",
451
451
  ):
452
452
  """dl: yt-dlp解析结果"""
453
453
  self.dl = dl
@@ -106,7 +106,6 @@ class BiliParse(BaseParser):
106
106
  view = data["View"]
107
107
 
108
108
  cid = view["cid"]
109
- part = ""
110
109
  duration = view["duration"]
111
110
  dimension = view["dimension"]
112
111
  desc = view["desc"]
@@ -114,7 +113,6 @@ class BiliParse(BaseParser):
114
113
  if p != 1 and (pages := view.get("pages")):
115
114
  if page_info := next((i for i in pages if i["page"] == p), None):
116
115
  cid = page_info["cid"]
117
- part = page_info["part"]
118
116
  duration = page_info["duration"]
119
117
  dimension = page_info["dimension"]
120
118
 
@@ -123,9 +121,12 @@ class BiliParse(BaseParser):
123
121
 
124
122
  durl = video_playurl["data"]["durl"][0]
125
123
  video_url = self.change_source(durl["backup_url"][0]) if durl.get("backup_url") else durl["url"]
124
+ content = desc.strip()
125
+ if content == "-":
126
+ content = ""
126
127
  return BiliVideoParseResult(
127
128
  title=data["View"]["title"],
128
- content=f"P{p}: {part}" if part else (desc.strip() or ""),
129
+ content=content,
129
130
  video=VideoRef(
130
131
  url=video_url,
131
132
  thumb_url=data["View"]["pic"],
@@ -147,9 +148,9 @@ class BiliParse(BaseParser):
147
148
  )
148
149
 
149
150
  @staticmethod
150
- def hashtag_handler(desc: str | None) -> str | None:
151
+ def hashtag_handler(desc: str) -> str:
151
152
  if not desc:
152
- return None
153
+ return ""
153
154
  hashtags = re.findall(r" ?#[^#]+# ?", desc)
154
155
  for hashtag in hashtags:
155
156
  desc = desc.replace(hashtag, f" {hashtag.strip().removesuffix('#')} ")
@@ -117,13 +117,17 @@ def remove_video_watermark(url: str) -> str:
117
117
  return url.replace("playwm", "play")
118
118
 
119
119
 
120
- def parse_video_info(video_data: dict) -> dict:
121
- bit_rates = video_data.get("bit_rate")
122
- if not bit_rates:
123
- raise ParseError("抖音解析失败: 未获取到视频下载地址")
120
+ def _p_play_addr(video_data: dict) -> tuple[str | None, int, int]:
121
+ play_addr = video_data["play_addr"]
122
+ url_list = play_addr["url_list"]
123
+ if not url_list:
124
+ return None, 0, 0
125
+ return url_list[0], play_addr.get("width", 0), play_addr.get("height", 0)
126
+
124
127
 
125
- # 按分辨率、文件大小、码率降序排列,选择最高质量。
126
- # Story/日常的移动端 default 播放地址通常没有 br 参数,但 data_size 最大。
128
+ def _p_bit_rates(video_data: dict) -> tuple[str, int, int]:
129
+ bit_rates: list = video_data.get("bit_rate", [])
130
+ bit_rates = list(filter(lambda i: i.get("is_bytevc1", 0) == 0, bit_rates))
127
131
  bit_rates.sort(
128
132
  key=lambda x: (
129
133
  x.get("play_addr", {}).get("width", 0) * x.get("play_addr", {}).get("height", 0),
@@ -135,11 +139,20 @@ def parse_video_info(video_data: dict) -> dict:
135
139
  best_quality = bit_rates[0]
136
140
 
137
141
  play_addr = best_quality.get("play_addr", {})
138
- video_url_list = play_addr.get("url_list", [])
139
- if not video_url_list:
140
- raise ParseError("抖音解析失败: 视频下载地址为空")
142
+ url_list = play_addr.get("url_list", [])
143
+ return url_list[0], play_addr.get("width", 0), play_addr.get("height", 0)
144
+
145
+
146
+ def parse_video_info(video_data: dict) -> dict:
147
+ u, w, h = None, 0, 0
148
+ is_bytevc1 = video_data.get("is_bytevc1")
149
+ # is_bytevc1 = 1 | 2 为字节自研视频编码 bytevc1 | bytevc2
150
+ if is_bytevc1 == 0:
151
+ u, w, h = _p_play_addr(video_data)
152
+ if not u:
153
+ u, w, h = _p_bit_rates(video_data)
141
154
 
142
- video_url = remove_video_watermark(video_url_list[0])
155
+ video_url = remove_video_watermark(u)
143
156
 
144
157
  cover = video_data.get("cover", {})
145
158
  thumb_url_list = cover.get("url_list", [])
@@ -148,9 +161,9 @@ def parse_video_info(video_data: dict) -> dict:
148
161
  return {
149
162
  "video_url": video_url,
150
163
  "thumb_url": thumb_url,
151
- "duration": best_quality.get("duration", 0),
152
- "width": play_addr.get("width", 0),
153
- "height": play_addr.get("height", 0),
164
+ "duration": video_data.get("duration", 0),
165
+ "width": w,
166
+ "height": h,
154
167
  }
155
168
 
156
169
 
@@ -245,8 +245,8 @@ class BiliImage:
245
245
 
246
246
  @dataclass(kw_only=True)
247
247
  class BiliDynamic:
248
- title: str | None = None
249
- content: str | None = None
248
+ title: str = ""
249
+ content: str = ""
250
250
  images: list[BiliImage] | None = None
251
251
 
252
252
  @classmethod
@@ -361,10 +361,10 @@ class BiliDynamic:
361
361
  return cls(title=title)
362
362
 
363
363
  @staticmethod
364
- def _get_desc_text(module_dynamic: dict) -> str | None:
364
+ def _get_desc_text(module_dynamic: dict) -> str:
365
365
  if desc := module_dynamic["desc"]:
366
366
  return str(desc["text"]).strip()
367
- return None
367
+ return ""
368
368
 
369
369
  @staticmethod
370
370
  def _get_major_cover(major_content: dict) -> list[BiliImage] | None:
@@ -10,9 +10,9 @@ from ..utils.helpers import UA
10
10
 
11
11
  @dataclass
12
12
  class Coolapk:
13
- title: str | None = None
14
- markdown_content: str | None = None
15
- text_content: str | None = None
13
+ title: str = ""
14
+ markdown_content: str = ""
15
+ text_content: str = ""
16
16
  imgs: list[str] | None = None
17
17
 
18
18
  @classmethod
@@ -25,6 +25,8 @@ class Coolapk:
25
25
  if title_element and (title := title_element.text.strip()):
26
26
  content = soup.find(class_="feed-article-message")
27
27
  if content is None:
28
+ if "s=" in url or "shareKey=" in url:
29
+ raise ValueError("获取内容失败")
28
30
  raise ValueError("获取内容失败, 分享时请保留 shareKey 或 s 参数")
29
31
  markdown_content = MarkdownConverter(heading_style="ATX").convert(str(content))
30
32
  text_content = "".join(BeautifulSoup(markdown(markdown_content), "lxml").find_all(string=True))
@@ -35,6 +37,6 @@ class Coolapk:
35
37
  if feed_element and (feed_content := feed_element.text.strip()):
36
38
  message_image_group = soup.find(class_="message-image-group")
37
39
  imgs = [f"https:{i['src']}" for i in message_image_group.find_all("img")] if message_image_group else []
38
- return cls(None, None, feed_content, imgs)
40
+ return cls("", "", feed_content, imgs)
39
41
 
40
42
  raise ValueError("获取内容失败, 分享时请保留 shareKey 或 s 参数")
@@ -1147,7 +1147,7 @@ class DouyinMobileCrawler:
1147
1147
  return cast(dict[str, Any], payload)
1148
1148
  last_error = f"{host} missing aweme_detail: {payload.get('status_msg') or payload}"
1149
1149
  await asyncio.sleep(0.15)
1150
- raise ParseError(f"获取抖音 Story/日常失败: {last_error}")
1150
+ raise ParseError(f"获取抖音作品失败: {last_error}")
1151
1151
 
1152
1152
  async def parse(self, raw_url: str) -> dict:
1153
1153
  aweme_id = await self.get_aweme_id(raw_url)
@@ -65,17 +65,17 @@ class InstagramPost:
65
65
  return bool(self._field("is_video"))
66
66
 
67
67
  @property
68
- def title(self) -> str | None:
69
- return self._node.get("title")
68
+ def title(self) -> str:
69
+ return str(self._node.get("title", ""))
70
70
 
71
71
  @property
72
- def caption(self) -> str | None:
72
+ def caption(self) -> str:
73
73
  caption_edges = self._node.get("edge_media_to_caption", {}).get("edges") or []
74
74
  if caption_edges:
75
75
  if text := caption_edges[0].get("node", {}).get("text"):
76
76
  return str(text)
77
- return None
78
- return self._node.get("caption")
77
+ return ""
78
+ return str(self._node.get("caption", ""))
79
79
 
80
80
  @property
81
81
  def url(self) -> str:
@@ -173,7 +173,7 @@ class TwitterTweet:
173
173
  def __init__(
174
174
  self,
175
175
  tweet_id: str,
176
- full_text: str | None = None,
176
+ full_text: str = "",
177
177
  media: list[TwitterVideo | TwitterPhoto | TwitterAni] | None = None,
178
178
  article: TwitterArticle | None = None,
179
179
  ):
@@ -122,7 +122,7 @@ class XHSAPI:
122
122
 
123
123
  @staticmethod
124
124
  def get_trace_id(img_url: str) -> str:
125
- match = re.search(r"/(spectrum|note_pre_post_uhdr|notes_pre_post|notes_uhdr)/([^/!]+)(?:!.*)?$", img_url)
125
+ match = re.search(r"/[a-f0-9]{32}/(.*)/([^/!]+)(?:!.*)?", img_url)
126
126
  if match:
127
127
  return f"{match.group(1)}/{match.group(2)}"
128
128
 
@@ -169,7 +169,7 @@ class XHSPost:
169
169
  if __name__ == "__main__":
170
170
 
171
171
  async def main() -> None:
172
- url = "https://www.xiaohongshu.com/explore/68fe2018000000000303a844?xsec_token=ABgAb-5vt3L-aTqeEjdxJP_ylf02hY5n2f-y75yMQWrUo=&xsec_source=pc_search&source=web_explore_feed"
172
+ url = ""
173
173
  xhs = XHSAPI()
174
174
  print(await xhs.extract(url))
175
175
 
@@ -91,8 +91,8 @@ class ZhihuQA:
91
91
  class ZhihuZhuanLan:
92
92
  title: str
93
93
  imgs: list[str]
94
- markdown_content: str | None = None
95
- plaintext_content: str | None = None
94
+ markdown_content: str = ""
95
+ plaintext_content: str = ""
96
96
 
97
97
  @classmethod
98
98
  def parse(cls, data: dict) -> Self:
@@ -122,8 +122,8 @@ class ZhihuPin:
122
122
  type: ZhihuPinType
123
123
  title: str
124
124
  media: list[ZhihuMedia]
125
- markdown_content: str | None = None
126
- plaintext_content: str | None = None
125
+ markdown_content: str = ""
126
+ plaintext_content: str = ""
127
127
 
128
128
  @classmethod
129
129
  def parse(cls, result: dict) -> "ZhihuPin":
@@ -30,8 +30,8 @@ class ParseResult(ABC): # noqa: B024
30
30
 
31
31
  def __init__(
32
32
  self,
33
- title: str | None = "",
34
- content: str | None = "",
33
+ title: str = "",
34
+ content: str = "",
35
35
  media: Sequence[AnyMediaRef] | AnyMediaRef | None = None,
36
36
  platform: Platform | None = None,
37
37
  ):
@@ -42,8 +42,8 @@ class ParseResult(ABC): # noqa: B024
42
42
  :param platform: 平台
43
43
  """
44
44
  self.raw_url: str = ""
45
- self.title = (title or "").strip()
46
- self.content = (content or "").strip()
45
+ self.title = title.strip()
46
+ self.content = content.strip()
47
47
  self.media = media
48
48
  self.platform = platform
49
49
  self.name = slugify(
@@ -294,9 +294,9 @@ class VideoParseResult(ParseResult):
294
294
 
295
295
  def __init__(
296
296
  self,
297
- title: str | None = "",
297
+ title: str = "",
298
298
  video: str | VideoRef | None = None,
299
- content: str | None = "",
299
+ content: str = "",
300
300
  ):
301
301
  video = VideoRef(url=video) if isinstance(video, str) else video
302
302
  super().__init__(
@@ -313,9 +313,9 @@ class ImageParseResult(ParseResult):
313
313
 
314
314
  def __init__(
315
315
  self,
316
- title: str | None = "",
316
+ title: str = "",
317
317
  photo: Sequence[str | ImageRef | AniRef | LivePhotoRef] | None = None,
318
- content: str | None = "",
318
+ content: str = "",
319
319
  ):
320
320
  media = [ImageRef(url=p) if isinstance(p, str) else p for p in photo] if photo else None
321
321
  super().__init__(title=title, media=media, content=content)
@@ -328,9 +328,9 @@ class MultimediaParseResult(ParseResult):
328
328
 
329
329
  def __init__(
330
330
  self,
331
- title: str | None = "",
331
+ title: str = "",
332
332
  media: Sequence[AnyMediaRef] | None = None,
333
- content: str | None = "",
333
+ content: str = "",
334
334
  ):
335
335
  super().__init__(title=title, media=media, content=content)
336
336
 
@@ -342,16 +342,16 @@ class RichTextParseResult(ParseResult):
342
342
 
343
343
  def __init__(
344
344
  self,
345
- title: str | None = "",
345
+ title: str = "",
346
346
  media: Sequence[AnyMediaRef] | None = None,
347
- markdown_content: str | None = "",
347
+ markdown_content: str = "",
348
348
  ):
349
349
  """
350
350
  :param title: 标题
351
351
  :param media: 文章中的媒体
352
352
  :param markdown_content: markdown 格式正文
353
353
  """
354
- self.markdown_content = markdown_content or ""
354
+ self.markdown_content = markdown_content
355
355
  super().__init__(title=title, media=media, content=self.plaintext_content)
356
356
 
357
357
  def __repr__(self) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
@@ -34,7 +34,7 @@ Requires-Dist: python-slugify[unidecode]>=8.0.4
34
34
  Requires-Dist: opencv-python-headless>=4.13.0.92
35
35
  Requires-Dist: cryptography>=49.0.0
36
36
  Requires-Dist: gmssl>=3.2.2
37
- Requires-Dist: SignerPy>=0.12.0
37
+ Requires-Dist: signerpy>=0.12.0
38
38
  Provides-Extra: cli
39
39
  Requires-Dist: argcomplete>=3.6.3; extra == "cli"
40
40
  Requires-Dist: platformdirs>=4.5.1; extra == "cli"
@@ -310,6 +310,21 @@ except ParseError as exc:
310
310
 
311
311
  ---
312
312
 
313
+ ## 🤝 参与贡献
314
+
315
+ 欢迎提交 Pull Request 或 Issue!
316
+
317
+ - Bug 反馈请附上相关 URL 和日志信息
318
+
319
+ ### 开发规范
320
+
321
+ 提交代码前请至少执行:
322
+
323
+ ```bash
324
+ ruff format && ruff check --fix && uv run mypy
325
+ uv run pytest
326
+ ```
327
+
313
328
  ## 🤝 参考项目
314
329
 
315
330
  - [Evil0ctal/Douyin_TikTok_Download_API](https://github.com/Evil0ctal/Douyin_TikTok_Download_API)
@@ -321,7 +336,7 @@ except ParseError as exc:
321
336
 
322
337
  ## 📜 开源协议
323
338
 
324
- 本项目基于 [MIT License](LICENSE) 开源。
339
+ 本项目基于 [MIT License](LICENSE) 开源
325
340
 
326
341
  ---
327
342
 
@@ -17,7 +17,7 @@ python-slugify[unidecode]>=8.0.4
17
17
  opencv-python-headless>=4.13.0.92
18
18
  cryptography>=49.0.0
19
19
  gmssl>=3.2.2
20
- SignerPy>=0.12.0
20
+ signerpy>=0.12.0
21
21
 
22
22
  [cli]
23
23
  argcomplete>=3.6.3
File without changes
File without changes
File without changes
File without changes
File without changes