nonebot-plugin-parser 2.6.1__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.1 → nonebot_plugin_parser-2.6.2}/PKG-INFO +3 -3
  2. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/README.md +1 -1
  3. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/pyproject.toml +6 -8
  4. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/acfun/__init__.py +1 -1
  5. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/base.py +25 -8
  6. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +8 -17
  7. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/data.py +24 -12
  8. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +1 -1
  9. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/tiktok.py +1 -1
  10. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/twitter.py +5 -12
  11. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/weibo/__init__.py +2 -3
  12. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/base.py +9 -23
  13. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/templates/card.html.jinja2 +43 -45
  14. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/utils.py +26 -7
  15. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/__init__.py +0 -0
  16. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/config.py +0 -0
  17. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/constants.py +0 -0
  18. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/download/__init__.py +0 -0
  19. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/download/task.py +0 -0
  20. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  21. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/exception.py +0 -0
  22. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/helper.py +0 -0
  23. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/matchers/__init__.py +0 -0
  24. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  25. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/matchers/rule.py +0 -0
  26. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/__init__.py +0 -0
  27. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/acfun/video.py +0 -0
  28. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  29. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  30. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
  31. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  32. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  33. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  34. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  35. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  36. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  37. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
  38. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py +0 -0
  39. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/kuaishou/states.py +0 -0
  40. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
  41. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/task.py +0 -0
  42. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/utils.py +0 -0
  43. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/weibo/article.py +0 -0
  44. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/weibo/common.py +0 -0
  45. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/weibo/show.py +0 -0
  46. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py +0 -0
  47. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/xiaohongshu/common.py +0 -0
  48. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/xiaohongshu/discovery.py +0 -0
  49. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/xiaohongshu/explore.py +0 -0
  50. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/youtube/__init__.py +0 -0
  51. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/parsers/youtube/meta.py +0 -0
  52. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/__init__.py +0 -0
  53. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/common.py +0 -0
  54. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/default.py +0 -0
  55. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/htmlrender.py +0 -0
  56. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW.ttf +0 -0
  57. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/__init__.py +0 -0
  58. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/avatar.png +0 -0
  59. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  60. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  61. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/1.jpg +0 -0
  62. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/2.jpg +0 -0
  63. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/3.jpg +0 -0
  64. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/4.jpg +0 -0
  65. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/5.jpg +0 -0
  66. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/6.jpg +0 -0
  67. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/7.jpg +0 -0
  68. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/8.jpg +0 -0
  69. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/fallback_pic/9.jpg +0 -0
  70. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  71. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/play.png +0 -0
  72. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  73. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  74. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  75. {nonebot_plugin_parser-2.6.1 → nonebot_plugin_parser-2.6.2}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  76. {nonebot_plugin_parser-2.6.1 → 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.1
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
@@ -24,7 +24,7 @@ Requires-Dist: pillow>=11.0.0
24
24
  Requires-Dist: aiofiles>=25.1.0
25
25
  Requires-Dist: httpx>=0.27.2,<1.0.0
26
26
  Requires-Dist: msgspec>=0.20.0,<1.0.0
27
- Requires-Dist: nonebot2>=2.4.3,<3.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
@@ -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
 
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parser"
3
- version = "2.6.1"
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"
@@ -44,7 +44,7 @@ dependencies = [
44
44
  "aiofiles>=25.1.0",
45
45
  "httpx>=0.27.2,<1.0.0",
46
46
  "msgspec>=0.20.0,<1.0.0",
47
- "nonebot2>=2.4.3,<3.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",
@@ -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.1"
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]]
@@ -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):
@@ -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
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
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,
@@ -39,15 +39,15 @@ class BilibiliParser(BaseParser):
39
39
  self._credential: Credential | None = None
40
40
  self._cookies_file = pconfig.config_dir / "bilibili_cookies.json"
41
41
 
42
- @handle("b23.tv", r"b23\.tv/[A-Za-z\d\._?%&+\-=/#]+")
43
- @handle("bili2233", r"bili2233\.cn/[A-Za-z\d\._?%&+\-=/#]+")
42
+ @handle("b23.tv", r"b23\.tv/[0-9a-zA-Z._?%&+-=/#]+")
43
+ @handle("bili2233", r"bili2233\.cn/[0-9a-zA-Z._?%&+-=/#]+")
44
44
  async def _parse_short_link(self, searched: Match[str]):
45
45
  """解析短链"""
46
46
  url = f"https://{searched.group(0)}"
47
47
  return await self.parse_with_redirect(url)
48
48
 
49
49
  @handle("BV", r"^(?P<bvid>BV[0-9a-zA-Z]{10})(?:\s)?(?P<page_num>\d{1,3})?$")
50
- @handle("/BV", r"bilibili\.com(?:/video)?/(?P<bvid>BV[0-9a-zA-Z]{10})(?:\?p=(?P<page_num>\d{1,3}))?")
50
+ @handle("/BV", r"bilibili\.com(?:/video)?/(?P<bvid>BV[0-9A-Za-z]{10})(?:.*?[?&]p=(?P<page_num>\d{1,3}))?")
51
51
  async def _parse_bv(self, searched: Match[str]):
52
52
  """解析视频信息"""
53
53
  bvid = str(searched.group("bvid"))
@@ -56,7 +56,7 @@ class BilibiliParser(BaseParser):
56
56
  return await self.parse_video(bvid=bvid, page_num=page_num)
57
57
 
58
58
  @handle("av", r"^av(?P<avid>\d{6,})(?:\s)?(?P<page_num>\d{1,3})?$")
59
- @handle("/av", r"bilibili\.com(?:/video)?/av(?P<avid>\d{6,})(?:\?p=(?P<page_num>\d{1,3}))?")
59
+ @handle("/av", r"bilibili\.com(?:/video)?/av(?P<avid>\d{6,})(?:.*?[?&]p=(?P<page_num>\d{1,3}))?")
60
60
  async def _parse_av(self, searched: Match[str]):
61
61
  """解析视频信息"""
62
62
  avid = int(searched.group("avid"))
@@ -65,8 +65,8 @@ class BilibiliParser(BaseParser):
65
65
  return await self.parse_video(avid=avid, page_num=page_num)
66
66
 
67
67
  @handle("/dynamic/", r"bilibili\.com/dynamic/(?P<dynamic_id>\d+)")
68
- @handle("t.bili", r"t\.bilibili\.com/(?P<dynamic_id>\d+)")
69
68
  @handle("/opus/", r"bilibili\.com/opus/(?P<dynamic_id>\d+)")
69
+ @handle("t.bili", r"t\.bilibili\.com/(?P<dynamic_id>\d+)")
70
70
  async def _parse_dynamic(self, searched: Match[str]):
71
71
  """解析动态信息"""
72
72
  dynamic_id = int(searched.group("dynamic_id"))
@@ -105,7 +105,7 @@ class BilibiliParser(BaseParser):
105
105
 
106
106
  from .video import VideoInfo, AIConclusion
107
107
 
108
- video = await self._get_video(bvid=bvid, avid=avid)
108
+ video = Video(bvid=bvid, aid=avid, credential=await self.credential)
109
109
  video_info = convert(await video.get_info(), VideoInfo)
110
110
  # UP
111
111
  author = self.create_author(video_info.owner.name, video_info.owner.face)
@@ -160,7 +160,7 @@ class BilibiliParser(BaseParser):
160
160
  timestamp=page_info.timestamp,
161
161
  text=video_info.desc,
162
162
  author=author,
163
- video=video_content,
163
+ contents=[video_content],
164
164
  extra={"info": ai_summary},
165
165
  )
166
166
 
@@ -298,15 +298,6 @@ class BilibiliParser(BaseParser):
298
298
  graphics=graphics,
299
299
  )
300
300
 
301
- async def _get_video(self, *, bvid: str | None = None, avid: int | None = None) -> Video:
302
- """解析视频"""
303
- if avid:
304
- return Video(aid=avid, credential=await self.credential)
305
- elif bvid:
306
- return Video(bvid=bvid, credential=await self.credential)
307
- else:
308
- raise ParseException("avid 和 bvid 至少指定一项")
309
-
310
301
  async def extract_download_urls(
311
302
  self,
312
303
  video: Video | None = None,
@@ -324,7 +315,7 @@ class BilibiliParser(BaseParser):
324
315
  )
325
316
 
326
317
  if video is None:
327
- video = await self._get_video(bvid=bvid, avid=avid)
318
+ video = Video(bvid=bvid, aid=avid, credential=await self.credential)
328
319
 
329
320
  # 获取下载数据
330
321
  download_url_data = await video.get_download_url(page_index=page_index)
@@ -39,8 +39,10 @@ class VideoContent(MediaContent):
39
39
  """视频封面"""
40
40
  duration: float | None = None
41
41
  """时长 单位: 秒"""
42
- gif_path: Path | None = None
43
- """视频转 GIF """
42
+ is_gif: bool = False
43
+ """是否是 GIF"""
44
+ gif_path: PathTask | None = None
45
+ """视频转为 GIF 的路径"""
44
46
 
45
47
  @property
46
48
  def display_duration(self) -> str | None:
@@ -110,10 +112,8 @@ class ParseResult:
110
112
  url: str | None = None
111
113
  """来源链接"""
112
114
 
113
- video: VideoContent | None = None
114
- """视频内容"""
115
115
  contents: list[MediaContent] = field(default_factory=list)
116
- """其他媒体内容"""
116
+ """媒体内容"""
117
117
  graphics: list[str | ImageContent] = field(default_factory=list)
118
118
  """图文内容"""
119
119
 
@@ -146,9 +146,22 @@ class ParseResult:
146
146
  def extra_info(self) -> str | None:
147
147
  return self.extra.get("info")
148
148
 
149
+ @property
150
+ def video(self) -> VideoContent | None:
151
+ """主视频 (只有 contents 首项为视频的时候才返回 否则为 None)"""
152
+ if len(self.contents) != 1:
153
+ return None
154
+ cont = self.contents[0]
155
+ return cont if isinstance(cont, VideoContent) and not cont.is_gif else None
156
+
157
+ @video.setter
158
+ def video(self, video: VideoContent | None):
159
+ if video is not None and len(self.contents) == 0:
160
+ self.contents.append(video)
161
+
149
162
  @property
150
163
  def video_contents(self) -> list[VideoContent]:
151
- """获取视频内容(如果有)"""
164
+ """获取所有视频内容(如果有)"""
152
165
  return [cont for cont in self.contents if isinstance(cont, VideoContent)]
153
166
 
154
167
  @property
@@ -171,6 +184,11 @@ class ParseResult:
171
184
  covers.append(cont.path_task)
172
185
  return covers
173
186
 
187
+ @property
188
+ def grid_medias(self) -> list[VideoContent | ImageContent]:
189
+ """获取所有用于渲染图片网格的媒体内容(视频封面 + 图片)"""
190
+ return [cont for cont in self.contents if isinstance(cont, (VideoContent, ImageContent))]
191
+
174
192
  @property
175
193
  def formartted_datetime(self, fmt: str = "%Y-%m-%d %H:%M:%S") -> str | None:
176
194
  """格式化时间戳"""
@@ -184,11 +202,6 @@ class ParseResult:
184
202
  if author.avatar:
185
203
  yield author.avatar.get()
186
204
 
187
- if self.video:
188
- if self.video.cover:
189
- yield self.video.cover.get()
190
- yield self.video.path_task.get()
191
-
192
205
  for cont in self.contents:
193
206
  if not img_only or isinstance(cont, ImageContent):
194
207
  yield cont.path_task.get()
@@ -248,7 +261,6 @@ class ParseResult:
248
261
  class ParseResultKwargs(TypedDict, total=False):
249
262
  title: str | None
250
263
  text: str | None
251
- video: VideoContent | None
252
264
  contents: list[MediaContent]
253
265
  graphics: list[str | ImageContent]
254
266
  timestamp: int | None
@@ -130,7 +130,7 @@ class DouyinParser(BaseParser):
130
130
  # 优先取动图
131
131
  if dynamic_urls := slides_data.dynamic_urls:
132
132
  for dynamic_url in dynamic_urls:
133
- result.contents.append(self.create_video(dynamic_url))
133
+ result.contents.append(self.create_gif(dynamic_url))
134
134
  elif image_urls := slides_data.image_urls:
135
135
  result.contents.extend(self.create_images(image_urls))
136
136
 
@@ -31,6 +31,6 @@ class TikTokParser(BaseParser):
31
31
  return self.result(
32
32
  title=video_info.title,
33
33
  author=Author(name=video_info.channel),
34
- video=video_content,
34
+ contents=[video_content],
35
35
  timestamp=video_info.timestamp,
36
36
  )
@@ -81,23 +81,16 @@ class TwitterParser(BaseParser):
81
81
  )
82
82
 
83
83
  for media in data.media_extended:
84
- if media.type == "video":
85
- result.video = self.create_video(
84
+ if media.type in ["video", "gif"]:
85
+ video = self.create_video(
86
86
  media.url,
87
87
  media.thumbnail_url,
88
88
  duration=media.duration,
89
+ is_gif=media.type == "gif",
89
90
  )
90
- break
91
+ result.contents.append(video)
91
92
  elif media.type == "image":
92
93
  result.contents.append(self.create_image(media.url))
93
- elif media.type == "gif":
94
- result.contents.append(
95
- self.create_video(
96
- media.url,
97
- media.thumbnail_url,
98
- duration=media.duration,
99
- )
100
- )
101
94
 
102
95
  result.repost = self._collect_result(data.qrt) if data.qrt else None
103
96
 
@@ -166,7 +159,7 @@ class TwitterParser(BaseParser):
166
159
  elif "下载图片" in text:
167
160
  result.contents.append(self.create_image(href))
168
161
  elif "下载 gif" in text:
169
- result.contents.append(self.create_video(href))
162
+ result.contents.append(self.create_gif(href))
170
163
 
171
164
  # 3. 提取标题
172
165
  title_tag = soup.find("h3")
@@ -150,7 +150,7 @@ class WeiBoParser(BaseParser):
150
150
  author=author,
151
151
  title=play_info.title,
152
152
  text=play_info.text,
153
- video=video_content,
153
+ contents=[video_content],
154
154
  timestamp=play_info.real_date,
155
155
  )
156
156
 
@@ -209,14 +209,13 @@ class WeiBoParser(BaseParser):
209
209
  url=data.url,
210
210
  )
211
211
 
212
- # 添加视频内容
212
+ # 主视频
213
213
  if video_url := data.video_url:
214
214
  result.video = self.create_video(
215
215
  video_url,
216
216
  data.cover_url,
217
217
  data.duration,
218
218
  )
219
-
220
219
  # 添加图片内容
221
220
  if image_urls := data.image_urls:
222
221
  result.contents.extend(self.create_images(image_urls))
@@ -11,7 +11,7 @@ import aiofiles
11
11
  from ..config import pconfig
12
12
  from ..helper import UniHelper, UniMessage, ForwardNodeInner
13
13
  from ..parsers import ParseResult, AudioContent, ImageContent, VideoContent
14
- from ..exception import IgnoreException, DownloadException
14
+ from ..exception import DownloadException
15
15
 
16
16
 
17
17
  class BaseRenderer(ABC):
@@ -39,16 +39,10 @@ class BaseRenderer(ABC):
39
39
  other_segs: list[ForwardNodeInner] = []
40
40
 
41
41
  def on_error(e: Exception):
42
- if isinstance(e, IgnoreException):
43
- pass
44
- elif isinstance(e, DownloadException):
42
+ if isinstance(e, DownloadException):
45
43
  nonlocal failed_count
46
44
  failed_count += 1
47
45
 
48
- if self.result.video:
49
- if video_path := await self.result.video.path_task.safe_get(on_error):
50
- yield UniMessage(UniHelper.video_seg(video_path))
51
-
52
46
  for cont in chain(
53
47
  self.result.contents,
54
48
  self.result.repost.contents if self.result.repost else (),
@@ -59,17 +53,16 @@ class BaseRenderer(ABC):
59
53
 
60
54
  match cont:
61
55
  case VideoContent() as video:
62
- thumbnail = await video.cover.safe_get() if video.cover else None
63
- other_segs.append(UniHelper.video_seg(path, thumbnail))
56
+ if video.gif_path and (gif_path := await video.gif_path.safe_get()):
57
+ mergeable_segs.append(UniHelper.img_seg(gif_path))
58
+ else:
59
+ thumbnail = await video.cover.safe_get() if video.cover else None
60
+ yield UniMessage(UniHelper.video_seg(path, thumbnail))
64
61
  case AudioContent():
65
62
  yield UniMessage(UniHelper.record_seg(path))
66
63
  case ImageContent():
67
64
  mergeable_segs.append(UniHelper.img_seg(path))
68
65
 
69
- if self.result.repost and self.result.repost.video:
70
- if video_path := await self.result.repost.video.path_task.safe_get(on_error):
71
- yield UniMessage(UniHelper.video_seg(video_path))
72
-
73
66
  for cont in chain(
74
67
  self.result.graphics,
75
68
  self.result.repost.graphics if self.result.repost else (),
@@ -85,15 +78,8 @@ class BaseRenderer(ABC):
85
78
  mergeable_segs.append(img_seg)
86
79
 
87
80
  if mergeable_segs or other_segs:
88
- if (
89
- pconfig.need_forward_contents
90
- or len(mergeable_segs) > 4
91
- or len(other_segs) > 1
92
- or (len(mergeable_segs) + len(other_segs)) > 4
93
- ):
94
- forward_msg = UniHelper.construct_forward_message(
95
- mergeable_segs + other_segs,
96
- )
81
+ if pconfig.need_forward_contents or len(other_segs) > 1 or (len(mergeable_segs) + len(other_segs)) > 4:
82
+ forward_msg = UniHelper.construct_forward_message(mergeable_segs + other_segs)
97
83
  yield UniMessage(forward_msg)
98
84
  else:
99
85
  if mergeable_segs:
@@ -577,52 +577,50 @@
577
577
  {% if result.title and result.video %}<div class="title">{{ result.title }}</div>{% endif %}
578
578
  {# ── Text(非动态类型) ── #}
579
579
  {% if content_type != '动态' and result.text %}<div class="text">{{ result.text }}</div>{% endif %}
580
- {# ── 非视频媒体 ── #}
581
- {% if not video %}
582
- {# ── Image Grid ── #}
583
- {% set imgs = result.all_grid_images %}
584
- {% if imgs %}
585
- {% set count = imgs|length %}
586
- {% if count == 1 %}
587
- {% set img = imgs[0] %}
588
- <div class="image-container">
589
- <img src="{{ img.uri | safe }}"
590
- class="single-image"
591
- alt="image" />
592
- </div>
593
- {% else %}
594
- {% set cols = 2 if count in [2, 4] else 3 %}
595
- <div class="image-grid cols-{{ cols }}">
596
- {% for img in imgs[:9] %}
597
- {% set img_uri = (img.uri if img.uri else fallback_pic) %}
598
- <div class="grid-item">
599
- <img src="{{ img_uri | safe }}" alt="image" />
600
- {% if loop.last and count > 9 %}
601
- <div class="more-count">+{{ count - 9 }}</div>
602
- {% endif %}
603
- </div>
604
- {% endfor %}
605
- </div>
606
- {% endif %}
607
- {# ── Graphics ── #}
608
- {% elif result.graphics %}
609
- {% for text_or_img in result.graphics %}
610
- <div class="graphics-item">
611
- {% if text_or_img is string %}
612
- <div class="text">{{ text_or_img }}</div>
613
- {% else %}
614
- {% set img_uri = text_or_img.path_task.uri %}
615
- {% set img_uri = (img_uri if img_uri else fallback_pic) %}
616
- <div class="image-container">
617
- <img src="{{ img_uri | safe }}"
618
- class="single-image"
619
- alt="graphics" />
620
- </div>
621
- {% endif %}
622
- {% if text_or_img.alt %}<div class="graphics-alt">{{ text_or_img.alt }}</div>{% endif %}
623
- </div>
624
- {% endfor %}
580
+
581
+ {# ── Image Grid ── #}
582
+ {% set imgs = result.all_grid_images %}
583
+ {% if imgs %}
584
+ {% set count = imgs|length %}
585
+ {% if count == 1 %}
586
+ {% set img = imgs[0] %}
587
+ <div class="image-container">
588
+ <img src="{{ img.uri | safe }}"
589
+ class="single-image"
590
+ alt="image" />
591
+ </div>
592
+ {% else %}
593
+ {% set cols = 2 if count in [2, 4] else 3 %}
594
+ <div class="image-grid cols-{{ cols }}">
595
+ {% for img in imgs[:9] %}
596
+ {% set img_uri = (img.uri if img.uri else fallback_pic) %}
597
+ <div class="grid-item">
598
+ <img src="{{ img_uri | safe }}" alt="image" />
599
+ {% if loop.last and count > 9 %}
600
+ <div class="more-count">+{{ count - 9 }}</div>
601
+ {% endif %}
602
+ </div>
603
+ {% endfor %}
604
+ </div>
625
605
  {% endif %}
606
+ {# ── Graphics ── #}
607
+ {% elif result.graphics %}
608
+ {% for text_or_img in result.graphics %}
609
+ <div class="graphics-item">
610
+ {% if text_or_img is string %}
611
+ <div class="text">{{ text_or_img }}</div>
612
+ {% else %}
613
+ {% set img_uri = text_or_img.path_task.uri %}
614
+ {% set img_uri = (img_uri if img_uri else fallback_pic) %}
615
+ <div class="image-container">
616
+ <img src="{{ img_uri | safe }}"
617
+ class="single-image"
618
+ alt="graphics" />
619
+ </div>
620
+ {% endif %}
621
+ {% if text_or_img.alt %}<div class="graphics-alt">{{ text_or_img.alt }}</div>{% endif %}
622
+ </div>
623
+ {% endfor %}
626
624
  {% endif %}
627
625
  {# ── Repost ── #}
628
626
  {% if result.repost %}{{ render_card(result.repost, is_repost=True) }}{% endif %}
@@ -144,11 +144,11 @@ async def encode_video_to_h264(video_path: Path) -> Path:
144
144
  return output_path
145
145
 
146
146
 
147
- async def extract_video_cover(video_path: Path) -> Path:
148
- """从视频中提取封面图(第一帧)"""
149
- cover_path = video_path.with_suffix(".jpg")
150
- if cover_path.exists():
151
- return cover_path
147
+ async def extract_video_first_frame(video_path: Path) -> Path:
148
+ """从视频中提取第一帧"""
149
+ first_frame_path = video_path.with_suffix(".jpg")
150
+ if first_frame_path.exists():
151
+ return first_frame_path
152
152
 
153
153
  cmd = [
154
154
  "ffmpeg",
@@ -159,11 +159,30 @@ async def extract_video_cover(video_path: Path) -> Path:
159
159
  "00:00:01",
160
160
  "-vframes",
161
161
  "1",
162
- str(cover_path),
162
+ str(first_frame_path),
163
163
  ]
164
164
 
165
165
  await exec_ffmpeg_cmd(cmd)
166
- return cover_path
166
+ return first_frame_path
167
+
168
+
169
+ async def convert_video_to_gif(video_path: Path) -> Path:
170
+ """将视频转换为 GIF"""
171
+ gif_path = video_path.with_suffix(".gif")
172
+ if gif_path.exists():
173
+ return gif_path
174
+
175
+ cmd = [
176
+ "ffmpeg",
177
+ "-y",
178
+ "-i",
179
+ str(video_path),
180
+ "-c:v",
181
+ "gif",
182
+ str(gif_path),
183
+ ]
184
+ await exec_ffmpeg_cmd(cmd)
185
+ return gif_path
167
186
 
168
187
 
169
188
  def fmt_size(file_path: Path) -> str: