nonebot-plugin-parser 2.0.8__tar.gz → 2.0.10__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 (54) hide show
  1. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/PKG-INFO +5 -7
  2. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/README.md +2 -4
  3. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/pyproject.toml +7 -7
  4. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/__init__.py +2 -2
  5. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/config.py +6 -5
  6. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/matchers/__init__.py +2 -2
  7. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/matchers/preprocess.py +13 -13
  8. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +19 -31
  9. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/base.py +7 -4
  10. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/common.py +86 -118
  11. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/constants.py +0 -0
  12. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/download/__init__.py +0 -0
  13. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/download/task.py +0 -0
  14. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  15. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/exception.py +0 -0
  16. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/helper.py +0 -0
  17. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  18. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/__init__.py +0 -0
  19. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/acfun.py +0 -0
  20. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/base.py +0 -0
  21. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/bilibili/article.py +0 -0
  22. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/bilibili/common.py +0 -0
  23. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +0 -0
  24. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +0 -0
  25. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/bilibili/live.py +0 -0
  26. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  27. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  28. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  29. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/data.py +0 -0
  30. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +0 -0
  31. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  32. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
  33. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/kuaishou.py +0 -0
  34. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
  35. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/tiktok.py +0 -0
  36. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/twitter.py +0 -0
  37. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/weibo.py +0 -0
  38. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/xiaohongshu.py +0 -0
  39. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/parsers/youtube.py +0 -0
  40. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/__init__.py +0 -0
  41. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/default.py +0 -0
  42. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW-1.ttf +0 -0
  43. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/bilibili.png +0 -0
  44. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/douyin.png +0 -0
  45. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/kuaishou.png +0 -0
  46. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/media_button.png +0 -0
  47. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/tiktok.png +0 -0
  48. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/twitter.png +0 -0
  49. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/weibo.png +0 -0
  50. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/xiaohongshu.png +0 -0
  51. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/resources/youtube.png +0 -0
  52. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -0
  53. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/renders/weibo.py +0 -0
  54. {nonebot_plugin_parser-2.0.8 → nonebot_plugin_parser-2.0.10}/src/nonebot_plugin_parser/utils.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nonebot-plugin-parser
3
- Version: 2.0.8
4
- Summary: NoneBot2 链接分享解析器自动解析, BV号/链接/小程序/卡片 | B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun
3
+ Version: 2.0.10
4
+ Summary: NoneBot2 链接分享解析 Alconna 版, 通用媒体卡片渲染(PIL 实现), 支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga
5
5
  Keywords: nonebot,nonebot2,video,bilibili,youtube,tiktok,twitter,kuaishou,acfun,weibo,xiaohongshu,nga,douyin
6
6
  Author: fllesser
7
7
  Author-email: fllesser <fllessive@gmail.com>
@@ -19,7 +19,7 @@ Requires-Dist: nonebot-plugin-localstore>=0.7.4,<1.0.0
19
19
  Requires-Dist: nonebot-plugin-apscheduler>=0.5.0,<1.0.0
20
20
  Requires-Dist: nonebot-plugin-alconna>=0.59.4
21
21
  Requires-Dist: nonebot-plugin-uninfo>=0.9.0
22
- Requires-Dist: nonebot-plugin-htmlkit>=0.1.0rc1 ; extra == 'htmlkit'
22
+ Requires-Dist: nonebot-plugin-htmlkit>=0.1.0rc3 ; extra == 'htmlkit'
23
23
  Requires-Dist: jinja2>=3.1.6 ; extra == 'htmlkit'
24
24
  Requires-Python: >=3.10
25
25
  Project-URL: IssueTracker, https://github.com/fllesser/nonebot-plugin-parser/issues
@@ -52,8 +52,6 @@ Description-Content-Type: text/markdown
52
52
 
53
53
  ## 📖 介绍
54
54
 
55
- [nonebot-plugin-resolver](https://github.com/zhiyu1998/nonebot-plugin-resolver) 重制版
56
-
57
55
  | 平台 | 触发的消息形态 | 视频 | 图集 | 音频 |
58
56
  | ------- | ------------------------------------- | ---- | ---- | ---- |
59
57
  | B站 | BV号/链接(包含短链,BV,av)/卡片/小程序 | ✅​ | ✅​ | ✅​ |
@@ -69,7 +67,7 @@ Description-Content-Type: text/markdown
69
67
  支持的链接,可参考 [测试链接](https://github.com/fllesser/nonebot-plugin-parser/blob/master/test_url.md)
70
68
 
71
69
  ## 🎨 效果图
72
-
70
+ 插件默认启用 PIL 实现的通用媒体卡片渲染,效果图如下
73
71
  <div align="center">
74
72
 
75
73
  <img src="https://raw.githubusercontent.com/fllesser/nonebot-plugin-parser/refs/heads/resources/resources/renderdamine/video.png" width="160" />
@@ -82,7 +80,7 @@ Description-Content-Type: text/markdown
82
80
 
83
81
  ## 💿 安装
84
82
  > [!Warning]
85
- > **如果你已经在使用 nonebot-plugin-resolver,请在安装此插件前卸载**
83
+ > **如果你已经在使用 nonebot-plugin-resolver[2],请在安装此插件前卸载**
86
84
 
87
85
  <details open>
88
86
  <summary>使用 nb-cli 安装/更新</summary>
@@ -22,8 +22,6 @@
22
22
 
23
23
  ## 📖 介绍
24
24
 
25
- [nonebot-plugin-resolver](https://github.com/zhiyu1998/nonebot-plugin-resolver) 重制版
26
-
27
25
  | 平台 | 触发的消息形态 | 视频 | 图集 | 音频 |
28
26
  | ------- | ------------------------------------- | ---- | ---- | ---- |
29
27
  | B站 | BV号/链接(包含短链,BV,av)/卡片/小程序 | ✅​ | ✅​ | ✅​ |
@@ -39,7 +37,7 @@
39
37
  支持的链接,可参考 [测试链接](https://github.com/fllesser/nonebot-plugin-parser/blob/master/test_url.md)
40
38
 
41
39
  ## 🎨 效果图
42
-
40
+ 插件默认启用 PIL 实现的通用媒体卡片渲染,效果图如下
43
41
  <div align="center">
44
42
 
45
43
  <img src="https://raw.githubusercontent.com/fllesser/nonebot-plugin-parser/refs/heads/resources/resources/renderdamine/video.png" width="160" />
@@ -52,7 +50,7 @@
52
50
 
53
51
  ## 💿 安装
54
52
  > [!Warning]
55
- > **如果你已经在使用 nonebot-plugin-resolver,请在安装此插件前卸载**
53
+ > **如果你已经在使用 nonebot-plugin-resolver[2],请在安装此插件前卸载**
56
54
 
57
55
  <details open>
58
56
  <summary>使用 nb-cli 安装/更新</summary>
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parser"
3
- version = "2.0.8"
4
- description = "NoneBot2 链接分享解析器自动解析, BV号/链接/小程序/卡片 | B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun"
3
+ version = "2.0.10"
4
+ description = "NoneBot2 链接分享解析 Alconna 版, 通用媒体卡片渲染(PIL 实现), 支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga"
5
5
  authors = [{ "name" = "fllesser", "email" = "fllessive@gmail.com" }]
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.10"
@@ -41,7 +41,7 @@ dependencies = [
41
41
  ]
42
42
 
43
43
  [project.optional-dependencies]
44
- htmlkit = ["nonebot-plugin-htmlkit>=0.1.0rc1", "jinja2>=3.1.6"]
44
+ htmlkit = ["nonebot-plugin-htmlkit>=0.1.0rc3", "jinja2>=3.1.6"]
45
45
 
46
46
  [project.urls]
47
47
  Repository = "https://github.com/fllesser/nonebot-plugin-parser"
@@ -54,7 +54,7 @@ dev = [
54
54
  "nonebot2[fastapi]>=2.4.3,<3.0.0",
55
55
  "nonebot-adapter-telegram>=0.1.0b20",
56
56
  "pre-commit>=4.3.0",
57
- "ruff>=0.13.3,<1.0.0",
57
+ "ruff>=0.14.0,<1.0.0",
58
58
  "bump-my-version>=1.2.4",
59
59
  ]
60
60
 
@@ -72,7 +72,7 @@ test = [
72
72
  all_extras = ["nonebot-plugin-htmlkit>=0.1.0rc1", "jinja2>=3.1.6"]
73
73
 
74
74
  [tool.uv]
75
- required-version = ">=0.8.14"
75
+ required-version = ">=0.9.2"
76
76
  default-groups = ["test", "dev", "all_extras"]
77
77
 
78
78
  [tool.nonebot]
@@ -99,7 +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 --log-cli-level=DEBUG"
102
+ test_render = "pytest tests/render --cov=src --cov-report=xml:coverage3.xml --junitxml=junit3.xml"
103
103
  bump = "bump-my-version bump"
104
104
  show-bump = "bump-my-version show-bump"
105
105
 
@@ -186,7 +186,7 @@ build-backend = "uv_build"
186
186
 
187
187
 
188
188
  [tool.bumpversion]
189
- current_version = "2.0.8"
189
+ current_version = "2.0.10"
190
190
  commit = true
191
191
  message = "🔖 release: bump vesion from {current_version} to {new_version}"
192
192
  tag = true
@@ -11,8 +11,8 @@ from .matchers import clear_result_cache
11
11
  from .utils import safe_unlink
12
12
 
13
13
  __plugin_meta__ = PluginMetadata(
14
- name="链接分享自动解析",
15
- description="BV号/链接/小程序/卡片 | B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun",
14
+ name="链接分享解析 Alconna 版",
15
+ description="全新通用媒体卡片渲染(PIL 实现), 支持 B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun/nga",
16
16
  usage="发送支持平台的(BV号/链接/小程序/卡片)即可",
17
17
  type="application",
18
18
  homepage="https://github.com/fllesser/nonebot-plugin-parser",
@@ -1,4 +1,5 @@
1
1
  from enum import Enum
2
+ from functools import cached_property
2
3
  from pathlib import Path
3
4
  from typing import Literal
4
5
 
@@ -56,22 +57,22 @@ class Config(BaseModel):
56
57
  parser_need_forward_contents: bool = True
57
58
  """是否需要转发媒体内容"""
58
59
 
59
- @property
60
+ @cached_property
60
61
  def nickname(self) -> str:
61
62
  """全局名称"""
62
63
  return _nickname
63
64
 
64
- @property
65
+ @cached_property
65
66
  def cache_dir(self) -> Path:
66
67
  """插件缓存目录"""
67
68
  return _cache_dir
68
69
 
69
- @property
70
+ @cached_property
70
71
  def config_dir(self) -> Path:
71
72
  """插件配置目录"""
72
73
  return _config_dir
73
74
 
74
- @property
75
+ @cached_property
75
76
  def data_dir(self) -> Path:
76
77
  """插件数据目录"""
77
78
  return _data_dir
@@ -131,7 +132,7 @@ class Config(BaseModel):
131
132
  """是否在解析结果中附加原始URL"""
132
133
  return self.parser_append_url
133
134
 
134
- @property
135
+ @cached_property
135
136
  def custom_font(self) -> Path | None:
136
137
  """自定义字体"""
137
138
  return (self.data_dir / self.parser_custom_font) if self.parser_custom_font else None
@@ -12,7 +12,7 @@ from ..config import pconfig
12
12
  from ..parsers import BaseParser, ParseResult
13
13
  from ..renders import get_renderer
14
14
  from ..utils import LimitedSizeDict
15
- from .preprocess import KeyPatternMatched, Keyword, on_keyword_regex
15
+ from .preprocess import Keyword, KwdRegexMatched, on_keyword_regex
16
16
 
17
17
 
18
18
  def _get_enabled_parser_classes() -> list[type[BaseParser]]:
@@ -60,7 +60,7 @@ parser_matcher = on_keyword_regex(*_get_enabled_patterns())
60
60
  async def _(
61
61
  event: Event,
62
62
  keyword: str = Keyword(),
63
- matched: re.Match[str] = KeyPatternMatched(),
63
+ matched: re.Match[str] = KwdRegexMatched(),
64
64
  ):
65
65
  """统一的解析处理器"""
66
66
  # 响应用户处理中
@@ -13,9 +13,9 @@ from nonebot_plugin_alconna.uniseg import Hyper, UniMsg
13
13
 
14
14
  from .filter import is_not_in_disabled_groups
15
15
 
16
- R_KEYWORD_KEY: Literal["_r_keyword"] = "_r_keyword"
17
- R_EXTRACT_KEY: Literal["_r_extract"] = "_r_extract"
18
- R_KEY_REGEX_MATCHED_KEY: Literal["_r_key_regex_matched"] = "_r_key_regex_matched"
16
+ PSR_KWD_KEY: Literal["_psr_kwd"] = "_psr_kwd"
17
+ PSR_EXTRACT_KEY: Literal["_psr_extract"] = "_psr_extract"
18
+ PSR_KWD_MATCHED_KEY: Literal["_psr_kwd_matched"] = "_psr_kwd_matched"
19
19
 
20
20
 
21
21
  def ExtractText() -> str:
@@ -23,7 +23,7 @@ def ExtractText() -> str:
23
23
 
24
24
 
25
25
  def _extract_text(state: T_State) -> str | None:
26
- return state.get(R_EXTRACT_KEY)
26
+ return state.get(PSR_EXTRACT_KEY)
27
27
 
28
28
 
29
29
  def Keyword() -> str:
@@ -31,15 +31,15 @@ def Keyword() -> str:
31
31
 
32
32
 
33
33
  def _keyword(state: T_State) -> str | None:
34
- return state.get(R_KEYWORD_KEY)
34
+ return state.get(PSR_KWD_KEY)
35
35
 
36
36
 
37
- def KeyPatternMatched() -> re.Match[str]:
38
- return Depends(_key_pattern_matched)
37
+ def KwdRegexMatched() -> re.Match[str]:
38
+ return Depends(_kwd_regex_matched)
39
39
 
40
40
 
41
- def _key_pattern_matched(state: T_State) -> re.Match[str] | None:
42
- return state.get(R_KEY_REGEX_MATCHED_KEY)
41
+ def _kwd_regex_matched(state: T_State) -> re.Match[str] | None:
42
+ return state.get(PSR_KWD_MATCHED_KEY)
43
43
 
44
44
 
45
45
  URL_KEY_MAPPING = {
@@ -104,12 +104,12 @@ def extract_msg_text(message: UniMsg, state: T_State) -> None:
104
104
  text: str | None = None
105
105
 
106
106
  if hyper := message.get(Hyper, 1):
107
- state[R_EXTRACT_KEY] = _extract_json_url(hyper.pop())
107
+ state[PSR_EXTRACT_KEY] = _extract_json_url(hyper.pop())
108
108
  return
109
109
 
110
110
  # 提取纯文本
111
111
  if text := message.extract_plain_text().strip():
112
- state[R_EXTRACT_KEY] = text
112
+ state[PSR_EXTRACT_KEY] = text
113
113
 
114
114
 
115
115
  class KeyPatternList(list[tuple[str, re.Pattern[str]]]):
@@ -145,8 +145,8 @@ class KeywordRegexRule:
145
145
  if keyword not in text:
146
146
  continue
147
147
  if matched := pattern.search(text):
148
- state[R_KEYWORD_KEY] = keyword
149
- state[R_KEY_REGEX_MATCHED_KEY] = matched
148
+ state[PSR_KWD_KEY] = keyword
149
+ state[PSR_KWD_MATCHED_KEY] = matched
150
150
  return True
151
151
  logger.debug(f"keyword '{keyword}' is in '{text}', but not matched")
152
152
  return False
@@ -61,21 +61,23 @@ class BilibiliParser(BaseParser):
61
61
  """
62
62
  # 从匹配对象中获取原始URL, 视频ID, 页码
63
63
  url, video_id, page_num = str(matched.group(0)), str(matched.group(1)), matched.group(2)
64
-
65
64
  # 处理短链
66
65
  if "b23.tv" in url or "bili2233.cn" in url:
67
66
  url = await self.get_redirect_url(url, self.headers)
68
67
 
69
68
  if not video_id:
70
- if _matched := re.search(r"(BV[\dA-Za-z]{10})[^?]*?(?:\?[^#]*?p=(\d{1,3}))?", url):
71
- video_id = _matched.group(1)
72
- page_num = _matched.group(2)
73
- elif _matched := re.search(r"av(\d{6,})[^?]*?(?:\?[^#]*?p=(\d{1,3}))?", url):
74
- video_id = _matched.group(1)
75
- page_num = _matched.group(2)
69
+ # https://www.bilibili.com/video/BV1584y167sD?a=20&p=40
70
+ if _matched := re.search(r"(?:(BV[\dA-Za-z]{10})|av(\d{6,}))", url):
71
+ video_id = _matched.group(1) or _matched.group(2)
76
72
  else:
77
73
  return await self.parse_others(url)
78
74
 
75
+ # 匹配页码参数
76
+ if _matched := re.search(r"(?:&|\?)p=(\d{1,3})", url):
77
+ page_num = _matched.group(1)
78
+ else:
79
+ page_num = None
80
+
79
81
  avid, bvid = None, None
80
82
  page_num = int(page_num) if page_num and page_num.isdigit() else 1
81
83
 
@@ -282,18 +284,11 @@ class BilibiliParser(BaseParser):
282
284
  current_text = ""
283
285
 
284
286
  for node in opus_data.gen_text_img():
285
- match node:
286
- case ImageNode():
287
- contents.append(
288
- self.create_graphics_content(
289
- node.url,
290
- current_text.strip(),
291
- node.alt,
292
- )
293
- )
294
- current_text = ""
295
- case TextNode():
296
- current_text += node.text
287
+ if isinstance(node, ImageNode):
288
+ contents.append(self.create_graphics_content(node.url, current_text.strip(), node.alt))
289
+ current_text = ""
290
+ elif isinstance(node, TextNode):
291
+ current_text += node.text
297
292
 
298
293
  return self.result(
299
294
  title=opus_data.title,
@@ -358,18 +353,11 @@ class BilibiliParser(BaseParser):
358
353
  contents: list[MediaContent] = []
359
354
  current_text = ""
360
355
  for child in article_info.gen_text_img():
361
- match child:
362
- case ImageNode():
363
- contents.append(
364
- self.create_graphics_content(
365
- child.url,
366
- current_text.strip(),
367
- child.alt,
368
- )
369
- )
370
- current_text = ""
371
- case TextNode():
372
- current_text += child.text
356
+ if isinstance(child, ImageNode):
357
+ contents.append(self.create_graphics_content(child.url, current_text.strip(), child.alt))
358
+ current_text = ""
359
+ elif isinstance(child, TextNode):
360
+ current_text += child.text
373
361
 
374
362
  author = self.create_author(*article_info.author_info)
375
363
 
@@ -44,6 +44,7 @@ class BaseRenderer(ABC):
44
44
  """
45
45
  failed_count = 0
46
46
  forwardable_segs: list[ForwardNodeInner] = []
47
+ dynamic_segs: list[ForwardNodeInner] = []
47
48
 
48
49
  for cont in chain(result.contents, result.repost.contents if result.repost else ()):
49
50
  try:
@@ -65,7 +66,7 @@ class BaseRenderer(ABC):
65
66
  case ImageContent():
66
67
  forwardable_segs.append(UniHelper.img_seg(path))
67
68
  case DynamicContent():
68
- forwardable_segs.append(UniHelper.video_seg(path))
69
+ dynamic_segs.append(UniHelper.video_seg(path))
69
70
  case GraphicsContent() as graphics:
70
71
  graphics_msg = UniHelper.img_seg(path)
71
72
  if graphics.text is not None:
@@ -76,11 +77,13 @@ class BaseRenderer(ABC):
76
77
 
77
78
  if forwardable_segs:
78
79
  if pconfig.need_forward_contents or len(forwardable_segs) > 4:
79
- forward_msg = UniHelper.construct_forward_message(forwardable_segs)
80
+ forward_msg = UniHelper.construct_forward_message(forwardable_segs + dynamic_segs)
80
81
  yield UniMessage(forward_msg)
81
82
  else:
82
- for seg in forwardable_segs:
83
- yield UniMessage(seg)
83
+ yield UniMessage(forwardable_segs)
84
+
85
+ if dynamic_segs:
86
+ yield UniMessage(UniHelper.construct_forward_message(dynamic_segs))
84
87
 
85
88
  if failed_count > 0:
86
89
  message = f"{failed_count} 项媒体下载失败"
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass
2
+ from functools import lru_cache
2
3
  from io import BytesIO
3
4
  from pathlib import Path
4
5
  from typing import ClassVar
@@ -19,6 +20,53 @@ class FontInfo:
19
20
  cjk_width: int
20
21
  ascii_width: int
21
22
 
23
+ def __hash__(self) -> int:
24
+ """实现哈希方法以支持 @lru_cache"""
25
+ return hash((self.line_height, self.cjk_width, self.ascii_width))
26
+
27
+ @lru_cache(maxsize=100)
28
+ def get_char_width(self, char: str) -> int:
29
+ """获取字符宽度,使用缓存优化"""
30
+ bbox = self.font.getbbox(char)
31
+ width = int(bbox[2] - bbox[0])
32
+ return width
33
+
34
+ def get_char_width_fast(self, char: str) -> int:
35
+ """快速获取单个字符宽度"""
36
+ if self._is_cjk_char(char):
37
+ return self.cjk_width
38
+ elif self._is_ascii_char(char):
39
+ return self.ascii_width
40
+ else:
41
+ return self.get_char_width(char)
42
+
43
+ def get_text_width(self, text: str) -> int:
44
+ """计算文本宽度,使用预计算的字符宽度优化性能
45
+
46
+ Args:
47
+ text: 要计算宽度的文本
48
+
49
+ Returns:
50
+ 文本宽度(像素)
51
+ """
52
+ if not text:
53
+ return 0
54
+
55
+ total_width = 0
56
+ for char in text:
57
+ total_width += self.get_char_width_fast(char)
58
+ return total_width
59
+
60
+ @staticmethod
61
+ def _is_cjk_char(char: str) -> bool:
62
+ """判断是否为中日韩字符"""
63
+ return "\u4e00" <= char <= "\u9fff"
64
+
65
+ @staticmethod
66
+ def _is_ascii_char(char: str) -> bool:
67
+ """判断是否为ASCII字符"""
68
+ return ord(char) < 128
69
+
22
70
 
23
71
  @dataclass(eq=False, frozen=True, slots=True)
24
72
  class FontSet:
@@ -28,6 +76,7 @@ class FontSet:
28
76
  title_font: FontInfo
29
77
  text_font: FontInfo
30
78
  extra_font: FontInfo
79
+ indicator_font: FontInfo
31
80
 
32
81
 
33
82
  @dataclass(eq=False, frozen=True, slots=True)
@@ -169,9 +218,9 @@ class CommonRenderer(ImageRenderer):
169
218
  """转发缩放比例"""
170
219
 
171
220
  # 字体大小和行高
172
- FONT_SIZES: ClassVar[dict[str, int]] = {"name": 28, "title": 30, "text": 24, "extra": 24}
221
+ FONT_SIZES: ClassVar[dict[str, int]] = {"name": 28, "title": 30, "text": 24, "extra": 24, "indicator": 60}
173
222
  """字体大小"""
174
- LINE_HEIGHTS: ClassVar[dict[str, int]] = {"name": 32, "title": 36, "text": 28, "extra": 28}
223
+ LINE_HEIGHTS: ClassVar[dict[str, int]] = {"name": 32, "title": 36, "text": 28, "extra": 28, "indicator": 68}
175
224
  """行高"""
176
225
 
177
226
  RESOURCES_DIR: ClassVar[Path] = Path(__file__).parent / "resources"
@@ -228,6 +277,7 @@ class CommonRenderer(ImageRenderer):
228
277
  title_font=font_infos["title"],
229
278
  text_font=font_infos["text"],
230
279
  extra_font=font_infos["extra"],
280
+ indicator_font=font_infos["indicator"],
231
281
  )
232
282
 
233
283
  logger.success(f"加载字体「{self.font_path.name}」成功")
@@ -859,11 +909,11 @@ class CommonRenderer(ImageRenderer):
859
909
  if section.alt_text:
860
910
  y_pos += self.SECTION_SPACING # 图片和alt文本之间的间距
861
911
  # 计算文本居中位置
862
- bbox = self.fontset.extra_font.font.getbbox(section.alt_text)
863
- text_width = bbox[2] - bbox[0]
912
+ extra_font_info = self.fontset.extra_font
913
+ text_width = extra_font_info.get_text_width(section.alt_text)
864
914
  text_x = self.PADDING + (content_width - text_width) // 2
865
- draw.text((text_x, y_pos), section.alt_text, fill=self.EXTRA_COLOR, font=self.fontset.extra_font.font)
866
- y_pos += self.fontset.extra_font.line_height
915
+ draw.text((text_x, y_pos), section.alt_text, fill=self.EXTRA_COLOR, font=extra_font_info.font)
916
+ y_pos += extra_font_info.line_height
867
917
 
868
918
  return y_pos + self.SECTION_SPACING
869
919
 
@@ -985,19 +1035,14 @@ class CommonRenderer(ImageRenderer):
985
1035
 
986
1036
  # 绘制+N文字
987
1037
  text = f"+{count}"
988
- # 使用更大的字体
989
- font_size = min(img_width, img_height) // 4
990
- font = ImageFont.truetype(self.font_path, font_size)
991
-
1038
+ font_info = self.fontset.indicator_font
992
1039
  # 计算文字位置(居中)
993
- bbox = font.getbbox(text)
994
- text_width = bbox[2] - bbox[0]
995
- text_height = bbox[3] - bbox[1]
1040
+ text_width = font_info.get_text_width(text)
996
1041
  text_x = img_x + (img_width - text_width) // 2
997
- text_y = img_y + (img_height - text_height) // 2
1042
+ text_y = img_y + (img_height - font_info.line_height) // 2
998
1043
 
999
- # 绘制白色文字
1000
- draw.text((text_x, text_y), text, fill=(255, 255, 255, 255), font=font)
1044
+ # 绘制50%透明白色文字
1045
+ draw.text((text_x, text_y), text, fill=(255, 255, 255), font=font_info.font)
1001
1046
 
1002
1047
  def _draw_rounded_rectangle(
1003
1048
  self, image: Image.Image, bbox: tuple[int, int, int, int], fill_color: tuple[int, int, int], radius: int = 8
@@ -1056,81 +1101,14 @@ class CommonRenderer(ImageRenderer):
1056
1101
  lines = []
1057
1102
  paragraphs = text.split("\n")
1058
1103
 
1059
- # 字符宽度缓存
1060
- char_width_cache = {}
1061
-
1062
- def get_char_width(char: str) -> int:
1063
- """获取字符宽度,使用缓存优化"""
1064
- if char in char_width_cache:
1065
- return char_width_cache[char]
1066
-
1067
- bbox = font_info.font.getbbox(char)
1068
- width = int(bbox[2] - bbox[0])
1069
- char_width_cache[char] = width
1070
- return width
1071
-
1072
- def is_cjk_char(char: str) -> bool:
1073
- """判断是否为中日韩字符"""
1074
- return "\u4e00" <= char <= "\u9fff"
1075
-
1076
- def is_ascii_char(char: str) -> bool:
1077
- """判断是否为ASCII字符"""
1078
- return ord(char) < 128
1079
-
1080
1104
  def is_punctuation(char: str) -> bool:
1081
- """判断是否为标点符号"""
1105
+ """判断是否为不能为行首的标点符号"""
1082
1106
  # 中文标点符号
1083
- chinese_punctuation = ",。!?;:、''()【】《》〈〉「」『』〔〕〖〗〘〙〚〛…—·"
1107
+ chinese_punctuation = ",。!?;:、)】》〉」』〕〗〙〛…—·"
1084
1108
  # 英文标点符号
1085
- english_punctuation = ",.;:!?()[]{}'\"-"
1086
- # Unicode 标点符号类别
1087
- import unicodedata
1088
-
1089
- return (
1090
- char in chinese_punctuation or char in english_punctuation or unicodedata.category(char).startswith("P")
1091
- )
1092
-
1093
- def get_text_width_fast(text: str) -> int:
1094
- """快速计算文本宽度"""
1095
- if not text:
1096
- return 0
1097
-
1098
- total_width = 0
1099
- for char in text:
1100
- if is_cjk_char(char):
1101
- total_width += font_info.cjk_width
1102
- elif is_ascii_char(char):
1103
- total_width += font_info.ascii_width
1104
- else:
1105
- total_width += get_char_width(char)
1106
- return total_width
1107
-
1108
- def find_break_point(text: str) -> int:
1109
- """找到合适的断点位置,避免标点符号在行首"""
1110
- if not text:
1111
- return 0
1112
-
1113
- # 从后往前找断点
1114
- for i in range(len(text) - 1, 0, -1):
1115
- char = text[i]
1116
-
1117
- # 优先在空格处断行
1118
- if char == " ":
1119
- return i
1120
-
1121
- # 对于中文,可以在任意字符处断行
1122
- if is_cjk_char(char):
1123
- return i
1109
+ english_punctuation = ",.;:!?)]}"
1124
1110
 
1125
- # 对于标点符号,不能在行首,需要跳过
1126
- if is_punctuation(char):
1127
- continue
1128
-
1129
- # 其他字符可以作为断点
1130
- return i
1131
-
1132
- # 如果找不到合适的断点,在中间位置断行
1133
- return max(1, len(text) // 2)
1111
+ return char in chinese_punctuation or char in english_punctuation
1134
1112
 
1135
1113
  for paragraph in paragraphs:
1136
1114
  if not paragraph:
@@ -1138,51 +1116,41 @@ class CommonRenderer(ImageRenderer):
1138
1116
  continue
1139
1117
 
1140
1118
  current_line = ""
1119
+ current_line_width = 0
1141
1120
  remaining_text = paragraph
1142
1121
 
1143
1122
  while remaining_text:
1123
+ next_char = remaining_text[0]
1124
+ char_width = font_info.get_char_width_fast(next_char)
1125
+
1144
1126
  # 如果当前行为空,直接添加字符
1145
1127
  if not current_line:
1146
- current_line = remaining_text[0]
1128
+ current_line = next_char
1129
+ current_line_width = char_width
1147
1130
  remaining_text = remaining_text[1:]
1148
1131
  continue
1149
1132
 
1150
- # 测试添加下一个字符
1151
- test_line = current_line + remaining_text[0]
1152
- test_width = get_text_width_fast(test_line)
1133
+ # 如果是标点符号,直接添加到当前行(标点符号不应该单独成行)
1134
+ if is_punctuation(next_char):
1135
+ current_line += next_char
1136
+ current_line_width += char_width
1137
+ remaining_text = remaining_text[1:]
1138
+ continue
1139
+
1140
+ # 测试添加下一个字符后的宽度
1141
+ test_width = current_line_width + char_width
1153
1142
 
1154
1143
  if test_width <= max_width:
1155
1144
  # 宽度合适,继续添加
1156
- current_line = test_line
1145
+ current_line += next_char
1146
+ current_line_width = test_width
1157
1147
  remaining_text = remaining_text[1:]
1158
1148
  else:
1159
1149
  # 宽度超限,需要断行
1160
- if len(current_line) == 1:
1161
- # 单个字符就超宽,强制添加
1162
- lines.append(current_line)
1163
- current_line = remaining_text[0]
1164
- remaining_text = remaining_text[1:]
1165
- else:
1166
- # 尝试找到合适的断点
1167
- break_point = find_break_point(current_line)
1168
-
1169
- # 保存当前行
1170
- lines.append(current_line[:break_point].rstrip())
1171
-
1172
- # 开始新行,跳过行首的标点符号
1173
- current_line = current_line[break_point:].lstrip()
1174
-
1175
- # 如果新行以标点符号开头,将其移到上一行
1176
- while current_line and is_punctuation(current_line[0]):
1177
- if lines:
1178
- lines[-1] += current_line[0]
1179
- current_line = current_line[1:]
1180
- else:
1181
- break
1182
-
1183
- if not current_line:
1184
- current_line = remaining_text[0]
1185
- remaining_text = remaining_text[1:]
1150
+ lines.append(current_line)
1151
+ current_line = next_char
1152
+ current_line_width = char_width
1153
+ remaining_text = remaining_text[1:]
1186
1154
 
1187
1155
  # 保存最后一行
1188
1156
  if current_line: