nonebot-plugin-parser 2.1.2__tar.gz → 2.2.0__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 (55) hide show
  1. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/PKG-INFO +39 -23
  2. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/README.md +38 -22
  3. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/pyproject.toml +3 -3
  4. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/config.py +13 -2
  5. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/download/__init__.py +12 -11
  6. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/download/ytdlp.py +10 -10
  7. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/helper.py +50 -4
  8. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/matchers/__init__.py +21 -63
  9. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/matchers/rule.py +9 -1
  10. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/__init__.py +3 -4
  11. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/acfun.py +24 -30
  12. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/base.py +74 -25
  13. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +88 -113
  14. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/video.py +21 -3
  15. nonebot_plugin_parser-2.2.0/src/nonebot_plugin_parser/parsers/douyin/__init__.py +128 -0
  16. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/kuaishou.py +13 -14
  17. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/nga.py +8 -14
  18. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/tiktok.py +3 -7
  19. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/twitter.py +3 -7
  20. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/weibo.py +31 -30
  21. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/xiaohongshu.py +33 -33
  22. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/youtube.py +22 -26
  23. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/__init__.py +9 -9
  24. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/common.py +218 -96
  25. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/utils.py +1 -1
  26. nonebot_plugin_parser-2.1.2/src/nonebot_plugin_parser/parsers/douyin/__init__.py +0 -174
  27. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/__init__.py +0 -0
  28. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/constants.py +0 -0
  29. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/download/task.py +0 -0
  30. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/exception.py +0 -0
  31. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  32. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  33. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  34. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
  35. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  36. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  37. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  38. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  39. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/data.py +0 -0
  40. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  41. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
  42. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/base.py +0 -0
  43. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/default.py +0 -0
  44. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW-1.ttf +0 -0
  45. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  46. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  47. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  48. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/media_button.png +0 -0
  49. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  50. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  51. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  52. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  53. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/youtube.png +0 -0
  54. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -0
  55. {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/weibo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nonebot-plugin-parser
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: NoneBot2 链接分享解析 Alconna 版, 通用媒体卡片渲染(PIL 实现), 支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga
5
5
  Keywords: acfun,bilibili,douyin,kuaishou,nga,nonebot,nonebot2,tiktok,twitter,video,weibo,xiaohongshu,youtube
6
6
  Author: fllesser
@@ -167,15 +167,27 @@ Description-Content-Type: text/markdown
167
167
 
168
168
  <details>
169
169
  <summary>安装必要组件</summary>
170
- 部分解析依赖于 ffmpeg
171
170
 
172
- ubuntu/debian
171
+ 部分解析依赖 `ffmpeg`
172
+
173
+ `ubuntu/debian`
173
174
 
174
175
  sudo apt-get install ffmpeg
175
176
 
176
- 其他 linux 参考(原项目推荐): https://gitee.com/baihu433/ffmpeg
177
+ 其他 `Linux` 参考(原项目推荐): https://gitee.com/baihu433/ffmpeg
178
+
179
+ `Windows` 参考(原项目推荐): https://www.jianshu.com/p/5015a477de3c
180
+
181
+ `yt-dlp` 自 `2025.11.12` 起要求用户安装外部 `JavaScript Runtime`,参考 https://github.com/yt-dlp/yt-dlp/releases/tag/2025.11.12, 推荐安装 [Deno](https://deno.com)
182
+
183
+ `macOS / Linux`
184
+
185
+ curl -fsSL https://deno.land/install.sh | sh
186
+
187
+ `windows`
188
+
189
+ irm https://deno.land/install.ps1 | iex
177
190
 
178
- Windows 参考(原项目推荐): https://www.jianshu.com/p/5015a477de3c
179
191
  </details>
180
192
 
181
193
  ## ⚙️ 配置
@@ -199,6 +211,10 @@ parser_bili_ck="SESSDATA=xxxxxxxxxx;ac_time_value=131231241231241"
199
211
  # 后两项在不同设备可能有兼容性问题,如需完全避免,可只填一项,如 '["avc"]'
200
212
  parser_bili_video_codes='["avc", "av01", "hev"]'
201
213
 
214
+ # [可选] B 站视频清晰度
215
+ # 360p(16), 480p(32), 720p(64), 1080p(80), 1080p+(112), 1080p_60(116), 4k(120)
216
+ parser_bili_video_quality=80
217
+
202
218
  # [可选] Youtube Cookie, Youtube 视频因人机检测下载失败,需填
203
219
  parser_ytb_ck=""
204
220
 
@@ -245,6 +261,7 @@ parser_need_forward_contents=True
245
261
 
246
262
  <details>
247
263
  <summary>推荐的字体</summary>
264
+
248
265
  - [LXGW ZhenKai / 霞鹜臻楷](https://github.com/lxgw/LxgwZhenKai) 效果图使用字体
249
266
  - [LXGW Neo XiHei / 霞鹜新晰黑](https://github.com/lxgw/LxgwNeoXiHei)
250
267
  - [LXGW Neo ZhiSong / 霞鹜新致宋 / 霞鶩新緻宋](https://github.com/lxgw/LxgwNeoZhiSong)
@@ -255,12 +272,12 @@ parser_need_forward_contents=True
255
272
  | :------: | :-------------------: | :---: | :---: | :---------------: |
256
273
  | 开启解析 | SUPERUSER/OWNER/ADMIN | 是 | 群聊 | 开启解析 |
257
274
  | 关闭解析 | SUPERUSER/OWNER/ADMIN | 是 | 群聊 | 关闭解析 |
258
- | bm | ALL | 否 | 群聊 | 下载 B 站音频 |
259
- | ym | ALL | 否 | 群聊 | 下载 youtube 音频 |
275
+ | bm | - | 否 | 群聊 | 下载 B 站音频 |
276
+ | ym | - | 否 | 群聊 | 下载 youtube 音频 |
260
277
 
261
278
  ## 🧩 扩展
262
279
  > [!IMPORTANT]
263
- > 插件自 `v2.1.1` 版本开始支持自定义解析器,通过继承 `BaseParser` 类并实现 `platform`, `patterns`, `parse` 即可
280
+ > 插件自 `v2.2.0` 版本开始支持自定义解析器,通过继承 `BaseParser` 类并实现 `platform`, `handle` 即可
264
281
  <details>
265
282
  <summary>完整示例</summary>
266
283
 
@@ -273,7 +290,7 @@ from nonebot import require
273
290
 
274
291
  require("nonebot_plugin_parser")
275
292
  from nonebot_plugin_parser.parsers import BaseParser, ParseResult
276
- from nonebot_plugin_parser.parsers.base import Platform
293
+ from nonebot_plugin_parser.parsers.base import Platform, handle
277
294
 
278
295
 
279
296
  class ExampleParser(BaseParser):
@@ -281,21 +298,20 @@ class ExampleParser(BaseParser):
281
298
 
282
299
  platform: ClassVar[Platform] = Platform(name="example", display_name="示例网站")
283
300
 
284
- patterns: ClassVar[list[tuple[str, str]]] = [
285
- ("example.com", r"example\.com/video/(?P<video_id>\w+)"),
286
- ("ex.short", r"ex\.short/(?P<short_id>\w+)"),
287
- ]
288
-
289
- async def parse(self, keyword: str, searched: Match[str]) -> ParseResult:
301
+ @handle("ex.short", r"ex\.short/\w+)")
302
+ async def _parse_short_link(self, searched: re.Match[str]):
303
+ """解析短链"""
304
+ url = f"https://{searched.group(0)}"
305
+ # 重定向再解析,请确保重定向链接的 handle 存在
306
+ # 比如 url 重定向到 example.com/... 就会调用 _parse 解析
307
+ return await self.parse_with_redirect(url)
308
+
309
+ @handle("example.com", r"example\.com/video/(?P<video_id>\w+)")
310
+ @handle("exam.ple", r"exam\.ple/(?P<video_id>\w+)")
311
+ async def _parse(self, searched: Match[str]) -> ParseResult:
290
312
  # 1. 提取视频 ID
291
- if keyword == "ex.short":
292
- # 处理短链接
293
- short_id = searched.group("short_id")
294
- full_url = await self.get_redirect_url(f"https://ex.short/{short_id}")
295
- video_id = full_url.split("/")[-1]
296
- else:
297
- video_id = searched.group("video_id")
298
-
313
+ video_id = searched.group("video_id")
314
+
299
315
  # 2. 请求 API 获取视频信息
300
316
  async with AsyncClient(headers=self.headers, timeout=self.timeout) as client:
301
317
  resp = await client.get(f"https://api.example.com/video/{video_id}")
@@ -121,15 +121,27 @@
121
121
 
122
122
  <details>
123
123
  <summary>安装必要组件</summary>
124
- 部分解析依赖于 ffmpeg
125
124
 
126
- ubuntu/debian
125
+ 部分解析依赖 `ffmpeg`
126
+
127
+ `ubuntu/debian`
127
128
 
128
129
  sudo apt-get install ffmpeg
129
130
 
130
- 其他 linux 参考(原项目推荐): https://gitee.com/baihu433/ffmpeg
131
+ 其他 `Linux` 参考(原项目推荐): https://gitee.com/baihu433/ffmpeg
132
+
133
+ `Windows` 参考(原项目推荐): https://www.jianshu.com/p/5015a477de3c
134
+
135
+ `yt-dlp` 自 `2025.11.12` 起要求用户安装外部 `JavaScript Runtime`,参考 https://github.com/yt-dlp/yt-dlp/releases/tag/2025.11.12, 推荐安装 [Deno](https://deno.com)
136
+
137
+ `macOS / Linux`
138
+
139
+ curl -fsSL https://deno.land/install.sh | sh
140
+
141
+ `windows`
142
+
143
+ irm https://deno.land/install.ps1 | iex
131
144
 
132
- Windows 参考(原项目推荐): https://www.jianshu.com/p/5015a477de3c
133
145
  </details>
134
146
 
135
147
  ## ⚙️ 配置
@@ -153,6 +165,10 @@ parser_bili_ck="SESSDATA=xxxxxxxxxx;ac_time_value=131231241231241"
153
165
  # 后两项在不同设备可能有兼容性问题,如需完全避免,可只填一项,如 '["avc"]'
154
166
  parser_bili_video_codes='["avc", "av01", "hev"]'
155
167
 
168
+ # [可选] B 站视频清晰度
169
+ # 360p(16), 480p(32), 720p(64), 1080p(80), 1080p+(112), 1080p_60(116), 4k(120)
170
+ parser_bili_video_quality=80
171
+
156
172
  # [可选] Youtube Cookie, Youtube 视频因人机检测下载失败,需填
157
173
  parser_ytb_ck=""
158
174
 
@@ -199,6 +215,7 @@ parser_need_forward_contents=True
199
215
 
200
216
  <details>
201
217
  <summary>推荐的字体</summary>
218
+
202
219
  - [LXGW ZhenKai / 霞鹜臻楷](https://github.com/lxgw/LxgwZhenKai) 效果图使用字体
203
220
  - [LXGW Neo XiHei / 霞鹜新晰黑](https://github.com/lxgw/LxgwNeoXiHei)
204
221
  - [LXGW Neo ZhiSong / 霞鹜新致宋 / 霞鶩新緻宋](https://github.com/lxgw/LxgwNeoZhiSong)
@@ -209,12 +226,12 @@ parser_need_forward_contents=True
209
226
  | :------: | :-------------------: | :---: | :---: | :---------------: |
210
227
  | 开启解析 | SUPERUSER/OWNER/ADMIN | 是 | 群聊 | 开启解析 |
211
228
  | 关闭解析 | SUPERUSER/OWNER/ADMIN | 是 | 群聊 | 关闭解析 |
212
- | bm | ALL | 否 | 群聊 | 下载 B 站音频 |
213
- | ym | ALL | 否 | 群聊 | 下载 youtube 音频 |
229
+ | bm | - | 否 | 群聊 | 下载 B 站音频 |
230
+ | ym | - | 否 | 群聊 | 下载 youtube 音频 |
214
231
 
215
232
  ## 🧩 扩展
216
233
  > [!IMPORTANT]
217
- > 插件自 `v2.1.1` 版本开始支持自定义解析器,通过继承 `BaseParser` 类并实现 `platform`, `patterns`, `parse` 即可
234
+ > 插件自 `v2.2.0` 版本开始支持自定义解析器,通过继承 `BaseParser` 类并实现 `platform`, `handle` 即可
218
235
  <details>
219
236
  <summary>完整示例</summary>
220
237
 
@@ -227,7 +244,7 @@ from nonebot import require
227
244
 
228
245
  require("nonebot_plugin_parser")
229
246
  from nonebot_plugin_parser.parsers import BaseParser, ParseResult
230
- from nonebot_plugin_parser.parsers.base import Platform
247
+ from nonebot_plugin_parser.parsers.base import Platform, handle
231
248
 
232
249
 
233
250
  class ExampleParser(BaseParser):
@@ -235,21 +252,20 @@ class ExampleParser(BaseParser):
235
252
 
236
253
  platform: ClassVar[Platform] = Platform(name="example", display_name="示例网站")
237
254
 
238
- patterns: ClassVar[list[tuple[str, str]]] = [
239
- ("example.com", r"example\.com/video/(?P<video_id>\w+)"),
240
- ("ex.short", r"ex\.short/(?P<short_id>\w+)"),
241
- ]
242
-
243
- async def parse(self, keyword: str, searched: Match[str]) -> ParseResult:
255
+ @handle("ex.short", r"ex\.short/\w+)")
256
+ async def _parse_short_link(self, searched: re.Match[str]):
257
+ """解析短链"""
258
+ url = f"https://{searched.group(0)}"
259
+ # 重定向再解析,请确保重定向链接的 handle 存在
260
+ # 比如 url 重定向到 example.com/... 就会调用 _parse 解析
261
+ return await self.parse_with_redirect(url)
262
+
263
+ @handle("example.com", r"example\.com/video/(?P<video_id>\w+)")
264
+ @handle("exam.ple", r"exam\.ple/(?P<video_id>\w+)")
265
+ async def _parse(self, searched: Match[str]) -> ParseResult:
244
266
  # 1. 提取视频 ID
245
- if keyword == "ex.short":
246
- # 处理短链接
247
- short_id = searched.group("short_id")
248
- full_url = await self.get_redirect_url(f"https://ex.short/{short_id}")
249
- video_id = full_url.split("/")[-1]
250
- else:
251
- video_id = searched.group("video_id")
252
-
267
+ video_id = searched.group("video_id")
268
+
253
269
  # 2. 请求 API 获取视频信息
254
270
  async with AsyncClient(headers=self.headers, timeout=self.timeout) as client:
255
271
  resp = await client.get(f"https://api.example.com/video/{video_id}")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parser"
3
- version = "2.1.2"
3
+ version = "2.2.0"
4
4
  description = "NoneBot2 链接分享解析 Alconna 版, 通用媒体卡片渲染(PIL 实现), 支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -86,9 +86,9 @@ requires = ["uv_build>=0.9.0,<0.10.0"]
86
86
  build-backend = "uv_build"
87
87
 
88
88
  [tool.bumpversion]
89
- current_version = "2.1.2"
89
+ current_version = "2.2.0"
90
90
  commit = true
91
- message = "🔖 release: bump vesion from {current_version} to {new_version}"
91
+ message = "release: bump vesion from {current_version} to {new_version}"
92
92
  tag = true
93
93
 
94
94
  [[tool.bumpversion.files]]
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
  from pathlib import Path
3
3
 
4
- from bilibili_api.video import VideoCodecs
4
+ from bilibili_api.video import VideoCodecs, VideoQuality
5
5
  from nonebot import get_driver, get_plugin_config, require
6
6
  from pydantic import BaseModel
7
7
 
@@ -43,8 +43,14 @@ class Config(BaseModel):
43
43
  """是否在解析结果中附加原始URL"""
44
44
  parser_disabled_platforms: list[PlatformEnum] = []
45
45
  """禁止的解析器"""
46
- parser_bili_video_codes: list[VideoCodecs] = [VideoCodecs.AVC, VideoCodecs.AV1, VideoCodecs.HEV]
46
+ parser_bili_video_codes: list[VideoCodecs] = [
47
+ VideoCodecs.AVC,
48
+ VideoCodecs.AV1,
49
+ VideoCodecs.HEV,
50
+ ]
47
51
  """B站视频编码"""
52
+ parser_bili_video_quality: VideoQuality = VideoQuality._1080P
53
+ """B站视频分辨率"""
48
54
  parser_render_type: RenderType = RenderType.common
49
55
  """Renderer 类型"""
50
56
  parser_custom_font: str | None = None
@@ -92,6 +98,11 @@ class Config(BaseModel):
92
98
  """B站视频编码"""
93
99
  return self.parser_bili_video_codes
94
100
 
101
+ @property
102
+ def bili_video_quality(self) -> VideoQuality:
103
+ """B站视频分辨率"""
104
+ return self.parser_bili_video_quality
105
+
95
106
  @property
96
107
  def render_type(self) -> RenderType:
97
108
  """Renderer 类型"""
@@ -33,8 +33,8 @@ class StreamDownloader:
33
33
 
34
34
  Args:
35
35
  url (str): url address
36
- file_name (str | None, optional): file name. Defaults to get name by parse_url_resource_name.
37
- ext_headers (dict[str, str] | None, optional): ext headers. Defaults to None.
36
+ file_name (str | None): file name. Defaults to generate_file_name.
37
+ ext_headers (dict[str, str] | None): ext headers. Defaults to None.
38
38
 
39
39
  Returns:
40
40
  Path: file path
@@ -84,7 +84,7 @@ class StreamDownloader:
84
84
 
85
85
  Args:
86
86
  desc (str): 描述
87
- total (int | None, optional): 总大小. Defaults to None.
87
+ total (int | None): 总大小. Defaults to None.
88
88
 
89
89
  Returns:
90
90
  tqdm: 进度条
@@ -111,8 +111,8 @@ class StreamDownloader:
111
111
 
112
112
  Args:
113
113
  url (str): url address
114
- video_name (str | None, optional): video name. Defaults to get name by parse url.
115
- ext_headers (dict[str, str] | None, optional): ext headers. Defaults to None.
114
+ video_name (str | None): video name. Defaults to get name by parse url.
115
+ ext_headers (dict[str, str] | None): ext headers. Defaults to None.
116
116
 
117
117
  Returns:
118
118
  Path: video file path
@@ -136,8 +136,8 @@ class StreamDownloader:
136
136
 
137
137
  Args:
138
138
  url (str): url address
139
- audio_name (str | None, optional): audio name. Defaults to get name by parse_url_resource_name.
140
- ext_headers (dict[str, str] | None, optional): ext headers. Defaults to None.
139
+ audio_name (str | None ): audio name. Defaults to get name by parse_url_resource_name.
140
+ ext_headers (dict[str, str] | None): ext headers. Defaults to None.
141
141
 
142
142
  Returns:
143
143
  Path: audio file path
@@ -161,8 +161,8 @@ class StreamDownloader:
161
161
 
162
162
  Args:
163
163
  url (str): url
164
- img_name (str, optional): image name. Defaults to None.
165
- ext_headers (dict[str, str], optional): ext headers. Defaults to None.
164
+ img_name (str | None): image name. Defaults to None.
165
+ ext_headers (dict[str, str] | None): ext headers. Defaults to None.
166
166
 
167
167
  Returns:
168
168
  Path: image file path
@@ -184,13 +184,14 @@ class StreamDownloader:
184
184
 
185
185
  Args:
186
186
  urls (list[str]): urls
187
- ext_headers (dict[str, str] | None, optional): ext headers. Defaults to None.
187
+ ext_headers (dict[str, str] | None): ext headers. Defaults to None.
188
188
 
189
189
  Returns:
190
190
  list[Path]: image file paths
191
191
  """
192
192
  paths_or_errs = await asyncio.gather(
193
- *[self.download_img(url, ext_headers=ext_headers) for url in urls], return_exceptions=True
193
+ *[self.download_img(url, ext_headers=ext_headers) for url in urls],
194
+ return_exceptions=True,
194
195
  )
195
196
  return [p for p in paths_or_errs if isinstance(p, Path)]
196
197
 
@@ -45,16 +45,16 @@ class YtdlpDownloader:
45
45
  "force_generic_extractor": True,
46
46
  }
47
47
  self._ydl_download_base_opts: dict[str, Any] = {}
48
- if pconfig.proxy is not None:
49
- self._ydl_download_base_opts["proxy"] = pconfig.proxy
50
- self._ydl_extract_base_opts["proxy"] = pconfig.proxy
48
+ if proxy := pconfig.proxy:
49
+ self._ydl_download_base_opts["proxy"] = proxy
50
+ self._ydl_extract_base_opts["proxy"] = proxy
51
51
 
52
52
  async def extract_video_info(self, url: str, cookiefile: Path | None = None) -> VideoInfo:
53
53
  """get video info by url
54
54
 
55
55
  Args:
56
56
  url (str): url address
57
- cookiefile (Path | None, optional): cookie file path. Defaults to None.
57
+ cookiefile (Path | None ): cookie file path. Defaults to None.
58
58
 
59
59
  Returns:
60
60
  dict[str, str]: video info
@@ -62,12 +62,12 @@ class YtdlpDownloader:
62
62
  video_info = self._video_info_mapping.get(url, None)
63
63
  if video_info:
64
64
  return video_info
65
- ydl_opts = {} | self._ydl_extract_base_opts
65
+ ydl_opts = self._ydl_extract_base_opts.copy()
66
66
 
67
67
  if cookiefile:
68
68
  ydl_opts["cookiefile"] = str(cookiefile)
69
69
 
70
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
70
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl: # pyright: ignore[reportArgumentType]
71
71
  info_dict = await asyncio.to_thread(ydl.extract_info, url, download=False)
72
72
  if not info_dict:
73
73
  raise ParseException("获取视频信息失败")
@@ -82,7 +82,7 @@ class YtdlpDownloader:
82
82
 
83
83
  Args:
84
84
  url (str): url address
85
- cookiefile (Path | None, optional): cookie file path. Defaults to None.
85
+ cookiefile (Path | None): cookie file path. Defaults to None.
86
86
 
87
87
  Returns:
88
88
  Path: video file path
@@ -105,7 +105,7 @@ class YtdlpDownloader:
105
105
  if cookiefile:
106
106
  ydl_opts["cookiefile"] = str(cookiefile)
107
107
 
108
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
108
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl: # pyright: ignore[reportArgumentType]
109
109
  await asyncio.to_thread(ydl.download, [url])
110
110
  return video_path
111
111
 
@@ -115,7 +115,7 @@ class YtdlpDownloader:
115
115
 
116
116
  Args:
117
117
  url (str): url address
118
- cookiefile (Path | None, optional): cookie file path. Defaults to None.
118
+ cookiefile (Path | None): cookie file path. Defaults to None.
119
119
 
120
120
  Returns:
121
121
  Path: audio file path
@@ -139,6 +139,6 @@ class YtdlpDownloader:
139
139
 
140
140
  if cookiefile:
141
141
  ydl_opts["cookiefile"] = str(cookiefile)
142
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
142
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl: # pyright: ignore[reportArgumentType]
143
143
  await asyncio.to_thread(ydl.download, [url])
144
144
  return audio_path
@@ -1,9 +1,14 @@
1
1
  from collections.abc import Sequence
2
+ from functools import wraps
2
3
  from pathlib import Path
4
+ from typing import Literal
3
5
 
6
+ from nonebot import logger
7
+ from nonebot.adapters import Event
4
8
  from nonebot.internal.matcher import current_bot
5
- from nonebot_plugin_alconna import File, Image, Text, Video
6
- from nonebot_plugin_alconna.uniseg import Segment, UniMessage, Voice
9
+ from nonebot.matcher import current_event
10
+ from nonebot_plugin_alconna import File, Image, Text, Video, uniseg
11
+ from nonebot_plugin_alconna.uniseg import Segment, SupportAdapter, UniMessage, Voice
7
12
  from nonebot_plugin_alconna.uniseg.segment import CustomNode, Reference
8
13
 
9
14
  from .config import pconfig
@@ -97,12 +102,12 @@ class UniHelper:
97
102
  return Video(path=video_path)
98
103
 
99
104
  @staticmethod
100
- def file_seg(file: Path, display_name: str = "") -> File:
105
+ def file_seg(file: Path, display_name: str | None = None) -> File:
101
106
  """获取文件 Seg
102
107
 
103
108
  Args:
104
109
  file (Path): 文件路径
105
- display_name (str, optional): 显示名称. Defaults to file.name.
110
+ display_name (str): 显示名称. Defaults to file.name.
106
111
 
107
112
  Returns:
108
113
  File: 文件 Seg
@@ -115,3 +120,44 @@ class UniHelper:
115
120
  return File(raw=file.read_bytes(), name=display_name)
116
121
  else:
117
122
  return File(path=file, name=display_name)
123
+
124
+ @staticmethod
125
+ async def message_reaction(
126
+ event: Event,
127
+ status: Literal["fail", "resolving", "done"],
128
+ ) -> None:
129
+ emoji_map = {
130
+ "fail": ("10060", "❌"),
131
+ "resolving": ("424", "👀"),
132
+ "done": ("144", "🎉"),
133
+ }
134
+ message_id = uniseg.get_message_id(event)
135
+ target = uniseg.get_target(event)
136
+
137
+ if target.adapter in (SupportAdapter.onebot11, SupportAdapter.qq):
138
+ emoji = emoji_map[status][0]
139
+ else:
140
+ emoji = emoji_map[status][1]
141
+
142
+ try:
143
+ await uniseg.message_reaction(emoji, message_id=message_id)
144
+ except Exception:
145
+ logger.warning(f"reaction {emoji} to {message_id} failed, maybe not support")
146
+
147
+ @staticmethod
148
+ def exception_handler(func):
149
+ @wraps(func)
150
+ async def wrapper(*args, **kwargs):
151
+ event = current_event.get()
152
+ await UniHelper.message_reaction(event, "resolving")
153
+
154
+ try:
155
+ result = await func(*args, **kwargs)
156
+ except Exception:
157
+ await UniHelper.message_reaction(event, "fail")
158
+ raise
159
+
160
+ await UniHelper.message_reaction(event, "done")
161
+ return result
162
+
163
+ return wrapper
@@ -1,12 +1,9 @@
1
1
  """统一的解析器 matcher"""
2
2
 
3
- from typing import Literal
4
-
5
3
  from nonebot import get_driver, logger
6
- from nonebot.adapters import Event
7
- from nonebot_plugin_alconna import SupportAdapter
8
4
 
9
5
  from ..config import pconfig
6
+ from ..helper import UniHelper
10
7
  from ..parsers import BaseParser, ParseResult
11
8
  from ..renders import get_renderer
12
9
  from ..utils import LimitedSizeDict
@@ -25,17 +22,17 @@ KEYWORD_PARSER_MAP: dict[str, BaseParser] = {}
25
22
 
26
23
  @get_driver().on_startup
27
24
  def register_parser_matcher():
28
- enabled_parser_classes = _get_enabled_parser_classes()
25
+ enabled_classes = _get_enabled_parser_classes()
29
26
 
30
- enabled_platform_names = []
31
- for _cls in enabled_parser_classes:
27
+ enabled_platforms = []
28
+ for _cls in enabled_classes:
32
29
  parser = _cls()
33
- enabled_platform_names.append(parser.platform.display_name)
34
- for keyword, _ in _cls.patterns:
30
+ enabled_platforms.append(parser.platform.display_name)
31
+ for keyword, _ in _cls._key_patterns:
35
32
  KEYWORD_PARSER_MAP[keyword] = parser
36
- logger.info(f"启用平台: {', '.join(sorted(enabled_platform_names))}")
33
+ logger.info(f"启用平台: {', '.join(sorted(enabled_platforms))}")
37
34
 
38
- patterns = [p for _cls in enabled_parser_classes for p in _cls.patterns]
35
+ patterns = [p for _cls in enabled_classes for p in _cls._key_patterns]
39
36
  matcher = on_keyword_regex(*patterns)
40
37
  matcher.append_handler(parser_handler)
41
38
 
@@ -48,14 +45,11 @@ def clear_result_cache():
48
45
  _RESULT_CACHE.clear()
49
46
 
50
47
 
48
+ @UniHelper.exception_handler
51
49
  async def parser_handler(
52
- event: Event,
53
50
  sr: SearchResult = Searched(),
54
51
  ):
55
52
  """统一的解析处理器"""
56
- # 响应用户处理中
57
- await _message_reaction(event, "resolving")
58
-
59
53
  # 1. 获取缓存结果
60
54
  cache_key = sr.searched.group(0)
61
55
  result = _RESULT_CACHE.get(cache_key)
@@ -63,58 +57,19 @@ async def parser_handler(
63
57
  if result is None:
64
58
  # 2. 获取对应平台 parser
65
59
  parser = KEYWORD_PARSER_MAP[sr.keyword]
66
-
67
- try:
68
- result = await parser.parse(sr.keyword, sr.searched)
69
- except Exception:
70
- # await UniMessage(str(e)).send()
71
- await _message_reaction(event, "fail")
72
- raise
60
+ result = await parser.parse(sr.keyword, sr.searched)
73
61
  logger.debug(f"解析结果: {result}")
74
62
  else:
75
63
  logger.debug(f"命中缓存: {cache_key}, 结果: {result}")
76
64
 
77
65
  # 3. 渲染内容消息并发送
78
- try:
79
- renderer = get_renderer(result.platform.name)
80
- async for message in renderer.render_messages(result):
81
- await message.send()
82
- except Exception:
83
- await _message_reaction(event, "fail")
84
- raise
66
+ renderer = get_renderer(result.platform.name)
67
+ async for message in renderer.render_messages(result):
68
+ await message.send()
85
69
 
86
70
  # 4. 无 raise 再缓存解析结果
87
71
  _RESULT_CACHE[cache_key] = result
88
72
 
89
- # 5. 添加成功的消息响应
90
- await _message_reaction(event, "done")
91
-
92
-
93
- from nonebot_plugin_alconna import uniseg
94
-
95
-
96
- async def _message_reaction(
97
- event: Event,
98
- status: Literal["fail", "resolving", "done"],
99
- ) -> None:
100
- emoji_map = {
101
- "fail": ("10060", "❌"),
102
- "resolving": ("424", "👀"),
103
- "done": ("144", "🎉"),
104
- }
105
- message_id = uniseg.get_message_id(event)
106
- target = uniseg.get_target(event)
107
-
108
- if target.adapter in (SupportAdapter.onebot11, SupportAdapter.qq):
109
- emoji = emoji_map[status][0]
110
- else:
111
- emoji = emoji_map[status][1]
112
-
113
- try:
114
- await uniseg.message_reaction(emoji, message_id=message_id)
115
- except Exception:
116
- logger.warning(f"reaction {emoji} to {message_id} failed, maybe not support")
117
-
118
73
 
119
74
  import re
120
75
  from typing import cast
@@ -125,11 +80,11 @@ from nonebot.params import CommandArg
125
80
  from nonebot_plugin_alconna import UniMessage
126
81
 
127
82
  from ..download import DOWNLOADER
128
- from ..helper import UniHelper
129
83
  from ..parsers import BilibiliParser
130
84
 
131
85
 
132
86
  @on_command("bm", priority=3, block=True).handle()
87
+ @UniHelper.exception_handler
133
88
  async def _(message: Message = CommandArg()):
134
89
  text = message.extract_plain_text()
135
90
  matched = re.search(r"(BV[A-Za-z0-9]{10})(\s\d{1,3})?", text)
@@ -139,14 +94,15 @@ async def _(message: Message = CommandArg()):
139
94
  bvid, page_num = matched.group(1), matched.group(2)
140
95
  page_idx = int(page_num) if page_num else 0
141
96
 
142
- bili_parser = KEYWORD_PARSER_MAP["BV"]
143
- bili_parser = cast(BilibiliParser, bili_parser)
144
- _, audio_url = await bili_parser.get_download_urls(bvid=bvid, page_index=page_idx)
97
+ parser = KEYWORD_PARSER_MAP["BV"]
98
+ parser = cast(BilibiliParser, parser)
99
+
100
+ _, audio_url = await parser.extract_download_urls(bvid=bvid, page_index=page_idx)
145
101
  if not audio_url:
146
102
  await UniMessage("未找到可下载的音频").finish()
147
103
 
148
104
  audio_path = await DOWNLOADER.download_audio(
149
- audio_url, audio_name=f"{bvid}-{page_idx}.mp3", ext_headers=bili_parser.headers
105
+ audio_url, audio_name=f"{bvid}-{page_idx}.mp3", ext_headers=parser.headers
150
106
  )
151
107
  await UniMessage(UniHelper.record_seg(audio_path)).send()
152
108
 
@@ -160,6 +116,7 @@ if YTDLP_DOWNLOADER is not None:
160
116
  from ..parsers import YouTubeParser
161
117
 
162
118
  @on_command("ym", priority=3, block=True).handle()
119
+ @UniHelper.exception_handler
163
120
  async def _(message: Message = CommandArg()):
164
121
  text = message.extract_plain_text()
165
122
  ytb_parser = cast(YouTubeParser, KEYWORD_PARSER_MAP["youtu.be"])
@@ -168,6 +125,7 @@ if YTDLP_DOWNLOADER is not None:
168
125
  await UniMessage("请发送正确的 youtube 链接").finish()
169
126
 
170
127
  url = matched.group(0)
128
+
171
129
  audio_path = await YTDLP_DOWNLOADER.download_audio(url)
172
130
  await UniMessage(UniHelper.record_seg(audio_path)).send()
173
131