新增日志支持、卡片显示专辑封面、菜单去掉音源标注
- 日志:控制台 + logs/musicbot.log(按天轮转、保留 7 天), LOG_LEVEL/LOG_DIR 可配;目录不可写时退化为仅控制台; diagnose=False 避免把凭证写进日志 - 卡片用 Markdown 图片语法显示专辑封面(), 封面取自 MusicServer 的 img 字段(tx/wy 有、kw/kg 无) - 按钮 data 加第 5 段(封面 URL),解码兼容旧 4 段,已发出的按钮不失效 - 菜单去掉每首歌后的(音源)标注;卡片保留"来源:" - 测试补:封面渲染、无封面源、旧 data 兼容、LOG_LEVEL、日志落盘
This commit is contained in:
1 parent
7a8adb4571
commit
9bcdad7aa1
7 files changed
+162
-24
No files matched your search
+83
-11
@@ -3,6 +3,7 @@
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
@@ -123,6 +124,7 @@ class FakeMusicServer:
|
||||
"singer": "歌手T",
|
||||
"albumName": "专辑T",
|
||||
"interval": "04:00",
|
||||
"img": "https://img.test/cover.jpg",
|
||||
},
|
||||
{"source": "kw", "songmid": "S1", "name": name, "singer": "歌手A"},
|
||||
]
|
||||
@@ -181,10 +183,13 @@ def _reset_menu_state():
|
||||
music._seen_messages.clear()
|
||||
|
||||
|
||||
# 回调按钮 data:音乐|动作|源|song_id|歌名|歌手(歌名/歌手 % 转义)
|
||||
PICK_1 = "音乐|pick|tx|T1|%E7%A8%BB%E9%A6%99|%E6%AD%8C%E6%89%8BT"
|
||||
PLAY_TX = "音乐|play|tx|T1|%E7%A8%BB%E9%A6%99|%E6%AD%8C%E6%89%8BT"
|
||||
DL_TX = "音乐|dl|tx|T1|%E7%A8%BB%E9%A6%99|%E6%AD%8C%E6%89%8BT"
|
||||
# 回调按钮 data:音乐|动作|源|song_id|歌名|歌手|封面(歌名/歌手/封面 % 转义)
|
||||
IMG_URL = "https://img.test/cover.jpg"
|
||||
PICK_1 = music._song_data("pick", music.Song("tx", "T1", "稻香", "歌手T", img=IMG_URL))
|
||||
PLAY_TX = music._song_data("play", music.Song("tx", "T1", "稻香", "歌手T", img=IMG_URL))
|
||||
DL_TX = music._song_data("dl", music.Song("tx", "T1", "稻香", "歌手T", img=IMG_URL))
|
||||
# 旧版按钮 data(无封面段)—— 兼容性用例
|
||||
PICK_1_LEGACY = "音乐|pick|tx|T1|%E7%A8%BB%E9%A6%99|%E6%AD%8C%E6%89%8BT"
|
||||
|
||||
|
||||
def posts(calls: list[dict]) -> list[dict]:
|
||||
@@ -214,7 +219,7 @@ async def test_search_prefers_tx_and_falls_back(server, env):
|
||||
|
||||
|
||||
async def test_menu_only_selects_songs(env):
|
||||
"""搜索菜单只负责选歌:编号回调按钮,不放播放/下载、也不放任何链接。"""
|
||||
"""搜索菜单只负责选歌:编号回调按钮,不放播放/下载、不放链接、不带音源。"""
|
||||
http, config = env
|
||||
api, calls = make_api()
|
||||
assert await handle_command(api, http, config, "G1", "m1", "点歌 稻香")
|
||||
@@ -222,10 +227,10 @@ async def test_menu_only_selects_songs(env):
|
||||
assert body["msg_type"] == 2
|
||||
md = body["markdown"]["content"]
|
||||
assert "## 点歌:稻香" in md
|
||||
assert "1. 稻香 - 歌手T(QQ音乐)" in md
|
||||
assert "(QQ音乐)" in md and "(酷我)" in md
|
||||
assert "1. 稻香 - 歌手T" in md
|
||||
assert "QQ音乐" not in md and "酷我" not in md # 每首歌后面不再标音源
|
||||
assert "http" not in md # 菜单里没有任何链接
|
||||
# 编号回调按钮(type=1):点一下弹卡片,data 自包含歌曲
|
||||
# 编号回调按钮(type=1):点一下弹卡片,data 自包含歌曲(含封面)
|
||||
rows = buttons(body)
|
||||
assert [b["render_data"]["label"] for b in rows[0]["buttons"]] == ["1", "2"]
|
||||
assert [b["action"]["type"] for b in rows[0]["buttons"]] == [1, 1]
|
||||
@@ -287,9 +292,9 @@ async def test_menu_fallback_keeps_keyboard(env):
|
||||
|
||||
|
||||
async def test_pick_button_sends_card(env):
|
||||
"""点编号 → 弹歌曲卡片:歌名/来源 + [▶播放][⬇下载] 回调按钮。
|
||||
"""点编号 → 弹歌曲卡片:封面图 + 歌名/来源 + [▶播放][⬇下载] 回调按钮。
|
||||
|
||||
卡片由回调 data 还原(自包含 4 字段),故不含专辑/时长等菜单外信息。"""
|
||||
卡片由回调 data 还原(自包含 song 字段),故不含专辑/时长等菜单外信息。"""
|
||||
http, config = env
|
||||
api, calls = make_api()
|
||||
await handle_button(
|
||||
@@ -299,7 +304,8 @@ async def test_pick_button_sends_card(env):
|
||||
assert card["msg_type"] == 2
|
||||
md = card["markdown"]["content"]
|
||||
assert "## 稻香 - 歌手T" in md
|
||||
assert "来源:QQ音乐" in md
|
||||
assert "" in md # 专辑封面
|
||||
assert "来源:QQ音乐" in md # 卡片保留音源
|
||||
assert labels(card) == [["▶ 播放", "⬇ 下载"]]
|
||||
assert [b["action"]["type"] for b in buttons(card)[0]["buttons"]] == [1, 1]
|
||||
assert [b["action"]["data"] for b in buttons(card)[0]["buttons"]] == [
|
||||
@@ -309,6 +315,27 @@ async def test_pick_button_sends_card(env):
|
||||
assert (calls[-1]["method"], calls[-1]["path"]) == ("PUT", "/interactions/I1")
|
||||
|
||||
|
||||
async def test_card_without_img_when_source_has_none(env):
|
||||
"""无封面源的歌曲(如 kw):卡片不含图片,其余照常。"""
|
||||
http, config = env
|
||||
api, calls = make_api()
|
||||
no_img = "音乐|pick|kw|S1|%E7%A8%BB%E9%A6%99|%E6%AD%8C%E6%89%8BA|"
|
||||
await handle_button(api, http, config, "G1", "I1", no_img)
|
||||
md = posts(calls)[0]["json"]["markdown"]["content"]
|
||||
assert "![封面" not in md
|
||||
assert "## 稻香 - 歌手A" in md
|
||||
|
||||
|
||||
async def test_legacy_button_data_without_img(env):
|
||||
"""旧版 4 段 data 仍能处理(封面缺省),保证已发出的菜单按钮不失效。"""
|
||||
http, config = env
|
||||
api, calls = make_api()
|
||||
assert await handle_button(api, http, config, "G1", "I1", PICK_1_LEGACY)
|
||||
md = posts(calls)[0]["json"]["markdown"]["content"]
|
||||
assert "## 稻香 - 歌手T" in md
|
||||
assert "![封面" not in md
|
||||
|
||||
|
||||
async def test_card_play_button_sends_voice(env):
|
||||
"""卡片上的 [▶ 播放] 是回调按钮:点一下直接发语音。"""
|
||||
http, config = env
|
||||
@@ -591,6 +618,51 @@ def test_config_send_mode(monkeypatch):
|
||||
assert BotConfig.load().send_mode == "link" # 非法值不报错,按默认处理
|
||||
|
||||
|
||||
def test_config_log_level(monkeypatch):
|
||||
"""LOG_LEVEL 默认 INFO,可显式设置(大小写不敏感),非法值回落 INFO。"""
|
||||
from musicbot.config import BotConfig
|
||||
|
||||
monkeypatch.delenv("LOG_LEVEL", raising=False)
|
||||
assert BotConfig.load().log_level == "INFO"
|
||||
|
||||
monkeypatch.setenv("LOG_LEVEL", "debug")
|
||||
assert BotConfig.load().log_level == "DEBUG"
|
||||
|
||||
monkeypatch.setenv("LOG_LEVEL", "乱写")
|
||||
assert BotConfig.load().log_level == "INFO"
|
||||
|
||||
|
||||
def test_setup_logging_writes_file(tmp_path: Path):
|
||||
"""setup_logging 同时写控制台与文件;写不进去时退化为仅控制台不抛错。"""
|
||||
from loguru import logger as lg
|
||||
|
||||
from musicbot.config import BotConfig
|
||||
from musicbot.main import setup_logging
|
||||
|
||||
cfg = BotConfig(
|
||||
music_server_url="http://127.0.0.1:1",
|
||||
music_quality="128k",
|
||||
music_source="tx",
|
||||
log_level="INFO",
|
||||
log_dir=tmp_path,
|
||||
)
|
||||
try:
|
||||
setup_logging(cfg)
|
||||
lg.info("测试日志落盘")
|
||||
lg.complete() # 等 enqueue 队列刷完
|
||||
logfile = tmp_path / "musicbot.log"
|
||||
assert logfile.is_file()
|
||||
assert "测试日志落盘" in logfile.read_text(encoding="utf-8")
|
||||
finally:
|
||||
lg.remove() # 清掉本用例加的 handler,避免影响其他测试
|
||||
|
||||
# 目录不可写(用一个文件当目录)→ 不抛错,仅告警
|
||||
blocked = tmp_path / "not-a-dir"
|
||||
blocked.write_text("x", encoding="utf-8")
|
||||
setup_logging(replace(cfg, log_dir=blocked))
|
||||
lg.remove()
|
||||
|
||||
|
||||
# ---- 全量模式(不 @ 也能用)与去重 ----
|
||||
|
||||
def make_emitter(api, http, config) -> EventEmitter:
|
||||
|
||||
Reference in new issue
Block a user