nonebot-plugin-bililive 2.0.3__tar.gz → 2.0.5__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.3 → nonebot_plugin_bililive-2.0.5}/PKG-INFO +1 -1
  2. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/database/db.py +70 -0
  3. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/pusher/dynamic_pusher.py +12 -10
  4. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/pusher/live_pusher.py +4 -0
  5. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/utils/mobile.js +3 -3
  6. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/version.py +1 -1
  7. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/pyproject.toml +1 -1
  8. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/tests/test_maintenance.py +27 -0
  9. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/LICENSE +0 -0
  10. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/README.md +0 -0
  11. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/__init__.py +0 -0
  12. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/__main__.py +0 -0
  13. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/cli/__init__.py +0 -0
  14. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/cli/bot.py +0 -0
  15. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/cli/utils.py +0 -0
  16. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/compat.py +0 -0
  17. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/config.py +0 -0
  18. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/database/__init__.py +0 -0
  19. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/database/models.py +0 -0
  20. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/libs/__init__.py +0 -0
  21. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/libs/dynamic/__init__.py +0 -0
  22. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/libs/dynamic/card.py +0 -0
  23. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/libs/dynamic/desc.py +0 -0
  24. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/libs/dynamic/display.py +0 -0
  25. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/libs/dynamic/user_profile.py +0 -0
  26. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/libs/dynamic/web.py +0 -0
  27. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/__init__.py +0 -0
  28. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/at/__init__.py +0 -0
  29. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/at/at_off.py +0 -0
  30. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/at/at_on.py +0 -0
  31. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/auto_agree.py +0 -0
  32. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/auto_delete.py +0 -0
  33. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/dynamic/__init__.py +0 -0
  34. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/dynamic/dynamic_off.py +0 -0
  35. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/dynamic/dynamic_on.py +0 -0
  36. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/help.py +0 -0
  37. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/live/__init__.py +0 -0
  38. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/live/live_now.py +0 -0
  39. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/live/live_off.py +0 -0
  40. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/live/live_on.py +0 -0
  41. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/permission/__init__.py +0 -0
  42. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/permission/permission_off.py +0 -0
  43. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/permission/permission_on.py +0 -0
  44. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/pusher/__init__.py +0 -0
  45. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/sub/__init__.py +0 -0
  46. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/sub/add_sub.py +0 -0
  47. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/sub/delete_sub.py +0 -0
  48. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/plugins/sub/sub_list.py +0 -0
  49. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/utils/__init__.py +0 -0
  50. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/utils/browser.py +0 -0
  51. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/utils/captcha_solver.py +0 -0
  52. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/bililive/utils/fonts_provider.py +0 -0
  53. {nonebot_plugin_bililive-2.0.3 → nonebot_plugin_bililive-2.0.5}/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.3
3
+ Version: 2.0.5
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>
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import json
2
3
  from pathlib import Path
3
4
 
@@ -18,9 +19,16 @@ dynamic_offset = {}
18
19
  class DB:
19
20
  """数据库交互类,与增删改查无关的部分不应该在这里面实现"""
20
21
 
22
+ _ready = False
23
+
24
+ @classmethod
25
+ def get_dynamic_offset_path(cls) -> Path:
26
+ return Path(get_path("dynamic_offset.json"))
27
+
21
28
  @classmethod
22
29
  async def init(cls):
23
30
  """初始化数据库"""
31
+ cls._ready = False
24
32
  config = {
25
33
  "connections": {
26
34
  # "bililive": {
@@ -42,11 +50,68 @@ class DB:
42
50
  await Tortoise.generate_schemas()
43
51
  await cls.migrate()
44
52
  await cls.update_uid_list()
53
+ await cls.load_dynamic_offsets()
54
+ await cls.save_dynamic_offsets()
55
+ cls._ready = True
45
56
 
46
57
  @classmethod
47
58
  async def close(cls):
59
+ cls._ready = False
60
+ await cls.save_dynamic_offsets()
48
61
  await connections.close_all()
49
62
 
63
+ @classmethod
64
+ async def wait_until_ready(cls, timeout: float = 30) -> bool:
65
+ if cls._ready:
66
+ return True
67
+
68
+ waited = 0.0
69
+ interval = 0.1
70
+ while waited < timeout:
71
+ if cls._ready:
72
+ return True
73
+ await asyncio.sleep(interval)
74
+ waited += interval
75
+
76
+ return cls._ready
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
+
50
115
  @classmethod
51
116
  async def get_user(cls, **kwargs):
52
117
  """获取 UP 主信息"""
@@ -256,12 +321,17 @@ class DB:
256
321
  )
257
322
 
258
323
  # 清除没有订阅的 offset
324
+ changed = False
259
325
  dynamic_offset_keys = set(dynamic_offset)
260
326
  dynamic_uids = set(uid_list["dynamic"]["list"])
261
327
  for uid in dynamic_offset_keys - dynamic_uids:
262
328
  del dynamic_offset[uid]
329
+ changed = True
263
330
  for uid in dynamic_uids - dynamic_offset_keys:
264
331
  dynamic_offset[uid] = -1
332
+ changed = True
333
+ if changed:
334
+ await cls.save_dynamic_offsets()
265
335
 
266
336
  async def backup(self):
267
337
  """备份数据库"""
@@ -145,6 +145,11 @@ async def get_user_dynamics_with_web_fallback(uid: int) -> tuple[list, bool]:
145
145
 
146
146
  async def dy_sched():
147
147
  """动态推送"""
148
+ if not await db.wait_until_ready():
149
+ logger.debug("数据库尚未初始化完成,跳过本轮动态推送")
150
+ await throttle_dynamic_loop()
151
+ return
152
+
148
153
  uid = await db.next_uid("dynamic")
149
154
  if not uid:
150
155
  # 没有订阅先暂停一秒再跳过,不然会导致 CPU 占用过高
@@ -197,20 +202,17 @@ async def dy_sched():
197
202
 
198
203
  if not dynamics: # 没发过动态
199
204
  if uid in offset and offset[uid] == -1: # 不记录会导致第一次发动态不推送
200
- offset[uid] = 0
205
+ await db.set_dynamic_offset(uid, 0)
201
206
  return
202
207
  name = get_dynamic_author_name(dynamics[0], use_web_fallback)
203
208
 
204
209
  if uid not in offset: # 已删除
205
210
  return
206
211
  elif offset[uid] == -1: # 第一次爬取
207
- if len(dynamics) == 1: # 只有一条动态
208
- offset[uid] = get_dynamic_id(dynamics[0], use_web_fallback)
209
- else: # 第一个可能是置顶动态,但置顶也可能是最新一条,所以取前两条的最大值
210
- offset[uid] = max(
211
- get_dynamic_id(dynamics[0], use_web_fallback),
212
- get_dynamic_id(dynamics[1], use_web_fallback),
213
- )
212
+ await db.set_dynamic_offset(
213
+ uid,
214
+ max(get_dynamic_id(item, use_web_fallback) for item in dynamics),
215
+ )
214
216
  return
215
217
 
216
218
  dynamic = None
@@ -229,7 +231,7 @@ async def dy_sched():
229
231
  return
230
232
  elif should_skip_dynamic(dynamic_type, use_web_fallback):
231
233
  logger.debug(f"无需推送的动态 {dynamic_type},已跳过:{url}")
232
- offset[uid] = dynamic_id
234
+ await db.set_dynamic_offset(uid, dynamic_id)
233
235
  return
234
236
  message = (
235
237
  f"{name} {get_dynamic_type_message(dynamic_type, use_web_fallback)}:\n"
@@ -248,7 +250,7 @@ async def dy_sched():
248
250
  at=bool(sets.at) and plugin_config.bililive_dynamic_at,
249
251
  )
250
252
 
251
- offset[uid] = dynamic_id
253
+ await db.set_dynamic_offset(uid, dynamic_id)
252
254
 
253
255
  if dynamic:
254
256
  await db.update_user(uid, name)
@@ -23,6 +23,10 @@ live_time = {}
23
23
  async def live_sched():
24
24
  # sourcery skip: use-fstring-for-concatenation
25
25
  """直播推送"""
26
+ if not await db.wait_until_ready():
27
+ logger.debug("数据库尚未初始化完成,跳过本轮直播推送")
28
+ return
29
+
26
30
  uids = await db.get_uid_list("live")
27
31
 
28
32
  if not uids: # 订阅为空
@@ -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 + "," + emojiFont + ",sans-serif";
190
+ appDom.style.fontFamily = [font, fallbackFonts, emojiFont, "sans-serif"].filter(Boolean).join(",");
190
191
  } else {
191
- const needLoadFont = needLoadFontList.reduce((defaultString, fontObject) => defaultString + fontObject.fontFamily + ",", "");
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,4 +1,4 @@
1
1
  from packaging.version import Version
2
2
 
3
- __version__ = "2.0.3"
3
+ __version__ = "2.0.5"
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.3"
41
+ version = "2.0.5"
42
42
 
43
43
  [project.license]
44
44
  text = "AGPL-3.0-or-later"
@@ -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
 
@@ -145,10 +147,35 @@ class DBPermissionTests(unittest.IsolatedAsyncioTestCase):
145
147
  await DB.init()
146
148
 
147
149
  self.assertTrue(init_db.await_args.kwargs["_enable_global_fallback"])
150
+ self.assertTrue(DB._ready)
148
151
  generate_schemas.assert_awaited_once()
149
152
  migrate.assert_awaited_once()
150
153
  update_uid_list.assert_awaited_once()
151
154
 
155
+ async def test_wait_until_ready_returns_false_before_init(self):
156
+ DB._ready = False
157
+
158
+ ready = await DB.wait_until_ready(timeout=0)
159
+
160
+ self.assertFalse(ready)
161
+
162
+ async def test_dynamic_offsets_are_persisted_and_restored(self):
163
+ with tempfile.TemporaryDirectory() as tmpdir:
164
+ offset_path = Path(tmpdir) / "dynamic_offset.json"
165
+ db_module.dynamic_offset.clear()
166
+ db_module.dynamic_offset[123] = 456
167
+
168
+ with patch.object(db_module, "get_path", return_value=str(offset_path)):
169
+ await DB.save_dynamic_offsets()
170
+
171
+ db_module.dynamic_offset.clear()
172
+ db_module.dynamic_offset[123] = -1
173
+ db_module.dynamic_offset[789] = -1
174
+ await DB.load_dynamic_offsets()
175
+
176
+ self.assertEqual(db_module.dynamic_offset[123], 456)
177
+ self.assertEqual(db_module.dynamic_offset[789], -1)
178
+
152
179
  async def test_set_permission_creates_group_when_missing(self):
153
180
  with (
154
181
  patch.object(DB, "get_group", new=AsyncMock(return_value=None)),