From a9e559a41348ab27967da5433be94a7e12c42cdb Mon Sep 17 00:00:00 2001 From: student_2333 Date: Sun, 10 Mar 2024 16:41:10 +0800 Subject: [PATCH] up --- README.md | 72 +++-- nonebot_plugin_multincm/__init__.py | 2 +- nonebot_plugin_multincm/compact.py | 17 ++ nonebot_plugin_multincm/config.py | 10 +- .../{draw/playwright.py => draw.py} | 57 +++- nonebot_plugin_multincm/draw/__init__.py | 12 - nonebot_plugin_multincm/draw/pil.py | 275 ------------------ nonebot_plugin_multincm/draw/shared.py | 32 -- nonebot_plugin_multincm/res/lyric.html.jinja | 2 +- .../res/song_list.html.jinja | 2 +- nonebot_plugin_multincm/types.py | 3 + pyproject.toml | 12 +- 12 files changed, 121 insertions(+), 375 deletions(-) create mode 100644 nonebot_plugin_multincm/compact.py rename nonebot_plugin_multincm/{draw/playwright.py => draw.py} (66%) delete mode 100644 nonebot_plugin_multincm/draw/__init__.py delete mode 100644 nonebot_plugin_multincm/draw/pil.py delete mode 100644 nonebot_plugin_multincm/draw/shared.py diff --git a/README.md b/README.md index 1a7a5e4..e3474bb 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ _✨ 网易云多选点歌 ✨_
+ + Pydantic Version 1 Or 2 + license @@ -136,31 +139,31 @@ plugins = [ 在 nonebot2 项目的 `.env` 文件中添加下表中的必填配置 -| 配置项 | 必填 | 默认值 | 说明 | -| :-------------------------: | :--: | :---------------: | :-------------------------------------------------------------------------------------------------------: | -| **登录相关** | | | | -| `NCM_CTCODE` | 否 | `86` | 手机号登录用,登录手机区号 | -| `NCM_PHONE` | 否 | 无 | 手机号登录用,登录手机号 | -| `NCM_EMAIL` | 否 | 无 | 邮箱登录用,登录邮箱 | -| `NCM_PASSWORD` | 否 | 无 | 帐号明文密码,邮箱登录时为邮箱密码 | -| `NCM_PASSWORD_HASH` | 否 | 无 | 帐号密码 MD5 哈希,邮箱登录时为邮箱密码 | -| **展示相关** | | | | -| `NCM_LIST_LIMIT` | 否 | `20` | 歌曲列表每页的最大数量 | -| `NCM_LIST_FONT` | 否 | 无 | 渲染歌曲列表使用的字体 | -| `NCM_MAX_NAME_LEN` | 否 | `600` | 歌曲列表中歌名列的最大文本宽度(像素) | -| `NCM_MAX_ARTIST_LEN` | 否 | `400` | 歌曲列表中歌手列的最大文本宽度(像素) | -| `NCM_LRC_EMPTY_LINE` | 否 | `--------` | 填充歌词空行的字符 | -| **功能相关** | | | | -| `NCM_MSG_CACHE_TIME` | 否 | `43200` | 缓存 用户最近一次操作 的时长(秒) | -| `NCM_AUTO_RESOLVE` | 否 | `False` | 当用户发送音乐链接时,是否自动解析并发送音乐卡片 | -| `NCM_RESOLVE_PLAYABLE_CARD` | 否 | `False` | 开启自动解析时,是否解析可播放的卡片 | -| `NCM_ILLEGAL_CMD_FINISH` | 否 | `False` | 当用户在点歌时输入了非法指令,是否直接退出点歌 | -| `NCM_ILLEGAL_CMD_LIMIT` | 否 | `3` | 当未启用 `NCM_ILLEGAL_CMD_FINISH` 时,用户在点歌时输入了多少次非法指令后直接退出点歌,填 `0` 以禁用此功能 | -| `NCM_USE_PLAYWRIGHT` | 否 | `False` | 是否使用 `playwright` 绘制歌曲列表与歌词图片 | -| `NCM_DELETE_LIST_MSG` | 否 | `True` | 是否在退出点歌模式后自动撤回歌曲列表 | -| `NCM_DELETE_LIST_MSG_DELAY` | 否 | `[0.5, 2.0]` | 自动撤回歌曲列表消息间隔时间(单位秒) | -| `NCM_UPLOAD_FOLDER_NAME` | 否 | `MultiNCM` | 在群内使用上传指令时,上传到的文件夹名称,不存在时会自动创建,如果创建失败会上传到根目录 | -| `NCM_ENABLE_RECORD` | 否 | `False` | 是否开启发送歌曲语音的功能 | +| 配置项 | 必填 | 默认值 | 说明 | +| :-------------------------: | :--: | :----------: | :-------------------------------------------------------------------------------------------------------: | +| **登录相关** | | | | +| `NCM_CTCODE` | 否 | `86` | 手机号登录用,登录手机区号 | +| `NCM_PHONE` | 否 | 无 | 手机号登录用,登录手机号 | +| `NCM_EMAIL` | 否 | 无 | 邮箱登录用,登录邮箱 | +| `NCM_PASSWORD` | 否 | 无 | 帐号明文密码,邮箱登录时为邮箱密码 | +| `NCM_PASSWORD_HASH` | 否 | 无 | 帐号密码 MD5 哈希,邮箱登录时为邮箱密码 | +| **展示相关** | | | | +| `NCM_LIST_LIMIT` | 否 | `20` | 歌曲列表每页的最大数量 | +| `NCM_LIST_FONT` | 否 | 无 | 渲染歌曲列表使用的字体 | +| `NCM_MAX_NAME_LEN` | 否 | `600` | 歌曲列表中歌名列的最大文本宽度(像素) | +| `NCM_MAX_ARTIST_LEN` | 否 | `400` | 歌曲列表中歌手列的最大文本宽度(像素) | +| `NCM_LRC_EMPTY_LINE` | 否 | `--------` | 填充歌词空行的字符 | +| **功能相关** | | | | +| `NCM_MSG_CACHE_TIME` | 否 | `43200` | 缓存 用户最近一次操作 的时长(秒) | +| `NCM_AUTO_RESOLVE` | 否 | `False` | 当用户发送音乐链接时,是否自动解析并发送音乐卡片 | +| `NCM_RESOLVE_PLAYABLE_CARD` | 否 | `False` | 开启自动解析时,是否解析可播放的卡片 | +| `NCM_ILLEGAL_CMD_FINISH` | 否 | `False` | 当用户在点歌时输入了非法指令,是否直接退出点歌 | +| `NCM_ILLEGAL_CMD_LIMIT` | 否 | `3` | 当未启用 `NCM_ILLEGAL_CMD_FINISH` 时,用户在点歌时输入了多少次非法指令后直接退出点歌,填 `0` 以禁用此功能 | +| `NCM_USE_PLAYWRIGHT` | 否 | `False` | 是否使用 `playwright` 绘制歌曲列表与歌词图片 | +| `NCM_DELETE_LIST_MSG` | 否 | `True` | 是否在退出点歌模式后自动撤回歌曲列表 | +| `NCM_DELETE_LIST_MSG_DELAY` | 否 | `[0.5, 2.0]` | 自动撤回歌曲列表消息间隔时间(单位秒) | +| `NCM_UPLOAD_FOLDER_NAME` | 否 | `MultiNCM` | 在群内使用上传指令时,上传到的文件夹名称,不存在时会自动创建,如果创建失败会上传到根目录 | +| `NCM_ENABLE_RECORD` | 否 | `False` | 是否开启发送歌曲语音的功能 | ## 🎉 使用 @@ -242,10 +245,25 @@ Telegram:[@lgc2333](https://t.me/lgc2333) ### 0.5.0(开发中) -- 支持歌单,专辑等(开发中) +
+TODO + +- 多平台发送逻辑(暂定): + - OneBot V11 首先发送卡片,如果发送失败则 fallback + - 以文件形式发送 + - 直接发送直链 +- issue #17 + - 只有在手动回复解析时才会要求选择,发送链接自动解析时只输出歌单信息 +- 重构图片样式,现在的歌曲列表好丑,歌词图片要限长 + +
+
+ +- 适配 Pydantic V1 & V2 +- 支持歌单,专辑等,支持多平台(开发中) - 点歌指令可以回复一条文本消息作为搜索内容了 -- 支持使用语音发送歌曲 - resolve [#14](https://github.com/lgc-NB2Dev/nonebot-plugin-multincm/issues/14) +- 弃用 Pillow - 重构部分代码 ### 0.4.4 diff --git a/nonebot_plugin_multincm/__init__.py b/nonebot_plugin_multincm/__init__.py index 8cebd5b..4e75eff 100644 --- a/nonebot_plugin_multincm/__init__.py +++ b/nonebot_plugin_multincm/__init__.py @@ -5,7 +5,7 @@ auto_resolve_tip = "▶ Bot 会自动解析你发送的网易云链接\n" -__version__ = "0.5.0.dev8" +__version__ = "0.5.0.dev9" __plugin_meta__ = PluginMetadata( name="MultiNCM", description="网易云多选点歌", diff --git a/nonebot_plugin_multincm/compact.py b/nonebot_plugin_multincm/compact.py new file mode 100644 index 0000000..0d4764e --- /dev/null +++ b/nonebot_plugin_multincm/compact.py @@ -0,0 +1,17 @@ +from typing import Literal, Optional + +from nonebot.compat import PYDANTIC_V2 + +if PYDANTIC_V2: + from pydantic import field_validator # type: ignore + +else: + from pydantic import validator + + def field_validator( + __field: str, + *fields: str, + mode: Literal["before", "after", "wrap", "plain"] = "after", + check_fields: Optional[bool] = None, # noqa: ARG001 + ): + return validator(__field, *fields, pre=(mode == "before"), allow_reuse=True) diff --git a/nonebot_plugin_multincm/config.py b/nonebot_plugin_multincm/config.py index a2146c2..2167cc1 100644 --- a/nonebot_plugin_multincm/config.py +++ b/nonebot_plugin_multincm/config.py @@ -1,7 +1,9 @@ from typing import Optional, Tuple -from nonebot import get_driver -from pydantic import BaseModel, validator +from nonebot import get_plugin_config +from pydantic import BaseModel + +from .compact import field_validator class ConfigModel(BaseModel): @@ -28,7 +30,7 @@ class ConfigModel(BaseModel): ncm_upload_folder_name: str = "MultiNCM" ncm_enable_record: bool = False - @validator("ncm_upload_folder_name") + @field_validator("ncm_upload_folder_name") def validate_upload_folder_name(cls, v: str) -> str: # noqa: N805 v = v.strip("/") if "/" in v: @@ -36,4 +38,4 @@ def validate_upload_folder_name(cls, v: str) -> str: # noqa: N805 return v -config: ConfigModel = ConfigModel.parse_obj(get_driver().config.dict()) +config = get_plugin_config(ConfigModel) diff --git a/nonebot_plugin_multincm/draw/playwright.py b/nonebot_plugin_multincm/draw.py similarity index 66% rename from nonebot_plugin_multincm/draw/playwright.py rename to nonebot_plugin_multincm/draw.py index e1ece87..1d46d61 100644 --- a/nonebot_plugin_multincm/draw/playwright.py +++ b/nonebot_plugin_multincm/draw.py @@ -1,36 +1,64 @@ +from dataclasses import dataclass +from math import ceil from pathlib import Path -from typing import Optional +from typing import List, Literal, NamedTuple, Optional, Tuple, Union -import bbcode +import bbcode # TODO 弃用 bbcode from jinja2 import Template -from nonebot import logger from nonebot_plugin_htmlrender import get_new_page -from pil_utils.fonts import Font -from pil_utils.types import ColorType, HAlignType -from ..config import config -from ..const import RES_DIR -from .shared import TablePage +from .config import config +from .const import RES_DIR + +ColorType = Union[str, Tuple[int, int, int], Tuple[int, int, int, int]] +HAlignType = Literal["left", "right", "center"] + SONG_LIST_TEMPLATE = Template( (RES_DIR / "song_list.html.jinja").read_text(encoding="u8"), + autoescape=True, enable_async=True, ) LYRIC_TEMPLATE = Template( (RES_DIR / "lyric.html.jinja").read_text(encoding="u8"), + autoescape=True, enable_async=True, ) BBCODE_PARSER = bbcode.Parser() BBCODE_PARSER.install_default_formatters() +@dataclass() +class TableHead: + name: str + align: HAlignType = "left" + min_width: Optional[int] = None + max_width: Optional[int] = None + + +class Table(NamedTuple): + head: List[TableHead] + rows: List[List[str]] + + +@dataclass() +class TablePage: + table: Table + calling: str + current_page: int + max_count: int + + @property + def max_page(self) -> int: + return ceil(self.max_count / config.ncm_list_limit) + + def get_font_path_uri() -> Optional[str]: font_path = config.ncm_list_font - if font_path: - if (path := Path(font_path)).exists(): - return path.resolve().as_uri() - return Font.find(font_path).path.as_uri() - return None + if font_path and (path := Path(font_path)).exists(): + p = path.resolve().as_uri().replace("\\", "\\\\").replace("'", "\\'") + return f"url('{p}')" + return f"local('{font_path}')" if font_path else None async def render_template( @@ -38,7 +66,8 @@ async def render_template( **kwargs, ) -> bytes: html_txt = await template.render_async(**kwargs) - logger.debug(html_txt) + if (dbg := Path.cwd() / "multincm-debug.html").exists(): + dbg.write_text(html_txt, encoding="u8") async with get_new_page() as page: await page.goto(RES_DIR.as_uri()) await page.set_content(html_txt, wait_until="networkidle") diff --git a/nonebot_plugin_multincm/draw/__init__.py b/nonebot_plugin_multincm/draw/__init__.py deleted file mode 100644 index a25adfb..0000000 --- a/nonebot_plugin_multincm/draw/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from nonebot import require - -from ..config import config -from .shared import Table as Table, TableHead as TableHead, TablePage as TablePage - -if config.ncm_use_playwright: - require("nonebot_plugin_htmlrender") - - from .playwright import draw_table_page as draw_table_page, str_to_pic as str_to_pic - -else: - from .pil import draw_table_page as draw_table_page, str_to_pic as str_to_pic diff --git a/nonebot_plugin_multincm/draw/pil.py b/nonebot_plugin_multincm/draw/pil.py deleted file mode 100644 index efecbbd..0000000 --- a/nonebot_plugin_multincm/draw/pil.py +++ /dev/null @@ -1,275 +0,0 @@ -from typing import List, Sequence, Union - -from pil_utils import BuildImage, Text2Image -from pil_utils.types import ColorType, HAlignType - -from ..config import config -from ..const import RES_DIR -from .shared import TableHead, TablePage - -BACKGROUND = BuildImage.open(RES_DIR / "bg.jpg") - - -def calculate_width(head: TableHead, now_max_width: int) -> int: - # max_width = head.max_width - min_width = head.min_width - - if min_width is not None and now_max_width < min_width: - return min_width - # if max_width is not None and now_max_width > max_width: - # return max_width - - return now_max_width - - -def calculate_pos_offset(align: HAlignType, max_len: int, item_len: int) -> int: - if align == "center": - return (max_len - item_len) // 2 - if align == "right": - return max_len - item_len - # default left - return 0 - - -def generate_line_text(head: TableHead, text: str, **kwargs) -> Text2Image: - text2img = Text2Image.from_bbcode_text( - text, - align=head.align, - fontname=config.ncm_list_font or "", - **kwargs, - ) - - width = text2img.width - max_width = head.max_width - if max_width is not None and width > max_width: - text2img.wrap(max_width) - - return text2img - - -def draw_table( - heads: Sequence[Union[TableHead, str]], - lines: Sequence[Sequence[str]], - border_radius: int = 15, - **kwargs, -) -> BuildImage: - font_size = 30 - pic_padding = 2 - item_padding_w = 10 - item_padding_h = 15 - border_width = 3 - font_color = (255, 255, 255, 255) - border_color = (255, 255, 255, 100) - - head_len = len(heads) - for li in lines: - if len(li) != head_len: - raise ValueError("line length not match head length") - - line_len = len(lines) - heads = [TableHead(x) if isinstance(x, str) else x for x in heads] - - head_line_text = [ - generate_line_text( - head, - f"[b]{head.name}[/b]", - fontsize=font_size, - fill=font_color, - **kwargs, - ) - for head in heads - ] - body_line_texts = [ - [ - generate_line_text( - heads[i], - x, - fontsize=font_size, - fill=font_color, - **kwargs, - ) - for i, x in enumerate(line) - ] - for line in lines - ] - line_texts: List[List[Text2Image]] = [head_line_text, *body_line_texts] - - col_widths = [ - calculate_width(x, max([y[i].width for y in line_texts])) - for i, x in enumerate(heads) - ] - - total_pic_padding = pic_padding * 2 - total_item_padding_w = item_padding_w * head_len * 2 - total_item_padding_h = item_padding_h * (line_len + 1) * 2 - - total_item_width = sum(col_widths) - total_border_width = border_width * (head_len + 1) - - total_item_height = sum([max([y.height for y in x]) for x in line_texts]) - total_border_height = border_width * (len(line_texts) + 1) - - width = ( - total_pic_padding + total_item_width + total_border_width + total_item_padding_w - ) - height = ( - total_pic_padding - + total_item_height - + total_border_height - + total_item_padding_h - ) - - pic = BuildImage.new("RGBA", (width, height), (255, 255, 255, 0)) - - # 画表格框 - pic.draw_rounded_rectangle( - ( - pic_padding, - pic_padding, - width - pic_padding, - height - pic_padding, - ), - border_radius, - outline=border_color, - width=border_width, - ) - - y_offset = pic_padding + item_padding_h + border_width - for line_index, line in enumerate(line_texts): - x_offset = pic_padding + item_padding_w + border_width - line_height = max([x.height for x in line]) - - for col_index, (head, item_width, item) in enumerate( - zip(heads, col_widths, line), - ): - item.draw_on_image( - pic.image, - ( - x_offset + calculate_pos_offset(head.align, item_width, item.width), - y_offset + calculate_pos_offset("center", line_height, item.height), - ), - ) - - # 写第一行字时画表格竖线,忽略第一列左侧 - if line_index == 0 and col_index != 0: - border_x = x_offset - item_padding_w - border_width // 2 - pic.draw_line( - (border_x, pic_padding, border_x, height - pic_padding), - fill=border_color, - width=border_width, - ) - - x_offset += item_width + item_padding_w * 2 + border_width - - y_offset += line_height + item_padding_h * 2 + border_width - - # 画表格横线,忽略最后一行下方 - if line_index != line_len: - border_y = y_offset - item_padding_h - border_width // 2 - pic.draw_line( - (pic_padding, border_y, width - pic_padding, border_y), - fill=border_color, - width=border_width, - ) - - return pic - - -async def draw_table_page(res: TablePage) -> bytes: - pic_padding = 50 - table_padding = 20 - table_border_radius = 15 - - heads, lines = res.table - table_img = draw_table(heads, lines, border_radius=table_border_radius) - - calling = res.calling - title_txt = Text2Image.from_text( - f"{calling}列表", - 80, - weight="bold", - fill=(255, 255, 255), - fontname=config.ncm_list_font or "", - ) - tip_txt = Text2Image.from_bbcode_text( - ( - f"Tip:[b]发送序号[/b] 选择{calling} | 发送 [b]P[/b]+[b]数字[/b] 跳到指定页数\n" - "其他操作:[b]上一页[/b](P) | [b]下一页[/b](N) | [b]退出[/b](E)" - ), - 30, - align="center", - fill=(255, 255, 255), - fontname=config.ncm_list_font or "", - ) - footer_txt = Text2Image.from_bbcode_text( - f"第 [b]{res.current_page}[/b] / [b]{res.max_page}[/b] 页 | 共 [b]{res.max_count}[/b] 首", - 30, - align="center", - fill=(255, 255, 255), - fontname=config.ncm_list_font or "", - ) - - width = table_img.width + pic_padding * 2 + table_padding * 2 - height = ( - table_img.height - + title_txt.height - + tip_txt.height - + footer_txt.height - + table_padding * 7 - ) - - bg = BACKGROUND.copy().convert("RGBA").resize((width, height), keep_ratio=True) - y_offset = table_padding - - title_txt.draw_on_image(bg.image, ((width - title_txt.width) // 2, y_offset)) - y_offset += title_txt.height + table_padding - - tip_txt.draw_on_image(bg.image, ((width - tip_txt.width) // 2, y_offset)) - y_offset += tip_txt.height + table_padding - - bg.paste( - ( - BuildImage.new( - "RGBA", - ( - table_img.width + table_padding * 2, - table_img.height + table_padding * 2, - ), - (255, 255, 255, 50), - ).circle_corner(table_border_radius) - ), - (pic_padding, y_offset), - alpha=True, - ) - y_offset += table_padding - - bg.paste(table_img, (pic_padding + table_padding, y_offset), alpha=True) - y_offset += table_img.height + table_padding * 2 - - footer_txt.draw_on_image(bg.image, ((width - footer_txt.width) // 2, y_offset)) - - return bg.save_jpg((0, 0, 0)).getvalue() - - -async def str_to_pic( - txt: str, - padding: int = 20, - font_color: ColorType = (241, 246, 249), - bg_color: ColorType = (33, 42, 62), - font_size: int = 30, - text_align: HAlignType = "left", -) -> bytes: - txt2img = Text2Image.from_bbcode_text( - txt, - fontname=config.ncm_list_font or "", - fill=font_color, - fontsize=font_size, - align=text_align, - ) - img = BuildImage.new( - "RGBA", - (txt2img.width + padding * 2, txt2img.height + padding * 2), - bg_color, - ) - txt2img.draw_on_image(img.image, (padding, padding)) - return img.save_jpg().getvalue() diff --git a/nonebot_plugin_multincm/draw/shared.py b/nonebot_plugin_multincm/draw/shared.py deleted file mode 100644 index 55b326f..0000000 --- a/nonebot_plugin_multincm/draw/shared.py +++ /dev/null @@ -1,32 +0,0 @@ -from dataclasses import dataclass -from math import ceil -from typing import List, NamedTuple, Optional - -from pil_utils.types import HAlignType - -from ..config import config - - -@dataclass() -class TableHead: - name: str - align: HAlignType = "left" - min_width: Optional[int] = None - max_width: Optional[int] = None - - -class Table(NamedTuple): - head: List[TableHead] - rows: List[List[str]] - - -@dataclass() -class TablePage: - table: Table - calling: str - current_page: int - max_count: int - - @property - def max_page(self) -> int: - return ceil(self.max_count / config.ncm_list_limit) diff --git a/nonebot_plugin_multincm/res/lyric.html.jinja b/nonebot_plugin_multincm/res/lyric.html.jinja index a32c8f5..ec983e6 100644 --- a/nonebot_plugin_multincm/res/lyric.html.jinja +++ b/nonebot_plugin_multincm/res/lyric.html.jinja @@ -6,7 +6,7 @@ {% if font_path %} @font-face { font-family: 'Custom'; - src: url('{{ font_path }}'); + src: {{ font_path }}; } {% endif %} diff --git a/nonebot_plugin_multincm/res/song_list.html.jinja b/nonebot_plugin_multincm/res/song_list.html.jinja index 4b75173..5d16e5d 100644 --- a/nonebot_plugin_multincm/res/song_list.html.jinja +++ b/nonebot_plugin_multincm/res/song_list.html.jinja @@ -6,7 +6,7 @@ {% if font_path %} @font-face { font-family: 'Custom'; - src: url('{{ font_path }}'); + src: {{ font_path }}; } {% endif %} diff --git a/nonebot_plugin_multincm/types.py b/nonebot_plugin_multincm/types.py index 5826a9e..de2177f 100644 --- a/nonebot_plugin_multincm/types.py +++ b/nonebot_plugin_multincm/types.py @@ -16,6 +16,9 @@ SongInfoModelType = Union["Song", "VoiceBaseInfo"] +# TODO snake_case with alias generator + + class Artist(BaseModel): id: int name: str diff --git a/pyproject.toml b/pyproject.toml index 93887af..f6685c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,13 +6,13 @@ authors = [{ name = "student_2333", email = "lgc2333@126.com" }] dependencies = [ "nonebot2>=2.2.0", "nonebot-adapter-onebot>=2.2.0", - "pydantic>=1.10.0,<2", - "pil-utils>=0.1.7", - "Pillow>=9,<10", + "nonebot-plugin-htmlrender>=0.2.0.3", "pyncm>=1.6.8.9.1", "typing-extensions>=4.5.0", "anyio>=3.6.2", "httpx>=0.24.0", + "jinja2>=3.1.2", + "bbcode>=1.1.0", ] requires-python = ">=3.9,<4.0" readme = "README.md" @@ -22,11 +22,7 @@ license = { text = "MIT" } homepage = "https://github.com/lgc-NB2Dev/nonebot-plugin-multincm" [project.optional-dependencies] -playwright = [ - "nonebot-plugin-htmlrender>=0.2.0.3", - "jinja2>=3.1.2", - "bbcode>=1.1.0", -] +pil = ["pil-utils>=0.1.7", "Pillow>=9,<10"] [tool.pdm.version] source = "file"