nonebot-plugin-parser 2.5.3__tar.gz → 2.6.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 (79) hide show
  1. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/PKG-INFO +27 -32
  2. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/README.md +23 -28
  3. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/pyproject.toml +6 -6
  4. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/config.py +7 -0
  5. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/download/__init__.py +114 -58
  6. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/download/task.py +20 -0
  7. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/helper.py +23 -8
  8. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/matchers/__init__.py +3 -3
  9. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/__init__.py +2 -4
  10. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/acfun/__init__.py +1 -1
  11. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/base.py +42 -64
  12. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +7 -7
  13. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/data.py +29 -24
  14. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +15 -11
  15. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/douyin/video.py +4 -0
  16. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py +6 -3
  17. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/kuaishou/states.py +4 -0
  18. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/nga.py +1 -1
  19. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/parsers/task.py +47 -0
  20. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/tiktok.py +4 -4
  21. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/twitter.py +12 -6
  22. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/parsers/utils.py +9 -0
  23. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/weibo/__init__.py +10 -5
  24. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/weibo/common.py +13 -0
  25. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/weibo/show.py +5 -0
  26. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py +15 -11
  27. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/parsers/xiaohongshu/common.py +38 -0
  28. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/xiaohongshu/discovery.py +9 -4
  29. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/xiaohongshu/explore.py +14 -4
  30. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/youtube/__init__.py +5 -5
  31. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/__init__.py +44 -0
  32. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/base.py +26 -18
  33. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/common.py +19 -13
  34. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/default.py +1 -1
  35. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/htmlrender.py +5 -5
  36. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/__init__.py +7 -0
  37. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/resources/fallback_pic/1.jpg +0 -0
  38. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/resources/fallback_pic/2.jpg +0 -0
  39. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/resources/fallback_pic/3.jpg +0 -0
  40. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/resources/fallback_pic/4.jpg +0 -0
  41. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/resources/fallback_pic/5.jpg +0 -0
  42. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/resources/fallback_pic/6.jpg +0 -0
  43. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/resources/fallback_pic/7.jpg +0 -0
  44. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/resources/fallback_pic/8.jpg +0 -0
  45. nonebot_plugin_parser-2.6.1/src/nonebot_plugin_parser/renders/resources/fallback_pic/9.jpg +0 -0
  46. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/templates/card.html.jinja2 +36 -9
  47. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/utils.py +25 -12
  48. nonebot_plugin_parser-2.5.3/src/nonebot_plugin_parser/download/task.py +0 -92
  49. nonebot_plugin_parser-2.5.3/src/nonebot_plugin_parser/parsers/xiaohongshu/common.py +0 -33
  50. nonebot_plugin_parser-2.5.3/src/nonebot_plugin_parser/renders/__init__.py +0 -43
  51. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/__init__.py +0 -0
  52. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/constants.py +0 -0
  53. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  54. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/exception.py +0 -0
  55. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  56. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/matchers/rule.py +0 -0
  57. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/acfun/video.py +0 -0
  58. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  59. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  60. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
  61. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  62. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  63. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  64. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  65. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  66. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  67. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/weibo/article.py +0 -0
  68. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/parsers/youtube/meta.py +0 -0
  69. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW.ttf +0 -0
  70. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/avatar.png +0 -0
  71. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  72. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  73. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  74. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/play.png +0 -0
  75. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  76. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  77. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  78. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.1}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  79. {nonebot_plugin_parser-2.5.3 → nonebot_plugin_parser-2.6.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.5.3
3
+ Version: 2.6.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
@@ -19,20 +19,20 @@ Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Topic :: Communications :: Chat
20
20
  Classifier: Topic :: Internet :: WWW/HTTP
21
21
  Classifier: Topic :: Multimedia :: Video
22
- Requires-Dist: nonebot2>=2.4.3,<3.0.0
23
22
  Requires-Dist: rich>=13.0.0
24
23
  Requires-Dist: pillow>=11.0.0
25
24
  Requires-Dist: aiofiles>=25.1.0
26
25
  Requires-Dist: httpx>=0.27.2,<1.0.0
27
26
  Requires-Dist: msgspec>=0.20.0,<1.0.0
27
+ Requires-Dist: nonebot2>=2.4.3,<3.0.0
28
28
  Requires-Dist: apilmoji[rich]>=0.3.1,<1.0.0
29
29
  Requires-Dist: beautifulsoup4>=4.12.0,<5.0.0
30
30
  Requires-Dist: curl-cffi>=0.13.0,!=0.14.0,<1.0.0
31
31
  Requires-Dist: bilibili-api-python>=17.4.1,<18.0.0
32
+ Requires-Dist: nonebot-plugin-uninfo>=0.10.1,<1.0.0
32
33
  Requires-Dist: nonebot-plugin-alconna>=0.60.4,<1.0.0
33
- Requires-Dist: nonebot-plugin-apscheduler>=0.5.0,<1.0.0
34
34
  Requires-Dist: nonebot-plugin-localstore>=0.7.4,<1.0.0
35
- Requires-Dist: nonebot-plugin-uninfo>=0.10.1,<1.0.0
35
+ Requires-Dist: nonebot-plugin-apscheduler>=0.5.0,<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
38
  Requires-Dist: yt-dlp[default]>=2026.3.13 ; extra == 'all'
@@ -71,7 +71,7 @@ Description-Content-Type: text/markdown
71
71
 
72
72
  </div>
73
73
 
74
- > [!IMPORTANT]
74
+ > [!IMPORTANT]
75
75
  > **收藏项目**,你将从 GitHub 上无延迟地接收所有发布通知~ ⭐️
76
76
 
77
77
  <img width="100%" src="https://starify.komoridevs.icu/api/starify?owner=fllesser&repo=nonebot-plugin-parser" alt="starify" />
@@ -81,14 +81,14 @@ Description-Content-Type: text/markdown
81
81
  | 平台 | 触发的消息形态 | 视频 | 图集 | 音频 |
82
82
  | ------- | --------------------------------- | ---- | ---- | ---- |
83
83
  | B 站 | av 号/BV 号/链接/短链/卡片/小程序 | ✅​ | ✅​ | ✅​ |
84
- | 抖音 | 链接(分享链接,兼容电脑端链接) | ✅​ | ✅​ | ❌️ |
85
- | 微博 | 链接(博文,视频,show, 文章) | ✅​ | ✅​ | ❌️ |
86
- | 小红书 | 链接(含短链)/卡片 | ✅​ | ✅​ | ❌️ |
87
- | 快手 | 链接(包含标准链接和短链) | ✅​ | ✅​ | ❌️ |
88
- | acfun | 链接 | ✅​ | ❌️ | ❌️ |
89
- | youtube | 链接(含短链) | ✅​ | ❌️ | ✅​ |
90
- | tiktok | 链接 | ✅​ | ❌️ | ❌️ |
91
- | twitter | 链接 | ✅​ | ✅​ | ❌️ |
84
+ | 抖音 | 链接(分享链接,兼容电脑端链接) | ✅​ | ✅​ | ❌️ |
85
+ | 微博 | 链接(博文,视频,show, 文章) | ✅​ | ✅​ | ❌️ |
86
+ | 小红书 | 链接(含短链)/卡片 | ✅​ | ✅​ | ❌️ |
87
+ | 快手 | 链接(包含标准链接和短链) | ✅​ | ✅​ | ❌️ |
88
+ | acfun | 链接 | ✅​ | ❌️ | ❌️ |
89
+ | youtube | 链接(含短链) | ✅​ | ❌️ | ✅​ |
90
+ | tiktok | 链接 | ✅​ | ❌️ | ❌️ |
91
+ | twitter | 链接 | ✅​ | ✅​ | ❌️ |
92
92
 
93
93
  支持的链接,可参考 [测试链接](https://github.com/fllesser/nonebot-plugin-parser/blob/master/tests/others/test_urls.md)
94
94
 
@@ -108,7 +108,7 @@ Description-Content-Type: text/markdown
108
108
 
109
109
  ## 💿 安装
110
110
 
111
- > [!Warning]
111
+ > [!Warning]
112
112
  > **如果你已经在使用 nonebot-plugin-resolver[2],请在安装此插件前卸载**
113
113
 
114
114
  <details>
@@ -304,6 +304,9 @@ parser_append_url=False
304
304
  # 例如: ./config/nonebot_plugin_parser/
305
305
  parser_custom_font="LXGWZhenKaiGB-Regular.ttf"
306
306
 
307
+ # [可选] 用户名及标题字体粗细程度(仅 htmlrender),如果使用 LXGW ZhenKai 字体建议设置为 500~600 之间
308
+ parser_custom_font_weight: int = 700
309
+
307
310
  # [可选] 是否需要转发媒体内容(超过 4 项时始终使用合并转发)
308
311
  parser_need_forward_contents=True
309
312
 
@@ -390,18 +393,19 @@ class ExampleParser(BaseParser):
390
393
 
391
394
  # 4. 视频内容
392
395
  author = self.create_author(author_name, avatar_url)
393
- video = self.create_video_content(video_url, cover_url, duration)
396
+ video = self.create_video(video_url, cover_url, duration)
394
397
 
395
398
  # 5. 图集内容
396
399
  image_urls = data.get("images")
397
- images = self.create_image_contents(image_urls)
400
+ images = self.create_image(image_urls)
398
401
 
399
402
  # 6. 返回解析结果
400
403
  return self.result(
401
404
  title=title,
402
405
  text=description,
403
406
  author=author,
404
- contents=[video, *images],
407
+ video=video,
408
+ contents=[*images],
405
409
  timestamp=timestamp,
406
410
  url=f"https://example.com/video/{video_id}",
407
411
  )
@@ -412,7 +416,7 @@ class ExampleParser(BaseParser):
412
416
  <details>
413
417
  <summary>辅助函数</summary>
414
418
 
415
- > 构建作者信息
419
+ > 构建作者
416
420
 
417
421
  ```python
418
422
  author = self.create_author(
@@ -426,7 +430,7 @@ author = self.create_author(
426
430
 
427
431
  ```python
428
432
  # 方式1:传入 URL,自动下载
429
- video = self.create_video_content(
433
+ video = self.create_video(
430
434
  url_or_task="https://example.com/video.mp4",
431
435
  cover_url="https://example.com/cover.jpg", # 可选
432
436
  duration=120.5 # 可选,单位:秒
@@ -435,7 +439,7 @@ video = self.create_video_content(
435
439
  # 方式2:传入已创建的下载任务
436
440
  from nonebot_plugin_parser.download import DOWNLOADER
437
441
  video_task = DOWNLOADER.download_video(url, ext_headers=self.headers)
438
- video = self.create_video_content(
442
+ video = self.create_video(
439
443
  url_or_task=video_task,
440
444
  cover_url=cover_url,
441
445
  duration=duration
@@ -446,21 +450,12 @@ video = self.create_video_content(
446
450
 
447
451
  ```python
448
452
  # 并发下载图集内容
449
- images = self.create_image_contents([
453
+ images = self.create_images([
450
454
  "https://example.com/img1.jpg",
451
455
  "https://example.com/img2.jpg",
452
456
  ])
453
457
  ```
454
458
 
455
- > 创建动图内容(GIF),平台一般只提供视频(后续插件会做自动转为 gif 的处理)
456
-
457
- ```python
458
- dynamics = self.create_dynamic_contents([
459
- "https://example.com/dynamic1.mp4",
460
- "https://example.com/dynamic2.mp4",
461
- ])
462
- ```
463
-
464
459
  > 重定向 url
465
460
 
466
461
  ```python
@@ -478,5 +473,5 @@ real_url = await self.get_redirect_url(
478
473
 
479
474
  ## 🎉 致谢
480
475
 
481
- [nonebot-plugin-resolver](https://github.com/zhiyu1998/nonebot-plugin-resolver)
482
- [parse-video-py](https://github.com/wujunwei928/parse-video-py)
476
+ - [nonebot-plugin-resolver](https://github.com/zhiyu1998/nonebot-plugin-resolver) - 本项目最初基于此插件进行开发,在此表示感谢。尽管当前版本代码已完全重构,但仍感谢原项目提供的初始思路和参考。
477
+ - [parse-video-py](https://github.com/wujunwei928/parse-video-py) - 在抖音解析功能实现方面提供了技术参考和借鉴。
@@ -17,7 +17,7 @@
17
17
 
18
18
  </div>
19
19
 
20
- > [!IMPORTANT]
20
+ > [!IMPORTANT]
21
21
  > **收藏项目**,你将从 GitHub 上无延迟地接收所有发布通知~ ⭐️
22
22
 
23
23
  <img width="100%" src="https://starify.komoridevs.icu/api/starify?owner=fllesser&repo=nonebot-plugin-parser" alt="starify" />
@@ -27,14 +27,14 @@
27
27
  | 平台 | 触发的消息形态 | 视频 | 图集 | 音频 |
28
28
  | ------- | --------------------------------- | ---- | ---- | ---- |
29
29
  | B 站 | av 号/BV 号/链接/短链/卡片/小程序 | ✅​ | ✅​ | ✅​ |
30
- | 抖音 | 链接(分享链接,兼容电脑端链接) | ✅​ | ✅​ | ❌️ |
31
- | 微博 | 链接(博文,视频,show, 文章) | ✅​ | ✅​ | ❌️ |
32
- | 小红书 | 链接(含短链)/卡片 | ✅​ | ✅​ | ❌️ |
33
- | 快手 | 链接(包含标准链接和短链) | ✅​ | ✅​ | ❌️ |
34
- | acfun | 链接 | ✅​ | ❌️ | ❌️ |
35
- | youtube | 链接(含短链) | ✅​ | ❌️ | ✅​ |
36
- | tiktok | 链接 | ✅​ | ❌️ | ❌️ |
37
- | twitter | 链接 | ✅​ | ✅​ | ❌️ |
30
+ | 抖音 | 链接(分享链接,兼容电脑端链接) | ✅​ | ✅​ | ❌️ |
31
+ | 微博 | 链接(博文,视频,show, 文章) | ✅​ | ✅​ | ❌️ |
32
+ | 小红书 | 链接(含短链)/卡片 | ✅​ | ✅​ | ❌️ |
33
+ | 快手 | 链接(包含标准链接和短链) | ✅​ | ✅​ | ❌️ |
34
+ | acfun | 链接 | ✅​ | ❌️ | ❌️ |
35
+ | youtube | 链接(含短链) | ✅​ | ❌️ | ✅​ |
36
+ | tiktok | 链接 | ✅​ | ❌️ | ❌️ |
37
+ | twitter | 链接 | ✅​ | ✅​ | ❌️ |
38
38
 
39
39
  支持的链接,可参考 [测试链接](https://github.com/fllesser/nonebot-plugin-parser/blob/master/tests/others/test_urls.md)
40
40
 
@@ -54,7 +54,7 @@
54
54
 
55
55
  ## 💿 安装
56
56
 
57
- > [!Warning]
57
+ > [!Warning]
58
58
  > **如果你已经在使用 nonebot-plugin-resolver[2],请在安装此插件前卸载**
59
59
 
60
60
  <details>
@@ -250,6 +250,9 @@ parser_append_url=False
250
250
  # 例如: ./config/nonebot_plugin_parser/
251
251
  parser_custom_font="LXGWZhenKaiGB-Regular.ttf"
252
252
 
253
+ # [可选] 用户名及标题字体粗细程度(仅 htmlrender),如果使用 LXGW ZhenKai 字体建议设置为 500~600 之间
254
+ parser_custom_font_weight: int = 700
255
+
253
256
  # [可选] 是否需要转发媒体内容(超过 4 项时始终使用合并转发)
254
257
  parser_need_forward_contents=True
255
258
 
@@ -336,18 +339,19 @@ class ExampleParser(BaseParser):
336
339
 
337
340
  # 4. 视频内容
338
341
  author = self.create_author(author_name, avatar_url)
339
- video = self.create_video_content(video_url, cover_url, duration)
342
+ video = self.create_video(video_url, cover_url, duration)
340
343
 
341
344
  # 5. 图集内容
342
345
  image_urls = data.get("images")
343
- images = self.create_image_contents(image_urls)
346
+ images = self.create_image(image_urls)
344
347
 
345
348
  # 6. 返回解析结果
346
349
  return self.result(
347
350
  title=title,
348
351
  text=description,
349
352
  author=author,
350
- contents=[video, *images],
353
+ video=video,
354
+ contents=[*images],
351
355
  timestamp=timestamp,
352
356
  url=f"https://example.com/video/{video_id}",
353
357
  )
@@ -358,7 +362,7 @@ class ExampleParser(BaseParser):
358
362
  <details>
359
363
  <summary>辅助函数</summary>
360
364
 
361
- > 构建作者信息
365
+ > 构建作者
362
366
 
363
367
  ```python
364
368
  author = self.create_author(
@@ -372,7 +376,7 @@ author = self.create_author(
372
376
 
373
377
  ```python
374
378
  # 方式1:传入 URL,自动下载
375
- video = self.create_video_content(
379
+ video = self.create_video(
376
380
  url_or_task="https://example.com/video.mp4",
377
381
  cover_url="https://example.com/cover.jpg", # 可选
378
382
  duration=120.5 # 可选,单位:秒
@@ -381,7 +385,7 @@ video = self.create_video_content(
381
385
  # 方式2:传入已创建的下载任务
382
386
  from nonebot_plugin_parser.download import DOWNLOADER
383
387
  video_task = DOWNLOADER.download_video(url, ext_headers=self.headers)
384
- video = self.create_video_content(
388
+ video = self.create_video(
385
389
  url_or_task=video_task,
386
390
  cover_url=cover_url,
387
391
  duration=duration
@@ -392,21 +396,12 @@ video = self.create_video_content(
392
396
 
393
397
  ```python
394
398
  # 并发下载图集内容
395
- images = self.create_image_contents([
399
+ images = self.create_images([
396
400
  "https://example.com/img1.jpg",
397
401
  "https://example.com/img2.jpg",
398
402
  ])
399
403
  ```
400
404
 
401
- > 创建动图内容(GIF),平台一般只提供视频(后续插件会做自动转为 gif 的处理)
402
-
403
- ```python
404
- dynamics = self.create_dynamic_contents([
405
- "https://example.com/dynamic1.mp4",
406
- "https://example.com/dynamic2.mp4",
407
- ])
408
- ```
409
-
410
405
  > 重定向 url
411
406
 
412
407
  ```python
@@ -424,5 +419,5 @@ real_url = await self.get_redirect_url(
424
419
 
425
420
  ## 🎉 致谢
426
421
 
427
- [nonebot-plugin-resolver](https://github.com/zhiyu1998/nonebot-plugin-resolver)
428
- [parse-video-py](https://github.com/wujunwei928/parse-video-py)
422
+ - [nonebot-plugin-resolver](https://github.com/zhiyu1998/nonebot-plugin-resolver) - 本项目最初基于此插件进行开发,在此表示感谢。尽管当前版本代码已完全重构,但仍感谢原项目提供的初始思路和参考。
423
+ - [parse-video-py](https://github.com/wujunwei928/parse-video-py) - 在抖音解析功能实现方面提供了技术参考和借鉴。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parser"
3
- version = "2.5.3"
3
+ version = "2.6.1"
4
4
  description = "NoneBot2 链接分享解析 Alconna 版, 现支持B站|抖音|快手|微博|小红书|YouTube|TikTok|Twitter|AcFun|NGA"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -39,20 +39,20 @@ classifiers = [
39
39
  ]
40
40
 
41
41
  dependencies = [
42
- "nonebot2>=2.4.3,<3.0.0",
43
42
  "rich>=13.0.0",
44
43
  "pillow>=11.0.0",
45
44
  "aiofiles>=25.1.0",
46
45
  "httpx>=0.27.2,<1.0.0",
47
46
  "msgspec>=0.20.0,<1.0.0",
47
+ "nonebot2>=2.4.3,<3.0.0",
48
48
  "apilmoji[rich]>=0.3.1,<1.0.0",
49
49
  "beautifulsoup4>=4.12.0,<5.0.0",
50
50
  "curl_cffi>=0.13.0,<1.0.0,!=0.14.0",
51
51
  "bilibili-api-python>=17.4.1,<18.0.0",
52
+ "nonebot-plugin-uninfo>=0.10.1,<1.0.0",
52
53
  "nonebot-plugin-alconna>=0.60.4,<1.0.0",
53
- "nonebot-plugin-apscheduler>=0.5.0,<1.0.0",
54
54
  "nonebot-plugin-localstore>=0.7.4,<1.0.0",
55
- "nonebot-plugin-uninfo>=0.10.1,<1.0.0",
55
+ "nonebot-plugin-apscheduler>=0.5.0,<1.0.0",
56
56
  ]
57
57
 
58
58
  [project.urls]
@@ -118,7 +118,7 @@ nonebug = { git = "https://github.com/nonebot/nonebug", rev = "master" }
118
118
  [tool.bumpversion]
119
119
  tag = true
120
120
  commit = true
121
- current_version = "2.5.3"
121
+ current_version = "2.6.1"
122
122
  message = "release: bump vesion from {current_version} to {new_version}"
123
123
 
124
124
  [[tool.bumpversion.files]]
@@ -150,7 +150,7 @@ test-render = "pytest tests/renders --cov=src --cov-report=xml --junitxml=junit.
150
150
  bump = "bump-my-version bump"
151
151
  show-bump = "bump-my-version show-bump"
152
152
 
153
- [tool.pyright]
153
+ [tool.basedpyright]
154
154
  pythonVersion = "3.10"
155
155
  pythonPlatform = "All"
156
156
  defineConstant = { PYDANTIC_V2 = true }
@@ -48,6 +48,8 @@ class Config(BaseModel):
48
48
  """Renderer 类型"""
49
49
  parser_custom_font: str | None = None
50
50
  """自定义字体"""
51
+ parser_custom_font_weight: int = 700
52
+ """字体粗细程度"""
51
53
  parser_need_forward_contents: bool = True
52
54
  """是否需要转发媒体内容"""
53
55
  parser_emoji_cdn: str = ELK_SH_CDN
@@ -160,6 +162,11 @@ class Config(BaseModel):
160
162
 
161
163
  return font_path
162
164
 
165
+ @property
166
+ def custom_font_weight(self) -> int:
167
+ """字体粗细程度"""
168
+ return self.parser_custom_font_weight
169
+
163
170
  @property
164
171
  def need_forward_contents(self) -> bool:
165
172
  """是否需要转发媒体内容"""
@@ -4,9 +4,10 @@ from functools import partial
4
4
  from contextlib import contextmanager
5
5
  from urllib.parse import urljoin
6
6
 
7
+ import httpx
7
8
  import aiofiles
8
- from httpx import HTTPError, AsyncClient
9
- from nonebot import logger
9
+ import curl_cffi
10
+ from nonebot import logger, get_driver
10
11
  from rich.progress import (
11
12
  Progress,
12
13
  BarColumn,
@@ -15,22 +16,27 @@ from rich.progress import (
15
16
  )
16
17
 
17
18
  from .task import auto_task
18
- from ..utils import merge_av, safe_unlink, generate_file_name
19
+ from ..utils import merge_av, safe_unlink, generate_file_name, is_module_available
19
20
  from ..config import pconfig
20
21
  from ..constants import COMMON_HEADER, DOWNLOAD_TIMEOUT
21
22
  from ..exception import IgnoreException, DownloadException
22
23
 
23
24
 
24
25
  class StreamDownloader:
25
- """Downloader class for downloading files with stream"""
26
-
27
26
  def __init__(self):
28
27
  self.headers: dict[str, str] = COMMON_HEADER.copy()
29
28
  self.cache_dir: Path = pconfig.cache_dir
30
- self.client: AsyncClient = AsyncClient(timeout=DOWNLOAD_TIMEOUT, verify=False)
29
+ self.client: httpx.AsyncClient = httpx.AsyncClient(timeout=DOWNLOAD_TIMEOUT, verify=False)
30
+
31
+ async def aclose(self):
32
+ await self.client.aclose()
31
33
 
34
+ @staticmethod
32
35
  @contextmanager
33
- def rich_progress(self, desc: str, total: int | None = None):
36
+ def rich_progress(
37
+ desc: str,
38
+ total: int | None = None,
39
+ ):
34
40
  with Progress(
35
41
  TextColumn("[bold blue]{task.description}", justify="right"),
36
42
  BarColumn(bar_width=None),
@@ -41,53 +47,83 @@ class StreamDownloader:
41
47
  task_id = progress.add_task(description=desc, total=total)
42
48
  yield partial(progress.update, task_id)
43
49
 
44
- async def _download_file(
50
+ @staticmethod
51
+ def _validate_content_length(
52
+ response: httpx.Response | curl_cffi.Response,
53
+ ) -> int:
54
+ """获取文件长度"""
55
+ content_length = response.headers.get("Content-Length")
56
+ content_length = int(content_length) if content_length else 0
57
+
58
+ if content_length == 0:
59
+ logger.warning(f"媒体 url: {response.url}, 大小为 0, 取消下载")
60
+ raise IgnoreException
61
+
62
+ if (file_size := content_length / 1024 / 1024) > pconfig.max_size:
63
+ logger.warning(f"媒体 url: {response.url} 大小 {file_size:.2f} MB, 超过 {pconfig.max_size} MB, 取消下载")
64
+ raise IgnoreException
65
+
66
+ return content_length
67
+
68
+ async def _download_file_with_httpx(
45
69
  self,
46
70
  url: str,
47
71
  *,
48
- file_name: str | None = None,
49
- ext_headers: dict[str, str] | None = None,
72
+ file_path: Path,
73
+ headers: dict[str, str],
50
74
  chunk_size: int = 64 * 1024,
51
75
  ) -> Path:
52
76
  """download file by url with stream"""
53
- if not file_name:
54
- file_name = generate_file_name(url)
55
- file_path = self.cache_dir / file_name
56
- # 如果文件存在,则直接返回
57
- if file_path.exists():
58
- return file_path
59
77
 
60
- headers = {**self.headers, **(ext_headers or {})}
78
+ async with self.client.stream(
79
+ "GET",
80
+ url,
81
+ headers=headers,
82
+ follow_redirects=True,
83
+ ) as response:
84
+ response.raise_for_status()
85
+ content_length = self._validate_content_length(response)
86
+
87
+ with self.rich_progress(
88
+ f"httpx | {file_path.name}",
89
+ content_length,
90
+ ) as update_progress:
91
+ async with aiofiles.open(file_path, "wb") as file:
92
+ async for chunk in response.aiter_bytes(chunk_size):
93
+ await file.write(chunk)
94
+ update_progress(advance=len(chunk))
61
95
 
62
- try:
63
- async with self.client.stream("GET", url, headers=headers, follow_redirects=True) as response:
64
- response.raise_for_status()
65
- content_length = response.headers.get("Content-Length")
66
- content_length = int(content_length) if content_length else 0
67
-
68
- if content_length == 0:
69
- logger.warning(f"媒体 url: {url}, 大小为 0, 取消下载")
70
- raise IgnoreException
71
-
72
- if (file_size := content_length / 1024 / 1024) > pconfig.max_size:
73
- logger.warning(f"媒体 url: {url} 大小 {file_size:.2f} MB, 超过 {pconfig.max_size} MB, 取消下载")
74
- raise IgnoreException
75
-
76
- with self.rich_progress(file_name, content_length) as update_progress:
77
- async with aiofiles.open(file_path, "wb") as file:
78
- async for chunk in response.aiter_bytes(chunk_size):
79
- await file.write(chunk)
80
- update_progress(advance=len(chunk))
81
-
82
- except HTTPError:
83
- await safe_unlink(file_path)
84
- logger.exception(f"下载失败 | url: {url}, file_path: {file_path}")
85
- raise DownloadException("媒体下载失败")
96
+ return file_path
97
+
98
+ async def _download_file_with_curl_cffi(
99
+ self,
100
+ url: str,
101
+ *,
102
+ file_path: Path,
103
+ headers: dict[str, str],
104
+ ) -> Path:
105
+ async with curl_cffi.AsyncSession(allow_redirects=True) as session:
106
+ response: curl_cffi.Response = await session.get(
107
+ url,
108
+ headers=headers,
109
+ timeout=DOWNLOAD_TIMEOUT,
110
+ stream=True,
111
+ )
112
+ response.raise_for_status()
113
+ content_length = self._validate_content_length(response)
114
+
115
+ with self.rich_progress(
116
+ f"curl_cffi | {file_path.name}",
117
+ content_length,
118
+ ) as update_progress:
119
+ async with aiofiles.open(file_path, "wb") as file:
120
+ async for chunk in response.aiter_content(chunk_size=8192):
121
+ await file.write(chunk)
122
+ update_progress(advance=len(chunk))
86
123
 
87
124
  return file_path
88
125
 
89
- @auto_task
90
- async def download_file(
126
+ async def _download_file(
91
127
  self,
92
128
  url: str,
93
129
  *,
@@ -95,13 +131,28 @@ class StreamDownloader:
95
131
  ext_headers: dict[str, str] | None = None,
96
132
  chunk_size: int = 64 * 1024,
97
133
  ) -> Path:
98
- """download file by url with stream"""
99
- return await self._download_file(
100
- url,
101
- file_name=file_name,
102
- ext_headers=ext_headers,
103
- chunk_size=chunk_size,
104
- )
134
+ """download file by url with fallback"""
135
+ if not file_name:
136
+ file_name = generate_file_name(url)
137
+ file_path = self.cache_dir / file_name
138
+ if file_path.exists():
139
+ return file_path
140
+
141
+ headers = {**self.headers, **(ext_headers or {})}
142
+
143
+ try:
144
+ path = await self._download_file_with_httpx(
145
+ url, file_path=file_path, headers=headers, chunk_size=chunk_size
146
+ )
147
+ except httpx.HTTPError:
148
+ logger.opt(exception=True).warning(f"下载失败(httpx) | url: {url}")
149
+ try:
150
+ path = await self._download_file_with_curl_cffi(url, file_path=file_path, headers=headers)
151
+ except curl_cffi.CurlError:
152
+ logger.opt(exception=True).warning(f"下载失败(curl_cffi) | url: {url}")
153
+ raise DownloadException("媒体下载失败")
154
+
155
+ return path
105
156
 
106
157
  @auto_task
107
158
  async def download_video(
@@ -199,7 +250,7 @@ class StreamDownloader:
199
250
  await f.write(chunk)
200
251
  total_size += len(chunk)
201
252
  update_progress(advance=len(chunk), total=total_size)
202
- except HTTPError:
253
+ except httpx.HTTPError:
203
254
  await safe_unlink(video_path)
204
255
  logger.exception("m3u8 视频下载失败")
205
256
  raise DownloadException("m3u8 视频下载失败")
@@ -224,13 +275,18 @@ class StreamDownloader:
224
275
  return slices
225
276
 
226
277
 
227
- DOWNLOADER: StreamDownloader = StreamDownloader()
228
-
229
- try:
230
- import yt_dlp as yt_dlp
278
+ downloader: StreamDownloader = StreamDownloader()
279
+ """全局下载器实例,提供下载功能"""
280
+ yt_dlp_downloader = None
281
+ """yt-dlp 下载器实例,提供下载视频功能,若 yt-dlp 未安装则为 None"""
231
282
 
283
+ if is_module_available("yt_dlp"):
232
284
  from .ytdlp import YtdlpDownloader
233
285
 
234
- YTDLP_DOWNLOADER = YtdlpDownloader()
235
- except ImportError:
236
- YTDLP_DOWNLOADER = None
286
+ yt_dlp_downloader = YtdlpDownloader()
287
+
288
+
289
+ @get_driver().on_shutdown
290
+ async def close_download_client():
291
+ logger.debug("正在关闭下载器...")
292
+ await downloader.aclose()
@@ -0,0 +1,20 @@
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
+ P = ParamSpec("P")
8
+ T = TypeVar("T")
9
+
10
+
11
+ def auto_task(func: Callable[P, Coroutine[Any, Any, Path]]) -> Callable[P, Task[Path]]:
12
+ """装饰器:自动将异步函数调用转换为 Task, 完整保留类型提示"""
13
+
14
+ @wraps(func)
15
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Task[Path]:
16
+ coro = func(*args, **kwargs)
17
+ name = " | ".join(str(arg) for arg in args if isinstance(arg, str))
18
+ return create_task(coro, name=func.__name__ + " | " + name)
19
+
20
+ return wrapper