From d666c470febf6c3bccc6100f2c2a3b582efc5201 Mon Sep 17 00:00:00 2001 From: student_2333 Date: Fri, 28 Jun 2024 01:58:50 +0800 Subject: [PATCH] up --- README.md | 9 ++- nonebot_plugin_multincm/__init__.py | 2 +- nonebot_plugin_multincm/config.py | 12 +-- .../interaction/message/common.py | 5 +- .../interaction/message/song_file.py | 73 +++++++++++-------- .../render/templates/base.html.jinja | 4 +- nonebot_plugin_multincm/utils/__init__.py | 2 + nonebot_plugin_multincm/utils/base.py | 46 ++++++++++++ pyproject.toml | 1 + 9 files changed, 105 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 4bcb605..93b7cb6 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ plugins = [ | `NCM_ILLEGAL_CMD_LIMIT` | 否 | `3` | 当未启用 `NCM_ILLEGAL_CMD_FINISH` 时,用户在点歌时输入了多少次非法指令后直接退出点歌,填 `0` 以禁用此功能 | | `NCM_DELETE_MSG` | 否 | `True` | 是否在退出点歌模式后自动撤回歌曲列表与操作提示信息 | | `NCM_DELETE_MSG_DELAY` | 否 | `[0.5, 2.0]` | 自动撤回消息间隔时间(单位秒) | +| `NCM_SEND_MEDIA` | 否 | `True` | 是否发送歌曲,如关闭将始终提示使用命令获取播放链接 | | `NCM_SEND_AS_CARD` | 否 | `True` | 在支持的平台下,发送歌曲卡片(目前支持 `OneBot V11` 与 `Kritor`) | | `NCM_SEND_AS_FILE` | 否 | `False` | 当无法发送卡片或卡片发送失败时,会回退到使用语音发送,启用此配置项将会换成回退到发送歌曲文件 | | **其他配置** | | | | @@ -161,6 +162,7 @@ plugins = [ | `NCM_CARD_SIGN_URL` | 否 | `None` | 音卡签名地址(与 LLOneBot 或 NapCat 共用),填写此 URL 后将会把音卡的签名工作交给本插件 | | `NCM_CARD_SIGN_TIMEOUT` | 否 | `5` | 请求音卡签名地址的超时时间 | | `NCM_OB_V11_LOCAL_MODE` | 否 | `False` | 在 OneBot V11 适配器下,是否下载歌曲后使用本地文件路径上传歌曲 | +| `NCM_FFMPEG_EXECUTABLE` | 否 | `ffmpeg` | FFmpeg 可执行文件路径,已经加进环境变量可以不用配置,在 OneBot V11 适配器下发送语音需要使用 | ## 🎉 使用 @@ -241,9 +243,10 @@ Telegram:[@lgc2333](https://t.me/lgc2333) 项目重构 -- 支持多平台 +- 支持多平台 + 目前多平台发歌逻辑还不是很完善,如果有建议欢迎提出 - UI 大改 -- 新增一些支持的搜索与解析项 +- 支持电台与专辑的搜索与解析 - 自动解析对同一歌曲有冷却了,防多 Bot 刷屏 - 配置项变动 - 移除配置项 `NCM_MAX_NAME_LEN`、`NCM_MAX_ARTIST_LEN`、`NCM_USE_PLAYWRIGHT` @@ -257,8 +260,6 @@ Telegram:[@lgc2333](https://t.me/lgc2333) 可自行寻找音卡签名服务填写于此 - 修改配置项 `NCM_DOWNLOAD_LOCALLY` -> `NCM_OB_V11_LOCAL_MODE` -~~其他的变动懒得写了~~ - ### 0.5.0 - 适配 Pydantic V1 & V2 diff --git a/nonebot_plugin_multincm/__init__.py b/nonebot_plugin_multincm/__init__.py index ee7e909..c6cfeee 100644 --- a/nonebot_plugin_multincm/__init__.py +++ b/nonebot_plugin_multincm/__init__.py @@ -29,7 +29,7 @@ "▶ Bot 会自动解析你发送的网易云链接\n" if config.ncm_auto_resolve else "" ) -__version__ = "1.0.0a3" +__version__ = "1.0.0a4" __plugin_meta__ = PluginMetadata( name="MultiNCM", description="网易云多选点歌", diff --git a/nonebot_plugin_multincm/config.py b/nonebot_plugin_multincm/config.py index 7b34080..856a359 100644 --- a/nonebot_plugin_multincm/config.py +++ b/nonebot_plugin_multincm/config.py @@ -1,6 +1,4 @@ -from pathlib import Path from typing import Annotated, Optional, Tuple -from urllib.parse import quote from nonebot import get_plugin_config from pydantic import AnyHttpUrl, BaseModel @@ -27,6 +25,7 @@ class ConfigModel(BaseModel): ncm_illegal_cmd_limit: int = 3 ncm_delete_msg: bool = True ncm_delete_msg_delay: Tuple[float, float] = (0.5, 2.0) + ncm_send_media: bool = True ncm_send_as_card: bool = True ncm_send_as_file: bool = False @@ -37,14 +36,7 @@ class ConfigModel(BaseModel): ncm_card_sign_url: Optional[Annotated[str, AnyHttpUrl]] = None ncm_card_sign_timeout: int = 5 ncm_ob_v11_local_mode: bool = False - - @property - def ncm_list_font_url(self) -> Optional[str]: - return ( - quote(p.as_uri()) - if self.ncm_list_font and (p := Path(self.ncm_list_font)).exists() - else self.ncm_list_font - ) + ncm_ffmpeg_executable: str = "ffmpeg" config = get_plugin_config(ConfigModel) diff --git a/nonebot_plugin_multincm/interaction/message/common.py b/nonebot_plugin_multincm/interaction/message/common.py index f50f7b2..7a73134 100644 --- a/nonebot_plugin_multincm/interaction/message/common.py +++ b/nonebot_plugin_multincm/interaction/message/common.py @@ -42,8 +42,9 @@ async def send(): return receipt = ... - with warning_suppress(f"Send {song} file failed"): - receipt = await send_song_media(song) + if config.ncm_send_media: + with warning_suppress(f"Send {song} file failed"): + receipt = await send_song_media(song) await (await construct_info_msg(song, tip_command=(receipt is ...))).send( reply_to=receipt.get_reply() if receipt and (receipt is not ...) else None, ) diff --git a/nonebot_plugin_multincm/interaction/message/song_file.py b/nonebot_plugin_multincm/interaction/message/song_file.py index 0bad6ee..aafe2e5 100644 --- a/nonebot_plugin_multincm/interaction/message/song_file.py +++ b/nonebot_plugin_multincm/interaction/message/song_file.py @@ -5,12 +5,14 @@ from cookit.loguru import warning_suppress from httpx import AsyncClient +from nonebot import logger from nonebot.matcher import current_bot, current_event from nonebot_plugin_alconna.uniseg import Receipt, UniMessage, get_exporter from ...config import config from ...const import SONG_CACHE_DIR from ...data_source import BaseSong, SongInfo +from ...utils import encode_silk, ffmpeg_exists async def download_song(info: SongInfo): @@ -58,39 +60,50 @@ async def send_song_media_telegram(info: SongInfo, as_file: bool = False): # no async def send_song_media_onebot_v11(info: SongInfo, as_file: bool = False): - if not as_file: - raise TypeError("Should fallback using UniMessage") - - from nonebot.adapters.onebot.v11 import ( - Bot as OB11Bot, - GroupMessageEvent, - MessageEvent, - PrivateMessageEvent, - ) + async def send_voice(): + if not await ffmpeg_exists(): + logger.warning( + "FFmpeg 无法使用,插件将不会把音乐文件转为 silk 格式提交给协议端", + ) + raise TypeError("FFmpeg unavailable, fallback to UniMessage") + + return await UniMessage.voice( + raw=(await encode_silk(await download_song(info))).read_bytes(), + ).send() + + async def send_file(): + from nonebot.adapters.onebot.v11 import ( + Bot as OB11Bot, + GroupMessageEvent, + PrivateMessageEvent, + ) - bot = cast(OB11Bot, current_bot.get()) - event = cast(MessageEvent, current_event.get()) + bot = cast(OB11Bot, current_bot.get()) + event = current_event.get() - file = ( - (await download_song(info)) - if config.ncm_ob_v11_local_mode - else cast(str, (await bot.download_file(url=info.playable_url))["file"]) - ) + if not isinstance(event, (GroupMessageEvent, PrivateMessageEvent)): + raise TypeError("Event not supported") - if isinstance(event, PrivateMessageEvent): - await bot.upload_private_file( - user_id=event.user_id, - file=file, - name=info.display_filename, + file = ( + (await download_song(info)) + if config.ncm_ob_v11_local_mode + else cast(str, (await bot.download_file(url=info.playable_url))["file"]) ) - elif isinstance(event, GroupMessageEvent): - await bot.upload_group_file( - group_id=event.group_id, - file=file, - name=info.display_filename, - ) - else: - raise TypeError("Event not supported") + + if isinstance(event, PrivateMessageEvent): + await bot.upload_private_file( + user_id=event.user_id, + file=file, + name=info.display_filename, + ) + else: + await bot.upload_group_file( + group_id=event.group_id, + file=file, + name=info.display_filename, + ) + + return (await send_file()) if as_file else (await send_voice()) async def send_song_media_platform_specific( @@ -119,7 +132,7 @@ async def send_song_media(song: BaseSong, as_file: bool = config.ncm_send_as_fil path = await download_song(info) with warning_suppress( - f"Failed to send {song} use file path, will try to send using raw bytes", + f"Failed to send {song} using file path, fallback using raw bytes", ): if not TYPE_CHECKING: return await send_song_media_uni_msg(path, info, raw=False, as_file=as_file) diff --git a/nonebot_plugin_multincm/render/templates/base.html.jinja b/nonebot_plugin_multincm/render/templates/base.html.jinja index a7f0dba..99e0e28 100644 --- a/nonebot_plugin_multincm/render/templates/base.html.jinja +++ b/nonebot_plugin_multincm/render/templates/base.html.jinja @@ -70,8 +70,8 @@ const fontFamily = '{{ config.font_family | escape_single_quotes }}'; const css = fontFamily.includes('://') ? `@font-face { font-family: 'Custom'; src: url('${fontFamily}'); }\n` + - `:root { --font-family: 'Custom'; }` - : `:root { --font-family: '${fontFamily}'; }`; + `:root { --font-family: 'Custom', sans-serif; }` + : `:root { --font-family: '${fontFamily}', sans-serif; }`; const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); diff --git a/nonebot_plugin_multincm/utils/__init__.py b/nonebot_plugin_multincm/utils/__init__.py index 7489fb6..c9273d9 100644 --- a/nonebot_plugin_multincm/utils/__init__.py +++ b/nonebot_plugin_multincm/utils/__init__.py @@ -5,6 +5,8 @@ calc_min_max_index as calc_min_max_index, calc_page_number as calc_page_number, cut_string as cut_string, + encode_silk as encode_silk, + ffmpeg_exists as ffmpeg_exists, format_alias as format_alias, format_artists as format_artists, format_time as format_time, diff --git a/nonebot_plugin_multincm/utils/base.py b/nonebot_plugin_multincm/utils/base.py index 549d9f5..202dddf 100644 --- a/nonebot_plugin_multincm/utils/base.py +++ b/nonebot_plugin_multincm/utils/base.py @@ -1,9 +1,12 @@ +import asyncio import json import math import time +from pathlib import Path from typing import TYPE_CHECKING, Any, List, Optional, Tuple, TypeVar from typing_extensions import ParamSpec +from nonebot.utils import run_sync from yarl import URL from ..config import config @@ -80,3 +83,46 @@ def cut_string(text: str, length: int = 50) -> str: if len(text) <= length: return text return text[: length - 1] + "…" + + +async def ffmpeg_exists() -> bool: + proc = await asyncio.create_subprocess_exec( + config.ncm_ffmpeg_executable, + "-version", + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + code = await proc.wait() + return code == 0 + + +async def encode_silk(path: Path, rate: int = 24000) -> Path: + silk_path = path.with_suffix(".silk") + if silk_path.exists(): + return silk_path + + pcm_path = path.with_suffix(".pcm") + proc = await asyncio.create_subprocess_exec( + config.ncm_ffmpeg_executable, + "-y", + "-i", str(path), + "-f", "s16le", "-ar", f"{rate}", "-ac", "1", str(pcm_path), + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) # fmt: skip + code = await proc.wait() + if code != 0: + raise RuntimeError( + f"Failed to use ffmpeg to convert {path} to pcm, return code {code}", + ) + + try: + from pysilk import encode + + await run_sync(encode)(pcm_path.open("rb"), silk_path.open("wb"), rate, rate) + finally: + pcm_path.unlink(missing_ok=True) + + return silk_path diff --git a/pyproject.toml b/pyproject.toml index 36c4da2..de8bb21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "nonebot-plugin-waiter>=0.6.2", "cachetools>=5.3.3", "yarl>=1.9.4", + "silk-python>=0.2.6", ] requires-python = ">=3.9,<4.0" readme = "README.md"