mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a583119d0 | ||
|
|
9349070e8b | ||
|
|
01d99dc699 | ||
|
|
f4d9a6c1fd | ||
|
|
1919bc84d9 | ||
|
|
4c1761468f | ||
|
|
c75230a67d | ||
|
|
ee7ffa55cb | ||
|
|
45bbc8af42 | ||
|
|
ab8bf8fa62 | ||
|
|
493cad080e | ||
|
|
eaa159c5cb | ||
|
|
96e3b8c2ff | ||
|
|
794e8dcd06 | ||
|
|
f2675e4340 | ||
|
|
e5059840fb | ||
|
|
f3e57789fa | ||
|
|
c151b826f7 | ||
|
|
1b3ed3b35a |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -28,3 +28,9 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: hanxi/xiaomusic
|
||||
|
||||
@@ -11,6 +11,7 @@ WORKDIR /app
|
||||
COPY --from=builder /app/.venv /app/.venv
|
||||
COPY --from=builder /app/ffmpeg /app/ffmpeg
|
||||
COPY xiaomusic/ ./xiaomusic/
|
||||
COPY plugins/ ./plugins/
|
||||
COPY xiaomusic.py .
|
||||
ENV XDG_CONFIG_HOME=/config
|
||||
ENV XIAOMUSIC_HOSTNAME=192.168.2.5
|
||||
|
||||
11
README.md
11
README.md
@@ -137,7 +137,7 @@ pdm run xiaomusic.py
|
||||
| ---- | ---------------------------------------------------------------------------------------------- |
|
||||
| L06A | [小爱音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l06a) |
|
||||
| L07A | [Redmi小爱音箱 Play](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l7a) |
|
||||
| S12/S12A | [小米AI音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.s12) |
|
||||
| S12/S12A/MDZ-25-DA | [小米AI音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.s12) |
|
||||
| LX5A | [小爱音箱 万能遥控版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx5a) |
|
||||
| LX05 | [小爱音箱Play(2019款)](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx05) |
|
||||
| L16A | [Xiaomi Sound](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l16a) |
|
||||
@@ -159,6 +159,7 @@ pdm run xiaomusic.py
|
||||
- wav
|
||||
- ape
|
||||
- ogg
|
||||
- m4a
|
||||
|
||||
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
|
||||
> 已知 L05B L05C 不支持 flac 格式。
|
||||
@@ -315,13 +316,19 @@ services:
|
||||
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码
|
||||
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,记得把目录映射到主机,默认情况会把配置存放在music目录,具体见 <https://github.com/hanxi/xiaomusic/issues/74>
|
||||
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82>
|
||||
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号,v0.1.86 之后的版本可以不用设置。
|
||||
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号,触屏版目前还需要设置这个为 true 。
|
||||
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
|
||||
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
|
||||
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
|
||||
- XIAOMUSIC_ENABLE_FUZZY_MATCH 设为 true 时开启模糊匹配(默认),设为 false 时关闭模糊匹配,支持模糊匹配歌名和歌单名。 具体见 <https://github.com/hanxi/xiaomusic/issues/52>
|
||||
- XIAOMUSIC_FUZZY_MATCH_CUTOFF 设置模糊搜索匹配的最低相似度阈值(默认0.6,可以配0到1直接的小数),越小越模糊,越大越精准。具体见 <https://github.com/hanxi/xiaomusic/issues/52>
|
||||
- XIAOMUSIC_PUBLIC_PORT 用于设置外网端口,当使用反向代理时可以设置为外网端口,XIAOMUSIC_HOSTNAME 设为外网IP或者域名即可。
|
||||
- XIAOMUSIC_DOWNLOAD_PATH 变量可以配置下载目录,默认为空,表示使用 music 目录为下载目录。设置这个目录必须是 music 的子目录,否则刷新列表后会找不到歌曲。具体见 <https://github.com/hanxi/xiaomusic/issues/98>
|
||||
|
||||
## 高级篇
|
||||
|
||||
- 自定义口令功能 <https://github.com/hanxi/xiaomusic/issues/105>
|
||||
- [ ] 缺少一篇教程 [如何写自定义插件](https://github.com/hanxi/xiaomusic/issues/105)
|
||||
|
||||
## 讨论区
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
"conf_path": null,
|
||||
"hostname": "192.168.2.5",
|
||||
"port": 8090,
|
||||
"public_port": 0,
|
||||
"proxy": null,
|
||||
"search_prefix": "ytsearch:",
|
||||
"search_prefix": "bilisearch:",
|
||||
"ffmpeg_location": "./ffmpeg/bin",
|
||||
"active_cmd": "play,random_play,playlocal,play_music_list,stop",
|
||||
"exclude_dirs": "@eaDir",
|
||||
@@ -21,12 +22,60 @@
|
||||
"music_list_url": "",
|
||||
"music_list_json": "",
|
||||
"disable_download": false,
|
||||
"key_word_dict": {
|
||||
"播放歌曲": "play",
|
||||
"播放本地歌曲": "playlocal",
|
||||
"关机": "stop",
|
||||
"下一首": "play_next",
|
||||
"单曲循环": "set_play_type_one",
|
||||
"全部循环": "set_play_type_all",
|
||||
"随机播放": "random_play",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"set_volume#": "set_volume",
|
||||
"get_volume#": "get_volume",
|
||||
"本地播放歌曲": "playlocal",
|
||||
"放歌曲": "play",
|
||||
"暂停": "stop",
|
||||
"停止": "stop",
|
||||
"停止播放": "stop",
|
||||
"测试自定义口令": "exec#code1(\"hello\")",
|
||||
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
|
||||
},
|
||||
"key_match_order": [
|
||||
"set_volume#",
|
||||
"get_volume#",
|
||||
"分钟后关机",
|
||||
"播放歌曲",
|
||||
"下一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"刷新列表",
|
||||
"播放列表",
|
||||
"播放本地歌曲",
|
||||
"本地播放歌曲",
|
||||
"放歌曲",
|
||||
"暂停",
|
||||
"停止",
|
||||
"停止播放",
|
||||
"测试自定义口令",
|
||||
"测试链接"
|
||||
],
|
||||
"use_music_api": false,
|
||||
"use_music_audio_id": "1582971365183456177",
|
||||
"use_music_id": "355454500",
|
||||
"log_file": "/tmp/xiaomusic.txt",
|
||||
"fuzzy_match_cutoff": 0.6,
|
||||
"enable_fuzzy_match": true,
|
||||
"stop_tts_msg": "收到,再见",
|
||||
"keywords_playlocal": "播放本地歌曲,本地播放歌曲",
|
||||
"keywords_play": "播放歌曲,放歌曲",
|
||||
"keywords_stop": "关机,暂停,停止,停止播放"
|
||||
"keywords_stop": "关机,暂停,停止,停止播放",
|
||||
"user_key_word_dict": {
|
||||
"测试自定义口令": "exec#code1(\"hello\")",
|
||||
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
|
||||
}
|
||||
}
|
||||
0
plugins/__init__.py
Normal file
0
plugins/__init__.py
Normal file
4
plugins/code1.py
Normal file
4
plugins/code1.py
Normal file
@@ -0,0 +1,4 @@
|
||||
async def code1(arg1):
|
||||
global log, xiaomusic
|
||||
log.info(f"code1:{arg1}")
|
||||
await xiaomusic.do_tts("你好,我是自定义的测试口令")
|
||||
10
plugins/httpget.py
Normal file
10
plugins/httpget.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import requests
|
||||
|
||||
|
||||
def httpget(url):
|
||||
global log
|
||||
|
||||
# 发起请求
|
||||
response = requests.get(url, timeout=5) # 增加超时以避免长时间挂起
|
||||
response.raise_for_status() # 如果响应不是200,引发HTTPError异常
|
||||
log.info(f"httpget url:{url} response:{response.text}")
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.1.92"
|
||||
version = "0.1.93"
|
||||
description = "Play Music with xiaomi AI speaker"
|
||||
authors = [
|
||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.92"
|
||||
__version__ = "0.1.93"
|
||||
|
||||
@@ -61,6 +61,12 @@ def main():
|
||||
dest="ffmpeg_location",
|
||||
help="ffmpeg bin path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable_config_example",
|
||||
dest="enable_config_example",
|
||||
help="是否输出示例配置文件",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
print(LOGO.format(f"XiaoMusic v{__version__} by: github.com/hanxi"))
|
||||
|
||||
|
||||
@@ -3,57 +3,72 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import get_type_hints
|
||||
|
||||
from xiaomusic.utils import validate_proxy
|
||||
|
||||
|
||||
# 默认口令
|
||||
DEFAULT_KEY_WORD_DICT = {
|
||||
"播放歌曲": "play",
|
||||
"播放本地歌曲": "playlocal",
|
||||
"关机": "stop",
|
||||
"下一首": "play_next",
|
||||
"单曲循环": "set_play_type_one",
|
||||
"全部循环": "set_play_type_all",
|
||||
"随机播放": "random_play",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"set_volume#": "set_volume",
|
||||
"get_volume#": "get_volume",
|
||||
}
|
||||
def default_key_word_dict():
|
||||
return {
|
||||
"播放歌曲": "play",
|
||||
"播放本地歌曲": "playlocal",
|
||||
"关机": "stop",
|
||||
"下一首": "play_next",
|
||||
"单曲循环": "set_play_type_one",
|
||||
"全部循环": "set_play_type_all",
|
||||
"随机播放": "random_play",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"set_volume#": "set_volume",
|
||||
"get_volume#": "get_volume",
|
||||
}
|
||||
|
||||
|
||||
def default_user_key_word_dict():
|
||||
return {
|
||||
"测试自定义口令": 'exec#code1("hello")',
|
||||
"测试链接": 'exec#httpget("https://github.com/hanxi/xiaomusic")',
|
||||
}
|
||||
|
||||
|
||||
# 命令参数在前面
|
||||
KEY_WORD_ARG_BEFORE_DICT = {
|
||||
"分钟后关机": True,
|
||||
}
|
||||
|
||||
|
||||
# 口令匹配优先级
|
||||
DEFAULT_KEY_MATCH_ORDER = [
|
||||
"set_volume#",
|
||||
"get_volume#",
|
||||
"分钟后关机",
|
||||
"播放歌曲",
|
||||
"下一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"刷新列表",
|
||||
"播放列表",
|
||||
]
|
||||
def default_key_match_order():
|
||||
return [
|
||||
"set_volume#",
|
||||
"get_volume#",
|
||||
"分钟后关机",
|
||||
"播放歌曲",
|
||||
"下一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"刷新列表",
|
||||
"播放列表",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
hardware: str = os.getenv("MI_HARDWARE", "L07A")
|
||||
account: str = os.getenv("MI_USER", "")
|
||||
password: str = os.getenv("MI_PASS", "")
|
||||
mi_did: str = os.getenv("MI_DID", "")
|
||||
mi_did: str = os.getenv("MI_DID", "") # 逗号分割支持多设备
|
||||
hardware: str = os.getenv("MI_HARDWARE", "L07A") # 逗号分割支持多设备
|
||||
cookie: str = ""
|
||||
verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
|
||||
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
|
||||
music_path: str = os.getenv(
|
||||
"XIAOMUSIC_MUSIC_PATH", "music"
|
||||
) # 只能是music目录下的子目录
|
||||
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "")
|
||||
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", None)
|
||||
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
|
||||
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
|
||||
@@ -71,15 +86,15 @@ class Config:
|
||||
disable_httpauth: bool = (
|
||||
os.getenv("XIAOMUSIC_DISABLE_HTTPAUTH", "true").lower() == "true"
|
||||
)
|
||||
httpauth_username: str = os.getenv("XIAOMUSIC_HTTPAUTH_USERNAME", "admin")
|
||||
httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "admin")
|
||||
httpauth_username: str = os.getenv("XIAOMUSIC_HTTPAUTH_USERNAME", "")
|
||||
httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "")
|
||||
music_list_url: str = os.getenv("XIAOMUSIC_MUSIC_LIST_URL", "")
|
||||
music_list_json: str = os.getenv("XIAOMUSIC_MUSIC_LIST_JSON", "")
|
||||
disable_download: bool = (
|
||||
os.getenv("XIAOMUSIC_DISABLE_DOWNLOAD", "false").lower() == "true"
|
||||
)
|
||||
key_word_dict = DEFAULT_KEY_WORD_DICT.copy()
|
||||
key_match_order = DEFAULT_KEY_MATCH_ORDER.copy()
|
||||
key_word_dict: dict[str, str] = field(default_factory=default_key_word_dict)
|
||||
key_match_order: list[str] = field(default_factory=default_key_match_order)
|
||||
use_music_api: bool = (
|
||||
os.getenv("XIAOMUSIC_USE_MUSIC_API", "false").lower() == "true"
|
||||
)
|
||||
@@ -95,12 +110,16 @@ class Config:
|
||||
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
|
||||
)
|
||||
stop_tts_msg: str = os.getenv("XIAOMUSIC_STOP_TTS_MSG", "收到,再见")
|
||||
enable_config_example: bool = False
|
||||
|
||||
keywords_playlocal: str = os.getenv(
|
||||
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
|
||||
)
|
||||
keywords_play: str = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
|
||||
keywords_stop: str = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止,停止播放")
|
||||
user_key_word_dict: dict[str, str] = field(
|
||||
default_factory=default_user_key_word_dict
|
||||
)
|
||||
|
||||
def append_keyword(self, keys, action):
|
||||
for key in keys.split(","):
|
||||
@@ -108,17 +127,26 @@ class Config:
|
||||
if key not in self.key_match_order:
|
||||
self.key_match_order.append(key)
|
||||
|
||||
def append_user_keyword(self):
|
||||
for k, v in self.user_key_word_dict.items():
|
||||
self.key_word_dict[k] = v
|
||||
self.key_match_order.append(k)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.proxy:
|
||||
validate_proxy(self.proxy)
|
||||
|
||||
self.append_keyword(self.keywords_playlocal, "playlocal")
|
||||
self.append_keyword(self.keywords_play, "play")
|
||||
self.append_keyword(self.keywords_stop, "stop")
|
||||
|
||||
self.append_user_keyword()
|
||||
|
||||
# 保存配置到 config-example.json 文件
|
||||
# with open("config-example.json", "w") as f:
|
||||
# data = asdict(self)
|
||||
# json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
if self.enable_config_example:
|
||||
with open("config-example.json", "w") as f:
|
||||
data = asdict(self)
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
@classmethod
|
||||
def from_options(cls, options: argparse.Namespace) -> Config:
|
||||
|
||||
@@ -4,6 +4,7 @@ SUPPORT_MUSIC_TYPE = [
|
||||
".wav",
|
||||
".ape",
|
||||
".ogg",
|
||||
".m4a",
|
||||
]
|
||||
|
||||
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={timestamp}&limit=2"
|
||||
|
||||
69
xiaomusic/plugin.py
Normal file
69
xiaomusic/plugin.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, xiaomusic, plugin_dir="plugins"):
|
||||
self.xiaomusic = xiaomusic
|
||||
self.log = xiaomusic.log
|
||||
self._funcs = {}
|
||||
self._load_plugins(plugin_dir)
|
||||
|
||||
def _load_plugins(self, plugin_dir):
|
||||
# 假设 plugins 已经在搜索路径上
|
||||
package_name = plugin_dir
|
||||
package = importlib.import_module(package_name)
|
||||
|
||||
# 遍历 package 中所有模块并动态导入它们
|
||||
for _, modname, _ in pkgutil.iter_modules(package.__path__, package_name + "."):
|
||||
# 跳过__init__文件
|
||||
if modname.endswith("__init__"):
|
||||
continue
|
||||
module = importlib.import_module(modname)
|
||||
# 将 log 和 xiaomusic 注入模块的命名空间
|
||||
module.log = self.log
|
||||
module.xiaomusic = self.xiaomusic
|
||||
|
||||
# 动态获取模块中与文件名同名的函数
|
||||
function_name = modname.split(".")[-1] # 从模块全名提取函数名
|
||||
if hasattr(module, function_name):
|
||||
self._funcs[function_name] = getattr(module, function_name)
|
||||
else:
|
||||
self.log.error(
|
||||
f"No function named '{function_name}' found in module {modname}"
|
||||
)
|
||||
|
||||
def get_func(self, plugin_name):
|
||||
"""根据插件名获取插件函数"""
|
||||
return self._funcs.get(plugin_name)
|
||||
|
||||
def get_local_namespace(self):
|
||||
"""返回包含所有插件函数的字典,可以用作 exec 要执行的代码的命名空间"""
|
||||
return self._funcs.copy()
|
||||
|
||||
async def execute_plugin(self, code):
|
||||
"""
|
||||
执行指定的插件代码。插件函数可以是同步或异步。
|
||||
:param code: 需要执行的插件函数代码(例如 'plugin1("hello")')
|
||||
"""
|
||||
# 分解代码字符串以获取函数名
|
||||
func_name = code.split("(")[0]
|
||||
|
||||
# 根据解析出的函数名从插件字典中获取函数
|
||||
plugin_func = self.get_func(func_name)
|
||||
|
||||
if not plugin_func:
|
||||
raise ValueError(f"No plugin function named '{func_name}' found.")
|
||||
|
||||
# 检查函数是否是异步函数
|
||||
global_namespace = globals().copy()
|
||||
local_namespace = self.get_local_namespace()
|
||||
if inspect.iscoroutinefunction(plugin_func):
|
||||
# 如果是异步函数,构建执行用的协程对象
|
||||
coroutine = eval(code, global_namespace, local_namespace)
|
||||
# 等待协程执行
|
||||
await coroutine
|
||||
else:
|
||||
# 如果是普通函数,直接执行代码
|
||||
eval(code, global_namespace, local_namespace)
|
||||
@@ -27,13 +27,33 @@ function postJSON() {
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
function sendDebugCmd() {
|
||||
var cmd = $("#cmd").val();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/cmd",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({cmd: cmd}),
|
||||
success: () => {
|
||||
},
|
||||
error: () => {
|
||||
// 请求失败时执行的操作
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Debug For XiaoMusic</h1>
|
||||
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
|
||||
<button onclick="postJSON()">提交</button><br>
|
||||
|
||||
<hr>
|
||||
<input id="cmd" type="text"></input>
|
||||
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
|
||||
</body>
|
||||
|
||||
<footer>
|
||||
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
||||
</footer>
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
<script src="/static/jquery-3.7.1.min.js"></script>
|
||||
<script src="/static/setting.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
</script>
|
||||
-->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h2>小爱音箱设置面板
|
||||
@@ -14,6 +22,14 @@
|
||||
</a>)
|
||||
</h2>
|
||||
<hr>
|
||||
|
||||
<div class="rows">
|
||||
<label for="mi_did_hardware">*勾选设备(至少勾选1个):</label>
|
||||
<div id="mi_did_hardware">
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div id="setting" class="rows">
|
||||
<label for="account">*小米账号:</label>
|
||||
<input id="account" type="text" placeholder="填写小米登录账号"></input>
|
||||
@@ -21,12 +37,6 @@
|
||||
<label for="password">*小米密码:</label>
|
||||
<input id="password" type="password" placeholder="填写小米登录密码"></input>
|
||||
|
||||
<label for="mi_did">*MI_DID:</label>
|
||||
<select id="mi_did"></select>
|
||||
|
||||
<label for="hardware">*MI_HARDWARE(型号):</label>
|
||||
<select id="hardware" disabled></select>
|
||||
|
||||
<label for="hostname">*XIAOMUSIC_HOSTNAME(IP或域名):</label>
|
||||
<input id="hostname" type="text"></input>
|
||||
|
||||
@@ -39,13 +49,16 @@
|
||||
<label for="music_path">音乐目录:</label>
|
||||
<input id="music_path" type="text" value="music"></input>
|
||||
|
||||
<label for="download_path">音乐下载目录(必须是music的子目录):</label>
|
||||
<input id="download_path" type="text"></input>
|
||||
|
||||
<label for="conf_path">配置文件目录:</label>
|
||||
<input id="conf_path" type="text"></input>
|
||||
|
||||
<label for="ffmpeg_location">ffmpeg路径:</label>
|
||||
<input id="ffmpeg_location" type="text" value="./ffmpeg/bin"></input>
|
||||
|
||||
<label for="log_file">ffmpeg路径:</label>
|
||||
<label for="log_file">日志路径:</label>
|
||||
<input id="log_file" type="text" value="/tmp/xiaomusic.txt"></input>
|
||||
|
||||
<label for="active_cmd">允许唤醒的命令:</label>
|
||||
@@ -72,9 +85,9 @@
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
<label for="httpauth_username">web控制台账户:</label>
|
||||
<input id="httpauth_username" type="text" value="admin"></input>
|
||||
<input id="httpauth_username" type="text" value=""></input>
|
||||
<label for="httpauth_password">web控制台密码:</label>
|
||||
<input id="httpauth_password" type="password" value="admin"></input>
|
||||
<input id="httpauth_password" type="password" value=""></input>
|
||||
|
||||
<label for="disable_download">关闭下载功能:</label>
|
||||
<select id="disable_download">
|
||||
|
||||
@@ -16,55 +16,76 @@ $(function(){
|
||||
});
|
||||
};
|
||||
|
||||
const updateSelectOptions = (selectId, optionsList, selectedOption) => {
|
||||
const select = $(selectId);
|
||||
select.empty();
|
||||
optionsList.forEach(option => {
|
||||
select.append(new Option(option, option));
|
||||
function updateCheckbox(selector, mi_did_list, mi_did, mi_hardware_list) {
|
||||
// 清除现有的内容
|
||||
$(selector).empty();
|
||||
|
||||
// 将 mi_did 字符串通过逗号分割转换为数组,以便于判断默认选中项
|
||||
var selected_dids = mi_did.split(',');
|
||||
|
||||
// 遍历传入的 mi_did_list 和 mi_hardware_list
|
||||
$.each(mi_did_list, function(index, did) {
|
||||
// 获取硬件标识,假定列表是一一对应的
|
||||
var hardware = mi_hardware_list[index];
|
||||
|
||||
// 创建复选框元素
|
||||
var checkbox = $('<input>', {
|
||||
type: 'checkbox',
|
||||
id: did,
|
||||
value: `${did}|${hardware}`,
|
||||
class: 'custom-checkbox', // 添加样式类
|
||||
// 如果mi_did中包含了该did,则默认选中
|
||||
checked: selected_dids.indexOf(did) !== -1
|
||||
});
|
||||
|
||||
// 创建标签元素
|
||||
var label = $('<label>', {
|
||||
for: did,
|
||||
class: 'checkbox-label', // 添加样式类
|
||||
text: `【${hardware}】 ${did}` // 设定标签内容为did和hardware的拼接
|
||||
});
|
||||
|
||||
// 将复选框和标签添加到目标选择器元素中
|
||||
$(selector).append(checkbox).append(label);
|
||||
});
|
||||
select.val(selectedOption);
|
||||
};
|
||||
}
|
||||
|
||||
let isChanging = false;
|
||||
// 更新下拉菜单的函数
|
||||
const updateSelect = (selectId, value) => {
|
||||
if (!isChanging) {
|
||||
isChanging = true;
|
||||
$(selectId).val(value);
|
||||
isChanging = false;
|
||||
}
|
||||
};
|
||||
function getSelectedDidsAndHardware(containerSelector) {
|
||||
var selectedDids = [];
|
||||
var selectedHardware = [];
|
||||
|
||||
// 联动逻辑
|
||||
const linkSelects = (sourceSelect, sourceList, targetSelect, targetList) => {
|
||||
$(sourceSelect).change(function() {
|
||||
if (!isChanging) {
|
||||
const selectedValue = $(this).val();
|
||||
const selectedIndex = sourceList.indexOf(selectedValue);
|
||||
console.log(selectedIndex, selectedValue,sourceList,targetList)
|
||||
if (selectedIndex !== -1) {
|
||||
updateSelect(targetSelect, targetList[selectedIndex]);
|
||||
}
|
||||
}
|
||||
// 仅选择给定容器中选中的复选框
|
||||
$(containerSelector + ' .custom-checkbox:checked').each(function() {
|
||||
// 解析当前复选框的值(值中包含了 did 和 hardware,使用 '|' 分割)
|
||||
var parts = this.value.split('|');
|
||||
selectedDids.push(parts[0]);
|
||||
selectedHardware.push(parts[1]);
|
||||
});
|
||||
};
|
||||
|
||||
// 返回包含 did_list 和 hardware_list 的对象
|
||||
return {
|
||||
did_list: selectedDids.join(','),
|
||||
hardware_list: selectedHardware.join(',')
|
||||
};
|
||||
}
|
||||
|
||||
// 拉取现有配置
|
||||
$.get("/getsetting", function(data, status) {
|
||||
console.log(data, status);
|
||||
updateSelectOptions("#mi_did", data.mi_did_list, data.mi_did);
|
||||
updateSelectOptions("#hardware", data.mi_hardware_list, data.hardware);
|
||||
|
||||
// 初始化联动
|
||||
linkSelects('#mi_did', data.mi_did_list, '#hardware', data.mi_hardware_list);
|
||||
updateCheckbox("#mi_did_hardware", data.mi_did_list, data.mi_did, data.mi_hardware_list);
|
||||
|
||||
// 初始化显示
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key) && data[key] != "") {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
const $element = $("#" + key);
|
||||
if ($element.length) {
|
||||
$element.val(data[key]);
|
||||
if (data[key] === true) {
|
||||
$element.val('true');
|
||||
} else if (data[key] === false) {
|
||||
$element.val('false');
|
||||
} else {
|
||||
$element.val(data[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +103,9 @@ $(function(){
|
||||
data[id] = $(this).val();
|
||||
}
|
||||
});
|
||||
var selectedData = getSelectedDidsAndHardware("#mi_did_hardware");
|
||||
data["mi_did"] = selectedData.did_list;
|
||||
data["hardware"] = selectedData.hardware_list;
|
||||
console.log(data)
|
||||
|
||||
$.ajax({
|
||||
|
||||
@@ -77,3 +77,41 @@ footer {
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
/* 隐藏原生复选框 */
|
||||
.custom-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 修改后的自定义复选框外观 */
|
||||
.checkbox-label {
|
||||
display: inline-block;
|
||||
width: 200px; /* 宽度 */
|
||||
height: 20px; /* 高度 */
|
||||
background-color: #fff; /* 背景颜色 */
|
||||
border: 0px solid #ccc; /* 边框 */
|
||||
border-radius: 3px; /* 圆角边框 */
|
||||
position: relative; /* 设置相对定位 */
|
||||
cursor: pointer; /* 鼠标形状 */
|
||||
padding-left: 40px; /* 给左边的复选框图标留下空位 */
|
||||
}
|
||||
|
||||
/* 对勾的样式 */
|
||||
.custom-checkbox:checked + .checkbox-label::after {
|
||||
content: '✔';
|
||||
position: absolute;
|
||||
left: 10px; /* 对勾图标靠左侧位置 */
|
||||
color: #000; /* 对勾颜色 */
|
||||
font-size: 18px; /* 对勾字体大小,视清晰度需调整 */
|
||||
}
|
||||
|
||||
/* 标签文本样式,使用 ::before 伪元素表示复选框未选中时的样式 */
|
||||
.custom-checkbox + .checkbox-label::before {
|
||||
content: '⬜'; /* 表示未选中时的复选框样式,这里用了白色方块 */
|
||||
position: absolute;
|
||||
left: 8px; /* 方块图标靠左侧位置 */
|
||||
top: 1px; /* 方块图标顶部位置 */
|
||||
color: #000; /* 方块颜色 */
|
||||
font-size: 18px; /* 方块字体大小,视清晰度需调整 */
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from miservice import MiAccount, MiIOService, MiNAService
|
||||
from miservice import MiAccount, MiNAService
|
||||
|
||||
from xiaomusic import (
|
||||
__version__,
|
||||
@@ -28,6 +28,7 @@ from xiaomusic.const import (
|
||||
SUPPORT_MUSIC_TYPE,
|
||||
)
|
||||
from xiaomusic.httpserver import StartHTTPServer
|
||||
from xiaomusic.plugin import PluginManager
|
||||
from xiaomusic.utils import (
|
||||
custom_sort_key,
|
||||
deepcopy_data_no_sensitive_info,
|
||||
@@ -54,12 +55,12 @@ class XiaoMusic:
|
||||
self.last_timestamp = int(time.time() * 1000) # timestamp last call mi speaker
|
||||
self.last_record = None
|
||||
self.cookie_jar = None
|
||||
self.device_id = ""
|
||||
self.mina_service = None
|
||||
self.miio_service = None
|
||||
self.polling_event = asyncio.Event()
|
||||
self.new_record_event = asyncio.Event()
|
||||
self.queue = queue.Queue()
|
||||
self.device2hardware = {}
|
||||
self.did2device = {}
|
||||
|
||||
# 下载对象
|
||||
self.download_proc = None
|
||||
@@ -94,6 +95,9 @@ class XiaoMusic:
|
||||
# 启动时初始化获取声音
|
||||
self.set_last_record("get_volume#")
|
||||
|
||||
# 初始化插件
|
||||
self.plugin_manager = PluginManager(self)
|
||||
|
||||
debug_config = deepcopy_data_no_sensitive_info(self.config)
|
||||
self.log.info(f"Startup OK. {debug_config}")
|
||||
|
||||
@@ -102,6 +106,12 @@ class XiaoMusic:
|
||||
self.conf_path = self.config.conf_path
|
||||
if not self.conf_path:
|
||||
self.conf_path = self.config.music_path
|
||||
self.download_path = self.config.download_path
|
||||
if not self.download_path:
|
||||
self.download_path = self.music_path
|
||||
|
||||
if not os.path.exists(self.download_path):
|
||||
os.makedirs(self.download_path)
|
||||
|
||||
self.hostname = self.config.hostname
|
||||
self.port = self.config.port
|
||||
@@ -141,12 +151,14 @@ class XiaoMusic:
|
||||
|
||||
async def poll_latest_ask(self):
|
||||
async with ClientSession() as session:
|
||||
session._cookie_jar = self.cookie_jar
|
||||
while True:
|
||||
self.log.debug(
|
||||
"Listening new message, timestamp: %s", self.last_timestamp
|
||||
)
|
||||
await self.get_latest_ask_from_xiaoai(session)
|
||||
session._cookie_jar = self.cookie_jar
|
||||
# 拉取所有音箱的对话记录
|
||||
for device_id in self.device2hardware:
|
||||
await self.get_latest_ask_from_xiaoai(session, device_id)
|
||||
start = time.perf_counter()
|
||||
self.log.debug("Polling_event, timestamp: %s", self.last_timestamp)
|
||||
await self.polling_event.wait()
|
||||
@@ -157,7 +169,7 @@ class XiaoMusic:
|
||||
|
||||
async def init_all_data(self, session):
|
||||
await self.login_miboy(session)
|
||||
await self._init_data_hardware()
|
||||
await self.try_update_device_id()
|
||||
cookie_jar = self.get_cookie()
|
||||
if cookie_jar:
|
||||
session.cookie_jar.update_cookies(cookie_jar)
|
||||
@@ -173,61 +185,26 @@ class XiaoMusic:
|
||||
# Forced login to refresh to refresh token
|
||||
await account.login("micoapi")
|
||||
self.mina_service = MiNAService(account)
|
||||
self.miio_service = MiIOService(account)
|
||||
|
||||
async def try_update_device_id(self):
|
||||
# fix multi xiaoai problems we check did first
|
||||
# why we use this way to fix?
|
||||
# some videos and articles already in the Internet
|
||||
# we do not want to change old way, so we check if miotDID in `env` first
|
||||
# to set device id
|
||||
|
||||
try:
|
||||
mi_dids = self.config.mi_did.split(",")
|
||||
hardware_data = await self.mina_service.device_list()
|
||||
self.device2hardware = {}
|
||||
self.did2device = {}
|
||||
for h in hardware_data:
|
||||
if did := self.config.mi_did:
|
||||
if h.get("miotDID", "") == str(did):
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if h.get("hardware", "") == self.config.hardware:
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
self.log.error(
|
||||
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
|
||||
)
|
||||
device = h.get("deviceID", "")
|
||||
hardware = h.get("hardware", "")
|
||||
did = h.get("miotDID", "")
|
||||
if device and hardware and did and (did in mi_dids):
|
||||
self.device2hardware[device] = hardware
|
||||
self.did2device[did] = device
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
async def _init_data_hardware(self):
|
||||
if self.config.cookie:
|
||||
# if use cookie do not need init
|
||||
return
|
||||
await self.try_update_device_id()
|
||||
if not self.config.mi_did:
|
||||
devices = await self.miio_service.device_list()
|
||||
try:
|
||||
self.config.mi_did = next(
|
||||
d["did"]
|
||||
for d in devices
|
||||
if d["model"].endswith(self.config.hardware.lower())
|
||||
)
|
||||
except StopIteration:
|
||||
self.log.error(
|
||||
f"cannot find did for hardware: {self.config.hardware} "
|
||||
"please set it via MI_DID env"
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption init hardware {e}")
|
||||
|
||||
def get_cookie(self):
|
||||
if self.config.cookie:
|
||||
cookie_jar = parse_cookie_string(self.config.cookie)
|
||||
# set attr from cookie fix #134
|
||||
cookie_dict = cookie_jar.get_dict()
|
||||
self.device_id = cookie_dict["deviceId"]
|
||||
return cookie_jar
|
||||
|
||||
if not os.path.exists(self.mi_token_home):
|
||||
@@ -238,12 +215,18 @@ class XiaoMusic:
|
||||
user_data = json.loads(f.read())
|
||||
user_id = user_data.get("userId")
|
||||
service_token = user_data.get("micoapi")[1]
|
||||
device_id = self.get_one_device()
|
||||
cookie_string = COOKIE_TEMPLATE.format(
|
||||
device_id=self.device_id, service_token=service_token, user_id=user_id
|
||||
device_id=device_id, service_token=service_token, user_id=user_id
|
||||
)
|
||||
return parse_cookie_string(cookie_string)
|
||||
|
||||
async def get_latest_ask_from_xiaoai(self, session):
|
||||
def get_one_device(self):
|
||||
device_id = next(iter(self.device2hardware), "")
|
||||
return device_id
|
||||
|
||||
async def get_latest_ask_from_xiaoai(self, session, device_id):
|
||||
cookies = {"deviceId": device_id}
|
||||
retries = 3
|
||||
for i in range(retries):
|
||||
try:
|
||||
@@ -253,7 +236,7 @@ class XiaoMusic:
|
||||
timestamp=str(int(time.time() * 1000)),
|
||||
)
|
||||
self.log.debug(f"url:{url}")
|
||||
r = await session.get(url, timeout=timeout)
|
||||
r = await session.get(url, timeout=timeout, cookies=cookies)
|
||||
except Exception as e:
|
||||
self.log.warning(
|
||||
"Execption when get latest ask from xiaoai: %s", str(e)
|
||||
@@ -298,16 +281,21 @@ class XiaoMusic:
|
||||
return
|
||||
|
||||
await self.force_stop_xiaoai()
|
||||
try:
|
||||
await self.mina_service.text_to_speech(self.device_id, value)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
await self.text_to_speech(value)
|
||||
|
||||
# 最大等8秒
|
||||
sec = min(8, int(len(value) / 3))
|
||||
await asyncio.sleep(sec)
|
||||
self.log.info(f"do_tts ok. cur_music:{self.cur_music}")
|
||||
await self.check_replay()
|
||||
|
||||
async def text_to_speech(self, value):
|
||||
try:
|
||||
for device_id in self.device2hardware:
|
||||
await self.mina_service.text_to_speech(device_id, value)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
# 继续播放被打断的歌曲
|
||||
async def check_replay(self):
|
||||
if self.isplaying() and not self.isdownloading():
|
||||
@@ -323,13 +311,17 @@ class XiaoMusic:
|
||||
value = int(value)
|
||||
self._volume = value
|
||||
self.log.info(f"声音设置为{value}")
|
||||
await self.player_set_volume(value)
|
||||
|
||||
async def player_set_volume(self, value):
|
||||
try:
|
||||
await self.mina_service.player_set_volume(self.device_id, value)
|
||||
for device_id in self.device2hardware:
|
||||
await self.mina_service.player_set_volume(device_id, value)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
async def get_if_xiaoai_is_playing(self):
|
||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
||||
async def get_if_xiaoai_is_playing(self, device_id):
|
||||
playing_info = await self.mina_service.player_get_status(device_id)
|
||||
self.log.info(playing_info)
|
||||
# WTF xiaomi api
|
||||
is_playing = (
|
||||
@@ -338,17 +330,25 @@ class XiaoMusic:
|
||||
)
|
||||
return is_playing
|
||||
|
||||
async def stop_if_xiaoai_is_playing(self):
|
||||
is_playing = await self.get_if_xiaoai_is_playing()
|
||||
async def stop_if_xiaoai_is_playing(self, device_id):
|
||||
is_playing = await self.get_if_xiaoai_is_playing(device_id)
|
||||
if is_playing:
|
||||
# stop it
|
||||
ret = await self.mina_service.player_stop(self.device_id)
|
||||
self.log.info(f"force_stop_xiaoai player_stop ret:{ret}")
|
||||
ret = await self.mina_service.player_stop(device_id)
|
||||
self.log.info(
|
||||
f"force_stop_xiaoai player_stop device_id:{device_id} ret:{ret}"
|
||||
)
|
||||
|
||||
async def force_stop_xiaoai(self):
|
||||
ret = await self.mina_service.player_pause(self.device_id)
|
||||
self.log.info(f"force_stop_xiaoai player_pause ret:{ret}")
|
||||
await self.stop_if_xiaoai_is_playing()
|
||||
try:
|
||||
for device_id in self.device2hardware:
|
||||
ret = await self.mina_service.player_pause(device_id)
|
||||
self.log.info(
|
||||
f"force_stop_xiaoai player_pause device_id:{device_id} ret:{ret}"
|
||||
)
|
||||
await self.stop_if_xiaoai_is_playing(device_id)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
# 是否在下载中
|
||||
def isdownloading(self):
|
||||
@@ -379,7 +379,7 @@ class XiaoMusic:
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"--paths",
|
||||
self.music_path,
|
||||
self.download_path,
|
||||
"-o",
|
||||
f"{name}.mp3",
|
||||
"--ffmpeg-location",
|
||||
@@ -557,7 +557,7 @@ class XiaoMusic:
|
||||
|
||||
# 把下载的音乐加入播放列表
|
||||
def add_download_music(self, name):
|
||||
self._all_music[name] = os.path.join(self.music_path, f"{name}.mp3")
|
||||
self._all_music[name] = os.path.join(self.download_path, f"{name}.mp3")
|
||||
if name not in self._play_list:
|
||||
self._play_list.append(name)
|
||||
self.log.debug("add_music %s", name)
|
||||
@@ -640,13 +640,13 @@ class XiaoMusic:
|
||||
self.log.info("收到消息:%s 控制面板:%s", query, ctrl_panel)
|
||||
|
||||
# 匹配命令
|
||||
opvalue, oparg = self.match_cmd(query, ctrl_panel)
|
||||
if not opvalue:
|
||||
await asyncio.sleep(1)
|
||||
await self.check_replay()
|
||||
continue
|
||||
|
||||
try:
|
||||
opvalue, oparg = self.match_cmd(query, ctrl_panel)
|
||||
if not opvalue:
|
||||
await asyncio.sleep(1)
|
||||
await self.check_replay()
|
||||
continue
|
||||
|
||||
func = getattr(self, opvalue)
|
||||
await func(arg1=oparg)
|
||||
except Exception as e:
|
||||
@@ -670,6 +670,10 @@ class XiaoMusic:
|
||||
opvalue = self.check_full_match_cmd(query, ctrl_panel)
|
||||
if opvalue:
|
||||
self.log.info(f"完全匹配指令. query:{query} opvalue:{opvalue}")
|
||||
# 自定义口令
|
||||
if opvalue.startswith("exec#"):
|
||||
code = opvalue.split("#", 1)[1]
|
||||
return ("exec", code)
|
||||
return (opvalue, "")
|
||||
|
||||
for opkey in self.config.key_match_order:
|
||||
@@ -714,17 +718,23 @@ class XiaoMusic:
|
||||
|
||||
async def play_url(self, **kwargs):
|
||||
url = kwargs.get("arg1", "")
|
||||
if self.config.use_music_api:
|
||||
ret = await self.play_by_music_url(self.device_id, url)
|
||||
self.log.info(
|
||||
f"play_url play_by_music_url {self.config.hardware}. ret:{ret} url:{url}"
|
||||
)
|
||||
else:
|
||||
ret = await self.mina_service.play_by_url(self.device_id, url)
|
||||
self.log.info(
|
||||
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
|
||||
)
|
||||
return ret
|
||||
await self.all_player_play(url)
|
||||
|
||||
async def all_player_play(self, url):
|
||||
try:
|
||||
for device_id in self.device2hardware:
|
||||
if self.config.use_music_api:
|
||||
ret = await self.play_by_music_url(device_id, url)
|
||||
self.log.info(
|
||||
f"player_play play_by_music_url device_id:{device_id} ret:{ret} url:{url}"
|
||||
)
|
||||
else:
|
||||
ret = await self.mina_service.play_by_url(device_id, url)
|
||||
self.log.info(
|
||||
f"player_play play_by_url device_id:{device_id} ret:{ret} url:{url}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
def find_real_music_name(self, name):
|
||||
if not self.config.enable_fuzzy_match:
|
||||
@@ -919,7 +929,9 @@ class XiaoMusic:
|
||||
await self.do_set_volume(value)
|
||||
|
||||
async def get_volume(self, **kwargs):
|
||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
||||
# 取一个音箱的声音
|
||||
device_id = self.get_one_device()
|
||||
playing_info = await self.mina_service.player_get_status(device_id)
|
||||
self.log.debug("get_volume. playing_info:%s", playing_info)
|
||||
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
|
||||
"volume", 0
|
||||
@@ -1089,10 +1101,15 @@ class XiaoMusic:
|
||||
if arg1 is None:
|
||||
arg1 = {}
|
||||
data = arg1
|
||||
self.log.info(f"debug_play_by_music_url: {data}")
|
||||
device_id = self.get_one_device()
|
||||
self.log.info(f"debug_play_by_music_url: {data} {device_id}")
|
||||
return await self.mina_service.ubus_request(
|
||||
self.device_id,
|
||||
device_id,
|
||||
"player_play_music",
|
||||
"mediaplayer",
|
||||
data,
|
||||
)
|
||||
|
||||
async def exec(self, arg1=None):
|
||||
code = arg1 if arg1 else 'code1("hello")'
|
||||
await self.plugin_manager.execute_plugin(code)
|
||||
|
||||
Reference in New Issue
Block a user