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.
Files changed (83) hide show
  1. {parsehub-2.3.7/src/parsehub.egg-info → parsehub-2.3.9}/PKG-INFO +1 -1
  2. {parsehub-2.3.7 → parsehub-2.3.9}/pyproject.toml +1 -1
  3. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/__init__.py +6 -0
  4. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/cli.py +2 -0
  5. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/base/ytdlp.py +1 -0
  6. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/bilibili.py +2 -0
  7. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/coolapk.py +2 -0
  8. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/douban.py +2 -0
  9. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/douyin.py +2 -0
  10. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/tiktok.py +2 -0
  11. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/xiaoheihe.py +18 -2
  12. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/xiaoheihe.py +15 -4
  13. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/result.py +76 -44
  14. {parsehub-2.3.7 → parsehub-2.3.9/src/parsehub.egg-info}/PKG-INFO +1 -1
  15. {parsehub-2.3.7 → parsehub-2.3.9}/test/test_cli.py +2 -0
  16. {parsehub-2.3.7 → parsehub-2.3.9}/LICENSE +0 -0
  17. {parsehub-2.3.7 → parsehub-2.3.9}/README.md +0 -0
  18. {parsehub-2.3.7 → parsehub-2.3.9}/setup.cfg +0 -0
  19. {parsehub-2.3.7 → parsehub-2.3.9}/src/__init__.py +0 -0
  20. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/cli_config.py +0 -0
  21. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/config/__init__.py +0 -0
  22. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/config/config.py +0 -0
  23. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/errors.py +0 -0
  24. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/__init__.py +0 -0
  25. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/base/__init__.py +0 -0
  26. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/base/base.py +0 -0
  27. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/__init__.py +0 -0
  28. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/bangumi.py +0 -0
  29. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/facebook.py +0 -0
  30. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/instagram.py +0 -0
  31. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/kuaishou.py +0 -0
  32. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/medium.py +0 -0
  33. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/pipix.py +0 -0
  34. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/snapchat.py +0 -0
  35. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/threads.py +0 -0
  36. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/tieba.py +0 -0
  37. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/toutiao.py +0 -0
  38. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/twitter.py +0 -0
  39. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/weibo.py +0 -0
  40. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/weixin.py +0 -0
  41. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/wxchannels.py +0 -0
  42. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/xhs.py +0 -0
  43. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/youtube.py +0 -0
  44. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/zhihu.py +0 -0
  45. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/parsers/parser/zuiyou.py +0 -0
  46. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/__init__.py +0 -0
  47. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/bangumi.py +0 -0
  48. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/bilibili.py +0 -0
  49. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/coolapk.py +0 -0
  50. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/douban.py +0 -0
  51. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/douyin.py +0 -0
  52. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/instagram.py +0 -0
  53. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/kuaishou.py +0 -0
  54. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/medium.py +0 -0
  55. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/pipix.py +0 -0
  56. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/threads.py +0 -0
  57. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/tieba.py +0 -0
  58. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/tiktok.py +0 -0
  59. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/toutiao.py +0 -0
  60. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/twitter.py +0 -0
  61. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/weibo.py +0 -0
  62. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/weixin.py +0 -0
  63. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/wxchannels.py +0 -0
  64. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/xhs.py +0 -0
  65. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/zhihu.py +0 -0
  66. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/provider_api/zuiyou.py +0 -0
  67. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/__init__.py +0 -0
  68. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/callback.py +0 -0
  69. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/media_file.py +0 -0
  70. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/media_ref.py +0 -0
  71. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/platform.py +0 -0
  72. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/types/post.py +0 -0
  73. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/utils/downloader.py +0 -0
  74. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/utils/helpers.py +0 -0
  75. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub/utils/media_info.py +0 -0
  76. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/SOURCES.txt +0 -0
  77. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/dependency_links.txt +0 -0
  78. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/entry_points.txt +0 -0
  79. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/requires.txt +0 -0
  80. {parsehub-2.3.7 → parsehub-2.3.9}/src/parsehub.egg-info/top_level.txt +0 -0
  81. {parsehub-2.3.7 → parsehub-2.3.9}/test/test_cli_config.py +0 -0
  82. {parsehub-2.3.7 → parsehub-2.3.9}/test/test_core_offline.py +0 -0
  83. {parsehub-2.3.7 → parsehub-2.3.9}/test/test_downloader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.3.7
3
+ Version: 2.3.9
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "parsehub"
3
- version = "2.3.7"
3
+ version = "2.3.9"
4
4
  description = "轻量、异步、开箱即用的社交媒体聚合解析库"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12.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
 
@@ -477,6 +477,7 @@ class YtVideoParseResult(VideoParseResult):
477
477
  proxy: str | None = None,
478
478
  headers: dict | None = None,
479
479
  connections: int = 4,
480
+ concurrency: int = 4,
480
481
  ) -> "DownloadResult":
481
482
  if callback_kwargs is None:
482
483
  callback_kwargs = {}
@@ -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(ImageRef(url=media.url, width=media.width or 0, height=media.height or 0))
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(AniRef(url=media.url, width=media.width or 0, height=media.height or 0))
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=image["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
- result_list: list[AnyMediaFile] = []
110
-
111
- for i, media in enumerate(media_list):
112
- dl_progress = None
113
- dl_progress_args = ()
114
- dl_progress_kwargs: dict = {}
115
- if callback and is_single:
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
- save_path,
123
+ output_dir.joinpath(f"{stem}.{media.ext}"),
135
124
  headers=headers,
136
125
  proxy=proxy,
137
- progress=dl_progress,
138
- progress_args=dl_progress_args,
139
- progress_kwargs=dl_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
- save_path,
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
- result_list.append(mf)
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
- if callback and not is_single:
179
- await callback(
180
- len(result_list),
181
- len(media_list),
182
- "count",
183
- *callback_args,
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
- result_media = result_list[0] if is_single else result_list
187
- return DownloadResult(result_media, output_dir)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.3.7
3
+ Version: 2.3.9
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
@@ -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