新增日志支持、卡片显示专辑封面、菜单去掉音源标注

- 日志:控制台 + logs/musicbot.log(按天轮转、保留 7 天),
  LOG_LEVEL/LOG_DIR 可配;目录不可写时退化为仅控制台;
  diagnose=False 避免把凭证写进日志
- 卡片用 Markdown 图片语法显示专辑封面(![封面 #240px #240px](img)),
  封面取自 MusicServer 的 img 字段(tx/wy 有、kw/kg 无)
- 按钮 data 加第 5 段(封面 URL),解码兼容旧 4 段,已发出的按钮不失效
- 菜单去掉每首歌后的(音源)标注;卡片保留"来源:"
- 测试补:封面渲染、无封面源、旧 data 兼容、LOG_LEVEL、日志落盘
This commit is contained in:
snowy-yang committed 2026-09-20 22:36:40 +08:00
1 parent 7a8adb4571
commit 9bcdad7aa1
7 files changed
+162 -24

No files matched your search

+20 -10
View File
@@ -62,6 +62,7 @@ class Song:
album: str | None = None
interval: str | None = None
album_id: str | None = None
img: str | None = None # 专辑封面直链(部分源无此字段,如 kw/kg)
# 每个群当前"可撤回"的 bot 消息 id(菜单或卡片):发新消息前撤回上一张,
@@ -126,6 +127,7 @@ def _song_of(item: dict[str, Any]) -> Song | None:
album=item.get("albumName"),
interval=item.get("interval"),
album_id=item.get("albumId"),
img=item.get("img") or None,
)
@@ -278,22 +280,26 @@ async def _swap_message(
def _song_data(action: str, song: Song) -> str:
"""回调按钮携带的歌曲数据:自包含(歌名/歌手 % 转义,避免撞上分隔符),
bot 收到回调直接还原 Song,无需记忆菜单快照。"""
"""回调按钮携带的歌曲数据:自包含(歌名/歌手/封面 % 转义,避免撞上
分隔符),bot 收到回调直接还原 Song,无需记忆菜单快照。"""
return (
f"音乐|{action}|{song.source}|{song.song_id}"
f"|{_q(song.name)}|{_q(song.singer)}"
f"|{_q(song.name)}|{_q(song.singer)}|{_q(song.img or '')}"
)
def _song_of_data(args: list[str]) -> Song:
"""从回调 data 的 [source, song_id, name, singer] 还原 Song。"""
source, song_id, name, singer = args
"""从回调 data 的 [source, song_id, name, singer, img?] 还原 Song。
img 段是后加的:旧按钮的 data 只有 4 段,缺省按无封面处理。"""
source, song_id, name, singer = args[:4]
img = unquote(args[4]) if len(args) > 4 and args[4] else None
return Song(
source=source,
song_id=song_id,
name=unquote(name),
singer=unquote(singer),
img=img,
)
@@ -374,8 +380,7 @@ async def send_menu(
lines = [f"## 点歌:{keyword}"]
for i, song in enumerate(songs, 1):
title = f"{song.name} - {song.singer}".strip(" -")
source = _SOURCE_NAMES.get(song.source, song.source)
lines.append(f"{i}. {title}({source})")
lines.append(f"{i}. {title}")
keyboard = _menu_keyboard(songs, keyword, page, config.menu_page_size)
try:
resp = await api.post_group_message(
@@ -410,10 +415,13 @@ async def send_card(
msg_id: str | None = None,
event_id: str | None = None,
) -> None:
"""选中歌曲后弹出卡片:Markdown 歌名/歌手/来源/专辑/时长
"""选中歌曲后弹出卡片:Markdown 封面图 + 歌名/歌手/来源/专辑/时长
+ [▶ 播放][⬇ 下载] 回调按钮;Markdown 失败回退纯文本(按钮保留)。"""
title = f"{song.name} - {song.singer}".strip(" -")
lines = [f"## {title}"]
if song.img:
# QQ 后台会代理并缓存图片到客户端(不落库),宽高按 #宽px #高px 指定
lines.append(f"![封面 #240px #240px]({song.img})")
details = [f"来源:{_SOURCE_NAMES.get(song.source, song.source)}"]
if song.album:
details.append(f"专辑:{song.album}")
@@ -603,6 +611,8 @@ async def handle_button(
裸 uuid 会被 QQ 判为无效(40034025)。"""
parts = data.split("|")
action, args = (parts[1] if len(parts) > 1 else ""), parts[2:]
# 歌曲按钮的 args 为 [source, song_id, name, singer](+ img,后加故兼容 4 段)
is_song = action in ("pick", "play", "dl") and len(args) in (4, 5)
try:
if action == "page" and len(args) == 2:
keyword, page = unquote(args[0]), int(args[1])
@@ -610,12 +620,12 @@ async def handle_button(
api, http, config, group_openid, keyword, page,
event_id=event_id,
)
elif action == "pick" and len(args) == 4:
elif action == "pick" and is_song:
await send_card(
api, http, config, group_openid, _song_of_data(args),
event_id=event_id,
)
elif action in ("play", "dl") and len(args) == 4:
elif action in ("play", "dl") and is_song:
song = _song_of_data(args)
try:
await send_song(