1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2026-05-16 10:56:46 +08:00

feat: 增加搜索多结果选择功能 (#864)

新增功能:
 - 搜索结果多条记录时通过TTS告知用户匹配数量
 - 支持用户重新呼叫'第X个'来选择并播放指定歌曲
 - 实现记忆机制:选择后保留待选列表,支持持续多次选择
 - 新增配置项 fuzzy_match_max_results 控制最大返回数量(默认100)

优化改进:
 - 搜索结果排序:从随机排序改为按文件名自然排序(custom_sort_key)
 - 日志输出优化:多结果时每个歌曲分行显示,带序号便于查看

修改文件:
 - command_handler.py: 添加待选择状态检查逻辑,优先匹配'第X个'指令
 - config.py: 新增 fuzzy_match_max_results 配置项
 - device_player.py: 添加 _pending_selection 属性、多结果处理逻辑、handle_selection 方法、优化日志格式
 - music_library.py: 将 random.shuffle 改为 sort(key=custom_sort_key) 自然排序
 - xiaomusic.py: 新增 select_index 命令处理方法
This commit is contained in:
jokinas
2026-05-09 22:24:37 +08:00
committed by GitHub
parent e8723e6e46
commit a09bbb0464
5 changed files with 67 additions and 5 deletions

View File

@@ -88,6 +88,14 @@ class CommandHandler:
Returns:
tuple: (命令值, 命令参数),未匹配返回 (None, None)
"""
# 检查是否有待选择的歌曲
if device._pending_selection:
patternarg = r"^第?([零一二三四五六七八九十百千万亿]+)[个首条集]$"
matcharg = re.match(patternarg, query)
if matcharg:
self.log.info(f"匹配到选择指令. query:{query}")
return "select_index", query
# 优先处理完全匹配
opvalue = self.check_full_match_cmd(device, query, ctrl_panel)
if opvalue:

View File

@@ -142,6 +142,8 @@ class Config:
enable_fuzzy_match: bool = (
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
)
# 模糊搜索返回的最大结果数量(用于多结果选择功能)
fuzzy_match_max_results: int = int(os.getenv("XIAOMUSIC_FUZZY_MATCH_MAX_RESULTS", "100"))
stop_tts_msg: str = os.getenv("XIAOMUSIC_STOP_TTS_MSG", "收到,再见")
enable_config_example: bool = False

View File

@@ -77,6 +77,8 @@ class XiaoMusicDevice:
# 关机定时器
self._stop_timer = None
self._last_cmd = None
self._pending_selection = None
self._pending_selection_count = 0
self.update_playlist()
# 添加歌曲定时器
@@ -237,6 +239,12 @@ class XiaoMusicDevice:
search_key: 搜索关键词
allow_download: 是否允许下载True: _play行为False: playlocal行为
"""
# 清除旧的待选择状态
if self._pending_selection:
self.log.info(f"清除旧的待选择状态,重新搜索: {name}")
self._pending_selection = None
self._pending_selection_count = 0
# 初始检查逻辑
if not search_key and not name:
if self.check_play_next():
@@ -253,9 +261,21 @@ class XiaoMusicDevice:
self.log.info(f"没有歌曲播放了 name:{name} search_key:{search_key}")
return
# 模糊搜索
names = self.xiaomusic.music_library.find_real_music_name(name, n=1)
self.log.info(f"play_internal. names:{names} {len(names)}")
# 模糊搜索(支持多结果选择)
max_results = self.config.fuzzy_match_max_results
names = self.xiaomusic.music_library.find_real_music_name(name, n=max_results)
self.log.info(f"play_internal. 搜索关键词:{name} 匹配数量:{len(names)}")
if len(names) > 1:
for idx, music_name in enumerate(names, 1):
self.log.info(f"{idx}个: {music_name}")
if len(names) > 1:
self._pending_selection = names
self._pending_selection_count = len(names)
selection_text = f"共找到{len(names)}条匹配记录,请重新呼叫小爱同学并告诉她第几个"
self.log.info(selection_text)
await self.xiaomusic.do_tts(self.did, selection_text)
return
if not names:
# 检查本地是否存在歌曲,不存在则根据参数决定是否下载
@@ -1041,3 +1061,18 @@ class XiaoMusicDevice:
if name in music_list.get("所有电台", []):
return "所有电台"
return "全部"
async def handle_selection(self, index):
"""处理用户选择第几个歌曲
Args:
index: 用户选择的序号从1开始
"""
if not self._pending_selection or index < 1 or index > len(self._pending_selection):
await self.xiaomusic.do_tts(self.did, "选择无效")
return
selected_name = self._pending_selection[index - 1]
self.log.info(f"用户选择了第{index}个: {selected_name}")
# 保持待选择状态不变,支持用户继续选择其他歌曲
await self._playmusic(selected_name)

View File

@@ -596,7 +596,7 @@ class MusicLibrary:
if name in real_names:
return [name]
# 音乐不在查找结果同时n大于1, 模糊匹配模式,扩大范围再找,最后保留随机 n 个
# 音乐不在查找结果同时n大于1, 模糊匹配模式,扩大范围再找,最后按文件名自然排序
if n > 1:
real_names = find_best_match(
name,
@@ -605,7 +605,7 @@ class MusicLibrary:
n=n * 2,
extra_search_index=self._extra_index_search,
)
random.shuffle(real_names)
real_names.sort(key=custom_sort_key)
self.log.info(f"没找到歌曲【{name}")
return real_names[:n]

View File

@@ -507,6 +507,23 @@ class XiaoMusic:
return
await self.do_tts(did, f"播放列表{list_name}中找不到第${index}")
# 口令:选择第几个
async def select_index(self, did="", arg1="", **kwargs):
patternarg = r"^第?([零一二三四五六七八九十百千万亿]+)[个首条集]$"
matcharg = re.match(patternarg, arg1)
if not matcharg:
return
chinese_index = matcharg.groups()[0]
index = chinese_to_number(chinese_index)
device = self.device_manager.devices.get(did)
if not device:
self.log.warning(f"设备 did:{did} 不存在")
return
await device.handle_selection(index)
# 口令:播放歌曲
async def play(self, did="", arg1="", **kwargs):
parts = arg1.split("|")