1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2026-03-15 08:13:16 +08:00

fix: 修复播放歌曲口令问题 close #731

This commit is contained in:
涵曦
2026-01-26 00:44:06 +08:00
parent 84ec02c5b4
commit 781e5eca85
11 changed files with 66 additions and 205 deletions

View File

@@ -159,9 +159,6 @@ docker run -p 58090:8090 -v /xiaomusic_music:/app/music -v /xiaomusic_conf:/app/
- **加入收藏** - 将当前播放的歌曲加入收藏歌单
- **取消收藏** - 将当前播放的歌曲从收藏歌单移除
#### 搜索播放
- **搜索播放+关键词** - 搜索关键词作为临时搜索列表播放,例如:搜索播放林俊杰
> [!TIP]
> **隐藏玩法**:对小爱同学说"播放歌曲小猪佩奇的故事",会先下载小猪佩奇的故事,然后再播放。

View File

@@ -17,7 +17,7 @@
"search_prefix": "bilisearch:",
"ffmpeg_location": "./ffmpeg/bin",
"get_duration_type": "ffprobe",
"active_cmd": "play,search_play,set_play_type_rnd,playlocal,search_playlocal,play_music_list,play_music_list_index,stop_after_minute,stop",
"active_cmd": "play,set_play_type_rnd,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop,play_next,play_prev,set_play_type_one,set_play_type_all,set_play_type_sin,set_play_type_seq,gen_music_list,add_to_favorites,del_from_favorites,cmd_del_music,online_play,singer_play",
"exclude_dirs": "@eaDir,tmp",
"ignore_tag_dirs": "",
"music_path_depth": 10,
@@ -45,10 +45,8 @@
"删除歌曲": "cmd_del_music",
"播放本地歌曲": "playlocal",
"本地播放歌曲": "playlocal",
"本地搜索播放": "search_playlocal",
"播放歌曲": "play",
"放歌曲": "play",
"搜索播放": "search_play",
"在线播放": "online_play",
"播放歌手": "singer_play",
"关机": "stop",
@@ -79,10 +77,8 @@
"删除歌曲",
"播放本地歌曲",
"本地播放歌曲",
"本地搜索播放",
"播放歌曲",
"放歌曲",
"搜索播放",
"在线播放",
"播放歌手",
"暂停",
@@ -101,9 +97,7 @@
"stop_tts_msg": "收到,再见",
"enable_config_example": true,
"keywords_playlocal": "播放本地歌曲,本地播放歌曲",
"keywords_search_playlocal": "本地搜索播放",
"keywords_play": "播放歌曲,放歌曲",
"keywords_search_play": "搜索播放",
"keywords_online_play": "在线播放",
"keywords_singer_play": "播放歌手",
"keywords_stop": "关机,暂停,停止,停止播放",
@@ -135,7 +129,6 @@
"play_type_seq_tts_msg": "已经设置为顺序播放",
"recently_added_playlist_len": 50,
"enable_cmd_del_music": false,
"search_music_count": 100,
"web_music_proxy": true,
"edge_tts_voice": "zh-CN-XiaoyiNeural",
"enable_auto_clean_temp": true

View File

@@ -30,7 +30,7 @@ router = APIRouter(dependencies=[Depends(verification)])
@router.get("/searchmusic")
def searchmusic(name: str = ""):
"""搜索音乐"""
return xiaomusic.searchmusic(name)
return xiaomusic.music_library.searchmusic(name)
"""======================在线搜索相关接口============================="""
@@ -185,7 +185,7 @@ def playingmusic(did: str = ""):
@router.get("/musiclist")
async def musiclist():
"""音乐列表"""
return xiaomusic.get_music_list()
return xiaomusic.music_library.get_music_list()
@router.get("/musicinfo")
@@ -246,7 +246,7 @@ async def playmusic(data: DidPlayMusic):
return {"ret": "Did not exist"}
log.info(f"playmusic {did} musicname:{musicname} searchkey:{searchkey}")
await xiaomusic.do_play(did, musicname, searchkey, exact=True)
await xiaomusic.do_play(did, musicname, searchkey)
return {"ret": "OK"}

View File

@@ -31,7 +31,6 @@ class CommandHandler:
self.config = config
self.log = log
self.device_manager = device_manager
self.active_cmd = config.active_cmd.split(",") if config.active_cmd else []
self.last_cmd = ""
async def do_check_cmd(self, did="", query="", ctrl_panel=True, **kwargs):
@@ -63,7 +62,7 @@ class CommandHandler:
await device.check_replay()
return
# 执行命令 todo 把self.xiaomusic改为从device获取并执行
# 执行命令
func = getattr(self.device, opvalue)
await func(did=did, arg1=oparg)
@@ -123,12 +122,13 @@ class CommandHandler:
opvalue = self.config.key_word_dict.get(opkey)
# 检查是否在激活命令中
active_cmd_arr = self.config.get_active_cmd_arr()
if (
not ctrl_panel
and not device.is_playing
and self.active_cmd
and opvalue not in self.active_cmd
and opkey not in self.active_cmd
and active_cmd_arr
and opvalue not in active_cmd_arr
and opkey not in active_cmd_arr
):
self.log.info(f"不在激活命令中 {opvalue}")
continue
@@ -160,12 +160,13 @@ class CommandHandler:
if query not in self.config.key_match_order:
return None
active_cmd_arr = self.config.get_active_cmd_arr()
opvalue = self.config.key_word_dict.get(query)
# 控制面板/正在播放时允许执行/是否在激活命令中
if (
ctrl_panel
or device.is_playing
or not self.active_cmd
or opvalue in self.active_cmd
or not active_cmd_arr
or opvalue in active_cmd_arr
):
return opvalue

View File

@@ -110,7 +110,7 @@ class Config:
) # mutagen or ffprobe
active_cmd: str = os.getenv(
"XIAOMUSIC_ACTIVE_CMD",
"play,search_play,set_play_type_rnd,playlocal,search_playlocal,play_music_list,play_music_list_index,stop_after_minute,stop,play_next,play_prev,set_play_type_one,set_play_type_all,set_play_type_sin,set_play_type_seq,gen_music_list,add_to_favorites,del_from_favorites,cmd_del_music,online_play,singer_play",
"play,set_play_type_rnd,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop,play_next,play_prev,set_play_type_one,set_play_type_all,set_play_type_sin,set_play_type_seq,gen_music_list,add_to_favorites,del_from_favorites,cmd_del_music,online_play,singer_play",
)
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir,tmp")
ignore_tag_dirs: str = os.getenv("XIAOMUSIC_IGNORE_TAG_DIRS", "")
@@ -148,11 +148,7 @@ class Config:
keywords_playlocal: str = os.getenv(
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
)
keywords_search_playlocal: str = os.getenv(
"XIAOMUSIC_KEYWORDS_SEARCH_PLAYLOCAL", "本地搜索播放"
)
keywords_play: str = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
keywords_search_play: str = os.getenv("XIAOMUSIC_KEYWORDS_SEARCH_PLAY", "搜索播放")
keywords_online_play: str = os.getenv("XIAOMUSIC_KEYWORDS_ONLINE_PLAY", "在线播放")
keywords_singer_play: str = os.getenv("XIAOMUSIC_KEYWORDS_SINGER_PLAY", "播放歌手")
keywords_stop: str = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止,停止播放")
@@ -223,8 +219,6 @@ class Config:
enable_cmd_del_music: bool = (
os.getenv("XIAOMUSIC_ENABLE_CMD_DEL_MUSIC", "false").lower() == "true"
)
# 搜索歌曲数量
search_music_count: int = int(os.getenv("XIAOMUSIC_SEARCH_MUSIC_COUNT", "100"))
# 网络歌曲使用proxy
web_music_proxy: bool = (
os.getenv("XIAOMUSIC_WEB_MUSIC_PROXY", "true").lower() == "true"
@@ -249,13 +243,11 @@ class Config:
if k not in self.key_match_order:
self.key_match_order.append(k)
def init_keyword(self):
def init(self):
self.key_match_order = default_key_match_order()
self.key_word_dict = default_key_word_dict()
self.append_keyword(self.keywords_playlocal, "playlocal")
self.append_keyword(self.keywords_search_playlocal, "search_playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_search_play, "search_play")
self.append_keyword(self.keywords_online_play, "online_play")
self.append_keyword(self.keywords_singer_play, "singer_play")
self.append_keyword(self.keywords_stop, "stop")
@@ -265,6 +257,10 @@ class Config:
x for x in self.key_match_order if x in self.key_word_dict
]
# 转换数据
self._active_cmd_arr = self.active_cmd.split(",") if self.active_cmd else []
self._exclude_dirs_set = set(self.exclude_dirs.split(","))
def __post_init__(self) -> None:
if self.proxy:
validate_proxy(self.proxy)
@@ -272,7 +268,7 @@ class Config:
if not self.hostname.startswith(("http://", "https://")):
self.hostname = f"http://{self.hostname}" # 默认 http
self.init_keyword()
self.init()
# 保存配置到 config-example.json 文件
if self.enable_config_example:
with open("config-example.json", "w") as f:
@@ -329,7 +325,13 @@ class Config:
converted_value = self.convert_value(k, v, type_hints)
if converted_value is not None:
setattr(self, k, converted_value)
self.init_keyword()
self.init()
def get_active_cmd_arr(self):
return self._active_cmd_arr
def get_exclude_dirs_set(self):
return self._exclude_dirs_set
# 获取设置文件
def getsettingfile(self):

View File

@@ -34,7 +34,7 @@ class DeviceManager:
self.xiaomusic = xiaomusic
# 设备相关数据结构
self.devices = {} # key 为 didvalue 为 XiaoMusicDevice 实例
self.devices: dict[str, XiaoMusicDevice] = {}
self.device_id_did = {} # device_id 到 did 的映射
self.groups = {} # 设备分组key 为组名value 为 device_id 列表
@@ -141,7 +141,7 @@ class DeviceManager:
await auth_manager.try_update_device_id()
self._update_devices()
def set_devices(self, devices):
def set_devices(self, devices: dict[str, XiaoMusicDevice]):
"""设置设备实例字典
这个方法用于在主类中设置实际的设备实例。

View File

@@ -161,53 +161,30 @@ class XiaoMusicDevice:
"""播放音乐(外部接口)"""
return await self._playmusic(name)
def update_playlist(self, reorder=True):
"""初始化/更新播放列表
Args:
reorder: 是否重新排序
"""
def update_playlist(self):
"""初始化/更新播放列表"""
# 没有重置 list 且非初始化
if self.device.cur_playlist == "临时搜索列表" and len(self._play_list) > 0:
# 更新总播放列表为了UI显示
self.xiaomusic.music_library.music_list["临时搜索列表"] = copy.copy(
self._play_list
)
elif (
self.device.cur_playlist == "临时搜索列表" and len(self._play_list) == 0
) or (self.device.cur_playlist not in self.xiaomusic.music_library.music_list):
if self.device.cur_playlist not in self.xiaomusic.music_library.music_list:
self.device.cur_playlist = "全部"
else:
pass # 指定了已知的播放列表名称
list_name = self.device.cur_playlist
self._play_list = copy.copy(self.xiaomusic.music_library.music_list[list_name])
if reorder:
if self.device.play_type == PLAY_TYPE_RND:
random.shuffle(self._play_list)
self.log.info(
f"随机打乱 {list_name} {list2str(self._play_list, self.config.verbose)}"
)
else:
self._play_list.sort(key=custom_sort_key)
self.log.info(
f"没打乱 {list_name} {list2str(self._play_list, self.config.verbose)}"
)
else:
if self.device.play_type == PLAY_TYPE_RND:
random.shuffle(self._play_list)
self.log.info(
f"更新 {list_name} {list2str(self._play_list, self.config.verbose)}"
f"随机打乱 {list_name} {list2str(self._play_list, self.config.verbose)}"
)
else:
self._play_list.sort(key=custom_sort_key)
self.log.info(
f"没打乱 {list_name} {list2str(self._play_list, self.config.verbose)}"
)
async def play(self, name="", search_key="", exact=True, update_cur_list=False):
async def play(self, name="", search_key=""):
"""播放歌曲(外部接口)"""
self._last_cmd = "play"
return await self._play(
name=name,
search_key=search_key,
exact=exact,
update_cur_list=update_cur_list,
)
return await self._play(name=name, search_key=search_key)
async def _check_and_download_music(self, name, search_key, allow_download):
"""检查本地歌曲是否存在,如果不存在则根据参数决定是否下载
@@ -242,21 +219,12 @@ class XiaoMusicDevice:
await self.add_download_music(name)
return True
async def _play_internal(
self,
name="",
search_key="",
exact=True,
update_cur_list=False,
allow_download=True,
):
async def _play_internal(self, name="", search_key="", allow_download=True):
"""播放歌曲的内部统一实现
Args:
name: 歌曲名称
search_key: 搜索关键词
exact: 是否精确匹配
update_cur_list: 是否更新当前列表
allow_download: 是否允许下载True: _play行为False: playlocal行为
"""
# 初始检查逻辑
@@ -268,29 +236,15 @@ class XiaoMusicDevice:
name = self.get_cur_music()
self.log.info(
f"play_internal. search_key:{search_key} name:{name} exact:{exact} allow_download:{allow_download}"
f"play_internal. search_key:{search_key} name:{name} allow_download:{allow_download}"
)
if not name:
self.log.info(f"没有歌曲播放了 name:{name} search_key:{search_key}")
return
# 精确匹配分支
if exact:
# 检查本地是否存在歌曲,不存在则根据参数决定是否下载
if not await self._check_and_download_music(
name, search_key, allow_download
):
return
# 播放歌曲
await self._playmusic(name)
return
# 模糊搜索分支
names = self.xiaomusic.find_real_music_name(
name, n=self.config.search_music_count
)
# 模糊搜索
names = self.xiaomusic.music_library.find_real_music_name(name, n=1)
self.log.info(f"play_internal. names:{names} {len(names)}")
if not names:
@@ -304,19 +258,8 @@ class XiaoMusicDevice:
await self._playmusic(name)
return
# 处理搜索结果
if len(names) > 1: # 大于一首歌才更新
self._play_list = names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist()
else: # 只有一首歌append
if names[0] not in self._play_list:
self._play_list = self._play_list + names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist(reorder=False)
name = names[0]
if update_cur_list and (name not in self._play_list):
if name not in self._play_list:
# 根据当前歌曲匹配歌曲列表
self.device.cur_playlist = self.find_cur_playlist(name)
self.update_playlist()
@@ -327,13 +270,11 @@ class XiaoMusicDevice:
# 本地存在歌曲,直接播放
await self._playmusic(name)
async def _play(self, name="", search_key="", exact=True, update_cur_list=False):
async def _play(self, name="", search_key=""):
"""播放歌曲(内部实现)- 支持下载"""
return await self._play_internal(
name=name,
search_key=search_key,
exact=exact,
update_cur_list=update_cur_list,
allow_download=True,
)
@@ -360,7 +301,7 @@ class XiaoMusicDevice:
if name == "":
self.log.info("本地没有歌曲")
return
await self._play(name, exact=True)
await self._play(name)
async def play_prev(self):
"""播放上一首(外部接口)"""
@@ -382,18 +323,12 @@ class XiaoMusicDevice:
if name == "":
await self.do_tts("本地没有歌曲")
return
await self._play(name, exact=True)
await self._play(name)
async def playlocal(self, name="", exact=True, update_cur_list=False):
async def playlocal(self, name=""):
"""播放本地歌曲 - 不下载"""
self._last_cmd = "playlocal"
return await self._play_internal(
name=name,
search_key="",
exact=exact,
update_cur_list=update_cur_list,
allow_download=False,
)
return await self._play_internal(name=name, search_key="", allow_download=False)
async def _playmusic(self, name):
"""播放音乐的核心实现"""
@@ -911,7 +846,7 @@ class XiaoMusicDevice:
if not music_name:
music_name = self.device.playlist2music.get(list_name, "")
self.log.info(f"开始播放列表{list_name} {music_name}")
await self._play(music_name, exact=True)
await self._play(music_name)
async def stop(self, arg1=""):
"""停止播放"""
@@ -1009,7 +944,7 @@ class XiaoMusicDevice:
匹配顺序:
1. 收藏
2. 最近新增
3. 排除(全部,所有歌曲,所有电台,临时搜索列表
3. 排除(全部,所有歌曲,所有电台)
4. 所有歌曲
5. 所有电台
6. 全部
@@ -1020,7 +955,7 @@ class XiaoMusicDevice:
if name in music_list.get("最近新增", []):
return "最近新增"
for list_name, play_list in music_list.items():
if (list_name not in ["全部", "所有歌曲", "所有电台", "临时搜索列表"]) and (
if (list_name not in ["全部", "所有歌曲", "所有电台"]) and (
name in play_list
):
return list_name

View File

@@ -45,8 +45,6 @@ class MusicLibrary:
self,
config,
log,
music_path_depth,
exclude_dirs,
event_bus=None,
):
"""初始化音乐库
@@ -54,14 +52,10 @@ class MusicLibrary:
Args:
config: 配置对象
log: 日志对象
music_path_depth: 音乐目录扫描深度
exclude_dirs: 排除的目录列表
event_bus: 事件总线对象(可选)
"""
self.config = config
self.log = log
self.music_path_depth = music_path_depth
self.exclude_dirs = exclude_dirs
self.event_bus = event_bus
# 音乐库数据
@@ -94,10 +88,11 @@ class MusicLibrary:
all_music_by_dir = {}
# 扫描本地音乐目录
exclude_dirs_set = self.config.get_exclude_dirs_set()
local_musics = traverse_music_directory(
self.config.music_path,
depth=self.music_path_depth,
exclude_dirs=self.exclude_dirs,
depth=self.config.music_path_depth,
exclude_dirs=exclude_dirs_set,
support_extension=SUPPORT_MUSIC_TYPE,
)
@@ -128,7 +123,6 @@ class MusicLibrary:
# 初始化播放列表(使用 OrderedDict 保持顺序)
self.music_list = OrderedDict(
{
"临时搜索列表": [],
"所有歌曲": [],
"所有电台": [],
"收藏": [],

View File

@@ -269,7 +269,9 @@ class OnlineMusicService:
# 解析歌手名可能通过AI或直接分割
parsed_keyword, parsed_artist = await self._parse_keyword_with_ai(name)
list_name = "_online_" + parsed_artist
artist_song_list = self.xiaomusic.get_music_list().get(list_name, [])
artist_song_list = self.xiaomusic.music_library.get_music_list().get(
list_name, []
)
if len(artist_song_list) > 0:
# 如果歌单存在,则直接播放
song_name = artist_song_list[0]
@@ -440,7 +442,7 @@ class OnlineMusicService:
if did != "web_device" and self.xiaomusic.did_exist(did):
# 歌单推送应该是全部播放,不随机打乱
await self.xiaomusic.set_play_type(did, PLAY_TYPE_ALL, False)
push_playlist = self.xiaomusic.get_music_list()[list_name]
push_playlist = self.xiaomusic.music_library.get_music_list()[list_name]
song_name = push_playlist[0]
await self.xiaomusic.do_play_music_list(did, list_name, song_name)
return {

View File

@@ -319,7 +319,7 @@
<input
id="active_cmd"
type="text"
value="play,search_play,set_play_type_rnd,playlocal,search_playlocal,play_music_list,play_music_list_index,stop_after_minute,stop,play_next,play_prev,set_play_type_one,set_play_type_all,set_play_type_sin,set_play_type_seq,gen_music_list,add_to_favorites,del_from_favorites,cmd_del_music,online_play,singer_play"
value="play,set_play_type_rnd,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop,play_next,play_prev,set_play_type_one,set_play_type_all,set_play_type_sin,set_play_type_seq,gen_music_list,add_to_favorites,del_from_favorites,cmd_del_music,online_play,singer_play"
/>
</div>
</div>
@@ -329,9 +329,6 @@
<h3 class="card-title">搜索与匹配配置</h3>
<div class="card-content">
<div class="rows">
<label for="search_music_count">搜索歌曲数量:</label>
<input id="search_music_count" type="number" value="100" />
<label for="fuzzy_match_cutoff">模糊匹配阈值(0.1~0.9):</label>
<input id="fuzzy_match_cutoff" type="number" value="0.6" />
@@ -480,20 +477,6 @@
value="关机,暂停,停止,停止播放"
/>
<label for="keywords_search_playlocal"
>本地搜索播放口令(会产生临时播放列表):</label
>
<input
id="keywords_search_playlocal"
type="text"
value="本地搜索播放"
/>
<label for="keywords_search_play"
>搜索播放口令(会产生临时播放列表):</label
>
<input id="keywords_search_play" type="text" value="搜索播放" />
<label for="keywords_online_play"
>在线播放口令(在线搜索接口或插件):</label
>

View File

@@ -107,8 +107,6 @@ class XiaoMusic:
self.music_library = MusicLibrary(
config=self.config,
log=self.log,
music_path_depth=self.music_path_depth,
exclude_dirs=self.exclude_dirs,
event_bus=self.event_bus,
)
@@ -172,9 +170,6 @@ class XiaoMusic:
if not os.path.exists(self.config.download_path):
os.makedirs(self.config.download_path)
self.active_cmd = self.config.active_cmd.split(",")
self.exclude_dirs = set(self.config.exclude_dirs.split(","))
self.music_path_depth = self.config.music_path_depth
self.continue_play = self.config.continue_play
def setup_logger(self):
@@ -292,10 +287,6 @@ class XiaoMusic:
async def check_replay(self, did):
return await self.device_manager.devices[did].check_replay()
def find_real_music_name(self, name, n):
"""模糊搜索音乐名称(委托给 music_library"""
return self.music_library.find_real_music_name(name, n)
def did_exist(self, did):
return did in self.device_manager.devices
@@ -501,42 +492,15 @@ class XiaoMusic:
name = search_key
# 语音播放会根据歌曲匹配更新当前播放列表
return await self.do_play(
did, name, search_key, exact=True, update_cur_list=True
)
# 搜索播放:会产生临时播放列表
async def search_play(self, did="", arg1="", **kwargs):
parts = arg1.split("|")
search_key = parts[0]
name = parts[1] if len(parts) > 1 else search_key
if not name:
name = search_key
# 语音搜索播放会更新当前播放列表为临时播放列表
return await self.do_play(
did, name, search_key, exact=False, update_cur_list=False
)
return await self.do_play(did, name, search_key)
# 后台搜索播放
async def do_play(
self, did, name, search_key="", exact=False, update_cur_list=False
):
return await self.device_manager.devices[did].play(
name, search_key, exact, update_cur_list
)
async def do_play(self, did, name, search_key=""):
return await self.device_manager.devices[did].play(name, search_key)
# 本地播放
async def playlocal(self, did="", arg1="", **kwargs):
return await self.device_manager.devices[did].playlocal(
arg1, update_cur_list=True
)
# 本地搜索播放
async def search_playlocal(self, did="", arg1="", **kwargs):
return await self.device_manager.devices[did].playlocal(
arg1, exact=False, update_cur_list=False
)
return await self.device_manager.devices[did].playlocal(arg1)
async def play_next(self, did="", **kwargs):
return await self.device_manager.devices[did].play_next()
@@ -596,16 +560,6 @@ class XiaoMusic:
volume = int(arg1)
return await self.device_manager.devices[did].set_volume(volume)
# 搜索音乐
def searchmusic(self, name):
"""搜索音乐(委托给 music_library"""
return self.music_library.searchmusic(name)
# 获取播放列表
def get_music_list(self):
"""获取播放列表(委托给 music_library"""
return self.music_library.get_music_list()
# 获取当前的播放列表
def get_cur_play_list(self, did):
return self.device_manager.devices[did].get_cur_play_list()