parsehub 2.3.17__tar.gz → 2.4.1__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 (88) hide show
  1. {parsehub-2.3.17/src/parsehub.egg-info → parsehub-2.4.1}/PKG-INFO +23 -23
  2. {parsehub-2.3.17 → parsehub-2.4.1}/README.md +1 -1
  3. {parsehub-2.3.17 → parsehub-2.4.1}/pyproject.toml +25 -25
  4. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/bilibili.py +66 -3
  5. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/instagram.py +9 -7
  6. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/kuaishou.py +1 -0
  7. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/bilibili.py +26 -0
  8. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/instagram.py +1 -2
  9. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/wxchannels.py +1 -3
  10. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/__init__.py +6 -2
  11. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/media_file.py +16 -2
  12. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/media_ref.py +18 -2
  13. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/post.py +1 -0
  14. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/result.py +20 -3
  15. {parsehub-2.3.17 → parsehub-2.4.1/src/parsehub.egg-info}/PKG-INFO +23 -23
  16. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub.egg-info/SOURCES.txt +1 -4
  17. parsehub-2.4.1/src/parsehub.egg-info/requires.txt +27 -0
  18. {parsehub-2.3.17 → parsehub-2.4.1}/test/test_core_offline.py +6 -335
  19. parsehub-2.3.17/src/parsehub.egg-info/requires.txt +0 -27
  20. parsehub-2.3.17/test/test_bilibili_offline.py +0 -63
  21. parsehub-2.3.17/test/test_instagram_duration_offline.py +0 -134
  22. parsehub-2.3.17/test/test_zuiyou_duration_offline.py +0 -69
  23. {parsehub-2.3.17 → parsehub-2.4.1}/LICENSE +0 -0
  24. {parsehub-2.3.17 → parsehub-2.4.1}/setup.cfg +0 -0
  25. {parsehub-2.3.17 → parsehub-2.4.1}/src/__init__.py +0 -0
  26. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/__init__.py +0 -0
  27. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/cli.py +0 -0
  28. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/cli_config.py +0 -0
  29. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/config/__init__.py +0 -0
  30. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/config/config.py +0 -0
  31. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/errors.py +0 -0
  32. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/__init__.py +0 -0
  33. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/base/__init__.py +0 -0
  34. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/base/base.py +0 -0
  35. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/base/ytdlp.py +0 -0
  36. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/__init__.py +0 -0
  37. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/bangumi.py +0 -0
  38. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/coolapk.py +0 -0
  39. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/douban.py +0 -0
  40. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/douyin.py +0 -0
  41. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/facebook.py +0 -0
  42. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/medium.py +0 -0
  43. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/pipix.py +0 -0
  44. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/snapchat.py +0 -0
  45. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/threads.py +0 -0
  46. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/tieba.py +0 -0
  47. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/tiktok.py +0 -0
  48. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/toutiao.py +0 -0
  49. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/twitter.py +0 -0
  50. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/weibo.py +0 -0
  51. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/weixin.py +0 -0
  52. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/wxchannels.py +0 -0
  53. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/xhs.py +0 -0
  54. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
  55. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/youtube.py +0 -0
  56. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/zhihu.py +0 -0
  57. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/zuiyou.py +0 -0
  58. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/__init__.py +0 -0
  59. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/bangumi.py +0 -0
  60. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/coolapk.py +0 -0
  61. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/douban.py +0 -0
  62. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/douyin.py +0 -0
  63. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/kuaishou.py +0 -0
  64. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/medium.py +0 -0
  65. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/pipix.py +0 -0
  66. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/threads.py +0 -0
  67. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/tieba.py +0 -0
  68. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/tiktok.py +0 -0
  69. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/toutiao.py +0 -0
  70. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/twitter.py +0 -0
  71. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/weibo.py +0 -0
  72. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/weixin.py +0 -0
  73. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/xhs.py +0 -0
  74. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/xiaoheihe.py +0 -0
  75. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/zhihu.py +0 -0
  76. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/zuiyou.py +0 -0
  77. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/callback.py +0 -0
  78. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/platform.py +0 -0
  79. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/utils/downloader.py +0 -0
  80. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/utils/fallback.py +0 -0
  81. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/utils/helpers.py +0 -0
  82. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/utils/media_info.py +0 -0
  83. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub.egg-info/dependency_links.txt +0 -0
  84. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub.egg-info/entry_points.txt +0 -0
  85. {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub.egg-info/top_level.txt +0 -0
  86. {parsehub-2.3.17 → parsehub-2.4.1}/test/test_cli.py +0 -0
  87. {parsehub-2.3.17 → parsehub-2.4.1}/test/test_cli_config.py +0 -0
  88. {parsehub-2.3.17 → parsehub-2.4.1}/test/test_downloader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.3.17
3
+ Version: 2.4.1
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
@@ -15,32 +15,32 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
15
  Requires-Python: >=3.12.0
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: aiofiles>=23.2
19
- Requires-Dist: beautifulsoup4>=4.12.3
20
- Requires-Dist: loguru>=0.6.0
18
+ Requires-Dist: aiofiles>=25.1.0
19
+ Requires-Dist: beautifulsoup4>=4.15.0
20
+ Requires-Dist: loguru>=0.7.3
21
21
  Requires-Dist: pydub>=0.25.1
22
- Requires-Dist: python-dotenv>=1.0.1
23
- Requires-Dist: tenacity>=8.5.0
24
- Requires-Dist: urlextract>=1.9.0
25
- Requires-Dist: yt-dlp[default]>=2026.10.26.220202.dev0
26
- Requires-Dist: lxml>=5.3.0
27
- Requires-Dist: pydantic>=1.10.19
28
- Requires-Dist: markdownify>=1.1.0
29
- Requires-Dist: markdown>=3.7
30
- Requires-Dist: json5>=0.12
31
- Requires-Dist: requests
32
- Requires-Dist: httpx[socks]>=0.24.1
33
- Requires-Dist: pillow>=12.1.0
34
- Requires-Dist: python-slugify[unidecode]>=8.0.4
35
- Requires-Dist: opencv-python-headless>=4.13.0.92
36
- Requires-Dist: cryptography>=49.0.0
22
+ Requires-Dist: python-dotenv>=1.2.4
23
+ Requires-Dist: tenacity>=9.2.1
24
+ Requires-Dist: urlextract>=1.10.0
25
+ Requires-Dist: yt-dlp[default]>=2026.11.4.232659.dev0
26
+ Requires-Dist: lxml>=6.1.3
27
+ Requires-Dist: pydantic>=2.14.3
28
+ Requires-Dist: markdownify>=1.2.3
29
+ Requires-Dist: markdown>=3.11.3
30
+ Requires-Dist: json5>=0.17.3
31
+ Requires-Dist: requests>=2.34.2
32
+ Requires-Dist: httpx[socks]>=0.28.1
33
+ Requires-Dist: pillow>=13.0.0
34
+ Requires-Dist: python-slugify[unidecode]>=9.1.3
35
+ Requires-Dist: opencv-python-headless>=5.0.0.93
36
+ Requires-Dist: cryptography>=50.0.2
37
37
  Requires-Dist: gmssl>=3.2.2
38
38
  Requires-Dist: signerpy>=0.12.0
39
- Requires-Dist: curl-cffi>=0.13
39
+ Requires-Dist: curl-cffi>=0.17.0
40
40
  Requires-Dist: xhshow>=0.2.0
41
41
  Provides-Extra: cli
42
- Requires-Dist: argcomplete>=3.6.3; extra == "cli"
43
- Requires-Dist: platformdirs>=4.5.1; extra == "cli"
42
+ Requires-Dist: argcomplete>=3.7.2; extra == "cli"
43
+ Requires-Dist: platformdirs>=4.14.5; extra == "cli"
44
44
  Dynamic: license-file
45
45
 
46
46
  <div align="center">
@@ -82,7 +82,7 @@ Dynamic: license-file
82
82
  | **YouTube** | ✅ | | 🎵 音乐 |
83
83
  | **Facebook** | ✅ | | |
84
84
  | **Threads** | ✅ | ✅ | |
85
- | **Bilibili** | ✅ | | 📝 动态 |
85
+ | **Bilibili** | ✅ | | 📝 动态, 🎵 音乐 |
86
86
  | **抖音** | ✅ | ✅ | ☀️日常 |
87
87
  | **TikTok** | ✅ | ✅ | |
88
88
  | **微博** | ✅ | ✅ | |
@@ -37,7 +37,7 @@
37
37
  | **YouTube** | ✅ | | 🎵 音乐 |
38
38
  | **Facebook** | ✅ | | |
39
39
  | **Threads** | ✅ | ✅ | |
40
- | **Bilibili** | ✅ | | 📝 动态 |
40
+ | **Bilibili** | ✅ | | 📝 动态, 🎵 音乐 |
41
41
  | **抖音** | ✅ | ✅ | ☀️日常 |
42
42
  | **TikTok** | ✅ | ✅ | |
43
43
  | **微博** | ✅ | ✅ | |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "parsehub"
3
- version = "2.3.17"
3
+ version = "2.4.1"
4
4
  description = "轻量、异步、开箱即用的社交媒体聚合解析库"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12.0"
@@ -18,28 +18,28 @@ classifiers = [
18
18
  ]
19
19
 
20
20
  dependencies = [
21
- "aiofiles>=23.2",
22
- "beautifulsoup4>=4.12.3",
23
- "loguru>=0.6.0",
21
+ "aiofiles>=25.1.0",
22
+ "beautifulsoup4>=4.15.0",
23
+ "loguru>=0.7.3",
24
24
  "pydub>=0.25.1",
25
- "python-dotenv>=1.0.1",
26
- "tenacity>=8.5.0",
27
- "urlextract>=1.9.0",
28
- "yt-dlp[default]>=2026.10.26.220202.dev0",
29
- "lxml>=5.3.0",
30
- "pydantic>=1.10.19",
31
- "markdownify>=1.1.0",
32
- "markdown>=3.7",
33
- "json5>=0.12",
34
- "requests",
35
- "httpx[socks]>=0.24.1",
36
- "pillow>=12.1.0",
37
- "python-slugify[unidecode]>=8.0.4",
38
- "opencv-python-headless>=4.13.0.92",
39
- "cryptography>=49.0.0",
25
+ "python-dotenv>=1.2.4",
26
+ "tenacity>=9.2.1",
27
+ "urlextract>=1.10.0",
28
+ "yt-dlp[default]>=2026.11.4.232659.dev0",
29
+ "lxml>=6.1.3",
30
+ "pydantic>=2.14.3",
31
+ "markdownify>=1.2.3",
32
+ "markdown>=3.11.3",
33
+ "json5>=0.17.3",
34
+ "requests>=2.34.2",
35
+ "httpx[socks]>=0.28.1",
36
+ "pillow>=13.0.0",
37
+ "python-slugify[unidecode]>=9.1.3",
38
+ "opencv-python-headless>=5.0.0.93",
39
+ "cryptography>=50.0.2",
40
40
  "gmssl>=3.2.2",
41
41
  "signerpy>=0.12.0",
42
- "curl-cffi>=0.13",
42
+ "curl-cffi>=0.17.0",
43
43
  "xhshow>=0.2.0",
44
44
  ]
45
45
 
@@ -49,15 +49,15 @@ ph = "parsehub.cli:main"
49
49
 
50
50
  [project.optional-dependencies]
51
51
  cli = [
52
- "argcomplete>=3.6.3",
53
- "platformdirs>=4.5.1",
52
+ "argcomplete>=3.7.2",
53
+ "platformdirs>=4.14.5",
54
54
  ]
55
55
 
56
56
  [dependency-groups]
57
57
  dev = [
58
- "mypy>=2.1.0",
59
- "pytest>=9.0.3",
60
- "ruff>=0.14.14",
58
+ "mypy>=2.4.1",
59
+ "pytest>=9.2.0",
60
+ "ruff>=0.17.4",
61
61
  ]
62
62
 
63
63
  [tool.hatch.build.targets.wheel]
@@ -12,6 +12,8 @@ from ...types import (
12
12
  ImageParseResult,
13
13
  ImageRef,
14
14
  LivePhotoRef,
15
+ MusicParseResult,
16
+ MusicRef,
15
17
  ParseError,
16
18
  Platform,
17
19
  ProgressCallback,
@@ -25,17 +27,21 @@ from ..base.ytdlp import YtParser, YtVideoParseResult
25
27
 
26
28
  class BiliParse(BaseParser):
27
29
  __platform__ = Platform.BILIBILI
28
- __supported_type__ = ["视频", "动态"]
29
- __match__ = r"^(http(s)?://)?((((w){3}.|(m).|(t).)?bilibili\.com)/(video|opus|\b\d{18,19}\b)|b23.tv|bili2233.cn).*"
30
+ __supported_type__ = ["视频", "动态", "音乐"]
31
+ __match__ = r"^(http(s)?://)?((((w){3}.|(m).|(t).)?bilibili\.com)/(video|opus|audio/au\d+|\b\d{18,19}\b)|b23.tv|bili2233.cn).*"
30
32
  __reserved_parameters__ = ["p"]
31
33
  __redirect_keywords__ = ["b23.tv", "bili2233.cn"]
32
34
 
33
- async def _do_parse(self, raw_url: str) -> YtVideoParseResult | BiliVideoParseResult | ImageParseResult:
35
+ async def _do_parse(
36
+ self, raw_url: str
37
+ ) -> YtVideoParseResult | BiliVideoParseResult | BiliMusicParseResult | ImageParseResult:
34
38
  parsed = urlparse(raw_url)
35
39
  if parsed.hostname == "mall.bilibili.com":
36
40
  raise ParseError("不支持解析商品")
37
41
  if parsed.path.startswith("/bangumi/"):
38
42
  raise ParseError("不支持解析番剧和影视")
43
+ if BiliAPI.get_music_id(raw_url):
44
+ return await self.bili_music_parse(raw_url)
39
45
  if await self.is_dynamic(raw_url):
40
46
  dynamic = await self.get_dynamic_info(raw_url)
41
47
  content = self.hashtag_handler(dynamic.content or "")
@@ -142,6 +148,36 @@ class BiliParse(BaseParser):
142
148
  ),
143
149
  )
144
150
 
151
+ async def bili_music_parse(self, url: str) -> BiliMusicParseResult:
152
+ async with BiliAPI(proxy=self.proxy) as bili:
153
+ info_response = await bili.get_music_info(url)
154
+ if not (info := info_response.get("data")):
155
+ if info_response.get("code") == 4511001:
156
+ raise ContentUnavailableError("音乐未找到或已下架")
157
+ raise ParseError(f"获取音乐信息失败: {info_response.get('message') or '未知错误'}")
158
+
159
+ play_response = await bili.get_music_url(str(info["id"]))
160
+ cdns: list[str] = (play_response.get("data") or {}).get("cdns") or []
161
+ if not cdns:
162
+ raise ParseError("获取音乐链接失败")
163
+
164
+ return BiliMusicParseResult(
165
+ title=info.get("title") or "",
166
+ music=MusicRef(
167
+ url=cdns[0],
168
+ ext=self._music_ext(cdns[0]),
169
+ thumb_url=info.get("cover") or None,
170
+ backup_urls=cdns[1:],
171
+ duration=int(info.get("duration") or 0),
172
+ ),
173
+ content=info.get("intro") or "",
174
+ )
175
+
176
+ @staticmethod
177
+ def _music_ext(url: str) -> str:
178
+ suffix = Path(urlparse(url).path).suffix
179
+ return suffix.removeprefix(".") or "m4a"
180
+
145
181
  async def ytp_parse(self, url: str) -> YtVideoParseResult:
146
182
  return await BiliYtParse(proxy=self.proxy, cookie=self.cookie)._do_parse(url)
147
183
 
@@ -205,7 +241,34 @@ class BiliVideoParseResult(VideoParseResult):
205
241
  )
206
242
 
207
243
 
244
+ class BiliMusicParseResult(MusicParseResult):
245
+ async def _do_download(
246
+ self,
247
+ *,
248
+ output_dir: Path,
249
+ callback: ProgressCallback | None = None,
250
+ callback_args: tuple = (),
251
+ callback_kwargs: dict | None = None,
252
+ proxy: str | None = None,
253
+ headers: dict | None = None,
254
+ connections: int = 4,
255
+ concurrency: int = 4,
256
+ ) -> DownloadResult:
257
+ headers = {"referer": "https://www.bilibili.com", "User-Agent": UA}
258
+ return await super()._do_download(
259
+ output_dir=output_dir,
260
+ callback=callback,
261
+ callback_args=callback_args,
262
+ callback_kwargs=callback_kwargs,
263
+ proxy=proxy,
264
+ headers=headers,
265
+ connections=connections,
266
+ concurrency=concurrency,
267
+ )
268
+
269
+
208
270
  __all__ = [
209
271
  "BiliParse",
210
272
  "BiliVideoParseResult",
273
+ "BiliMusicParseResult",
211
274
  ]
@@ -66,7 +66,7 @@ class InstagramParser(BaseParser):
66
66
  except ParseError as e:
67
67
  logger.warning(f"Instagram yt-dlp 解析失败, 使用直链视频: {e}")
68
68
  else:
69
- return YtVideoParseResult(
69
+ return InstagramYtVideoParseResult(
70
70
  dl=info,
71
71
  title=post.title,
72
72
  content=post.caption,
@@ -98,12 +98,8 @@ class InstagramParser(BaseParser):
98
98
  async def _parse(self, shortcode: str) -> InstagramPost:
99
99
  stages: list[FallbackStage[InstagramPost]] = [FallbackStage("匿名", lambda: self._fetch(shortcode))]
100
100
  if self.cookie.get_value():
101
- stages.append(
102
- FallbackStage("cookie", lambda: self._fetch(shortcode, self.cookie), cookie=True)
103
- )
104
- return await self._fallback(
105
- *stages, stop=lambda e: not isinstance(e.__cause__, InstagramRestrictedError)
106
- )
101
+ stages.append(FallbackStage("cookie", lambda: self._fetch(shortcode, self.cookie), cookie=True))
102
+ return await self._fallback(*stages, stop=lambda e: not isinstance(e.__cause__, InstagramRestrictedError))
107
103
 
108
104
  async def _fetch(self, shortcode: str, cookie: SecretCookie | None = None) -> InstagramPost:
109
105
  api = InstagramAPI(proxy=self.proxy, cookie=cookie.get_value() if cookie else None, timeout=30)
@@ -127,4 +123,10 @@ class InstagramYtParse(YtParser, register=False):
127
123
  pass
128
124
 
129
125
 
126
+ class InstagramYtVideoParseResult(YtVideoParseResult):
127
+ @property
128
+ def cli_args(self) -> list[str]:
129
+ return [*super().cli_args, "-S", "vcodec:h264"]
130
+
131
+
130
132
  __all__ = ["InstagramParser"]
@@ -46,6 +46,7 @@ class KuaiShouParser(BaseParser):
46
46
  if (
47
47
  ksp.page_type == "VIDEO" or "/short-video/" in raw_url
48
48
  ): # 地区风控导致获取到的 page_type 可能为 ATLAS 但实际为视频
49
+
49
50
  async def parse_html() -> VideoParseResult:
50
51
  content = ksp.get_title_content()
51
52
  video = ksp.get_real_video_url()
@@ -125,6 +125,27 @@ class BiliAPI:
125
125
  )
126
126
  return cast(dict[str, Any], response.json())
127
127
 
128
+ async def get_music_info(self, url: str) -> dict:
129
+ """获取音乐详细信息"""
130
+ sid = self.get_music_id(url)
131
+ if not sid:
132
+ raise ValueError(f"Invalid url: {url}")
133
+ response = await self._get_client().get(
134
+ "https://www.bilibili.com/audio/music-service-c/web/song/info",
135
+ params={"sid": sid},
136
+ )
137
+ response.raise_for_status()
138
+ return cast(dict[str, Any], response.json())
139
+
140
+ async def get_music_url(self, sid: str) -> dict:
141
+ """获取音乐播放链接"""
142
+ response = await self._get_client().get(
143
+ "https://www.bilibili.com/audio/music-service-c/web/url",
144
+ params={"sid": sid},
145
+ )
146
+ response.raise_for_status()
147
+ return cast(dict[str, Any], response.json())
148
+
128
149
  async def get_buvid(self):
129
150
  """获取 buvid"""
130
151
  response = await self._get_client().get(
@@ -188,6 +209,11 @@ class BiliAPI:
188
209
  tmp = tmp * BASE + idx
189
210
  return f"av{(tmp & MASK_CODE) ^ XOR_CODE}"
190
211
 
212
+ @staticmethod
213
+ def get_music_id(url: str) -> str | None:
214
+ m = re.search(r"/audio/au(\d+)", url)
215
+ return m.group(1) if m else None
216
+
191
217
  def get_bvid(self, url: str):
192
218
  m_bv = re.search(r"BV[0-9A-Za-z]{10,}", url)
193
219
  if m_bv:
@@ -131,8 +131,7 @@ class InstagramAPI:
131
131
  ) -> InstagramPost:
132
132
  """用 embed 补齐 v1 / GraphQL 缺失的视频直链和时长。"""
133
133
  if not any(
134
- (post.type is InstagramMediaType.VIDEO or item.is_video)
135
- and (not item.video_url or item.duration <= 0)
134
+ (post.type is InstagramMediaType.VIDEO or item.is_video) and (not item.video_url or item.duration <= 0)
136
135
  for item in post.media
137
136
  ):
138
137
  return post
@@ -95,9 +95,7 @@ class WXChannels:
95
95
 
96
96
  feed_data = feed_result.get("data") or {}
97
97
  feed_error = feed_data.get("errMsg") or {}
98
- error_text = " ".join(
99
- _strip_html(str(feed_error.get(key) or "")) for key in ("title", "content")
100
- ).strip()
98
+ error_text = " ".join(_strip_html(str(feed_error.get(key) or "")) for key in ("title", "content")).strip()
101
99
  if feed_result.get("errCode") != 0:
102
100
  message = feed_result.get("errMsg") or "微信视频号解析失败"
103
101
  raise WXChannelsError(str(message))
@@ -8,8 +8,8 @@ from ..errors import (
8
8
  ParseError,
9
9
  )
10
10
  from .callback import ProgressCallback, ProgressUnit
11
- from .media_file import AniFile, AnyMediaFile, ImageFile, LivePhotoFile, MediaFile, VideoFile
12
- from .media_ref import AniRef, AnyMediaRef, ImageRef, LivePhotoRef, MediaRef, VideoRef
11
+ from .media_file import AniFile, AnyMediaFile, ImageFile, LivePhotoFile, MediaFile, MusicFile, VideoFile
12
+ from .media_ref import AniRef, AnyMediaRef, ImageRef, LivePhotoRef, MediaRef, MusicRef, VideoRef
13
13
  from .platform import Platform
14
14
  from .post import PostType
15
15
  from .result import (
@@ -17,6 +17,7 @@ from .result import (
17
17
  DownloadResult,
18
18
  ImageParseResult,
19
19
  MultimediaParseResult,
20
+ MusicParseResult,
20
21
  ParseResult,
21
22
  RichTextParseResult,
22
23
  VideoParseResult,
@@ -33,12 +34,14 @@ __all__ = [
33
34
  "MediaRef",
34
35
  "AniRef",
35
36
  "VideoRef",
37
+ "MusicRef",
36
38
  "ImageRef",
37
39
  "LivePhotoRef",
38
40
  "DownloadResult",
39
41
  "ParseResult",
40
42
  "ImageParseResult",
41
43
  "VideoParseResult",
44
+ "MusicParseResult",
42
45
  "MultimediaParseResult",
43
46
  "RichTextParseResult",
44
47
  "AnyMediaRef",
@@ -49,6 +52,7 @@ __all__ = [
49
52
  "ImageFile",
50
53
  "AniFile",
51
54
  "LivePhotoFile",
55
+ "MusicFile",
52
56
  "AnyMediaFile",
53
57
  "ProgressCallback",
54
58
  "ProgressUnit",
@@ -78,6 +78,20 @@ class AniFile(MediaFile):
78
78
  self.duration = info.duration
79
79
 
80
80
 
81
+ @dataclass(kw_only=True)
82
+ class MusicFile(MediaFile):
83
+ """本地音乐
84
+ Attributes:
85
+ path: 路径
86
+ duration: 音乐时长,单位: 秒
87
+ """
88
+
89
+ duration: int = 0
90
+
91
+ def __post_init__(self) -> None:
92
+ """音乐不做宽高探测;时长由解析结果传入。"""
93
+
94
+
81
95
  @dataclass(kw_only=True)
82
96
  class LivePhotoFile(MediaFile):
83
97
  """本地 Live Photo
@@ -100,6 +114,6 @@ class LivePhotoFile(MediaFile):
100
114
  self.duration = info.duration
101
115
 
102
116
 
103
- AnyMediaFile = MediaFile | VideoFile | ImageFile | AniFile | LivePhotoFile
117
+ AnyMediaFile = MediaFile | VideoFile | ImageFile | AniFile | LivePhotoFile | MusicFile
104
118
 
105
- __all__ = ["MediaFile", "VideoFile", "ImageFile", "AniFile", "LivePhotoFile", "AnyMediaFile"]
119
+ __all__ = ["MediaFile", "VideoFile", "ImageFile", "AniFile", "LivePhotoFile", "MusicFile", "AnyMediaFile"]
@@ -37,6 +37,22 @@ class VideoRef(MediaRef):
37
37
  duration: int = 0
38
38
 
39
39
 
40
+ @dataclass(kw_only=True)
41
+ class MusicRef(MediaRef):
42
+ """音乐
43
+ Attributes:
44
+ url: URL
45
+ ext: 默认扩展名
46
+ thumb_url: 缩略图 URL
47
+ width: 宽度
48
+ height: 高度
49
+ duration: 音乐时长,单位: 秒
50
+ """
51
+
52
+ ext: str = "m4a"
53
+ duration: int = 0
54
+
55
+
40
56
  @dataclass(kw_only=True)
41
57
  class ImageRef(MediaRef):
42
58
  """图片
@@ -87,6 +103,6 @@ class LivePhotoRef(MediaRef):
87
103
  duration: int = 3
88
104
 
89
105
 
90
- AnyMediaRef = ImageRef | VideoRef | AniRef | LivePhotoRef
106
+ AnyMediaRef = ImageRef | VideoRef | AniRef | LivePhotoRef | MusicRef
91
107
 
92
- __all__ = ["MediaRef", "VideoRef", "ImageRef", "AniRef", "LivePhotoRef", "AnyMediaRef"]
108
+ __all__ = ["MediaRef", "VideoRef", "MusicRef", "ImageRef", "AniRef", "LivePhotoRef", "AnyMediaRef"]
@@ -9,3 +9,4 @@ class PostType(StrEnum):
9
9
  VIDEO = "video"
10
10
  MULTIMEDIA = "multimedia"
11
11
  RICHTEXT = "richtext"
12
+ MUSIC = "music"
@@ -20,8 +20,8 @@ from ..utils.downloader import download
20
20
  from ..utils.fallback import FallbackStage, run_fallback
21
21
  from ..utils.helpers import run_sync
22
22
  from .callback import ProgressCallback
23
- from .media_file import AniFile, AnyMediaFile, ImageFile, LivePhotoFile, VideoFile
24
- from .media_ref import AniRef, AnyMediaRef, ImageRef, LivePhotoRef, VideoRef
23
+ from .media_file import AniFile, AnyMediaFile, ImageFile, LivePhotoFile, MusicFile, VideoFile
24
+ from .media_ref import AniRef, AnyMediaRef, ImageRef, LivePhotoRef, MusicRef, VideoRef
25
25
  from .platform import Platform
26
26
  from .post import PostType
27
27
 
@@ -155,6 +155,8 @@ class ParseResult(ABC): # noqa: B024
155
155
  mf = ImageFile(path=f, width=media.width, height=media.height)
156
156
  case VideoRef():
157
157
  mf = VideoFile(path=f, width=media.width, height=media.height, duration=media.duration)
158
+ case MusicRef():
159
+ mf = MusicFile(path=f, duration=media.duration)
158
160
  case AniRef():
159
161
  mf = AniFile(path=f, width=media.width, height=media.height, duration=media.duration)
160
162
  case LivePhotoRef():
@@ -361,6 +363,21 @@ class VideoParseResult(ParseResult):
361
363
  )
362
364
 
363
365
 
366
+ class MusicParseResult(ParseResult):
367
+ """单个音乐"""
368
+
369
+ type = PostType.MUSIC
370
+
371
+ def __init__(
372
+ self,
373
+ title: str = "",
374
+ music: str | MusicRef | None = None,
375
+ content: str = "",
376
+ ):
377
+ music = MusicRef(url=music) if isinstance(music, str) else music
378
+ super().__init__(title=title, media=music, content=content)
379
+
380
+
364
381
  class ImageParseResult(ParseResult):
365
382
  """单图 / 多图 / 图集 / 实况照片"""
366
383
 
@@ -464,4 +481,4 @@ class DownloadResult:
464
481
  return f"{self.__class__.__name__}(media={media_count}, output_dir={self.output_dir})"
465
482
 
466
483
 
467
- AnyParseResult = VideoParseResult | ImageParseResult | MultimediaParseResult | RichTextParseResult
484
+ AnyParseResult = VideoParseResult | ImageParseResult | MultimediaParseResult | RichTextParseResult | MusicParseResult
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.3.17
3
+ Version: 2.4.1
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
@@ -15,32 +15,32 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
15
  Requires-Python: >=3.12.0
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: aiofiles>=23.2
19
- Requires-Dist: beautifulsoup4>=4.12.3
20
- Requires-Dist: loguru>=0.6.0
18
+ Requires-Dist: aiofiles>=25.1.0
19
+ Requires-Dist: beautifulsoup4>=4.15.0
20
+ Requires-Dist: loguru>=0.7.3
21
21
  Requires-Dist: pydub>=0.25.1
22
- Requires-Dist: python-dotenv>=1.0.1
23
- Requires-Dist: tenacity>=8.5.0
24
- Requires-Dist: urlextract>=1.9.0
25
- Requires-Dist: yt-dlp[default]>=2026.10.26.220202.dev0
26
- Requires-Dist: lxml>=5.3.0
27
- Requires-Dist: pydantic>=1.10.19
28
- Requires-Dist: markdownify>=1.1.0
29
- Requires-Dist: markdown>=3.7
30
- Requires-Dist: json5>=0.12
31
- Requires-Dist: requests
32
- Requires-Dist: httpx[socks]>=0.24.1
33
- Requires-Dist: pillow>=12.1.0
34
- Requires-Dist: python-slugify[unidecode]>=8.0.4
35
- Requires-Dist: opencv-python-headless>=4.13.0.92
36
- Requires-Dist: cryptography>=49.0.0
22
+ Requires-Dist: python-dotenv>=1.2.4
23
+ Requires-Dist: tenacity>=9.2.1
24
+ Requires-Dist: urlextract>=1.10.0
25
+ Requires-Dist: yt-dlp[default]>=2026.11.4.232659.dev0
26
+ Requires-Dist: lxml>=6.1.3
27
+ Requires-Dist: pydantic>=2.14.3
28
+ Requires-Dist: markdownify>=1.2.3
29
+ Requires-Dist: markdown>=3.11.3
30
+ Requires-Dist: json5>=0.17.3
31
+ Requires-Dist: requests>=2.34.2
32
+ Requires-Dist: httpx[socks]>=0.28.1
33
+ Requires-Dist: pillow>=13.0.0
34
+ Requires-Dist: python-slugify[unidecode]>=9.1.3
35
+ Requires-Dist: opencv-python-headless>=5.0.0.93
36
+ Requires-Dist: cryptography>=50.0.2
37
37
  Requires-Dist: gmssl>=3.2.2
38
38
  Requires-Dist: signerpy>=0.12.0
39
- Requires-Dist: curl-cffi>=0.13
39
+ Requires-Dist: curl-cffi>=0.17.0
40
40
  Requires-Dist: xhshow>=0.2.0
41
41
  Provides-Extra: cli
42
- Requires-Dist: argcomplete>=3.6.3; extra == "cli"
43
- Requires-Dist: platformdirs>=4.5.1; extra == "cli"
42
+ Requires-Dist: argcomplete>=3.7.2; extra == "cli"
43
+ Requires-Dist: platformdirs>=4.14.5; extra == "cli"
44
44
  Dynamic: license-file
45
45
 
46
46
  <div align="center">
@@ -82,7 +82,7 @@ Dynamic: license-file
82
82
  | **YouTube** | ✅ | | 🎵 音乐 |
83
83
  | **Facebook** | ✅ | | |
84
84
  | **Threads** | ✅ | ✅ | |
85
- | **Bilibili** | ✅ | | 📝 动态 |
85
+ | **Bilibili** | ✅ | | 📝 动态, 🎵 音乐 |
86
86
  | **抖音** | ✅ | ✅ | ☀️日常 |
87
87
  | **TikTok** | ✅ | ✅ | |
88
88
  | **微博** | ✅ | ✅ | |
@@ -76,10 +76,7 @@ src/parsehub/utils/downloader.py
76
76
  src/parsehub/utils/fallback.py
77
77
  src/parsehub/utils/helpers.py
78
78
  src/parsehub/utils/media_info.py
79
- test/test_bilibili_offline.py
80
79
  test/test_cli.py
81
80
  test/test_cli_config.py
82
81
  test/test_core_offline.py
83
- test/test_downloader.py
84
- test/test_instagram_duration_offline.py
85
- test/test_zuiyou_duration_offline.py
82
+ test/test_downloader.py
@@ -0,0 +1,27 @@
1
+ aiofiles>=25.1.0
2
+ beautifulsoup4>=4.15.0
3
+ loguru>=0.7.3
4
+ pydub>=0.25.1
5
+ python-dotenv>=1.2.4
6
+ tenacity>=9.2.1
7
+ urlextract>=1.10.0
8
+ yt-dlp[default]>=2026.11.4.232659.dev0
9
+ lxml>=6.1.3
10
+ pydantic>=2.14.3
11
+ markdownify>=1.2.3
12
+ markdown>=3.11.3
13
+ json5>=0.17.3
14
+ requests>=2.34.2
15
+ httpx[socks]>=0.28.1
16
+ pillow>=13.0.0
17
+ python-slugify[unidecode]>=9.1.3
18
+ opencv-python-headless>=5.0.0.93
19
+ cryptography>=50.0.2
20
+ gmssl>=3.2.2
21
+ signerpy>=0.12.0
22
+ curl-cffi>=0.17.0
23
+ xhshow>=0.2.0
24
+
25
+ [cli]
26
+ argcomplete>=3.7.2
27
+ platformdirs>=4.14.5