nonebot-plugin-bililive 2.0.5__tar.gz → 2.0.6__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 (53) hide show
  1. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/PKG-INFO +1 -1
  2. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/web.py +11 -8
  3. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/pusher/dynamic_pusher.py +69 -42
  4. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/utils/__init__.py +5 -1
  5. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/utils/browser.py +27 -0
  6. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/version.py +1 -1
  7. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/pyproject.toml +1 -1
  8. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/tests/test_maintenance.py +26 -1
  9. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/LICENSE +0 -0
  10. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/README.md +0 -0
  11. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/__init__.py +0 -0
  12. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/__main__.py +0 -0
  13. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/cli/__init__.py +0 -0
  14. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/cli/bot.py +0 -0
  15. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/cli/utils.py +0 -0
  16. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/compat.py +0 -0
  17. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/config.py +0 -0
  18. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/database/__init__.py +0 -0
  19. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/database/db.py +0 -0
  20. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/database/models.py +0 -0
  21. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/libs/__init__.py +0 -0
  22. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/__init__.py +0 -0
  23. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/card.py +0 -0
  24. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/desc.py +0 -0
  25. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/display.py +0 -0
  26. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/user_profile.py +0 -0
  27. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/__init__.py +0 -0
  28. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/at/__init__.py +0 -0
  29. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/at/at_off.py +0 -0
  30. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/at/at_on.py +0 -0
  31. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/auto_agree.py +0 -0
  32. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/auto_delete.py +0 -0
  33. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/dynamic/__init__.py +0 -0
  34. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/dynamic/dynamic_off.py +0 -0
  35. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/dynamic/dynamic_on.py +0 -0
  36. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/help.py +0 -0
  37. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/__init__.py +0 -0
  38. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/live_now.py +0 -0
  39. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/live_off.py +0 -0
  40. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/live_on.py +0 -0
  41. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/permission/__init__.py +0 -0
  42. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/permission/permission_off.py +0 -0
  43. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/permission/permission_on.py +0 -0
  44. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/pusher/__init__.py +0 -0
  45. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/pusher/live_pusher.py +0 -0
  46. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/__init__.py +0 -0
  47. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/add_sub.py +0 -0
  48. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/delete_sub.py +0 -0
  49. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/sub_list.py +0 -0
  50. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/utils/captcha_solver.py +0 -0
  51. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/utils/fonts_provider.py +0 -0
  52. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/bililive/utils/mobile.js +0 -0
  53. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.6}/nonebot_plugin_bililive/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-bililive
3
- Version: 2.0.5
3
+ Version: 2.0.6
4
4
  Summary: Push bilibili dynamics and live notifications to QQ with NoneBot2.
5
5
  Keywords: nonebot,nonebot2,nonebot-plugin,qqbot,bilibili
6
6
  Author-Email: SK-415 <2967923486@qq.com>
@@ -47,6 +47,16 @@ def parse_web_dynamic_items(payload: dict) -> list[WebDynamicItem]:
47
47
  return parsed_items
48
48
 
49
49
 
50
+ def parse_web_dynamic_payload(payload: dict) -> list[WebDynamicItem]:
51
+ if payload.get("code") != 0:
52
+ raise WebDynamicError(
53
+ payload.get("code"),
54
+ payload.get("message") or "unknown error",
55
+ payload.get("data"),
56
+ )
57
+ return parse_web_dynamic_items(payload)
58
+
59
+
50
60
  async def get_user_dynamics_web(
51
61
  uid: int,
52
62
  cookies: dict[str, str],
@@ -69,11 +79,4 @@ async def get_user_dynamics_web(
69
79
  follow_redirects=True,
70
80
  ) as client:
71
81
  response = await client.get(WEB_DYNAMIC_URL, params={"host_mid": uid})
72
- payload = response.json()
73
- if payload.get("code") != 0:
74
- raise WebDynamicError(
75
- payload.get("code"),
76
- payload.get("message") or "unknown error",
77
- payload.get("data"),
78
- )
79
- return parse_web_dynamic_items(payload)
82
+ return parse_web_dynamic_payload(response.json())
@@ -19,15 +19,22 @@ from nonebot.log import logger
19
19
  from ...config import plugin_config
20
20
  from ...database import DB as db
21
21
  from ...database import dynamic_offset as offset
22
- from ...libs.dynamic.web import WebDynamicError, get_user_dynamics_web
22
+ from ...libs.dynamic.web import (
23
+ WebDynamicError,
24
+ get_user_dynamics_web,
25
+ parse_web_dynamic_payload,
26
+ )
23
27
  from ...utils import (
24
28
  get_bilibili_cookies,
25
29
  get_dynamic_screenshot,
30
+ get_user_dynamics_payload_in_browser,
26
31
  safe_send,
27
32
  scheduler,
28
33
  )
29
34
 
30
- RISK_CONTROL_RETRY_SECONDS = 3600
35
+ GRPC_RISK_CONTROL_RETRY_SECONDS = 3600
36
+ WEB_REQUEST_BANNED_RETRY_SECONDS = 300
37
+ WEB_REQUEST_ERROR_RETRY_SECONDS = 600
31
38
  dynamic_risk_control_until = {}
32
39
  dynamic_web_fallback_until = {}
33
40
  WEB_SKIP_DYNAMIC_TYPES = {
@@ -44,6 +51,7 @@ WEB_DYNAMIC_TYPE_MESSAGES = {
44
51
  "DYNAMIC_TYPE_ARTICLE": "发布了新专栏",
45
52
  "DYNAMIC_TYPE_MUSIC": "发布了新音频",
46
53
  }
54
+ DYNAMIC_FETCH_CONCURRENCY = 4
47
55
 
48
56
 
49
57
  async def throttle_dynamic_loop():
@@ -95,21 +103,32 @@ def should_skip_dynamic(dynamic_type, use_web_fallback: bool) -> bool:
95
103
 
96
104
 
97
105
  async def get_user_dynamics_with_web_fallback(uid: int) -> tuple[list, bool]:
106
+ async def fetch_web_dynamics() -> list:
107
+ try:
108
+ payload = await get_user_dynamics_payload_in_browser(uid)
109
+ return parse_web_dynamic_payload(payload)
110
+ except WebDynamicError as browser_error:
111
+ logger.debug(
112
+ f"浏览器上下文动态接口获取失败,尝试直连 Web API:{uid} "
113
+ f"{browser_error.code} {browser_error.msg}"
114
+ )
115
+
116
+ cookies = await get_bilibili_cookies()
117
+ if not cookies:
118
+ raise WebDynamicError(-1, "browser cookies unavailable")
119
+ return await get_user_dynamics_web(
120
+ uid,
121
+ cookies,
122
+ proxy=plugin_config.bililive_proxy,
123
+ user_agent=plugin_config.bililive_browser_ua or None,
124
+ timeout=plugin_config.bililive_dynamic_timeout,
125
+ )
126
+
98
127
  fallback_until = dynamic_web_fallback_until.get(uid)
99
128
  if fallback_until is not None:
100
129
  if fallback_until > monotonic():
101
130
  logger.debug(f"动态 gRPC 接口仍在风控,继续使用 Web 接口:{uid}")
102
- cookies = await get_bilibili_cookies()
103
- if not cookies:
104
- raise WebDynamicError(-1, "browser cookies unavailable")
105
- dynamics = await get_user_dynamics_web(
106
- uid,
107
- cookies,
108
- proxy=plugin_config.bililive_proxy,
109
- user_agent=plugin_config.bililive_browser_ua or None,
110
- timeout=plugin_config.bililive_dynamic_timeout,
111
- )
112
- return dynamics, True
131
+ return await fetch_web_dynamics(), True
113
132
  del dynamic_web_fallback_until[uid]
114
133
 
115
134
  try:
@@ -129,36 +148,14 @@ async def get_user_dynamics_with_web_fallback(uid: int) -> tuple[list, bool]:
129
148
  f"动态 gRPC 接口触发风控,切换 Web 接口:{uid} "
130
149
  f"{e.code} {e.msg}"
131
150
  )
132
- dynamic_web_fallback_until[uid] = monotonic() + RISK_CONTROL_RETRY_SECONDS
133
- cookies = await get_bilibili_cookies()
134
- if not cookies:
135
- raise WebDynamicError(-1, "browser cookies unavailable")
136
- dynamics = await get_user_dynamics_web(
137
- uid,
138
- cookies,
139
- proxy=plugin_config.bililive_proxy,
140
- user_agent=plugin_config.bililive_browser_ua or None,
141
- timeout=plugin_config.bililive_dynamic_timeout,
142
- )
143
- return dynamics, True
144
-
151
+ dynamic_web_fallback_until[uid] = monotonic() + GRPC_RISK_CONTROL_RETRY_SECONDS
152
+ return await fetch_web_dynamics(), True
145
153
 
146
- async def dy_sched():
147
- """动态推送"""
148
- if not await db.wait_until_ready():
149
- logger.debug("数据库尚未初始化完成,跳过本轮动态推送")
150
- await throttle_dynamic_loop()
151
- return
152
154
 
153
- uid = await db.next_uid("dynamic")
154
- if not uid:
155
- # 没有订阅先暂停一秒再跳过,不然会导致 CPU 占用过高
156
- await throttle_dynamic_loop()
157
- return
155
+ async def process_dynamic_uid(uid: int):
158
156
  user = await db.get_user(uid=uid)
159
157
  if user is None:
160
158
  logger.warning(f"动态推送跳过异常订阅 UID:{uid}")
161
- await throttle_dynamic_loop()
162
159
  return
163
160
  name = user.name
164
161
 
@@ -186,16 +183,19 @@ async def dy_sched():
186
183
  return
187
184
  except GrpcError as e:
188
185
  logger.error(f"爬取动态失败:{e.code} {e.msg}")
189
- await throttle_dynamic_loop()
190
186
  return
191
187
  except WebDynamicError as e:
192
- dynamic_risk_control_until[uid] = monotonic() + RISK_CONTROL_RETRY_SECONDS
193
- retry_minutes = RISK_CONTROL_RETRY_SECONDS // 60
188
+ retry_seconds = (
189
+ WEB_REQUEST_BANNED_RETRY_SECONDS
190
+ if e.code == -412
191
+ else WEB_REQUEST_ERROR_RETRY_SECONDS
192
+ )
193
+ dynamic_risk_control_until[uid] = monotonic() + retry_seconds
194
+ retry_minutes = max(retry_seconds // 60, 1)
194
195
  logger.warning(
195
196
  f"动态 Web 接口获取失败,{name}({uid})将在 "
196
197
  f"{retry_minutes} 分钟后重试:{e.code} {e.msg}"
197
198
  )
198
- await throttle_dynamic_loop()
199
199
  return
200
200
 
201
201
  dynamic_risk_control_until.pop(uid, None)
@@ -256,6 +256,30 @@ async def dy_sched():
256
256
  await db.update_user(uid, name)
257
257
 
258
258
 
259
+ async def dy_sched():
260
+ """动态推送"""
261
+ if not await db.wait_until_ready():
262
+ logger.debug("数据库尚未初始化完成,跳过本轮动态推送")
263
+ await throttle_dynamic_loop()
264
+ return
265
+
266
+ uids = await db.get_uid_list("dynamic")
267
+ if not uids:
268
+ # 没有订阅先暂停一秒再跳过,不然会导致 CPU 占用过高
269
+ await throttle_dynamic_loop()
270
+ return
271
+
272
+ logger.debug(f"爬取动态列表,总共 {len(uids)} 人")
273
+ semaphore = asyncio.Semaphore(DYNAMIC_FETCH_CONCURRENCY)
274
+
275
+ async def run_for_uid(uid: int):
276
+ async with semaphore:
277
+ await process_dynamic_uid(uid)
278
+
279
+ await asyncio.gather(*(run_for_uid(uid) for uid in uids))
280
+ await throttle_dynamic_loop()
281
+
282
+
259
283
  def dynamic_lisener(event):
260
284
  if hasattr(event, "job_id") and event.job_id != "dynamic_sched":
261
285
  return
@@ -277,4 +301,7 @@ else:
277
301
  "interval",
278
302
  seconds=plugin_config.bililive_dynamic_interval,
279
303
  id="dynamic_sched",
304
+ coalesce=True,
305
+ max_instances=1,
306
+ misfire_grace_time=5,
280
307
  )
@@ -304,4 +304,8 @@ PROXIES = {"all://": plugin_config.bililive_proxy}
304
304
  require("nonebot_plugin_apscheduler")
305
305
  from nonebot_plugin_apscheduler import scheduler # noqa
306
306
 
307
- from .browser import get_bilibili_cookies, get_dynamic_screenshot # noqa
307
+ from .browser import ( # noqa
308
+ get_bilibili_cookies,
309
+ get_dynamic_screenshot,
310
+ get_user_dynamics_payload_in_browser,
311
+ )
@@ -16,6 +16,7 @@ from .fonts_provider import fill_font
16
16
 
17
17
  _browser: BrowserContext | None = None
18
18
  mobile_js = Path(__file__).parent.joinpath("mobile.js")
19
+ WEB_DYNAMIC_URL = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space"
19
20
 
20
21
 
21
22
  async def init_browser(proxy=plugin_config.bililive_proxy, **kwargs) -> BrowserContext:
@@ -72,6 +73,32 @@ async def get_bilibili_cookies() -> dict[str, str]:
72
73
  return {cookie["name"]: cookie["value"] for cookie in cookies}
73
74
 
74
75
 
76
+ async def get_user_dynamics_payload_in_browser(uid: int) -> dict:
77
+ browser = await get_browser()
78
+ page = await browser.new_page()
79
+ try:
80
+ await page.goto(
81
+ "https://www.bilibili.com/",
82
+ wait_until="domcontentloaded",
83
+ timeout=plugin_config.bililive_dynamic_timeout * 1000,
84
+ )
85
+ return await page.evaluate(
86
+ """async ({ url, uid }) => {
87
+ const response = await fetch(`${url}?host_mid=${uid}`, {
88
+ credentials: 'include',
89
+ headers: {
90
+ accept: 'application/json, text/plain, */*',
91
+ },
92
+ });
93
+ return await response.json();
94
+ }""",
95
+ {"url": WEB_DYNAMIC_URL, "uid": str(uid)},
96
+ )
97
+ finally:
98
+ with contextlib.suppress(Exception):
99
+ await page.close()
100
+
101
+
75
102
  async def get_dynamic_screenshot(
76
103
  dynamic_id,
77
104
  style=plugin_config.bililive_screenshot_style,
@@ -1,4 +1,4 @@
1
1
  from packaging.version import Version
2
2
 
3
- __version__ = "2.0.5"
3
+ __version__ = "2.0.6"
4
4
  VERSION = Version(__version__)
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "msvc-runtime>=14.34.31931; sys_platform == \"win32\"",
39
39
  ]
40
40
  dynamic = []
41
- version = "2.0.5"
41
+ version = "2.0.6"
42
42
 
43
43
  [project.license]
44
44
  text = "AGPL-3.0-or-later"
@@ -20,7 +20,20 @@ class DummyDriver:
20
20
 
21
21
 
22
22
  fake_apscheduler = ModuleType("nonebot_plugin_apscheduler")
23
- fake_apscheduler.scheduler = SimpleNamespace()
23
+
24
+
25
+ class DummyScheduler:
26
+ def add_listener(self, *args, **kwargs):
27
+ return None
28
+
29
+ def add_job(self, *args, **kwargs):
30
+ return None
31
+
32
+ def get_job(self, *args, **kwargs):
33
+ return None
34
+
35
+
36
+ fake_apscheduler.scheduler = DummyScheduler()
24
37
 
25
38
 
26
39
  with patch("nonebot.get_driver", return_value=DummyDriver()), patch(
@@ -131,6 +144,18 @@ class WebDynamicTests(unittest.TestCase):
131
144
 
132
145
  self.assertEqual(web_dynamic.parse_web_dynamic_items(payload), [])
133
146
 
147
+ def test_parse_web_dynamic_payload_raises_for_error_code(self):
148
+ payload = {
149
+ "code": -412,
150
+ "message": "request was banned",
151
+ "data": None,
152
+ }
153
+
154
+ with self.assertRaises(web_dynamic.WebDynamicError) as context:
155
+ web_dynamic.parse_web_dynamic_payload(payload)
156
+
157
+ self.assertEqual(context.exception.code, -412)
158
+
134
159
 
135
160
  class DBPermissionTests(unittest.IsolatedAsyncioTestCase):
136
161
  async def test_db_init_enables_global_fallback(self):