mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b185d8768 | ||
|
|
202105a11f | ||
|
|
7da80594e3 | ||
|
|
a032a1d50a | ||
|
|
be62d8abc8 | ||
|
|
aaf9f4b6a7 | ||
|
|
bb5d82097e | ||
|
|
d6ba656641 | ||
|
|
742cae0543 | ||
|
|
a4ab1af160 | ||
|
|
86f158532a | ||
|
|
9ea7935cfb | ||
|
|
20c7e14076 | ||
|
|
e10f5b89b6 | ||
|
|
cb0bae5ae7 | ||
|
|
6a583119d0 | ||
|
|
9349070e8b | ||
|
|
01d99dc699 | ||
|
|
f4d9a6c1fd | ||
|
|
1919bc84d9 | ||
|
|
4c1761468f | ||
|
|
c75230a67d | ||
|
|
ee7ffa55cb | ||
|
|
45bbc8af42 | ||
|
|
ab8bf8fa62 | ||
|
|
493cad080e | ||
|
|
eaa159c5cb | ||
|
|
96e3b8c2ff | ||
|
|
794e8dcd06 | ||
|
|
f2675e4340 | ||
|
|
e5059840fb | ||
|
|
f3e57789fa | ||
|
|
c151b826f7 | ||
|
|
1b3ed3b35a | ||
|
|
77a37a9438 | ||
|
|
f22de9f906 | ||
|
|
090e8c3f4c | ||
|
|
aef51fb65d | ||
|
|
a5a3a2dc62 | ||
|
|
ce9adcee7f | ||
|
|
485a42a9a0 | ||
|
|
3754970c84 | ||
|
|
924fbc208b | ||
|
|
c1a2ee4577 | ||
|
|
e84ee5de1e | ||
|
|
0414830539 | ||
|
|
385f23842d | ||
|
|
1deceaa5a5 | ||
|
|
b8f1157e27 | ||
|
|
06558c24b7 | ||
|
|
6bd399b654 | ||
|
|
228d89f1f8 | ||
|
|
e97639302f | ||
|
|
7f4e51be08 | ||
|
|
cdab5fc92d | ||
|
|
6efe498f2a | ||
|
|
0f3f2e47f5 | ||
|
|
3b720b7367 | ||
|
|
9a3e513b6c | ||
|
|
5a8e5dfa82 | ||
|
|
70d9ad93cb | ||
|
|
87b3411f5e |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -28,3 +28,9 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: hanxi/xiaomusic
|
||||
|
||||
8
.pre-commit-config.yaml
Normal file
8
.pre-commit-config.yaml
Normal 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
|
||||
355
CHANGELOG.md
Normal file
355
CHANGELOG.md
Normal file
@@ -0,0 +1,355 @@
|
||||
## 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)
|
||||
@@ -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
|
||||
|
||||
50
README.md
50
README.md
@@ -29,26 +29,17 @@ 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端口
|
||||
|
||||
@@ -65,14 +56,12 @@ 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 方式安装运行
|
||||
|
||||
@@ -84,15 +73,16 @@ services:
|
||||
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
|
||||
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
|
||||
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
|
||||
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 | [小爱音箱Play(2019款)](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx05) |
|
||||
| L16A | [Xiaomi Sound](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l16a) |
|
||||
@@ -159,6 +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 接口播放音乐,用于兼容不能播放的型号,触屏版目前还需要设置这个为 true 。
|
||||
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
|
||||
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
|
||||
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
|
||||
- XIAOMUSIC_ENABLE_FUZZY_MATCH 设为 true 时开启模糊匹配(默认),设为 false 时关闭模糊匹配,支持模糊匹配歌名和歌单名。 具体见 <https://github.com/hanxi/xiaomusic/issues/52>
|
||||
- XIAOMUSIC_FUZZY_MATCH_CUTOFF 设置模糊搜索匹配的最低相似度阈值(默认0.6,可以配0到1直接的小数),越小越模糊,越大越精准。具体见 <https://github.com/hanxi/xiaomusic/issues/52>
|
||||
- XIAOMUSIC_PUBLIC_PORT 用于设置外网端口,当使用反向代理时可以设置为外网端口,XIAOMUSIC_HOSTNAME 设为外网IP或者域名即可。
|
||||
- XIAOMUSIC_DOWNLOAD_PATH 变量可以配置下载目录,默认为空,表示使用 music 目录为下载目录。设置这个目录必须是 music 的子目录,否则刷新列表后会找不到歌曲。具体见 <https://github.com/hanxi/xiaomusic/issues/98>
|
||||
|
||||
## 高级篇
|
||||
|
||||
- 自定义口令功能 <https://github.com/hanxi/xiaomusic/issues/105>
|
||||
- [ ] 缺少一篇教程 [如何写自定义插件](https://github.com/hanxi/xiaomusic/issues/105)
|
||||
|
||||
## 讨论区
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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\")"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
183
pdm.lock
generated
183
pdm.lock
generated
@@ -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"
|
||||
content_hash = "sha256:3631f504ea2c9e450ff20fe555e2ec0143bc315c22ff257e17a992d1e6d3c39d"
|
||||
|
||||
[[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"
|
||||
@@ -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]]
|
||||
@@ -686,6 +810,26 @@ files = [
|
||||
{file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"},
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "typing-extensions"
|
||||
version = "4.9.0"
|
||||
@@ -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.1.232715.dev0"
|
||||
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.1.232715.dev0-py3-none-any.whl", hash = "sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503"},
|
||||
{file = "yt_dlp-2024.7.1.232715.dev0.tar.gz", hash = "sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8"},
|
||||
]
|
||||
|
||||
[[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
0
plugins/__init__.py
Normal file
4
plugins/code1.py
Normal file
4
plugins/code1.py
Normal file
@@ -0,0 +1,4 @@
|
||||
async def code1(arg1):
|
||||
global log, xiaomusic
|
||||
log.info(f"code1:{arg1}")
|
||||
await xiaomusic.do_tts("你好,我是自定义的测试口令")
|
||||
10
plugins/httpget.py
Normal file
10
plugins/httpget.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import requests
|
||||
|
||||
|
||||
def httpget(url):
|
||||
global log
|
||||
|
||||
# 发起请求
|
||||
response = requests.get(url, timeout=5) # 增加超时以避免长时间挂起
|
||||
response.raise_for_status() # 如果响应不是200,引发HTTPError异常
|
||||
log.info(f"httpget url:{url} response:{response.text}")
|
||||
@@ -1,16 +1,15 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.1.87"
|
||||
version = "0.1.97"
|
||||
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",
|
||||
"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",
|
||||
]
|
||||
|
||||
@@ -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.1.232715.dev0 \
|
||||
--hash=sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8 \
|
||||
--hash=sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.87"
|
||||
__version__ = "0.1.97"
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -3,62 +3,79 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import get_type_hints
|
||||
|
||||
from xiaomusic.utils import validate_proxy
|
||||
|
||||
|
||||
# 默认口令
|
||||
DEFAULT_KEY_WORD_DICT = {
|
||||
"播放歌曲": "play",
|
||||
"播放本地歌曲": "playlocal",
|
||||
"关机": "stop",
|
||||
"下一首": "play_next",
|
||||
"单曲循环": "set_play_type_one",
|
||||
"全部循环": "set_play_type_all",
|
||||
"随机播放": "random_play",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"set_volume#": "set_volume",
|
||||
"get_volume#": "get_volume",
|
||||
}
|
||||
def default_key_word_dict():
|
||||
return {
|
||||
"播放歌曲": "play",
|
||||
"播放本地歌曲": "playlocal",
|
||||
"关机": "stop",
|
||||
"下一首": "play_next",
|
||||
"单曲循环": "set_play_type_one",
|
||||
"全部循环": "set_play_type_all",
|
||||
"随机播放": "random_play",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"set_volume#": "set_volume",
|
||||
"get_volume#": "get_volume",
|
||||
}
|
||||
|
||||
|
||||
def default_user_key_word_dict():
|
||||
return {
|
||||
"测试自定义口令": 'exec#code1("hello")',
|
||||
"测试链接": 'exec#httpget("https://github.com/hanxi/xiaomusic")',
|
||||
}
|
||||
|
||||
|
||||
# 命令参数在前面
|
||||
KEY_WORD_ARG_BEFORE_DICT = {
|
||||
"分钟后关机": True,
|
||||
}
|
||||
|
||||
|
||||
# 口令匹配优先级
|
||||
DEFAULT_KEY_MATCH_ORDER = [
|
||||
"set_volume#",
|
||||
"get_volume#",
|
||||
"分钟后关机",
|
||||
"播放歌曲",
|
||||
"下一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"刷新列表",
|
||||
"播放列表",
|
||||
]
|
||||
def default_key_match_order():
|
||||
return [
|
||||
"set_volume#",
|
||||
"get_volume#",
|
||||
"分钟后关机",
|
||||
"播放歌曲",
|
||||
"下一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"刷新列表",
|
||||
"播放列表",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
hardware: str = os.getenv("MI_HARDWARE", "L07A")
|
||||
account: str = os.getenv("MI_USER", "")
|
||||
password: str = os.getenv("MI_PASS", "")
|
||||
mi_did: str = os.getenv("MI_DID", "")
|
||||
mi_did: str = os.getenv("MI_DID", "") # 逗号分割支持多设备
|
||||
hardware: str = os.getenv("MI_HARDWARE", "L07A") # 逗号分割支持多设备
|
||||
cookie: str = ""
|
||||
verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
|
||||
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
|
||||
music_path: str = os.getenv(
|
||||
"XIAOMUSIC_MUSIC_PATH", "music"
|
||||
) # 只能是music目录下的子目录
|
||||
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "")
|
||||
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", None)
|
||||
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
|
||||
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090"))
|
||||
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
|
||||
public_port: int = int(os.getenv("XIAOMUSIC_PUBLIC_PORT", 0)) # 歌曲访问端口
|
||||
proxy: str | None = os.getenv("XIAOMUSIC_PROXY", None)
|
||||
search_prefix: str = os.getenv(
|
||||
"XIAOMUSIC_SEARCH", "ytsearch:"
|
||||
"XIAOMUSIC_SEARCH", "bilisearch:"
|
||||
) # "bilisearch:" or "ytsearch:"
|
||||
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
|
||||
active_cmd: str = os.getenv(
|
||||
@@ -69,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,19 @@ 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"
|
||||
)
|
||||
|
||||
def append_keyword(self, keys, action):
|
||||
for key in keys.split(","):
|
||||
@@ -102,17 +130,26 @@ class Config:
|
||||
if key not in self.key_match_order:
|
||||
self.key_match_order.append(key)
|
||||
|
||||
def append_user_keyword(self):
|
||||
for k, v in self.user_key_word_dict.items():
|
||||
self.key_word_dict[k] = v
|
||||
self.key_match_order.append(k)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.proxy:
|
||||
validate_proxy(self.proxy)
|
||||
|
||||
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:
|
||||
@@ -133,3 +170,34 @@ class Config:
|
||||
if value is not None and key in cls.__dataclass_fields__:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def update_config(self, data):
|
||||
# 获取类型提示
|
||||
type_hints = get_type_hints(self)
|
||||
|
||||
for k, v in data.items():
|
||||
if v and k in type_hints:
|
||||
# 获取字段的类型
|
||||
expected_type = type_hints[k]
|
||||
|
||||
# 根据期望的类型进行转换
|
||||
if isinstance(v, expected_type):
|
||||
# 如果v已经是正确的类型,则直接赋值
|
||||
setattr(self, k, v)
|
||||
else:
|
||||
# 尝试转换类型
|
||||
try:
|
||||
# 特殊情况处理(例如对布尔值的转换)
|
||||
if expected_type is bool:
|
||||
converted_value = False
|
||||
if v and v.lower() == "true":
|
||||
converted_value = True
|
||||
else:
|
||||
# 使用期望类型的构造函数进行转换
|
||||
converted_value = expected_type(v)
|
||||
except (ValueError, TypeError) as e:
|
||||
print(f"Error converting {v} to {expected_type}: {e}")
|
||||
continue
|
||||
|
||||
# 设置转换后的值
|
||||
setattr(self, k, converted_value)
|
||||
|
||||
@@ -4,6 +4,7 @@ SUPPORT_MUSIC_TYPE = [
|
||||
".wav",
|
||||
".ape",
|
||||
".ogg",
|
||||
".m4a",
|
||||
]
|
||||
|
||||
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={timestamp}&limit=2"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
from dataclasses import asdict
|
||||
from threading import Thread
|
||||
|
||||
from flask import Flask, request, send_file, send_from_directory
|
||||
@@ -10,6 +11,7 @@ from xiaomusic import (
|
||||
__version__,
|
||||
)
|
||||
from xiaomusic.utils import (
|
||||
deepcopy_data_no_sensitive_info,
|
||||
downloadfile,
|
||||
)
|
||||
|
||||
@@ -98,20 +100,11 @@ async def do_cmd():
|
||||
@auth.login_required
|
||||
async def getsetting():
|
||||
config = xiaomusic.getconfig()
|
||||
log.debug(config)
|
||||
|
||||
data = asdict(config)
|
||||
alldevices = await xiaomusic.call_main_thread_function(xiaomusic.getalldevices)
|
||||
log.info(alldevices)
|
||||
data = {
|
||||
"mi_did": config.mi_did,
|
||||
"mi_did_list": alldevices["did_list"],
|
||||
"mi_hardware": config.hardware,
|
||||
"mi_hardware_list": alldevices["hardware_list"],
|
||||
"xiaomusic_search": config.search_prefix,
|
||||
"xiaomusic_proxy": config.proxy,
|
||||
"xiaomusic_music_list_url": config.music_list_url,
|
||||
"xiaomusic_music_list_json": config.music_list_json,
|
||||
}
|
||||
log.info(f"getsetting alldevices: {alldevices}")
|
||||
data["mi_did_list"] = alldevices["did_list"]
|
||||
data["mi_hardware_list"] = alldevices["hardware_list"]
|
||||
return data
|
||||
|
||||
|
||||
@@ -119,7 +112,8 @@ async def getsetting():
|
||||
@auth.login_required
|
||||
async def savesetting():
|
||||
data = request.get_json()
|
||||
log.info(data)
|
||||
debug_data = deepcopy_data_no_sensitive_info(data)
|
||||
log.info(f"saveconfig: {debug_data}")
|
||||
await xiaomusic.saveconfig(data)
|
||||
return "save success"
|
||||
|
||||
|
||||
69
xiaomusic/plugin.py
Normal file
69
xiaomusic/plugin.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, xiaomusic, plugin_dir="plugins"):
|
||||
self.xiaomusic = xiaomusic
|
||||
self.log = xiaomusic.log
|
||||
self._funcs = {}
|
||||
self._load_plugins(plugin_dir)
|
||||
|
||||
def _load_plugins(self, plugin_dir):
|
||||
# 假设 plugins 已经在搜索路径上
|
||||
package_name = plugin_dir
|
||||
package = importlib.import_module(package_name)
|
||||
|
||||
# 遍历 package 中所有模块并动态导入它们
|
||||
for _, modname, _ in pkgutil.iter_modules(package.__path__, package_name + "."):
|
||||
# 跳过__init__文件
|
||||
if modname.endswith("__init__"):
|
||||
continue
|
||||
module = importlib.import_module(modname)
|
||||
# 将 log 和 xiaomusic 注入模块的命名空间
|
||||
module.log = self.log
|
||||
module.xiaomusic = self.xiaomusic
|
||||
|
||||
# 动态获取模块中与文件名同名的函数
|
||||
function_name = modname.split(".")[-1] # 从模块全名提取函数名
|
||||
if hasattr(module, function_name):
|
||||
self._funcs[function_name] = getattr(module, function_name)
|
||||
else:
|
||||
self.log.error(
|
||||
f"No function named '{function_name}' found in module {modname}"
|
||||
)
|
||||
|
||||
def get_func(self, plugin_name):
|
||||
"""根据插件名获取插件函数"""
|
||||
return self._funcs.get(plugin_name)
|
||||
|
||||
def get_local_namespace(self):
|
||||
"""返回包含所有插件函数的字典,可以用作 exec 要执行的代码的命名空间"""
|
||||
return self._funcs.copy()
|
||||
|
||||
async def execute_plugin(self, code):
|
||||
"""
|
||||
执行指定的插件代码。插件函数可以是同步或异步。
|
||||
:param code: 需要执行的插件函数代码(例如 'plugin1("hello")')
|
||||
"""
|
||||
# 分解代码字符串以获取函数名
|
||||
func_name = code.split("(")[0]
|
||||
|
||||
# 根据解析出的函数名从插件字典中获取函数
|
||||
plugin_func = self.get_func(func_name)
|
||||
|
||||
if not plugin_func:
|
||||
raise ValueError(f"No plugin function named '{func_name}' found.")
|
||||
|
||||
# 检查函数是否是异步函数
|
||||
global_namespace = globals().copy()
|
||||
local_namespace = self.get_local_namespace()
|
||||
if inspect.iscoroutinefunction(plugin_func):
|
||||
# 如果是异步函数,构建执行用的协程对象
|
||||
coroutine = eval(code, global_namespace, local_namespace)
|
||||
# 等待协程执行
|
||||
await coroutine
|
||||
else:
|
||||
# 如果是普通函数,直接执行代码
|
||||
eval(code, global_namespace, local_namespace)
|
||||
@@ -27,13 +27,33 @@ function postJSON() {
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
function sendDebugCmd() {
|
||||
var cmd = $("#cmd").val();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/cmd",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({cmd: cmd}),
|
||||
success: () => {
|
||||
},
|
||||
error: () => {
|
||||
// 请求失败时执行的操作
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Debug For XiaoMusic</h1>
|
||||
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
|
||||
<button onclick="postJSON()">提交</button><br>
|
||||
|
||||
<hr>
|
||||
<input id="cmd" type="text"></input>
|
||||
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
|
||||
</body>
|
||||
|
||||
<footer>
|
||||
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
||||
</footer>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -77,3 +77,41 @@ footer {
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
/* 隐藏原生复选框 */
|
||||
.custom-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 修改后的自定义复选框外观 */
|
||||
.checkbox-label {
|
||||
display: inline-block;
|
||||
width: 200px; /* 宽度 */
|
||||
height: 20px; /* 高度 */
|
||||
background-color: #fff; /* 背景颜色 */
|
||||
border: 0px solid #ccc; /* 边框 */
|
||||
border-radius: 3px; /* 圆角边框 */
|
||||
position: relative; /* 设置相对定位 */
|
||||
cursor: pointer; /* 鼠标形状 */
|
||||
padding-left: 40px; /* 给左边的复选框图标留下空位 */
|
||||
}
|
||||
|
||||
/* 对勾的样式 */
|
||||
.custom-checkbox:checked + .checkbox-label::after {
|
||||
content: '✔';
|
||||
position: absolute;
|
||||
left: 10px; /* 对勾图标靠左侧位置 */
|
||||
color: #000; /* 对勾颜色 */
|
||||
font-size: 18px; /* 对勾字体大小,视清晰度需调整 */
|
||||
}
|
||||
|
||||
/* 标签文本样式,使用 ::before 伪元素表示复选框未选中时的样式 */
|
||||
.custom-checkbox + .checkbox-label::before {
|
||||
content: '⬜'; /* 表示未选中时的复选框样式,这里用了白色方块 */
|
||||
position: absolute;
|
||||
left: 8px; /* 方块图标靠左侧位置 */
|
||||
top: 1px; /* 方块图标顶部位置 */
|
||||
color: #000; /* 方块颜色 */
|
||||
font-size: 18px; /* 方块字体大小,视清晰度需调整 */
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import difflib
|
||||
import os
|
||||
import random
|
||||
@@ -240,3 +241,30 @@ def get_local_music_duration(filename):
|
||||
|
||||
def get_random(length):
|
||||
return "".join(random.sample(string.ascii_letters + string.digits, length))
|
||||
|
||||
|
||||
# 深拷贝把敏感数据设置位*
|
||||
def deepcopy_data_no_sensitive_info(data, fields_to_anonymize=None):
|
||||
if fields_to_anonymize is None:
|
||||
fields_to_anonymize = [
|
||||
"account",
|
||||
"password",
|
||||
"httpauth_username",
|
||||
"httpauth_password",
|
||||
]
|
||||
|
||||
copy_data = copy.deepcopy(data)
|
||||
|
||||
# 检查copy_data是否是字典或具有属性的对象
|
||||
if isinstance(copy_data, dict):
|
||||
# 对字典进行处理
|
||||
for field in fields_to_anonymize:
|
||||
if field in copy_data:
|
||||
copy_data[field] = "******"
|
||||
else:
|
||||
# 对对象进行处理
|
||||
for field in fields_to_anonymize:
|
||||
if hasattr(copy_data, field):
|
||||
setattr(copy_data, field, "******")
|
||||
|
||||
return copy_data
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -14,7 +13,7 @@ from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from miservice import MiAccount, MiIOService, MiNAService
|
||||
from miservice import MiAccount, MiNAService
|
||||
|
||||
from xiaomusic import (
|
||||
__version__,
|
||||
@@ -29,8 +28,10 @@ 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,
|
||||
@@ -51,29 +52,15 @@ class XiaoMusic:
|
||||
self.config = config
|
||||
|
||||
self.mi_token_home = Path.home() / ".mi.token"
|
||||
self.last_timestamp = int(time.time() * 1000) # timestamp last call mi speaker
|
||||
self.last_timestamp = {} # timestamp last call mi speaker
|
||||
self.last_record = None
|
||||
self.cookie_jar = None
|
||||
self.device_id = ""
|
||||
self.mina_service = None
|
||||
self.miio_service = None
|
||||
self.polling_event = asyncio.Event()
|
||||
self.new_record_event = asyncio.Event()
|
||||
self.queue = queue.Queue()
|
||||
|
||||
self.music_path = config.music_path
|
||||
self.conf_path = config.conf_path
|
||||
if not self.conf_path:
|
||||
self.conf_path = config.music_path
|
||||
|
||||
self.hostname = config.hostname
|
||||
self.port = config.port
|
||||
self.proxy = config.proxy
|
||||
self.search_prefix = config.search_prefix
|
||||
self.ffmpeg_location = config.ffmpeg_location
|
||||
self.active_cmd = config.active_cmd.split(",")
|
||||
self.exclude_dirs = set(config.exclude_dirs.split(","))
|
||||
self.music_path_depth = config.music_path_depth
|
||||
self.device2hardware = {}
|
||||
self.did2device = {}
|
||||
|
||||
# 下载对象
|
||||
self.download_proc = None
|
||||
@@ -93,6 +80,9 @@ class XiaoMusic:
|
||||
# 关机定时器
|
||||
self._stop_timer = None
|
||||
|
||||
# 初始化配置
|
||||
self.init_config()
|
||||
|
||||
# 初始化日志
|
||||
self.setup_logger()
|
||||
|
||||
@@ -105,10 +95,44 @@ class XiaoMusic:
|
||||
# 启动时初始化获取声音
|
||||
self.set_last_record("get_volume#")
|
||||
|
||||
# 初始化插件
|
||||
self.plugin_manager = PluginManager(self)
|
||||
|
||||
debug_config = deepcopy_data_no_sensitive_info(self.config)
|
||||
self.log.info(f"Startup OK. {debug_config}")
|
||||
|
||||
def init_config(self):
|
||||
self.music_path = self.config.music_path
|
||||
self.conf_path = self.config.conf_path
|
||||
if not self.conf_path:
|
||||
self.conf_path = self.config.music_path
|
||||
self.download_path = self.config.download_path
|
||||
if not self.download_path:
|
||||
self.download_path = self.music_path
|
||||
|
||||
if not os.path.exists(self.download_path):
|
||||
os.makedirs(self.download_path)
|
||||
|
||||
self.hostname = self.config.hostname
|
||||
self.port = self.config.port
|
||||
self.public_port = self.config.public_port
|
||||
if self.public_port == 0:
|
||||
self.public_port = self.port
|
||||
|
||||
self.proxy = self.config.proxy
|
||||
self.search_prefix = self.config.search_prefix
|
||||
self.ffmpeg_location = self.config.ffmpeg_location
|
||||
self.active_cmd = self.config.active_cmd.split(",")
|
||||
self.exclude_dirs = set(self.config.exclude_dirs.split(","))
|
||||
self.music_path_depth = self.config.music_path_depth
|
||||
|
||||
def setup_logger(self):
|
||||
log_format = f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s"
|
||||
date_format = "[%X]"
|
||||
formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
logging.basicConfig(
|
||||
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
|
||||
datefmt="[%X]",
|
||||
format=log_format,
|
||||
datefmt=date_format,
|
||||
)
|
||||
|
||||
log_file = self.config.log_file
|
||||
@@ -120,35 +144,37 @@ class XiaoMusic:
|
||||
handler = RotatingFileHandler(
|
||||
self.config.log_file, maxBytes=10 * 1024 * 1024, backupCount=1
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
self.log = logging.getLogger("xiaomusic")
|
||||
self.log.addHandler(handler)
|
||||
self.log.setLevel(logging.DEBUG if self.config.verbose else logging.INFO)
|
||||
debug_config = copy.deepcopy(self.config)
|
||||
debug_config.account = "******"
|
||||
debug_config.password = "******"
|
||||
debug_config.httpauth_username = "******"
|
||||
debug_config.httpauth_password = "******"
|
||||
self.log.info(debug_config)
|
||||
|
||||
async def poll_latest_ask(self):
|
||||
async with ClientSession() as session:
|
||||
session._cookie_jar = self.cookie_jar
|
||||
while True:
|
||||
self.log.debug(
|
||||
"Listening new message, timestamp: %s", self.last_timestamp
|
||||
f"Listening new message, timestamp: {self.last_timestamp}"
|
||||
)
|
||||
await self.get_latest_ask_from_xiaoai(session)
|
||||
session._cookie_jar = self.cookie_jar
|
||||
|
||||
# 拉取所有音箱的对话记录
|
||||
tasks = [
|
||||
self.get_latest_ask_from_xiaoai(session, device_id)
|
||||
for device_id in self.device2hardware
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
start = time.perf_counter()
|
||||
self.log.debug("Polling_event, timestamp: %s", self.last_timestamp)
|
||||
self.log.debug(f"Polling_event, timestamp: {self.last_timestamp}")
|
||||
await self.polling_event.wait()
|
||||
if (d := time.perf_counter() - start) < 1:
|
||||
# sleep to avoid too many request
|
||||
self.log.debug("Sleep %f, timestamp: %s", d, self.last_timestamp)
|
||||
self.log.debug(f"Sleep {d}, timestamp: {self.last_timestamp}")
|
||||
await asyncio.sleep(1 - d)
|
||||
|
||||
async def init_all_data(self, session):
|
||||
await self.login_miboy(session)
|
||||
await self._init_data_hardware()
|
||||
await self.try_update_device_id()
|
||||
cookie_jar = self.get_cookie()
|
||||
if cookie_jar:
|
||||
session.cookie_jar.update_cookies(cookie_jar)
|
||||
@@ -164,61 +190,26 @@ class XiaoMusic:
|
||||
# Forced login to refresh to refresh token
|
||||
await account.login("micoapi")
|
||||
self.mina_service = MiNAService(account)
|
||||
self.miio_service = MiIOService(account)
|
||||
|
||||
async def try_update_device_id(self):
|
||||
# fix multi xiaoai problems we check did first
|
||||
# why we use this way to fix?
|
||||
# some videos and articles already in the Internet
|
||||
# we do not want to change old way, so we check if miotDID in `env` first
|
||||
# to set device id
|
||||
|
||||
try:
|
||||
mi_dids = self.config.mi_did.split(",")
|
||||
hardware_data = await self.mina_service.device_list()
|
||||
self.device2hardware = {}
|
||||
self.did2device = {}
|
||||
for h in hardware_data:
|
||||
if did := self.config.mi_did:
|
||||
if h.get("miotDID", "") == str(did):
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if h.get("hardware", "") == self.config.hardware:
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
self.log.error(
|
||||
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
|
||||
)
|
||||
device = h.get("deviceID", "")
|
||||
hardware = h.get("hardware", "")
|
||||
did = h.get("miotDID", "")
|
||||
if device and hardware and did and (did in mi_dids):
|
||||
self.device2hardware[device] = hardware
|
||||
self.did2device[did] = device
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
async def _init_data_hardware(self):
|
||||
if self.config.cookie:
|
||||
# if use cookie do not need init
|
||||
return
|
||||
await self.try_update_device_id()
|
||||
if not self.config.mi_did:
|
||||
devices = await self.miio_service.device_list()
|
||||
try:
|
||||
self.config.mi_did = next(
|
||||
d["did"]
|
||||
for d in devices
|
||||
if d["model"].endswith(self.config.hardware.lower())
|
||||
)
|
||||
except StopIteration:
|
||||
self.log.error(
|
||||
f"cannot find did for hardware: {self.config.hardware} "
|
||||
"please set it via MI_DID env"
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption init hardware {e}")
|
||||
|
||||
def get_cookie(self):
|
||||
if self.config.cookie:
|
||||
cookie_jar = parse_cookie_string(self.config.cookie)
|
||||
# set attr from cookie fix #134
|
||||
cookie_dict = cookie_jar.get_dict()
|
||||
self.device_id = cookie_dict["deviceId"]
|
||||
return cookie_jar
|
||||
|
||||
if not os.path.exists(self.mi_token_home):
|
||||
@@ -229,12 +220,18 @@ class XiaoMusic:
|
||||
user_data = json.loads(f.read())
|
||||
user_id = user_data.get("userId")
|
||||
service_token = user_data.get("micoapi")[1]
|
||||
device_id = self.get_one_device()
|
||||
cookie_string = COOKIE_TEMPLATE.format(
|
||||
device_id=self.device_id, service_token=service_token, user_id=user_id
|
||||
device_id=device_id, service_token=service_token, user_id=user_id
|
||||
)
|
||||
return parse_cookie_string(cookie_string)
|
||||
|
||||
async def get_latest_ask_from_xiaoai(self, session):
|
||||
def get_one_device(self):
|
||||
device_id = next(iter(self.device2hardware), "")
|
||||
return device_id
|
||||
|
||||
async def get_latest_ask_from_xiaoai(self, session, device_id):
|
||||
cookies = {"deviceId": device_id}
|
||||
retries = 3
|
||||
for i in range(retries):
|
||||
try:
|
||||
@@ -244,7 +241,7 @@ class XiaoMusic:
|
||||
timestamp=str(int(time.time() * 1000)),
|
||||
)
|
||||
self.log.debug(f"url:{url}")
|
||||
r = await session.get(url, timeout=timeout)
|
||||
r = await session.get(url, timeout=timeout, cookies=cookies)
|
||||
except Exception as e:
|
||||
self.log.warning(
|
||||
"Execption when get latest ask from xiaoai: %s", str(e)
|
||||
@@ -259,18 +256,21 @@ class XiaoMusic:
|
||||
self.log.info("Maybe outof date trying to re init it")
|
||||
await self.init_all_data(self.session)
|
||||
else:
|
||||
return self._get_last_query(data)
|
||||
return self._get_last_query(device_id, data)
|
||||
|
||||
def _get_last_query(self, data):
|
||||
self.log.debug(f"_get_last_query:{data}")
|
||||
def _get_last_query(self, device_id, data):
|
||||
self.log.debug(f"_get_last_query device_id:{device_id} data:{data}")
|
||||
if d := data.get("data"):
|
||||
records = json.loads(d).get("records")
|
||||
if not records:
|
||||
return
|
||||
last_record = records[0]
|
||||
timestamp = last_record.get("time")
|
||||
if timestamp > self.last_timestamp:
|
||||
self.last_timestamp = timestamp
|
||||
# 首次用当前时间初始化
|
||||
if device_id not in self.last_timestamp:
|
||||
self.last_timestamp[device_id] = int(time.time() * 1000)
|
||||
if timestamp > self.last_timestamp[device_id]:
|
||||
self.last_timestamp[device_id] = timestamp
|
||||
self.last_record = last_record
|
||||
self.new_record_event.set()
|
||||
|
||||
@@ -283,32 +283,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 +344,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 +398,7 @@ class XiaoMusic:
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"--paths",
|
||||
self.music_path,
|
||||
self.download_path,
|
||||
"-o",
|
||||
f"{name}.mp3",
|
||||
"--ffmpeg-location",
|
||||
@@ -364,7 +409,8 @@ class XiaoMusic:
|
||||
if self.proxy:
|
||||
sbp_args += ("--proxy", f"{self.proxy}")
|
||||
|
||||
self.log.info(f"download: {sbp_args}")
|
||||
cmd = " ".join(sbp_args)
|
||||
self.log.info(f"download cmd: {cmd}")
|
||||
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
|
||||
await self.do_tts(f"正在下载歌曲{search_key}")
|
||||
|
||||
@@ -428,12 +474,12 @@ class XiaoMusic:
|
||||
self.log.debug("get_music_url web music. name:%s, url:%s", name, url)
|
||||
return url
|
||||
|
||||
filename = self.get_filename(name)
|
||||
filename = self.get_filename(name).replace("\\", "/")
|
||||
self.log.debug(
|
||||
"get_music_url local music. name:%s, filename:%s", name, filename
|
||||
)
|
||||
encoded_name = urllib.parse.quote(filename)
|
||||
return f"http://{self.hostname}:{self.port}/{encoded_name}"
|
||||
return f"http://{self.hostname}:{self.public_port}/{encoded_name}"
|
||||
|
||||
# 递归获取目录下所有歌曲,生成随机播放列表
|
||||
def _gen_all_music_list(self):
|
||||
@@ -530,7 +576,7 @@ class XiaoMusic:
|
||||
|
||||
# 把下载的音乐加入播放列表
|
||||
def add_download_music(self, name):
|
||||
self._all_music[name] = os.path.join(self.music_path, f"{name}.mp3")
|
||||
self._all_music[name] = os.path.join(self.download_path, f"{name}.mp3")
|
||||
if name not in self._play_list:
|
||||
self._play_list.append(name)
|
||||
self.log.debug("add_music %s", name)
|
||||
@@ -565,7 +611,7 @@ class XiaoMusic:
|
||||
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info("定时器已取消")
|
||||
self.log.info("旧定时器已取消")
|
||||
|
||||
self._timeout = sec
|
||||
|
||||
@@ -613,12 +659,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 +676,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 +689,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,15 +715,13 @@ 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 not ctrl_panel and not self.isplaying():
|
||||
if self.active_cmd and opvalue not in self.active_cmd:
|
||||
self.log.ifno(f"不在激活命令中 {opvalue}")
|
||||
continue
|
||||
self.log.info(f"匹配到指令. opkey:{opkey} opvalue:{opvalue} oparg:{oparg}")
|
||||
return (opvalue, oparg)
|
||||
if self._playing:
|
||||
self.log.info("未匹配到指令,自动停止")
|
||||
return ("stop", "notts")
|
||||
self.log.info(f"未匹配到指令 {query} {ctrl_panel}")
|
||||
return (None, None)
|
||||
|
||||
# 判断是否播放下一首歌曲
|
||||
@@ -688,18 +737,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.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 +802,8 @@ class XiaoMusic:
|
||||
self.cur_music = name
|
||||
self.log.info(f"cur_music {self.cur_music}")
|
||||
sec, url = await self.get_music_sec_url(name)
|
||||
self.log.info(f"播放 {url}")
|
||||
await self.force_stop_xiaoai()
|
||||
self.log.info(f"播放 {url}")
|
||||
await self.play_url(arg1=url)
|
||||
self.log.info("已经开始播放了")
|
||||
# 设置下一首歌曲的播放定时器
|
||||
@@ -865,12 +928,12 @@ class XiaoMusic:
|
||||
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 +956,9 @@ class XiaoMusic:
|
||||
await self.do_set_volume(value)
|
||||
|
||||
async def get_volume(self, **kwargs):
|
||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
||||
# 取一个音箱的声音
|
||||
device_id = self.get_one_device()
|
||||
playing_info = await self.mina_service.player_get_status(device_id)
|
||||
self.log.debug("get_volume. playing_info:%s", playing_info)
|
||||
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
|
||||
"volume", 0
|
||||
@@ -961,20 +1026,24 @@ class XiaoMusic:
|
||||
await self.call_main_thread_function(self.reinit)
|
||||
|
||||
def update_config_from_setting(self, data):
|
||||
self.config.mi_did = data.get("mi_did")
|
||||
# 兼容旧配置:一段时间后清理这里的旧代码
|
||||
self.config.hardware = data.get("mi_hardware")
|
||||
self.config.search_prefix = data.get("xiaomusic_search")
|
||||
self.config.proxy = data.get("xiaomusic_proxy")
|
||||
self.config.music_list_url = data.get("xiaomusic_music_list_url")
|
||||
self.config.music_list_json = data.get("xiaomusic_music_list_json")
|
||||
|
||||
self.search_prefix = self.config.search_prefix
|
||||
self.proxy = self.config.proxy
|
||||
self.log.debug("update_config_from_setting ok. data:%s", data)
|
||||
# 自动赋值相同字段的配置
|
||||
self.config.update_config(data)
|
||||
|
||||
self.init_config()
|
||||
debug_config = deepcopy_data_no_sensitive_info(self.config)
|
||||
self.log.info("update_config_from_setting ok. data:%s", debug_config)
|
||||
|
||||
# 重新初始化
|
||||
async def reinit(self, **kwargs):
|
||||
await self.try_update_device_id()
|
||||
self.setup_logger()
|
||||
await self.init_all_data(self.session)
|
||||
self._gen_all_music_list()
|
||||
self.log.info("reinit success")
|
||||
|
||||
@@ -982,14 +1051,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,
|
||||
@@ -1012,14 +1084,62 @@ class XiaoMusic:
|
||||
result = await future
|
||||
return result
|
||||
|
||||
async def debug_play_by_music_url(self, arg1=None):
|
||||
if arg1 is None:
|
||||
arg1 = {}
|
||||
data = arg1
|
||||
self.log.info(f"debug_play_by_music_url: {data}")
|
||||
async def play_by_music_url(self, deviceId, url, _type=2):
|
||||
self.log.info(f"play_by_music_url url:{url}, type:{_type}")
|
||||
audio_type = ""
|
||||
if _type == 1:
|
||||
# If set to MUSIC, the light will be on
|
||||
audio_type = "MUSIC"
|
||||
audio_id = self.config.use_music_audio_id
|
||||
id = self.config.use_music_id
|
||||
music = {
|
||||
"payload": {
|
||||
"audio_type": audio_type,
|
||||
"audio_items": [
|
||||
{
|
||||
"item_id": {
|
||||
"audio_id": audio_id,
|
||||
"cp": {
|
||||
"album_id": "-1",
|
||||
"episode_index": 0,
|
||||
"id": id,
|
||||
"name": "xiaowei",
|
||||
},
|
||||
},
|
||||
"stream": {"url": url},
|
||||
}
|
||||
],
|
||||
"list_params": {
|
||||
"listId": "-1",
|
||||
"loadmore_offset": 0,
|
||||
"origin": "xiaowei",
|
||||
"type": "MUSIC",
|
||||
},
|
||||
},
|
||||
"play_behavior": "REPLACE_ALL",
|
||||
}
|
||||
data = {"startaudioid": audio_id, "music": json.dumps(music)}
|
||||
self.log.info(json.dumps(data))
|
||||
return await self.mina_service.ubus_request(
|
||||
self.device_id,
|
||||
deviceId,
|
||||
"player_play_music",
|
||||
"mediaplayer",
|
||||
data,
|
||||
)
|
||||
|
||||
async def debug_play_by_music_url(self, arg1=None):
|
||||
if arg1 is None:
|
||||
arg1 = {}
|
||||
data = arg1
|
||||
device_id = self.get_one_device()
|
||||
self.log.info(f"debug_play_by_music_url: {data} {device_id}")
|
||||
return await self.mina_service.ubus_request(
|
||||
device_id,
|
||||
"player_play_music",
|
||||
"mediaplayer",
|
||||
data,
|
||||
)
|
||||
|
||||
async def exec(self, arg1=None):
|
||||
code = arg1 if arg1 else 'code1("hello")'
|
||||
await self.plugin_manager.execute_plugin(code)
|
||||
|
||||
Reference in New Issue
Block a user