nonebot-plugin-parser 2.6.0__tar.gz → 2.6.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 (76) hide show
  1. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/PKG-INFO +8 -8
  2. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/README.md +4 -4
  3. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/pyproject.toml +9 -11
  4. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/download/__init__.py +114 -58
  5. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/helper.py +10 -5
  6. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/matchers/__init__.py +3 -3
  7. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/__init__.py +2 -2
  8. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/acfun/__init__.py +1 -1
  9. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/base.py +35 -18
  10. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +9 -18
  11. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/data.py +26 -12
  12. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +1 -1
  13. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py +5 -2
  14. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/kuaishou/states.py +4 -0
  15. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/tiktok.py +4 -4
  16. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/twitter.py +5 -12
  17. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/weibo/__init__.py +2 -3
  18. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/youtube/__init__.py +3 -3
  19. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/__init__.py +2 -9
  20. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/base.py +9 -23
  21. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/templates/card.html.jinja2 +43 -45
  22. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/utils.py +33 -7
  23. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/__init__.py +0 -0
  24. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/config.py +0 -0
  25. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/constants.py +0 -0
  26. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/download/task.py +0 -0
  27. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  28. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/exception.py +0 -0
  29. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  30. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/matchers/rule.py +0 -0
  31. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/acfun/video.py +0 -0
  32. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  33. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  34. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
  35. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  36. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  37. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  38. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  39. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  40. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  41. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
  42. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
  43. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/task.py +0 -0
  44. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/utils.py +0 -0
  45. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/weibo/article.py +0 -0
  46. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/weibo/common.py +0 -0
  47. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/weibo/show.py +0 -0
  48. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py +0 -0
  49. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/xiaohongshu/common.py +0 -0
  50. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/xiaohongshu/discovery.py +0 -0
  51. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/xiaohongshu/explore.py +0 -0
  52. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/youtube/meta.py +0 -0
  53. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/common.py +0 -0
  54. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/default.py +0 -0
  55. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/htmlrender.py +0 -0
  56. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW.ttf +0 -0
  57. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/__init__.py +0 -0
  58. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/avatar.png +0 -0
  59. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  60. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  61. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/1.jpg +0 -0
  62. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/2.jpg +0 -0
  63. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/3.jpg +0 -0
  64. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/4.jpg +0 -0
  65. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/5.jpg +0 -0
  66. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/6.jpg +0 -0
  67. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/7.jpg +0 -0
  68. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/8.jpg +0 -0
  69. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/9.jpg +0 -0
  70. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  71. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/play.png +0 -0
  72. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  73. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  74. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  75. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  76. {nonebot_plugin_parser-2.6.0 → nonebot_plugin_parser-2.6.2}/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.6.0
3
+ Version: 2.6.2
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.5.0,<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'
@@ -65,7 +65,7 @@ Description-Content-Type: text/markdown
65
65
  [![uv](https://img.shields.io/badge/package%20manager-uv-black?style=flat-square&logo=uv)](https://github.com/astral-sh/uv)
66
66
  [![ruff](https://img.shields.io/badge/code%20style-ruff-black?style=flat-square&logo=ruff)](https://github.com/astral-sh/ruff)
67
67
  <br/>
68
- [![pre-commit](https://results.pre-commit.ci/badge/github/fllesser/nonebot-plugin-parser/master.svg)](https://results.pre-commit.ci/latest/github/fllesser/nonebot-plugin-parser/master)
68
+ [![prek](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/docs/assets/badge-v0.json)](https://github.com/j178/prek)
69
69
  [![codecov](https://codecov.io/gh/fllesser/nonebot-plugin-parser/graph/badge.svg?token=VCS8IHSO7U)](https://codecov.io/gh/fllesser/nonebot-plugin-parser)
70
70
  [![qqgroup](https://img.shields.io/badge/QQ%E7%BE%A4-820082006-orange?style=flat-square)](https://qm.qq.com/q/y4T4CjHimc)
71
71
 
@@ -416,7 +416,7 @@ class ExampleParser(BaseParser):
416
416
  <details>
417
417
  <summary>辅助函数</summary>
418
418
 
419
- > 构建作者信息
419
+ > 构建作者
420
420
 
421
421
  ```python
422
422
  author = self.create_author(
@@ -473,5 +473,5 @@ real_url = await self.get_redirect_url(
473
473
 
474
474
  ## 🎉 致谢
475
475
 
476
- - [nonebot-plugin-resolver](https://github.com/zhiyu1998/nonebot-plugin-resolver) | 初代解析插件
477
- - [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) - 在抖音解析功能实现方面提供了技术参考和借鉴。
@@ -11,7 +11,7 @@
11
11
  [![uv](https://img.shields.io/badge/package%20manager-uv-black?style=flat-square&logo=uv)](https://github.com/astral-sh/uv)
12
12
  [![ruff](https://img.shields.io/badge/code%20style-ruff-black?style=flat-square&logo=ruff)](https://github.com/astral-sh/ruff)
13
13
  <br/>
14
- [![pre-commit](https://results.pre-commit.ci/badge/github/fllesser/nonebot-plugin-parser/master.svg)](https://results.pre-commit.ci/latest/github/fllesser/nonebot-plugin-parser/master)
14
+ [![prek](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/docs/assets/badge-v0.json)](https://github.com/j178/prek)
15
15
  [![codecov](https://codecov.io/gh/fllesser/nonebot-plugin-parser/graph/badge.svg?token=VCS8IHSO7U)](https://codecov.io/gh/fllesser/nonebot-plugin-parser)
16
16
  [![qqgroup](https://img.shields.io/badge/QQ%E7%BE%A4-820082006-orange?style=flat-square)](https://qm.qq.com/q/y4T4CjHimc)
17
17
 
@@ -362,7 +362,7 @@ class ExampleParser(BaseParser):
362
362
  <details>
363
363
  <summary>辅助函数</summary>
364
364
 
365
- > 构建作者信息
365
+ > 构建作者
366
366
 
367
367
  ```python
368
368
  author = self.create_author(
@@ -419,5 +419,5 @@ real_url = await self.get_redirect_url(
419
419
 
420
420
  ## 🎉 致谢
421
421
 
422
- - [nonebot-plugin-resolver](https://github.com/zhiyu1998/nonebot-plugin-resolver) | 初代解析插件
423
- - [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.6.0"
3
+ version = "2.6.2"
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.5.0,<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]
@@ -74,7 +74,7 @@ all = [
74
74
 
75
75
  [dependency-groups]
76
76
  dev = [
77
- "nonebot2[fastapi]>=2.4.3,<3.0.0",
77
+ "nonebot2[fastapi]>=2.5.0,<3.0.0",
78
78
  "nonebot-adapter-onebot>=2.4.6",
79
79
  "ruff>=0.15.6,<1.0.0",
80
80
  { include-group = "extras" },
@@ -88,8 +88,8 @@ extras = [
88
88
  "types-yt-dlp>=2026.3.13.20260314",
89
89
  ]
90
90
  test = [
91
- "nonebug>=0.4.3,<1.0.0",
92
- "poethepoet>=0.42.1",
91
+ "nonebug>=0.4.4,<1.0.0",
92
+ "poethepoet>=0.42.1,<1.0.0",
93
93
  "pytest-asyncio>=1.3.0,<1.4.0",
94
94
  "pytest-cov>=7.0.0",
95
95
  "pytest-xdist>=3.8.0,<4.0.0",
@@ -112,13 +112,11 @@ conflicts = [
112
112
  ],
113
113
  ]
114
114
 
115
- [tool.uv.sources]
116
- nonebug = { git = "https://github.com/nonebot/nonebug", rev = "master" }
117
115
 
118
116
  [tool.bumpversion]
119
117
  tag = true
120
118
  commit = true
121
- current_version = "2.6.0"
119
+ current_version = "2.6.2"
122
120
  message = "release: bump vesion from {current_version} to {new_version}"
123
121
 
124
122
  [[tool.bumpversion.files]]
@@ -150,7 +148,7 @@ test-render = "pytest tests/renders --cov=src --cov-report=xml --junitxml=junit.
150
148
  bump = "bump-my-version bump"
151
149
  show-bump = "bump-my-version show-bump"
152
150
 
153
- [tool.pyright]
151
+ [tool.basedpyright]
154
152
  pythonVersion = "3.10"
155
153
  pythonPlatform = "All"
156
154
  defineConstant = { PYDANTIC_V2 = true }
@@ -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()
@@ -31,6 +31,14 @@ EMOJI_MAP = {
31
31
  "resolving": ("424", "👀"),
32
32
  "done": ("144", "🎉"),
33
33
  }
34
+ """emoji 映射"""
35
+
36
+ ID_ADAPTERS = {
37
+ SupportAdapter.onebot11,
38
+ SupportAdapter.qq,
39
+ SupportAdapter.milky,
40
+ }
41
+ """支持的传入 emoji id 发送 reaction 的适配器"""
34
42
 
35
43
 
36
44
  class UniHelper:
@@ -127,15 +135,12 @@ class UniHelper:
127
135
  message_id = uniseg.get_message_id(event)
128
136
  target = uniseg.get_target(event)
129
137
 
130
- if target.adapter in (SupportAdapter.onebot11, SupportAdapter.qq):
131
- emoji = EMOJI_MAP[status][0]
132
- else:
133
- emoji = EMOJI_MAP[status][1]
138
+ emoji = EMOJI_MAP[status][0 if target.adapter in ID_ADAPTERS else 1]
134
139
 
135
140
  try:
136
141
  await uniseg.message_reaction(emoji, message_id=message_id)
137
142
  except Exception:
138
- logger.warning(f"reaction {emoji} to {message_id} failed, maybe not support")
143
+ logger.opt(exception=True).warning(f"reaction {emoji} to {message_id} failed, maybe not support")
139
144
 
140
145
  @classmethod
141
146
  def with_reaction(cls, func: Callable[..., Awaitable[Any]]):
@@ -112,9 +112,9 @@ async def _(message: Message = CommandArg()):
112
112
  await UniMessage(UniHelper.file_seg(audio_path)).send()
113
113
 
114
114
 
115
- from ..download import YTDLP_DOWNLOADER
115
+ from ..download import yt_dlp_downloader
116
116
 
117
- if YTDLP_DOWNLOADER is not None:
117
+ if yt_dlp_downloader is not None:
118
118
  from ..parsers import YouTubeParser
119
119
 
120
120
  @on_command("ym", priority=3, block=True).handle()
@@ -128,7 +128,7 @@ if YTDLP_DOWNLOADER is not None:
128
128
 
129
129
  url = matched.group(0)
130
130
 
131
- audio_path = await YTDLP_DOWNLOADER.download_audio(url)
131
+ audio_path = await yt_dlp_downloader.download_audio(url)
132
132
  await UniMessage(UniHelper.record_seg(audio_path)).send()
133
133
 
134
134
  if pconfig.need_upload:
@@ -7,10 +7,10 @@ from .douyin import DouyinParser as DouyinParser
7
7
  from .twitter import TwitterParser as TwitterParser
8
8
  from .bilibili import BilibiliParser as BilibiliParser
9
9
  from .kuaishou import KuaiShouParser as KuaiShouParser
10
- from ..download import YTDLP_DOWNLOADER
10
+ from ..download import yt_dlp_downloader as yt_dlp_downloader
11
11
  from .xiaohongshu import XiaoHongShuParser as XiaoHongShuParser
12
12
 
13
- if YTDLP_DOWNLOADER is not None:
13
+ if yt_dlp_downloader is not None:
14
14
  from .tiktok import TikTokParser as TikTokParser
15
15
  from .youtube import YouTubeParser as YouTubeParser
16
16
 
@@ -50,7 +50,7 @@ class AcfunParser(BaseParser):
50
50
  text=video_info.text,
51
51
  author=author,
52
52
  timestamp=video_info.timestamp,
53
- video=video_content,
53
+ contents=[video_content],
54
54
  )
55
55
 
56
56
  async def parse_video_info(self, url: str):
@@ -1,15 +1,15 @@
1
1
  from re import Match, Pattern, compile
2
2
  from abc import ABC
3
- from typing import TYPE_CHECKING, Any, TypeVar, ClassVar, cast
3
+ from typing import TYPE_CHECKING, Any, TypeVar, ClassVar, cast, final
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, final
7
+ from typing_extensions import Unpack
8
8
 
9
9
  from .data import Platform, ParseResult, ImageContent, ParseResultKwargs
10
10
  from .task import PathTask
11
11
  from ..config import pconfig as pconfig
12
- from ..download import DOWNLOADER
12
+ from ..download import downloader
13
13
  from ..constants import IOS_HEADER, COMMON_HEADER, ANDROID_HEADER, COMMON_TIMEOUT
14
14
  from ..constants import DOWNLOAD_TIMEOUT as DOWNLOAD_TIMEOUT
15
15
  from ..constants import PlatformEnum as PlatformEnum
@@ -170,7 +170,7 @@ class BaseParser:
170
170
  author = Author(name=name, description=description)
171
171
 
172
172
  if avatar_url:
173
- author.avatar = PathTask(DOWNLOADER.download_img(avatar_url, ext_headers=self.headers))
173
+ author.avatar = PathTask(downloader.download_img(avatar_url, ext_headers=self.headers))
174
174
 
175
175
  return author
176
176
 
@@ -179,31 +179,48 @@ class BaseParser:
179
179
  url_or_task: str | Task[Path],
180
180
  cover_url: str | None = None,
181
181
  duration: float | None = None,
182
+ is_gif: bool = False,
182
183
  ):
183
- """创建视频内容"""
184
+ """创建视频内容, 未指定封面时会尝试从视频中提取封面"""
184
185
  from .data import VideoContent
185
- from ..utils import extract_video_cover
186
+ from ..utils import convert_video_to_gif, extract_video_first_frame
186
187
 
187
188
  if isinstance(url_or_task, str):
188
- path_task = DOWNLOADER.download_video(url_or_task, ext_headers=self.headers)
189
+ path_task = downloader.download_video(url_or_task, ext_headers=self.headers)
189
190
  elif isinstance(url_or_task, Task):
190
191
  path_task = url_or_task
191
192
 
193
+ video_content = VideoContent(PathTask(path_task), duration=duration, is_gif=is_gif)
194
+
192
195
  if cover_url:
193
- cover_task = DOWNLOADER.download_img(cover_url, ext_headers=self.headers)
196
+ cover_task = downloader.download_img(cover_url, ext_headers=self.headers)
194
197
  else:
195
198
  # 如果没有封面 URL,尝试从视频中提取封面
196
199
  async def extract_cover():
197
200
  video_path = await path_task
198
- return await extract_video_cover(video_path)
201
+ return await extract_video_first_frame(video_path)
199
202
 
200
203
  cover_task = extract_cover()
201
204
 
202
- return VideoContent(
203
- PathTask(path_task),
204
- cover=PathTask(cover_task),
205
- duration=duration,
206
- )
205
+ video_content.cover = PathTask(cover_task)
206
+
207
+ if is_gif:
208
+ # 需要转换为 GIF
209
+ async def convert_to_gif():
210
+ video_path = await path_task
211
+ return await convert_video_to_gif(video_path)
212
+
213
+ video_content.gif_path = PathTask(convert_to_gif())
214
+
215
+ return video_content
216
+
217
+ def create_gif(
218
+ self,
219
+ url_or_task: str | Task[Path],
220
+ cover_url: str | None = None,
221
+ ):
222
+ """创建 GIF 内容"""
223
+ return self.create_video(url_or_task, cover_url=cover_url, is_gif=True)
207
224
 
208
225
  def create_images(
209
226
  self,
@@ -212,7 +229,7 @@ class BaseParser:
212
229
  """创建图片内容列表"""
213
230
  contents: list[ImageContent] = []
214
231
  for url in image_urls:
215
- task = DOWNLOADER.download_img(url, ext_headers=self.headers)
232
+ task = downloader.download_img(url, ext_headers=self.headers)
216
233
  contents.append(ImageContent(PathTask(task)))
217
234
  return contents
218
235
 
@@ -223,7 +240,7 @@ class BaseParser:
223
240
  ):
224
241
  """创建单个图片内容"""
225
242
  if isinstance(url_or_task, str):
226
- path_task = DOWNLOADER.download_img(url_or_task, ext_headers=self.headers)
243
+ path_task = downloader.download_img(url_or_task, ext_headers=self.headers)
227
244
  elif isinstance(url_or_task, Task):
228
245
  path_task = url_or_task
229
246
 
@@ -238,7 +255,7 @@ class BaseParser:
238
255
  from .data import AudioContent
239
256
 
240
257
  if isinstance(url_or_task, str):
241
- path_task = DOWNLOADER.download_audio(url_or_task, ext_headers=self.headers)
258
+ path_task = downloader.download_audio(url_or_task, ext_headers=self.headers)
242
259
  elif isinstance(url_or_task, Task):
243
260
  path_task = url_or_task
244
261
 
@@ -246,4 +263,4 @@ class BaseParser:
246
263
 
247
264
  @property
248
265
  def downloader(self):
249
- return DOWNLOADER
266
+ return downloader