parsehub 2.3.7__tar.gz → 2.3.9__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.7/src/parsehub.egg-info → parsehub-2.3.9}/PKG-INFO +1 -1
- {parsehub-2.3.7 → parsehub-2.3.9}/pyproject.toml +1 -1
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/__init__.py +6 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/cli.py +2 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/base/ytdlp.py +1 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/bilibili.py +2 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/coolapk.py +2 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/douban.py +2 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/douyin.py +2 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/tiktok.py +2 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/xiaoheihe.py +18 -2
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/xiaoheihe.py +15 -4
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/result.py +76 -44
- {parsehub-2.3.7 → parsehub-2.3.9/src/parsehub.egg-info}/PKG-INFO +1 -1
- {parsehub-2.3.7 → parsehub-2.3.9}/test/test_cli.py +2 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/LICENSE +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/README.md +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/setup.cfg +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/__init__.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/cli_config.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/config/__init__.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/config/config.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/errors.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/__init__.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/base/__init__.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/base/base.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/__init__.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/bangumi.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/facebook.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/instagram.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/kuaishou.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/medium.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/pipix.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/snapchat.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/threads.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/tieba.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/toutiao.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/twitter.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/weibo.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/weixin.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/wxchannels.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/xhs.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/youtube.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/zhihu.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/zuiyou.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/__init__.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/bangumi.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/bilibili.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/coolapk.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/douban.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/douyin.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/instagram.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/kuaishou.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/medium.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/pipix.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/threads.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/tieba.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/tiktok.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/toutiao.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/twitter.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/weibo.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/weixin.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/wxchannels.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/xhs.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/zhihu.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/zuiyou.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/__init__.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/callback.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/media_file.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/media_ref.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/platform.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/post.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/utils/downloader.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/utils/helpers.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/utils/media_info.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/SOURCES.txt +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/dependency_links.txt +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/entry_points.txt +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/requires.txt +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/top_level.txt +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/test/test_cli_config.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/test/test_core_offline.py +0 -0
- {parsehub-2.3.7 → parsehub-2.3.9}/test/test_downloader.py +0 -0
|
@@ -61,6 +61,7 @@ class ParseHub:
|
|
|
61
61
|
parse_cookie: str | dict | None = None,
|
|
62
62
|
save_metadata: bool = False,
|
|
63
63
|
connections: int = 4,
|
|
64
|
+
concurrency: int = 4,
|
|
64
65
|
) -> DownloadResult:
|
|
65
66
|
"""下载
|
|
66
67
|
:param url: 分享文案 / 分享链接
|
|
@@ -73,6 +74,7 @@ class ParseHub:
|
|
|
73
74
|
:param parse_cookie: 解析 cookie
|
|
74
75
|
:param save_metadata: 保存解析结果为 metadata.json, 默认为 False
|
|
75
76
|
:param connections: 多线程下载连接数, 默认为 4
|
|
77
|
+
:param concurrency: 多文件并发下载数, 默认为 4
|
|
76
78
|
:return: DownloadResult
|
|
77
79
|
|
|
78
80
|
Note:
|
|
@@ -95,6 +97,7 @@ class ParseHub:
|
|
|
95
97
|
proxy=proxy,
|
|
96
98
|
save_metadata=save_metadata,
|
|
97
99
|
connections=connections,
|
|
100
|
+
concurrency=concurrency,
|
|
98
101
|
)
|
|
99
102
|
|
|
100
103
|
def download_sync(
|
|
@@ -109,6 +112,7 @@ class ParseHub:
|
|
|
109
112
|
parse_cookie: str | dict | None = None,
|
|
110
113
|
save_metadata: bool = False,
|
|
111
114
|
connections: int = 4,
|
|
115
|
+
concurrency: int = 4,
|
|
112
116
|
) -> DownloadResult:
|
|
113
117
|
"""
|
|
114
118
|
同步下载
|
|
@@ -122,6 +126,7 @@ class ParseHub:
|
|
|
122
126
|
:param parse_cookie: 解析 cookie
|
|
123
127
|
:param save_metadata: 保存解析结果为 metadata.json, 默认为 False
|
|
124
128
|
:param connections: 多线程下载连接数, 默认为 4
|
|
129
|
+
:param concurrency: 多文件并发下载数, 默认为 4
|
|
125
130
|
:return: DownloadResult
|
|
126
131
|
|
|
127
132
|
Note:
|
|
@@ -147,6 +152,7 @@ class ParseHub:
|
|
|
147
152
|
parse_cookie=parse_cookie,
|
|
148
153
|
save_metadata=save_metadata,
|
|
149
154
|
connections=connections,
|
|
155
|
+
concurrency=concurrency,
|
|
150
156
|
)
|
|
151
157
|
)
|
|
152
158
|
|
|
@@ -117,6 +117,7 @@ def _build_parser(prog: str) -> argparse.ArgumentParser:
|
|
|
117
117
|
download_parser.add_argument("-q", "--quiet", action="store_true", help="不输出状态和进度信息")
|
|
118
118
|
download_parser.add_argument("--no-progress", action="store_true", help="不显示下载进度")
|
|
119
119
|
download_parser.add_argument("--connections", type=int, default=4, help="单文件分片下载连接数,设为 1 可禁用分片")
|
|
120
|
+
download_parser.add_argument("--concurrency", type=int, default=4, help="多文件并发下载数,设为 1 即串行")
|
|
120
121
|
_add_json_options(download_parser)
|
|
121
122
|
download_parser.set_defaults(func=_cmd_download)
|
|
122
123
|
|
|
@@ -258,6 +259,7 @@ def _cmd_download(args: argparse.Namespace) -> int:
|
|
|
258
259
|
parse_cookie=parse_cookie,
|
|
259
260
|
save_metadata=args.save_metadata,
|
|
260
261
|
connections=args.connections,
|
|
262
|
+
concurrency=args.concurrency,
|
|
261
263
|
)
|
|
262
264
|
reporter.finish()
|
|
263
265
|
|
|
@@ -191,6 +191,7 @@ class BiliVideoParseResult(VideoParseResult):
|
|
|
191
191
|
proxy: str | None = None,
|
|
192
192
|
headers: dict | None = None,
|
|
193
193
|
connections: int = 4,
|
|
194
|
+
concurrency: int = 4,
|
|
194
195
|
) -> DownloadResult:
|
|
195
196
|
headers = {"referer": "https://www.bilibili.com", "User-Agent": UA}
|
|
196
197
|
return await super()._do_download(
|
|
@@ -201,6 +202,7 @@ class BiliVideoParseResult(VideoParseResult):
|
|
|
201
202
|
proxy=proxy,
|
|
202
203
|
headers=headers,
|
|
203
204
|
connections=connections,
|
|
205
|
+
concurrency=concurrency,
|
|
204
206
|
)
|
|
205
207
|
|
|
206
208
|
|
|
@@ -70,6 +70,7 @@ class CoolapkParseResult(ParseResult):
|
|
|
70
70
|
proxy: str | None = None,
|
|
71
71
|
headers: dict | None = None,
|
|
72
72
|
connections: int = 4,
|
|
73
|
+
concurrency: int = 4,
|
|
73
74
|
) -> "DownloadResult":
|
|
74
75
|
headers = {
|
|
75
76
|
"Accept": (
|
|
@@ -85,6 +86,7 @@ class CoolapkParseResult(ParseResult):
|
|
|
85
86
|
proxy=proxy,
|
|
86
87
|
headers=headers,
|
|
87
88
|
connections=connections,
|
|
89
|
+
concurrency=concurrency,
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
|
|
@@ -78,6 +78,7 @@ class DoubanParseResult(ParseResult):
|
|
|
78
78
|
proxy: str | None = None,
|
|
79
79
|
headers: dict | None = None,
|
|
80
80
|
connections: int = 4,
|
|
81
|
+
concurrency: int = 4,
|
|
81
82
|
) -> "DownloadResult":
|
|
82
83
|
headers = {"User-Agent": UA, "Referer": IMAGE_REFERER}
|
|
83
84
|
return await super()._do_download(
|
|
@@ -88,6 +89,7 @@ class DoubanParseResult(ParseResult):
|
|
|
88
89
|
proxy=proxy,
|
|
89
90
|
headers=headers,
|
|
90
91
|
connections=connections,
|
|
92
|
+
concurrency=concurrency,
|
|
91
93
|
)
|
|
92
94
|
|
|
93
95
|
|
|
@@ -88,6 +88,7 @@ class DouyinParseResult(ParseResult):
|
|
|
88
88
|
proxy: str | None = None,
|
|
89
89
|
headers: dict | None = None,
|
|
90
90
|
connections: int = 4,
|
|
91
|
+
concurrency: int = 4,
|
|
91
92
|
) -> "DownloadResult":
|
|
92
93
|
headers = {
|
|
93
94
|
"Referer": "https://www.douyin.com/",
|
|
@@ -100,6 +101,7 @@ class DouyinParseResult(ParseResult):
|
|
|
100
101
|
proxy=proxy,
|
|
101
102
|
headers=headers,
|
|
102
103
|
connections=connections,
|
|
104
|
+
concurrency=concurrency,
|
|
103
105
|
)
|
|
104
106
|
|
|
105
107
|
|
|
@@ -70,6 +70,7 @@ class TikTokVideoParseResult(VideoParseResult):
|
|
|
70
70
|
proxy: str | None = None,
|
|
71
71
|
headers: dict | None = None,
|
|
72
72
|
connections: int = 4,
|
|
73
|
+
concurrency: int = 4,
|
|
73
74
|
) -> "DownloadResult":
|
|
74
75
|
headers = {
|
|
75
76
|
"Referer": "https://www.tiktok.com/",
|
|
@@ -82,6 +83,7 @@ class TikTokVideoParseResult(VideoParseResult):
|
|
|
82
83
|
proxy=proxy,
|
|
83
84
|
headers=headers,
|
|
84
85
|
connections=connections,
|
|
86
|
+
concurrency=concurrency,
|
|
85
87
|
)
|
|
86
88
|
|
|
87
89
|
|
|
@@ -54,7 +54,23 @@ class XiaoHeiHeParser(BaseParser):
|
|
|
54
54
|
images: list[ImageRef | AniRef] = []
|
|
55
55
|
for media in xhh.media or []:
|
|
56
56
|
if media.type == XiaoHeiHeMediaType.IMAGE:
|
|
57
|
-
images.append(
|
|
57
|
+
images.append(
|
|
58
|
+
ImageRef(
|
|
59
|
+
url=media.url,
|
|
60
|
+
thumb_url=media.thumb_url,
|
|
61
|
+
ext=media.ext or "jpg",
|
|
62
|
+
width=media.width or 0,
|
|
63
|
+
height=media.height or 0,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
58
66
|
else:
|
|
59
|
-
images.append(
|
|
67
|
+
images.append(
|
|
68
|
+
AniRef(
|
|
69
|
+
url=media.url,
|
|
70
|
+
thumb_url=media.thumb_url,
|
|
71
|
+
ext=media.ext or "gif",
|
|
72
|
+
width=media.width or 0,
|
|
73
|
+
height=media.height or 0,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
60
76
|
return images
|
|
@@ -23,6 +23,11 @@ from cryptography.hazmat.primitives.ciphers.modes import CBC, ECB
|
|
|
23
23
|
from markdownify import MarkdownConverter
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def original_image_url(url: str) -> str:
|
|
27
|
+
path = url.split("?", 1)[0]
|
|
28
|
+
return re.sub(r"/thumb\.(\w+)$", r".\1", path)
|
|
29
|
+
|
|
30
|
+
|
|
26
31
|
class XiaoHeiHePostType(Enum):
|
|
27
32
|
VIDEO = "video"
|
|
28
33
|
IMAGE = "image" # 图文, 单张或多张图片 + 文案
|
|
@@ -40,6 +45,7 @@ class XiaoHeiHeMedia:
|
|
|
40
45
|
type: XiaoHeiHeMediaType
|
|
41
46
|
url: str
|
|
42
47
|
thumb_url: str | None = None
|
|
48
|
+
ext: str | None = None
|
|
43
49
|
height: int | None = None
|
|
44
50
|
width: int | None = None
|
|
45
51
|
|
|
@@ -89,11 +95,14 @@ class XiaoHeiHeAPI:
|
|
|
89
95
|
for image in text_list[1:]:
|
|
90
96
|
if image["type"] == "img":
|
|
91
97
|
media_type = XiaoHeiHeMediaType.GIF if ".gif" in image["url"] else XiaoHeiHeMediaType.IMAGE
|
|
98
|
+
url = original_image_url(image["url"])
|
|
99
|
+
ext_match = re.search(r"\.([^./]+)$", url)
|
|
92
100
|
images.append(
|
|
93
101
|
XiaoHeiHeMedia(
|
|
94
102
|
type=media_type,
|
|
95
|
-
url=
|
|
103
|
+
url=url,
|
|
96
104
|
thumb_url=image["url"],
|
|
105
|
+
ext=ext_match.group(1) if ext_match else None,
|
|
97
106
|
height=int(float(image.get("height", 0))),
|
|
98
107
|
width=int(float(image.get("width", 0))),
|
|
99
108
|
)
|
|
@@ -134,7 +143,7 @@ class XiaoHeiHeAPI:
|
|
|
134
143
|
"owner_only": "1",
|
|
135
144
|
**sig_params,
|
|
136
145
|
}
|
|
137
|
-
cookies = {"x_xhh_tokenid": await SecuritySm.get_d_id()}
|
|
146
|
+
cookies = {"x_xhh_tokenid": await SecuritySm.get_d_id(self.proxy)}
|
|
138
147
|
async with httpx.AsyncClient(proxy=self.proxy) as cli:
|
|
139
148
|
result = await cli.get(self.api_url + "/bbs/app/link/tree", params=params, cookies=cookies)
|
|
140
149
|
result.raise_for_status()
|
|
@@ -355,6 +364,8 @@ class XHHConverter(MarkdownConverter):
|
|
|
355
364
|
def convert_img(self, el, _, parent_tags):
|
|
356
365
|
alt = el.attrs.get("alt", None) or ""
|
|
357
366
|
src = el.attrs.get("data-original", None) or ""
|
|
367
|
+
if src:
|
|
368
|
+
src = original_image_url(src)
|
|
358
369
|
title = el.attrs.get("title", None) or ""
|
|
359
370
|
title_part = ' "{}"'.format(title.replace('"', r"\"")) if title else ""
|
|
360
371
|
options = cast(dict[str, Any], getattr(self, "options")) # noqa: B009
|
|
@@ -504,7 +515,7 @@ class SecuritySm:
|
|
|
504
515
|
return v + smsk_web + "0"
|
|
505
516
|
|
|
506
517
|
@classmethod
|
|
507
|
-
async def get_d_id(cls):
|
|
518
|
+
async def get_d_id(cls, proxy: str | None = None):
|
|
508
519
|
uid = str(uuid.uuid4()).encode("utf-8")
|
|
509
520
|
priId = hashlib.md5(uid).hexdigest()[0:16]
|
|
510
521
|
encrypted_uid = cls.PK.encrypt(uid, padding.PKCS1v15())
|
|
@@ -531,7 +542,7 @@ class SecuritySm:
|
|
|
531
542
|
des_target["tn"] = hashlib.md5(cls.get_tn(des_target).encode()).hexdigest()
|
|
532
543
|
|
|
533
544
|
des_result = cls._AES(cls.GZIP(cls._DES(des_target)), priId.encode("utf-8"))
|
|
534
|
-
async with httpx.AsyncClient() as client:
|
|
545
|
+
async with httpx.AsyncClient(proxy=proxy) as client:
|
|
535
546
|
response = await client.post(
|
|
536
547
|
cls.DEVICES_INFO_URL,
|
|
537
548
|
json={
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import json
|
|
2
3
|
import shutil
|
|
3
4
|
import time
|
|
@@ -89,6 +90,7 @@ class ParseResult(ABC): # noqa: B024
|
|
|
89
90
|
proxy: str | None = None,
|
|
90
91
|
headers: dict | None = None,
|
|
91
92
|
connections: int = 4,
|
|
93
|
+
concurrency: int = 4,
|
|
92
94
|
) -> "DownloadResult":
|
|
93
95
|
"""
|
|
94
96
|
执行下载
|
|
@@ -99,48 +101,34 @@ class ParseResult(ABC): # noqa: B024
|
|
|
99
101
|
:param proxy: 代理
|
|
100
102
|
:param headers: 请求头
|
|
101
103
|
:param connections: 多线程下载连接数, 默认为 4
|
|
104
|
+
:param concurrency: 多文件并发下载数, 默认为 4
|
|
102
105
|
:return: DownloadResult
|
|
103
106
|
"""
|
|
104
107
|
if self.media is None:
|
|
105
108
|
return DownloadResult(output_dir=output_dir, media=[])
|
|
106
109
|
media_list = list(self.media) if isinstance(self.media, Sequence) else [self.media]
|
|
107
110
|
is_single = not isinstance(self.media, Sequence)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
async def _byte_callback(current: int, total: int, *args: Any, **kwargs: Any) -> None:
|
|
118
|
-
await callback(current, total, "bytes", *args, **kwargs)
|
|
119
|
-
|
|
120
|
-
dl_progress = _byte_callback
|
|
121
|
-
dl_progress_args = callback_args
|
|
122
|
-
dl_progress_kwargs = callback_kwargs or {}
|
|
123
|
-
|
|
124
|
-
index = i + 1
|
|
125
|
-
|
|
111
|
+
concurrency = max(1, concurrency)
|
|
112
|
+
|
|
113
|
+
async def _download_media(
|
|
114
|
+
media: AnyMediaRef,
|
|
115
|
+
stem: str,
|
|
116
|
+
progress: ProgressCallback | None = None,
|
|
117
|
+
progress_args: tuple = (),
|
|
118
|
+
progress_kwargs: dict | None = None,
|
|
119
|
+
) -> AnyMediaFile:
|
|
126
120
|
try:
|
|
127
|
-
save_path = (
|
|
128
|
-
output_dir.joinpath(f"{self.name}.{media.ext}")
|
|
129
|
-
if is_single
|
|
130
|
-
else output_dir.joinpath(f"{index:03d}_{self.name}.{media.ext}")
|
|
131
|
-
)
|
|
132
121
|
f = await download(
|
|
133
122
|
media.url,
|
|
134
|
-
|
|
123
|
+
output_dir.joinpath(f"{stem}.{media.ext}"),
|
|
135
124
|
headers=headers,
|
|
136
125
|
proxy=proxy,
|
|
137
|
-
progress=
|
|
138
|
-
progress_args=
|
|
139
|
-
progress_kwargs=
|
|
126
|
+
progress=progress,
|
|
127
|
+
progress_args=progress_args,
|
|
128
|
+
progress_kwargs=progress_kwargs,
|
|
140
129
|
connections=connections,
|
|
141
130
|
)
|
|
142
131
|
except Exception as e:
|
|
143
|
-
shutil.rmtree(output_dir, ignore_errors=True)
|
|
144
132
|
raise DownloadError(f"下载失败: {e}") from e
|
|
145
133
|
|
|
146
134
|
mf: AnyMediaFile
|
|
@@ -155,36 +143,74 @@ class ParseResult(ABC): # noqa: B024
|
|
|
155
143
|
mf = LivePhotoFile(path=f, width=media.width, height=media.height, duration=media.duration)
|
|
156
144
|
if media.video_url:
|
|
157
145
|
try:
|
|
158
|
-
save_path = (
|
|
159
|
-
output_dir.joinpath(f"{self.name}_video.{media.video_ext}")
|
|
160
|
-
if is_single
|
|
161
|
-
else output_dir.joinpath(f"{index:03d}_{self.name}_video.{media.video_ext}")
|
|
162
|
-
)
|
|
163
146
|
vf = await download(
|
|
164
147
|
media.video_url,
|
|
165
|
-
|
|
148
|
+
output_dir.joinpath(f"{stem}_video.{media.video_ext}"),
|
|
166
149
|
headers=headers,
|
|
167
150
|
proxy=proxy,
|
|
168
151
|
connections=connections,
|
|
169
152
|
)
|
|
170
153
|
except Exception as e:
|
|
171
|
-
shutil.rmtree(output_dir, ignore_errors=True)
|
|
172
154
|
raise DownloadError(f"LivePhoto 视频下载失败: {e}") from e
|
|
173
155
|
else:
|
|
174
156
|
mf.video_path = vf
|
|
175
157
|
|
|
176
|
-
|
|
158
|
+
return mf
|
|
159
|
+
|
|
160
|
+
if is_single:
|
|
161
|
+
media = media_list[0]
|
|
162
|
+
dl_progress = None
|
|
163
|
+
dl_progress_args = ()
|
|
164
|
+
dl_progress_kwargs: dict = {}
|
|
165
|
+
if callback:
|
|
166
|
+
|
|
167
|
+
async def _byte_callback(current: int, total: int, *args: Any, **kwargs: Any) -> None:
|
|
168
|
+
await callback(current, total, "bytes", *args, **kwargs)
|
|
177
169
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
170
|
+
dl_progress = _byte_callback
|
|
171
|
+
dl_progress_args = callback_args
|
|
172
|
+
dl_progress_kwargs = callback_kwargs or {}
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
mf = await _download_media(
|
|
176
|
+
media,
|
|
177
|
+
self.name,
|
|
178
|
+
progress=dl_progress,
|
|
179
|
+
progress_args=dl_progress_args,
|
|
180
|
+
progress_kwargs=dl_progress_kwargs,
|
|
184
181
|
)
|
|
182
|
+
except DownloadError:
|
|
183
|
+
shutil.rmtree(output_dir, ignore_errors=True)
|
|
184
|
+
raise
|
|
185
|
+
return DownloadResult(mf, output_dir)
|
|
186
|
+
|
|
187
|
+
n = len(media_list)
|
|
188
|
+
results: list[AnyMediaFile | None] = [None] * n
|
|
189
|
+
semaphore = asyncio.Semaphore(concurrency)
|
|
190
|
+
completed = 0
|
|
191
|
+
|
|
192
|
+
async def _download_one(i: int, media: AnyMediaRef) -> AnyMediaFile:
|
|
193
|
+
nonlocal completed
|
|
194
|
+
async with semaphore:
|
|
195
|
+
mf = await _download_media(media, f"{i + 1:03d}_{self.name}")
|
|
196
|
+
results[i] = mf
|
|
197
|
+
completed += 1
|
|
198
|
+
if callback:
|
|
199
|
+
await callback(completed, n, "count", *callback_args)
|
|
200
|
+
return mf
|
|
201
|
+
|
|
202
|
+
tasks = [asyncio.create_task(_download_one(i, media)) for i, media in enumerate(media_list)]
|
|
203
|
+
try:
|
|
204
|
+
await asyncio.gather(*tasks)
|
|
205
|
+
except BaseException:
|
|
206
|
+
for task in tasks:
|
|
207
|
+
task.cancel()
|
|
208
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
209
|
+
shutil.rmtree(output_dir, ignore_errors=True)
|
|
210
|
+
raise
|
|
185
211
|
|
|
186
|
-
|
|
187
|
-
return DownloadResult(
|
|
212
|
+
result_list = [media for media in results if media is not None]
|
|
213
|
+
return DownloadResult(result_list, output_dir)
|
|
188
214
|
|
|
189
215
|
async def download(
|
|
190
216
|
self,
|
|
@@ -196,6 +222,7 @@ class ParseResult(ABC): # noqa: B024
|
|
|
196
222
|
proxy: str | None = None,
|
|
197
223
|
save_metadata: bool = False,
|
|
198
224
|
connections: int = 4,
|
|
225
|
+
concurrency: int = 4,
|
|
199
226
|
) -> "DownloadResult":
|
|
200
227
|
"""
|
|
201
228
|
:param path: 保存路径
|
|
@@ -205,6 +232,7 @@ class ParseResult(ABC): # noqa: B024
|
|
|
205
232
|
:param proxy: 代理
|
|
206
233
|
:param save_metadata: 保存解析结果为 metadata.json, 默认为 False
|
|
207
234
|
:param connections: 多线程下载连接数, 默认为 4
|
|
235
|
+
:param concurrency: 多文件并发下载数, 默认为 4
|
|
208
236
|
:return: DownloadResult
|
|
209
237
|
|
|
210
238
|
Note:
|
|
@@ -238,6 +266,7 @@ class ParseResult(ABC): # noqa: B024
|
|
|
238
266
|
callback_kwargs=callback_kwargs,
|
|
239
267
|
proxy=proxy,
|
|
240
268
|
connections=connections,
|
|
269
|
+
concurrency=concurrency,
|
|
241
270
|
)
|
|
242
271
|
except Exception as e:
|
|
243
272
|
shutil.rmtree(output_dir, ignore_errors=True)
|
|
@@ -253,6 +282,7 @@ class ParseResult(ABC): # noqa: B024
|
|
|
253
282
|
proxy: str | None = None,
|
|
254
283
|
save_metadata: bool = False,
|
|
255
284
|
connections: int = 4,
|
|
285
|
+
concurrency: int = 4,
|
|
256
286
|
) -> "DownloadResult":
|
|
257
287
|
"""
|
|
258
288
|
:param path: 保存路径
|
|
@@ -262,6 +292,7 @@ class ParseResult(ABC): # noqa: B024
|
|
|
262
292
|
:param proxy: 代理
|
|
263
293
|
:param save_metadata: 保存解析结果为 metadata.json, 默认为 False
|
|
264
294
|
:param connections: 多线程下载连接数, 默认为 4
|
|
295
|
+
:param concurrency: 多文件并发下载数, 默认为 4
|
|
265
296
|
:return: DownloadResult
|
|
266
297
|
|
|
267
298
|
Note:
|
|
@@ -284,6 +315,7 @@ class ParseResult(ABC): # noqa: B024
|
|
|
284
315
|
proxy=proxy,
|
|
285
316
|
save_metadata=save_metadata,
|
|
286
317
|
connections=connections,
|
|
318
|
+
concurrency=concurrency,
|
|
287
319
|
)
|
|
288
320
|
)
|
|
289
321
|
|
|
@@ -62,6 +62,7 @@ class FakeParseHub:
|
|
|
62
62
|
parse_cookie=None,
|
|
63
63
|
save_metadata=False,
|
|
64
64
|
connections=4,
|
|
65
|
+
concurrency=4,
|
|
65
66
|
):
|
|
66
67
|
self.download_calls.append(
|
|
67
68
|
{
|
|
@@ -75,6 +76,7 @@ class FakeParseHub:
|
|
|
75
76
|
"parse_cookie": parse_cookie,
|
|
76
77
|
"save_metadata": save_metadata,
|
|
77
78
|
"connections": connections,
|
|
79
|
+
"concurrency": concurrency,
|
|
78
80
|
}
|
|
79
81
|
)
|
|
80
82
|
if callback:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|