parsehub 2.3.10__tar.gz → 2.3.11__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.10/src/parsehub.egg-info → parsehub-2.3.11}/PKG-INFO +1 -1
- {parsehub-2.3.10 → parsehub-2.3.11}/pyproject.toml +1 -1
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/errors.py +19 -4
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/base/base.py +25 -4
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/bilibili.py +2 -2
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/instagram.py +39 -19
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/kuaishou.py +2 -2
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/twitter.py +2 -20
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/instagram.py +54 -1
- {parsehub-2.3.10 → parsehub-2.3.11/src/parsehub.egg-info}/PKG-INFO +1 -1
- {parsehub-2.3.10 → parsehub-2.3.11}/LICENSE +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/README.md +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/setup.cfg +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/__init__.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/__init__.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/cli.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/cli_config.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/config/__init__.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/config/config.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/__init__.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/base/__init__.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/base/ytdlp.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/__init__.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/bangumi.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/coolapk.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/douban.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/douyin.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/facebook.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/medium.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/pipix.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/snapchat.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/threads.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/tieba.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/tiktok.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/toutiao.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/weibo.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/weixin.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/wxchannels.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/xhs.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/youtube.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/zhihu.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/parsers/parser/zuiyou.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/__init__.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/bangumi.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/bilibili.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/coolapk.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/douban.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/douyin.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/kuaishou.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/medium.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/pipix.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/threads.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/tieba.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/tiktok.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/toutiao.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/twitter.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/weibo.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/weixin.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/wxchannels.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/xhs.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/xiaoheihe.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/zhihu.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/provider_api/zuiyou.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/types/__init__.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/types/callback.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/types/media_file.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/types/media_ref.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/types/platform.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/types/post.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/types/result.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/utils/downloader.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/utils/helpers.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub/utils/media_info.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub.egg-info/SOURCES.txt +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub.egg-info/dependency_links.txt +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub.egg-info/entry_points.txt +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub.egg-info/requires.txt +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/src/parsehub.egg-info/top_level.txt +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/test/test_cli.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/test/test_cli_config.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/test/test_core_offline.py +0 -0
- {parsehub-2.3.10 → parsehub-2.3.11}/test/test_downloader.py +0 -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
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
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
|
|
4
|
-
|
|
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(
|
|
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,
|
|
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
|
|
57
|
-
|
|
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
|
|
67
|
-
text = f"Instagram 账号可能已被封禁\n\n使用的Cookie: {cookie}"
|
|
68
|
-
else:
|
|
84
|
+
if isinstance(e, InstagramRestrictedError):
|
|
69
85
|
text = str(e)
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
|
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
|
]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|