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.
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/PKG-INFO +39 -23
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/README.md +38 -22
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/pyproject.toml +3 -3
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/config.py +13 -2
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/download/__init__.py +12 -11
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/download/ytdlp.py +10 -10
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/helper.py +50 -4
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/matchers/__init__.py +21 -63
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/matchers/rule.py +9 -1
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/__init__.py +3 -4
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/acfun.py +24 -30
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/base.py +74 -25
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +88 -113
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/video.py +21 -3
- nonebot_plugin_parser-2.2.0/src/nonebot_plugin_parser/parsers/douyin/__init__.py +128 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/kuaishou.py +13 -14
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/nga.py +8 -14
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/tiktok.py +3 -7
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/twitter.py +3 -7
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/weibo.py +31 -30
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/xiaohongshu.py +33 -33
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/youtube.py +22 -26
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/__init__.py +9 -9
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/common.py +218 -96
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/utils.py +1 -1
- nonebot_plugin_parser-2.1.2/src/nonebot_plugin_parser/parsers/douyin/__init__.py +0 -174
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/__init__.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/constants.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/download/task.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/exception.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/data.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/base.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/default.py +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW-1.ttf +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/media_button.png +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/resources/youtube.png +0 -0
- {nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -0
- {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.
|
|
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
|
-
|
|
171
|
+
部分解析依赖 `ffmpeg`
|
|
172
|
+
|
|
173
|
+
`ubuntu/debian`
|
|
173
174
|
|
|
174
175
|
sudo apt-get install ffmpeg
|
|
175
176
|
|
|
176
|
-
其他
|
|
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 |
|
|
259
|
-
| ym |
|
|
275
|
+
| bm | - | 否 | 群聊 | 下载 B 站音频 |
|
|
276
|
+
| ym | - | 否 | 群聊 | 下载 youtube 音频 |
|
|
260
277
|
|
|
261
278
|
## 🧩 扩展
|
|
262
279
|
> [!IMPORTANT]
|
|
263
|
-
> 插件自 `v2.
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
+
部分解析依赖 `ffmpeg`
|
|
126
|
+
|
|
127
|
+
`ubuntu/debian`
|
|
127
128
|
|
|
128
129
|
sudo apt-get install ffmpeg
|
|
129
130
|
|
|
130
|
-
其他
|
|
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 |
|
|
213
|
-
| ym |
|
|
229
|
+
| bm | - | 否 | 群聊 | 下载 B 站音频 |
|
|
230
|
+
| ym | - | 否 | 群聊 | 下载 youtube 音频 |
|
|
214
231
|
|
|
215
232
|
## 🧩 扩展
|
|
216
233
|
> [!IMPORTANT]
|
|
217
|
-
> 插件自 `v2.
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
89
|
+
current_version = "2.2.0"
|
|
90
90
|
commit = true
|
|
91
|
-
message = "
|
|
91
|
+
message = "release: bump vesion from {current_version} to {new_version}"
|
|
92
92
|
tag = true
|
|
93
93
|
|
|
94
94
|
[[tool.bumpversion.files]]
|
{nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/config.py
RENAMED
|
@@ -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] = [
|
|
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
|
|
37
|
-
ext_headers (dict[str, str] | 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
|
|
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
|
|
115
|
-
ext_headers (dict[str, str] | 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
|
|
140
|
-
ext_headers (dict[str, str] | 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
|
|
165
|
-
ext_headers (dict[str, str]
|
|
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
|
|
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],
|
|
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
|
|
49
|
-
self._ydl_download_base_opts["proxy"] =
|
|
50
|
-
self._ydl_extract_base_opts["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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
{nonebot_plugin_parser-2.1.2 → nonebot_plugin_parser-2.2.0}/src/nonebot_plugin_parser/helper.py
RENAMED
|
@@ -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
|
|
6
|
-
from nonebot_plugin_alconna
|
|
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 =
|
|
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
|
|
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
|
-
|
|
25
|
+
enabled_classes = _get_enabled_parser_classes()
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
for _cls in
|
|
27
|
+
enabled_platforms = []
|
|
28
|
+
for _cls in enabled_classes:
|
|
32
29
|
parser = _cls()
|
|
33
|
-
|
|
34
|
-
for keyword, _ in _cls.
|
|
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(
|
|
33
|
+
logger.info(f"启用平台: {', '.join(sorted(enabled_platforms))}")
|
|
37
34
|
|
|
38
|
-
patterns = [p for _cls in
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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=
|
|
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
|
|