diff --git a/musicbot/music.py b/musicbot/music.py index 48aeedd..0a47bb3 100644 --- a/musicbot/music.py +++ b/musicbot/music.py @@ -534,10 +534,11 @@ async def _swap_message( def _song_data(action: str, song: Song) -> str: - """回调按钮携带的歌曲数据:自包含(歌名/歌手/封面 % 转义,避免撞上 - 分隔符),bot 收到回调直接还原 Song,无需记忆菜单快照。""" + """回调按钮携带的歌曲数据:自包含(歌曲 id/歌名/歌手/封面 % 转义, + 避免撞上分隔符),bot 收到回调直接还原 Song,无需记忆菜单快照。 + id 也要转义:bili 的 songmid 是「BV号|cid」,裸拼会把 data 拆散。""" return ( - f"音乐|{action}|{song.source}|{song.song_id}" + f"音乐|{action}|{song.source}|{_q(song.song_id)}" f"|{_q(song.name)}|{_q(song.singer)}|{_q(song.img or '')}" ) @@ -545,12 +546,14 @@ def _song_data(action: str, song: Song) -> str: def _song_of_data(args: list[str]) -> Song: """从回调 data 的 [source, song_id, name, singer, img?] 还原 Song。 - img 段是后加的:旧按钮的 data 只有 4 段,缺省按无封面处理。""" + img 段是后加的:旧按钮的 data 只有 4 段,缺省按无封面处理。 + song_id 段的 % 转义也是后加的(bili songmid 带 |):旧按钮 id 不含 + %,unquote 幂等,直接解不影响。""" 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, + song_id=unquote(song_id), name=unquote(name), singer=unquote(singer), img=img, diff --git a/tests/test_music.py b/tests/test_music.py index 825e0cb..f4709c7 100644 --- a/tests/test_music.py +++ b/tests/test_music.py @@ -510,6 +510,25 @@ async def test_card_for_qs_song_shows_source_and_cover(env): assert f"![封面 #240px #240px]({cover})" in md +async def test_bili_songmid_pipe_round_trips(env): + """bili 的 songmid 是「BV号|cid」,自带分隔符:按钮 data 里的 id 必须 + % 转义,否则拆段变 6 段、整个回调被当成非本 bot 按钮静默丢弃(点歌 + 无反应)。转义后 pick 正常弹卡片,播放/下载按钮携带完整 id。""" + http, config = env + api, calls = make_api() + data = music._song_data( + "pick", + music.Song("bili", "BV1vg2AYdE4J|26216173455", "听", "UP主"), + ) + assert data.count("|") == 6 # 7 段:头+action+source+id+name+singer+img + await handle_button(api, http, config, "G1", "I1", data) + card = posts(calls)[0]["json"] + assert "## 听 - UP主" in card["markdown"]["content"] + assert "来源:哔哩哔哩" in card["markdown"]["content"] + datas = [b["action"]["data"] for b in buttons(card)[0]["buttons"]] + assert "BV1vg2AYdE4J%7C26216173455" in datas[0] # ▶ 播放带完整 id + + async def test_card_cover_url_is_encoded(env): """卡片封面 URL 走标准百分号编码:空格/中文转义、Markdown 语法里的 括号也要转(否则会截断 ![]() 链接),查询串结构保留。"""