mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-07 15:02:55 +08:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6ba656641 | ||
|
|
742cae0543 | ||
|
|
a4ab1af160 | ||
|
|
86f158532a | ||
|
|
9ea7935cfb | ||
|
|
20c7e14076 | ||
|
|
e10f5b89b6 | ||
|
|
cb0bae5ae7 | ||
|
|
6a583119d0 | ||
|
|
9349070e8b | ||
|
|
01d99dc699 | ||
|
|
f4d9a6c1fd | ||
|
|
1919bc84d9 | ||
|
|
4c1761468f | ||
|
|
c75230a67d | ||
|
|
ee7ffa55cb | ||
|
|
45bbc8af42 | ||
|
|
ab8bf8fa62 | ||
|
|
493cad080e | ||
|
|
eaa159c5cb | ||
|
|
96e3b8c2ff | ||
|
|
794e8dcd06 | ||
|
|
f2675e4340 | ||
|
|
e5059840fb | ||
|
|
f3e57789fa | ||
|
|
c151b826f7 | ||
|
|
1b3ed3b35a | ||
|
|
77a37a9438 | ||
|
|
f22de9f906 | ||
|
|
090e8c3f4c | ||
|
|
aef51fb65d | ||
|
|
a5a3a2dc62 | ||
|
|
ce9adcee7f | ||
|
|
485a42a9a0 | ||
|
|
3754970c84 | ||
|
|
924fbc208b | ||
|
|
c1a2ee4577 | ||
|
|
e84ee5de1e | ||
|
|
0414830539 | ||
|
|
385f23842d | ||
|
|
1deceaa5a5 | ||
|
|
b8f1157e27 | ||
|
|
06558c24b7 | ||
|
|
6bd399b654 | ||
|
|
228d89f1f8 | ||
|
|
e97639302f | ||
|
|
7f4e51be08 | ||
|
|
cdab5fc92d | ||
|
|
6efe498f2a | ||
|
|
0f3f2e47f5 | ||
|
|
3b720b7367 | ||
|
|
9a3e513b6c | ||
|
|
5a8e5dfa82 | ||
|
|
70d9ad93cb | ||
|
|
87b3411f5e | ||
|
|
5c88c79ac6 | ||
|
|
5df91e7a59 | ||
|
|
71e9c15b5d | ||
|
|
c151144a5a | ||
|
|
82a3373e72 | ||
|
|
29ef5f238f | ||
|
|
1809a2ab54 | ||
|
|
3b1684f553 | ||
|
|
d088374333 | ||
|
|
80da6bd1e6 | ||
|
|
619bb9c853 | ||
|
|
5add7b7a5c | ||
|
|
f61f14e16c | ||
|
|
125421db22 | ||
|
|
98b73f72df | ||
|
|
e68bc3b937 | ||
|
|
ab447a4633 | ||
|
|
23d321a722 | ||
|
|
20945954b1 | ||
|
|
d6c2078917 | ||
|
|
a5b8dc639c | ||
|
|
84751e0d68 | ||
|
|
e759658481 | ||
|
|
d83100588f | ||
|
|
83d0e02eb4 | ||
|
|
20f1f33b6c | ||
|
|
fbb5d26c28 | ||
|
|
2c21778675 | ||
|
|
959acd8fb7 | ||
|
|
148c5b7621 | ||
|
|
d7a2afba48 | ||
|
|
559ed23214 | ||
|
|
1d1e63df8a | ||
|
|
27e9d92a0a | ||
|
|
69573f3fa4 | ||
|
|
edafd79140 |
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM python:3.10 AS builder
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN python3 -m venv .venv && .venv/bin/pip install --no-cache-dir -r requirements.txt
|
||||
@@ -10,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
|
||||
|
||||
102
README.md
102
README.md
@@ -13,6 +13,8 @@
|
||||
|
||||
<https://github.com/hanxi/xiaomusic>
|
||||
|
||||
> 初次安装遇到问题请查阅 <https://github.com/hanxi/xiaomusic/issues/99> 上是否已经有解决办法。
|
||||
|
||||
## 最简配置运行
|
||||
|
||||
已经支持在 web 页面配置其他参数,docker compose 配置如下:
|
||||
@@ -27,28 +29,22 @@ services:
|
||||
- 8090:8090
|
||||
volumes:
|
||||
- ./music:/app/music
|
||||
environment:
|
||||
MI_USER: '小米账号'
|
||||
MI_PASS: '小米密码'
|
||||
XIAOMUSIC_VERBOSE: 'true'
|
||||
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 \
|
||||
docker run -p 8090:8090 \
|
||||
-v ./music:/app/music \
|
||||
hanxi/xiaomusic
|
||||
```
|
||||
|
||||
启动成功后,在 web 页面可以配置 MI_DID, MI_HARDWARE, XIAOMUSIC_SEARCH, XIAOMUSIC_PROXY 参数。
|
||||
启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。
|
||||
|
||||
### ✨ 修改8090端口
|
||||
|
||||
如果需要修改 8090 端口为其他端口,比如 5678,需要这样配,3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
|
||||
|
||||
如果需要修改 8090 端口为其他端口,比如 5678,需要这样配,3个数字都需要是 5678
|
||||
```yaml
|
||||
services:
|
||||
xiaomusic:
|
||||
@@ -60,14 +56,46 @@ services:
|
||||
volumes:
|
||||
- ./music:/app/music
|
||||
environment:
|
||||
MI_USER: '小米账号'
|
||||
MI_PASS: '小米密码'
|
||||
XIAOMUSIC_VERBOSE: 'true'
|
||||
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
|
||||
XIAOMUSIC_PORT: 5678
|
||||
```
|
||||
|
||||
其中 XIAOMUSIC_VERBOSE 设置为 'true' 时表示开启 debug 日志,遇到问题可以去 web 设置页面底部【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
|
||||
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
|
||||
|
||||
> 目前除了 XIAOMUSIC_PORT 只能在启动前配置,其他参数都可以在 web 网页里配置。
|
||||
|
||||
## pip 方式安装运行
|
||||
|
||||
```shell
|
||||
> pip install xiaomusic
|
||||
> xiaomusic --help
|
||||
__ __ _ __ __ _
|
||||
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
|
||||
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
|
||||
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
|
||||
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
|
||||
XiaoMusic v0.1.92 by: github.com/hanxi
|
||||
|
||||
usage: xiaomusic [-h] [--port PORT] [--hardware HARDWARE] [--account ACCOUNT]
|
||||
[--password PASSWORD] [--cookie COOKIE] [--verbose]
|
||||
[--config CONFIG] [--ffmpeg_location FFMPEG_LOCATION]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--port PORT 监听端口
|
||||
--hardware HARDWARE 小爱音箱型号
|
||||
--account ACCOUNT xiaomi account
|
||||
--password PASSWORD xiaomi password
|
||||
--cookie COOKIE xiaomi cookie
|
||||
--verbose show info
|
||||
--config CONFIG config file path
|
||||
--ffmpeg_location FFMPEG_LOCATION
|
||||
ffmpeg bin path
|
||||
> xiaomusic --config config.json
|
||||
```
|
||||
|
||||
其中 `config.json` 文件可以参考 `config-example.json` 文件配置。见 <https://github.com/hanxi/xiaomusic/issues/94>
|
||||
|
||||
不修改默认端口 8090 的情况下,只需要执行 `xiaomusic` 即可启动。
|
||||
|
||||
## 开发环境运行
|
||||
|
||||
@@ -105,24 +133,24 @@ pdm run xiaomusic.py
|
||||
|
||||
## 已测试支持的设备
|
||||
|
||||
```txt
|
||||
- L06A
|
||||
- L07A
|
||||
- S12
|
||||
- S12A
|
||||
- LX5A
|
||||
- LX05
|
||||
- L16A
|
||||
- L17A
|
||||
- LX06
|
||||
- LX01
|
||||
- L05B
|
||||
- L05C
|
||||
````
|
||||
| 型号 | 名称 |
|
||||
| ---- | ---------------------------------------------------------------------------------------------- |
|
||||
| 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/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) |
|
||||
| L17A | [Xiaomi Sound Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l17a) |
|
||||
| LX06 | [小爱音箱Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx06) |
|
||||
| LX01 | [小爱音箱mini](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx01) |
|
||||
| L05B | [小爱音箱Play](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05b) |
|
||||
| L05C | [小米小爱音箱Play 增强版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05c) |
|
||||
|
||||
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
|
||||
|
||||
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
|
||||
> 目前应该所有设备类型都已经支持播放,有问题随时反馈。
|
||||
|
||||
## 支持音乐格式
|
||||
|
||||
@@ -131,6 +159,7 @@ pdm run xiaomusic.py
|
||||
- wav
|
||||
- ape
|
||||
- ogg
|
||||
- m4a
|
||||
|
||||
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
|
||||
> 已知 L05B L05C 不支持 flac 格式。
|
||||
@@ -287,12 +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 接口播放音乐,用于兼容不能播放的型号
|
||||
- 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)
|
||||
|
||||
## 讨论区
|
||||
|
||||
@@ -311,6 +347,8 @@ services:
|
||||
- [NAS部署教程](https://post.m.smzdm.com/p/avpe7n99/)
|
||||
- [群晖部署教程](https://post.m.smzdm.com/p/a7px7dol/)
|
||||
- [QNAS部署教程](https://post.smzdm.com/p/a5xz5x63/)
|
||||
- 所有帮忙调试和测试的朋友
|
||||
- 所有反馈问题和建议的朋友
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
81
config-example.json
Normal file
81
config-example.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"hardware": "L07A",
|
||||
"account": "",
|
||||
"password": "",
|
||||
"mi_did": "",
|
||||
"cookie": "",
|
||||
"verbose": false,
|
||||
"music_path": "music",
|
||||
"conf_path": null,
|
||||
"hostname": "192.168.2.5",
|
||||
"port": 8090,
|
||||
"public_port": 0,
|
||||
"proxy": null,
|
||||
"search_prefix": "bilisearch:",
|
||||
"ffmpeg_location": "./ffmpeg/bin",
|
||||
"active_cmd": "play,random_play,playlocal,play_music_list,stop",
|
||||
"exclude_dirs": "@eaDir",
|
||||
"music_path_depth": 10,
|
||||
"disable_httpauth": true,
|
||||
"httpauth_username": "admin",
|
||||
"httpauth_password": "admin",
|
||||
"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": "关机,暂停,停止,停止播放",
|
||||
"user_key_word_dict": {
|
||||
"测试自定义口令": "exec#code1(\"hello\")",
|
||||
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,47 @@
|
||||
# https://github.com/yt-dlp/yt-dlp#dependencies
|
||||
|
||||
# 判断系统架构
|
||||
arch=$(arch)
|
||||
arch=$(uname -m)
|
||||
|
||||
pkg=ffmpeg-master-latest-linuxarm64-gpl
|
||||
if [[ "${arch}" == "x86_64" ]]; then
|
||||
pkg=ffmpeg-master-latest-linux64-gpl
|
||||
fi
|
||||
# 输出架构信息
|
||||
echo "当前系统架构是:$arch"
|
||||
|
||||
#export ALL_PROXY=http://192.168.2.5:8080
|
||||
wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz
|
||||
tar -xvJf $pkg.tar.xz
|
||||
mv $pkg ffmpeg
|
||||
install_from_github() {
|
||||
pkg=$1
|
||||
wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz
|
||||
tar -xvJf $pkg.tar.xz
|
||||
mkdir -p ffmpeg/bin
|
||||
mv $pkg/bin/ffmpeg ffmpeg/bin/
|
||||
mv $pkg/bin/ffprobe ffmpeg/bin/
|
||||
}
|
||||
|
||||
install_from_ffmpeg() {
|
||||
pkg=$1
|
||||
wget https://johnvansickle.com/ffmpeg/builds/$pkg.tar.xz
|
||||
mkdir -p $pkg
|
||||
tar -xvJf $pkg.tar.xz -C $pkg
|
||||
mkdir -p ffmpeg/bin
|
||||
mv $pkg/*/ffmpeg ffmpeg/bin/
|
||||
mv $pkg/*/ffprobe ffmpeg/bin/
|
||||
}
|
||||
|
||||
# 基于架构执行不同的操作
|
||||
case "$arch" in
|
||||
x86_64)
|
||||
echo "64位 x86 架构"
|
||||
install_from_github ffmpeg-master-latest-linux64-gpl
|
||||
#install_from_ffmpeg ffmpeg-git-amd64-static
|
||||
;;
|
||||
arm64 | aarch64)
|
||||
echo "64位 ARM 架构"
|
||||
install_from_github ffmpeg-master-latest-linuxarm64-gpl
|
||||
#install_from_ffmpeg ffmpeg-git-arm64-static
|
||||
;;
|
||||
armv7l)
|
||||
echo "armv7l 架构"
|
||||
install_from_ffmpeg ffmpeg-git-armhf-static
|
||||
;;
|
||||
*)
|
||||
echo "未知架构 $arch"
|
||||
;;
|
||||
esac
|
||||
|
||||
14
pdm.lock
generated
14
pdm.lock
generated
@@ -5,7 +5,7 @@
|
||||
groups = ["default", "lint"]
|
||||
strategy = ["cross_platform"]
|
||||
lock_version = "4.4.1"
|
||||
content_hash = "sha256:813253734c7d7835a76cd87fe8fe0329e02ad067f535aee6a9e11cb106569dd2"
|
||||
content_hash = "sha256:ac53cf6421de7aded8475907adc40a716a3e5c6429c614b93e5cfbddea36d048"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
@@ -519,7 +519,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "miservice-fork"
|
||||
version = "2.6.0"
|
||||
version = "2.6.1"
|
||||
requires_python = ">=3.8"
|
||||
summary = "XiaoMi Cloud Service fork from https://github.com/Yonsm/MiService"
|
||||
dependencies = [
|
||||
@@ -528,8 +528,8 @@ dependencies = [
|
||||
"rich",
|
||||
]
|
||||
files = [
|
||||
{file = "miservice_fork-2.6.0-py3-none-any.whl", hash = "sha256:98169a77ea41a7b9392e1b1fab8cb80a4165fed8a9e882d9ada9a16dd1120347"},
|
||||
{file = "miservice_fork-2.6.0.tar.gz", hash = "sha256:a59d337d1f7a92566aa147e96595a8d2f5bf3f7000ae5e7dd9ed451f18d6e2fd"},
|
||||
{file = "miservice_fork-2.6.1-py3-none-any.whl", hash = "sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39"},
|
||||
{file = "miservice_fork-2.6.1.tar.gz", hash = "sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -832,7 +832,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "yt-dlp"
|
||||
version = "2024.6.24.232830.dev0"
|
||||
version = "2024.7.1.232715.dev0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A feature-rich command-line audio/video downloader"
|
||||
dependencies = [
|
||||
@@ -846,6 +846,6 @@ dependencies = [
|
||||
"websockets>=12.0",
|
||||
]
|
||||
files = [
|
||||
{file = "yt_dlp-2024.6.24.232830.dev0-py3-none-any.whl", hash = "sha256:efffecef44ce688e9ee3c02226eb1ba4ad64b37744726e9e4df5c2bd04ea93c5"},
|
||||
{file = "yt_dlp-2024.6.24.232830.dev0.tar.gz", hash = "sha256:0e89b46958984954393692a8c41e0f6d76a773be2df381c3d3a4ff24ce89aa32"},
|
||||
{file = "yt_dlp-2024.7.1.232715.dev0-py3-none-any.whl", hash = "sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503"},
|
||||
{file = "yt_dlp-2024.7.1.232715.dev0.tar.gz", hash = "sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8"},
|
||||
]
|
||||
|
||||
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,16 +1,15 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.1.79"
|
||||
version = "0.1.94"
|
||||
description = "Play Music with xiaomi AI speaker"
|
||||
authors = [
|
||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||
]
|
||||
dependencies = [
|
||||
"requests>=2.31.0",
|
||||
"aiohttp>=3.8.6",
|
||||
"miservice-fork>=2.5.0",
|
||||
"mutagen>=1.47.0",
|
||||
"yt-dlp>=2024.04.09",
|
||||
"yt-dlp>=2024.07.01",
|
||||
"flask[async]>=3.0.1",
|
||||
"waitress>=3.0.0",
|
||||
"flask-HTTPAuth>=4.8.0",
|
||||
@@ -19,6 +18,12 @@ requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/hanxi/xiaomusic"
|
||||
|
||||
[project.scripts]
|
||||
xiaomusic = "xiaomusic.cli:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["pdm-backend"]
|
||||
build-backend = "pdm.backend"
|
||||
|
||||
@@ -305,9 +305,9 @@ MarkupSafe==2.1.4 \
|
||||
mdurl==0.1.2 \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
miservice-fork==2.6.0 \
|
||||
--hash=sha256:98169a77ea41a7b9392e1b1fab8cb80a4165fed8a9e882d9ada9a16dd1120347 \
|
||||
--hash=sha256:a59d337d1f7a92566aa147e96595a8d2f5bf3f7000ae5e7dd9ed451f18d6e2fd
|
||||
miservice-fork==2.6.1 \
|
||||
--hash=sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c \
|
||||
--hash=sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39
|
||||
multidict==6.0.4 \
|
||||
--hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \
|
||||
--hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \
|
||||
@@ -472,6 +472,6 @@ yarl==1.9.2 \
|
||||
--hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \
|
||||
--hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \
|
||||
--hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560
|
||||
yt-dlp==2024.6.24.232830.dev0 \
|
||||
--hash=sha256:0e89b46958984954393692a8c41e0f6d76a773be2df381c3d3a4ff24ce89aa32 \
|
||||
--hash=sha256:efffecef44ce688e9ee3c02226eb1ba4ad64b37744726e9e4df5c2bd04ea93c5
|
||||
yt-dlp==2024.7.1.232715.dev0 \
|
||||
--hash=sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8 \
|
||||
--hash=sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.79"
|
||||
__version__ = "0.1.94"
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
|
||||
from xiaomusic import (
|
||||
__version__,
|
||||
)
|
||||
from xiaomusic.config import Config
|
||||
from xiaomusic.xiaomusic import XiaoMusic
|
||||
|
||||
LOGO = r"""
|
||||
__ __ _ __ __ _
|
||||
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
|
||||
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
|
||||
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
|
||||
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
|
||||
{}
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
dest="port",
|
||||
help="监听端口",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hardware",
|
||||
dest="hardware",
|
||||
help="小爱 hardware",
|
||||
help="小爱音箱型号",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--account",
|
||||
@@ -44,6 +61,14 @@ 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"))
|
||||
|
||||
options = parser.parse_args()
|
||||
config = Config.from_options(options)
|
||||
|
||||
@@ -3,62 +3,79 @@ 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"))
|
||||
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
|
||||
public_port: int = int(os.getenv("XIAOMUSIC_PUBLIC_PORT", 0)) # 歌曲访问端口
|
||||
proxy: str | None = os.getenv("XIAOMUSIC_PROXY", None)
|
||||
search_prefix: str = os.getenv(
|
||||
"XIAOMUSIC_SEARCH", "ytsearch:"
|
||||
"XIAOMUSIC_SEARCH", "bilisearch:"
|
||||
) # "bilisearch:" or "ytsearch:"
|
||||
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
|
||||
active_cmd: str = os.getenv(
|
||||
@@ -69,25 +86,43 @@ 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"
|
||||
)
|
||||
log_file: str = os.getenv("XIAOMUSIC_MUSIC_LOG_FILE", "/tmp/xiaomusic.txt")
|
||||
use_music_audio_id: str = os.getenv(
|
||||
"XIAOMUSIC_USE_MUSIC_AUDIO_ID", "1582971365183456177"
|
||||
)
|
||||
use_music_id: str = os.getenv("XIAOMUSIC_USE_MUSIC_ID", "355454500")
|
||||
log_file: str = os.getenv("XIAOMUSIC_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"
|
||||
)
|
||||
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
|
||||
)
|
||||
enable_force_stop: bool = (
|
||||
os.getenv("XIAOMUSIC_ENABLE_FORCE_STOP", "false").lower() == "true"
|
||||
)
|
||||
|
||||
def append_keyword(self, keys, action):
|
||||
for key in keys.split(","):
|
||||
@@ -95,17 +130,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)
|
||||
keywords_playlocal = os.getenv(
|
||||
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
|
||||
)
|
||||
self.append_keyword(keywords_playlocal, "playlocal")
|
||||
keywords_play = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
|
||||
self.append_keyword(keywords_play, "play")
|
||||
keywords_stop = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止")
|
||||
self.append_keyword(keywords_stop, "stop")
|
||||
|
||||
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 文件
|
||||
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:
|
||||
@@ -126,3 +170,34 @@ class Config:
|
||||
if value is not None and key in cls.__dataclass_fields__:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def update_config(self, data):
|
||||
# 获取类型提示
|
||||
type_hints = get_type_hints(self)
|
||||
|
||||
for k, v in data.items():
|
||||
if v and k in type_hints:
|
||||
# 获取字段的类型
|
||||
expected_type = type_hints[k]
|
||||
|
||||
# 根据期望的类型进行转换
|
||||
if isinstance(v, expected_type):
|
||||
# 如果v已经是正确的类型,则直接赋值
|
||||
setattr(self, k, v)
|
||||
else:
|
||||
# 尝试转换类型
|
||||
try:
|
||||
# 特殊情况处理(例如对布尔值的转换)
|
||||
if expected_type is bool:
|
||||
converted_value = False
|
||||
if v and v.lower() == "true":
|
||||
converted_value = True
|
||||
else:
|
||||
# 使用期望类型的构造函数进行转换
|
||||
converted_value = expected_type(v)
|
||||
except (ValueError, TypeError) as e:
|
||||
print(f"Error converting {v} to {expected_type}: {e}")
|
||||
continue
|
||||
|
||||
# 设置转换后的值
|
||||
setattr(self, k, converted_value)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
from dataclasses import asdict
|
||||
from threading import Thread
|
||||
|
||||
from flask import Flask, request, send_file, send_from_directory
|
||||
@@ -10,6 +11,7 @@ from xiaomusic import (
|
||||
__version__,
|
||||
)
|
||||
from xiaomusic.utils import (
|
||||
deepcopy_data_no_sensitive_info,
|
||||
downloadfile,
|
||||
)
|
||||
|
||||
@@ -98,20 +100,11 @@ async def do_cmd():
|
||||
@auth.login_required
|
||||
async def getsetting():
|
||||
config = xiaomusic.getconfig()
|
||||
log.debug(config)
|
||||
|
||||
data = asdict(config)
|
||||
alldevices = await xiaomusic.call_main_thread_function(xiaomusic.getalldevices)
|
||||
log.info(alldevices)
|
||||
data = {
|
||||
"mi_did": config.mi_did,
|
||||
"mi_did_list": alldevices["did_list"],
|
||||
"mi_hardware": config.hardware,
|
||||
"mi_hardware_list": alldevices["hardware_list"],
|
||||
"xiaomusic_search": config.search_prefix,
|
||||
"xiaomusic_proxy": config.proxy,
|
||||
"xiaomusic_music_list_url": config.music_list_url,
|
||||
"xiaomusic_music_list_json": config.music_list_json,
|
||||
}
|
||||
log.info(f"getsetting alldevices: {alldevices}")
|
||||
data["mi_did_list"] = alldevices["did_list"]
|
||||
data["mi_hardware_list"] = alldevices["hardware_list"]
|
||||
return data
|
||||
|
||||
|
||||
@@ -119,7 +112,8 @@ async def getsetting():
|
||||
@auth.login_required
|
||||
async def savesetting():
|
||||
data = request.get_json()
|
||||
log.info(data)
|
||||
debug_data = deepcopy_data_no_sensitive_info(data)
|
||||
log.info(f"saveconfig: {debug_data}")
|
||||
await xiaomusic.saveconfig(data)
|
||||
return "save success"
|
||||
|
||||
@@ -177,6 +171,16 @@ async def playurl():
|
||||
return await xiaomusic.call_main_thread_function(xiaomusic.play_url, arg1=url)
|
||||
|
||||
|
||||
@app.route("/debug_play_by_music_url", methods=["POST"])
|
||||
@auth.login_required
|
||||
async def debug_play_by_music_url():
|
||||
data = request.get_json()
|
||||
log.info(f"data:{data}")
|
||||
return await xiaomusic.call_main_thread_function(
|
||||
xiaomusic.debug_play_by_music_url, arg1=data
|
||||
)
|
||||
|
||||
|
||||
def static_path_handler(filename):
|
||||
log.debug(filename)
|
||||
log.debug(static_path)
|
||||
|
||||
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)
|
||||
@@ -23,7 +23,7 @@ $(function(){
|
||||
// 拉取版本
|
||||
$.get("/getversion", function(data, status) {
|
||||
console.log(data, status, data["version"]);
|
||||
$("#version").text(`(${data.version})`);
|
||||
$("#version").text(`${data.version}`);
|
||||
});
|
||||
|
||||
// 拉取播放列表
|
||||
|
||||
60
xiaomusic/static/debug.html
Normal file
60
xiaomusic/static/debug.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Debug For XiaoMusic</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script src="/static/jquery-3.7.1.min.js"></script>
|
||||
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
|
||||
function postJSON() {
|
||||
var data = $('#post-input').val();
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/debug_play_by_music_url',
|
||||
data: data,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
success: (err) => {
|
||||
console.log("succ", res);
|
||||
},
|
||||
error: (res) => {
|
||||
console.log("error", res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
</html>
|
||||
@@ -8,7 +8,11 @@
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h2>小爱音箱操控面板<span id="version">(版本未知)</span></h2>
|
||||
<h2>小爱音箱操控面板
|
||||
(<a id="version" href="https://github.com/hanxi/xiaomusic/releases">
|
||||
版本未知
|
||||
</a>)
|
||||
</h2>
|
||||
<hr>
|
||||
<div id="cmds">
|
||||
</div>
|
||||
|
||||
@@ -6,26 +6,127 @@
|
||||
<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>小爱音箱设置面板<span id="version">(版本未知)</span></h2>
|
||||
<h2>小爱音箱设置面板
|
||||
(<a id="version" href="https://github.com/hanxi/xiaomusic/releases">
|
||||
版本未知
|
||||
</a>)
|
||||
</h2>
|
||||
<hr>
|
||||
|
||||
<div class="rows">
|
||||
<label for="mi_did">MI_DID:</label>
|
||||
<select id="mi_did"></select>
|
||||
<label for="mi_hardware">MI_HARDWARE(型号):</label>
|
||||
<select id="mi_hardware" disabled></select>
|
||||
<label for="xiaomusic_search">XIAOMUSIC_SEARCH:</label>
|
||||
<select id="xiaomusic_search">
|
||||
<option value="ytsearch:">ytsearch:</option>
|
||||
<option value="bilisearch:">bilisearch:</option>
|
||||
<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>
|
||||
|
||||
<label for="password">*小米密码:</label>
|
||||
<input id="password" type="password" placeholder="填写小米登录密码"></input>
|
||||
|
||||
<label for="hostname">*XIAOMUSIC_HOSTNAME(IP或域名):</label>
|
||||
<input id="hostname" type="text"></input>
|
||||
|
||||
<label for="verbose">是否开启调试日志:</label>
|
||||
<select id="verbose">
|
||||
<option value="true" selected>true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
<label for="xiaomusic_proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
|
||||
<input id="xiaomusic_proxy" type="text" placeholder="http://192.168.2.5:8080"></input>
|
||||
<label for="xiaomusic_music_list_url">歌单地址:</label>
|
||||
<input id="xiaomusic_music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json"></input>
|
||||
<label for="xiaomusic_music_list_json">歌单内容:</label>
|
||||
<textarea id="xiaomusic_music_list_json" type="text"></textarea>
|
||||
|
||||
<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" value='music/download'></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">日志路径:</label>
|
||||
<input id="log_file" type="text" value="/tmp/xiaomusic.txt"></input>
|
||||
|
||||
<label for="active_cmd">允许唤醒的命令:</label>
|
||||
<input id="active_cmd" type="text" value="play,random_play,playlocal,play_music_list,stop"></input>
|
||||
|
||||
<label for="exclude_dirs">忽略目录(逗号分割):</label>
|
||||
<input id="exclude_dirs" type="text" value="@eaDir"></input>
|
||||
|
||||
<label for="music_path_depth">目录深度:</label>
|
||||
<input id="music_path_depth" type="number" value="10"></input>
|
||||
|
||||
<label for="search_prefix">XIAOMUSIC_SEARCH(歌曲下载方式):</label>
|
||||
<select id="search_prefix">
|
||||
<option value="bilisearch:">bilisearch:</option>
|
||||
<option value="ytsearch:">ytsearch:</option>
|
||||
</select>
|
||||
|
||||
<label for="proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
|
||||
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080"></input>
|
||||
|
||||
<label for="disable_httpauth">关闭密码验证:</label>
|
||||
<select id="disable_httpauth">
|
||||
<option value="true" selected>true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
<label for="httpauth_username">web控制台账户:</label>
|
||||
<input id="httpauth_username" type="text" value=""></input>
|
||||
<label for="httpauth_password">web控制台密码:</label>
|
||||
<input id="httpauth_password" type="password" value=""></input>
|
||||
|
||||
<label for="disable_download">关闭下载功能:</label>
|
||||
<select id="disable_download">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="use_music_audio_id">触屏版显示歌曲ID:</label>
|
||||
<input id="use_music_audio_id" type="text" value="1582971365183456177"></input>
|
||||
<label for="use_music_id">触屏版显示歌曲分段ID:</label>
|
||||
<input id="use_music_id" type="text" value="355454500"></input>
|
||||
|
||||
<label for="fuzzy_match_cutoff">模糊匹配阈值(0.1~0.9):</label>
|
||||
<input id="fuzzy_match_cutoff" type="number" value="0.6"></input>
|
||||
|
||||
<label for="enable_fuzzy_match">开启模糊搜索:</label>
|
||||
<select id="enable_fuzzy_match">
|
||||
<option value="true" selected>true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
|
||||
<label for="public_port">外网访问端口(0表示跟监听端口一致):</label>
|
||||
<input id="public_port" type="number" value="0"></input>
|
||||
|
||||
<label for="stop_tts_msg">停止提示音:</label>
|
||||
<input id="stop_tts_msg" type="text" value="收到,再见"></input>
|
||||
<label for="keywords_playlocal">播放本地歌曲口令:</label>
|
||||
<input id="keywords_playlocal" type="text" value="播放本地歌曲,本地播放歌曲"></input>
|
||||
<label for="keywords_play">播放歌曲口令:</label>
|
||||
<input id="keywords_play" type="text" value="播放歌曲,放歌曲"></input>
|
||||
<label for="keywords_stop">停止口令:</label>
|
||||
<input id="keywords_stop" type="text" value="关机,暂停,停止,停止播放"></input>
|
||||
|
||||
<label for="music_list_url">歌单地址:</label>
|
||||
<input id="music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json"></input>
|
||||
|
||||
<label for="music_list_json">歌单内容:</label>
|
||||
<textarea id="music_list_json" type="text"></textarea>
|
||||
|
||||
</div>
|
||||
<hr>
|
||||
<button onclick="location.href='/';">返回首页</button>
|
||||
@@ -35,6 +136,8 @@
|
||||
<hr>
|
||||
|
||||
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>
|
||||
<hr>
|
||||
<a href="/static/debug.html" target="_blank">调试工具</a>
|
||||
|
||||
<footer>
|
||||
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
||||
|
||||
@@ -2,91 +2,112 @@ $(function(){
|
||||
// 拉取版本
|
||||
$.get("/getversion", function(data, status) {
|
||||
console.log(data, status, data["version"]);
|
||||
$("#version").text(`(${data.version})`);
|
||||
$("#version").text(`${data.version}`);
|
||||
});
|
||||
|
||||
const updateSelectOptions = (selectId, optionsList, selectedOption) => {
|
||||
const select = $(selectId);
|
||||
select.empty();
|
||||
optionsList.forEach(option => {
|
||||
select.append(new Option(option, option));
|
||||
});
|
||||
select.val(selectedOption);
|
||||
};
|
||||
|
||||
let isChanging = false;
|
||||
// 更新下拉菜单的函数
|
||||
const updateSelect = (selectId, value) => {
|
||||
if (!isChanging) {
|
||||
isChanging = true;
|
||||
$(selectId).val(value);
|
||||
isChanging = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 联动逻辑
|
||||
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]);
|
||||
}
|
||||
// 遍历所有的select元素,默认选中只有1个选项的
|
||||
const autoSelectOne = () => {
|
||||
$('select').each(function() {
|
||||
// 如果select元素仅有一个option子元素
|
||||
if ($(this).children('option').length === 1) {
|
||||
// 选中这个option
|
||||
$(this).find('option').prop('selected', true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedDidsAndHardware(containerSelector) {
|
||||
var selectedDids = [];
|
||||
var selectedHardware = [];
|
||||
|
||||
// 仅选择给定容器中选中的复选框
|
||||
$(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);
|
||||
updateCheckbox("#mi_did_hardware", data.mi_did_list, data.mi_did, data.mi_hardware_list);
|
||||
|
||||
updateSelectOptions("#mi_did", data.mi_did_list, data.mi_did);
|
||||
updateSelectOptions("#mi_hardware", data.mi_hardware_list, data.mi_hardware);
|
||||
|
||||
// 初始化联动
|
||||
linkSelects('#mi_did', data.mi_did_list, '#mi_hardware', data.mi_hardware_list);
|
||||
|
||||
if (data.xiaomusic_search != "") {
|
||||
$("#xiaomusic_search").val(data.xiaomusic_search);
|
||||
// 初始化显示
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
const $element = $("#" + key);
|
||||
if ($element.length && data[key] !== '') {
|
||||
if (data[key] === true) {
|
||||
$element.val('true');
|
||||
} else if (data[key] === false) {
|
||||
$element.val('false');
|
||||
} else {
|
||||
$element.val(data[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.xiaomusic_proxy != "") {
|
||||
$("#xiaomusic_proxy").val(data.xiaomusic_proxy);
|
||||
}
|
||||
|
||||
if (data.xiaomusic_music_list_url != "") {
|
||||
$("#xiaomusic_music_list_url").val(data.xiaomusic_music_list_url);
|
||||
}
|
||||
|
||||
if (data.xiaomusic_music_list_json != "") {
|
||||
$("#xiaomusic_music_list_json").val(data.xiaomusic_music_list_json);
|
||||
}
|
||||
autoSelectOne();
|
||||
});
|
||||
|
||||
$("#save").on("click", () => {
|
||||
var mi_did = $("#mi_did").val();
|
||||
var mi_hardware = $("#mi_hardware").val();
|
||||
var xiaomusic_search = $("#xiaomusic_search").val();
|
||||
var xiaomusic_proxy = $("#xiaomusic_proxy").val();
|
||||
var xiaomusic_music_list_url = $("#xiaomusic_music_list_url").val();
|
||||
var xiaomusic_music_list_json = $("#xiaomusic_music_list_json").val();
|
||||
console.log("mi_did", mi_did);
|
||||
console.log("mi_hardware", mi_hardware);
|
||||
console.log("xiaomusic_search", xiaomusic_search);
|
||||
console.log("xiaomusic_proxy", xiaomusic_proxy);
|
||||
console.log("xiaomusic_music_list_url", xiaomusic_music_list_url);
|
||||
console.log("xiaomusic_music_list_json", xiaomusic_music_list_json);
|
||||
var data = {
|
||||
mi_did: mi_did,
|
||||
mi_hardware: mi_hardware,
|
||||
xiaomusic_search: xiaomusic_search,
|
||||
xiaomusic_proxy: xiaomusic_proxy,
|
||||
xiaomusic_music_list_url: xiaomusic_music_list_url,
|
||||
xiaomusic_music_list_json: xiaomusic_music_list_json,
|
||||
};
|
||||
var setting = $('#setting');
|
||||
var inputs = setting.find('input, select, textarea');
|
||||
var data = {};
|
||||
inputs.each(function() {
|
||||
var id = this.id;
|
||||
if (id) {
|
||||
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({
|
||||
type: "POST",
|
||||
url: "/savesetting",
|
||||
@@ -102,10 +123,10 @@ $(function(){
|
||||
});
|
||||
|
||||
$("#get_music_list").on("click", () => {
|
||||
var xiaomusic_music_list_url = $("#xiaomusic_music_list_url").val();
|
||||
console.log("xiaomusic_music_list_url", xiaomusic_music_list_url);
|
||||
var music_list_url = $("#music_list_url").val();
|
||||
console.log("music_list_url", music_list_url);
|
||||
var data = {
|
||||
url: xiaomusic_music_list_url,
|
||||
url: music_list_url,
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
@@ -114,7 +135,7 @@ $(function(){
|
||||
data: JSON.stringify(data),
|
||||
success: (res) => {
|
||||
if (res.ret == "OK") {
|
||||
$("#xiaomusic_music_list_json").val(res.content);
|
||||
$("#music_list_json").val(res.content);
|
||||
} else {
|
||||
console.log(res);
|
||||
alert(res.ret);
|
||||
|
||||
@@ -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; /* 方块字体大小,视清晰度需调整 */
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import difflib
|
||||
import os
|
||||
import random
|
||||
@@ -240,3 +241,30 @@ def get_local_music_duration(filename):
|
||||
|
||||
def get_random(length):
|
||||
return "".join(random.sample(string.ascii_letters + string.digits, length))
|
||||
|
||||
|
||||
# 深拷贝把敏感数据设置位*
|
||||
def deepcopy_data_no_sensitive_info(data, fields_to_anonymize=None):
|
||||
if fields_to_anonymize is None:
|
||||
fields_to_anonymize = [
|
||||
"account",
|
||||
"password",
|
||||
"httpauth_username",
|
||||
"httpauth_password",
|
||||
]
|
||||
|
||||
copy_data = copy.deepcopy(data)
|
||||
|
||||
# 检查copy_data是否是字典或具有属性的对象
|
||||
if isinstance(copy_data, dict):
|
||||
# 对字典进行处理
|
||||
for field in fields_to_anonymize:
|
||||
if field in copy_data:
|
||||
copy_data[field] = "******"
|
||||
else:
|
||||
# 对对象进行处理
|
||||
for field in fields_to_anonymize:
|
||||
if hasattr(copy_data, field):
|
||||
setattr(copy_data, field, "******")
|
||||
|
||||
return copy_data
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -14,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__,
|
||||
@@ -29,12 +28,13 @@ 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,
|
||||
find_best_match,
|
||||
fuzzyfinder,
|
||||
get_local_music_duration,
|
||||
get_random,
|
||||
get_web_music_duration,
|
||||
parse_cookie_string,
|
||||
walk_to_depth,
|
||||
@@ -52,29 +52,15 @@ class XiaoMusic:
|
||||
self.config = config
|
||||
|
||||
self.mi_token_home = Path.home() / ".mi.token"
|
||||
self.last_timestamp = int(time.time() * 1000) # timestamp last call mi speaker
|
||||
self.last_timestamp = {} # 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.music_path = config.music_path
|
||||
self.conf_path = config.conf_path
|
||||
if not self.conf_path:
|
||||
self.conf_path = config.music_path
|
||||
|
||||
self.hostname = config.hostname
|
||||
self.port = config.port
|
||||
self.proxy = config.proxy
|
||||
self.search_prefix = config.search_prefix
|
||||
self.ffmpeg_location = config.ffmpeg_location
|
||||
self.active_cmd = config.active_cmd.split(",")
|
||||
self.exclude_dirs = set(config.exclude_dirs.split(","))
|
||||
self.music_path_depth = config.music_path_depth
|
||||
self.device2hardware = {}
|
||||
self.did2device = {}
|
||||
|
||||
# 下载对象
|
||||
self.download_proc = None
|
||||
@@ -94,6 +80,9 @@ class XiaoMusic:
|
||||
# 关机定时器
|
||||
self._stop_timer = None
|
||||
|
||||
# 初始化配置
|
||||
self.init_config()
|
||||
|
||||
# 初始化日志
|
||||
self.setup_logger()
|
||||
|
||||
@@ -106,10 +95,44 @@ 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}")
|
||||
|
||||
def init_config(self):
|
||||
self.music_path = self.config.music_path
|
||||
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
|
||||
self.public_port = self.config.public_port
|
||||
if self.public_port == 0:
|
||||
self.public_port = self.port
|
||||
|
||||
self.proxy = self.config.proxy
|
||||
self.search_prefix = self.config.search_prefix
|
||||
self.ffmpeg_location = self.config.ffmpeg_location
|
||||
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
|
||||
|
||||
def setup_logger(self):
|
||||
log_format = f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s"
|
||||
date_format = "[%X]"
|
||||
formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
logging.basicConfig(
|
||||
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
|
||||
datefmt="[%X]",
|
||||
format=log_format,
|
||||
datefmt=date_format,
|
||||
)
|
||||
|
||||
log_file = self.config.log_file
|
||||
@@ -121,35 +144,37 @@ class XiaoMusic:
|
||||
handler = RotatingFileHandler(
|
||||
self.config.log_file, maxBytes=10 * 1024 * 1024, backupCount=1
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
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 with ClientSession() as session:
|
||||
session._cookie_jar = self.cookie_jar
|
||||
while True:
|
||||
self.log.debug(
|
||||
"Listening new message, timestamp: %s", self.last_timestamp
|
||||
f"Listening new message, timestamp: {self.last_timestamp}"
|
||||
)
|
||||
await self.get_latest_ask_from_xiaoai(session)
|
||||
session._cookie_jar = self.cookie_jar
|
||||
|
||||
# 拉取所有音箱的对话记录
|
||||
tasks = [
|
||||
self.get_latest_ask_from_xiaoai(session, device_id)
|
||||
for device_id in self.device2hardware
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
start = time.perf_counter()
|
||||
self.log.debug("Polling_event, timestamp: %s", self.last_timestamp)
|
||||
self.log.debug(f"Polling_event, timestamp: {self.last_timestamp}")
|
||||
await self.polling_event.wait()
|
||||
if (d := time.perf_counter() - start) < 1:
|
||||
# sleep to avoid too many request
|
||||
self.log.debug("Sleep %f, timestamp: %s", d, self.last_timestamp)
|
||||
self.log.debug(f"Sleep {d}, timestamp: {self.last_timestamp}")
|
||||
await asyncio.sleep(1 - d)
|
||||
|
||||
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)
|
||||
@@ -165,61 +190,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):
|
||||
@@ -230,12 +220,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:
|
||||
@@ -245,7 +241,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)
|
||||
@@ -260,18 +256,21 @@ class XiaoMusic:
|
||||
self.log.info("Maybe outof date trying to re init it")
|
||||
await self.init_all_data(self.session)
|
||||
else:
|
||||
return self._get_last_query(data)
|
||||
return self._get_last_query(device_id, data)
|
||||
|
||||
def _get_last_query(self, data):
|
||||
self.log.debug(f"_get_last_query:{data}")
|
||||
def _get_last_query(self, device_id, data):
|
||||
self.log.debug(f"_get_last_query device_id:{device_id} data:{data}")
|
||||
if d := data.get("data"):
|
||||
records = json.loads(d).get("records")
|
||||
if not records:
|
||||
return
|
||||
last_record = records[0]
|
||||
timestamp = last_record.get("time")
|
||||
if timestamp > self.last_timestamp:
|
||||
self.last_timestamp = timestamp
|
||||
# 首次用当前时间初始化
|
||||
if device_id not in self.last_timestamp:
|
||||
self.last_timestamp[device_id] = int(time.time() * 1000)
|
||||
if timestamp > self.last_timestamp[device_id]:
|
||||
self.last_timestamp[device_id] = timestamp
|
||||
self.last_record = last_record
|
||||
self.new_record_event.set()
|
||||
|
||||
@@ -284,45 +283,104 @@ class XiaoMusic:
|
||||
self.new_record_event.set()
|
||||
|
||||
async def do_tts(self, value):
|
||||
self.log.info("do_tts: %s", value)
|
||||
self.log.info(f"try do_tts value:{value}")
|
||||
if not value:
|
||||
self.log.info("do_tts no value")
|
||||
return
|
||||
|
||||
await self.force_stop_xiaoai()
|
||||
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_one(self, device_id, value):
|
||||
try:
|
||||
await self.mina_service.text_to_speech(self.device_id, value)
|
||||
await self.mina_service.text_to_speech(device_id, value)
|
||||
except Exception as 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}")
|
||||
if self._playing and not self.is_downloading():
|
||||
|
||||
async def text_to_speech(self, value):
|
||||
tasks = [
|
||||
self.text_to_speech_one(device_id, value)
|
||||
for device_id in self.device2hardware
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# 继续播放被打断的歌曲
|
||||
async def check_replay(self):
|
||||
if self.isplaying() and not self.isdownloading():
|
||||
# 继续播放歌曲
|
||||
self.log.info("继续播放歌曲")
|
||||
self.log.info("现在继续播放歌曲")
|
||||
await self.play()
|
||||
else:
|
||||
self.log.info(
|
||||
f"不会继续播放歌曲. isplaying:{self.isplaying()} isdownloading:{self.isdownloading()}"
|
||||
)
|
||||
|
||||
async def do_set_volume(self, value):
|
||||
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, device_id):
|
||||
playing_info = await self.mina_service.player_get_status(device_id)
|
||||
self.log.info(playing_info)
|
||||
# WTF xiaomi api
|
||||
is_playing = (
|
||||
json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1)
|
||||
== 1
|
||||
)
|
||||
return 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 or self.config.enable_force_stop:
|
||||
# stop it
|
||||
ret = await self.mina_service.player_stop(device_id)
|
||||
self.log.info(
|
||||
f"stop_if_xiaoai_is_playing player_stop device_id:{device_id} enable_force_stop:{self.config.enable_force_stop} ret:{ret}"
|
||||
)
|
||||
|
||||
async def force_stop_one_xiaoai(self, device_id):
|
||||
try:
|
||||
ret = await self.mina_service.player_pause(device_id)
|
||||
self.log.info(
|
||||
f"force_stop_one_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}")
|
||||
|
||||
async def force_stop_xiaoai(self):
|
||||
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}")
|
||||
tasks = [
|
||||
self.force_stop_one_xiaoai(device_id) for device_id in self.device2hardware
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# 是否在下载中
|
||||
def is_downloading(self):
|
||||
def isdownloading(self):
|
||||
if not self.download_proc:
|
||||
return False
|
||||
if (
|
||||
self.download_proc.returncode is not None
|
||||
and self.download_proc.returncode < 0
|
||||
):
|
||||
|
||||
if self.download_proc.returncode is not None:
|
||||
self.log.info(
|
||||
f"Process exited with returncode:{self.download_proc.returncode}"
|
||||
)
|
||||
return False
|
||||
|
||||
self.log.info("Download Process is still running.")
|
||||
return True
|
||||
|
||||
# 下载歌曲
|
||||
@@ -340,7 +398,7 @@ class XiaoMusic:
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"--paths",
|
||||
self.music_path,
|
||||
self.download_path,
|
||||
"-o",
|
||||
f"{name}.mp3",
|
||||
"--ffmpeg-location",
|
||||
@@ -351,7 +409,8 @@ class XiaoMusic:
|
||||
if self.proxy:
|
||||
sbp_args += ("--proxy", f"{self.proxy}")
|
||||
|
||||
self.log.info(f"download: {sbp_args}")
|
||||
cmd = " ".join(sbp_args)
|
||||
self.log.info(f"download cmd: {cmd}")
|
||||
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
|
||||
await self.do_tts(f"正在下载歌曲{search_key}")
|
||||
|
||||
@@ -415,12 +474,12 @@ class XiaoMusic:
|
||||
self.log.debug("get_music_url web music. name:%s, url:%s", name, url)
|
||||
return url
|
||||
|
||||
filename = self.get_filename(name)
|
||||
filename = self.get_filename(name).replace("\\", "/")
|
||||
self.log.debug(
|
||||
"get_music_url local music. name:%s, filename:%s", name, filename
|
||||
)
|
||||
encoded_name = urllib.parse.quote(filename)
|
||||
return f"http://{self.hostname}:{self.port}/{encoded_name}"
|
||||
return f"http://{self.hostname}:{self.public_port}/{encoded_name}"
|
||||
|
||||
# 递归获取目录下所有歌曲,生成随机播放列表
|
||||
def _gen_all_music_list(self):
|
||||
@@ -476,6 +535,7 @@ class XiaoMusic:
|
||||
if not self.config.music_list_json:
|
||||
return
|
||||
|
||||
self._all_radio = {}
|
||||
music_list = json.loads(self.config.music_list_json)
|
||||
try:
|
||||
for item in music_list:
|
||||
@@ -516,7 +576,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)
|
||||
@@ -551,7 +611,7 @@ class XiaoMusic:
|
||||
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info("定时器已取消")
|
||||
self.log.info("旧定时器已取消")
|
||||
|
||||
self._timeout = sec
|
||||
|
||||
@@ -599,19 +659,42 @@ 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)
|
||||
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:
|
||||
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
|
||||
|
||||
# 检查是否匹配到完全一样的指令
|
||||
def check_full_match_cmd(self, query, ctrl_panel):
|
||||
if query in self.config.key_match_order:
|
||||
opkey = query
|
||||
opvalue = self.config.key_word_dict.get(opkey)
|
||||
if ctrl_panel or self.isplaying():
|
||||
return opvalue
|
||||
else:
|
||||
if not self.active_cmd or opvalue in self.active_cmd:
|
||||
return opvalue
|
||||
return None
|
||||
|
||||
# 匹配命令
|
||||
def match_cmd(self, query, ctrl_panel):
|
||||
# 优先处理完全匹配
|
||||
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:
|
||||
patternarg = rf"(.*){opkey}(.*)"
|
||||
# 匹配参数
|
||||
@@ -629,20 +712,16 @@ class XiaoMusic:
|
||||
argafter,
|
||||
)
|
||||
oparg = argafter
|
||||
opvalue = self.config.key_word_dict.get(opkey)
|
||||
if not ctrl_panel and not self._playing:
|
||||
if self.active_cmd and opvalue not in self.active_cmd:
|
||||
self.log.debug(f"不在激活命令中 {opvalue}")
|
||||
continue
|
||||
if opkey in KEY_WORD_ARG_BEFORE_DICT:
|
||||
oparg = argpre
|
||||
self.log.info(
|
||||
"匹配到指令. opkey:%s opvalue:%s oparg:%s", opkey, opvalue, oparg
|
||||
)
|
||||
opvalue = self.config.key_word_dict.get(opkey)
|
||||
if not ctrl_panel and not self.isplaying():
|
||||
if self.active_cmd and opvalue not in self.active_cmd:
|
||||
self.log.ifno(f"不在激活命令中 {opvalue}")
|
||||
continue
|
||||
self.log.info(f"匹配到指令. opkey:{opkey} opvalue:{opvalue} oparg:{oparg}")
|
||||
return (opvalue, oparg)
|
||||
if self._playing:
|
||||
self.log.info("未匹配到指令,自动停止")
|
||||
return ("stop", "notts")
|
||||
self.log.info(f"未匹配到指令 {query} {ctrl_panel}")
|
||||
return (None, None)
|
||||
|
||||
# 判断是否播放下一首歌曲
|
||||
@@ -656,40 +735,34 @@ class XiaoMusic:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _play_by_music_url(self, device_id, url):
|
||||
audio_id = get_random(30)
|
||||
audio_type = ""
|
||||
if self.config.hardware in ["LX04", "X10A", "X08A"]:
|
||||
audio_type = "MUSIC"
|
||||
music = {
|
||||
"payload": {
|
||||
"audio_items": [
|
||||
{"item_id": {"audio_id": audio_id}, "stream": {"url": url}}
|
||||
],
|
||||
"audio_type": audio_type,
|
||||
}
|
||||
}
|
||||
return await self.mina_service.ubus_request(
|
||||
device_id,
|
||||
"player_play_music",
|
||||
"mediaplayer",
|
||||
{"startaudioid": audio_id, "music": json.dumps(music)},
|
||||
)
|
||||
|
||||
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.debug(
|
||||
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.debug(
|
||||
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
|
||||
)
|
||||
return await self.all_player_play(url)
|
||||
|
||||
async def play_one_url(self, device_id, url):
|
||||
try:
|
||||
if self.config.use_music_api:
|
||||
ret = await self.play_by_music_url(device_id, url)
|
||||
self.log.info(
|
||||
f"play_one_url 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"play_one_url play_by_url device_id:{device_id} ret:{ret} url:{url}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
return ret
|
||||
|
||||
async def all_player_play(self, url):
|
||||
tasks = [
|
||||
self.play_one_url(device_id, url) for device_id in self.device2hardware
|
||||
]
|
||||
results = await asyncio.gather(*tasks)
|
||||
self.log.info(f"all_player_play {url} {results}")
|
||||
return results
|
||||
|
||||
def find_real_music_name(self, name):
|
||||
if not self.config.enable_fuzzy_match:
|
||||
self.log.debug("没开启模糊匹配")
|
||||
@@ -703,6 +776,7 @@ class XiaoMusic:
|
||||
self.log.info(f"根据【{name}】找到歌曲【{real_name}】")
|
||||
return real_name
|
||||
self.log.info(f"没找到歌曲【{name}】")
|
||||
return name
|
||||
|
||||
# 播放本地歌曲
|
||||
async def playlocal(self, **kwargs):
|
||||
@@ -728,8 +802,8 @@ class XiaoMusic:
|
||||
self.cur_music = name
|
||||
self.log.info(f"cur_music {self.cur_music}")
|
||||
sec, url = await self.get_music_sec_url(name)
|
||||
self.log.info(f"播放 {url}")
|
||||
await self.force_stop_xiaoai()
|
||||
self.log.info(f"播放 {url}")
|
||||
await self.play_url(arg1=url)
|
||||
self.log.info("已经开始播放了")
|
||||
# 设置下一首歌曲的播放定时器
|
||||
@@ -759,7 +833,7 @@ class XiaoMusic:
|
||||
await self.do_tts(f"本地不存在歌曲{name}")
|
||||
return
|
||||
await self.download(search_key, name)
|
||||
self.log.info("正在下载中 %s", search_key + ":" + name)
|
||||
self.log.info(f"正在下载中 {search_key} {name}")
|
||||
await self.download_proc.wait()
|
||||
# 把文件插入到播放列表里
|
||||
self.add_download_music(name)
|
||||
@@ -854,11 +928,12 @@ class XiaoMusic:
|
||||
async def stop(self, **kwargs):
|
||||
self._playing = False
|
||||
if kwargs.get("arg1", "") != "notts":
|
||||
await self.do_tts("收到关机口令,再见")
|
||||
await self.do_tts(self.config.stop_tts_msg)
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info("定时器已取消")
|
||||
await self.force_stop_xiaoai()
|
||||
self.log.info("stop now")
|
||||
|
||||
async def stop_after_minute(self, **kwargs):
|
||||
if self._stop_timer:
|
||||
@@ -881,7 +956,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
|
||||
@@ -949,20 +1026,24 @@ class XiaoMusic:
|
||||
await self.call_main_thread_function(self.reinit)
|
||||
|
||||
def update_config_from_setting(self, data):
|
||||
self.config.mi_did = data.get("mi_did")
|
||||
# 兼容旧配置:一段时间后清理这里的旧代码
|
||||
self.config.hardware = data.get("mi_hardware")
|
||||
self.config.search_prefix = data.get("xiaomusic_search")
|
||||
self.config.proxy = data.get("xiaomusic_proxy")
|
||||
self.config.music_list_url = data.get("xiaomusic_music_list_url")
|
||||
self.config.music_list_json = data.get("xiaomusic_music_list_json")
|
||||
|
||||
self.search_prefix = self.config.search_prefix
|
||||
self.proxy = self.config.proxy
|
||||
self.log.debug("update_config_from_setting ok. data:%s", data)
|
||||
# 自动赋值相同字段的配置
|
||||
self.config.update_config(data)
|
||||
|
||||
self.init_config()
|
||||
debug_config = deepcopy_data_no_sensitive_info(self.config)
|
||||
self.log.info("update_config_from_setting ok. data:%s", debug_config)
|
||||
|
||||
# 重新初始化
|
||||
async def reinit(self, **kwargs):
|
||||
await self.try_update_device_id()
|
||||
self.setup_logger()
|
||||
await self.init_all_data(self.session)
|
||||
self._gen_all_music_list()
|
||||
self.log.info("reinit success")
|
||||
|
||||
@@ -999,3 +1080,63 @@ class XiaoMusic:
|
||||
self.new_record_event.set()
|
||||
result = await future
|
||||
return result
|
||||
|
||||
async def play_by_music_url(self, deviceId, url, _type=2):
|
||||
self.log.info(f"play_by_music_url url:{url}, type:{_type}")
|
||||
audio_type = ""
|
||||
if _type == 1:
|
||||
# If set to MUSIC, the light will be on
|
||||
audio_type = "MUSIC"
|
||||
audio_id = self.config.use_music_audio_id
|
||||
id = self.config.use_music_id
|
||||
music = {
|
||||
"payload": {
|
||||
"audio_type": audio_type,
|
||||
"audio_items": [
|
||||
{
|
||||
"item_id": {
|
||||
"audio_id": audio_id,
|
||||
"cp": {
|
||||
"album_id": "-1",
|
||||
"episode_index": 0,
|
||||
"id": id,
|
||||
"name": "xiaowei",
|
||||
},
|
||||
},
|
||||
"stream": {"url": url},
|
||||
}
|
||||
],
|
||||
"list_params": {
|
||||
"listId": "-1",
|
||||
"loadmore_offset": 0,
|
||||
"origin": "xiaowei",
|
||||
"type": "MUSIC",
|
||||
},
|
||||
},
|
||||
"play_behavior": "REPLACE_ALL",
|
||||
}
|
||||
data = {"startaudioid": audio_id, "music": json.dumps(music)}
|
||||
self.log.info(json.dumps(data))
|
||||
return await self.mina_service.ubus_request(
|
||||
deviceId,
|
||||
"player_play_music",
|
||||
"mediaplayer",
|
||||
data,
|
||||
)
|
||||
|
||||
async def debug_play_by_music_url(self, arg1=None):
|
||||
if arg1 is None:
|
||||
arg1 = {}
|
||||
data = arg1
|
||||
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(
|
||||
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