nonebot-plugin-parser 2.2.0__tar.gz → 2.2.2__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 (54) hide show
  1. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/PKG-INFO +18 -19
  2. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/README.md +16 -18
  3. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/pyproject.toml +6 -3
  4. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/constants.py +12 -6
  5. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/exception.py +12 -6
  6. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/helper.py +64 -23
  7. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/matchers/__init__.py +5 -5
  8. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/__init__.py +18 -1
  9. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/acfun.py +2 -2
  10. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/base.py +32 -6
  11. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +29 -27
  12. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +5 -2
  13. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/weibo.py +99 -10
  14. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/base.py +4 -2
  15. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/common.py +57 -50
  16. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/__init__.py +0 -0
  17. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/config.py +0 -0
  18. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/download/__init__.py +0 -0
  19. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/download/task.py +0 -0
  20. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  21. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  22. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/matchers/rule.py +0 -0
  23. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  24. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  25. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
  26. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  27. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  28. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  29. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  30. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  31. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/data.py +0 -0
  32. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  33. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
  34. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/kuaishou.py +0 -0
  35. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
  36. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/tiktok.py +0 -0
  37. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/twitter.py +0 -0
  38. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/xiaohongshu.py +0 -0
  39. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/parsers/youtube.py +0 -0
  40. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/__init__.py +0 -0
  41. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/default.py +0 -0
  42. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW-1.ttf +0 -0
  43. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  44. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  45. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  46. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/media_button.png +0 -0
  47. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  48. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  49. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  50. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  51. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/resources/youtube.png +0 -0
  52. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -0
  53. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/src/nonebot_plugin_parser/renders/weibo.py +0 -0
  54. {nonebot_plugin_parser-2.2.0 → nonebot_plugin_parser-2.2.2}/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.2.0
3
+ Version: 2.2.2
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
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
17
18
  Classifier: Topic :: Communications :: Chat
18
19
  Classifier: Topic :: Internet :: WWW/HTTP
19
20
  Classifier: Topic :: Multimedia :: Video
@@ -22,7 +23,7 @@ Requires-Dist: beautifulsoup4>=4.12.0,<5.0.0
22
23
  Requires-Dist: bilibili-api-python>=17.4.0,<18.0.0
23
24
  Requires-Dist: curl-cffi>=0.13.0,<1.0.0
24
25
  Requires-Dist: httpx>=0.27.2,<1.0.0
25
- Requires-Dist: msgspec>=0.19.0,<1.0.0
26
+ Requires-Dist: msgspec>=0.20.0,<1.0.0
26
27
  Requires-Dist: nonebot-plugin-alconna>=0.59.4
27
28
  Requires-Dist: nonebot-plugin-apscheduler>=0.5.0,<1.0.0
28
29
  Requires-Dist: nonebot-plugin-localstore>=0.7.4,<1.0.0
@@ -68,19 +69,19 @@ Description-Content-Type: text/markdown
68
69
 
69
70
  ## 📖 介绍
70
71
 
71
- | 平台 | 触发的消息形态 | 视频 | 图集 | 音频 |
72
- | ------- | ------------------------------------- | ---- | ---- | ---- |
73
- | B站 | BV号/链接(包含短链,BV,av)/卡片/小程序 | ✅​ | ✅​ | ✅​ |
74
- | 抖音 | 链接(分享链接,兼容电脑端链接) | ✅​ | ✅​ | ❌️ |
75
- | 微博 | 链接(博文,视频,show) | ✅​ | ✅​ | ❌️ |
76
- | 小红书 | 链接(含短链)/卡片 | ✅​ | ✅​ | ❌️ |
77
- | 快手 | 链接(包含标准链接和短链) | ✅​ | ✅​ | ❌️ |
78
- | acfun | 链接 | ✅​ | ❌️ | ❌️ |
79
- | youtube | 链接(含短链) | ✅​ | ❌️ | ✅​ |
80
- | tiktok | 链接 | ✅​ | ❌️ | ❌️ |
81
- | twitter | 链接 | ✅​ | ✅​ | ❌️ |
72
+ | 平台 | 触发的消息形态 | 视频 | 图集 | 音频 |
73
+ | ------- | ------------------------------- | ---- | ---- | ---- |
74
+ | B站 | av号/BV号/链接/短链/卡片/小程序 | ✅​ | ✅​ | ✅​ |
75
+ | 抖音 | 链接(分享链接,兼容电脑端链接) | ✅​ | ✅​ | ❌️ |
76
+ | 微博 | 链接(博文,视频,show, 文章) | ✅​ | ✅​ | ❌️ |
77
+ | 小红书 | 链接(含短链)/卡片 | ✅​ | ✅​ | ❌️ |
78
+ | 快手 | 链接(包含标准链接和短链) | ✅​ | ✅​ | ❌️ |
79
+ | acfun | 链接 | ✅​ | ❌️ | ❌️ |
80
+ | youtube | 链接(含短链) | ✅​ | ❌️ | ✅​ |
81
+ | tiktok | 链接 | ✅​ | ❌️ | ❌️ |
82
+ | twitter | 链接 | ✅​ | ✅​ | ❌️ |
82
83
 
83
- 支持的链接,可参考 [测试链接](https://github.com/fllesser/nonebot-plugin-parser/blob/master/test_url.md)
84
+ 支持的链接,可参考 [测试链接](https://github.com/fllesser/nonebot-plugin-parser/blob/master/tests/others/test_urls.md)
84
85
 
85
86
  ## 🎨 效果图
86
87
  插件默认启用 PIL 实现的通用媒体卡片渲染,效果图如下
@@ -289,9 +290,7 @@ from httpx import AsyncClient
289
290
  from nonebot import require
290
291
 
291
292
  require("nonebot_plugin_parser")
292
- from nonebot_plugin_parser.parsers import BaseParser, ParseResult
293
- from nonebot_plugin_parser.parsers.base import Platform, handle
294
-
293
+ from nonebot_plugin_parser.parsers import BaseParser, Platform, handle
295
294
 
296
295
  class ExampleParser(BaseParser):
297
296
  """示例视频网站解析器"""
@@ -299,7 +298,7 @@ class ExampleParser(BaseParser):
299
298
  platform: ClassVar[Platform] = Platform(name="example", display_name="示例网站")
300
299
 
301
300
  @handle("ex.short", r"ex\.short/\w+)")
302
- async def _parse_short_link(self, searched: re.Match[str]):
301
+ async def _parse_short_link(self, searched: Match[str]):
303
302
  """解析短链"""
304
303
  url = f"https://{searched.group(0)}"
305
304
  # 重定向再解析,请确保重定向链接的 handle 存在
@@ -308,7 +307,7 @@ class ExampleParser(BaseParser):
308
307
 
309
308
  @handle("example.com", r"example\.com/video/(?P<video_id>\w+)")
310
309
  @handle("exam.ple", r"exam\.ple/(?P<video_id>\w+)")
311
- async def _parse(self, searched: Match[str]) -> ParseResult:
310
+ async def _parse(self, searched: Match[str]):
312
311
  # 1. 提取视频 ID
313
312
  video_id = searched.group("video_id")
314
313
 
@@ -22,19 +22,19 @@
22
22
 
23
23
  ## 📖 介绍
24
24
 
25
- | 平台 | 触发的消息形态 | 视频 | 图集 | 音频 |
26
- | ------- | ------------------------------------- | ---- | ---- | ---- |
27
- | B站 | BV号/链接(包含短链,BV,av)/卡片/小程序 | ✅​ | ✅​ | ✅​ |
28
- | 抖音 | 链接(分享链接,兼容电脑端链接) | ✅​ | ✅​ | ❌️ |
29
- | 微博 | 链接(博文,视频,show) | ✅​ | ✅​ | ❌️ |
30
- | 小红书 | 链接(含短链)/卡片 | ✅​ | ✅​ | ❌️ |
31
- | 快手 | 链接(包含标准链接和短链) | ✅​ | ✅​ | ❌️ |
32
- | acfun | 链接 | ✅​ | ❌️ | ❌️ |
33
- | youtube | 链接(含短链) | ✅​ | ❌️ | ✅​ |
34
- | tiktok | 链接 | ✅​ | ❌️ | ❌️ |
35
- | twitter | 链接 | ✅​ | ✅​ | ❌️ |
36
-
37
- 支持的链接,可参考 [测试链接](https://github.com/fllesser/nonebot-plugin-parser/blob/master/test_url.md)
25
+ | 平台 | 触发的消息形态 | 视频 | 图集 | 音频 |
26
+ | ------- | ------------------------------- | ---- | ---- | ---- |
27
+ | B站 | av号/BV号/链接/短链/卡片/小程序 | ✅​ | ✅​ | ✅​ |
28
+ | 抖音 | 链接(分享链接,兼容电脑端链接) | ✅​ | ✅​ | ❌️ |
29
+ | 微博 | 链接(博文,视频,show, 文章) | ✅​ | ✅​ | ❌️ |
30
+ | 小红书 | 链接(含短链)/卡片 | ✅​ | ✅​ | ❌️ |
31
+ | 快手 | 链接(包含标准链接和短链) | ✅​ | ✅​ | ❌️ |
32
+ | acfun | 链接 | ✅​ | ❌️ | ❌️ |
33
+ | youtube | 链接(含短链) | ✅​ | ❌️ | ✅​ |
34
+ | tiktok | 链接 | ✅​ | ❌️ | ❌️ |
35
+ | twitter | 链接 | ✅​ | ✅​ | ❌️ |
36
+
37
+ 支持的链接,可参考 [测试链接](https://github.com/fllesser/nonebot-plugin-parser/blob/master/tests/others/test_urls.md)
38
38
 
39
39
  ## 🎨 效果图
40
40
  插件默认启用 PIL 实现的通用媒体卡片渲染,效果图如下
@@ -243,9 +243,7 @@ from httpx import AsyncClient
243
243
  from nonebot import require
244
244
 
245
245
  require("nonebot_plugin_parser")
246
- from nonebot_plugin_parser.parsers import BaseParser, ParseResult
247
- from nonebot_plugin_parser.parsers.base import Platform, handle
248
-
246
+ from nonebot_plugin_parser.parsers import BaseParser, Platform, handle
249
247
 
250
248
  class ExampleParser(BaseParser):
251
249
  """示例视频网站解析器"""
@@ -253,7 +251,7 @@ class ExampleParser(BaseParser):
253
251
  platform: ClassVar[Platform] = Platform(name="example", display_name="示例网站")
254
252
 
255
253
  @handle("ex.short", r"ex\.short/\w+)")
256
- async def _parse_short_link(self, searched: re.Match[str]):
254
+ async def _parse_short_link(self, searched: Match[str]):
257
255
  """解析短链"""
258
256
  url = f"https://{searched.group(0)}"
259
257
  # 重定向再解析,请确保重定向链接的 handle 存在
@@ -262,7 +260,7 @@ class ExampleParser(BaseParser):
262
260
 
263
261
  @handle("example.com", r"example\.com/video/(?P<video_id>\w+)")
264
262
  @handle("exam.ple", r"exam\.ple/(?P<video_id>\w+)")
265
- async def _parse(self, searched: Match[str]) -> ParseResult:
263
+ async def _parse(self, searched: Match[str]):
266
264
  # 1. 提取视频 ID
267
265
  video_id = searched.group("video_id")
268
266
 
@@ -1,6 +1,8 @@
1
+ #:tombi schema.strict = false
2
+
1
3
  [project]
2
4
  name = "nonebot-plugin-parser"
3
- version = "2.2.0"
5
+ version = "2.2.2"
4
6
  description = "NoneBot2 链接分享解析 Alconna 版, 通用媒体卡片渲染(PIL 实现), 支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga"
5
7
  readme = "README.md"
6
8
  requires-python = ">=3.10"
@@ -30,6 +32,7 @@ classifiers = [
30
32
  "Programming Language :: Python :: 3.10",
31
33
  "Programming Language :: Python :: 3.11",
32
34
  "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
33
36
  "Topic :: Communications :: Chat",
34
37
  "Topic :: Internet :: WWW/HTTP",
35
38
  "Topic :: Multimedia :: Video",
@@ -40,7 +43,7 @@ dependencies = [
40
43
  "bilibili-api-python>=17.4.0,<18.0.0",
41
44
  "curl_cffi>=0.13.0,<1.0.0",
42
45
  "httpx>=0.27.2,<1.0.0",
43
- "msgspec>=0.19.0,<1.0.0",
46
+ "msgspec>=0.20.0,<1.0.0",
44
47
  "nonebot-plugin-alconna>=0.59.4",
45
48
  "nonebot-plugin-apscheduler>=0.5.0,<1.0.0",
46
49
  "nonebot-plugin-localstore>=0.7.4,<1.0.0",
@@ -86,7 +89,7 @@ requires = ["uv_build>=0.9.0,<0.10.0"]
86
89
  build-backend = "uv_build"
87
90
 
88
91
  [tool.bumpversion]
89
- current_version = "2.2.0"
92
+ current_version = "2.2.2"
90
93
  commit = true
91
94
  message = "release: bump vesion from {current_version} to {new_version}"
92
95
  tag = true
@@ -4,18 +4,24 @@ from typing import Final
4
4
  from httpx import Timeout
5
5
 
6
6
  COMMON_HEADER: Final[dict[str, str]] = {
7
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) "
8
- "Chrome/55.0.2883.87 UBrowser/6.2.4098.3 Safari/537.36"
7
+ "User-Agent": (
8
+ "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) "
9
+ "Chrome/55.0.2883.87 UBrowser/6.2.4098.3 Safari/537.36"
10
+ )
9
11
  }
10
12
 
11
13
  IOS_HEADER: Final[dict[str, str]] = {
12
- "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) "
13
- "Version/16.6 Mobile/15E148 Safari/604.1 Edg/132.0.0.0"
14
+ "User-Agent": (
15
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) "
16
+ "Version/16.6 Mobile/15E148 Safari/604.1 Edg/132.0.0.0"
17
+ )
14
18
  }
15
19
 
16
20
  ANDROID_HEADER: Final[dict[str, str]] = {
17
- "User-Agent": "Mozilla/5.0 (Linux; Android 15; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) "
18
- "Chrome/132.0.0.0 Mobile Safari/537.36 Edg/132.0.0.0"
21
+ "User-Agent": (
22
+ "Mozilla/5.0 (Linux; Android 15; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) "
23
+ "Chrome/132.0.0.0 Mobile Safari/537.36 Edg/132.0.0.0"
24
+ )
19
25
  }
20
26
 
21
27
  COMMON_TIMEOUT: Final[Timeout] = Timeout(connect=15.0, read=20.0, write=10.0, pool=10.0)
@@ -2,39 +2,45 @@ class ParseException(Exception):
2
2
  """异常基类"""
3
3
 
4
4
  def __init__(self, message: str):
5
+ super().__init__(message)
5
6
  self.message = message
6
7
 
7
8
 
9
+ class TipException(ParseException):
10
+ """提示异常"""
11
+
12
+ pass
13
+
14
+
8
15
  class DownloadException(ParseException):
9
16
  """下载异常"""
10
17
 
11
18
  def __init__(self, message: str | None = None):
12
- self.message = message or "媒体下载失败"
19
+ super().__init__(message or "媒体下载失败")
13
20
 
14
21
 
15
22
  class DownloadLimitException(DownloadException):
16
23
  """下载超过限制异常"""
17
24
 
18
- def __init__(self):
19
- raise NotImplementedError
25
+ pass
20
26
 
21
27
 
22
28
  class SizeLimitException(DownloadLimitException):
23
29
  """下载大小超过限制异常"""
24
30
 
25
31
  def __init__(self):
26
- self.message = "媒体大小超过配置限制,取消下载"
32
+ super().__init__("媒体大小超过配置限制,取消下载")
27
33
 
28
34
 
29
35
  class DurationLimitException(DownloadLimitException):
30
36
  """下载时长超过限制异常"""
31
37
 
32
38
  def __init__(self):
33
- self.message = "媒体时长超过配置限制,取消下载"
39
+ super().__init__("媒体时长超过配置限制,取消下载")
34
40
 
35
41
 
36
42
  class ZeroSizeException(DownloadException):
37
43
  """下载大小为 0 异常"""
38
44
 
39
45
  def __init__(self):
40
- self.message = "媒体大小为 0, 取消下载"
46
+ super().__init__("媒体大小为 0, 取消下载")
@@ -1,25 +1,38 @@
1
- from collections.abc import Sequence
1
+ from collections.abc import Awaitable, Callable, Sequence
2
2
  from functools import wraps
3
3
  from pathlib import Path
4
- from typing import Literal
4
+ from typing import Any, ClassVar, Literal
5
5
 
6
6
  from nonebot import logger
7
7
  from nonebot.adapters import Event
8
- from nonebot.internal.matcher import current_bot
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
12
- from nonebot_plugin_alconna.uniseg.segment import CustomNode, Reference
8
+ from nonebot.matcher import current_bot, current_event
9
+ from nonebot_plugin_alconna import SupportAdapter, uniseg
10
+ from nonebot_plugin_alconna.uniseg import (
11
+ CustomNode,
12
+ File,
13
+ Image,
14
+ Reference,
15
+ Segment,
16
+ Text,
17
+ UniMessage,
18
+ Video,
19
+ Voice,
20
+ )
13
21
 
14
22
  from .config import pconfig
15
23
 
24
+ # from .exception import TipException
25
+
16
26
  ForwardNodeInner = str | Segment | UniMessage
17
27
  """转发消息节点内部允许的类型"""
18
28
 
19
29
 
20
30
  class UniHelper:
21
31
  @staticmethod
22
- def construct_forward_message(segments: Sequence[ForwardNodeInner], user_id: str | None = None) -> Reference:
32
+ def construct_forward_message(
33
+ segments: Sequence[ForwardNodeInner],
34
+ user_id: str | None = None,
35
+ ) -> Reference:
23
36
  """构造转发消息
24
37
 
25
38
  Args:
@@ -45,7 +58,10 @@ class UniHelper:
45
58
  return Reference(nodes=nodes)
46
59
 
47
60
  @staticmethod
48
- def img_seg(img_path: Path | None = None, raw: bytes | None = None) -> Image:
61
+ def img_seg(
62
+ img_path: Path | None = None,
63
+ raw: bytes | None = None,
64
+ ) -> Image:
49
65
  """获取图片 Seg
50
66
 
51
67
  Args:
@@ -102,7 +118,10 @@ class UniHelper:
102
118
  return Video(path=video_path)
103
119
 
104
120
  @staticmethod
105
- def file_seg(file: Path, display_name: str | None = None) -> File:
121
+ def file_seg(
122
+ file: Path,
123
+ display_name: str | None = None,
124
+ ) -> File:
106
125
  """获取文件 Seg
107
126
 
108
127
  Args:
@@ -121,43 +140,65 @@ class UniHelper:
121
140
  else:
122
141
  return File(path=file, name=display_name)
123
142
 
124
- @staticmethod
143
+ EMOJI_MAP: ClassVar[dict[str, tuple[str, str]]] = {
144
+ "fail": ("10060", "❌"),
145
+ "resolving": ("424", "👀"),
146
+ "done": ("144", "🎉"),
147
+ }
148
+
149
+ @classmethod
125
150
  async def message_reaction(
151
+ cls,
126
152
  event: Event,
127
153
  status: Literal["fail", "resolving", "done"],
128
154
  ) -> None:
129
- emoji_map = {
130
- "fail": ("10060", "❌"),
131
- "resolving": ("424", "👀"),
132
- "done": ("144", "🎉"),
133
- }
155
+ """发送消息回应
156
+
157
+ Args:
158
+ event (Event): 事件对象
159
+ status (Literal["fail", "resolving", "done"]): 状态
160
+ """
134
161
  message_id = uniseg.get_message_id(event)
135
162
  target = uniseg.get_target(event)
136
163
 
137
164
  if target.adapter in (SupportAdapter.onebot11, SupportAdapter.qq):
138
- emoji = emoji_map[status][0]
165
+ emoji = cls.EMOJI_MAP[status][0]
139
166
  else:
140
- emoji = emoji_map[status][1]
167
+ emoji = cls.EMOJI_MAP[status][1]
141
168
 
142
169
  try:
143
170
  await uniseg.message_reaction(emoji, message_id=message_id)
144
171
  except Exception:
145
172
  logger.warning(f"reaction {emoji} to {message_id} failed, maybe not support")
146
173
 
147
- @staticmethod
148
- def exception_handler(func):
174
+ @classmethod
175
+ def with_reaction(cls, func: Callable[..., Awaitable[Any]]):
176
+ """自动回应装饰器
177
+
178
+ 自动处理消息响应状态,并捕获 TipException 发送提示消息
179
+
180
+ Args:
181
+ func: 被装饰的函数
182
+
183
+ Returns:
184
+ 装饰后的函数
185
+ """
186
+
149
187
  @wraps(func)
150
188
  async def wrapper(*args, **kwargs):
151
189
  event = current_event.get()
152
- await UniHelper.message_reaction(event, "resolving")
190
+ await cls.message_reaction(event, "resolving")
153
191
 
154
192
  try:
155
193
  result = await func(*args, **kwargs)
194
+ # except TipException as e:
195
+ # await UniMessage.text(e.message).send()
196
+ # raise
156
197
  except Exception:
157
- await UniHelper.message_reaction(event, "fail")
198
+ await cls.message_reaction(event, "fail")
158
199
  raise
159
200
 
160
- await UniHelper.message_reaction(event, "done")
201
+ await cls.message_reaction(event, "done")
161
202
  return result
162
203
 
163
204
  return wrapper
@@ -45,7 +45,7 @@ def clear_result_cache():
45
45
  _RESULT_CACHE.clear()
46
46
 
47
47
 
48
- @UniHelper.exception_handler
48
+ @UniHelper.with_reaction
49
49
  async def parser_handler(
50
50
  sr: SearchResult = Searched(),
51
51
  ):
@@ -67,7 +67,7 @@ async def parser_handler(
67
67
  async for message in renderer.render_messages(result):
68
68
  await message.send()
69
69
 
70
- # 4. 无 raise 再缓存解析结果
70
+ # 4. 缓存解析结果
71
71
  _RESULT_CACHE[cache_key] = result
72
72
 
73
73
 
@@ -84,7 +84,7 @@ from ..parsers import BilibiliParser
84
84
 
85
85
 
86
86
  @on_command("bm", priority=3, block=True).handle()
87
- @UniHelper.exception_handler
87
+ @UniHelper.with_reaction
88
88
  async def _(message: Message = CommandArg()):
89
89
  text = message.extract_plain_text()
90
90
  matched = re.search(r"(BV[A-Za-z0-9]{10})(\s\d{1,3})?", text)
@@ -116,13 +116,13 @@ if YTDLP_DOWNLOADER is not None:
116
116
  from ..parsers import YouTubeParser
117
117
 
118
118
  @on_command("ym", priority=3, block=True).handle()
119
- @UniHelper.exception_handler
119
+ @UniHelper.with_reaction
120
120
  async def _(message: Message = CommandArg()):
121
121
  text = message.extract_plain_text()
122
122
  ytb_parser = cast(YouTubeParser, KEYWORD_PARSER_MAP["youtu.be"])
123
123
  _, matched = ytb_parser.search_url(text)
124
124
  if not matched:
125
- await UniMessage("请发送正确的 youtube 链接").finish()
125
+ await UniMessage("请发送正确的油管链接").finish()
126
126
 
127
127
  url = matched.group(0)
128
128
 
@@ -3,7 +3,6 @@ from ..download import YTDLP_DOWNLOADER
3
3
  from .acfun import AcfunParser as AcfunParser
4
4
  from .base import BaseParser as BaseParser
5
5
  from .bilibili import BilibiliParser as BilibiliParser
6
- from .data import ParseResult as ParseResult
7
6
  from .douyin import DouyinParser as DouyinParser
8
7
  from .kuaishou import KuaiShouParser as KuaiShouParser
9
8
  from .nga import NGAParser as NGAParser
@@ -15,6 +14,24 @@ if YTDLP_DOWNLOADER is not None:
15
14
  from .tiktok import TikTokParser as TikTokParser
16
15
  from .youtube import YouTubeParser as YouTubeParser
17
16
 
17
+ from .base import handle as handle
18
+ from .data import AudioContent as AudioContent
19
+ from .data import Author
20
+ from .data import DynamicContent as DynamicContent
21
+ from .data import GraphicsContent as GraphicsContent
22
+ from .data import ImageContent as ImageContent
23
+ from .data import ParseResult as ParseResult
24
+ from .data import Platform as Platform
25
+ from .data import VideoContent as VideoContent
26
+
18
27
  __all__ = [
28
+ "AudioContent",
29
+ "Author",
30
+ "DynamicContent",
31
+ "GraphicsContent",
32
+ "ImageContent",
19
33
  "ParseResult",
34
+ "Platform",
35
+ "VideoContent",
36
+ "handle",
20
37
  ]
@@ -119,8 +119,8 @@ class AcfunParser(BaseParser):
119
119
  break
120
120
  except HTTPError:
121
121
  await safe_unlink(video_file)
122
- logger.exception("acfun 视频下载失败")
123
- raise DownloadException("acfun 视频下载失败")
122
+ logger.exception("视频下载失败")
123
+ raise DownloadException("视频下载失败")
124
124
  return video_file
125
125
 
126
126
  async def _parse_m3u8(self, m3u8_url: str):
@@ -5,7 +5,7 @@ from asyncio import Task
5
5
  from collections.abc import Callable, Coroutine
6
6
  from pathlib import Path
7
7
  from re import Match, Pattern, compile
8
- from typing import Any, ClassVar, TypeVar, cast
8
+ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast
9
9
  from typing_extensions import Unpack
10
10
 
11
11
  from ..config import pconfig as pconfig
@@ -16,6 +16,7 @@ from ..exception import DownloadException as DownloadException
16
16
  from ..exception import DurationLimitException as DurationLimitException
17
17
  from ..exception import ParseException as ParseException
18
18
  from ..exception import SizeLimitException as SizeLimitException
19
+ from ..exception import TipException as TipException
19
20
  from ..exception import ZeroSizeException as ZeroSizeException
20
21
  from .data import ParseResult, ParseResultKwargs, Platform
21
22
 
@@ -55,8 +56,9 @@ class BaseParser:
55
56
  platform: ClassVar[Platform]
56
57
  """ 平台信息(包含名称和显示名称) """
57
58
 
58
- _key_patterns: ClassVar[KeyPatterns]
59
- _handlers: ClassVar[dict[str, HandlerFunc]]
59
+ if TYPE_CHECKING:
60
+ _key_patterns: ClassVar[KeyPatterns]
61
+ _handlers: ClassVar[dict[str, HandlerFunc]]
60
62
 
61
63
  def __init__(self):
62
64
  self.headers = COMMON_HEADER.copy()
@@ -106,7 +108,11 @@ class BaseParser:
106
108
  """
107
109
  return await self._handlers[keyword](self, searched)
108
110
 
109
- async def parse_with_redirect(self, url: str, headers: dict[str, str] | None = None) -> ParseResult:
111
+ async def parse_with_redirect(
112
+ self,
113
+ url: str,
114
+ headers: dict[str, str] | None = None,
115
+ ) -> ParseResult:
110
116
  """先重定向再解析"""
111
117
  redirect_url = await self.get_redirect_url(url, headers=headers or self.headers)
112
118
 
@@ -124,7 +130,7 @@ class BaseParser:
124
130
  continue
125
131
  if searched := pattern.search(url):
126
132
  return keyword, searched
127
- raise ValueError(f"无法匹配 {url}")
133
+ raise ParseException(f"无法匹配 {url}")
128
134
 
129
135
  @classmethod
130
136
  def result(cls, **kwargs: Unpack[ParseResultKwargs]) -> ParseResult:
@@ -136,7 +142,7 @@ class BaseParser:
136
142
  url: str,
137
143
  headers: dict[str, str] | None = None,
138
144
  ) -> str:
139
- """获取重定向后的URL"""
145
+ """获取重定向后的 URL, 单次重定向"""
140
146
  from httpx import AsyncClient
141
147
 
142
148
  headers = headers or COMMON_HEADER.copy()
@@ -151,6 +157,26 @@ class BaseParser:
151
157
  response.raise_for_status()
152
158
  return response.headers.get("Location", url)
153
159
 
160
+ @staticmethod
161
+ async def get_final_url(
162
+ url: str,
163
+ headers: dict[str, str] | None = None,
164
+ ) -> str:
165
+ """获取重定向后的 URL, 允许多次重定向"""
166
+ from httpx import AsyncClient
167
+
168
+ headers = headers or COMMON_HEADER.copy()
169
+ async with AsyncClient(
170
+ headers=headers,
171
+ verify=False,
172
+ follow_redirects=True,
173
+ timeout=COMMON_TIMEOUT,
174
+ ) as client:
175
+ response = await client.get(url)
176
+ if response.status_code >= 400:
177
+ response.raise_for_status()
178
+ return str(response.url)
179
+
154
180
  def create_author(
155
181
  self,
156
182
  name: str,