nonebot-plugin-parser 2.0.5__tar.gz → 2.0.6__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 (41) hide show
  1. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/PKG-INFO +3 -4
  2. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/README.md +2 -3
  3. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/pyproject.toml +4 -3
  4. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/data.py +19 -10
  5. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/twitter.py +1 -1
  6. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/weibo.py +3 -3
  7. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/renders/base.py +73 -0
  8. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/renders/common.py +5 -21
  9. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/renders/default.py +8 -7
  10. nonebot_plugin_parser-2.0.6/src/nonebot_plugin_parser/renders/weibo.py +18 -0
  11. nonebot_plugin_parser-2.0.5/src/nonebot_plugin_parser/renders/weibo.py +0 -28
  12. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/__init__.py +0 -0
  13. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/config.py +0 -0
  14. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/constants.py +0 -0
  15. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/download/__init__.py +0 -0
  16. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/download/task.py +0 -0
  17. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  18. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/exception.py +0 -0
  19. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/helper.py +0 -0
  20. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/matchers/__init__.py +0 -0
  21. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  22. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/matchers/preprocess.py +0 -0
  23. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/__init__.py +0 -0
  24. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/acfun.py +0 -0
  25. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/base.py +0 -0
  26. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +0 -0
  27. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  28. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  29. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  30. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +0 -0
  31. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  32. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
  33. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/kuaishou.py +0 -0
  34. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
  35. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/tiktok.py +0 -0
  36. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/xiaohongshu.py +0 -0
  37. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/parsers/youtube.py +0 -0
  38. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/renders/__init__.py +0 -0
  39. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/renders/fonts/HYSongYunLangHeiW-1.ttf +0 -0
  40. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -0
  41. {nonebot_plugin_parser-2.0.5 → nonebot_plugin_parser-2.0.6}/src/nonebot_plugin_parser/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nonebot-plugin-parser
3
- Version: 2.0.5
3
+ Version: 2.0.6
4
4
  Summary: NoneBot2 链接分享解析器自动解析, BV号/链接/小程序/卡片 | B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun
5
5
  Keywords: nonebot,nonebot2,video,bilibili,youtube,tiktok,twitter,kuaishou,acfun,weibo,xiaohongshu,nga,douyin
6
6
  Author: fllesser
@@ -151,10 +151,9 @@ Windows 参考(原项目推荐): https://www.jianshu.com/p/5015a477de3c
151
151
  | parser_duration_maximum | 否 | 480 | 视频最大解析时长,单位:_秒_ |
152
152
  | parser_max_size | 否 | 90 | 音视频下载最大文件大小,单位 MB,超过该配置将阻断下载 |
153
153
  | parser_disabled_platforms | 否 | [] | 全局禁止的解析,示例 parser_disabled_platforms=["bilibili", "douyin"] 表示禁止了哔哩哔哩和抖, 请根据自己需求填写["bilibili", "douyin", "kuaishou", "twitter", "youtube", "acfun", "tiktok", "weibo", "xiaohongshu"] |
154
- | parser_render_type | 否 | "common" | 渲染器类型,可选 "default"(无图片渲染), "common"(PIL 通用图片渲染), "htmlkit"(htmlkit) |
154
+ | parser_render_type | 否 | "common" | 渲染器类型,可选 "default"(无图片渲染), "common"(PIL 通用图片渲染), "htmlkit"(htmlkit, 暂不可用) |
155
155
  | parser_append_url | 否 | False | 是否在解析结果中附加原始URL |
156
- | parser_custom_font | 否 | None | 自定义字体,如未指定则使用内置字体,需将字体文件放置于localstore 生成的插件 data 目录下,parser_custom_font 为字体文件名 |
157
-
156
+ | parser_custom_font | 否 | None | 自定义渲染字体,配置字体文件名,并将字体文件放置于 localstore 生成的插件 data 目录下(如 ./data/nonebot_plugin_parser/) |
158
157
  ## 🎉 使用
159
158
  ### 指令表
160
159
  | 指令 | 权限 | 需要@ | 范围 | 说明 |
@@ -121,10 +121,9 @@ Windows 参考(原项目推荐): https://www.jianshu.com/p/5015a477de3c
121
121
  | parser_duration_maximum | 否 | 480 | 视频最大解析时长,单位:_秒_ |
122
122
  | parser_max_size | 否 | 90 | 音视频下载最大文件大小,单位 MB,超过该配置将阻断下载 |
123
123
  | parser_disabled_platforms | 否 | [] | 全局禁止的解析,示例 parser_disabled_platforms=["bilibili", "douyin"] 表示禁止了哔哩哔哩和抖, 请根据自己需求填写["bilibili", "douyin", "kuaishou", "twitter", "youtube", "acfun", "tiktok", "weibo", "xiaohongshu"] |
124
- | parser_render_type | 否 | "common" | 渲染器类型,可选 "default"(无图片渲染), "common"(PIL 通用图片渲染), "htmlkit"(htmlkit) |
124
+ | parser_render_type | 否 | "common" | 渲染器类型,可选 "default"(无图片渲染), "common"(PIL 通用图片渲染), "htmlkit"(htmlkit, 暂不可用) |
125
125
  | parser_append_url | 否 | False | 是否在解析结果中附加原始URL |
126
- | parser_custom_font | 否 | None | 自定义字体,如未指定则使用内置字体,需将字体文件放置于localstore 生成的插件 data 目录下,parser_custom_font 为字体文件名 |
127
-
126
+ | parser_custom_font | 否 | None | 自定义渲染字体,配置字体文件名,并将字体文件放置于 localstore 生成的插件 data 目录下(如 ./data/nonebot_plugin_parser/) |
128
127
  ## 🎉 使用
129
128
  ### 指令表
130
129
  | 指令 | 权限 | 需要@ | 范围 | 说明 |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parser"
3
- version = "2.0.5"
3
+ version = "2.0.6"
4
4
  description = "NoneBot2 链接分享解析器自动解析, BV号/链接/小程序/卡片 | B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun"
5
5
  authors = [{ "name" = "fllesser", "email" = "fllessive@gmail.com" }]
6
6
  readme = "README.md"
@@ -99,6 +99,7 @@ addopts = [
99
99
  [tool.poe.tasks]
100
100
  test_others = "pytest tests/others --cov=src --cov-report=xml:coverage1.xml --junitxml=junit1.xml -n auto"
101
101
  test_parsers = "pytest tests/parsers --cov=src --cov-report=xml:coverage2.xml --junitxml=junit2.xml -n auto"
102
+ test_render = "pytest tests/render --cov=src --cov-report=xml:coverage3.xml --junitxml=junit3.xml -n auto"
102
103
  bump = "bump-my-version bump"
103
104
  show-bump = "bump-my-version show-bump"
104
105
 
@@ -180,12 +181,12 @@ reportShadowedImports = false
180
181
  disableBytesTypePromotions = true
181
182
 
182
183
  [build-system]
183
- requires = ["uv_build>=0.8.14,<0.9.0"]
184
+ requires = ["uv_build>=0.9.0,<0.10.0"]
184
185
  build-backend = "uv_build"
185
186
 
186
187
 
187
188
  [tool.bumpversion]
188
- current_version = "2.0.5"
189
+ current_version = "2.0.6"
189
190
  commit = true
190
191
  message = "🔖 release: bump vesion from {current_version} to {new_version}"
191
192
  tag = true
@@ -7,7 +7,7 @@ from typing import Any
7
7
 
8
8
  def repr_path_task(path_task: Path | Task[Path]) -> str:
9
9
  if isinstance(path_task, Path):
10
- return f"path={path_task}"
10
+ return f"path={path_task.name}"
11
11
  else:
12
12
  return f"task={path_task.get_name()}, done={path_task.done()}"
13
13
 
@@ -57,6 +57,12 @@ class VideoContent(MediaContent):
57
57
  seconds = int(self.duration) % 60
58
58
  return f"时长: {minutes}:{seconds:02d}"
59
59
 
60
+ def __repr__(self) -> str:
61
+ repr = f"VideoContent(path={repr_path_task(self.path_task)}"
62
+ if self.cover is not None:
63
+ repr += f", cover={repr_path_task(self.cover)}"
64
+ return repr + ")"
65
+
60
66
 
61
67
  @dataclass(repr=False)
62
68
  class ImageContent(MediaContent):
@@ -139,6 +145,8 @@ class ParseResult:
139
145
  """额外信息"""
140
146
  repost: "ParseResult | None" = None
141
147
  """转发的内容"""
148
+ render_image: Path | None = None
149
+ """渲染图片"""
142
150
 
143
151
  @property
144
152
  def header(self) -> str:
@@ -197,15 +205,16 @@ class ParseResult:
197
205
 
198
206
  def __repr__(self) -> str:
199
207
  return (
200
- f"\nplatform: {self.platform}\n"
201
- f"title: {self.title}\n"
202
- f"timestamp: {self.timestamp}\n"
203
- f"author: {self.author}\n"
204
- f"text: {self.text}\n"
205
- f"contents: {self.contents}\n"
206
- f"url: {self.url}\n"
207
- f"extra: {self.extra}\n"
208
- f"repost: {self.repost}\n"
208
+ f"platform: {self.platform.display_name}, "
209
+ f"timestamp: {self.timestamp}, "
210
+ f"title: {self.title}, "
211
+ f"text: {self.text}, "
212
+ f"url: {self.url}, "
213
+ f"author: {self.author}, "
214
+ f"contents: {self.contents}, "
215
+ f"extra: {self.extra}, "
216
+ f"repost: <<<<<<<{self.repost}>>>>>>, "
217
+ f"render_image: {self.render_image.name if self.render_image else 'None'}"
209
218
  )
210
219
 
211
220
 
@@ -71,7 +71,7 @@ class TwitterParser(BaseParser):
71
71
  soup = BeautifulSoup(html_content, "html.parser")
72
72
 
73
73
  # 初始化数据
74
- title = ""
74
+ title = None
75
75
  cover_url = None
76
76
  video_url = None
77
77
  images_urls = []
@@ -257,7 +257,7 @@ class PagePic(Struct):
257
257
 
258
258
 
259
259
  class PageInfo(Struct):
260
- title: str = ""
260
+ title: str | None = None
261
261
  urls: Urls | None = None
262
262
  page_pic: PagePic | None = None
263
263
 
@@ -286,8 +286,8 @@ class WeiboData(Struct):
286
286
  retweeted_status: "WeiboData | None" = None # 转发微博
287
287
 
288
288
  @property
289
- def title(self) -> str:
290
- return self.page_info.title if self.page_info else ""
289
+ def title(self) -> str | None:
290
+ return self.page_info.title if self.page_info else None
291
291
 
292
292
  @property
293
293
  def display_name(self) -> str:
@@ -3,6 +3,8 @@ from collections.abc import AsyncGenerator
3
3
  from itertools import chain
4
4
  from pathlib import Path
5
5
  from typing import Any, ClassVar
6
+ from typing_extensions import override
7
+ import uuid
6
8
 
7
9
  from ..config import pconfig
8
10
  from ..exception import DownloadException, DownloadLimitException, ZeroSizeException
@@ -79,3 +81,74 @@ class BaseRenderer(ABC):
79
81
  @property
80
82
  def append_url(self) -> bool:
81
83
  return pconfig.append_url
84
+
85
+
86
+ class ImageRenderer(BaseRenderer):
87
+ """图片渲染器"""
88
+
89
+ @abstractmethod
90
+ async def render_image(self, result: ParseResult) -> bytes:
91
+ """渲染图片
92
+
93
+ Args:
94
+ result (ParseResult): 解析结果
95
+
96
+ Returns:
97
+ bytes: 图片字节 png 格式
98
+ """
99
+ raise NotImplementedError
100
+
101
+ @override
102
+ async def render_messages(self, result: ParseResult):
103
+ """渲染消息
104
+
105
+ Args:
106
+ result (ParseResult): 解析结果
107
+ """
108
+ image_seg = await self.cache_or_render_image(result)
109
+
110
+ msg = UniMessage(image_seg)
111
+ if self.append_url:
112
+ urls = (result.display_url, result.repost_display_url)
113
+ msg += "\n".join(url for url in urls if url)
114
+ yield msg
115
+
116
+ # 媒体内容
117
+ async for message in self.render_contents(result):
118
+ yield message
119
+
120
+ async def cache_or_render_image(self, result: ParseResult):
121
+ """获取缓存图片
122
+
123
+ Args:
124
+ result (ParseResult): 解析结果
125
+
126
+ Returns:
127
+ Image: 图片 Segment
128
+ """
129
+ if result.render_image is None:
130
+ image_raw = await self.render_image(result)
131
+ image_path = await self.save_img(image_raw)
132
+ result.render_image = image_path
133
+ if pconfig.use_base64:
134
+ return UniHelper.img_seg(raw=image_raw)
135
+
136
+ return UniHelper.img_seg(result.render_image)
137
+
138
+ @classmethod
139
+ async def save_img(cls, raw: bytes) -> Path:
140
+ """保存图片
141
+
142
+ Args:
143
+ raw (bytes): 图片字节
144
+
145
+ Returns:
146
+ Path: 图片路径
147
+ """
148
+ import aiofiles
149
+
150
+ file_name = f"{uuid.uuid4().hex}.png"
151
+ image_path = pconfig.cache_dir / file_name
152
+ async with aiofiles.open(image_path, "wb+") as f:
153
+ await f.write(raw)
154
+ return image_path
@@ -6,10 +6,10 @@ from typing_extensions import override
6
6
  from nonebot import logger
7
7
  from PIL import Image, ImageDraw, ImageFont
8
8
 
9
- from .base import BaseRenderer, ParseResult, UniHelper, UniMessage
9
+ from .base import ImageRenderer, ParseResult
10
10
 
11
11
 
12
- class CommonRenderer(BaseRenderer):
12
+ class CommonRenderer(ImageRenderer):
13
13
  """统一的渲染器,将解析结果转换为消息"""
14
14
 
15
15
  __slots__ = ("font_path", "fonts")
@@ -86,20 +86,7 @@ class CommonRenderer(BaseRenderer):
86
86
  logger.success(f"加载字体「{self.font_path.name}」成功")
87
87
 
88
88
  @override
89
- async def render_messages(self, result: ParseResult):
90
- # 生成图片卡片
91
- if image_raw := await self.draw_common_image(result):
92
- msg = UniMessage(UniHelper.img_seg(raw=image_raw))
93
- if self.append_url:
94
- urls = (result.display_url, result.repost_display_url)
95
- msg += "\n".join(url for url in urls if url)
96
- yield msg
97
-
98
- # 媒体内容
99
- async for message in self.render_contents(result):
100
- yield message
101
-
102
- async def draw_common_image(self, result: ParseResult) -> bytes | None:
89
+ async def render_image(self, result: ParseResult) -> bytes:
103
90
  """使用 PIL 绘制通用社交媒体帖子卡片
104
91
 
105
92
  Args:
@@ -110,8 +97,6 @@ class CommonRenderer(BaseRenderer):
110
97
  """
111
98
  # 调用内部方法生成图片
112
99
  image = await self._create_card_image(result)
113
- if not image:
114
- return None
115
100
 
116
101
  # 将图片转换为字节
117
102
  output = BytesIO()
@@ -120,7 +105,7 @@ class CommonRenderer(BaseRenderer):
120
105
 
121
106
  async def _create_card_image(
122
107
  self, result: ParseResult, bg_color: tuple[int, int, int] | None = None, apply_min_cover_size: bool = True
123
- ) -> Image.Image | None:
108
+ ) -> Image.Image:
124
109
  """创建卡片图片(内部方法,用于递归调用)
125
110
 
126
111
  Args:
@@ -129,9 +114,8 @@ class CommonRenderer(BaseRenderer):
129
114
  apply_min_cover_size: 是否对封面应用最小尺寸限制,转发内容不需要
130
115
 
131
116
  Returns:
132
- PIL Image 对象,如果没有足够的内容则返回 None
117
+ PIL Image 对象
133
118
  """
134
- # 使用预加载的字体
135
119
 
136
120
  # 先确定固定的卡片宽度和内容区域宽度
137
121
  card_width = max(self.DEFAULT_CARD_WIDTH, self.MIN_CARD_WIDTH)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing_extensions import override
4
4
 
5
+ from ..helper import Segment, Text
5
6
  from .base import BaseRenderer, ParseResult, UniHelper, UniMessage
6
7
 
7
8
 
@@ -19,20 +20,20 @@ class DefaultRenderer(BaseRenderer):
19
20
  Generator[UniMessage[Any], None, None]: 消息生成器
20
21
  """
21
22
 
22
- texts: list[str] = [
23
+ texts = (
23
24
  result.header,
24
25
  result.text,
25
26
  result.extra_info,
26
27
  result.display_url,
27
28
  result.repost_display_url,
28
- ]
29
+ )
29
30
  texts = [text for text in texts if text]
30
- texts[:-1] = [seg + "\n" for seg in texts[:-1]]
31
+ texts[:-1] = [text + "\n" for text in texts[:-1]]
31
32
 
32
- if cover_path := await result.cover_path:
33
- segs = [texts[0], UniHelper.img_seg(cover_path), *texts[1:]]
34
- else:
35
- segs = texts
33
+ segs: list[Segment] = [Text(text) for text in texts]
34
+ cover_path = await result.cover_path
35
+ if cover_path is not None:
36
+ segs.insert(1, UniHelper.img_seg(cover_path))
36
37
  yield UniMessage(segs)
37
38
 
38
39
  async for message in self.render_contents(result):
@@ -0,0 +1,18 @@
1
+ from typing_extensions import override
2
+
3
+ from nonebot import require
4
+
5
+ require("nonebot_plugin_htmlkit")
6
+ from nonebot_plugin_htmlkit import template_to_pic
7
+
8
+ from .base import ImageRenderer, ParseResult
9
+
10
+
11
+ class Renderer(ImageRenderer):
12
+ @override
13
+ async def render_image(self, result: ParseResult) -> bytes:
14
+ return await template_to_pic(
15
+ self.templates_dir.as_posix(),
16
+ "weibo.html.jinja",
17
+ templates={"result": result},
18
+ )
@@ -1,28 +0,0 @@
1
- from typing_extensions import override
2
-
3
- from nonebot import require
4
-
5
- require("nonebot_plugin_htmlkit")
6
- from nonebot_plugin_htmlkit import template_to_pic
7
-
8
- from .base import BaseRenderer, ParseResult, UniHelper, UniMessage
9
-
10
-
11
- class Renderer(BaseRenderer):
12
- @override
13
- async def render_messages(self, result: ParseResult):
14
- # 生成图片消息
15
- image_raw = await template_to_pic(
16
- self.templates_dir.as_posix(),
17
- "weibo.html.jinja",
18
- templates={"result": result},
19
- )
20
- # 组合文本消息
21
- texts = [result.header, result.display_url, result.repost_display_url]
22
- texts = [text for text in texts if text]
23
- texts[:-1] = [seg + "\n" for seg in texts[:-1]]
24
-
25
- yield UniMessage([texts[0], UniHelper.img_seg(raw=image_raw), *texts[1:]])
26
-
27
- async for message in self.render_contents(result):
28
- yield message