nonebot-plugin-parser 2.4.3__tar.gz → 2.5.1__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 (71) hide show
  1. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/PKG-INFO +7 -17
  2. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/README.md +3 -13
  3. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/pyproject.toml +11 -12
  4. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/config.py +17 -2
  5. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/download/__init__.py +19 -3
  6. nonebot_plugin_parser-2.5.1/src/nonebot_plugin_parser/download/task.py +84 -0
  7. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/helper.py +1 -2
  8. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/matchers/__init__.py +2 -2
  9. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/__init__.py +0 -2
  10. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/acfun/__init__.py +5 -2
  11. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/base.py +46 -29
  12. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +55 -30
  13. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +60 -25
  14. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +42 -33
  15. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/data.py +89 -80
  16. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +26 -26
  17. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py +13 -13
  18. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/nga.py +12 -30
  19. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/tiktok.py +8 -4
  20. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/twitter.py +29 -41
  21. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/weibo/__init__.py +25 -30
  22. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py +24 -25
  23. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/youtube/__init__.py +11 -32
  24. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/__init__.py +6 -11
  25. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/base.py +41 -36
  26. nonebot_plugin_parser-2.5.1/src/nonebot_plugin_parser/renders/common.py +748 -0
  27. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/default.py +8 -8
  28. nonebot_plugin_parser-2.5.1/src/nonebot_plugin_parser/renders/htmlrender.py +38 -0
  29. nonebot_plugin_parser-2.5.1/src/nonebot_plugin_parser/renders/resources/__init__.py +10 -0
  30. nonebot_plugin_parser-2.5.1/src/nonebot_plugin_parser/renders/templates/card.html.jinja2 +668 -0
  31. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/utils.py +11 -0
  32. nonebot_plugin_parser-2.4.3/src/nonebot_plugin_parser/download/task.py +0 -19
  33. nonebot_plugin_parser-2.4.3/src/nonebot_plugin_parser/renders/common.py +0 -665
  34. nonebot_plugin_parser-2.4.3/src/nonebot_plugin_parser/renders/htmlrender.py +0 -88
  35. nonebot_plugin_parser-2.4.3/src/nonebot_plugin_parser/renders/templates/card.html.jinja +0 -394
  36. nonebot_plugin_parser-2.4.3/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -425
  37. nonebot_plugin_parser-2.4.3/src/nonebot_plugin_parser/renders/weibo.py +0 -18
  38. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/__init__.py +0 -0
  39. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/constants.py +0 -0
  40. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  41. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/exception.py +0 -0
  42. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  43. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/matchers/rule.py +0 -0
  44. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/acfun/video.py +0 -0
  45. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  46. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  47. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  48. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  49. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  50. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  51. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  52. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
  53. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/kuaishou/states.py +0 -0
  54. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/weibo/article.py +0 -0
  55. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/weibo/common.py +0 -0
  56. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/weibo/show.py +0 -0
  57. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/xiaohongshu/common.py +0 -0
  58. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/xiaohongshu/discovery.py +0 -0
  59. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/xiaohongshu/explore.py +0 -0
  60. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/parsers/youtube/meta.py +0 -0
  61. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW.ttf +0 -0
  62. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/avatar.png +0 -0
  63. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  64. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  65. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  66. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/play.png +0 -0
  67. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  68. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  69. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  70. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  71. {nonebot_plugin_parser-2.4.3 → nonebot_plugin_parser-2.5.1}/src/nonebot_plugin_parser/renders/resources/youtube.png +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nonebot-plugin-parser
3
- Version: 2.4.3
3
+ Version: 2.5.1
4
4
  Summary: NoneBot2 链接分享解析 Alconna 版, 现支持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
@@ -35,9 +35,9 @@ Requires-Dist: nonebot-plugin-localstore>=0.7.4,<1.0.0
35
35
  Requires-Dist: nonebot-plugin-uninfo>=0.10.1,<1.0.0
36
36
  Requires-Dist: nonebot-plugin-htmlkit>=0.1.0rc4 ; extra == 'all'
37
37
  Requires-Dist: nonebot-plugin-htmlrender>=0.6.7 ; extra == 'all'
38
- Requires-Dist: yt-dlp[default]>=2026.2.21 ; extra == 'all'
39
- Requires-Dist: emosvg>=0.1.6 ; extra == 'all'
40
- Requires-Dist: emosvg>=0.1.6 ; extra == 'emosvg'
38
+ Requires-Dist: yt-dlp[default]>=2026.3.13 ; extra == 'all'
39
+ Requires-Dist: emosvg>=0.1.7 ; extra == 'all'
40
+ Requires-Dist: emosvg>=0.1.7 ; extra == 'emosvg'
41
41
  Requires-Dist: nonebot-plugin-htmlkit>=0.1.0rc4 ; extra == 'htmlkit'
42
42
  Requires-Dist: nonebot-plugin-htmlrender>=0.6.7 ; extra == 'htmlrender'
43
43
  Requires-Dist: yt-dlp[default]>=2025.2.21 ; extra == 'ytdlp'
@@ -199,7 +199,7 @@ Description-Content-Type: text/markdown
199
199
 
200
200
  uv add "nonebot-plugin-parser[htmlkit]"
201
201
 
202
- `htmlrender`, 使用 `playwright` 渲染 `html`, 插件现有模版有点问题,并且极其丑陋,不建议使用
202
+ `htmlrender`, 使用 `playwright` 渲染 `html`, 插件自 `v2.5.0` 起已正式支持
203
203
 
204
204
  uv add "nonebot-plugin-parser[htmlrender]"
205
205
 
@@ -300,8 +300,8 @@ parser_render_type="common"
300
300
  parser_append_url=False
301
301
 
302
302
  # [可选] 自定义渲染字体
303
- # 配置字体文件名,并将字体文件放置于 localstore 生成的插件 data 目录下
304
- # 例如: ./data/nonebot_plugin_parser/
303
+ # 配置字体文件名,并将字体文件放置于 localstore 生成的插件 config 目录下
304
+ # 例如: ./config/nonebot_plugin_parser/
305
305
  parser_custom_font="LXGWZhenKaiGB-Regular.ttf"
306
306
 
307
307
  # [可选] 是否需要转发媒体内容(超过 4 项时始终使用合并转发)
@@ -452,16 +452,6 @@ images = self.create_image_contents([
452
452
  ])
453
453
  ```
454
454
 
455
- > 构建图文内容(适用于类似 Bilibili 动态图文混排)
456
-
457
- ```python
458
- graphics = self.create_graphics_content(
459
- image_url="https://example.com/image.jpg",
460
- text="图片前的文字说明", # 可选
461
- alt="图片描述" # 可选,居中显示
462
- )
463
- ```
464
-
465
455
  > 创建动图内容(GIF),平台一般只提供视频(后续插件会做自动转为 gif 的处理)
466
456
 
467
457
  ```python
@@ -145,7 +145,7 @@
145
145
 
146
146
  uv add "nonebot-plugin-parser[htmlkit]"
147
147
 
148
- `htmlrender`, 使用 `playwright` 渲染 `html`, 插件现有模版有点问题,并且极其丑陋,不建议使用
148
+ `htmlrender`, 使用 `playwright` 渲染 `html`, 插件自 `v2.5.0` 起已正式支持
149
149
 
150
150
  uv add "nonebot-plugin-parser[htmlrender]"
151
151
 
@@ -246,8 +246,8 @@ parser_render_type="common"
246
246
  parser_append_url=False
247
247
 
248
248
  # [可选] 自定义渲染字体
249
- # 配置字体文件名,并将字体文件放置于 localstore 生成的插件 data 目录下
250
- # 例如: ./data/nonebot_plugin_parser/
249
+ # 配置字体文件名,并将字体文件放置于 localstore 生成的插件 config 目录下
250
+ # 例如: ./config/nonebot_plugin_parser/
251
251
  parser_custom_font="LXGWZhenKaiGB-Regular.ttf"
252
252
 
253
253
  # [可选] 是否需要转发媒体内容(超过 4 项时始终使用合并转发)
@@ -398,16 +398,6 @@ images = self.create_image_contents([
398
398
  ])
399
399
  ```
400
400
 
401
- > 构建图文内容(适用于类似 Bilibili 动态图文混排)
402
-
403
- ```python
404
- graphics = self.create_graphics_content(
405
- image_url="https://example.com/image.jpg",
406
- text="图片前的文字说明", # 可选
407
- alt="图片描述" # 可选,居中显示
408
- )
409
- ```
410
-
411
401
  > 创建动图内容(GIF),平台一般只提供视频(后续插件会做自动转为 gif 的处理)
412
402
 
413
403
  ```python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parser"
3
- version = "2.4.3"
3
+ version = "2.5.1"
4
4
  description = "NoneBot2 链接分享解析 Alconna 版, 现支持B站|抖音|快手|微博|小红书|YouTube|TikTok|Twitter|AcFun|NGA"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -53,7 +53,6 @@ dependencies = [
53
53
  "nonebot-plugin-apscheduler>=0.5.0,<1.0.0",
54
54
  "nonebot-plugin-localstore>=0.7.4,<1.0.0",
55
55
  "nonebot-plugin-uninfo>=0.10.1,<1.0.0",
56
-
57
56
  ]
58
57
 
59
58
  [project.urls]
@@ -65,32 +64,32 @@ Repository = "https://github.com/fllesser/nonebot-plugin-parser"
65
64
  htmlkit = ["nonebot-plugin-htmlkit>=0.1.0rc4"]
66
65
  htmlrender = ["nonebot-plugin-htmlrender>=0.6.7"]
67
66
  ytdlp = ["yt-dlp[default]>=2025.2.21"]
68
- emosvg = ["emosvg>=0.1.6"]
67
+ emosvg = ["emosvg>=0.1.7"]
69
68
  all = [
70
69
  "nonebot-plugin-htmlkit>=0.1.0rc4",
71
70
  "nonebot-plugin-htmlrender>=0.6.7",
72
- "yt-dlp[default]>=2026.2.21",
73
- "emosvg>=0.1.6",
71
+ "yt-dlp[default]>=2026.3.13",
72
+ "emosvg>=0.1.7",
74
73
  ]
75
74
 
76
75
  [dependency-groups]
77
76
  dev = [
78
77
  "nonebot2[fastapi]>=2.4.3,<3.0.0",
79
78
  "nonebot-adapter-onebot>=2.4.6",
80
- "ruff>=0.15.0,<1.0.0",
79
+ "ruff>=0.15.6,<1.0.0",
81
80
  { include-group = "extras" },
82
81
  { include-group = "test" },
83
82
  ]
84
83
  extras = [
85
- "emosvg>=0.1.6",
84
+ "emosvg>=0.1.7",
86
85
  "nonebot-plugin-htmlkit>=0.1.0rc4",
87
86
  "nonebot-plugin-htmlrender>=0.6.7",
88
- "yt-dlp[default]>=2026.2.21",
89
- "types-yt-dlp>=2026.2.21.20260223",
87
+ "yt-dlp[default]>=2026.3.13",
88
+ "types-yt-dlp>=2026.3.13.20260314",
90
89
  ]
91
90
  test = [
92
91
  "nonebug>=0.4.3,<1.0.0",
93
- "poethepoet>=0.42.0",
92
+ "poethepoet>=0.42.1",
94
93
  "pytest-asyncio>=1.3.0,<1.4.0",
95
94
  "pytest-cov>=7.0.0",
96
95
  "pytest-xdist>=3.8.0,<4.0.0",
@@ -114,12 +113,12 @@ conflicts = [
114
113
  ]
115
114
 
116
115
  [tool.uv.sources]
117
- nonebug = { git = "https://github.com/nonebot/nonebug" }
116
+ nonebug = { git = "https://github.com/nonebot/nonebug", rev = "master" }
118
117
 
119
118
  [tool.bumpversion]
120
119
  tag = true
121
120
  commit = true
122
- current_version = "2.4.3"
121
+ current_version = "2.5.1"
123
122
  message = "release: bump vesion from {current_version} to {new_version}"
124
123
 
125
124
  [[tool.bumpversion.files]]
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from nonebot import require, get_driver, get_plugin_config
3
+ from nonebot import logger, require, get_driver, get_plugin_config
4
4
  from apilmoji import ELK_SH_CDN, EmojiStyle
5
5
  from pydantic import BaseModel
6
6
  from bilibili_api.video import VideoCodecs, VideoQuality
@@ -143,7 +143,22 @@ class Config(BaseModel):
143
143
  @property
144
144
  def custom_font(self) -> Path | None:
145
145
  """自定义字体"""
146
- return (self.data_dir / self.parser_custom_font) if self.parser_custom_font else None
146
+ if self.parser_custom_font:
147
+ font_path = self.config_dir / self.parser_custom_font
148
+ if font_path.exists():
149
+ return font_path
150
+
151
+ # 尝试从旧路径迁移字体文件
152
+ old_path = self.data_dir / self.parser_custom_font
153
+ if old_path.exists():
154
+ try:
155
+ old_path.rename(font_path)
156
+ logger.info(f"字体文件 {old_path} 成功迁移到 {font_path}")
157
+ except OSError:
158
+ logger.error(f"字体文件迁移失败, 请手动将其移动到 {font_path}")
159
+ return old_path
160
+
161
+ return font_path
147
162
 
148
163
  @property
149
164
  def need_forward_contents(self) -> bool:
@@ -98,7 +98,13 @@ class StreamDownloader:
98
98
  """download video file by url with stream"""
99
99
  if video_name is None:
100
100
  video_name = generate_file_name(url, ".mp4")
101
- return await self.download_file(url, file_name=video_name, ext_headers=ext_headers, chunk_size=1024 * 1024)
101
+
102
+ return await self.download_file(
103
+ url,
104
+ file_name=video_name,
105
+ ext_headers=ext_headers,
106
+ chunk_size=1024 * 1024,
107
+ )
102
108
 
103
109
  @auto_task
104
110
  async def download_audio(
@@ -111,7 +117,12 @@ class StreamDownloader:
111
117
  """download audio file by url with stream"""
112
118
  if audio_name is None:
113
119
  audio_name = generate_file_name(url, ".mp3")
114
- return await self.download_file(url, file_name=audio_name, ext_headers=ext_headers)
120
+
121
+ return await self.download_file(
122
+ url,
123
+ file_name=audio_name,
124
+ ext_headers=ext_headers,
125
+ )
115
126
 
116
127
  @auto_task
117
128
  async def download_img(
@@ -124,7 +135,12 @@ class StreamDownloader:
124
135
  """download image file by url with stream"""
125
136
  if img_name is None:
126
137
  img_name = generate_file_name(url, ".jpg")
127
- return await self.download_file(url, file_name=img_name, ext_headers=ext_headers)
138
+
139
+ return await self.download_file(
140
+ url,
141
+ file_name=img_name,
142
+ ext_headers=ext_headers,
143
+ )
128
144
 
129
145
  @auto_task
130
146
  async def download_av_and_merge(
@@ -0,0 +1,84 @@
1
+ from typing import Any, TypeVar, ParamSpec
2
+ from asyncio import Task, create_task
3
+ from pathlib import Path
4
+ from functools import wraps
5
+ from collections.abc import Callable, Coroutine
6
+
7
+
8
+ class PathTask:
9
+ def __init__(self, task: Task[Path]):
10
+ self._task: Task[Path] = task
11
+ self._path: Path | None = None
12
+
13
+ async def get(self) -> Path:
14
+ if self._path is not None:
15
+ return self._path
16
+
17
+ self._path = await self._task
18
+ return self._path
19
+
20
+ def __await__(self):
21
+ return self.get().__await__()
22
+
23
+ async def safe_get(self) -> Path | None:
24
+ """任务失败, 返回 None"""
25
+ try:
26
+ return await self.get()
27
+ except Exception:
28
+ return None
29
+
30
+ @property
31
+ def uri(self) -> str | None:
32
+ return self._path.as_uri() if self._path is not None else None
33
+
34
+ def __repr__(self) -> str:
35
+ if self._path is not None:
36
+ return f"PathTask(path={self._path.name})"
37
+ else:
38
+ return f"PathTask(task={self._task.get_name()}, done={self._task.done()})"
39
+
40
+
41
+ class OptionalPathTask:
42
+ """封装可选的 PathTask, 提供便捷的 API 避免频繁判空"""
43
+
44
+ def __init__(self, path_task: PathTask | None = None):
45
+ self._path_task: PathTask | None = path_task
46
+
47
+ async def get(self) -> Path | None:
48
+ if self._path_task is None:
49
+ return None
50
+ return await self._path_task.get()
51
+
52
+ def __await__(self):
53
+ return self.get().__await__()
54
+
55
+ async def safe_get(self) -> Path | None:
56
+ """任务失败, 返回 None"""
57
+ if self._path_task is None:
58
+ return None
59
+ return await self._path_task.safe_get()
60
+
61
+ @property
62
+ def uri(self) -> str | None:
63
+ if self._path_task is None:
64
+ return None
65
+ return self._path_task.uri
66
+
67
+ def __repr__(self) -> str:
68
+ return f"{self._path_task}"
69
+
70
+
71
+ P = ParamSpec("P")
72
+ T = TypeVar("T")
73
+
74
+
75
+ def auto_task(func: Callable[P, Coroutine[Any, Any, Path]]) -> Callable[P, PathTask]:
76
+ """装饰器:自动将异步函数调用转换为 Task, 完整保留类型提示"""
77
+
78
+ @wraps(func)
79
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> PathTask:
80
+ coro = func(*args, **kwargs)
81
+ name = " | ".join(str(arg) for arg in args if isinstance(arg, str))
82
+ return PathTask(create_task(coro, name=func.__name__ + " | " + name))
83
+
84
+ return wrapper
@@ -101,8 +101,7 @@ class UniHelper:
101
101
  """文件 Seg"""
102
102
  if not display_name:
103
103
  display_name = file.name
104
- if not display_name:
105
- raise ValueError("文件名不能为空")
104
+
106
105
  if pconfig.use_base64:
107
106
  return File(raw=file.read_bytes(), name=display_name)
108
107
  else:
@@ -78,8 +78,8 @@ async def parser_handler(
78
78
  logger.debug(f"命中缓存: {cache_key}, 结果: {result}")
79
79
 
80
80
  # 3. 渲染内容消息并发送
81
- renderer = get_renderer(result.platform.name)
82
- async for message in renderer.render_messages(result):
81
+ renderer = get_renderer(result.platform.name)(result)
82
+ async for message in renderer.render_messages():
83
83
  await message.send()
84
84
 
85
85
  # 4. 缓存解析结果
@@ -23,7 +23,6 @@ from .data import (
23
23
  ImageContent,
24
24
  VideoContent,
25
25
  DynamicContent,
26
- GraphicsContent,
27
26
  )
28
27
 
29
28
  __all__ = [
@@ -31,7 +30,6 @@ __all__ = [
31
30
  "Author",
32
31
  "BaseParser",
33
32
  "DynamicContent",
34
- "GraphicsContent",
35
33
  "ImageContent",
36
34
  "ParseResult",
37
35
  "Platform",
@@ -40,14 +40,17 @@ class AcfunParser(BaseParser):
40
40
  video_name=f"acfun_{acid}.mp4",
41
41
  )
42
42
 
43
- video_content = self.create_video_content(video_task, cover_url=video_info.coverUrl)
43
+ video_content = self.create_video_content(
44
+ video_task,
45
+ cover_url=video_info.coverUrl,
46
+ )
44
47
 
45
48
  return self.result(
46
49
  title=video_info.title,
47
50
  text=video_info.text,
48
51
  author=author,
49
52
  timestamp=video_info.timestamp,
50
- contents=[video_content],
53
+ video=video_content,
51
54
  )
52
55
 
53
56
  async def parse_video_info(self, url: str):
@@ -4,9 +4,9 @@ from typing import TYPE_CHECKING, Any, TypeVar, ClassVar, cast
4
4
  from asyncio import Task
5
5
  from pathlib import Path
6
6
  from collections.abc import Callable, Coroutine
7
- from typing_extensions import Unpack
7
+ from typing_extensions import Unpack, final
8
8
 
9
- from .data import Platform, ParseResult, ParseResultKwargs
9
+ from .data import Platform, ParseResult, ImageContent, ParseResultKwargs
10
10
  from ..config import pconfig as pconfig
11
11
  from ..download import DOWNLOADER
12
12
  from ..constants import IOS_HEADER, COMMON_HEADER, ANDROID_HEADER, COMMON_TIMEOUT
@@ -15,6 +15,7 @@ from ..constants import PlatformEnum as PlatformEnum
15
15
  from ..exception import ParseException
16
16
  from ..exception import IgnoreException as IgnoreException
17
17
  from ..exception import DownloadException as DownloadException
18
+ from ..download.task import PathTask, OptionalPathTask
18
19
 
19
20
  T = TypeVar("T", bound="BaseParser")
20
21
  HandlerFunc = Callable[[T, Match[str]], Coroutine[Any, Any, ParseResult]]
@@ -83,9 +84,11 @@ class BaseParser:
83
84
  """获取所有已注册的 Parser 类"""
84
85
  return cls._registry
85
86
 
87
+ @final
86
88
  async def parse(self, keyword: str, searched: Match[str]) -> ParseResult:
87
89
  return await self._handlers[keyword](self, searched)
88
90
 
91
+ @final
89
92
  async def parse_with_redirect(
90
93
  self,
91
94
  url: str,
@@ -167,13 +170,13 @@ class BaseParser:
167
170
  avatar_task = None
168
171
  if avatar_url:
169
172
  avatar_task = DOWNLOADER.download_img(avatar_url, ext_headers=self.headers)
170
- return Author(name=name, avatar=avatar_task, description=description)
173
+ return Author(name=name, avatar=OptionalPathTask(avatar_task), description=description)
171
174
 
172
175
  def create_video_content(
173
176
  self,
174
- url_or_task: str | Task[Path],
177
+ url_or_task: str | Task[Path] | PathTask,
175
178
  cover_url: str | None = None,
176
- duration: float = 0.0,
179
+ duration: float | None = None,
177
180
  ):
178
181
  """创建视频内容"""
179
182
  from .data import VideoContent
@@ -181,18 +184,21 @@ class BaseParser:
181
184
  cover_task = None
182
185
  if cover_url:
183
186
  cover_task = DOWNLOADER.download_img(cover_url, ext_headers=self.headers)
187
+
184
188
  if isinstance(url_or_task, str):
185
- url_or_task = DOWNLOADER.download_video(url_or_task, ext_headers=self.headers)
189
+ path_task = DOWNLOADER.download_video(url_or_task, ext_headers=self.headers)
190
+ elif isinstance(url_or_task, Task):
191
+ path_task = PathTask(url_or_task)
192
+ elif isinstance(url_or_task, PathTask):
193
+ path_task = url_or_task
186
194
 
187
- return VideoContent(url_or_task, cover_task, duration)
195
+ return VideoContent(path_task, OptionalPathTask(cover_task), duration)
188
196
 
189
197
  def create_image_contents(
190
198
  self,
191
199
  image_urls: list[str],
192
200
  ):
193
201
  """创建图片内容列表"""
194
- from .data import ImageContent
195
-
196
202
  contents: list[ImageContent] = []
197
203
  for url in image_urls:
198
204
  task = DOWNLOADER.download_img(url, ext_headers=self.headers)
@@ -201,15 +207,18 @@ class BaseParser:
201
207
 
202
208
  def create_image_content(
203
209
  self,
204
- url_or_task: str | Task[Path],
210
+ url_or_task: str | Task[Path] | PathTask,
211
+ alt: str | None = None,
205
212
  ):
206
213
  """创建图片内容"""
207
- from .data import ImageContent
208
-
209
214
  if isinstance(url_or_task, str):
210
- url_or_task = DOWNLOADER.download_img(url_or_task, ext_headers=self.headers)
215
+ path_task = DOWNLOADER.download_img(url_or_task, ext_headers=self.headers)
216
+ elif isinstance(url_or_task, Task):
217
+ path_task = PathTask(url_or_task)
218
+ elif isinstance(url_or_task, PathTask):
219
+ path_task = url_or_task
211
220
 
212
- return ImageContent(url_or_task)
221
+ return ImageContent(path_task, alt=alt)
213
222
 
214
223
  def create_dynamic_contents(
215
224
  self,
@@ -224,30 +233,38 @@ class BaseParser:
224
233
  contents.append(DynamicContent(task))
225
234
  return contents
226
235
 
227
- def create_audio_content(
236
+ def create_dynamic_content(
228
237
  self,
229
- url_or_task: str | Task[Path],
230
- duration: float = 0.0,
238
+ url_or_task: str | Task[Path] | PathTask,
231
239
  ):
232
- """创建音频内容"""
233
- from .data import AudioContent
240
+ """创建动态图片内容"""
241
+ from .data import DynamicContent
234
242
 
235
243
  if isinstance(url_or_task, str):
236
- url_or_task = DOWNLOADER.download_audio(url_or_task, ext_headers=self.headers)
244
+ path_task = DOWNLOADER.download_video(url_or_task, ext_headers=self.headers)
245
+ elif isinstance(url_or_task, Task):
246
+ path_task = PathTask(url_or_task)
247
+ elif isinstance(url_or_task, PathTask):
248
+ path_task = url_or_task
237
249
 
238
- return AudioContent(url_or_task, duration)
250
+ return DynamicContent(path_task)
239
251
 
240
- def create_graphics_content(
252
+ def create_audio_content(
241
253
  self,
242
- image_url: str,
243
- text: str | None = None,
244
- alt: str | None = None,
254
+ url_or_task: str | Task[Path] | PathTask,
255
+ duration: float = 0.0,
245
256
  ):
246
- """创建图文内容 图片不能为空 文字可空 渲染时文字在前 图片在后"""
247
- from .data import GraphicsContent
257
+ """创建音频内容"""
258
+ from .data import AudioContent
259
+
260
+ if isinstance(url_or_task, str):
261
+ path_task = DOWNLOADER.download_audio(url_or_task, ext_headers=self.headers)
262
+ elif isinstance(url_or_task, Task):
263
+ path_task = PathTask(url_or_task)
264
+ elif isinstance(url_or_task, PathTask):
265
+ path_task = url_or_task
248
266
 
249
- image_task = DOWNLOADER.download_img(image_url, ext_headers=self.headers)
250
- return GraphicsContent(image_task, text, alt)
267
+ return AudioContent(path_task, duration)
251
268
 
252
269
  @property
253
270
  def downloader(self):