mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-15 16:18:14 +08:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c45d93fea | ||
|
|
f19a7e1080 | ||
|
|
5e0ae07978 | ||
|
|
116a05ce4b | ||
|
|
d6fdee5905 | ||
|
|
af25300917 | ||
|
|
f18b2f49bf | ||
|
|
db1e4e6fc4 | ||
|
|
dc49f63a37 | ||
|
|
6837841872 | ||
|
|
637347ae0c | ||
|
|
ff968c4db4 | ||
|
|
ca547e0d81 | ||
|
|
f0931c447b | ||
|
|
d098b5eb60 | ||
|
|
09111e849d | ||
|
|
49a76dee60 | ||
|
|
637672473e | ||
|
|
e4e1d13b69 | ||
|
|
7736f8c5b4 | ||
|
|
29db69b52f | ||
|
|
37fe51771b | ||
|
|
77ece713dc | ||
|
|
4436cc3a15 | ||
|
|
c875350112 | ||
|
|
d6df2f6bfe | ||
|
|
03e3312218 | ||
|
|
5d7451c3f2 | ||
|
|
551dfa0c7f | ||
|
|
3cc35e8f97 | ||
|
|
01c68ba64e | ||
|
|
61d167d347 |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github: [hanxi]
|
||||||
|
custom: ['https://afdian.net/a/imhanxi']
|
||||||
56
README.md
56
README.md
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
|
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
|
||||||
|
|
||||||
|
<https://github.com/hanxi/xiaomusic>
|
||||||
|
|
||||||
## 最简配置运行
|
## 最简配置运行
|
||||||
|
|
||||||
已经支持在 web 页面配置其他参数,docker compose 配置如下:
|
已经支持在 web 页面配置其他参数,docker compose 配置如下:
|
||||||
@@ -28,8 +30,22 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
MI_USER: '小米账号'
|
MI_USER: '小米账号'
|
||||||
MI_PASS: '小米密码'
|
MI_PASS: '小米密码'
|
||||||
|
XIAOMUSIC_VERBOSE: 'true'
|
||||||
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
|
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
对应的 docker 启动命令如下:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
docker run -e MI_USER='小米账号' \
|
||||||
|
-e MI_PASS='小米密码' \
|
||||||
|
-e XIAOMUSIC_VERBOSE='true' \
|
||||||
|
-e XIAOMUSIC_HOSTNAME='docker 主机 ip' \
|
||||||
|
-p 8090:8090 \
|
||||||
|
-v ./music:/app/music \
|
||||||
|
hanxi/xiaomusic
|
||||||
|
```
|
||||||
|
|
||||||
启动成功后,在 web 页面可以配置 MI_DID, MI_HARDWARE, XIAOMUSIC_SEARCH, XIAOMUSIC_PROXY 参数。
|
启动成功后,在 web 页面可以配置 MI_DID, MI_HARDWARE, XIAOMUSIC_SEARCH, XIAOMUSIC_PROXY 参数。
|
||||||
|
|
||||||
如果需要修改 8090 端口为其他端口,比如 5678,需要这样配,3个数字都需要是 5678
|
如果需要修改 8090 端口为其他端口,比如 5678,需要这样配,3个数字都需要是 5678
|
||||||
@@ -46,10 +62,13 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
MI_USER: '小米账号'
|
MI_USER: '小米账号'
|
||||||
MI_PASS: '小米密码'
|
MI_PASS: '小米密码'
|
||||||
|
XIAOMUSIC_VERBOSE: 'true'
|
||||||
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
|
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
|
||||||
XIAOMUSIC_PORT: 5678
|
XIAOMUSIC_PORT: 5678
|
||||||
```
|
```
|
||||||
|
|
||||||
|
其中 XIAOMUSIC_VERBOSE 设置为 'true' 时表示开启 debug 日志,遇到问题可以去 web 设置页面底部【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
|
||||||
|
|
||||||
## 开发环境运行
|
## 开发环境运行
|
||||||
|
|
||||||
- 使用 install_dependencies.sh 下载依赖
|
- 使用 install_dependencies.sh 下载依赖
|
||||||
@@ -87,26 +106,34 @@ pdm run xiaomusic.py
|
|||||||
## 已测试支持的设备
|
## 已测试支持的设备
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
|
- L06A
|
||||||
- L07A
|
- L07A
|
||||||
- S12
|
- S12
|
||||||
|
- S12A
|
||||||
- LX5A
|
- LX5A
|
||||||
- LX05
|
- LX05
|
||||||
- L16A
|
- L16A
|
||||||
|
- L17A
|
||||||
- LX06
|
- LX06
|
||||||
- LX01
|
- LX01
|
||||||
|
- L05B
|
||||||
|
- L05C
|
||||||
````
|
````
|
||||||
|
|
||||||
|
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
|
||||||
|
|
||||||
|
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
|
||||||
|
|
||||||
## 支持音乐格式
|
## 支持音乐格式
|
||||||
|
|
||||||
- mp3
|
- mp3
|
||||||
- flac
|
- flac
|
||||||
- wav
|
- wav
|
||||||
- ape
|
- ape
|
||||||
|
- ogg
|
||||||
|
|
||||||
> 本地音乐会搜索 mp3 和 flac 格式的文件,下载的歌曲是 mp3 格式的。
|
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
|
||||||
|
> 已知 L05B L05C 不支持 flac 格式。
|
||||||
## 其他参数
|
|
||||||
|
|
||||||
- XIAOMUSIC_ACTIVE_CMD 环境变量,配置成'play,random_play',在非播放状态下,只有这两个指令(播放歌曲和随机播放)可以触发,触发后,xiaomusic进入playing状态,其他指令则可以正常触发。
|
|
||||||
|
|
||||||
## 在 Docker 里使用
|
## 在 Docker 里使用
|
||||||
|
|
||||||
@@ -197,7 +224,7 @@ services:
|
|||||||
XIAOMUSIC_HOSTNAME: '192.168.2.5'
|
XIAOMUSIC_HOSTNAME: '192.168.2.5'
|
||||||
```
|
```
|
||||||
|
|
||||||
setting.json 文件不存到 music 可以这样写,会把 setting.json 文件放到容器的 /app/conf 目录且映射到本地的 ./conf 目录:
|
如果想让 setting.json 文件不存储到 music 目录,可以这样配,下面的示例会把 setting.json 文件放到容器的 /app/conf 目录且映射到本地的 ./conf 目录:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -224,11 +251,14 @@ services:
|
|||||||
|
|
||||||
- ip 是 XIAOMUSIC_HOSTNAME 设置的
|
- ip 是 XIAOMUSIC_HOSTNAME 设置的
|
||||||
- 8090 是默认端口
|
- 8090 是默认端口
|
||||||
- 新功能
|
- 支持功能
|
||||||
- 显示正在播放的歌曲
|
- 显示正在播放的歌曲
|
||||||
- 模糊搜索本地歌曲
|
- 模糊搜索本地歌曲
|
||||||
|
- 播放列表
|
||||||
|
- 删除歌曲
|
||||||
- 设置页面
|
- 设置页面
|
||||||
|
- 配置网络歌单
|
||||||
|
- 日志文件下载
|
||||||
|
|
||||||
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
|
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
|
||||||
- MI_USER
|
- MI_USER
|
||||||
@@ -243,29 +273,33 @@ services:
|
|||||||
|
|
||||||
## 网络歌单功能
|
## 网络歌单功能
|
||||||
|
|
||||||
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
|
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
|
||||||
|
|
||||||
|
> 欢迎有想法的朋友们制作更多的歌单转换工具。
|
||||||
|
|
||||||
## 更多其他可选配置
|
## 更多其他可选配置
|
||||||
|
|
||||||
- XIAOMUSIC_ACTIVE_CMD 配置唤醒命令,具体见 <https://github.com/hanxi/xiaomusic/pull/43>
|
- XIAOMUSIC_ACTIVE_CMD 环境变量,用于唤醒口令,配置成'play,random_play',在非播放状态下,只有这两个指令(播放歌曲和随机播放)可以触发,触发后,xiaomusic进入playing状态,其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
|
||||||
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录
|
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录
|
||||||
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,具体见 <https://github.com/hanxi/xiaomusic/issues/76>
|
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,具体见 <https://github.com/hanxi/xiaomusic/issues/76>
|
||||||
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台,具体见 <https://github.com/hanxi/xiaomusic/issues/47>
|
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台,具体见 <https://github.com/hanxi/xiaomusic/issues/47>
|
||||||
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户
|
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户
|
||||||
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码
|
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码
|
||||||
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,记得把目录映射到主机,默认情况会把配置存放在music目录,具体见 <https://github.com/hanxi/xiaomusic/issues/74>
|
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,记得把目录映射到主机,默认情况会把配置存放在music目录,具体见 <https://github.com/hanxi/xiaomusic/issues/74>
|
||||||
- XIAOMUSIC_VERBOSE 设置为 true 时开启 debug 日志,用于排查问题
|
|
||||||
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82>
|
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82>
|
||||||
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号
|
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号
|
||||||
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
|
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
|
||||||
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
|
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
|
||||||
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
|
- 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>
|
||||||
|
|
||||||
## 讨论区
|
## 讨论区
|
||||||
|
|
||||||
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
|
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
|
||||||
- [点击链接加入群聊【xiaomusic】 604526973](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=13St5PLVcTxYlWTAs_iAawazjtdD1l-a&authKey=dJWEpaT2fDBDpdUUOWj%2FLt6NS1ePBfShDfz7a6seNURi05VvVnAGQzXF%2FM%2F5HgIm&noverify=0&group_code=604526973)
|
- [点击链接加入群聊【xiaomusic】 604526973](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=13St5PLVcTxYlWTAs_iAawazjtdD1l-a&authKey=dJWEpaT2fDBDpdUUOWj%2FLt6NS1ePBfShDfz7a6seNURi05VvVnAGQzXF%2FM%2F5HgIm&noverify=0&group_code=604526973)
|
||||||
- <https://github.com/hanxi/xiaomusic/issues>
|
- <https://github.com/hanxi/xiaomusic/issues>
|
||||||
|
- [微信群二维码](https://github.com/hanxi/xiaomusic/issues/86)
|
||||||
|
|
||||||
## 感谢
|
## 感谢
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "xiaomusic"
|
name = "xiaomusic"
|
||||||
version = "0.1.72"
|
version = "0.1.79"
|
||||||
description = "Play Music with xiaomi AI speaker"
|
description = "Play Music with xiaomi AI speaker"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.1.72"
|
__version__ = "0.1.79"
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ class Config:
|
|||||||
"XIAOMUSIC_SEARCH", "ytsearch:"
|
"XIAOMUSIC_SEARCH", "ytsearch:"
|
||||||
) # "bilisearch:" or "ytsearch:"
|
) # "bilisearch:" or "ytsearch:"
|
||||||
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
|
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
|
||||||
active_cmd: str = os.getenv("XIAOMUSIC_ACTIVE_CMD", "play,random_play,playlocal")
|
active_cmd: str = os.getenv(
|
||||||
|
"XIAOMUSIC_ACTIVE_CMD", "play,random_play,playlocal,play_music_list,stop"
|
||||||
|
)
|
||||||
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir")
|
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir")
|
||||||
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
|
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
|
||||||
disable_httpauth: bool = (
|
disable_httpauth: bool = (
|
||||||
@@ -79,6 +81,13 @@ class Config:
|
|||||||
use_music_api: bool = (
|
use_music_api: bool = (
|
||||||
os.getenv("XIAOMUSIC_USE_MUSIC_API", "false").lower() == "true"
|
os.getenv("XIAOMUSIC_USE_MUSIC_API", "false").lower() == "true"
|
||||||
)
|
)
|
||||||
|
log_file: str = os.getenv("XIAOMUSIC_MUSIC_LOG_FILE", "/tmp/xiaomusic.txt")
|
||||||
|
# 模糊搜索匹配的最低相似度阈值
|
||||||
|
fuzzy_match_cutoff: float = float(os.getenv("XIAOMUSIC_FUZZY_MATCH_CUTOFF", "0.6"))
|
||||||
|
# 开启模糊搜索
|
||||||
|
enable_fuzzy_match: bool = (
|
||||||
|
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
def append_keyword(self, keys, action):
|
def append_keyword(self, keys, action):
|
||||||
for key in keys.split(","):
|
for key in keys.split(","):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ SUPPORT_MUSIC_TYPE = [
|
|||||||
".flac",
|
".flac",
|
||||||
".wav",
|
".wav",
|
||||||
".ape",
|
".ape",
|
||||||
|
".ogg",
|
||||||
]
|
]
|
||||||
|
|
||||||
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={timestamp}&limit=2"
|
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={timestamp}&limit=2"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from flask import Flask, request, send_from_directory
|
from flask import Flask, request, send_file, send_from_directory
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
from waitress import serve
|
from waitress import serve
|
||||||
|
|
||||||
@@ -163,6 +163,20 @@ def downloadjson():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/downloadlog", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def downloadlog():
|
||||||
|
return send_file(xiaomusic.config.log_file, as_attachment=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/playurl", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
async def playurl():
|
||||||
|
url = request.args.get("url")
|
||||||
|
log.info(f"play_url:{url}")
|
||||||
|
return await xiaomusic.call_main_thread_function(xiaomusic.play_url, arg1=url)
|
||||||
|
|
||||||
|
|
||||||
def static_path_handler(filename):
|
def static_path_handler(filename):
|
||||||
log.debug(filename)
|
log.debug(filename)
|
||||||
log.debug(static_path)
|
log.debug(static_path)
|
||||||
|
|||||||
@@ -82,6 +82,13 @@ $(function(){
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#playurl").on("click", () => {
|
||||||
|
var url = $("#music-url").val();
|
||||||
|
$.get(`/playurl?url=${url}`, function(data, status) {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function append_op_button_name(name) {
|
function append_op_button_name(name) {
|
||||||
append_op_button(name, name);
|
append_op_button(name, name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,12 @@
|
|||||||
<button id="play_music_list">播放列表歌曲</button>
|
<button id="play_music_list">播放列表歌曲</button>
|
||||||
<button id="del_music">删除选中歌曲</button>
|
<button id="del_music">删除选中歌曲</button>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<input id="music-url" type="text" placeholder="链接(http://ngcdn001.cnr.cn/live/zgzs/index.m3u8)"></input>
|
||||||
|
</div>
|
||||||
|
<button id="playurl">播放链接</button>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<button onclick="location.href='/';">返回首页</button>
|
<button onclick="location.href='/';">返回首页</button>
|
||||||
<button id="get_music_list">获取歌单</button>
|
<button id="get_music_list">获取歌单</button>
|
||||||
<button id="save">保存</button>
|
<button id="save">保存</button>
|
||||||
|
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>
|
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
button {
|
.button {
|
||||||
|
line-height: 50px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
button, .button {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@@ -10,7 +14,7 @@ button {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: #008CBA;
|
background-color: #008CBA;
|
||||||
}
|
}
|
||||||
button:active {
|
button:active, .button:active {
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
background-color: #007CBA;
|
background-color: #007CBA;
|
||||||
transform: translateY(2px);
|
transform: translateY(2px);
|
||||||
|
|||||||
@@ -72,7 +72,12 @@ def validate_proxy(proxy_str: str) -> bool:
|
|||||||
|
|
||||||
# 模糊搜索
|
# 模糊搜索
|
||||||
def fuzzyfinder(user_input, collection):
|
def fuzzyfinder(user_input, collection):
|
||||||
return difflib.get_close_matches(user_input, collection, 10, cutoff=0.1)
|
return difflib.get_close_matches(user_input, collection, n=10, cutoff=0.1)
|
||||||
|
|
||||||
|
|
||||||
|
def find_best_match(user_input, collection, cutoff=0.6):
|
||||||
|
matches = difflib.get_close_matches(user_input, collection, n=1, cutoff=cutoff)
|
||||||
|
return matches[0] if matches else None
|
||||||
|
|
||||||
|
|
||||||
# 歌曲排序
|
# 歌曲排序
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -9,6 +10,7 @@ import re
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from aiohttp import ClientSession, ClientTimeout
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
@@ -29,6 +31,7 @@ from xiaomusic.const import (
|
|||||||
from xiaomusic.httpserver import StartHTTPServer
|
from xiaomusic.httpserver import StartHTTPServer
|
||||||
from xiaomusic.utils import (
|
from xiaomusic.utils import (
|
||||||
custom_sort_key,
|
custom_sort_key,
|
||||||
|
find_best_match,
|
||||||
fuzzyfinder,
|
fuzzyfinder,
|
||||||
get_local_music_duration,
|
get_local_music_duration,
|
||||||
get_random,
|
get_random,
|
||||||
@@ -91,14 +94,8 @@ class XiaoMusic:
|
|||||||
# 关机定时器
|
# 关机定时器
|
||||||
self._stop_timer = None
|
self._stop_timer = None
|
||||||
|
|
||||||
# setup logger
|
# 初始化日志
|
||||||
logging.basicConfig(
|
self.setup_logger()
|
||||||
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
|
|
||||||
datefmt="[%X]",
|
|
||||||
)
|
|
||||||
self.log = logging.getLogger("xiaomusic")
|
|
||||||
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
|
|
||||||
self.log.debug(config)
|
|
||||||
|
|
||||||
# 尝试从设置里加载配置
|
# 尝试从设置里加载配置
|
||||||
self.try_init_setting()
|
self.try_init_setting()
|
||||||
@@ -109,7 +106,30 @@ class XiaoMusic:
|
|||||||
# 启动时初始化获取声音
|
# 启动时初始化获取声音
|
||||||
self.set_last_record("get_volume#")
|
self.set_last_record("get_volume#")
|
||||||
|
|
||||||
self.log.info("ffmpeg_location: %s", self.ffmpeg_location)
|
def setup_logger(self):
|
||||||
|
logging.basicConfig(
|
||||||
|
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
|
||||||
|
datefmt="[%X]",
|
||||||
|
)
|
||||||
|
|
||||||
|
log_file = self.config.log_file
|
||||||
|
log_path = os.path.dirname(log_file)
|
||||||
|
if not os.path.exists(log_path):
|
||||||
|
os.makedirs(log_path)
|
||||||
|
if os.path.exists(log_file):
|
||||||
|
os.remove(log_file)
|
||||||
|
handler = RotatingFileHandler(
|
||||||
|
self.config.log_file, maxBytes=10 * 1024 * 1024, backupCount=1
|
||||||
|
)
|
||||||
|
self.log = logging.getLogger("xiaomusic")
|
||||||
|
self.log.addHandler(handler)
|
||||||
|
self.log.setLevel(logging.DEBUG if self.config.verbose else logging.INFO)
|
||||||
|
debug_config = copy.deepcopy(self.config)
|
||||||
|
debug_config.account = "******"
|
||||||
|
debug_config.password = "******"
|
||||||
|
debug_config.httpauth_username = "******"
|
||||||
|
debug_config.httpauth_password = "******"
|
||||||
|
self.log.info(debug_config)
|
||||||
|
|
||||||
async def poll_latest_ask(self):
|
async def poll_latest_ask(self):
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
@@ -270,10 +290,13 @@ class XiaoMusic:
|
|||||||
await self.mina_service.text_to_speech(self.device_id, value)
|
await self.mina_service.text_to_speech(self.device_id, value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Execption {e}")
|
self.log.error(f"Execption {e}")
|
||||||
|
# 最大等8秒
|
||||||
|
sec = min(8, int(len(value) / 3.3))
|
||||||
|
await asyncio.sleep(sec)
|
||||||
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
|
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
|
||||||
if self._playing and not self.is_downloading():
|
if self._playing and not self.is_downloading():
|
||||||
# 继续播放歌曲
|
# 继续播放歌曲
|
||||||
|
self.log.info("继续播放歌曲")
|
||||||
await self.play()
|
await self.play()
|
||||||
|
|
||||||
async def do_set_volume(self, value):
|
async def do_set_volume(self, value):
|
||||||
@@ -286,7 +309,10 @@ class XiaoMusic:
|
|||||||
self.log.error(f"Execption {e}")
|
self.log.error(f"Execption {e}")
|
||||||
|
|
||||||
async def force_stop_xiaoai(self):
|
async def force_stop_xiaoai(self):
|
||||||
await self.mina_service.player_stop(self.device_id)
|
ret = await self.mina_service.player_pause(self.device_id)
|
||||||
|
self.log.debug(f"force_stop_xiaoai player_pause ret:{ret}")
|
||||||
|
#ret = await self.mina_service.player_stop(self.device_id)
|
||||||
|
#self.log.debug(f"force_stop_xiaoai player_stop ret:{ret}")
|
||||||
|
|
||||||
# 是否在下载中
|
# 是否在下载中
|
||||||
def is_downloading(self):
|
def is_downloading(self):
|
||||||
@@ -570,7 +596,7 @@ class XiaoMusic:
|
|||||||
self.polling_event.clear() # stop polling when processing the question
|
self.polling_event.clear() # stop polling when processing the question
|
||||||
query = new_record.get("query", "").strip()
|
query = new_record.get("query", "").strip()
|
||||||
ctrl_panel = new_record.get("ctrl_panel", False)
|
ctrl_panel = new_record.get("ctrl_panel", False)
|
||||||
self.log.debug("收到消息:%s 控制面板:%s", query, ctrl_panel)
|
self.log.info("收到消息:%s 控制面板:%s", query, ctrl_panel)
|
||||||
|
|
||||||
# 匹配命令
|
# 匹配命令
|
||||||
opvalue, oparg = self.match_cmd(query, ctrl_panel)
|
opvalue, oparg = self.match_cmd(query, ctrl_panel)
|
||||||
@@ -616,7 +642,7 @@ class XiaoMusic:
|
|||||||
return (opvalue, oparg)
|
return (opvalue, oparg)
|
||||||
if self._playing:
|
if self._playing:
|
||||||
self.log.info("未匹配到指令,自动停止")
|
self.log.info("未匹配到指令,自动停止")
|
||||||
return ("stop", {})
|
return ("stop", "notts")
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
# 判断是否播放下一首歌曲
|
# 判断是否播放下一首歌曲
|
||||||
@@ -650,7 +676,8 @@ class XiaoMusic:
|
|||||||
{"startaudioid": audio_id, "music": json.dumps(music)},
|
{"startaudioid": audio_id, "music": json.dumps(music)},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def play_url(self, url):
|
async def play_url(self, **kwargs):
|
||||||
|
url = kwargs.get("arg1", "")
|
||||||
if self.config.use_music_api:
|
if self.config.use_music_api:
|
||||||
ret = await self._play_by_music_url(self.device_id, url)
|
ret = await self._play_by_music_url(self.device_id, url)
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
@@ -661,6 +688,21 @@ class XiaoMusic:
|
|||||||
self.log.debug(
|
self.log.debug(
|
||||||
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
|
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
|
||||||
)
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def find_real_music_name(self, name):
|
||||||
|
if not self.config.enable_fuzzy_match:
|
||||||
|
self.log.debug("没开启模糊匹配")
|
||||||
|
return name
|
||||||
|
|
||||||
|
all_music_list = list(self._all_music.keys())
|
||||||
|
real_name = find_best_match(
|
||||||
|
name, all_music_list, cutoff=self.config.fuzzy_match_cutoff
|
||||||
|
)
|
||||||
|
if real_name:
|
||||||
|
self.log.info(f"根据【{name}】找到歌曲【{real_name}】")
|
||||||
|
return real_name
|
||||||
|
self.log.info(f"没找到歌曲【{name}】")
|
||||||
|
|
||||||
# 播放本地歌曲
|
# 播放本地歌曲
|
||||||
async def playlocal(self, **kwargs):
|
async def playlocal(self, **kwargs):
|
||||||
@@ -675,6 +717,7 @@ class XiaoMusic:
|
|||||||
self.log.info(f"playlocal. name:{name}")
|
self.log.info(f"playlocal. name:{name}")
|
||||||
|
|
||||||
# 本地歌曲不存在时下载
|
# 本地歌曲不存在时下载
|
||||||
|
name = self.find_real_music_name(name)
|
||||||
if not self.is_music_exist(name):
|
if not self.is_music_exist(name):
|
||||||
await self.do_tts(f"本地不存在歌曲{name}")
|
await self.do_tts(f"本地不存在歌曲{name}")
|
||||||
return
|
return
|
||||||
@@ -687,7 +730,7 @@ class XiaoMusic:
|
|||||||
sec, url = await self.get_music_sec_url(name)
|
sec, url = await self.get_music_sec_url(name)
|
||||||
self.log.info(f"播放 {url}")
|
self.log.info(f"播放 {url}")
|
||||||
await self.force_stop_xiaoai()
|
await self.force_stop_xiaoai()
|
||||||
await self.play_url(url)
|
await self.play_url(arg1=url)
|
||||||
self.log.info("已经开始播放了")
|
self.log.info("已经开始播放了")
|
||||||
# 设置下一首歌曲的播放定时器
|
# 设置下一首歌曲的播放定时器
|
||||||
await self.set_next_music_timeout(sec)
|
await self.set_next_music_timeout(sec)
|
||||||
@@ -710,6 +753,7 @@ class XiaoMusic:
|
|||||||
self.log.info("play. search_key:%s name:%s", search_key, name)
|
self.log.info("play. search_key:%s name:%s", search_key, name)
|
||||||
|
|
||||||
# 本地歌曲不存在时下载
|
# 本地歌曲不存在时下载
|
||||||
|
name = self.find_real_music_name(name)
|
||||||
if not self.is_music_exist(name):
|
if not self.is_music_exist(name):
|
||||||
if self.config.disable_download:
|
if self.config.disable_download:
|
||||||
await self.do_tts(f"本地不存在歌曲{name}")
|
await self.do_tts(f"本地不存在歌曲{name}")
|
||||||
@@ -771,10 +815,27 @@ class XiaoMusic:
|
|||||||
self.log.error(f"del ${filename} failed")
|
self.log.error(f"del ${filename} failed")
|
||||||
self._gen_all_music_list()
|
self._gen_all_music_list()
|
||||||
|
|
||||||
|
def find_real_music_list_name(self, list_name):
|
||||||
|
if not self.config.enable_fuzzy_match:
|
||||||
|
self.log.debug("没开启模糊匹配")
|
||||||
|
return list_name
|
||||||
|
|
||||||
|
# 模糊搜一个播放列表
|
||||||
|
real_name = find_best_match(
|
||||||
|
list_name, self._music_list, cutoff=self.config.fuzzy_match_cutoff
|
||||||
|
)
|
||||||
|
if real_name:
|
||||||
|
self.log.info(f"根据【{list_name}】找到播放列表【{real_name}】")
|
||||||
|
list_name = real_name
|
||||||
|
self.log.info(f"没找到播放列表【{list_name}】")
|
||||||
|
return list_name
|
||||||
|
|
||||||
# 播放一个播放列表
|
# 播放一个播放列表
|
||||||
async def play_music_list(self, **kwargs):
|
async def play_music_list(self, **kwargs):
|
||||||
parts = kwargs.get("arg1").split("|")
|
parts = kwargs.get("arg1").split("|")
|
||||||
list_name = parts[0]
|
list_name = parts[0]
|
||||||
|
|
||||||
|
list_name = self.find_real_music_list_name(list_name)
|
||||||
if list_name not in self._music_list:
|
if list_name not in self._music_list:
|
||||||
await self.do_tts(f"播放列表{list_name}不存在")
|
await self.do_tts(f"播放列表{list_name}不存在")
|
||||||
return
|
return
|
||||||
@@ -792,6 +853,8 @@ class XiaoMusic:
|
|||||||
|
|
||||||
async def stop(self, **kwargs):
|
async def stop(self, **kwargs):
|
||||||
self._playing = False
|
self._playing = False
|
||||||
|
if kwargs.get("arg1", "") != "notts":
|
||||||
|
await self.do_tts("收到关机口令,再见")
|
||||||
if self._next_timer:
|
if self._next_timer:
|
||||||
self._next_timer.cancel()
|
self._next_timer.cancel()
|
||||||
self.log.info("定时器已取消")
|
self.log.info("定时器已取消")
|
||||||
@@ -806,12 +869,12 @@ class XiaoMusic:
|
|||||||
async def _do_stop():
|
async def _do_stop():
|
||||||
await asyncio.sleep(minute * 60)
|
await asyncio.sleep(minute * 60)
|
||||||
try:
|
try:
|
||||||
await self.stop()
|
await self.stop(arg1="notts")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
|
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
self._stop_timer = asyncio.ensure_future(_do_stop())
|
self._stop_timer = asyncio.ensure_future(_do_stop())
|
||||||
self.log.info(f"{minute}分钟后将关机")
|
await self.do_tts(f"收到,{minute}分钟后将关机")
|
||||||
|
|
||||||
async def set_volume(self, **kwargs):
|
async def set_volume(self, **kwargs):
|
||||||
value = kwargs.get("arg1", 0)
|
value = kwargs.get("arg1", 0)
|
||||||
@@ -821,7 +884,7 @@ class XiaoMusic:
|
|||||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
playing_info = await self.mina_service.player_get_status(self.device_id)
|
||||||
self.log.debug("get_volume. playing_info:%s", playing_info)
|
self.log.debug("get_volume. playing_info:%s", playing_info)
|
||||||
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
|
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
|
||||||
"volume", 5
|
"volume", 0
|
||||||
)
|
)
|
||||||
self.log.info("get_volume. volume:%s", self._volume)
|
self.log.info("get_volume. volume:%s", self._volume)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user