parsehub 2.1.6__tar.gz → 2.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {parsehub-2.1.6/src/parsehub.egg-info → parsehub-2.1.7}/PKG-INFO +1 -1
  2. {parsehub-2.1.6 → parsehub-2.1.7}/pyproject.toml +1 -1
  3. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/base/base.py +4 -2
  4. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/bilibili.py +16 -15
  5. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/instagram.py +1 -1
  6. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/threads.py +5 -1
  7. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/twitter.py +3 -2
  8. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/bilibili.py +17 -17
  9. {parsehub-2.1.6 → parsehub-2.1.7/src/parsehub.egg-info}/PKG-INFO +1 -1
  10. {parsehub-2.1.6 → parsehub-2.1.7}/test/test_core_offline.py +1 -0
  11. {parsehub-2.1.6 → parsehub-2.1.7}/LICENSE +0 -0
  12. {parsehub-2.1.6 → parsehub-2.1.7}/README.md +0 -0
  13. {parsehub-2.1.6 → parsehub-2.1.7}/setup.cfg +0 -0
  14. {parsehub-2.1.6 → parsehub-2.1.7}/src/__init__.py +0 -0
  15. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/__init__.py +0 -0
  16. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/cli.py +0 -0
  17. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/cli_config.py +0 -0
  18. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/config/__init__.py +0 -0
  19. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/config/config.py +0 -0
  20. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/errors.py +0 -0
  21. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/__init__.py +0 -0
  22. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/base/__init__.py +0 -0
  23. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/base/ytdlp.py +0 -0
  24. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/__init__.py +0 -0
  25. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/coolapk.py +0 -0
  26. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/douyin.py +0 -0
  27. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/facebook.py +0 -0
  28. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/kuaishou.py +0 -0
  29. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/pipix.py +0 -0
  30. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/snapchat.py +0 -0
  31. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/tieba.py +0 -0
  32. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/tiktok.py +0 -0
  33. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/weibo.py +0 -0
  34. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/weixin.py +0 -0
  35. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/xhs.py +0 -0
  36. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
  37. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/youtube.py +0 -0
  38. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/zhihu.py +0 -0
  39. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/parsers/parser/zuiyou.py +0 -0
  40. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/__init__.py +0 -0
  41. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/coolapk.py +0 -0
  42. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/douyin.py +0 -0
  43. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/instagram.py +0 -0
  44. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/kuaishou.py +0 -0
  45. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/pipix.py +0 -0
  46. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/threads.py +0 -0
  47. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/tieba.py +0 -0
  48. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/tiktok.py +0 -0
  49. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/twitter.py +0 -0
  50. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/weibo.py +0 -0
  51. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/weixin.py +0 -0
  52. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/xhs.py +0 -0
  53. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/xiaoheihe.py +0 -0
  54. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/zhihu.py +0 -0
  55. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/provider_api/zuiyou.py +0 -0
  56. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/types/__init__.py +0 -0
  57. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/types/callback.py +0 -0
  58. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/types/media_file.py +0 -0
  59. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/types/media_ref.py +0 -0
  60. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/types/platform.py +0 -0
  61. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/types/post.py +0 -0
  62. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/types/result.py +0 -0
  63. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/utils/downloader.py +0 -0
  64. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/utils/helpers.py +0 -0
  65. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub/utils/media_info.py +0 -0
  66. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub.egg-info/SOURCES.txt +0 -0
  67. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub.egg-info/dependency_links.txt +0 -0
  68. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub.egg-info/entry_points.txt +0 -0
  69. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub.egg-info/requires.txt +0 -0
  70. {parsehub-2.1.6 → parsehub-2.1.7}/src/parsehub.egg-info/top_level.txt +0 -0
  71. {parsehub-2.1.6 → parsehub-2.1.7}/test/test_cli.py +0 -0
  72. {parsehub-2.1.6 → parsehub-2.1.7}/test/test_cli_config.py +0 -0
  73. {parsehub-2.1.6 → parsehub-2.1.7}/test/test_downloader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.1.6
3
+ Version: 2.1.7
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.1.6"
3
+ version = "2.1.7"
4
4
  description = "轻量、异步、开箱即用的社交媒体聚合解析库"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12.0"
@@ -77,11 +77,12 @@ class BaseParser(ABC):
77
77
  """
78
78
  raise NotImplementedError
79
79
 
80
- async def get_raw_url(self, url: str, clean_all: bool = False) -> str:
80
+ async def get_raw_url(self, url: str, *, clean_all: bool = False, headers: dict | None = None) -> str:
81
81
  """
82
82
  清除链接中的参数
83
83
  :param url: 链接
84
84
  :param clean_all: 是否清除全部可清除的参数 (包括解析后才需清除的参数)
85
+ :param headers: 请求头, 默认为 {"User-Agent": UA}, 不需要请求头需传入空字典
85
86
 
86
87
  Example:
87
88
  以小红书为例,其解析器配置如下::
@@ -109,13 +110,14 @@ class BaseParser(ABC):
109
110
  url = matched_url or url
110
111
  if not url.startswith("http"):
111
112
  url = f"https://{url}"
113
+
112
114
  if any(x in url for x in self.__redirect_keywords__):
113
115
  async with httpx.AsyncClient(proxy=self.proxy, timeout=30) as client:
114
116
  try:
115
117
  r = await client.get(
116
118
  url,
117
119
  follow_redirects=True,
118
- headers={"User-Agent": UA},
120
+ headers={"User-Agent": UA} if headers is None else headers,
119
121
  )
120
122
  except (httpx.ReadTimeout, httpx.ConnectTimeout) as e:
121
123
  raise ParseError("获取原始链接超时") from e
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  from pathlib import Path
5
- from typing import cast
5
+ from typing import Any, cast
6
6
  from urllib.parse import parse_qs, urlparse
7
7
 
8
8
  from loguru import logger
@@ -34,7 +34,7 @@ class BiliParse(BaseParser):
34
34
  async def _do_parse(self, raw_url: str) -> YtVideoParseResult | BiliVideoParseResult | ImageParseResult:
35
35
  if await self.is_dynamic(raw_url):
36
36
  dynamic = await self.get_dynamic_info(raw_url)
37
- content = self.hashtag_handler(dynamic.content)
37
+ content = self.hashtag_handler(dynamic.content or "")
38
38
  photos: list[LivePhotoRef | ImageRef] = []
39
39
  if dynamic.images:
40
40
  for i in dynamic.images:
@@ -43,7 +43,7 @@ class BiliParse(BaseParser):
43
43
  else:
44
44
  photos.append(ImageRef(url=i.url, width=i.width, height=i.height))
45
45
  return ImageParseResult(
46
- title=dynamic.title,
46
+ title=dynamic.title or "",
47
47
  content=content,
48
48
  photo=photos,
49
49
  )
@@ -65,18 +65,18 @@ class BiliParse(BaseParser):
65
65
  return False
66
66
 
67
67
  @classmethod
68
- def match(cls, url: str) -> bool:
69
- if cls._is_bvid(url):
68
+ def match(cls, text: str) -> bool:
69
+ if cls._is_bvid(text):
70
70
  return True
71
71
  else:
72
- return super().match(url)
72
+ return super().match(text)
73
73
 
74
- async def get_raw_url(self, url: str, clean_all: bool = False) -> str:
74
+ async def get_raw_url(self, url: str, **kwargs: Any) -> str:
75
75
  """获取原始链接"""
76
76
  if self._is_bvid(url):
77
77
  return f"https://www.bilibili.com/video/{url}"
78
78
  else:
79
- return await super().get_raw_url(url, clean_all=clean_all)
79
+ return await super().get_raw_url(url, **kwargs)
80
80
 
81
81
  @staticmethod
82
82
  async def is_dynamic(url: str) -> str | None:
@@ -86,14 +86,15 @@ class BiliParse(BaseParser):
86
86
  return None
87
87
 
88
88
  async def get_dynamic_info(self, url: str) -> BiliDynamic:
89
- async with BiliAPI(proxy=self.proxy) as bili:
90
- try:
89
+ try:
90
+ async with BiliAPI(proxy=self.proxy) as bili:
91
91
  dynamic_info = await bili.get_dynamic_info(url, cookie=self.cookie.get_value())
92
- except Exception as e:
93
- if "风控" in str(e):
94
- raise ParseError(f"账号风控\n使用的cookie: {self.cookie}") from e
95
- raise ParseError(str(e)) from e
96
- return cast(BiliDynamic, dynamic_info)
92
+ except Exception as e:
93
+ if "风控" in str(e):
94
+ raise ParseError(f"账号风控\n使用的cookie: {self.cookie}") from e
95
+ raise ParseError(str(e)) from e
96
+ else:
97
+ return cast(BiliDynamic, dynamic_info)
97
98
 
98
99
  async def bili_api_parse(self, url: str) -> BiliVideoParseResult | ImageParseResult:
99
100
  async with BiliAPI(proxy=self.proxy) as bili:
@@ -61,7 +61,7 @@ class InstagramParser(BaseParser):
61
61
  else:
62
62
  raise ParseError("受限视频无法解析: 你必须年满 18 周岁才能观看这个视频") from e
63
63
  case _:
64
- raise ParseError("无法获取帖子内容(可能为私人内容)") from e
64
+ raise ParseError("无法获取帖子内容") from e
65
65
  except Exception as e:
66
66
  if cookie:
67
67
  text = f"Instagram 账号可能已被封禁\n\n使用的Cookie: {cookie}"
@@ -6,7 +6,11 @@ from ..base.base import BaseParser
6
6
  class ThreadsParser(BaseParser):
7
7
  __platform__ = Platform.THREADS
8
8
  __supported_type__ = ["视频", "图文"]
9
- __match__ = r"^(http(s)?://)?.+threads.com/@[\w.]+/post/.*"
9
+ __match__ = r"^(http(s)?://)?.+threads.com/(@[\w.]+/post|share)/.*"
10
+ __redirect_keywords__ = ["/share/"]
11
+
12
+ async def get_raw_url(self, url: str, *, clean_all: bool = False, headers: dict | None = None) -> str:
13
+ return await super().get_raw_url(url, clean_all=clean_all, headers={})
10
14
 
11
15
  async def _do_parse(self, raw_url: str) -> "MultimediaParseResult":
12
16
  post = await self._parse(raw_url)
@@ -1,3 +1,4 @@
1
+ from typing import Any
1
2
  from urllib.parse import urlparse, urlunparse
2
3
 
3
4
  from ...provider_api.twitter import (
@@ -29,8 +30,8 @@ class TwitterParser(BaseParser):
29
30
  tweet = await self._parse(raw_url)
30
31
  return await self.media_parse(tweet)
31
32
 
32
- async def get_raw_url(self, url: str, clean_all: bool = False) -> str:
33
- url = await super().get_raw_url(url, clean_all=clean_all)
33
+ async def get_raw_url(self, url: str, **kwargs: Any) -> str:
34
+ url = await super().get_raw_url(url, **kwargs)
34
35
  return str(urlunparse(urlparse(url)._replace(netloc="x.com")))
35
36
 
36
37
  async def _parse(self, url: str) -> TwitterTweet:
@@ -8,7 +8,7 @@ from dataclasses import dataclass
8
8
  from enum import Enum
9
9
  from functools import reduce
10
10
  from hashlib import md5
11
- from typing import Any, cast
11
+ from typing import Any, Self, cast
12
12
 
13
13
  import httpx
14
14
 
@@ -245,12 +245,12 @@ class BiliImage:
245
245
 
246
246
  @dataclass(kw_only=True)
247
247
  class BiliDynamic:
248
- title: str = ""
249
- content: str = ""
248
+ title: str | None = ""
249
+ content: str | None = ""
250
250
  images: list[BiliImage] | None = None
251
251
 
252
252
  @classmethod
253
- def parse(cls, data: dict) -> "BiliDynamic":
253
+ def parse(cls, data: dict) -> Self:
254
254
  module_dynamic: dict = data["item"]["modules"]["module_dynamic"]
255
255
  major: dict | None = module_dynamic.get("major", None)
256
256
  if not major:
@@ -259,9 +259,9 @@ class BiliDynamic:
259
259
  return cls._parse_major(module_dynamic, major)
260
260
 
261
261
  @classmethod
262
- def _parse_major(cls, module_dynamic: dict, major: dict) -> "BiliDynamic":
262
+ def _parse_major(cls, module_dynamic: dict, major: dict) -> Self:
263
263
  major_type = major["type"]
264
- major_parsers: dict[MajorType, Callable[[dict, dict], BiliDynamic]] = {
264
+ major_parsers: dict[MajorType, Callable[[dict, dict], Self]] = {
265
265
  MajorType.MAJOR_TYPE_MEDIALIST: cls._parse_medialist,
266
266
  MajorType.MAJOR_TYPE_UPOWER_COMMON: cls._parse_upower_common,
267
267
  MajorType.MAJOR_TYPE_COMMON: cls._parse_common,
@@ -279,30 +279,30 @@ class BiliDynamic:
279
279
  return major_parser(module_dynamic, major)
280
280
 
281
281
  @classmethod
282
- def _parse_pgc_union(cls, _: dict, major: dict) -> "BiliDynamic":
282
+ def _parse_pgc_union(cls, _: dict, major: dict) -> Self:
283
283
  pgc = major["pgc"]
284
284
  return cls(title=pgc["title"], images=[BiliImage(url=pgc["cover"])])
285
285
 
286
286
  @classmethod
287
- def _parse_forward(cls, module_dynamic: dict) -> "BiliDynamic":
287
+ def _parse_forward(cls, module_dynamic: dict) -> Self:
288
288
  return cls(content=cls._get_desc_text(module_dynamic))
289
289
 
290
290
  @classmethod
291
- def _parse_av(cls, module_dynamic: dict, major: dict):
291
+ def _parse_av(cls, module_dynamic: dict, major: dict) -> Self:
292
292
  if content := cls._get_desc_text(module_dynamic):
293
293
  return cls(content=content)
294
294
  archive = major["archive"]
295
295
  return cls(title=archive["title"], content=archive["desc"], images=cls._get_major_cover(archive))
296
296
 
297
297
  @classmethod
298
- def _parse_music(cls, module_dynamic: dict, major: dict):
298
+ def _parse_music(cls, module_dynamic: dict, major: dict) -> Self:
299
299
  if content := cls._get_desc_text(module_dynamic):
300
300
  return cls(content=content)
301
301
  music = major["music"]
302
302
  return cls(title=music["title"], images=cls._get_major_cover(music))
303
303
 
304
304
  @classmethod
305
- def _parse_opus(cls, _: dict, major: dict) -> "BiliDynamic":
305
+ def _parse_opus(cls, _: dict, major: dict) -> Self:
306
306
  opus = major["opus"]
307
307
  images = None
308
308
  if pics := opus["pics"]:
@@ -312,14 +312,14 @@ class BiliDynamic:
312
312
  return cls(title=opus["title"], content=opus["summary"]["text"], images=images)
313
313
 
314
314
  @classmethod
315
- def _parse_common(cls, module_dynamic: dict, major: dict):
315
+ def _parse_common(cls, module_dynamic: dict, major: dict) -> Self:
316
316
  if content := cls._get_desc_text(module_dynamic):
317
317
  return cls(content=content)
318
318
  common = major["common"]
319
319
  return cls(title=common["title"], content=common["desc"], images=cls._get_major_cover(common))
320
320
 
321
321
  @classmethod
322
- def _parse_live(cls, module_dynamic: dict, major: dict):
322
+ def _parse_live(cls, module_dynamic: dict, major: dict) -> Self:
323
323
  if content := cls._get_desc_text(module_dynamic):
324
324
  return cls(content=content)
325
325
  live = major["live"]
@@ -327,14 +327,14 @@ class BiliDynamic:
327
327
  return cls(title=live["title"], content=content, images=cls._get_major_cover(live))
328
328
 
329
329
  @classmethod
330
- def _parse_medialist(cls, module_dynamic: dict, major: dict):
330
+ def _parse_medialist(cls, module_dynamic: dict, major: dict) -> Self:
331
331
  if content := cls._get_desc_text(module_dynamic):
332
332
  return cls(content=content)
333
333
  medialist = major["medialist"]
334
334
  return cls(title=medialist["title"], content=medialist["sub_title"], images=cls._get_major_cover(medialist))
335
335
 
336
336
  @classmethod
337
- def _parse_courses(cls, module_dynamic: dict, major: dict):
337
+ def _parse_courses(cls, module_dynamic: dict, major: dict) -> Self:
338
338
  if content := cls._get_desc_text(module_dynamic):
339
339
  return cls(content=content)
340
340
  courses = major["courses"]
@@ -342,14 +342,14 @@ class BiliDynamic:
342
342
  return cls(title=courses["title"], content=content, images=cls._get_major_cover(courses))
343
343
 
344
344
  @classmethod
345
- def _parse_ugc_season(cls, module_dynamic: dict, major: dict):
345
+ def _parse_ugc_season(cls, module_dynamic: dict, major: dict) -> Self:
346
346
  if content := cls._get_desc_text(module_dynamic):
347
347
  return cls(content=content)
348
348
  ugc_season = major["ugc_season"]
349
349
  return cls(title=ugc_season["title"], content=ugc_season["desc"], images=cls._get_major_cover(ugc_season))
350
350
 
351
351
  @classmethod
352
- def _parse_upower_common(cls, module_dynamic: dict, major: dict):
352
+ def _parse_upower_common(cls, module_dynamic: dict, major: dict) -> Self:
353
353
  if content := cls._get_desc_text(module_dynamic):
354
354
  return cls(content=content)
355
355
  upower_common = major["upower_common"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.1.6
3
+ Version: 2.1.7
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
@@ -348,6 +348,7 @@ class TestPlatformUrlMatching(unittest.TestCase):
348
348
  Platform.THREADS: [
349
349
  "https://www.threads.com/@zaborona.magazine/post/DBuqMBwMfxW",
350
350
  "https://www.threads.com/@user_name/post/DBuqMBwMfxW",
351
+ "https://www.threads.com/share/Dq6fjYWK-/",
351
352
  ],
352
353
  Platform.TIEBA: [
353
354
  "https://tieba.baidu.com/p/9939510114",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes