parsehub 2.3.10__tar.gz → 2.3.12__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.10/src/parsehub.egg-info → parsehub-2.3.12}/PKG-INFO +1 -1
  2. {parsehub-2.3.10 → parsehub-2.3.12}/pyproject.toml +1 -1
  3. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/errors.py +19 -4
  4. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/base/base.py +25 -4
  5. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/bilibili.py +2 -2
  6. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/instagram.py +39 -19
  7. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/kuaishou.py +2 -2
  8. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/twitter.py +2 -20
  9. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/zhihu.py +1 -1
  10. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/instagram.py +54 -1
  11. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/zhihu.py +1 -1
  12. {parsehub-2.3.10 → parsehub-2.3.12/src/parsehub.egg-info}/PKG-INFO +1 -1
  13. {parsehub-2.3.10 → parsehub-2.3.12}/LICENSE +0 -0
  14. {parsehub-2.3.10 → parsehub-2.3.12}/README.md +0 -0
  15. {parsehub-2.3.10 → parsehub-2.3.12}/setup.cfg +0 -0
  16. {parsehub-2.3.10 → parsehub-2.3.12}/src/__init__.py +0 -0
  17. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/__init__.py +0 -0
  18. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/cli.py +0 -0
  19. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/cli_config.py +0 -0
  20. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/config/__init__.py +0 -0
  21. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/config/config.py +0 -0
  22. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/__init__.py +0 -0
  23. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/base/__init__.py +0 -0
  24. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/base/ytdlp.py +0 -0
  25. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/__init__.py +0 -0
  26. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/bangumi.py +0 -0
  27. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/coolapk.py +0 -0
  28. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/douban.py +0 -0
  29. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/douyin.py +0 -0
  30. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/facebook.py +0 -0
  31. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/medium.py +0 -0
  32. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/pipix.py +0 -0
  33. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/snapchat.py +0 -0
  34. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/threads.py +0 -0
  35. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/tieba.py +0 -0
  36. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/tiktok.py +0 -0
  37. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/toutiao.py +0 -0
  38. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/weibo.py +0 -0
  39. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/weixin.py +0 -0
  40. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/wxchannels.py +0 -0
  41. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/xhs.py +0 -0
  42. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
  43. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/youtube.py +0 -0
  44. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/parsers/parser/zuiyou.py +0 -0
  45. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/__init__.py +0 -0
  46. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/bangumi.py +0 -0
  47. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/bilibili.py +0 -0
  48. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/coolapk.py +0 -0
  49. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/douban.py +0 -0
  50. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/douyin.py +0 -0
  51. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/kuaishou.py +0 -0
  52. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/medium.py +0 -0
  53. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/pipix.py +0 -0
  54. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/threads.py +0 -0
  55. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/tieba.py +0 -0
  56. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/tiktok.py +0 -0
  57. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/toutiao.py +0 -0
  58. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/twitter.py +0 -0
  59. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/weibo.py +0 -0
  60. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/weixin.py +0 -0
  61. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/wxchannels.py +0 -0
  62. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/xhs.py +0 -0
  63. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/xiaoheihe.py +0 -0
  64. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/provider_api/zuiyou.py +0 -0
  65. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/types/__init__.py +0 -0
  66. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/types/callback.py +0 -0
  67. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/types/media_file.py +0 -0
  68. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/types/media_ref.py +0 -0
  69. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/types/platform.py +0 -0
  70. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/types/post.py +0 -0
  71. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/types/result.py +0 -0
  72. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/utils/downloader.py +0 -0
  73. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/utils/helpers.py +0 -0
  74. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub/utils/media_info.py +0 -0
  75. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub.egg-info/SOURCES.txt +0 -0
  76. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub.egg-info/dependency_links.txt +0 -0
  77. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub.egg-info/entry_points.txt +0 -0
  78. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub.egg-info/requires.txt +0 -0
  79. {parsehub-2.3.10 → parsehub-2.3.12}/src/parsehub.egg-info/top_level.txt +0 -0
  80. {parsehub-2.3.10 → parsehub-2.3.12}/test/test_cli.py +0 -0
  81. {parsehub-2.3.10 → parsehub-2.3.12}/test/test_cli_config.py +0 -0
  82. {parsehub-2.3.10 → parsehub-2.3.12}/test/test_core_offline.py +0 -0
  83. {parsehub-2.3.10 → parsehub-2.3.12}/test/test_downloader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.3.10
3
+ Version: 2.3.12
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.10"
3
+ version = "2.3.12"
4
4
  description = "轻量、异步、开箱即用的社交媒体聚合解析库"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12.0"
@@ -1,11 +1,24 @@
1
1
  from dataclasses import dataclass
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from .utils.helpers import SecretCookie
2
6
 
3
7
 
4
8
  class ParseHubError(Exception):
5
9
  MSG = __doc__
10
+ cookie: "SecretCookie | None" = None
6
11
 
7
- def __init__(self, msg: str = ""):
12
+ def __init__(self, msg: str = "", *, cookie: "SecretCookie | None" = None):
8
13
  super().__init__(self.MSG if not msg else f"{self.MSG}: {msg}")
14
+ self.cookie = cookie
15
+
16
+ @property
17
+ def message(self) -> str:
18
+ return super().__str__()
19
+
20
+ def __str__(self) -> str:
21
+ return f"{self.message}\n\n使用的 Cookie: {self.cookie}" if self.cookie else self.message
9
22
 
10
23
 
11
24
  class ParseError(ParseHubError):
@@ -34,10 +47,12 @@ def format_attempts(attempts: list[FallbackAttempt], indent: int = 0) -> str:
34
47
  if isinstance(attempt.error, FallbackError):
35
48
  lines.append(f"{prefix}:")
36
49
  lines.append(format_attempts(attempt.error.attempts, indent + 1))
50
+ elif isinstance(attempt.error, ParseHubError):
51
+ lines.append(f"{prefix}: {attempt.error.message}")
52
+ if attempt.error.cookie:
53
+ lines.append(" " * (indent + 1) + f"使用的 Cookie: {attempt.error.cookie}")
37
54
  else:
38
- error = attempt.error
39
- text = str(error) if isinstance(error, ParseHubError) else f"{type(error).__name__}: {error}"
40
- lines.append(f"{prefix}: {text}")
55
+ lines.append(f"{prefix}: {type(attempt.error).__name__}: {attempt.error}")
41
56
  return "\n".join(lines)
42
57
 
43
58
 
@@ -11,6 +11,7 @@ import httpx
11
11
  from loguru import logger
12
12
 
13
13
  from ... import parsers
14
+ from ...errors import ParseHubError
14
15
  from ...types import (
15
16
  AnyParseResult,
16
17
  ContentUnavailableError,
@@ -32,6 +33,16 @@ class FallbackStage(Generic[T]): # noqa: UP046
32
33
  cookie: bool = False
33
34
 
34
35
 
36
+ def _attach_cookie(e: Exception, cookie: SecretCookie) -> Exception:
37
+ if isinstance(e, (ContentUnavailableError, FallbackError)):
38
+ return e
39
+ if not isinstance(e, ParseHubError):
40
+ return ParseError(f"{type(e).__name__}: {e}" if str(e) else type(e).__name__, cookie=cookie)
41
+ if e.cookie is None:
42
+ e.cookie = cookie
43
+ return e
44
+
45
+
35
46
  def is_content_unavailable(e: Exception) -> bool:
36
47
  return isinstance(e, ContentUnavailableError)
37
48
 
@@ -89,7 +100,13 @@ class BaseParser(ABC):
89
100
  """
90
101
  self._fallback_attempts = []
91
102
  raw_url = await self.get_raw_url(url, clean_all=False)
92
- result = await self._do_parse(raw_url)
103
+ try:
104
+ result = await self._do_parse(raw_url)
105
+ except Exception as e:
106
+ err = _attach_cookie(e, self.cookie)
107
+ if err is e:
108
+ raise
109
+ raise err from e
93
110
  result.fallback_errors = list(self._fallback_attempts)
94
111
  result.platform = self.__platform__
95
112
  raw_url_clean = self._clean_params(raw_url, self.__after_clean_parameters__)
@@ -107,9 +124,13 @@ class BaseParser(ABC):
107
124
  result = await stage.func()
108
125
  except Exception as e:
109
126
  logger.opt(exception=e).debug(f"{self.__platform__} {stage.name} 解析失败")
110
- if stop(e):
111
- raise
112
- attempts.append(FallbackAttempt(stage.name, e, stage.cookie))
127
+ should_stop = stop(e)
128
+ err = _attach_cookie(e, self.cookie if stage.cookie else SecretCookie())
129
+ if should_stop:
130
+ if err is e:
131
+ raise
132
+ raise err from e
133
+ attempts.append(FallbackAttempt(stage.name, err, stage.cookie))
113
134
  else:
114
135
  self._fallback_attempts.extend(attempts)
115
136
  return result
@@ -54,7 +54,7 @@ class BiliParse(BaseParser):
54
54
  else:
55
55
  return await self._fallback(
56
56
  FallbackStage("API", lambda: self.bili_api_parse(raw_url)),
57
- FallbackStage("yt-dlp", lambda: self.ytp_parse(raw_url)),
57
+ FallbackStage("yt-dlp", lambda: self.ytp_parse(raw_url), cookie=bool(self.cookie)),
58
58
  stop=lambda e: False,
59
59
  )
60
60
 
@@ -97,7 +97,7 @@ class BiliParse(BaseParser):
97
97
  raise
98
98
  except Exception as e:
99
99
  if "风控" in str(e):
100
- raise ParseError(f"账号风控\n使用的cookie: {self.cookie}") from e
100
+ raise ParseError("账号风控") from e
101
101
  raise ParseError(str(e)) from e
102
102
  else:
103
103
  return cast(BiliDynamic, dynamic_info)
@@ -1,9 +1,24 @@
1
1
  import re
2
2
 
3
- from ...provider_api.instagram import InstagramAPI, InstagramAPIError, InstagramMediaType, InstagramPost
4
- from ...types import ImageParseResult, ImageRef, MultimediaParseResult, ParseError, Platform, VideoParseResult, VideoRef
3
+ from ...provider_api.instagram import (
4
+ InstagramAPI,
5
+ InstagramAPIError,
6
+ InstagramMediaType,
7
+ InstagramPost,
8
+ InstagramRestrictedError,
9
+ )
10
+ from ...types import (
11
+ ContentUnavailableError,
12
+ ImageParseResult,
13
+ ImageRef,
14
+ MultimediaParseResult,
15
+ ParseError,
16
+ Platform,
17
+ VideoParseResult,
18
+ VideoRef,
19
+ )
5
20
  from ...utils.helpers import SecretCookie
6
- from ..base.base import BaseParser
21
+ from ..base.base import BaseParser, FallbackStage
7
22
 
8
23
 
9
24
  class InstagramParser(BaseParser):
@@ -17,7 +32,7 @@ class InstagramParser(BaseParser):
17
32
  if not shortcode:
18
33
  raise ValueError("Instagram帖子链接无效")
19
34
 
20
- post = await self._parse(raw_url, shortcode)
35
+ post = await self._parse(shortcode)
21
36
 
22
37
  width, height = post.width, post.height
23
38
 
@@ -49,25 +64,30 @@ class InstagramParser(BaseParser):
49
64
  case _:
50
65
  raise ParseError("不支持的类型")
51
66
 
52
- async def _parse(self, url: str, shortcode: str, cookie: SecretCookie | None = None) -> InstagramPost:
67
+ async def _parse(self, shortcode: str) -> InstagramPost:
68
+ stages: list[FallbackStage[InstagramPost]] = [FallbackStage("匿名", lambda: self._fetch(shortcode))]
69
+ if self.cookie.get_value():
70
+ stages.append(
71
+ FallbackStage("cookie", lambda: self._fetch(shortcode, self.cookie), cookie=True)
72
+ )
73
+ return await self._fallback(
74
+ *stages, stop=lambda e: not isinstance(e.__cause__, InstagramRestrictedError)
75
+ )
76
+
77
+ async def _fetch(self, shortcode: str, cookie: SecretCookie | None = None) -> InstagramPost:
78
+ api = InstagramAPI(proxy=self.proxy, cookie=cookie.get_value() if cookie else None, timeout=30)
53
79
  try:
54
- api = InstagramAPI(proxy=self.proxy, cookie=cookie.get_value() if cookie else None, timeout=30)
55
80
  return await api.get_post(shortcode)
56
- except InstagramAPIError as e:
57
- match str(e):
58
- case "Fetching Post metadata failed.":
59
- if self.cookie and cookie is None:
60
- return await self._parse(url, shortcode, self.cookie)
61
- else:
62
- raise ParseError("受限视频无法解析: 你必须年满 18 周岁才能观看这个视频") from e
63
- case _:
64
- raise ParseError(f"无法获取帖子内容: {e}") from e
81
+ except ContentUnavailableError:
82
+ raise
65
83
  except Exception as e:
66
- if cookie:
67
- text = f"Instagram 账号可能已被封禁\n\n使用的Cookie: {cookie}"
68
- else:
84
+ if isinstance(e, InstagramRestrictedError):
69
85
  text = str(e)
70
- raise ParseError(f"无法获取帖子内容(可能为私人内容): {text}") from e
86
+ elif isinstance(e, InstagramAPIError):
87
+ text = f"无法获取帖子内容: {e}"
88
+ else:
89
+ text = f"无法获取帖子内容(可能为私人内容): {e}"
90
+ raise ParseError(text) from e
71
91
 
72
92
  @staticmethod
73
93
  def get_short_code(url: str) -> str | None:
@@ -79,8 +79,8 @@ class KuaiShouParser(BaseParser):
79
79
  )
80
80
 
81
81
  return await self._fallback(
82
- FallbackStage("HTML", parse_html),
83
- FallbackStage("API", parse_api),
82
+ FallbackStage("HTML", parse_html, cookie=bool(self.cookie)),
83
+ FallbackStage("API", parse_api, cookie=bool(self.cookie)),
84
84
  )
85
85
  else:
86
86
  cover = ksp.get_cover_photo_url()
@@ -13,12 +13,11 @@ from ...types import (
13
13
  AnyMediaRef,
14
14
  ImageRef,
15
15
  MultimediaParseResult,
16
- ParseError,
17
16
  Platform,
18
17
  RichTextParseResult,
19
18
  VideoRef,
20
19
  )
21
- from ..base.base import BaseParser, FallbackStage, is_content_unavailable
20
+ from ..base.base import BaseParser
22
21
 
23
22
 
24
23
  class TwitterParser(BaseParser):
@@ -35,24 +34,7 @@ class TwitterParser(BaseParser):
35
34
  return str(urlunparse(urlparse(url)._replace(netloc="x.com")))
36
35
 
37
36
  async def _parse(self, url: str) -> TwitterTweet:
38
- stages: list[FallbackStage[TwitterTweet]] = [
39
- FallbackStage("匿名", lambda: Twitter(self.proxy, cookie=None).fetch_tweet(url))
40
- ]
41
- if cookie := self.cookie.get_value():
42
-
43
- async def fetch_cookie() -> TwitterTweet:
44
- try:
45
- return await Twitter(self.proxy, cookie=cookie).fetch_tweet(url)
46
- except Exception as e2:
47
- raise ParseError(f"{e2}\n\n使用的 Cookie: {self.cookie}") from e2
48
-
49
- stages.append(FallbackStage("cookie", fetch_cookie, cookie=True))
50
-
51
- return await self._fallback(
52
- *stages,
53
- stop=lambda e: is_content_unavailable(e)
54
- or not any(s in str(e) for s in ("error -2", "error -3")),
55
- )
37
+ return await Twitter(self.proxy, cookie=self.cookie.get_value()).fetch_tweet(url)
56
38
 
57
39
  @staticmethod
58
40
  async def media_parse(tweet: TwitterTweet) -> MultimediaParseResult | RichTextParseResult:
@@ -14,7 +14,7 @@ from ..base.base import BaseParser
14
14
  class ZhihuParser(BaseParser):
15
15
  __platform__ = Platform.ZHIHU
16
16
  __supported_type__ = ["问答", "专栏", "圈子", "日报"]
17
- __match__ = r"^(http(s)?://)?(www|zhuanlan|daily).zhihu.com/(pin|question|p|story)/.*"
17
+ __match__ = r"^(http(s)?://)?(www|zhuanlan|daily).zhihu.com/(pin|question|p|story|appview/p)/.*"
18
18
 
19
19
  async def _do_parse(
20
20
  self, raw_url: str
@@ -8,11 +8,17 @@ from typing import Any, cast
8
8
 
9
9
  import httpx
10
10
 
11
+ from ..types import ContentUnavailableError
12
+
11
13
 
12
14
  class InstagramAPIError(RuntimeError):
13
15
  """Instagram 接口请求或响应解析失败。"""
14
16
 
15
17
 
18
+ class InstagramRestrictedError(InstagramAPIError):
19
+ """内容受限, 需要登录 Cookie。"""
20
+
21
+
16
22
  class InstagramMediaType(StrEnum):
17
23
  IMAGE = "GraphImage"
18
24
  VIDEO = "GraphVideo"
@@ -129,6 +135,8 @@ class InstagramPost:
129
135
  class InstagramAPI:
130
136
  GRAPHQL_URL = "https://www.instagram.com/graphql/query"
131
137
  INSTAGRAM_URL = "https://www.instagram.com/"
138
+ RULING_URL = "https://www.instagram.com/api/v1/web/get_ruling_for_content/"
139
+ APP_ID = "936619743392459"
132
140
  SHORTCODE_DOC_ID = "27128499623469141"
133
141
 
134
142
  DEFAULT_COOKIES = {
@@ -179,11 +187,55 @@ class InstagramAPI:
179
187
  },
180
188
  )
181
189
 
190
+ if payload.get("data") is None and payload.get("errors"):
191
+ await self._check_ruling(shortcode)
192
+
182
193
  media = self._extract_shortcode_media(payload)
183
194
  if media is None:
184
- raise InstagramAPIError("Fetching Post metadata failed.")
195
+ raise InstagramRestrictedError("受限视频无法解析: 你必须年满 18 周岁才能观看这个视频")
185
196
  return media
186
197
 
198
+ async def _check_ruling(self, shortcode: str) -> None:
199
+ """查询内容裁决接口, 在 GraphQL 报错时解释原因。失败时静默返回。"""
200
+ try:
201
+ async with self._new_client() as client:
202
+ try:
203
+ await self._ensure_csrf_token(client)
204
+ except InstagramAPIError:
205
+ return
206
+ response = await client.get(
207
+ self.RULING_URL,
208
+ params={
209
+ "content_type": "MEDIA",
210
+ "target_id": str(self.shortcode_to_media_id(shortcode)),
211
+ },
212
+ headers={
213
+ "X-IG-App-ID": self.APP_ID,
214
+ "Accept-Language": "zh-CN,zh;q=0.9",
215
+ },
216
+ )
217
+ body = response.json()
218
+ except (httpx.HTTPError, ValueError):
219
+ return
220
+ if not isinstance(body, dict):
221
+ return
222
+
223
+ if body.get("message") == "Media cannot be found":
224
+ raise ContentUnavailableError("帖子不存在或已被删除")
225
+ if title := body.get("title"):
226
+ description = body.get("description")
227
+ raise InstagramRestrictedError(f"{title}: {description}" if description else title)
228
+
229
+ @staticmethod
230
+ def shortcode_to_media_id(shortcode: str) -> int:
231
+ if len(shortcode) > 28:
232
+ shortcode = shortcode[:-28]
233
+ alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
234
+ media_id = 0
235
+ for char in shortcode:
236
+ media_id = media_id * 64 + alphabet.index(char)
237
+ return media_id
238
+
187
239
  async def _post_graphql(self, *, doc_id: str, variables: dict[str, Any]) -> dict[str, Any]:
188
240
  async with self._new_client() as client:
189
241
  await self._ensure_csrf_token(client)
@@ -376,5 +428,6 @@ __all__ = [
376
428
  "InstagramAPIError",
377
429
  "InstagramMediaType",
378
430
  "InstagramPost",
431
+ "InstagramRestrictedError",
379
432
  "InstagramSidecarNode",
380
433
  ]
@@ -211,7 +211,7 @@ class ZhihuAPI:
211
211
  async def parse(self, raw_url: str) -> ZhihuQA | ZhihuZhuanLan | ZhihuPin:
212
212
  if "/question" in raw_url:
213
213
  return await self.parse_qa(raw_url)
214
- if "zhuanlan." in raw_url:
214
+ if "zhuanlan." in raw_url or "/appview/p/" in raw_url:
215
215
  return await self.parse_zl(raw_url)
216
216
  if "/pin/" in raw_url:
217
217
  return await self.parse_pin(raw_url)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parsehub
3
- Version: 2.3.10
3
+ Version: 2.3.12
4
4
  Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
5
5
  Author-email: 梓澪 <zilingmio@gmail.com>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes