1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2025-12-06 14:52:50 +08:00

Compare commits

...

80 Commits

Author SHA1 Message Date
涵曦
aa698667c9 bump: version 0.1.101 → 0.2.0 2024-07-09 15:12:14 +00:00
涵曦
700e17854c Update README.md 2024-07-09 23:11:18 +08:00
涵曦
40258c9fa1 feat: 触屏版可以不用设置 XIAOMUSIC_USE_MUSIC_API 2024-07-09 15:07:46 +00:00
涵曦
eb59bf0db5 feat: 升级依赖库 2024-07-09 15:03:20 +00:00
涵曦
a9df78af97 Update README.md 2024-07-08 18:00:02 +08:00
涵曦
901506a32d feat: 唤醒口令配置支持配语音词,简化自定义口令配置 see #105 2024-07-08 01:06:01 +00:00
涵曦
0ddbe58fbd bump: version 0.1.100 → 0.1.101 2024-07-07 08:47:33 +00:00
涵曦
350d82184f fix: #81 修复播放列表时,当前歌曲不在列表没有更换歌曲的问题 2024-07-07 08:43:55 +00:00
涵曦
5b8054abd9 fix: #110 修复配置加载问题 2024-07-07 08:28:09 +00:00
涵曦
c5c691b653 bump: version 0.1.99 → 0.1.100 2024-07-07 06:59:44 +00:00
涵曦
7a44c8587c fix: 日志代码写错 2024-07-07 06:41:45 +00:00
涵曦
5092ffc91a bump: version 0.1.98 → 0.1.99 2024-07-07 06:39:08 +00:00
涵曦
2da12e12d5 fix: #81 修复播放列表没有继续播放上次播放的歌曲,并把随机播放,全部循环,单曲循环状态落地 2024-07-07 06:21:15 +00:00
涵曦
5aff72dbb6 bump: version 0.1.97 → 0.1.98 2024-07-07 05:48:40 +00:00
涵曦
043f452e71 Update README.md 2024-07-07 13:47:30 +08:00
涵曦
5cedf8a907 fix: 修复多设备获取不到对话记录的问题 see #65 2024-07-07 04:46:33 +00:00
涵曦
ae77c7232e Update README.md 2024-07-07 11:42:05 +08:00
涵曦
d559413d46 fix: #93 修复目录深度设置后导致目录下的歌曲无法加到播放列表里的问题 2024-07-07 02:03:17 +00:00
涵曦
8b185d8768 bump: version 0.1.96 → 0.1.97 2024-07-06 16:05:28 +00:00
涵曦
202105a11f fix: 修复网页控制台设置页面保存报错 2024-07-06 16:04:07 +00:00
涵曦
7da80594e3 bump: version 0.1.95 → 0.1.96 2024-07-06 15:46:36 +00:00
涵曦
a032a1d50a feat: 使用commitizen管理版本号 2024-07-06 15:46:15 +00:00
涵曦
be62d8abc8 feat: 页面版本号链接到CHANGELOG页面 2024-07-06 15:06:51 +00:00
涵曦
aaf9f4b6a7 feat: 规范版本管理 2024-07-06 14:49:41 +00:00
涵曦
bb5d82097e new version v0.1.95 2024-07-06 11:25:57 +00:00
涵曦
d6ba656641 new version v0.1.94 2024-07-06 11:25:33 +00:00
涵曦
742cae0543 style: 调整调试日志输出 2024-07-06 11:19:50 +00:00
涵曦
a4ab1af160 fix: 新增参数配置强制打断小爱说话 2024-07-06 11:16:42 +00:00
涵曦
86f158532a fix: 修复多设备获取对话记录的问题 2024-07-06 11:00:47 +00:00
Zhang Tinmix
9ea7935cfb fix: 修复windows下路径分隔符被视为转移符导致音箱无法播放音乐的问题
由于Windows路径中使用反斜杠(\)作为目录分隔符,而在URL编码中反斜杠被视为特殊字符所导致windows下拼接得到的url不符合预期
示例
filename:music\outside.mp3
预期:
url: /music/outside.mp3
实际:
url:/music%5Coutside.mp3

需要一个办法来处理windows下的\\路径分隔符,我给出了一种简单的解法,作者可以考虑看看怎么处理会完全一点
2024-07-06 11:00:40 +00:00
涵曦
20c7e14076 fix: 修复播放链接报错 2024-07-06 03:50:47 +00:00
涵曦
e10f5b89b6 fix: 修复配置页面默认配置被置空的问题 2024-07-06 00:32:10 +00:00
涵曦
cb0bae5ae7 feat: 优化多设备接口执行效果,尽量做到同时执行 2024-07-05 17:06:13 +00:00
涵曦
6a583119d0 new version v0.1.93 2024-07-05 15:28:51 +00:00
涵曦
9349070e8b Update README.md 2024-07-05 23:21:43 +08:00
涵曦
01d99dc699 feat: 访问账号密码默认为空 2024-07-05 15:15:31 +00:00
涵曦
f4d9a6c1fd feat: 支持下载的目录与本地音乐目录分开 see #98 2024-07-05 15:12:36 +00:00
涵曦
1919bc84d9 feat: 新增m4a文件格式支持 2024-07-05 14:55:47 +00:00
涵曦
4c1761468f fix: 修复设置页面没成功初始化设置问题 2024-07-05 14:52:14 +00:00
涵曦
c75230a67d Update ci.yml 2024-07-05 22:15:14 +08:00
涵曦
ee7ffa55cb fix: 修复镜像缺少文件问题 2024-07-05 13:45:44 +00:00
涵曦
45bbc8af42 Update ci.yml 2024-07-05 21:22:36 +08:00
涵曦
ab8bf8fa62 fix: 尝试解决插件路径问题 2024-07-05 13:12:03 +00:00
涵曦
493cad080e Update README.md 2024-07-05 18:43:06 +08:00
涵曦
eaa159c5cb feat: 设置页面支持配置多设备 2024-07-05 10:34:38 +00:00
涵曦
96e3b8c2ff feat: 默认用空的后台账号和密码 2024-07-05 09:10:48 +00:00
涵曦
794e8dcd06 feat: 支持多个设备同时播放 see #65 2024-07-05 09:10:01 +00:00
涵曦
f2675e4340 Update README.md 2024-07-05 13:08:18 +08:00
涵曦
e5059840fb fix: 设置页面日志路径写错了 2024-07-05 04:57:20 +00:00
涵曦
f3e57789fa feat: 新增自定义口令功能 #105 2024-07-05 04:26:21 +00:00
涵曦
c151b826f7 Update README.md 2024-07-04 23:31:43 +08:00
涵曦
1b3ed3b35a fix: 修复口令导致异常关闭的问题 2024-07-04 13:07:34 +00:00
涵曦
77a37a9438 new version v0.1.92 2024-07-04 11:12:00 +00:00
涵曦
f22de9f906 Update README.md 2024-07-04 19:11:03 +08:00
涵曦
090e8c3f4c feat: 启动参数新增 --port 配置监听端口 2024-07-04 10:42:31 +00:00
涵曦
aef51fb65d Update README.md 2024-07-04 18:32:15 +08:00
涵曦
a5a3a2dc62 Update README.md 2024-07-04 18:26:38 +08:00
涵曦
ce9adcee7f feat: 外网访问端口可独立配置 2024-07-04 10:15:48 +00:00
涵曦
485a42a9a0 feat: 优化设置页面,新增更多配置项 2024-07-04 09:36:06 +00:00
涵曦
3754970c84 feat: 首次保存设置后不需要重启容器 2024-07-04 04:42:19 +00:00
涵曦
924fbc208b fix: 日志文件配置的环境变量写错了 2024-07-04 04:03:23 +00:00
涵曦
c1a2ee4577 new version v0.1.91 2024-07-03 16:08:00 +00:00
涵曦
e84ee5de1e feat:触屏版显示界面的歌曲id支持配置 2024-07-03 13:48:52 +00:00
涵曦
0414830539 fix: 尝试解决触屏版不能播放的问题 2024-07-03 13:48:52 +00:00
涵曦
385f23842d Update README.md 2024-07-03 12:05:01 +08:00
涵曦
1deceaa5a5 Update README.md 2024-07-03 01:02:26 +08:00
涵曦
b8f1157e27 new version v0.1.90 2024-07-02 16:33:30 +00:00
涵曦
06558c24b7 feat: 优化触屏版播放页面显示歌曲 2024-07-02 16:33:06 +00:00
涵曦
6bd399b654 new version v0.1.89 2024-07-02 16:12:14 +00:00
涵曦
228d89f1f8 fix: 播放歌曲写成固定的了 2024-07-02 15:55:02 +00:00
涵曦
e97639302f feat: 尝试解决触屏版无法播放的问题 2024-07-02 13:40:56 +00:00
涵曦
7f4e51be08 fix: 播放歌曲时被其他指令打断后没有继续播放 2024-07-02 13:25:38 +00:00
涵曦
cdab5fc92d new version v0.1.88 2024-07-02 09:40:22 +00:00
涵曦
6efe498f2a feat: 日志里不要输出敏感信息 2024-07-02 05:27:17 +00:00
涵曦
0f3f2e47f5 feat: 优化下载 ffmpeg 脚本,尝试解决 armv7 环境问题 2024-07-02 05:24:29 +00:00
涵曦
3b720b7367 fix: 是否下载中判断错误导致播放无法自动重新开始播放 2024-07-02 03:29:01 +00:00
涵曦
9a3e513b6c fix: 升级yt-dlp到2024.07.01 2024-07-02 03:28:31 +00:00
涵曦
5a8e5dfa82 feat: 优化日志输出信息 2024-07-02 03:10:33 +00:00
涵曦
70d9ad93cb fix: 修复部分型号关机失败的问题 2024-07-01 16:57:04 +00:00
涵曦
87b3411f5e feat: 尝试解决触屏版无法播放的问题 2024-07-01 15:28:57 +00:00
27 changed files with 1580 additions and 527 deletions

View File

@@ -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

8
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,8 @@
repos:
- hooks:
- id: commitizen
- id: commitizen-branch
stages:
- push
repo: https://github.com/commitizen-tools/commitizen
rev: v3.27.0

389
CHANGELOG.md Normal file
View File

@@ -0,0 +1,389 @@
## v0.2.0 (2024-07-09)
### Feat
- 触屏版可以不用设置 XIAOMUSIC_USE_MUSIC_API
- 升级依赖库
- 唤醒口令配置支持配语音词,简化自定义口令配置 see #105
## v0.1.101 (2024-07-07)
### Fix
- #81 修复播放列表时,当前歌曲不在列表没有更换歌曲的问题
- #110 修复配置加载问题
## v0.1.100 (2024-07-07)
### Fix
- 日志代码写错
## v0.1.99 (2024-07-07)
### Fix
- #81 修复播放列表没有继续播放上次播放的歌曲,并把随机播放,全部循环,单曲循环状态落地
## v0.1.98 (2024-07-07)
### Fix
- 修复多设备获取不到对话记录的问题 see #65
- #93 修复目录深度设置后导致目录下的歌曲无法加到播放列表里的问题
## v0.1.97 (2024-07-06)
### Fix
- 修复网页控制台设置页面保存报错
## v0.1.96 (2024-07-06)
### Feat
- 使用commitizen管理版本号
- 页面版本号链接到CHANGELOG页面
- 规范版本管理
## v0.1.95 (2024-07-06)
## v0.1.94 (2024-07-06)
### Feat
- 优化多设备接口执行效果,尽量做到同时执行
### Fix
- 新增参数配置强制打断小爱说话
- 修复多设备获取对话记录的问题
- 修复windows下路径分隔符被视为转移符导致音箱无法播放音乐的问题
- 修复播放链接报错
- 修复配置页面默认配置被置空的问题
## v0.1.93 (2024-07-05)
### Feat
- 访问账号密码默认为空
- 支持下载的目录与本地音乐目录分开 see #98
- 新增m4a文件格式支持
- 设置页面支持配置多设备
- 默认用空的后台账号和密码
- 支持多个设备同时播放 see #65
- 新增自定义口令功能 #105
### Fix
- 修复设置页面没成功初始化设置问题
- 修复镜像缺少文件问题
- 尝试解决插件路径问题
- 设置页面日志路径写错了
- 修复口令导致异常关闭的问题
## v0.1.92 (2024-07-04)
### Feat
- 启动参数新增 --port 配置监听端口
- 外网访问端口可独立配置
- 优化设置页面,新增更多配置项
- 首次保存设置后不需要重启容器
### Fix
- 日志文件配置的环境变量写错了
## v0.1.91 (2024-07-03)
### Fix
- 尝试解决触屏版不能播放的问题
## v0.1.90 (2024-07-02)
### Feat
- 优化触屏版播放页面显示歌曲
## v0.1.89 (2024-07-02)
### Feat
- 尝试解决触屏版无法播放的问题
### Fix
- 播放歌曲写成固定的了
- 播放歌曲时被其他指令打断后没有继续播放
## v0.1.88 (2024-07-02)
### Feat
- 日志里不要输出敏感信息
- 优化下载 ffmpeg 脚本,尝试解决 armv7 环境问题
- 优化日志输出信息
- 尝试解决触屏版无法播放的问题
### Fix
- 是否下载中判断错误导致播放无法自动重新开始播放
- 升级yt-dlp到2024.07.01
- 修复部分型号关机失败的问题
## v0.1.87 (2024-07-01)
### Fix
- 修复XIAOMUSIC_USE_MUSIC_API=true时播放不了的问题
## v0.1.86 (2024-07-01)
### Feat
- 优化 ffmpeg 安装脚本
- 新增调试工具用来调试 player_play_music 接口
- 升级依赖库 MiService
### Fix
- 尝试修复 armv7 的 ffmpeg 问题
- 尝试修复关机失败的问题
- 修复口令不能播放的问题
## v0.1.85 (2024-06-30)
### Feat
- 版本号链接到github的release页面方便查看版本更新日志
### Fix
- 修复电台删除后没有从电台列表中删除的问题
## v0.1.84 (2024-06-30)
### Feat
- config.json 支持更多配置选项
- 新增 XIAOMUSIC_STOP_TTS_MSG 配置关机提示音
## v0.1.83 (2024-06-30)
## v0.1.82 (2024-06-30)
### Feat
- 优化指令匹配规则
## v0.1.81 (2024-06-30)
## v0.1.80 (2024-06-30)
### Fix
- #91 修复下载歌曲报错
## v0.1.79 (2024-06-29)
## v0.1.77 (2024-06-29)
### Fix
- #52 支持配置模糊匹配本地歌曲
## v0.1.76 (2024-06-28)
## v0.1.75 (2024-06-28)
## v0.1.74 (2024-06-28)
## v0.1.73 (2024-06-28)
## v0.1.72 (2024-06-28)
## v0.1.71 (2024-06-28)
### Fix
- #83
## v0.1.70 (2024-06-27)
## v0.1.69 (2024-06-26)
## v0.1.67 (2024-06-26)
## v0.1.66 (2024-06-26)
## v0.1.65 (2024-06-26)
## v0.1.64 (2024-06-26)
## v0.1.62 (2024-06-25)
## v0.1.61 (2024-06-25)
## v0.1.60 (2024-06-25)
## v0.1.58 (2024-06-25)
### Fix
- 登陆失败不阻塞启动
## v0.1.57 (2024-06-24)
## v0.1.56 (2024-06-24)
## v0.1.55 (2024-06-23)
### Fix
- #47 支持配置基础的BaseAuth登录
## v0.1.54 (2024-06-23)
### Fix
- #76 新增XIAOMUSIC_MUSIC_PATH_DEPTH配置生成播放列表的目录深度默认10
- #74 配置目录可以和下载目录分开配置, 新增XIAOMUSIC_CONF_PATH用来设置配置目录不配置时使用下载目录
## v0.1.53 (2024-06-23)
## v0.1.52 (2024-06-21)
## v0.1.51 (2024-06-20)
## v0.1.49 (2024-06-20)
## v0.1.48 (2024-06-16)
## v0.1.47 (2024-06-16)
## v0.1.46 (2024-06-15)
## v0.1.45 (2024-06-15)
## v0.1.44 (2024-06-14)
## v0.1.43 (2024-06-14)
## v0.1.41 (2024-06-14)
## v0.1.40 (2024-06-12)
## v0.1.39 (2024-06-12)
## v0.1.38 (2024-06-12)
### Fix
- #70 下一首歌曲不存在时从播放列表中删除并继续找下一首
## v0.1.37 (2024-06-04)
## v0.1.36 (2024-05-30)
## v0.1.35 (2024-05-30)
### Fix
- #67 没配置did时也允许启动 http 服务
## v0.1.34 (2024-05-19)
## v0.1.33 (2024-05-19)
### Fix
- #50 新增配置页面
- #62
## v0.1.32 (2024-05-17)
## v0.1.31 (2024-05-16)
## v0.1.30 (2024-05-16)
### Fix
- 控制台显示版本号 #59
## v0.1.29 (2024-05-16)
### Fix
- #57 #55
## v0.1.28 (2024-05-16)
## v0.1.27 (2024-05-16)
## v0.1.26 (2024-05-08)
## v0.1.25 (2024-05-06)
## v0.1.24 (2024-04-30)
## v0.1.23 (2024-04-30)
## v0.1.22 (2024-04-30)
## v0.1.21 (2024-04-08)
## v0.1.20 (2024-04-08)
## v0.1.19 (2024-04-04)
## v0.1.18 (2024-02-24)
## v0.1.16 (2024-02-24)
## v0.1.15 (2024-02-03)
## v0.1.14 (2024-02-03)
## v0.1.13 (2024-02-02)
## v0.1.12 (2024-01-30)
### Fix
- set volume failed
## v0.1.11 (2024-01-29)
## v0.1.10 (2024-01-29)
## v0.1.9 (2024-01-28)
### Fix
- arg1 漏修改
## v0.1.8 (2024-01-28)
### Fix
- http server listen host
## v0.1.7 (2024-01-28)
## v0.1.6 (2024-01-28)
## v0.1.5 (2024-01-27)
## v0.1.4 (2024-01-27)
### Fix
- error when play next
## v0.1.3 (2023-10-15)
## v0.1.2 (2023-10-15)
## v0.1.1 (2023-10-14)

View File

@@ -11,6 +11,7 @@ WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/ffmpeg /app/ffmpeg
COPY xiaomusic/ ./xiaomusic/
COPY plugins/ ./plugins/
COPY xiaomusic.py .
ENV XDG_CONFIG_HOME=/config
ENV XIAOMUSIC_HOSTNAME=192.168.2.5

View File

@@ -29,28 +29,19 @@ 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端口映射 ✨✨✨
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
@@ -65,34 +56,33 @@ 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
> pip install -U xiaomusic
> xiaomusic --help
__ __ _ __ __ _
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
XiaoMusic v0.1.81 by: github.com/hanxi
XiaoMusic v0.1.92 by: github.com/hanxi
usage: xiaomusic.py [-h] [--hardware HARDWARE] [--account ACCOUNT]
[--password PASSWORD] [--cookie COOKIE] [--verbose]
[--config CONFIG] [--ffmpeg_location FFMPEG_LOCATION]
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
--hardware HARDWARE 小爱 hardware
--port PORT 监听端口
--hardware HARDWARE 小爱音箱型号
--account ACCOUNT xiaomi account
--password PASSWORD xiaomi password
--cookie COOKIE xiaomi cookie
@@ -105,6 +95,8 @@ options:
其中 `config.json` 文件可以参考 `config-example.json` 文件配置。见 <https://github.com/hanxi/xiaomusic/issues/94>
不修改默认端口 8090 的情况下,只需要执行 `xiaomusic` 即可启动。
## 开发环境运行
- 使用 install_dependencies.sh 下载依赖
@@ -145,8 +137,7 @@ pdm run xiaomusic.py
| ---- | ---------------------------------------------------------------------------------------------- |
| L06A | [小爱音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l06a) |
| L07A | [Redmi小爱音箱 Play](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l7a) |
| S12 | [小米AI音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.s12) |
| S12A | - |
| 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 | [小爱音箱Play2019款](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx05) |
| L16A | [Xiaomi Sound](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l16a) |
@@ -159,6 +150,7 @@ pdm run xiaomusic.py
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
> 目前应该所有设备类型都已经支持播放,有问题随时反馈。
## 支持音乐格式
@@ -167,6 +159,7 @@ pdm run xiaomusic.py
- wav
- ape
- ogg
- m4a
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
@@ -323,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 接口播放音乐,用于兼容不能播放的型号,v0.1.86 之后的版本可以不用设置。
- ~XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号,目前应该所有型号都已经兼容,可以不用设置这个~
- 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)
## 讨论区
@@ -347,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

View File

@@ -9,8 +9,9 @@
"conf_path": null,
"hostname": "192.168.2.5",
"port": 8090,
"public_port": 0,
"proxy": null,
"search_prefix": "ytsearch:",
"search_prefix": "bilisearch:",
"ffmpeg_location": "./ffmpeg/bin",
"active_cmd": "play,random_play,playlocal,play_music_list,stop",
"exclude_dirs": "@eaDir",
@@ -21,12 +22,60 @@
"music_list_url": "",
"music_list_json": "",
"disable_download": false,
"key_word_dict": {
"播放歌曲": "play",
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "random_play",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"set_volume#": "set_volume",
"get_volume#": "get_volume",
"本地播放歌曲": "playlocal",
"放歌曲": "play",
"暂停": "stop",
"停止": "stop",
"停止播放": "stop",
"测试自定义口令": "exec#code1(\"hello\")",
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
},
"key_match_order": [
"set_volume#",
"get_volume#",
"分钟后关机",
"播放歌曲",
"下一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"刷新列表",
"播放列表",
"播放本地歌曲",
"本地播放歌曲",
"放歌曲",
"暂停",
"停止",
"停止播放",
"测试自定义口令",
"测试链接"
],
"use_music_api": false,
"use_music_audio_id": "1582971365183456177",
"use_music_id": "355454500",
"log_file": "/tmp/xiaomusic.txt",
"fuzzy_match_cutoff": 0.6,
"enable_fuzzy_match": true,
"stop_tts_msg": "收到,再见",
"keywords_playlocal": "播放本地歌曲,本地播放歌曲",
"keywords_play": "播放歌曲,放歌曲",
"keywords_stop": "关机,暂停,停止,停止播放"
"keywords_stop": "关机,暂停,停止,停止播放",
"user_key_word_dict": {
"测试自定义口令": "exec#code1(\"hello\")",
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
}
}

View File

@@ -9,36 +9,42 @@ arch=$(uname -m)
# 输出架构信息
echo "当前系统架构是:$arch"
install_from_build() {
install_from_github() {
pkg=$1
wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz
tar -xvJf $pkg.tar.xz
mv $pkg ffmpeg
mkdir -p ffmpeg/bin
mv $pkg/bin/ffmpeg ffmpeg/bin/
mv $pkg/bin/ffprobe ffmpeg/bin/
}
install_from_apt() {
apt-get update
apt-get install -y ffmpeg
rm -rf /var/lib/apt/lists/*
mkdir -p /app/ffmpeg/bin
ln -s /usr/bin/ffmpeg /app/ffmpeg/bin/ffmpeg
ln -s /usr/bin/ffprobe /app/ffmpeg/bin/ffprobe
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 架构"
pkg=ffmpeg-master-latest-linux64-gpl
install_from_build "$pkg"
install_from_github ffmpeg-master-latest-linux64-gpl
#install_from_ffmpeg ffmpeg-git-amd64-static
;;
arm64 | aarch64)
echo "64位 ARM 架构"
pkg=ffmpeg-master-latest-linuxarm64-gpl
install_from_build "$pkg"
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"
install_from_apt
;;
esac

View File

@@ -1,37 +1,5 @@
#!/bin/bash
set -e
cz bump --check-consistency --increment patch
version_file=./pyproject.toml
init_file=./xiaomusic/__init__.py
# 获取当前版本号
current_version=$(grep -oE "version = \"[0-9]+\.[0-9]+\.[0-9]+\"" $version_file | cut -d'"' -f2)
echo "当前版本号: "$current_version
# 将版本号分割成三部分
major=$(echo $current_version | cut -d'.' -f1)
minor=$(echo $current_version | cut -d'.' -f2)
patch=$(echo $current_version | cut -d'.' -f3)
echo "major: $major"
echo "minor: $minor"
echo "patch: $patch"
# 将补丁号加1
patch=$((patch + 1))
# 生成新版本号
new_version="$major.$minor.$patch"
# 将新版本号写入文件
sed -i "s/version.*/version = \"$new_version\"/g" $version_file
sed -i "s/__version__.*/__version__ = \"$new_version\"/g" $init_file
echo "新版本号:$new_version"
git diff
git add $version_file
git add $init_file
git commit -m "new version v$new_version"
git tag v$new_version
git push -u origin main --tags

229
pdm.lock generated
View File

@@ -2,10 +2,10 @@
# It is not intended for manual editing.
[metadata]
groups = ["default", "lint"]
groups = ["default", "lint", "dev"]
strategy = ["cross_platform"]
lock_version = "4.4.1"
content_hash = "sha256:e7455b13bf13306ccf5ad11781191edb62991d9fbe8f8ce1e61a2f35c713cc2a"
lock_version = "4.4.2"
content_hash = "sha256:eb54172c14fe68ae15ebe610f91827d981b225e4b8c44cf1fa7de1521d6f7453"
[[package]]
name = "aiohttp"
@@ -82,6 +82,16 @@ files = [
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
]
[[package]]
name = "argcomplete"
version = "3.3.0"
requires_python = ">=3.8"
summary = "Bash tab completion for argparse"
files = [
{file = "argcomplete-3.3.0-py3-none-any.whl", hash = "sha256:c168c3723482c031df3c207d4ba8fa702717ccb9fc0bfe4117166c1f537b4a54"},
{file = "argcomplete-3.3.0.tar.gz", hash = "sha256:fd03ff4a5b9e6580569d34b273f741e85cd9e072f3feeeee3eba4891c70eda62"},
]
[[package]]
name = "asgiref"
version = "3.7.2"
@@ -339,6 +349,39 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "commitizen"
version = "3.27.0"
requires_python = ">=3.8"
summary = "Python commitizen client tool"
dependencies = [
"argcomplete<3.4,>=1.12.1",
"charset-normalizer<4,>=2.1.0",
"colorama<0.5.0,>=0.4.1",
"decli<0.7.0,>=0.6.0",
"importlib-metadata<8,>=4.13",
"jinja2>=2.10.3",
"packaging>=19",
"pyyaml>=3.08",
"questionary<3.0,>=2.0",
"termcolor<3,>=1.1",
"tomlkit<1.0.0,>=0.5.3",
]
files = [
{file = "commitizen-3.27.0-py3-none-any.whl", hash = "sha256:11948fa563d5ad5464baf09eaacff3cf8cbade1ca029ed9c4978f2227f033130"},
{file = "commitizen-3.27.0.tar.gz", hash = "sha256:5874d0c7e8e1be3b75b1b0a2269cffe3dd5c843b860d84b0bdbb9ea86e3474b8"},
]
[[package]]
name = "decli"
version = "0.6.2"
requires_python = ">=3.7"
summary = "Minimal, easy-to-use, declarative cli tool"
files = [
{file = "decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed"},
{file = "decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f"},
]
[[package]]
name = "flask"
version = "3.0.3"
@@ -432,6 +475,19 @@ files = [
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
[[package]]
name = "importlib-metadata"
version = "7.2.1"
requires_python = ">=3.8"
summary = "Read metadata from Python packages"
dependencies = [
"zipp>=0.5",
]
files = [
{file = "importlib_metadata-7.2.1-py3-none-any.whl", hash = "sha256:ffef94b0b66046dd8ea2d619b701fe978d9264d38f3998bc4c27ec3b146a87c8"},
{file = "importlib_metadata-7.2.1.tar.gz", hash = "sha256:509ecb2ab77071db5137c655e24ceb3eee66e7bbc6574165d0d114d9fc4bbe68"},
]
[[package]]
name = "itsdangerous"
version = "2.1.2"
@@ -519,7 +575,7 @@ files = [
[[package]]
name = "miservice-fork"
version = "2.6.1"
version = "2.7.0"
requires_python = ">=3.8"
summary = "XiaoMi Cloud Service fork from https://github.com/Yonsm/MiService"
dependencies = [
@@ -528,8 +584,8 @@ dependencies = [
"rich",
]
files = [
{file = "miservice_fork-2.6.1-py3-none-any.whl", hash = "sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39"},
{file = "miservice_fork-2.6.1.tar.gz", hash = "sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c"},
{file = "miservice_fork-2.7.0-py3-none-any.whl", hash = "sha256:8e2c91ebe1fc4b3c63b01cbf1818e5d5833d024fd3a8311970d649ad2d49d6e6"},
{file = "miservice_fork-2.7.0.tar.gz", hash = "sha256:8e87ef6d89adceaf3f7a98242cdae1c9135498d77f2c743ec3871d2e42bcbab8"},
]
[[package]]
@@ -581,6 +637,29 @@ files = [
{file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"},
]
[[package]]
name = "packaging"
version = "24.1"
requires_python = ">=3.8"
summary = "Core utilities for Python packages"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "prompt-toolkit"
version = "3.0.36"
requires_python = ">=3.6.2"
summary = "Library for building powerful interactive command lines in Python"
dependencies = [
"wcwidth",
]
files = [
{file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"},
{file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"},
]
[[package]]
name = "pycparser"
version = "2.21"
@@ -630,10 +709,55 @@ files = [
{file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
]
[[package]]
name = "pyyaml"
version = "6.0.1"
requires_python = ">=3.6"
summary = "YAML parser and emitter for Python"
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "questionary"
version = "2.0.1"
requires_python = ">=3.8"
summary = "Python library to build pretty command line user prompts ⭐️"
dependencies = [
"prompt-toolkit<=3.0.36,>=2.0",
]
files = [
{file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"},
{file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"},
]
[[package]]
name = "requests"
version = "2.31.0"
requires_python = ">=3.7"
version = "2.32.3"
requires_python = ">=3.8"
summary = "Python HTTP for Humans."
dependencies = [
"certifi>=2017.4.17",
@@ -642,8 +766,8 @@ dependencies = [
"urllib3<3,>=1.21.1",
]
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[[package]]
@@ -662,28 +786,48 @@ files = [
[[package]]
name = "ruff"
version = "0.5.0"
version = "0.5.1"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [
{file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"},
{file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"},
{file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"},
{file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"},
{file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"},
{file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"},
{file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"},
{file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"},
{file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"},
{file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"},
{file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"},
{file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"},
{file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"},
{file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"},
{file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"},
{file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"},
{file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"},
{file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"},
]
[[package]]
name = "termcolor"
version = "2.4.0"
requires_python = ">=3.8"
summary = "ANSI color formatting for output in terminal"
files = [
{file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"},
{file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"},
]
[[package]]
name = "tomlkit"
version = "0.12.5"
requires_python = ">=3.7"
summary = "Style preserving TOML library"
files = [
{file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"},
{file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"},
]
[[package]]
@@ -716,6 +860,15 @@ files = [
{file = "waitress-3.0.0.tar.gz", hash = "sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1"},
]
[[package]]
name = "wcwidth"
version = "0.2.13"
summary = "Measures the displayed width of unicode strings in a terminal"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]]
name = "websockets"
version = "12.0"
@@ -832,7 +985,7 @@ files = [
[[package]]
name = "yt-dlp"
version = "2024.5.30.232720.dev0"
version = "2024.7.9"
requires_python = ">=3.8"
summary = "A feature-rich command-line audio/video downloader"
dependencies = [
@@ -841,11 +994,21 @@ dependencies = [
"certifi",
"mutagen",
"pycryptodomex",
"requests<3,>=2.31.0",
"requests<3,>=2.32.2",
"urllib3<3,>=1.26.17",
"websockets>=12.0",
]
files = [
{file = "yt_dlp-2024.5.30.232720.dev0-py3-none-any.whl", hash = "sha256:d6e563a2923807392325722028e7792e35affb694a505617b008195d0d212d2c"},
{file = "yt_dlp-2024.5.30.232720.dev0.tar.gz", hash = "sha256:9e2b177c5b13ea6f54cee1c56a69dd7832d506fba73a2247c6470e7d1952f959"},
{file = "yt_dlp-2024.7.9-py3-none-any.whl", hash = "sha256:b50a595abde523b5cc84d788f97e69c642503bd673ba740f709ebf65b5ec6592"},
{file = "yt_dlp-2024.7.9.tar.gz", hash = "sha256:e19f00f9e55e90bca1c94bcaf809aa33e51634be9f0de2df84a72d3206934f94"},
]
[[package]]
name = "zipp"
version = "3.19.2"
requires_python = ">=3.8"
summary = "Backport of pathlib-compatible object wrapper for zip files"
files = [
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
]

0
plugins/__init__.py Normal file
View File

4
plugins/code1.py Normal file
View 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
View 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}")

View File

@@ -1,16 +1,15 @@
[project]
name = "xiaomusic"
version = "0.1.87"
version = "0.2.0"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},
{name = "涵曦", email = "im.hanxi@gmail.com"},
]
dependencies = [
"requests==2.31.0",
"aiohttp>=3.8.6",
"miservice-fork>=2.5.0",
"miservice-fork>=2.7.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",
@@ -34,19 +33,22 @@ build-backend = "pdm.backend"
lint = [
"ruff>=0.4.8",
]
dev = [
"commitizen>=3.27.0",
]
[tool.ruff]
lint.select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"E", # pycodestyle - Error
"F", # Pyflakes
"I", # isort
"W", # pycodestyle - Warning
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"E", # pycodestyle - Error
"F", # Pyflakes
"I", # isort
"W", # pycodestyle - Warning
"UP", # pyupgrade
]
lint.ignore = [
"E501", # line-too-long
"W191", # tab-indentation
"E501", # line-too-long
"W191", # tab-indentation
]
include = ["**/*.py", "**/*.pyi", "**/pyproject.toml"]
@@ -56,3 +58,14 @@ convention = "google"
[tool.pdm.scripts]
lint = "ruff check ."
fmt = "ruff format ."
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "v$version"
version_scheme = "pep440"
version_provider = "pep621"
update_changelog_on_bump = true
major_version_zero = true
version_files = [
"xiaomusic/__init__.py",
]

View File

@@ -220,12 +220,12 @@ click==8.1.7 \
colorama==0.4.6; platform_system == "Windows" \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
flask==3.0.3 \
--hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3 \
--hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842
flask-HTTPAuth==4.8.0 \
--hash=sha256:66568a05bc73942c65f1e2201ae746295816dc009edd84b482c44c758d75097a \
--hash=sha256:a58fedd09989b9975448eef04806b096a3964a7feeebc0a78831ff55685b62b0
flask[async]==3.0.3 \
--hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3 \
--hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842
frozenlist==1.4.0 \
--hash=sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01 \
--hash=sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251 \
@@ -305,9 +305,9 @@ MarkupSafe==2.1.4 \
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
miservice-fork==2.6.1 \
--hash=sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c \
--hash=sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39
miservice-fork==2.7.0 \
--hash=sha256:8e2c91ebe1fc4b3c63b01cbf1818e5d5833d024fd3a8311970d649ad2d49d6e6 \
--hash=sha256:8e87ef6d89adceaf3f7a98242cdae1c9135498d77f2c743ec3871d2e42bcbab8
multidict==6.0.4 \
--hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \
--hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \
@@ -371,9 +371,9 @@ pycryptodomex==3.19.0 \
pygments==2.16.1 \
--hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
--hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
requests==2.31.0 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
rich==13.7.1 \
--hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \
--hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432
@@ -472,6 +472,6 @@ yarl==1.9.2 \
--hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \
--hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \
--hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560
yt-dlp==2024.5.30.232720.dev0 \
--hash=sha256:9e2b177c5b13ea6f54cee1c56a69dd7832d506fba73a2247c6470e7d1952f959 \
--hash=sha256:d6e563a2923807392325722028e7792e35affb694a505617b008195d0d212d2c
yt-dlp==2024.7.9 \
--hash=sha256:b50a595abde523b5cc84d788f97e69c642503bd673ba740f709ebf65b5ec6592 \
--hash=sha256:e19f00f9e55e90bca1c94bcaf809aa33e51634be9f0de2df84a72d3206934f94

View File

@@ -1 +1 @@
__version__ = "0.1.87"
__version__ = "0.2.0"

View File

@@ -19,10 +19,15 @@ 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",
@@ -56,6 +61,12 @@ def main():
dest="ffmpeg_location",
help="ffmpeg bin path",
)
parser.add_argument(
"--enable_config_example",
dest="enable_config_example",
help="是否输出示例配置文件",
action="store_true",
)
print(LOGO.format(f"XiaoMusic v{__version__} by: github.com/hanxi"))

View File

@@ -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"))
proxy: str | None = os.getenv("XIAOMUSIC_PROXY", None)
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
public_port: int = int(os.getenv("XIAOMUSIC_PUBLIC_PORT", 0)) # 歌曲访问端口
proxy: str = 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,19 +86,23 @@ 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"))
# 开启模糊搜索
@@ -89,12 +110,20 @@ class Config:
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
)
stop_tts_msg: str = os.getenv("XIAOMUSIC_STOP_TTS_MSG", "收到,再见")
enable_config_example: bool = False
keywords_playlocal: str = os.getenv(
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
)
keywords_play: str = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
keywords_stop: str = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止,停止播放")
user_key_word_dict: dict[str, str] = field(
default_factory=default_user_key_word_dict
)
enable_force_stop: bool = (
os.getenv("XIAOMUSIC_ENABLE_FORCE_STOP", "false").lower() == "true"
)
play_type: int = int(os.getenv("XIAOMUSIC_PLAY_TYPE", "2"))
def append_keyword(self, keys, action):
for key in keys.split(","):
@@ -102,17 +131,26 @@ class Config:
if key not in self.key_match_order:
self.key_match_order.append(key)
def append_user_keyword(self):
for k, v in self.user_key_word_dict.items():
self.key_word_dict[k] = v
self.key_match_order.append(k)
def __post_init__(self) -> None:
if self.proxy:
validate_proxy(self.proxy)
self.append_keyword(self.keywords_playlocal, "playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_stop, "stop")
self.append_user_keyword()
# 保存配置到 config-example.json 文件
# with open("config-example.json", "w") as f:
# data = asdict(self)
# json.dump(data, f, ensure_ascii=False, indent=4)
if self.enable_config_example:
with open("config-example.json", "w") as f:
data = asdict(self)
json.dump(data, f, ensure_ascii=False, indent=4)
@classmethod
def from_options(cls, options: argparse.Namespace) -> Config:
@@ -124,12 +162,39 @@ class Config:
config[key] = value
return cls(**config)
@classmethod
def convert_value(cls, k, v, type_hints):
if v is not None and k in type_hints:
expected_type = type_hints[k]
try:
if expected_type is bool:
converted_value = False
if str(v).lower() == "true":
converted_value = True
else:
converted_value = expected_type(v)
return converted_value
except (ValueError, TypeError) as e:
print(f"Error converting {k}:{v} to {expected_type}: {e}")
return None
@classmethod
def read_from_file(cls, config_path: str) -> dict:
result = {}
with open(config_path, "rb") as f:
config = json.load(f)
for key, value in config.items():
if value is not None and key in cls.__dataclass_fields__:
result[key] = value
data = json.load(f)
type_hints = get_type_hints(cls)
for k, v in data.items():
converted_value = cls.convert_value(k, v, type_hints)
if converted_value is not None:
result[k] = converted_value
return result
def update_config(self, data):
type_hints = get_type_hints(self)
for k, v in data.items():
converted_value = self.convert_value(k, v, type_hints)
if converted_value is not None:
setattr(self, k, converted_value)

View File

@@ -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}&timestamp={timestamp}&limit=2"

View File

@@ -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"

69
xiaomusic/plugin.py Normal file
View 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)

View File

@@ -27,13 +27,33 @@ function postJSON() {
}
});
}
</script>
function sendDebugCmd() {
var cmd = $("#cmd").val();
$.ajax({
type: "POST",
url: "/cmd",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({cmd: cmd}),
success: () => {
},
error: () => {
// 请求失败时执行的操作
}
});
}
</script>
</head>
<body>
<h1>Debug For XiaoMusic</h1>
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
<button onclick="postJSON()">提交</button><br>
<hr>
<input id="cmd" type="text"></input>
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
</body>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>

View File

@@ -9,7 +9,7 @@
</head>
<body>
<h2>小爱音箱操控面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/releases">
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
版本未知
</a>)
</h2>

View File

@@ -6,30 +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>小爱音箱设置面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/releases">
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
版本未知
</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>

View File

@@ -5,88 +5,109 @@ $(function(){
$("#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);

View File

@@ -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; /* 方块字体大小,视清晰度需调整 */
}

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
from __future__ import annotations
import copy
import difflib
import os
import random
@@ -100,63 +101,48 @@ def custom_sort_key(s):
return (2, s)
# fork from https://gist.github.com/dougthor42/001355248518bc64d2f8
def walk_to_depth(root, depth=None, *args, **kwargs):
"""
Wrapper around os.walk that stops after going down `depth` folders.
I had my own version, but it wasn't as efficient as
http://stackoverflow.com/a/234329/1354930, so I modified to be more
similar to nosklo's answer.
However, nosklo's answer doesn't work if topdown=False, so I kept my
version.
"""
# Let people use this as a standard `os.walk` function.
if depth is None:
return os.walk(root, *args, **kwargs)
def _get_depth_path(root, directory, depth):
# 计算当前目录的深度
relative_path = root[len(directory) :].strip(os.sep)
path_parts = relative_path.split(os.sep)
if len(path_parts) >= depth:
return os.path.join(directory, *path_parts[:depth])
else:
return root
# remove any trailing separators so that our counts are correct.
root = root.rstrip(os.path.sep)
def main_func(root, depth, *args, **kwargs):
"""Faster because it skips traversing dirs that are too deep."""
root_depth = root.count(os.path.sep)
for dirpath, dirnames, filenames in os.walk(root, *args, **kwargs):
yield (dirpath, dirnames, filenames)
def _append_files_result(result, root, joinpath, files, support_extension):
dir_name = os.path.basename(root)
if dir_name not in result:
result[dir_name] = []
for file in files:
# 过滤隐藏文件
if file.startswith("."):
continue
# 过滤文件后缀
(name, extension) = os.path.splitext(file)
if extension.lower() not in support_extension:
continue
# calculate how far down we are.
current_folder_depth = dirpath.count(os.path.sep)
if current_folder_depth >= root_depth + depth:
del dirnames[:]
result[dir_name].append(os.path.join(joinpath, file))
def fallback_func(root, depth, *args, **kwargs):
"""Slower, but works when topdown is False"""
root_depth = root.count(os.path.sep)
for dirpath, dirnames, filenames in os.walk(root, *args, **kwargs):
current_folder_depth = dirpath.count(os.path.sep)
if current_folder_depth <= root_depth + depth:
yield (dirpath, dirnames, filenames)
# there's gotta be a better way do do this...
try:
if args[0] is False:
yield from fallback_func(root, depth, *args, **kwargs)
return
def traverse_music_directory(
directory, depth=10, exclude_dirs=None, support_extension=None
):
result = {}
for root, dirs, files in os.walk(directory):
# 忽略排除的目录
dirs[:] = [d for d in dirs if d not in exclude_dirs]
# 计算当前目录的深度
current_depth = root[len(directory) :].count(os.sep) + 1
if current_depth > depth:
depth_path = _get_depth_path(root, directory, depth - 1)
_append_files_result(result, depth_path, root, files, support_extension)
else:
yield from main_func(root, depth, *args, **kwargs)
return
except IndexError:
pass
try:
if kwargs["topdown"] is False:
yield from fallback_func(root, depth, *args, **kwargs)
return
else:
yield from main_func(root, depth, *args, **kwargs)
return
except KeyError:
yield from main_func(root, depth, *args, **kwargs)
return
_append_files_result(result, root, root, files, support_extension)
return result
def downloadfile(url):
@@ -240,3 +226,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

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import asyncio
import copy
import json
import logging
import os
@@ -10,11 +9,12 @@ import re
import time
import traceback
import urllib.parse
from dataclasses import asdict
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,14 +29,16 @@ 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_web_music_duration,
parse_cookie_string,
walk_to_depth,
traverse_music_directory,
)
EOF = object()
@@ -51,34 +53,18 @@ 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
# 单曲循环,全部循环
self.play_type = PLAY_TYPE_RND
self.cur_music = ""
self._next_timer = None
self._timeout = 0
@@ -93,6 +79,9 @@ class XiaoMusic:
# 关机定时器
self._stop_timer = None
# 初始化配置
self.init_config()
# 初始化日志
self.setup_logger()
@@ -105,10 +94,46 @@ 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.play_type = self.config.play_type
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
@@ -120,35 +145,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)
@@ -164,61 +191,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_id = h.get("deviceID", "")
hardware = h.get("hardware", "")
did = h.get("miotDID", "")
if device_id and hardware and did and (did in mi_dids):
self.device2hardware[device_id] = hardware
self.did2device[did] = device_id
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):
@@ -229,22 +221,29 @@ 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:
timeout = ClientTimeout(total=15)
hardware = self.device2hardware[device_id]
url = LATEST_ASK_API.format(
hardware=self.config.hardware,
hardware=hardware,
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)
@@ -259,18 +258,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()
@@ -283,32 +285,60 @@ 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()
try:
await self.mina_service.text_to_speech(self.device_id, value)
except Exception as e:
self.log.error(f"Execption {e}")
await self.text_to_speech(value)
# 最大等8秒
sec = min(8, int(len(value) / 3))
await asyncio.sleep(sec)
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
if self._playing and not self.is_downloading():
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(device_id, value)
except Exception as e:
self.log.error(f"Execption {e}")
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):
playing_info = await self.mina_service.player_get_status(self.device_id)
async def get_if_xiaoai_is_playing(self, device_id):
playing_info = await self.mina_service.player_get_status(device_id)
self.log.info(playing_info)
# WTF xiaomi api
is_playing = (
json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1)
@@ -316,26 +346,43 @@ class XiaoMusic:
)
return is_playing
async def stop_if_xiaoai_is_playing(self):
is_playing = await self.get_if_xiaoai_is_playing()
if 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(self.device_id)
self.log.debug(f"force_stop_xiaoai player_stop ret:{ret}")
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}")
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
# 下载歌曲
@@ -353,7 +400,7 @@ class XiaoMusic:
"--audio-format",
"mp3",
"--paths",
self.music_path,
self.download_path,
"-o",
f"{name}.mp3",
"--ffmpeg-location",
@@ -364,7 +411,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}")
@@ -428,46 +476,41 @@ 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):
self._all_music = {}
all_music_by_dir = {}
for root, dirs, filenames in walk_to_depth(
self.music_path, depth=self.music_path_depth
):
dirs[:] = [d for d in dirs if d not in self.exclude_dirs]
self.log.debug("root:%s dirs:%s music_path:%s", root, dirs, self.music_path)
dir_name = os.path.basename(root)
if self.music_path == root:
local_musics = traverse_music_directory(
self.music_path,
depth=self.music_path_depth,
exclude_dirs=self.exclude_dirs,
support_extension=SUPPORT_MUSIC_TYPE,
)
for dir_name, files in local_musics.items():
if len(files) == 0:
continue
if dir_name == os.path.basename(self.music_path):
dir_name = "其他"
if self.music_path != self.download_path and dir_name == os.path.basename(
self.download_path
):
dir_name = "下载"
if dir_name not in all_music_by_dir:
all_music_by_dir[dir_name] = {}
for filename in filenames:
self.log.debug("gen_all_music_list. filename:%s", filename)
# 过滤隐藏文件
if filename.startswith("."):
continue
# 过滤非音乐文件
(name, extension) = os.path.splitext(filename)
self.log.debug(
"gen_all_music_list. filename:%s, name:%s, extension:%s",
filename,
name,
extension,
)
if extension.lower() not in SUPPORT_MUSIC_TYPE:
continue
for file in files:
# 歌曲名字相同会覆盖
self._all_music[name] = os.path.join(root, filename)
filename = os.path.basename(file)
(name, _) = os.path.splitext(filename)
self._all_music[name] = file
all_music_by_dir[dir_name][name] = True
self._play_list = list(self._all_music.keys())
self._cur_play_list = "全部"
self._gen_play_list()
@@ -530,7 +573,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)
@@ -565,7 +608,7 @@ class XiaoMusic:
if self._next_timer:
self._next_timer.cancel()
self.log.info("定时器已取消")
self.log.info("定时器已取消")
self._timeout = sec
@@ -613,12 +656,13 @@ class XiaoMusic:
self.log.info("收到消息:%s 控制面板:%s", query, ctrl_panel)
# 匹配命令
opvalue, oparg = self.match_cmd(query, ctrl_panel)
if not opvalue:
await asyncio.sleep(1)
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:
@@ -629,7 +673,7 @@ class XiaoMusic:
if query in self.config.key_match_order:
opkey = query
opvalue = self.config.key_word_dict.get(opkey)
if ctrl_panel or self._playing:
if ctrl_panel or self.isplaying():
return opvalue
else:
if not self.active_cmd or opvalue in self.active_cmd:
@@ -642,6 +686,10 @@ class XiaoMusic:
opvalue = self.check_full_match_cmd(query, ctrl_panel)
if opvalue:
self.log.info(f"完全匹配指令. query:{query} opvalue:{opvalue}")
# 自定义口令
if opvalue.startswith("exec#"):
code = opvalue.split("#", 1)[1]
return ("exec", code)
return (opvalue, "")
for opkey in self.config.key_match_order:
@@ -664,19 +712,28 @@ class XiaoMusic:
if opkey in KEY_WORD_ARG_BEFORE_DICT:
oparg = argpre
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.ifno(f"不在激活命令中 {opvalue}")
continue
if (
(not ctrl_panel)
and (not self.isplaying())
and self.active_cmd
and (opvalue not in self.active_cmd)
and (opkey not in self.active_cmd)
):
self.log.info(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)
# 判断是否播放下一首歌曲
def check_play_next(self):
# 当前歌曲不在当前播放列表
if self.cur_music not in self._play_list:
return True
# 当前没我在播放的歌曲
if self.cur_music == "":
return True
@@ -688,18 +745,32 @@ class XiaoMusic:
async def play_url(self, **kwargs):
url = kwargs.get("arg1", "")
if self.config.use_music_api:
ret = await self.mina_service.play_by_music_url(self.device_id, url, _type=2)
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.mina_service.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("没开启模糊匹配")
@@ -739,8 +810,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("已经开始播放了")
# 设置下一首歌曲的播放定时器
@@ -778,7 +849,7 @@ class XiaoMusic:
# 下一首
async def play_next(self, **kwargs):
self.log.info("下一首")
self.log.info("开始播放下一首")
name = self.cur_music
self.log.debug("play_next. name:%s, cur_music:%s", name, self.cur_music)
if (
@@ -795,18 +866,21 @@ class XiaoMusic:
# 单曲循环
async def set_play_type_one(self, **kwargs):
self.play_type = PLAY_TYPE_ONE
self.save_play_type()
await self.do_tts("已经设置为单曲循环")
# 全部循环
async def set_play_type_all(self, **kwargs):
self.play_type = PLAY_TYPE_ALL
self._gen_play_list()
self.save_play_type()
await self.do_tts("已经设置为全部循环")
# 随机播放
async def random_play(self, **kwargs):
self.play_type = PLAY_TYPE_RND
self._gen_play_list()
self.save_play_type()
await self.do_tts("已经设置为随机播放")
# 刷新列表
@@ -838,7 +912,8 @@ class XiaoMusic:
if real_name:
self.log.info(f"根据【{list_name}】找到播放列表【{real_name}")
list_name = real_name
self.log.info(f"没找到播放列表【{list_name}")
else:
self.log.info(f"没找到播放列表【{list_name}")
return list_name
# 播放一个播放列表
@@ -858,19 +933,17 @@ class XiaoMusic:
music_name = ""
if len(parts) > 1:
music_name = parts[1]
else:
music_name = self.get_next_music()
await self.play(arg1=music_name)
async def stop(self, **kwargs):
self._playing = False
if kwargs.get("arg1", "") != "notts":
if self.config.stop_tts_msg:
await self.do_tts(self.config.stop_tts_msg)
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:
@@ -893,7 +966,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
@@ -953,28 +1028,50 @@ class XiaoMusic:
# 保存配置并重新启动
async def saveconfig(self, data):
# 配置文件落地
self.do_saveconfig(data)
# 更新配置
self.update_config_from_setting(data)
# 重新初始化
await self.call_main_thread_function(self.reinit)
# 配置文件落地
def do_saveconfig(self, data):
# 默认暂时配置保存到 music 目录下
filename = self.getsettingfile()
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
self.update_config_from_setting(data)
await self.call_main_thread_function(self.reinit)
# 把当前配置落地
def save_cur_config(self):
data = asdict(self.config)
self.do_saveconfig(data)
self.log.info("save_cur_config ok")
# 播放类型落地
def save_play_type(self):
self.config.play_type = self.play_type
self.save_cur_config()
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")
@@ -982,14 +1079,17 @@ class XiaoMusic:
async def getalldevices(self, **kwargs):
did_list = []
hardware_list = []
hardware_data = await self.mina_service.device_list()
for h in hardware_data:
did = h.get("miotDID", "")
if did != "":
did_list.append(did)
hardware = h.get("hardware", "")
if h.get("hardware", "") != "":
hardware_list.append(hardware)
try:
hardware_data = await self.mina_service.device_list()
for h in hardware_data:
did = h.get("miotDID", "")
if did != "":
did_list.append(did)
hardware = h.get("hardware", "")
if h.get("hardware", "") != "":
hardware_list.append(hardware)
except Exception as e:
self.log.error(f"Execption {e}")
alldevices = {
"did_list": did_list,
"hardware_list": hardware_list,
@@ -1016,10 +1116,15 @@ class XiaoMusic:
if arg1 is None:
arg1 = {}
data = arg1
self.log.info(f"debug_play_by_music_url: {data}")
device_id = self.get_one_device()
self.log.info(f"debug_play_by_music_url: {data} {device_id}")
return await self.mina_service.ubus_request(
self.device_id,
device_id,
"player_play_music",
"mediaplayer",
data,
)
async def exec(self, arg1=None):
code = arg1 if arg1 else 'code1("hello")'
await self.plugin_manager.execute_plugin(code)