nonebot-plugin-parser 2.1.3__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.3 → nonebot_plugin_parser-2.2.0}/PKG-INFO +34 -23
  2. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/README.md +33 -22
  3. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/pyproject.toml +2 -2
  4. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/download/ytdlp.py +7 -7
  5. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/helper.py +48 -2
  6. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/matchers/__init__.py +21 -63
  7. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/matchers/rule.py +9 -1
  8. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/__init__.py +3 -4
  9. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/acfun.py +24 -30
  10. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/base.py +74 -25
  11. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +87 -107
  12. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/video.py +20 -2
  13. nonebot_plugin_parser-2.2.0/src/nonebot_plugin_parser/parsers/douyin/__init__.py +128 -0
  14. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/kuaishou.py +13 -14
  15. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/nga.py +8 -14
  16. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/tiktok.py +3 -7
  17. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/twitter.py +3 -7
  18. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/weibo.py +31 -30
  19. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/xiaohongshu.py +33 -33
  20. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/youtube.py +22 -26
  21. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/__init__.py +9 -9
  22. nonebot_plugin_parser-2.1.3/src/nonebot_plugin_parser/parsers/douyin/__init__.py +0 -174
  23. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/__init__.py +0 -0
  24. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/config.py +0 -0
  25. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/constants.py +0 -0
  26. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/download/__init__.py +0 -0
  27. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/download/task.py +0 -0
  28. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/exception.py +0 -0
  29. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  30. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  31. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  32. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
  33. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  34. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  35. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  36. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  37. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/data.py +0 -0
  38. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  39. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
  40. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/base.py +0 -0
  41. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/common.py +0 -0
  42. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/default.py +0 -0
  43. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW-1.ttf +0 -0
  44. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  45. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  46. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  47. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/media_button.png +0 -0
  48. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  49. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  50. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  51. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  52. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/youtube.png +0 -0
  53. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -0
  54. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/weibo.py +0 -0
  55. {nonebot_plugin_parser-2.1.3 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nonebot-plugin-parser
3
- Version: 2.1.3
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,9 +211,9 @@ parser_bili_ck="SESSDATA=xxxxxxxxxx;ac_time_value=131231241231241"
199
211
  # 后两项在不同设备可能有兼容性问题,如需完全避免,可只填一项,如 '["avc"]'
200
212
  parser_bili_video_codes='["avc", "av01", "hev"]'
201
213
 
202
- # B 站视频分辨率
214
+ # [可选] B 站视频清晰度
203
215
  # 360p(16), 480p(32), 720p(64), 1080p(80), 1080p+(112), 1080p_60(116), 4k(120)
204
- parser_bili_video_quality= 80
216
+ parser_bili_video_quality=80
205
217
 
206
218
  # [可选] Youtube Cookie, Youtube 视频因人机检测下载失败,需填
207
219
  parser_ytb_ck=""
@@ -265,7 +277,7 @@ parser_need_forward_contents=True
265
277
 
266
278
  ## 🧩 扩展
267
279
  > [!IMPORTANT]
268
- > 插件自 `v2.1.1` 版本开始支持自定义解析器,通过继承 `BaseParser` 类并实现 `platform`, `patterns`, `parse` 即可
280
+ > 插件自 `v2.2.0` 版本开始支持自定义解析器,通过继承 `BaseParser` 类并实现 `platform`, `handle` 即可
269
281
  <details>
270
282
  <summary>完整示例</summary>
271
283
 
@@ -278,7 +290,7 @@ from nonebot import require
278
290
 
279
291
  require("nonebot_plugin_parser")
280
292
  from nonebot_plugin_parser.parsers import BaseParser, ParseResult
281
- from nonebot_plugin_parser.parsers.base import Platform
293
+ from nonebot_plugin_parser.parsers.base import Platform, handle
282
294
 
283
295
 
284
296
  class ExampleParser(BaseParser):
@@ -286,21 +298,20 @@ class ExampleParser(BaseParser):
286
298
 
287
299
  platform: ClassVar[Platform] = Platform(name="example", display_name="示例网站")
288
300
 
289
- patterns: ClassVar[list[tuple[str, str]]] = [
290
- ("example.com", r"example\.com/video/(?P<video_id>\w+)"),
291
- ("ex.short", r"ex\.short/(?P<short_id>\w+)"),
292
- ]
293
-
294
- 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:
295
312
  # 1. 提取视频 ID
296
- if keyword == "ex.short":
297
- # 处理短链接
298
- short_id = searched.group("short_id")
299
- full_url = await self.get_redirect_url(f"https://ex.short/{short_id}")
300
- video_id = full_url.split("/")[-1]
301
- else:
302
- video_id = searched.group("video_id")
303
-
313
+ video_id = searched.group("video_id")
314
+
304
315
  # 2. 请求 API 获取视频信息
305
316
  async with AsyncClient(headers=self.headers, timeout=self.timeout) as client:
306
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,9 +165,9 @@ parser_bili_ck="SESSDATA=xxxxxxxxxx;ac_time_value=131231241231241"
153
165
  # 后两项在不同设备可能有兼容性问题,如需完全避免,可只填一项,如 '["avc"]'
154
166
  parser_bili_video_codes='["avc", "av01", "hev"]'
155
167
 
156
- # B 站视频分辨率
168
+ # [可选] B 站视频清晰度
157
169
  # 360p(16), 480p(32), 720p(64), 1080p(80), 1080p+(112), 1080p_60(116), 4k(120)
158
- parser_bili_video_quality= 80
170
+ parser_bili_video_quality=80
159
171
 
160
172
  # [可选] Youtube Cookie, Youtube 视频因人机检测下载失败,需填
161
173
  parser_ytb_ck=""
@@ -219,7 +231,7 @@ parser_need_forward_contents=True
219
231
 
220
232
  ## 🧩 扩展
221
233
  > [!IMPORTANT]
222
- > 插件自 `v2.1.1` 版本开始支持自定义解析器,通过继承 `BaseParser` 类并实现 `platform`, `patterns`, `parse` 即可
234
+ > 插件自 `v2.2.0` 版本开始支持自定义解析器,通过继承 `BaseParser` 类并实现 `platform`, `handle` 即可
223
235
  <details>
224
236
  <summary>完整示例</summary>
225
237
 
@@ -232,7 +244,7 @@ from nonebot import require
232
244
 
233
245
  require("nonebot_plugin_parser")
234
246
  from nonebot_plugin_parser.parsers import BaseParser, ParseResult
235
- from nonebot_plugin_parser.parsers.base import Platform
247
+ from nonebot_plugin_parser.parsers.base import Platform, handle
236
248
 
237
249
 
238
250
  class ExampleParser(BaseParser):
@@ -240,21 +252,20 @@ class ExampleParser(BaseParser):
240
252
 
241
253
  platform: ClassVar[Platform] = Platform(name="example", display_name="示例网站")
242
254
 
243
- patterns: ClassVar[list[tuple[str, str]]] = [
244
- ("example.com", r"example\.com/video/(?P<video_id>\w+)"),
245
- ("ex.short", r"ex\.short/(?P<short_id>\w+)"),
246
- ]
247
-
248
- 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:
249
266
  # 1. 提取视频 ID
250
- if keyword == "ex.short":
251
- # 处理短链接
252
- short_id = searched.group("short_id")
253
- full_url = await self.get_redirect_url(f"https://ex.short/{short_id}")
254
- video_id = full_url.split("/")[-1]
255
- else:
256
- video_id = searched.group("video_id")
257
-
267
+ video_id = searched.group("video_id")
268
+
258
269
  # 2. 请求 API 获取视频信息
259
270
  async with AsyncClient(headers=self.headers, timeout=self.timeout) as client:
260
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.3"
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,7 +86,7 @@ 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.3"
89
+ current_version = "2.2.0"
90
90
  commit = true
91
91
  message = "release: bump vesion from {current_version} to {new_version}"
92
92
  tag = true
@@ -45,9 +45,9 @@ 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
@@ -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("获取视频信息失败")
@@ -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
 
@@ -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
@@ -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
 
@@ -21,7 +21,12 @@ class SearchResult:
21
21
 
22
22
  __slots__ = ("keyword", "searched", "text")
23
23
 
24
- def __init__(self, text: str, keyword: str, searched: re.Match[str]):
24
+ def __init__(
25
+ self,
26
+ text: str,
27
+ keyword: str,
28
+ searched: re.Match[str],
29
+ ):
25
30
  self.text: str = text
26
31
  self.keyword: str = keyword
27
32
  self.searched: re.Match[str] = searched
@@ -110,6 +115,9 @@ class KeyPatternList(list[tuple[str, re.Pattern[str]]]):
110
115
  if isinstance(pattern, str):
111
116
  pattern = re.compile(pattern)
112
117
  self.append((key, pattern))
118
+ # 按 key 长 -> 短
119
+ self.sort(key=lambda x: -len(x[0]))
120
+ logger.debug(f"KeyWords: {[k for k, _ in self]}")
113
121
 
114
122
 
115
123
  class KeywordRegexRule:
@@ -1,6 +1,8 @@
1
1
  # 导出所有 Parser 类
2
- from .acfun import AcfunParser as AcfunParser # noqa: I001
2
+ from ..download import YTDLP_DOWNLOADER
3
+ from .acfun import AcfunParser as AcfunParser
3
4
  from .base import BaseParser as BaseParser
5
+ from .bilibili import BilibiliParser as BilibiliParser
4
6
  from .data import ParseResult as ParseResult
5
7
  from .douyin import DouyinParser as DouyinParser
6
8
  from .kuaishou import KuaiShouParser as KuaiShouParser
@@ -8,9 +10,6 @@ from .nga import NGAParser as NGAParser
8
10
  from .twitter import TwitterParser as TwitterParser
9
11
  from .weibo import WeiBoParser as WeiBoParser
10
12
  from .xiaohongshu import XiaoHongShuParser as XiaoHongShuParser
11
- from .bilibili import BilibiliParser as BilibiliParser
12
-
13
- from ..download import YTDLP_DOWNLOADER
14
13
 
15
14
  if YTDLP_DOWNLOADER is not None:
16
15
  from .tiktok import TikTokParser as TikTokParser
@@ -4,7 +4,6 @@ from pathlib import Path
4
4
  import re
5
5
  import time
6
6
  from typing import ClassVar
7
- from typing_extensions import override
8
7
 
9
8
  import aiofiles
10
9
  from httpx import AsyncClient, HTTPError
@@ -13,22 +12,40 @@ from nonebot import logger
13
12
  from ..constants import COMMON_TIMEOUT, DOWNLOAD_TIMEOUT
14
13
  from ..exception import DownloadException, ParseException
15
14
  from ..utils import safe_unlink
16
- from .base import DOWNLOADER, BaseParser, Platform, PlatformEnum, pconfig
15
+ from .base import DOWNLOADER, BaseParser, Platform, PlatformEnum, handle, pconfig
17
16
 
18
17
 
19
18
  class AcfunParser(BaseParser):
20
19
  # 平台信息
21
20
  platform: ClassVar[Platform] = Platform(name=PlatformEnum.ACFUN, display_name="猴山")
22
21
 
23
- # URL 正则表达式模式(keyword, pattern)
24
- patterns: ClassVar[list[tuple[str, str]]] = [
25
- ("acfun.cn", r"(?:ac=|/ac)(\d+)"),
26
- ]
27
-
28
22
  def __init__(self):
29
23
  super().__init__()
30
24
  self.headers["referer"] = "https://www.acfun.cn/"
31
25
 
26
+ @handle("acfun.cn", r"(?:ac=|/ac)(?P<acid>\d+)")
27
+ async def _parse(self, searched: re.Match[str]):
28
+ acid = int(searched.group("acid"))
29
+ url = f"https://www.acfun.cn/v/ac{acid}"
30
+
31
+ m3u8_url, title, description, author, upload_time = await self.parse_video_info(url)
32
+ author = self.create_author(author) if author else None
33
+
34
+ # 2024-12-1 -> timestamp
35
+ timestamp = int(time.mktime(time.strptime(upload_time, "%Y-%m-%d")))
36
+ text = f"简介: {description}"
37
+
38
+ # 下载视频
39
+ video_task = asyncio.create_task(self.download_video(m3u8_url, acid))
40
+
41
+ return self.result(
42
+ title=title,
43
+ text=text,
44
+ author=author,
45
+ timestamp=timestamp,
46
+ contents=[self.create_video_content(video_task)],
47
+ )
48
+
32
49
  async def parse_video_info(self, url: str) -> tuple[str, str, str, str, str]:
33
50
  """解析acfun链接获取详细信息
34
51
 
@@ -132,26 +149,3 @@ class AcfunParser(BaseParser):
132
149
  m3u8_full_urls = [f"{m3u8_prefix}/{d}" for d in m3u8_relative_links]
133
150
 
134
151
  return m3u8_full_urls
135
-
136
- @override
137
- async def parse(self, keyword: str, searched: re.Match[str]):
138
- acid = int(searched.group(1))
139
- url = f"https://www.acfun.cn/v/ac{acid}"
140
-
141
- m3u8_url, title, description, author, upload_time = await self.parse_video_info(url)
142
- author = self.create_author(author) if author else None
143
-
144
- # 2024-12-1 -> timestamp
145
- timestamp = int(time.mktime(time.strptime(upload_time, "%Y-%m-%d")))
146
- text = f"简介: {description}"
147
-
148
- # 下载视频
149
- video_task = asyncio.create_task(self.download_video(m3u8_url, acid))
150
-
151
- return self.result(
152
- title=title,
153
- text=text,
154
- author=author,
155
- timestamp=timestamp,
156
- contents=[self.create_video_content(video_task)],
157
- )