nonebot-plugin-bililive 2.0.5__tar.gz → 2.0.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 (53) hide show
  1. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/PKG-INFO +16 -9
  2. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/README.md +11 -5
  3. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/__init__.py +1 -1
  4. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/cli/utils.py +0 -1
  5. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/libs/dynamic/web.py +11 -8
  6. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/pusher/dynamic_pusher.py +69 -42
  7. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/utils/__init__.py +20 -11
  8. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/utils/browser.py +27 -0
  9. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/version.py +1 -1
  10. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/nonebot_plugin_bililive/__init__.py +1 -1
  11. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/pyproject.toml +6 -5
  12. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/tests/test_maintenance.py +47 -2
  13. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/LICENSE +0 -0
  14. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/__main__.py +0 -0
  15. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/cli/__init__.py +0 -0
  16. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/cli/bot.py +0 -0
  17. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/compat.py +0 -0
  18. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/config.py +0 -0
  19. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/database/__init__.py +0 -0
  20. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/database/db.py +0 -0
  21. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/database/models.py +0 -0
  22. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/libs/__init__.py +0 -0
  23. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/libs/dynamic/__init__.py +0 -0
  24. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/libs/dynamic/card.py +0 -0
  25. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/libs/dynamic/desc.py +0 -0
  26. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/libs/dynamic/display.py +0 -0
  27. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/libs/dynamic/user_profile.py +0 -0
  28. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/__init__.py +0 -0
  29. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/at/__init__.py +0 -0
  30. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/at/at_off.py +0 -0
  31. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/at/at_on.py +0 -0
  32. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/auto_agree.py +0 -0
  33. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/auto_delete.py +0 -0
  34. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/dynamic/__init__.py +0 -0
  35. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/dynamic/dynamic_off.py +0 -0
  36. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/dynamic/dynamic_on.py +0 -0
  37. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/help.py +0 -0
  38. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/live/__init__.py +0 -0
  39. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/live/live_now.py +0 -0
  40. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/live/live_off.py +0 -0
  41. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/live/live_on.py +0 -0
  42. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/permission/__init__.py +0 -0
  43. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/permission/permission_off.py +0 -0
  44. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/permission/permission_on.py +0 -0
  45. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/pusher/__init__.py +0 -0
  46. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/pusher/live_pusher.py +0 -0
  47. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/sub/__init__.py +0 -0
  48. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/sub/add_sub.py +0 -0
  49. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/sub/delete_sub.py +0 -0
  50. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/plugins/sub/sub_list.py +0 -0
  51. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/utils/captcha_solver.py +0 -0
  52. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/utils/fonts_provider.py +0 -0
  53. {nonebot_plugin_bililive-2.0.5 → nonebot_plugin_bililive-2.0.7}/bililive/utils/mobile.js +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-bililive
3
- Version: 2.0.5
3
+ Version: 2.0.7
4
4
  Summary: Push bilibili dynamics and live notifications to QQ with NoneBot2.
5
- Keywords: nonebot,nonebot2,nonebot-plugin,qqbot,bilibili
5
+ Keywords: nonebot,nonebot2,nonebot-plugin,bilibili,onebot,onebot-v11
6
6
  Author-Email: SK-415 <2967923486@qq.com>
7
7
  License: AGPL-3.0-or-later
8
8
  Classifier: Development Status :: 4 - Beta
9
- Classifier: Framework :: Robot Framework
10
- Classifier: Framework :: Robot Framework :: Library
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
@@ -22,6 +22,7 @@ Requires-Dist: click>=8.1.3
22
22
  Requires-Dist: httpx<1.0,>=0.28.1
23
23
  Requires-Dist: nonebot-adapter-onebot>=2.4.6
24
24
  Requires-Dist: nonebot-plugin-apscheduler>=0.5.0
25
+ Requires-Dist: nonebot-plugin-localstore>=0.7
25
26
  Requires-Dist: nonebot2[fastapi]>=2.5.0
26
27
  Requires-Dist: playwright>=1.58.0
27
28
  Requires-Dist: pydantic<3.0,>=2.12.5
@@ -58,6 +59,8 @@ _✨ 将 B 站 UP 主动态与直播推送到 QQ 的 NoneBot2 插件 ✨_
58
59
 
59
60
  BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直播与动态消息推送到 QQ 群或私聊场景。
60
61
 
62
+ 目前支持的适配器:OneBot V11。
63
+
61
64
  ### 特性
62
65
 
63
66
  - 支持按 UP 主维度分别开启或关闭动态、直播推送。
@@ -79,24 +82,28 @@ BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直
79
82
 
80
83
  <details>
81
84
  <summary>使用包管理器安装</summary>
82
- 在 nonebot2 项目的插件目录下, 打开命令行, 进入虚拟环境, 输入相应的安装命令
85
+
86
+ 在 NoneBot2 项目虚拟环境中安装:
83
87
 
84
88
  pip install nonebot-plugin-bililive
85
89
 
86
90
 
87
- 打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot.plugins]` 部分追加写入
91
+ 然后在 NoneBot2 项目根目录的 pyproject.toml 中加载插件:
88
92
 
89
- nonebot-plugin-bililive = ["nonebot-plugin-bililive"]
93
+ [tool.nonebot]
94
+ plugins = ["nonebot_plugin_bililive"]
90
95
 
91
96
  </details>
92
97
 
98
+ 首次使用动态截图功能时,插件会自动检查并安装 Chromium;如果需要自定义数据目录,可通过 BILILIVE_DIR 覆盖默认存储位置。未配置时,插件会使用 nonebot-plugin-localstore 提供的插件数据目录。
99
+
93
100
  ## ⚙️ 配置
94
101
 
95
102
  在 NoneBot2 项目的 .env 文件中按需添加配置项:
96
103
 
97
104
  | 配置项 | 必填 | 默认值 | 说明 |
98
105
  |:-----:|:----:|:----:|:----|
99
- | BILILIVE_DIR | 否 | data | 数据目录 |
106
+ | BILILIVE_DIR | 否 | nonebot-plugin-localstore 自动分配 | 插件数据目录,包含数据库、浏览器数据与动态偏移缓存 |
100
107
  | BILILIVE_TO_ME | 否 | true | 是否需要 @机器人 或命令前缀触发 |
101
108
  | BILILIVE_PROXY | 否 | 无 | HTTP 代理地址,用于 B 站请求和 Playwright 下载 |
102
109
  | BILILIVE_INTERVAL | 否 | 10 | 默认轮询间隔,单位秒 |
@@ -136,7 +143,7 @@ BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直
136
143
 
137
144
  ### 效果图
138
145
 
139
- ![demo](/docs/.vuepress/public/demo.png)
146
+ ![demo](./docs/.vuepress/public/demo.png)
140
147
 
141
148
  ## 开发
142
149
 
@@ -24,6 +24,8 @@ _✨ 将 B 站 UP 主动态与直播推送到 QQ 的 NoneBot2 插件 ✨_
24
24
 
25
25
  BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直播与动态消息推送到 QQ 群或私聊场景。
26
26
 
27
+ 目前支持的适配器:OneBot V11。
28
+
27
29
  ### 特性
28
30
 
29
31
  - 支持按 UP 主维度分别开启或关闭动态、直播推送。
@@ -45,24 +47,28 @@ BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直
45
47
 
46
48
  <details>
47
49
  <summary>使用包管理器安装</summary>
48
- 在 nonebot2 项目的插件目录下, 打开命令行, 进入虚拟环境, 输入相应的安装命令
50
+
51
+ 在 NoneBot2 项目虚拟环境中安装:
49
52
 
50
53
  pip install nonebot-plugin-bililive
51
54
 
52
55
 
53
- 打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot.plugins]` 部分追加写入
56
+ 然后在 NoneBot2 项目根目录的 pyproject.toml 中加载插件:
54
57
 
55
- nonebot-plugin-bililive = ["nonebot-plugin-bililive"]
58
+ [tool.nonebot]
59
+ plugins = ["nonebot_plugin_bililive"]
56
60
 
57
61
  </details>
58
62
 
63
+ 首次使用动态截图功能时,插件会自动检查并安装 Chromium;如果需要自定义数据目录,可通过 BILILIVE_DIR 覆盖默认存储位置。未配置时,插件会使用 nonebot-plugin-localstore 提供的插件数据目录。
64
+
59
65
  ## ⚙️ 配置
60
66
 
61
67
  在 NoneBot2 项目的 .env 文件中按需添加配置项:
62
68
 
63
69
  | 配置项 | 必填 | 默认值 | 说明 |
64
70
  |:-----:|:----:|:----:|:----|
65
- | BILILIVE_DIR | 否 | data | 数据目录 |
71
+ | BILILIVE_DIR | 否 | nonebot-plugin-localstore 自动分配 | 插件数据目录,包含数据库、浏览器数据与动态偏移缓存 |
66
72
  | BILILIVE_TO_ME | 否 | true | 是否需要 @机器人 或命令前缀触发 |
67
73
  | BILILIVE_PROXY | 否 | 无 | HTTP 代理地址,用于 B 站请求和 Playwright 下载 |
68
74
  | BILILIVE_INTERVAL | 否 | 10 | 默认轮询间隔,单位秒 |
@@ -102,7 +108,7 @@ BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直
102
108
 
103
109
  ### 效果图
104
110
 
105
- ![demo](/docs/.vuepress/public/demo.png)
111
+ ![demo](./docs/.vuepress/public/demo.png)
106
112
 
107
113
  ## 开发
108
114
 
@@ -22,7 +22,7 @@ from .version import VERSION, __version__ # noqa: F401
22
22
  __plugin_meta__ = PluginMetadata(
23
23
  name="BiliLive",
24
24
  description="将B站UP主的动态和直播信息推送至QQ",
25
- usage="https://github.com/Akiyy-dev/nonebot-plugin-bililive#readme",
25
+ usage="发送“帮助”查看命令列表,发送“关注 UID”订阅 UP 主",
26
26
  homepage="https://github.com/Akiyy-dev/nonebot-plugin-bililive",
27
27
  type="application",
28
28
  config=Config,
@@ -10,7 +10,6 @@ env = {
10
10
  "ACCESS_TOKEN": "",
11
11
  "SUPERUSERS": [],
12
12
  "COMMAND_START": [""],
13
- "BILILIVE_DIR": "./data/",
14
13
  }
15
14
 
16
15
 
@@ -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
  )
@@ -30,15 +30,22 @@ from nonebot.rule import Rule
30
30
 
31
31
  from ..config import plugin_config
32
32
 
33
+ require("nonebot_plugin_localstore")
34
+ import nonebot_plugin_localstore as store
33
35
 
34
- def get_path(*other):
35
- """获取数据文件绝对路径"""
36
+ PLUGIN_ENTRY_NAME = "nonebot_plugin_bililive"
37
+
38
+
39
+ def get_data_dir() -> Path:
40
+ """获取插件数据目录。"""
36
41
  if plugin_config.bililive_dir:
37
- dir_path = Path(plugin_config.bililive_dir).resolve()
38
- else:
39
- dir_path = Path.cwd().joinpath("data")
40
- # dir_path = Path.cwd().joinpath('data', 'bililive')
41
- return str(dir_path.joinpath(*other))
42
+ return Path(plugin_config.bililive_dir).resolve()
43
+ return store.get_data_dir(PLUGIN_ENTRY_NAME)
44
+
45
+
46
+ def get_path(*other):
47
+ """获取插件数据文件绝对路径。"""
48
+ return str(get_data_dir().joinpath(*other))
42
49
 
43
50
 
44
51
  async def handle_uid(
@@ -290,9 +297,7 @@ def on_startup():
290
297
  check_proxy()
291
298
  install()
292
299
  asyncio.get_event_loop().run_until_complete(check_playwright_env())
293
- # 创建数据存储目录
294
- if not Path(get_path()).is_dir():
295
- Path(get_path()).mkdir(parents=True)
300
+ get_data_dir().mkdir(parents=True, exist_ok=True)
296
301
 
297
302
 
298
303
  def on_command(cmd, *args, **kwargs):
@@ -304,4 +309,8 @@ PROXIES = {"all://": plugin_config.bililive_proxy}
304
309
  require("nonebot_plugin_apscheduler")
305
310
  from nonebot_plugin_apscheduler import scheduler # noqa
306
311
 
307
- from .browser import get_bilibili_cookies, get_dynamic_screenshot # noqa
312
+ from .browser import ( # noqa
313
+ get_bilibili_cookies,
314
+ get_dynamic_screenshot,
315
+ get_user_dynamics_payload_in_browser,
316
+ )
@@ -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.7"
4
4
  VERSION = Version(__version__)
@@ -10,7 +10,7 @@ if isinstance(globals()["__loader__"], PluginLoader):
10
10
  __plugin_meta__ = PluginMetadata(
11
11
  name="BiliLive",
12
12
  description="将 B 站 UP 主的动态和直播信息推送至 QQ",
13
- usage="发送 帮助 查看命令列表",
13
+ usage="发送“帮助”查看命令列表,发送“关注 UID”订阅 UP 主",
14
14
  homepage="https://github.com/Akiyy-dev/nonebot-plugin-bililive",
15
15
  type="application",
16
16
  config=Config,
@@ -10,13 +10,14 @@ keywords = [
10
10
  "nonebot",
11
11
  "nonebot2",
12
12
  "nonebot-plugin",
13
- "qqbot",
14
13
  "bilibili",
14
+ "onebot",
15
+ "onebot-v11",
15
16
  ]
16
17
  classifiers = [
17
18
  "Development Status :: 4 - Beta",
18
- "Framework :: Robot Framework",
19
- "Framework :: Robot Framework :: Library",
19
+ "Framework :: AsyncIO",
20
+ "Operating System :: OS Independent",
20
21
  "Programming Language :: Python :: 3",
21
22
  "Programming Language :: Python :: 3.10",
22
23
  "Programming Language :: Python :: 3.11",
@@ -28,6 +29,7 @@ dependencies = [
28
29
  "httpx>=0.28.1,<1.0",
29
30
  "nonebot-adapter-onebot>=2.4.6",
30
31
  "nonebot-plugin-apscheduler>=0.5.0",
32
+ "nonebot-plugin-localstore>=0.7",
31
33
  "nonebot2[fastapi]>=2.5.0",
32
34
  "playwright>=1.58.0",
33
35
  "pydantic>=2.12.5,<3.0",
@@ -38,7 +40,7 @@ dependencies = [
38
40
  "msvc-runtime>=14.34.31931; sys_platform == \"win32\"",
39
41
  ]
40
42
  dynamic = []
41
- version = "2.0.5"
43
+ version = "2.0.7"
42
44
 
43
45
  [project.license]
44
46
  text = "AGPL-3.0-or-later"
@@ -50,7 +52,6 @@ Documentation = "https://github.com/Akiyy-dev/nonebot-plugin-bililive#readme"
50
52
  Issues = "https://github.com/Akiyy-dev/nonebot-plugin-bililive/issues"
51
53
 
52
54
  [project.entry-points."nonebot.plugin"]
53
- nonebot-plugin-bililive = "nonebot_plugin_bililive"
54
55
  nonebot_plugin_bililive = "nonebot_plugin_bililive"
55
56
 
56
57
  [project.scripts]
@@ -20,12 +20,39 @@ class DummyDriver:
20
20
 
21
21
 
22
22
  fake_apscheduler = ModuleType("nonebot_plugin_apscheduler")
23
- fake_apscheduler.scheduler = SimpleNamespace()
23
+ fake_localstore = ModuleType("nonebot_plugin_localstore")
24
+
25
+
26
+ class DummyScheduler:
27
+ def add_listener(self, *args, **kwargs):
28
+ return None
29
+
30
+ def add_job(self, *args, **kwargs):
31
+ return None
32
+
33
+ def get_job(self, *args, **kwargs):
34
+ return None
35
+
36
+
37
+ fake_apscheduler.scheduler = DummyScheduler()
38
+
39
+
40
+ def _get_data_dir(plugin_name: str | None) -> Path:
41
+ return Path(tempfile.gettempdir()) / (plugin_name or "nonebot2")
42
+
43
+
44
+ fake_localstore.get_data_dir = _get_data_dir
24
45
 
25
46
 
26
47
  with patch("nonebot.get_driver", return_value=DummyDriver()), patch(
27
48
  "nonebot.require", return_value=None
28
- ), patch.dict(sys.modules, {"nonebot_plugin_apscheduler": fake_apscheduler}):
49
+ ), patch.dict(
50
+ sys.modules,
51
+ {
52
+ "nonebot_plugin_apscheduler": fake_apscheduler,
53
+ "nonebot_plugin_localstore": fake_localstore,
54
+ },
55
+ ):
29
56
  compat = import_module("bililive.compat")
30
57
  Config = import_module("bililive.config").Config
31
58
  core_version = import_module("bililive.version")
@@ -35,6 +62,7 @@ with patch("nonebot.get_driver", return_value=DummyDriver()), patch(
35
62
  DB = db_module.DB
36
63
  models = import_module("bililive.database.models")
37
64
  Group = models.Group
65
+ get_path = import_module("bililive.utils").get_path
38
66
 
39
67
 
40
68
  class ConfigTests(unittest.TestCase):
@@ -91,6 +119,11 @@ class PluginEntryTests(unittest.TestCase):
91
119
  self.assertEqual(plugin_entry.__plugin_meta__.config, Config)
92
120
  self.assertEqual(plugin_entry.__version__, core_version.__version__)
93
121
 
122
+ def test_default_data_dir_uses_localstore(self):
123
+ expected = _get_data_dir("nonebot_plugin_bililive") / "data.sqlite3"
124
+
125
+ self.assertEqual(Path(get_path("data.sqlite3")), expected)
126
+
94
127
 
95
128
  class WebDynamicTests(unittest.TestCase):
96
129
  def test_parse_web_dynamic_items_extracts_required_fields(self):
@@ -131,6 +164,18 @@ class WebDynamicTests(unittest.TestCase):
131
164
 
132
165
  self.assertEqual(web_dynamic.parse_web_dynamic_items(payload), [])
133
166
 
167
+ def test_parse_web_dynamic_payload_raises_for_error_code(self):
168
+ payload = {
169
+ "code": -412,
170
+ "message": "request was banned",
171
+ "data": None,
172
+ }
173
+
174
+ with self.assertRaises(web_dynamic.WebDynamicError) as context:
175
+ web_dynamic.parse_web_dynamic_payload(payload)
176
+
177
+ self.assertEqual(context.exception.code, -412)
178
+
134
179
 
135
180
  class DBPermissionTests(unittest.IsolatedAsyncioTestCase):
136
181
  async def test_db_init_enables_global_fallback(self):