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.
- {parsehub-2.3.17/src/parsehub.egg-info → parsehub-2.4.1}/PKG-INFO +23 -23
- {parsehub-2.3.17 → parsehub-2.4.1}/README.md +1 -1
- {parsehub-2.3.17 → parsehub-2.4.1}/pyproject.toml +25 -25
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/bilibili.py +66 -3
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/instagram.py +9 -7
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/kuaishou.py +1 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/bilibili.py +26 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/instagram.py +1 -2
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/wxchannels.py +1 -3
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/__init__.py +6 -2
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/media_file.py +16 -2
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/media_ref.py +18 -2
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/post.py +1 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/result.py +20 -3
- {parsehub-2.3.17 → parsehub-2.4.1/src/parsehub.egg-info}/PKG-INFO +23 -23
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub.egg-info/SOURCES.txt +1 -4
- parsehub-2.4.1/src/parsehub.egg-info/requires.txt +27 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/test/test_core_offline.py +6 -335
- parsehub-2.3.17/src/parsehub.egg-info/requires.txt +0 -27
- parsehub-2.3.17/test/test_bilibili_offline.py +0 -63
- parsehub-2.3.17/test/test_instagram_duration_offline.py +0 -134
- parsehub-2.3.17/test/test_zuiyou_duration_offline.py +0 -69
- {parsehub-2.3.17 → parsehub-2.4.1}/LICENSE +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/setup.cfg +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/__init__.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/__init__.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/cli.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/cli_config.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/config/__init__.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/config/config.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/errors.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/__init__.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/base/__init__.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/base/base.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/base/ytdlp.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/__init__.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/bangumi.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/coolapk.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/douban.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/douyin.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/facebook.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/medium.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/pipix.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/snapchat.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/threads.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/tieba.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/tiktok.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/toutiao.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/twitter.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/weibo.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/weixin.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/wxchannels.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/xhs.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/youtube.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/zhihu.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/parsers/parser/zuiyou.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/__init__.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/bangumi.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/coolapk.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/douban.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/douyin.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/kuaishou.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/medium.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/pipix.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/threads.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/tieba.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/tiktok.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/toutiao.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/twitter.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/weibo.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/weixin.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/xhs.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/xiaoheihe.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/zhihu.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/provider_api/zuiyou.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/callback.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/types/platform.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/utils/downloader.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/utils/fallback.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/utils/helpers.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub/utils/media_info.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub.egg-info/dependency_links.txt +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub.egg-info/entry_points.txt +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/src/parsehub.egg-info/top_level.txt +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/test/test_cli.py +0 -0
- {parsehub-2.3.17 → parsehub-2.4.1}/test/test_cli_config.py +0 -0
- {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
|
+
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>=
|
|
19
|
-
Requires-Dist: beautifulsoup4>=4.
|
|
20
|
-
Requires-Dist: loguru>=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.
|
|
23
|
-
Requires-Dist: tenacity>=
|
|
24
|
-
Requires-Dist: urlextract>=1.
|
|
25
|
-
Requires-Dist: yt-dlp[default]>=2026.
|
|
26
|
-
Requires-Dist: lxml>=
|
|
27
|
-
Requires-Dist: pydantic>=
|
|
28
|
-
Requires-Dist: markdownify>=1.
|
|
29
|
-
Requires-Dist: markdown>=3.
|
|
30
|
-
Requires-Dist: json5>=0.
|
|
31
|
-
Requires-Dist: requests
|
|
32
|
-
Requires-Dist: httpx[socks]>=0.
|
|
33
|
-
Requires-Dist: pillow>=
|
|
34
|
-
Requires-Dist: python-slugify[unidecode]>=
|
|
35
|
-
Requires-Dist: opencv-python-headless>=
|
|
36
|
-
Requires-Dist: cryptography>=
|
|
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.
|
|
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.
|
|
43
|
-
Requires-Dist: platformdirs>=4.5
|
|
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
|
| **微博** | ✅ | ✅ | |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "parsehub"
|
|
3
|
-
version = "2.
|
|
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>=
|
|
22
|
-
"beautifulsoup4>=4.
|
|
23
|
-
"loguru>=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.
|
|
26
|
-
"tenacity>=
|
|
27
|
-
"urlextract>=1.
|
|
28
|
-
"yt-dlp[default]>=2026.
|
|
29
|
-
"lxml>=
|
|
30
|
-
"pydantic>=
|
|
31
|
-
"markdownify>=1.
|
|
32
|
-
"markdown>=3.
|
|
33
|
-
"json5>=0.
|
|
34
|
-
"requests",
|
|
35
|
-
"httpx[socks]>=0.
|
|
36
|
-
"pillow>=
|
|
37
|
-
"python-slugify[unidecode]>=
|
|
38
|
-
"opencv-python-headless>=
|
|
39
|
-
"cryptography>=
|
|
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.
|
|
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.
|
|
53
|
-
"platformdirs>=4.5
|
|
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
|
|
59
|
-
"pytest>=9.0
|
|
60
|
-
"ruff>=0.
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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"]
|
|
@@ -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
|
+
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>=
|
|
19
|
-
Requires-Dist: beautifulsoup4>=4.
|
|
20
|
-
Requires-Dist: loguru>=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.
|
|
23
|
-
Requires-Dist: tenacity>=
|
|
24
|
-
Requires-Dist: urlextract>=1.
|
|
25
|
-
Requires-Dist: yt-dlp[default]>=2026.
|
|
26
|
-
Requires-Dist: lxml>=
|
|
27
|
-
Requires-Dist: pydantic>=
|
|
28
|
-
Requires-Dist: markdownify>=1.
|
|
29
|
-
Requires-Dist: markdown>=3.
|
|
30
|
-
Requires-Dist: json5>=0.
|
|
31
|
-
Requires-Dist: requests
|
|
32
|
-
Requires-Dist: httpx[socks]>=0.
|
|
33
|
-
Requires-Dist: pillow>=
|
|
34
|
-
Requires-Dist: python-slugify[unidecode]>=
|
|
35
|
-
Requires-Dist: opencv-python-headless>=
|
|
36
|
-
Requires-Dist: cryptography>=
|
|
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.
|
|
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.
|
|
43
|
-
Requires-Dist: platformdirs>=4.5
|
|
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
|