nonebot-plugin-bililive 2.1.5__tar.gz → 2.1.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.
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/PKG-INFO +4 -3
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/README.md +2 -1
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/config.py +10 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/database/db.py +55 -14
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/pusher/dynamic_pusher.py +1 -3
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/utils/__init__.py +13 -7
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/utils/browser.py +90 -19
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/version.py +1 -1
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/pyproject.toml +2 -2
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/tests/test_maintenance.py +147 -1
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/LICENSE +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/__main__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/bilibili_api.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/cli/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/cli/bot.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/cli/utils.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/database/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/database/models.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/libs/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/libs/dynamic/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/libs/dynamic/card.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/libs/dynamic/desc.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/libs/dynamic/display.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/libs/dynamic/user_profile.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/libs/dynamic/web.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/at/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/at/at_off.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/at/at_on.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/auto_agree.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/auto_delete.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/dynamic/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/dynamic/dynamic_off.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/dynamic/dynamic_on.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/help.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/live/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/live/live_now.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/live/live_off.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/live/live_on.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/permission/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/permission/permission_off.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/permission/permission_on.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/pusher/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/pusher/live_pusher.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/sub/__init__.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/sub/add_sub.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/sub/delete_sub.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/plugins/sub/sub_list.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/utils/captcha_solver.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/utils/fonts_provider.py +0 -0
- {nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/utils/mobile.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: nonebot-plugin-bililive
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.7
|
|
4
4
|
Summary: Push bilibili dynamics and live notifications to QQ with NoneBot2.
|
|
5
5
|
Keywords: nonebot,nonebot2,nonebot-plugin,bilibili,onebot,onebot-v11
|
|
6
6
|
Author-Email: Akiyy_Lab <2806578374@qq.com>
|
|
@@ -27,7 +27,7 @@ Requires-Dist: nonebot2>=2.5.0
|
|
|
27
27
|
Requires-Dist: playwright>=1.58.0
|
|
28
28
|
Requires-Dist: pydantic<3.0,>=1.10.0
|
|
29
29
|
Requires-Dist: python-dotenv>=1.2.2
|
|
30
|
-
Requires-Dist: tortoise-orm[asyncpg]
|
|
30
|
+
Requires-Dist: tortoise-orm[asyncpg]<2.0,>=1.0.0
|
|
31
31
|
Requires-Dist: packaging>=26.0
|
|
32
32
|
Requires-Dist: msvc-runtime>=14.34.31931; sys_platform == "win32"
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
@@ -116,8 +116,9 @@ BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直
|
|
|
116
116
|
| BILILIVE_DYNAMIC_FONT | 否 | Noto Sans CJK SC | 截图字体 |
|
|
117
117
|
| BILILIVE_DYNAMIC_BIG_IMAGE | 否 | false | 是否优先展示大图 |
|
|
118
118
|
| BILILIVE_COMMAND_PREFIX | 否 | 空字符串 | 命令额外前缀 |
|
|
119
|
+
| BILILIVE_CHROMIUM_ENDPOINT | 否 | 无 | 外部 Chromium CDP 地址,如 `http://127.0.0.1:9222` |
|
|
119
120
|
|
|
120
|
-
|
|
121
|
+
动态抓取优先使用 `BILILIVE_CHROMIUM_ENDPOINT` 连接的外部 Chromium;未配置时使用 Playwright 持久化浏览器中的 cookies 请求网页动态接口。若某些 UID 持续抓取失败,建议在外部 Chromium 或插件浏览器数据目录中登录常用 B 站账号,以降低风控概率。
|
|
121
122
|
|
|
122
123
|
## 🎉 使用
|
|
123
124
|
|
|
@@ -82,8 +82,9 @@ BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直
|
|
|
82
82
|
| BILILIVE_DYNAMIC_FONT | 否 | Noto Sans CJK SC | 截图字体 |
|
|
83
83
|
| BILILIVE_DYNAMIC_BIG_IMAGE | 否 | false | 是否优先展示大图 |
|
|
84
84
|
| BILILIVE_COMMAND_PREFIX | 否 | 空字符串 | 命令额外前缀 |
|
|
85
|
+
| BILILIVE_CHROMIUM_ENDPOINT | 否 | 无 | 外部 Chromium CDP 地址,如 `http://127.0.0.1:9222` |
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
动态抓取优先使用 `BILILIVE_CHROMIUM_ENDPOINT` 连接的外部 Chromium;未配置时使用 Playwright 持久化浏览器中的 cookies 请求网页动态接口。若某些 UID 持续抓取失败,建议在外部 Chromium 或插件浏览器数据目录中登录常用 B 站账号,以降低风控概率。
|
|
87
88
|
|
|
88
89
|
## 🎉 使用
|
|
89
90
|
|
{nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/config.py
RENAMED
|
@@ -21,6 +21,7 @@ class Config(BaseModel):
|
|
|
21
21
|
bililive_captcha_address: str = "https://captcha-cd.ngworks.cn"
|
|
22
22
|
bililive_captcha_token: str = "bililive"
|
|
23
23
|
bililive_browser_ua: str | None = None
|
|
24
|
+
bililive_chromium_endpoint: str | None = None
|
|
24
25
|
bililive_dynamic_timeout: int = 30
|
|
25
26
|
bililive_dynamic_font_source: str = "system"
|
|
26
27
|
bililive_dynamic_font: str | None = "Noto Sans CJK SC"
|
|
@@ -46,6 +47,7 @@ class Config(BaseModel):
|
|
|
46
47
|
"haruka_captcha_address": "bililive_captcha_address",
|
|
47
48
|
"haruka_captcha_token": "bililive_captcha_token",
|
|
48
49
|
"haruka_browser_ua": "bililive_browser_ua",
|
|
50
|
+
"haruka_chromium_endpoint": "bililive_chromium_endpoint",
|
|
49
51
|
"haruka_dynamic_timeout": "bililive_dynamic_timeout",
|
|
50
52
|
"haruka_dynamic_font_source": "bililive_dynamic_font_source",
|
|
51
53
|
"haruka_dynamic_font": "bililive_dynamic_font",
|
|
@@ -72,6 +74,14 @@ class Config(BaseModel):
|
|
|
72
74
|
def dynamic_interval_non_negative(cls, value: int):
|
|
73
75
|
return 0 if value < 1 else value
|
|
74
76
|
|
|
77
|
+
@field_validator("bililive_chromium_endpoint")
|
|
78
|
+
@classmethod
|
|
79
|
+
def chromium_endpoint(cls, value: str | None):
|
|
80
|
+
if value is None:
|
|
81
|
+
return None
|
|
82
|
+
value = value.strip()
|
|
83
|
+
return value or None
|
|
84
|
+
|
|
75
85
|
@field_validator("bililive_screenshot_style")
|
|
76
86
|
@classmethod
|
|
77
87
|
def screenshot_style(cls, value: str):
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import inspect
|
|
2
3
|
import json
|
|
4
|
+
from collections.abc import Callable
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
5
7
|
from nonebot import get_driver, logger
|
|
6
8
|
from packaging.version import Version as version_parser
|
|
7
9
|
from tortoise import Tortoise
|
|
8
10
|
|
|
11
|
+
get_current_context: Callable[[], object | None] | None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _detect_tortoise_v1() -> bool:
|
|
15
|
+
"""Tortoise 1.x 才有 context 模块与 _enable_global_fallback 参数。"""
|
|
16
|
+
try:
|
|
17
|
+
from tortoise.context import get_current_context as _gcc
|
|
18
|
+
except ImportError:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
global get_current_context
|
|
22
|
+
get_current_context = _gcc
|
|
23
|
+
params = inspect.signature(Tortoise.init).parameters
|
|
24
|
+
return "_enable_global_fallback" in params
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
get_current_context = None
|
|
28
|
+
_TORTOISE_V1 = _detect_tortoise_v1()
|
|
29
|
+
|
|
9
30
|
from ..utils import get_path
|
|
10
31
|
from ..version import VERSION as APP_VERSION
|
|
11
32
|
from .models import Group, Sub, User, Version
|
|
@@ -28,9 +49,11 @@ class DB:
|
|
|
28
49
|
@classmethod
|
|
29
50
|
def _orm_context_ok(cls) -> bool:
|
|
30
51
|
"""_ready 与 Tortoise 1.x 全局上下文需同时成立,否则会出现 No TortoiseContext。"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
if not cls._ready:
|
|
53
|
+
return False
|
|
54
|
+
if not _TORTOISE_V1:
|
|
55
|
+
return True
|
|
56
|
+
return get_current_context() is not None
|
|
34
57
|
|
|
35
58
|
@classmethod
|
|
36
59
|
async def _do_init(cls) -> None:
|
|
@@ -48,7 +71,16 @@ class DB:
|
|
|
48
71
|
},
|
|
49
72
|
}
|
|
50
73
|
|
|
51
|
-
|
|
74
|
+
init_kwargs = (
|
|
75
|
+
{"_enable_global_fallback": True} if _TORTOISE_V1 else {}
|
|
76
|
+
)
|
|
77
|
+
try:
|
|
78
|
+
await Tortoise.init(config, **init_kwargs)
|
|
79
|
+
except TypeError:
|
|
80
|
+
if init_kwargs:
|
|
81
|
+
await Tortoise.init(config)
|
|
82
|
+
else:
|
|
83
|
+
raise
|
|
52
84
|
|
|
53
85
|
await Tortoise.generate_schemas()
|
|
54
86
|
await cls.migrate()
|
|
@@ -73,9 +105,9 @@ class DB:
|
|
|
73
105
|
@classmethod
|
|
74
106
|
async def _recover_stale_orm(cls) -> None:
|
|
75
107
|
"""_ready 仍为 True 但 Tortoise 上下文已丢失时,关闭并重新初始化。"""
|
|
108
|
+
if not _TORTOISE_V1:
|
|
109
|
+
return
|
|
76
110
|
async with _db_init_lock:
|
|
77
|
-
from tortoise.context import get_current_context
|
|
78
|
-
|
|
79
111
|
if get_current_context() is not None:
|
|
80
112
|
return
|
|
81
113
|
if not cls._ready:
|
|
@@ -101,13 +133,10 @@ class DB:
|
|
|
101
133
|
while waited < timeout:
|
|
102
134
|
if cls._orm_context_ok():
|
|
103
135
|
return True
|
|
104
|
-
if cls._ready:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
await cls._recover_stale_orm()
|
|
109
|
-
if cls._orm_context_ok():
|
|
110
|
-
return True
|
|
136
|
+
if cls._ready and _TORTOISE_V1 and get_current_context() is None:
|
|
137
|
+
await cls._recover_stale_orm()
|
|
138
|
+
if cls._orm_context_ok():
|
|
139
|
+
return True
|
|
111
140
|
await asyncio.sleep(interval)
|
|
112
141
|
waited += interval
|
|
113
142
|
|
|
@@ -275,7 +304,19 @@ class DB:
|
|
|
275
304
|
@classmethod
|
|
276
305
|
async def set_sub(cls, conf, switch, **kwargs):
|
|
277
306
|
"""开关订阅设置"""
|
|
278
|
-
|
|
307
|
+
sub = await cls.get_sub(**kwargs)
|
|
308
|
+
if not sub:
|
|
309
|
+
return False
|
|
310
|
+
previous = getattr(sub, conf, None)
|
|
311
|
+
updated = await Sub.update(kwargs, **{conf: switch})
|
|
312
|
+
if not updated:
|
|
313
|
+
return False
|
|
314
|
+
if conf == "dynamic":
|
|
315
|
+
if switch and not previous:
|
|
316
|
+
dynamic_offset[int(kwargs["uid"])] = -1
|
|
317
|
+
await cls.save_dynamic_offsets()
|
|
318
|
+
await cls.update_uid_list()
|
|
319
|
+
return True
|
|
279
320
|
|
|
280
321
|
@classmethod
|
|
281
322
|
async def get_version(cls):
|
|
@@ -135,9 +135,7 @@ async def process_dynamic_uid(uid: int):
|
|
|
135
135
|
|
|
136
136
|
dynamic_risk_control_until.pop(uid, None)
|
|
137
137
|
|
|
138
|
-
if not dynamics:
|
|
139
|
-
if uid in offset and offset[uid] == -1: # 不记录会导致第一次发动态不推送
|
|
140
|
-
await db.set_dynamic_offset(uid, 0)
|
|
138
|
+
if not dynamics:
|
|
141
139
|
return
|
|
142
140
|
name = get_dynamic_author_name(dynamics[0], use_web_fallback)
|
|
143
141
|
|
|
@@ -298,14 +298,20 @@ def on_startup():
|
|
|
298
298
|
from .browser import check_playwright_env, install
|
|
299
299
|
|
|
300
300
|
check_proxy()
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
logger.warning(
|
|
306
|
-
"Playwright 运行环境不完整,已跳过启动时强校验;"
|
|
307
|
-
f"涉及截图/浏览器能力时可能不可用。错误:{err}"
|
|
301
|
+
if plugin_config.bililive_chromium_endpoint:
|
|
302
|
+
logger.info(
|
|
303
|
+
"已配置 BILILIVE_CHROMIUM_ENDPOINT,"
|
|
304
|
+
"将优先连接外部 Chromium,跳过内置 Chromium 安装"
|
|
308
305
|
)
|
|
306
|
+
else:
|
|
307
|
+
install()
|
|
308
|
+
try:
|
|
309
|
+
asyncio.get_event_loop().run_until_complete(check_playwright_env())
|
|
310
|
+
except ImportError as err:
|
|
311
|
+
logger.warning(
|
|
312
|
+
"Playwright 运行环境不完整,已跳过启动时强校验;"
|
|
313
|
+
f"涉及截图/浏览器能力时可能不可用。错误:{err}"
|
|
314
|
+
)
|
|
309
315
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
310
316
|
|
|
311
317
|
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from nonebot import logger
|
|
9
9
|
from playwright.__main__ import main
|
|
10
|
-
from playwright.async_api import BrowserContext, Page, async_playwright
|
|
10
|
+
from playwright.async_api import BrowserContext, Page, Playwright, async_playwright
|
|
11
11
|
|
|
12
12
|
from ..config import plugin_config
|
|
13
13
|
from ..utils import get_path
|
|
@@ -15,29 +15,81 @@ from .captcha_solver import CaptchaInfer
|
|
|
15
15
|
from .fonts_provider import fill_font
|
|
16
16
|
|
|
17
17
|
_browser: BrowserContext | None = None
|
|
18
|
+
_playwright: Playwright | None = None
|
|
18
19
|
mobile_js = Path(__file__).parent.joinpath("mobile.js")
|
|
19
20
|
WEB_DYNAMIC_URL = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space"
|
|
21
|
+
DEFAULT_MOBILE_USER_AGENT = (
|
|
22
|
+
"Mozilla/5.0 (Linux; Android 10; RMX1911) AppleWebKit/537.36 "
|
|
23
|
+
"(KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
|
|
24
|
+
)
|
|
25
|
+
DEFAULT_DESKTOP_USER_AGENT = (
|
|
26
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
27
|
+
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_user_agent() -> str:
|
|
32
|
+
if plugin_config.bililive_browser_ua:
|
|
33
|
+
return plugin_config.bililive_browser_ua
|
|
34
|
+
if plugin_config.bililive_screenshot_style.lower() == "mobile":
|
|
35
|
+
return DEFAULT_MOBILE_USER_AGENT
|
|
36
|
+
return DEFAULT_DESKTOP_USER_AGENT
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_dynamic_api_headers(uid: int) -> dict[str, str]:
|
|
40
|
+
return {
|
|
41
|
+
"accept": "application/json, text/plain, */*",
|
|
42
|
+
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
43
|
+
"referer": f"https://space.bilibili.com/{uid}/dynamic",
|
|
44
|
+
"origin": "https://space.bilibili.com",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _ensure_playwright() -> Playwright:
|
|
49
|
+
global _playwright
|
|
50
|
+
if _playwright is None:
|
|
51
|
+
_playwright = await async_playwright().start()
|
|
52
|
+
return _playwright
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def _apply_browser_headers(context: BrowserContext) -> None:
|
|
56
|
+
await context.set_extra_http_headers(
|
|
57
|
+
{
|
|
58
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
20
61
|
|
|
21
62
|
|
|
22
|
-
async def
|
|
23
|
-
logger.info("
|
|
63
|
+
async def init_browser_cdp(endpoint: str) -> BrowserContext:
|
|
64
|
+
logger.info(f"连接外部 Chromium:{endpoint}")
|
|
65
|
+
global _browser
|
|
66
|
+
playwright = await _ensure_playwright()
|
|
67
|
+
browser = await playwright.chromium.connect_over_cdp(endpoint)
|
|
68
|
+
if browser.contexts:
|
|
69
|
+
browser_context = browser.contexts[0]
|
|
70
|
+
else:
|
|
71
|
+
browser_context = await browser.new_context(
|
|
72
|
+
user_agent=get_user_agent(),
|
|
73
|
+
device_scale_factor=2,
|
|
74
|
+
)
|
|
75
|
+
await _apply_browser_headers(browser_context)
|
|
76
|
+
_browser = browser_context
|
|
77
|
+
return _browser
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def init_browser_playwright(
|
|
81
|
+
proxy=plugin_config.bililive_proxy, **kwargs
|
|
82
|
+
) -> BrowserContext:
|
|
83
|
+
logger.info("初始化 Playwright 内置浏览器")
|
|
24
84
|
if proxy:
|
|
25
85
|
kwargs["proxy"] = {"server": proxy}
|
|
26
86
|
global _browser
|
|
27
|
-
|
|
87
|
+
playwright = await _ensure_playwright()
|
|
28
88
|
browser_data = Path(get_path("browser"))
|
|
29
89
|
browser_data.mkdir(parents=True, exist_ok=True)
|
|
30
|
-
browser_context = await
|
|
90
|
+
browser_context = await playwright.chromium.launch_persistent_context(
|
|
31
91
|
browser_data,
|
|
32
|
-
user_agent=
|
|
33
|
-
or (
|
|
34
|
-
(
|
|
35
|
-
"Mozilla/5.0 (Linux; Android 10; RMX1911) AppleWebKit/537.36 "
|
|
36
|
-
"(KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
|
|
37
|
-
)
|
|
38
|
-
if plugin_config.bililive_screenshot_style.lower() == "mobile"
|
|
39
|
-
else None
|
|
40
|
-
),
|
|
92
|
+
user_agent=get_user_agent(),
|
|
41
93
|
device_scale_factor=2,
|
|
42
94
|
timeout=plugin_config.bililive_dynamic_timeout * 1000,
|
|
43
95
|
**kwargs,
|
|
@@ -53,10 +105,24 @@ async def init_browser(proxy=plugin_config.bililive_proxy, **kwargs) -> BrowserC
|
|
|
53
105
|
}
|
|
54
106
|
]
|
|
55
107
|
)
|
|
108
|
+
await _apply_browser_headers(browser_context)
|
|
56
109
|
_browser = browser_context
|
|
57
110
|
return _browser
|
|
58
111
|
|
|
59
112
|
|
|
113
|
+
async def init_browser(proxy=plugin_config.bililive_proxy, **kwargs) -> BrowserContext:
|
|
114
|
+
endpoint = plugin_config.bililive_chromium_endpoint
|
|
115
|
+
if endpoint:
|
|
116
|
+
try:
|
|
117
|
+
return await init_browser_cdp(endpoint)
|
|
118
|
+
except Exception as err:
|
|
119
|
+
logger.warning(
|
|
120
|
+
f"连接外部 Chromium 失败({endpoint}),"
|
|
121
|
+
f"将回退到 Playwright 内置浏览器:{err}"
|
|
122
|
+
)
|
|
123
|
+
return await init_browser_playwright(proxy=proxy, **kwargs)
|
|
124
|
+
|
|
125
|
+
|
|
60
126
|
async def get_browser() -> BrowserContext:
|
|
61
127
|
global _browser
|
|
62
128
|
if not _browser:
|
|
@@ -76,23 +142,28 @@ async def get_bilibili_cookies() -> dict[str, str]:
|
|
|
76
142
|
async def get_user_dynamics_payload_in_browser(uid: int) -> dict:
|
|
77
143
|
browser = await get_browser()
|
|
78
144
|
page = await browser.new_page()
|
|
145
|
+
api_headers = get_dynamic_api_headers(uid)
|
|
79
146
|
try:
|
|
147
|
+
await page.set_extra_http_headers(
|
|
148
|
+
{
|
|
149
|
+
**api_headers,
|
|
150
|
+
"User-Agent": get_user_agent(),
|
|
151
|
+
}
|
|
152
|
+
)
|
|
80
153
|
await page.goto(
|
|
81
154
|
"https://www.bilibili.com/",
|
|
82
155
|
wait_until="domcontentloaded",
|
|
83
156
|
timeout=plugin_config.bililive_dynamic_timeout * 1000,
|
|
84
157
|
)
|
|
85
158
|
return await page.evaluate(
|
|
86
|
-
"""async ({ url, uid }) => {
|
|
159
|
+
"""async ({ url, uid, headers }) => {
|
|
87
160
|
const response = await fetch(`${url}?host_mid=${uid}`, {
|
|
88
161
|
credentials: 'include',
|
|
89
|
-
headers
|
|
90
|
-
accept: 'application/json, text/plain, */*',
|
|
91
|
-
},
|
|
162
|
+
headers,
|
|
92
163
|
});
|
|
93
164
|
return await response.json();
|
|
94
165
|
}""",
|
|
95
|
-
{"url": WEB_DYNAMIC_URL, "uid": str(uid)},
|
|
166
|
+
{"url": WEB_DYNAMIC_URL, "uid": str(uid), "headers": api_headers},
|
|
96
167
|
)
|
|
97
168
|
finally:
|
|
98
169
|
with contextlib.suppress(Exception):
|
|
@@ -34,12 +34,12 @@ dependencies = [
|
|
|
34
34
|
"playwright>=1.58.0",
|
|
35
35
|
"pydantic>=1.10.0,<3.0",
|
|
36
36
|
"python-dotenv>=1.2.2",
|
|
37
|
-
"tortoise-orm[asyncpg]>=0.
|
|
37
|
+
"tortoise-orm[asyncpg]>=1.0.0,<2.0",
|
|
38
38
|
"packaging>=26.0",
|
|
39
39
|
"msvc-runtime>=14.34.31931; sys_platform == \"win32\"",
|
|
40
40
|
]
|
|
41
41
|
dynamic = []
|
|
42
|
-
version = "2.1.
|
|
42
|
+
version = "2.1.7"
|
|
43
43
|
|
|
44
44
|
[project.license]
|
|
45
45
|
text = "AGPL-3.0-or-later"
|
|
@@ -65,10 +65,12 @@ with patch("nonebot.get_driver", return_value=DummyDriver()), patch(
|
|
|
65
65
|
core_version = import_module("nonebot_plugin_bililive.version")
|
|
66
66
|
db_module = import_module("nonebot_plugin_bililive.database.db")
|
|
67
67
|
web_dynamic = import_module("nonebot_plugin_bililive.libs.dynamic.web")
|
|
68
|
+
browser_module = import_module("nonebot_plugin_bililive.utils.browser")
|
|
68
69
|
plugin_entry = import_module("nonebot_plugin_bililive")
|
|
69
70
|
DB = db_module.DB
|
|
70
71
|
models = import_module("nonebot_plugin_bililive.database.models")
|
|
71
72
|
Group = models.Group
|
|
73
|
+
Sub = models.Sub
|
|
72
74
|
get_path = import_module("nonebot_plugin_bililive.utils").get_path
|
|
73
75
|
|
|
74
76
|
|
|
@@ -102,6 +104,16 @@ class ConfigTests(unittest.TestCase):
|
|
|
102
104
|
self.assertEqual(config.bililive_live_interval, 12)
|
|
103
105
|
self.assertEqual(config.bililive_command_prefix, "hb")
|
|
104
106
|
|
|
107
|
+
def test_chromium_endpoint_is_trimmed(self):
|
|
108
|
+
config = Config(bililive_chromium_endpoint=" http://127.0.0.1:9222 ")
|
|
109
|
+
|
|
110
|
+
self.assertEqual(config.bililive_chromium_endpoint, "http://127.0.0.1:9222")
|
|
111
|
+
|
|
112
|
+
def test_legacy_haruka_chromium_endpoint_is_migrated(self):
|
|
113
|
+
config = Config(**{"haruka_chromium_endpoint": "http://127.0.0.1:9333"})
|
|
114
|
+
|
|
115
|
+
self.assertEqual(config.bililive_chromium_endpoint, "http://127.0.0.1:9333")
|
|
116
|
+
|
|
105
117
|
|
|
106
118
|
class PluginEntryTests(unittest.TestCase):
|
|
107
119
|
def test_plugin_entry_exposes_plugin_metadata(self):
|
|
@@ -131,6 +143,67 @@ class PluginEntryTests(unittest.TestCase):
|
|
|
131
143
|
self.assertNotIn('bilireq>=', pyproject)
|
|
132
144
|
|
|
133
145
|
|
|
146
|
+
class BrowserHelperTests(unittest.IsolatedAsyncioTestCase):
|
|
147
|
+
def test_get_dynamic_api_headers_include_space_referer(self):
|
|
148
|
+
headers = browser_module.get_dynamic_api_headers(477332594)
|
|
149
|
+
|
|
150
|
+
self.assertEqual(
|
|
151
|
+
headers["referer"],
|
|
152
|
+
"https://space.bilibili.com/477332594/dynamic",
|
|
153
|
+
)
|
|
154
|
+
self.assertEqual(headers["origin"], "https://space.bilibili.com")
|
|
155
|
+
self.assertIn("application/json", headers["accept"])
|
|
156
|
+
|
|
157
|
+
async def test_init_browser_prefers_external_chromium_when_configured(self):
|
|
158
|
+
cdp_context = object()
|
|
159
|
+
with (
|
|
160
|
+
patch.object(
|
|
161
|
+
browser_module.plugin_config,
|
|
162
|
+
"bililive_chromium_endpoint",
|
|
163
|
+
"http://127.0.0.1:9222",
|
|
164
|
+
),
|
|
165
|
+
patch.object(
|
|
166
|
+
browser_module,
|
|
167
|
+
"init_browser_cdp",
|
|
168
|
+
new=AsyncMock(return_value=cdp_context),
|
|
169
|
+
) as init_browser_cdp,
|
|
170
|
+
patch.object(
|
|
171
|
+
browser_module,
|
|
172
|
+
"init_browser_playwright",
|
|
173
|
+
new=AsyncMock(),
|
|
174
|
+
) as init_browser_playwright,
|
|
175
|
+
):
|
|
176
|
+
context = await browser_module.init_browser()
|
|
177
|
+
|
|
178
|
+
self.assertIs(context, cdp_context)
|
|
179
|
+
init_browser_cdp.assert_awaited_once_with("http://127.0.0.1:9222")
|
|
180
|
+
init_browser_playwright.assert_not_awaited()
|
|
181
|
+
|
|
182
|
+
async def test_init_browser_falls_back_to_playwright_when_cdp_fails(self):
|
|
183
|
+
playwright_context = object()
|
|
184
|
+
with (
|
|
185
|
+
patch.object(
|
|
186
|
+
browser_module.plugin_config,
|
|
187
|
+
"bililive_chromium_endpoint",
|
|
188
|
+
"http://127.0.0.1:9222",
|
|
189
|
+
),
|
|
190
|
+
patch.object(
|
|
191
|
+
browser_module,
|
|
192
|
+
"init_browser_cdp",
|
|
193
|
+
new=AsyncMock(side_effect=RuntimeError("connect failed")),
|
|
194
|
+
),
|
|
195
|
+
patch.object(
|
|
196
|
+
browser_module,
|
|
197
|
+
"init_browser_playwright",
|
|
198
|
+
new=AsyncMock(return_value=playwright_context),
|
|
199
|
+
) as init_browser_playwright,
|
|
200
|
+
):
|
|
201
|
+
context = await browser_module.init_browser()
|
|
202
|
+
|
|
203
|
+
self.assertIs(context, playwright_context)
|
|
204
|
+
init_browser_playwright.assert_awaited_once()
|
|
205
|
+
|
|
206
|
+
|
|
134
207
|
class WebDynamicTests(unittest.TestCase):
|
|
135
208
|
def test_parse_web_dynamic_items_extracts_required_fields(self):
|
|
136
209
|
payload = {
|
|
@@ -197,7 +270,14 @@ class DBPermissionTests(unittest.IsolatedAsyncioTestCase):
|
|
|
197
270
|
):
|
|
198
271
|
await DB.init()
|
|
199
272
|
|
|
200
|
-
|
|
273
|
+
if db_module._TORTOISE_V1:
|
|
274
|
+
self.assertTrue(
|
|
275
|
+
init_db.await_args.kwargs["_enable_global_fallback"]
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
self.assertNotIn(
|
|
279
|
+
"_enable_global_fallback", init_db.await_args.kwargs
|
|
280
|
+
)
|
|
201
281
|
self.assertTrue(DB._ready)
|
|
202
282
|
generate_schemas.assert_awaited_once()
|
|
203
283
|
migrate.assert_awaited_once()
|
|
@@ -261,6 +341,72 @@ class DBPermissionTests(unittest.IsolatedAsyncioTestCase):
|
|
|
261
341
|
self.assertFalse(changed)
|
|
262
342
|
update.assert_not_awaited()
|
|
263
343
|
|
|
344
|
+
async def test_set_sub_enabling_dynamic_resets_offset_and_updates_uid_list(self):
|
|
345
|
+
sub = SimpleNamespace(dynamic=False)
|
|
346
|
+
db_module.dynamic_offset.clear()
|
|
347
|
+
db_module.dynamic_offset[123] = 456
|
|
348
|
+
with (
|
|
349
|
+
patch.object(DB, "get_sub", new=AsyncMock(return_value=sub)),
|
|
350
|
+
patch.object(Sub, "update", new=AsyncMock(return_value=True)),
|
|
351
|
+
patch.object(DB, "update_uid_list", new=AsyncMock()) as update_uid_list,
|
|
352
|
+
patch.object(DB, "save_dynamic_offsets", new=AsyncMock()) as save_offsets,
|
|
353
|
+
):
|
|
354
|
+
updated = await DB.set_sub(
|
|
355
|
+
"dynamic",
|
|
356
|
+
True,
|
|
357
|
+
uid=123,
|
|
358
|
+
type="group",
|
|
359
|
+
type_id=456,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
self.assertTrue(updated)
|
|
363
|
+
self.assertEqual(db_module.dynamic_offset[123], -1)
|
|
364
|
+
save_offsets.assert_awaited_once()
|
|
365
|
+
update_uid_list.assert_awaited_once()
|
|
366
|
+
|
|
367
|
+
async def test_set_sub_enabling_dynamic_skips_reset_when_already_enabled(self):
|
|
368
|
+
sub = SimpleNamespace(dynamic=True)
|
|
369
|
+
db_module.dynamic_offset.clear()
|
|
370
|
+
db_module.dynamic_offset[123] = 456
|
|
371
|
+
with (
|
|
372
|
+
patch.object(DB, "get_sub", new=AsyncMock(return_value=sub)),
|
|
373
|
+
patch.object(Sub, "update", new=AsyncMock(return_value=True)),
|
|
374
|
+
patch.object(DB, "update_uid_list", new=AsyncMock()) as update_uid_list,
|
|
375
|
+
patch.object(DB, "save_dynamic_offsets", new=AsyncMock()) as save_offsets,
|
|
376
|
+
):
|
|
377
|
+
updated = await DB.set_sub(
|
|
378
|
+
"dynamic",
|
|
379
|
+
True,
|
|
380
|
+
uid=123,
|
|
381
|
+
type="group",
|
|
382
|
+
type_id=456,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
self.assertTrue(updated)
|
|
386
|
+
self.assertEqual(db_module.dynamic_offset[123], 456)
|
|
387
|
+
save_offsets.assert_not_awaited()
|
|
388
|
+
update_uid_list.assert_awaited_once()
|
|
389
|
+
|
|
390
|
+
async def test_set_sub_disabling_dynamic_updates_uid_list(self):
|
|
391
|
+
sub = SimpleNamespace(dynamic=True)
|
|
392
|
+
with (
|
|
393
|
+
patch.object(DB, "get_sub", new=AsyncMock(return_value=sub)),
|
|
394
|
+
patch.object(Sub, "update", new=AsyncMock(return_value=True)),
|
|
395
|
+
patch.object(DB, "update_uid_list", new=AsyncMock()) as update_uid_list,
|
|
396
|
+
patch.object(DB, "save_dynamic_offsets", new=AsyncMock()) as save_offsets,
|
|
397
|
+
):
|
|
398
|
+
updated = await DB.set_sub(
|
|
399
|
+
"dynamic",
|
|
400
|
+
False,
|
|
401
|
+
uid=123,
|
|
402
|
+
type="group",
|
|
403
|
+
type_id=456,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
self.assertTrue(updated)
|
|
407
|
+
save_offsets.assert_not_awaited()
|
|
408
|
+
update_uid_list.assert_awaited_once()
|
|
409
|
+
|
|
264
410
|
|
|
265
411
|
if __name__ == "__main__":
|
|
266
412
|
unittest.main()
|
|
File without changes
|
{nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/cli/bot.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.1.5 → nonebot_plugin_bililive-2.1.7}/nonebot_plugin_bililive/cli/utils.py
RENAMED
|
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
|