nonebot-plugin-bililive 2.0.4__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.
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/PKG-INFO +1 -1
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/database/db.py +49 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/web.py +11 -8
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/pusher/dynamic_pusher.py +76 -52
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/utils/__init__.py +5 -1
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/utils/browser.py +27 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/utils/mobile.js +3 -3
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/version.py +1 -1
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/pyproject.toml +1 -1
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/tests/test_maintenance.py +45 -1
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/LICENSE +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/README.md +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/__main__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/cli/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/cli/bot.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/cli/utils.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/compat.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/config.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/database/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/database/models.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/card.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/desc.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/display.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/user_profile.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/at/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/at/at_off.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/at/at_on.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/auto_agree.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/auto_delete.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/dynamic/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/dynamic/dynamic_off.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/dynamic/dynamic_on.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/help.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/live_now.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/live_off.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/live_on.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/permission/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/permission/permission_off.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/permission/permission_on.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/pusher/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/pusher/live_pusher.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/add_sub.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/delete_sub.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/sub_list.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/utils/captcha_solver.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/utils/fonts_provider.py +0 -0
- {nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/nonebot_plugin_bililive/__init__.py +0 -0
|
@@ -21,6 +21,10 @@ class DB:
|
|
|
21
21
|
|
|
22
22
|
_ready = False
|
|
23
23
|
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_dynamic_offset_path(cls) -> Path:
|
|
26
|
+
return Path(get_path("dynamic_offset.json"))
|
|
27
|
+
|
|
24
28
|
@classmethod
|
|
25
29
|
async def init(cls):
|
|
26
30
|
"""初始化数据库"""
|
|
@@ -46,11 +50,14 @@ class DB:
|
|
|
46
50
|
await Tortoise.generate_schemas()
|
|
47
51
|
await cls.migrate()
|
|
48
52
|
await cls.update_uid_list()
|
|
53
|
+
await cls.load_dynamic_offsets()
|
|
54
|
+
await cls.save_dynamic_offsets()
|
|
49
55
|
cls._ready = True
|
|
50
56
|
|
|
51
57
|
@classmethod
|
|
52
58
|
async def close(cls):
|
|
53
59
|
cls._ready = False
|
|
60
|
+
await cls.save_dynamic_offsets()
|
|
54
61
|
await connections.close_all()
|
|
55
62
|
|
|
56
63
|
@classmethod
|
|
@@ -68,6 +75,43 @@ class DB:
|
|
|
68
75
|
|
|
69
76
|
return cls._ready
|
|
70
77
|
|
|
78
|
+
@classmethod
|
|
79
|
+
async def load_dynamic_offsets(cls):
|
|
80
|
+
path = cls.get_dynamic_offset_path()
|
|
81
|
+
if not path.exists():
|
|
82
|
+
return
|
|
83
|
+
try:
|
|
84
|
+
raw_offsets = json.loads(path.read_text(encoding="utf-8"))
|
|
85
|
+
except Exception:
|
|
86
|
+
logger.warning("动态偏移量缓存读取失败,将使用当前内存状态继续运行")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
for uid_str, value in raw_offsets.items():
|
|
90
|
+
try:
|
|
91
|
+
uid = int(uid_str)
|
|
92
|
+
dynamic_id = int(value)
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
continue
|
|
95
|
+
if uid in dynamic_offset:
|
|
96
|
+
dynamic_offset[uid] = dynamic_id
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
async def save_dynamic_offsets(cls):
|
|
100
|
+
path = cls.get_dynamic_offset_path()
|
|
101
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
serialized_offsets = {
|
|
103
|
+
str(uid): dynamic_offset[uid] for uid in sorted(dynamic_offset)
|
|
104
|
+
}
|
|
105
|
+
path.write_text(
|
|
106
|
+
json.dumps(serialized_offsets, ensure_ascii=False, sort_keys=True),
|
|
107
|
+
encoding="utf-8",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
async def set_dynamic_offset(cls, uid: int, value: int):
|
|
112
|
+
dynamic_offset[int(uid)] = int(value)
|
|
113
|
+
await cls.save_dynamic_offsets()
|
|
114
|
+
|
|
71
115
|
@classmethod
|
|
72
116
|
async def get_user(cls, **kwargs):
|
|
73
117
|
"""获取 UP 主信息"""
|
|
@@ -277,12 +321,17 @@ class DB:
|
|
|
277
321
|
)
|
|
278
322
|
|
|
279
323
|
# 清除没有订阅的 offset
|
|
324
|
+
changed = False
|
|
280
325
|
dynamic_offset_keys = set(dynamic_offset)
|
|
281
326
|
dynamic_uids = set(uid_list["dynamic"]["list"])
|
|
282
327
|
for uid in dynamic_offset_keys - dynamic_uids:
|
|
283
328
|
del dynamic_offset[uid]
|
|
329
|
+
changed = True
|
|
284
330
|
for uid in dynamic_uids - dynamic_offset_keys:
|
|
285
331
|
dynamic_offset[uid] = -1
|
|
332
|
+
changed = True
|
|
333
|
+
if changed:
|
|
334
|
+
await cls.save_dynamic_offsets()
|
|
286
335
|
|
|
287
336
|
async def backup(self):
|
|
288
337
|
"""备份数据库"""
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/web.py
RENAMED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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() +
|
|
133
|
-
|
|
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
|
|
151
|
+
dynamic_web_fallback_until[uid] = monotonic() + GRPC_RISK_CONTROL_RETRY_SECONDS
|
|
152
|
+
return await fetch_web_dynamics(), True
|
|
144
153
|
|
|
145
154
|
|
|
146
|
-
async def
|
|
147
|
-
"""动态推送"""
|
|
148
|
-
if not await db.wait_until_ready():
|
|
149
|
-
logger.debug("数据库尚未初始化完成,跳过本轮动态推送")
|
|
150
|
-
await throttle_dynamic_loop()
|
|
151
|
-
return
|
|
152
|
-
|
|
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,36 +183,36 @@ 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
|
-
|
|
193
|
-
|
|
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)
|
|
202
202
|
|
|
203
203
|
if not dynamics: # 没发过动态
|
|
204
204
|
if uid in offset and offset[uid] == -1: # 不记录会导致第一次发动态不推送
|
|
205
|
-
|
|
205
|
+
await db.set_dynamic_offset(uid, 0)
|
|
206
206
|
return
|
|
207
207
|
name = get_dynamic_author_name(dynamics[0], use_web_fallback)
|
|
208
208
|
|
|
209
209
|
if uid not in offset: # 已删除
|
|
210
210
|
return
|
|
211
211
|
elif offset[uid] == -1: # 第一次爬取
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
get_dynamic_id(dynamics[0], use_web_fallback),
|
|
217
|
-
get_dynamic_id(dynamics[1], use_web_fallback),
|
|
218
|
-
)
|
|
212
|
+
await db.set_dynamic_offset(
|
|
213
|
+
uid,
|
|
214
|
+
max(get_dynamic_id(item, use_web_fallback) for item in dynamics),
|
|
215
|
+
)
|
|
219
216
|
return
|
|
220
217
|
|
|
221
218
|
dynamic = None
|
|
@@ -234,7 +231,7 @@ async def dy_sched():
|
|
|
234
231
|
return
|
|
235
232
|
elif should_skip_dynamic(dynamic_type, use_web_fallback):
|
|
236
233
|
logger.debug(f"无需推送的动态 {dynamic_type},已跳过:{url}")
|
|
237
|
-
|
|
234
|
+
await db.set_dynamic_offset(uid, dynamic_id)
|
|
238
235
|
return
|
|
239
236
|
message = (
|
|
240
237
|
f"{name} {get_dynamic_type_message(dynamic_type, use_web_fallback)}:\n"
|
|
@@ -253,12 +250,36 @@ async def dy_sched():
|
|
|
253
250
|
at=bool(sets.at) and plugin_config.bililive_dynamic_at,
|
|
254
251
|
)
|
|
255
252
|
|
|
256
|
-
|
|
253
|
+
await db.set_dynamic_offset(uid, dynamic_id)
|
|
257
254
|
|
|
258
255
|
if dynamic:
|
|
259
256
|
await db.update_user(uid, name)
|
|
260
257
|
|
|
261
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
|
+
|
|
262
283
|
def dynamic_lisener(event):
|
|
263
284
|
if hasattr(event, "job_id") and event.job_id != "dynamic_sched":
|
|
264
285
|
return
|
|
@@ -280,4 +301,7 @@ else:
|
|
|
280
301
|
"interval",
|
|
281
302
|
seconds=plugin_config.bililive_dynamic_interval,
|
|
282
303
|
id="dynamic_sched",
|
|
304
|
+
coalesce=True,
|
|
305
|
+
max_instances=1,
|
|
306
|
+
misfire_grace_time=5,
|
|
283
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
|
|
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,
|
|
@@ -183,13 +183,13 @@ function setFont(font = "", fontSource = "local") {
|
|
|
183
183
|
// 将字体样式设置到 div#app 上
|
|
184
184
|
const appDom = document.querySelector("#app");
|
|
185
185
|
const emojiFont = emojiFontList.join(",");
|
|
186
|
+
const fallbackFonts = needLoadFontList.map(fontObject => fontObject.fontFamily).join(",");
|
|
186
187
|
if (appDom) {
|
|
187
188
|
// 动态加字体, 并给与默认值 sans-serif
|
|
188
189
|
if (fontSource === "system") {
|
|
189
|
-
appDom.style.fontFamily = font
|
|
190
|
+
appDom.style.fontFamily = [font, fallbackFonts, emojiFont, "sans-serif"].filter(Boolean).join(",");
|
|
190
191
|
} else {
|
|
191
|
-
|
|
192
|
-
appDom.style.fontFamily = needLoadFont + emojiFont + ",sans-serif";
|
|
192
|
+
appDom.style.fontFamily = [fallbackFonts, emojiFont, "sans-serif"].filter(Boolean).join(",");
|
|
193
193
|
};
|
|
194
194
|
appDom.style.overflowWrap = "break-word";
|
|
195
195
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import sys
|
|
2
|
+
import tempfile
|
|
2
3
|
import unittest
|
|
3
4
|
from importlib import import_module
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
from types import ModuleType, SimpleNamespace
|
|
5
7
|
from unittest.mock import AsyncMock, patch
|
|
6
8
|
|
|
@@ -18,7 +20,20 @@ class DummyDriver:
|
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
fake_apscheduler = ModuleType("nonebot_plugin_apscheduler")
|
|
21
|
-
|
|
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()
|
|
22
37
|
|
|
23
38
|
|
|
24
39
|
with patch("nonebot.get_driver", return_value=DummyDriver()), patch(
|
|
@@ -129,6 +144,18 @@ class WebDynamicTests(unittest.TestCase):
|
|
|
129
144
|
|
|
130
145
|
self.assertEqual(web_dynamic.parse_web_dynamic_items(payload), [])
|
|
131
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
|
+
|
|
132
159
|
|
|
133
160
|
class DBPermissionTests(unittest.IsolatedAsyncioTestCase):
|
|
134
161
|
async def test_db_init_enables_global_fallback(self):
|
|
@@ -157,6 +184,23 @@ class DBPermissionTests(unittest.IsolatedAsyncioTestCase):
|
|
|
157
184
|
|
|
158
185
|
self.assertFalse(ready)
|
|
159
186
|
|
|
187
|
+
async def test_dynamic_offsets_are_persisted_and_restored(self):
|
|
188
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
189
|
+
offset_path = Path(tmpdir) / "dynamic_offset.json"
|
|
190
|
+
db_module.dynamic_offset.clear()
|
|
191
|
+
db_module.dynamic_offset[123] = 456
|
|
192
|
+
|
|
193
|
+
with patch.object(db_module, "get_path", return_value=str(offset_path)):
|
|
194
|
+
await DB.save_dynamic_offsets()
|
|
195
|
+
|
|
196
|
+
db_module.dynamic_offset.clear()
|
|
197
|
+
db_module.dynamic_offset[123] = -1
|
|
198
|
+
db_module.dynamic_offset[789] = -1
|
|
199
|
+
await DB.load_dynamic_offsets()
|
|
200
|
+
|
|
201
|
+
self.assertEqual(db_module.dynamic_offset[123], 456)
|
|
202
|
+
self.assertEqual(db_module.dynamic_offset[789], -1)
|
|
203
|
+
|
|
160
204
|
async def test_set_permission_creates_group_when_missing(self):
|
|
161
205
|
with (
|
|
162
206
|
patch.object(DB, "get_group", new=AsyncMock(return_value=None)),
|
|
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
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/database/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/card.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/desc.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/libs/dynamic/display.py
RENAMED
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/at/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/at/at_off.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/at/at_on.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/auto_agree.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/auto_delete.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/dynamic/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/live_now.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/live_off.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/live/live_on.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/pusher/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/add_sub.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/delete_sub.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/plugins/sub/sub_list.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/utils/captcha_solver.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/bililive/utils/fonts_provider.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.4 → nonebot_plugin_bililive-2.0.6}/nonebot_plugin_bililive/__init__.py
RENAMED
|
File without changes
|