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

Compare commits

...

104 Commits

Author SHA1 Message Date
涵曦
5619584481 bump: version 0.3.30 → 0.3.31 2024-09-10 16:42:15 +00:00
涵曦
3f0a1cb8f5 build: update static version 2024-09-10 16:42:14 +00:00
涵曦
2f6105843b fix: 修复插件示例报错 #105 2024-09-10 16:24:11 +00:00
涵曦
d71f99de53 feat: 新增播放上一首歌曲功能 #90 2024-09-10 16:13:44 +00:00
涵曦
d7385405d9 fix: 修复当前播放歌曲没保存的问题 #90 2024-09-10 14:38:14 +00:00
涵曦
781e5ebb2f feat: 新增所有歌曲列表 2024-09-10 06:42:16 +00:00
Yan Zenghui
980772bf9c feat: 触屏版显示歌曲名称 (#156)
* feat:触屏版显示歌曲名称

* fix:修改日志级别

* fix:修改歌曲名称获取方式
2024-09-09 17:32:02 +08:00
涵曦
632e411c6e Update README.md 2024-09-08 16:43:51 +08:00
涵曦
789d442029 Update README.md 2024-09-08 16:41:02 +08:00
涵曦
0452d49930 Update README.md 2024-09-08 16:39:28 +08:00
涵曦
19d781fa1f Update README.md 2024-09-08 14:30:40 +08:00
涵曦
ad82d13a7e Update README.md 2024-09-08 14:28:28 +08:00
涵曦
9c4d757dc0 bump: version 0.3.29 → 0.3.30 2024-09-07 14:34:42 +00:00
涵曦
e369d80875 build: update static version 2024-09-07 14:34:41 +00:00
涵曦
9b0c8510a3 feat: 修改设置按钮位置 2024-09-07 13:58:20 +00:00
涵曦
94fb158d7d Update README.md 2024-09-07 10:02:47 +08:00
涵曦
11df6e9f0c Update README.md 2024-09-07 09:51:21 +08:00
涵曦
4e8550a56c Update README.md 2024-09-07 09:39:56 +08:00
涵曦
7f349410a0 feat: 新增网页播放接口 #138 2024-09-07 00:16:55 +00:00
涵曦
6610c29fe4 Update README.md 2024-09-07 07:42:10 +08:00
涵曦
3da1b8eac1 Update README.md 2024-09-07 07:38:07 +08:00
涵曦
003068e62c Update README.md 2024-09-07 07:34:50 +08:00
涵曦
1d12f0d508 Update README.md 2024-09-07 07:29:56 +08:00
涵曦
1ee4667a79 Update README.md 2024-09-07 02:36:16 +08:00
涵曦
521605e9c8 Update README.md 2024-09-07 02:31:35 +08:00
涵曦
9add14408c bump: version 0.3.28 → 0.3.29 2024-09-06 18:07:48 +00:00
涵曦
e554ace7ae build: update static version 2024-09-06 18:07:47 +00:00
涵曦
cdd0cdd237 Update README.md 2024-09-07 01:40:18 +08:00
涵曦
c713d32230 Update README.md 2024-09-07 01:37:49 +08:00
涵曦
8ee4e88f82 Update README.md 2024-09-07 01:34:47 +08:00
涵曦
ca7679e9d3 Update README.md 2024-09-07 01:19:09 +08:00
涵曦
b8c18ef33b feat: 设置页面新增接口文档入口 2024-09-06 17:17:17 +00:00
涵曦
1ce324151e fix: 修复网页开启秘密验证无法播歌的问题 #149 2024-09-06 17:13:35 +00:00
涵曦
faa452253c Update README.md 2024-09-07 00:29:47 +08:00
涵曦
ae41ae57b3 Update README.md 2024-09-04 18:52:57 +08:00
涵曦
6d3fe9381d Update README.md 2024-09-04 18:43:48 +08:00
涵曦
f879c0aeb9 Update release.yml 2024-09-04 06:56:59 +08:00
涵曦
58ffb93d3f Update release.yml 2024-09-04 06:56:38 +08:00
涵曦
8e5df7094c bump: version 0.3.27 → 0.3.28 2024-09-03 16:35:08 +00:00
涵曦
64c2f54ff0 build: update static version 2024-09-03 16:35:07 +00:00
涵曦
d1b869ae43 feat: 新增歌曲收藏功能 #87 2024-09-03 15:55:19 +00:00
涵曦
d3895f2632 refactor: ffmpeg_location 从配置里读取 2024-09-03 14:33:20 +00:00
hui
5bf62c4b1a fix: docker下minetypes无法判断m4a 2024-09-03 18:33:57 +08:00
hui
406e09922c fix:m4a无法正确获取播放时长 2024-09-03 18:33:57 +08:00
hui
ae34572d13 fix:指定文件编码,修复windows下的文件读取错误 2024-09-03 18:33:57 +08:00
涵曦
1e3c69ea90 Update release.yml 2024-09-02 09:09:49 +08:00
涵曦
3c232505f8 bump: version 0.3.26 → 0.3.27 2024-09-02 00:37:48 +00:00
涵曦
44177db9b6 build: update static version 2024-09-02 00:37:47 +00:00
涵曦
e72ae973bc refactor: 处理 code review 问题' 2024-09-01 16:09:33 +00:00
Hi-Jiajun
4ab3c5cbee feat: Add feature as requested in issue #143 2024-09-01 16:09:20 +00:00
涵曦
4e532d298d fix: 默认下载目录修改 2024-08-18 02:39:22 +00:00
涵曦
3372440f4e bump: version 0.3.25 → 0.3.26 2024-08-17 15:00:11 +00:00
涵曦
1255239912 build: update static version 2024-08-17 15:00:10 +00:00
涵曦
e401a73595 feat: 删除网关模式 2024-08-17 11:22:23 +00:00
涵曦
cca6e47da5 bump: version 0.3.24 → 0.3.25 2024-08-16 11:18:21 +00:00
涵曦
415e75d4b4 build: update static version 2024-08-16 11:18:20 +00:00
涵曦
3c5573a2fc feat: 设置页面支持配置 use_music_api 选项 2024-08-16 11:18:11 +00:00
涵曦
7275b59d40 Update README.md 2024-08-08 09:35:17 +08:00
涵曦
a8d0631c33 bump: version 0.3.23 → 0.3.24 2024-08-01 23:47:58 +00:00
涵曦
3cfc96b779 build: update static version 2024-08-01 23:47:57 +00:00
涵曦
489f3f1d6f fix: #131 修复多设备切换时播放模式显示错误问题 2024-08-01 23:21:31 +00:00
涵曦
a5f2fc195e bump: version 0.3.22 → 0.3.23 2024-08-01 16:00:06 +00:00
涵曦
393dbabf4b build: update static version 2024-08-01 16:00:05 +00:00
涵曦
444e697f9d fix: 修复部分文件获取不到播放时长问题 2024-08-01 15:52:20 +00:00
涵曦
cf01039b53 fix: 处理安全问题 2024-08-01 00:46:18 +00:00
涵曦
a8fefc6f82 bump: version 0.3.21 → 0.3.22 2024-08-01 00:23:17 +00:00
涵曦
ae0b6066d9 build: update static version 2024-08-01 00:23:17 +00:00
涵曦
53f5e7db8c feat: 网关模式支持配置,默认关闭 2024-08-01 00:22:48 +00:00
涵曦
2a1fa9f8cf fix: 继续优化延迟问题 2024-08-01 00:05:19 +00:00
涵曦
6e2d674758 bump: version 0.3.20 → 0.3.21 2024-07-30 22:55:35 +00:00
涵曦
3bb6573ec0 build: update static version 2024-07-30 22:55:34 +00:00
涵曦
4ae3774a0e fix: 使用前置网关处理静态文件来加速,尝试解决延迟的问题 2024-07-30 11:49:29 +00:00
涵曦
b34215560c feat: 尝试加个网关在前面处理静态文件来加速文件获取 2024-07-30 11:36:51 +00:00
涵曦
4426017ba8 fix: 播放前先立即暂停之前的音乐 2024-07-30 11:36:35 +00:00
涵曦
f1635f8e32 build: 限定python版本必须是3.10和3.11 2024-07-30 06:41:52 +00:00
涵曦
4de032e193 bump: version 0.3.19 → 0.3.20 2024-07-30 06:09:27 +00:00
涵曦
d655157e1d build: update static version 2024-07-30 06:09:26 +00:00
涵曦
ff06958ab3 fix: 尝试修复延迟问题,修复播放停止不了的问题 2024-07-30 06:09:18 +00:00
涵曦
6e98b5ee2b bump: version 0.3.18 → 0.3.19 2024-07-30 01:59:32 +00:00
涵曦
da90fe2633 build: update static version 2024-07-30 01:59:31 +00:00
涵曦
eaedef452c fix: 调整配置,优化获取歌曲时长接口 2024-07-30 01:09:01 +00:00
涵曦
2d5f3799a3 bump: version 0.3.17 → 0.3.18 2024-07-29 10:16:10 +00:00
涵曦
b52bfe0848 build: update static version 2024-07-29 10:16:09 +00:00
涵曦
e2261b2d19 fix: #135 修复获取不到播放时长时只播放3秒的问题 2024-07-29 10:15:49 +00:00
涵曦
2443444165 Update release.yml 2024-07-29 05:51:04 +08:00
涵曦
7c912a51be bump: version 0.3.16 → 0.3.17 2024-07-28 21:42:49 +00:00
涵曦
bda55a1faa build: update static version 2024-07-28 21:42:48 +00:00
涵曦
5b5f957f8e fix: 优化日志输出,尝试排查延迟播放的问题 2024-07-28 21:42:23 +00:00
涵曦
12f54e3ad4 Update release.yml 2024-07-29 04:48:39 +08:00
quxiaozha
d6594e1270 Fix docker run command 2024-07-29 04:42:02 +08:00
涵曦
b678447417 bump: version 0.3.15 → 0.3.16 2024-07-28 13:58:52 +00:00
涵曦
02508f6997 build: update static version 2024-07-28 13:58:52 +00:00
涵曦
d50fff9e31 build: 修复版本打包问题 2024-07-28 13:58:44 +00:00
涵曦
2d7d7ddc95 bump: version 0.3.14 → 0.3.15 2024-07-28 13:07:36 +00:00
涵曦
9c9825d423 build: update static version 2024-07-28 13:07:35 +00:00
涵曦
f788c0f37b fix: 修复自定义口令重复的问题 2024-07-28 13:07:14 +00:00
涵曦
36d72d1eca fix: 修复日志输出问题 2024-07-28 12:59:32 +00:00
涵曦
6b17779c59 Update release.yml 2024-07-28 17:33:43 +08:00
涵曦
759130e38d fix: 修复退出异常问题 2024-07-28 08:48:39 +00:00
涵曦
49f727477e style: 优化日志输出,方便排查问题 2024-07-28 08:09:34 +00:00
涵曦
43886116c1 bump: version 0.3.13 → 0.3.14 2024-07-28 02:02:09 +00:00
涵曦
a38194027d build: update static version 2024-07-28 02:02:08 +00:00
涵曦
eab4f4bd46 feat: 优化播放延迟问题,并新增配置下一首播放的延迟秒数 2024-07-28 00:51:20 +00:00
涵曦
6b38676766 build: 打包优化 2024-07-26 14:21:43 +00:00
20 changed files with 916 additions and 421 deletions

View File

@@ -6,41 +6,36 @@ permissions:
on:
push:
tags:
- "*"
- "v*"
workflow_dispatch:
jobs:
release-pypi:
name: Build and Release PyPI
pypi-publish:
name: upload release to PyPI
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v4
- uses: actions/setup-node@v4
with:
python-version: "3.10"
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Build artifacts
run: |
pip install build
python -m build
- uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
registry-url: https://registry.npmjs.org/
node-version: lts/*
- run: npx changelogithub
continue-on-error: true
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- uses: pdm-project/setup-pdm@v3
- name: Publish package distributions to PyPI
run: pdm publish
build-image:
runs-on: ubuntu-latest
#needs: release-pypi

View File

@@ -1,3 +1,147 @@
## v0.3.31 (2024-09-10)
### Feat
- 新增播放上一首歌曲功能 #90
- 新增所有歌曲列表
- 触屏版显示歌曲名称 (#156)
### Fix
- 修复插件示例报错 #105
- 修复当前播放歌曲没保存的问题 #90
## v0.3.30 (2024-09-07)
### Feat
- 修改设置按钮位置
- 新增网页播放接口 #138
## v0.3.29 (2024-09-06)
### Feat
- 设置页面新增接口文档入口
### Fix
- 修复网页开启秘密验证无法播歌的问题 #149
## v0.3.28 (2024-09-03)
### Feat
- 新增歌曲收藏功能 #87
### Fix
- docker下minetypes无法判断m4a
### Refactor
- ffmpeg_location 从配置里读取
## v0.3.27 (2024-09-02)
### Feat
- Add feature as requested in issue #143
### Fix
- 默认下载目录修改
### Refactor
- 处理 code review 问题'
## v0.3.26 (2024-08-17)
### Feat
- 删除网关模式
## v0.3.25 (2024-08-16)
### Feat
- 设置页面支持配置 use_music_api 选项
## v0.3.24 (2024-08-01)
### Fix
- #131 修复多设备切换时播放模式显示错误问题
## v0.3.23 (2024-08-01)
### Fix
- 修复部分文件获取不到播放时长问题
- 处理安全问题
## v0.3.22 (2024-08-01)
### Feat
- 网关模式支持配置,默认关闭
### Fix
- 继续优化延迟问题
## v0.3.21 (2024-07-30)
### Feat
- 尝试加个网关在前面处理静态文件来加速文件获取
### Fix
- 使用前置网关处理静态文件来加速,尝试解决延迟的问题
- 播放前先立即暂停之前的音乐
## v0.3.20 (2024-07-30)
### Fix
- 尝试修复延迟问题,修复播放停止不了的问题
## v0.3.19 (2024-07-30)
### Fix
- 调整配置,优化获取歌曲时长接口
## v0.3.18 (2024-07-29)
### Fix
- #135 修复获取不到播放时长时只播放3秒的问题
## v0.3.17 (2024-07-28)
### Fix
- 优化日志输出,尝试排查延迟播放的问题
## v0.3.16 (2024-07-28)
## v0.3.15 (2024-07-28)
### Fix
- 修复自定义口令重复的问题
- 修复日志输出问题
- 修复退出异常问题
## v0.3.14 (2024-07-28)
### Feat
- 优化播放延迟问题,并新增配置下一首播放的延迟秒数
## v0.3.13 (2024-07-24)
### Fix

236
README.md
View File

@@ -13,11 +13,20 @@
<https://github.com/hanxi/xiaomusic>
> 初次安装遇到问题请查阅 <https://github.com/hanxi/xiaomusic/issues/99> 上是否已经有解决办法。
> 初次安装遇到问题请查阅 FAQ: <https://github.com/hanxi/xiaomusic/issues/99> 上是否已经有解决办法。
## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下
已经支持在 web 页面配置其他参数docker 启动命令如下:
```bash
docker run -p 8090:8090 \
-v /xiaomusic/music:/app/music \
-v /xiaomusic/conf:/app/conf \
hanxi/xiaomusic
```
对应的 docker compose 配置如下:
```yaml
services:
@@ -28,24 +37,19 @@ services:
ports:
- 8090:8090
volumes:
- ./music:/app/music
- ./conf:/app/conf
```
对应的 docker 启动命令如下:
```yaml
docker run -p 8090:8090 \
-v ./music:/app/music \
-v ./conf:/app/conf
hanxi/xiaomusic
- /xiaomusic/music:/app/music
- /xiaomusic/conf:/app/conf
```
其中 conf 目录为配置文件存放目录music 目录为音乐存放目录,建议分开配置为不同的目录。
启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表
> 上面配置的 /xiaomusic/music 和 /xiaomusic/conf 是 docker 主机里的 /xiaomusic 目录下的,可以修改为其他目录。如果报错找不到 /xiaomusic/music 目录,可以先执行 `mkdir -p /xiaomusic/{music,conf}` 命令新建目录
### ✨✨✨ 修改默认8090端口映射 ✨✨✨
docker 和 docker compose 二选一即可,启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
> 目前安装步骤已经是最简化了,如果还是嫌安装麻烦,可以微信或者 QQ 约我远程安装,我一般周末和晚上才有时间,收个辛苦费 :moneybag: 50 元一次,安装失败不收费。
### 修改默认8090端口映射
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
@@ -58,11 +62,14 @@ services:
ports:
- 5678:5678
volumes:
- ./music:/app/music
- /xiaomusic/music:/app/music
- /xiaomusic/conf:/app/conf
environment:
XIAOMUSIC_PORT: 5678
```
如果不是首次修改端口,还需要修改 /xiaomusic/conf/setting.json 文件里的端口。
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
> 目前除了 XIAOMUSIC_PORT 只能在启动前配置,其他参数都可以在 web 网页里配置。
@@ -77,7 +84,7 @@ services:
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
XiaoMusic v0.1.92 by: github.com/hanxi
XiaoMusic v0.3.29 by: github.com/hanxi
usage: xiaomusic [-h] [--port PORT] [--hardware HARDWARE] [--account ACCOUNT]
[--password PASSWORD] [--cookie COOKIE] [--verbose]
@@ -105,22 +112,20 @@ options:
- 使用 install_dependencies.sh 下载依赖
- 使用 pdm 安装环境
- 参考 [xiaogpt](https://github.com/yihong0618/xiaogpt) 设置好环境变量
```shell
export MI_USER="xxxxx"
export MI_PASS="xxxx"
export MI_DID=00000
export XIAOMUSIC_SEARCH='bilisearch:'
```
然后启动即可。默认监听了端口 8090 , 使用其他端口自行修改。
- 默认监听了端口 8090 , 使用其他端口自行修改。
```shell
pdm run xiaomusic.py
````
如果是开发前端界面,可以通过 <http://localhost:8090/docs> 查看有什么接口。
如果是开发前端界面,可以通过 <http://localhost:8090/docs>
查看有什么接口。目前的 web 控制台非常简陋,欢迎有兴趣的朋友帮忙实现一个漂亮的前端,需要什么接口可以随时提需求。
### 本地编译 Docker Image
```shell
docker build -t xiaomusic .
```
### 支持口令
@@ -134,6 +139,10 @@ pdm run xiaomusic.py
- 停止播放
- 刷新列表
- 播放列表+列表名 比如:播放列表其他
- 加入收藏
- 取消收藏
- 播放列表收藏
- 播放本地歌曲+歌名
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
@@ -152,13 +161,14 @@ pdm run xiaomusic.py
| LX01 | [小爱音箱mini](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx01) |
| L05B | [小爱音箱Play](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05b) |
| L05C | [小米小爱音箱Play 增强版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05c) |
| L09A | [小米音箱Art](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l09a) |
| LX04 X10A X08A | 已经支持的触屏版 |
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
> 目前应该所有设备类型都已经支持播放,有问题随时反馈。
> 其他触屏版不能播放可以设置 XIAOMUSIC_USE_MUSIC_API true 试试。
> 其他触屏版不能播放可以设置【触屏版兼容模式】选项为 true 试试。见 <https://github.com/hanxi/xiaomusic/issues/30>
## 支持音乐格式
@@ -170,116 +180,8 @@ pdm run xiaomusic.py
- m4a
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
## 在 Docker 里使用
```shell
docker run -e MI_USER='your-xiaomi-account' \
-e MI_PASS='your-xiaomi-password' \
-e MI_DID='your-xiaomi-speaker-mid' \
-e MI_HARDWARE='L07A' \
-e XIAOMUSIC_PROXY='proxy-for-yt-dlp' \
-e XIAOMUSIC_HOSTNAME=192.168.2.5 \
-e XIAOMUSIC_SEARCH='bilisearch:' \
-p 8090:8090 \
-v ./music:/app/music hanxi/xiaomusic
```
- XIAOMUSIC_SEARCH 可以配置为 'bilisearch:' 表示歌曲从哔哩哔哩下载;
- 配置为 'ytsearch:' 表示歌曲从 youtube 下载。
- XIAOMUSIC_PROXY 用于配置代理,默认为空;
- 当 XIAOMUSIC_SEARCH 配置为 'ytsearch:' 时在国内需要用到。
- MI_HARDWARE 是小米音箱的型号,默认为'L07A'
- 注意端口必须映射为与容器内一致, XIAOMUSIC_HOSTNAME 需要设置为宿主机的 IP 地址,否则小爱无法正常播放。
- 可以把 /app/music 目录映射到本地,用于保存下载的歌曲。
XIAOMUSIC_PROXY 参数格式参考 yt-dlp 文档说明:
```
Use the specified HTTP/HTTPS/SOCKS proxy. To
enable SOCKS proxy, specify a proper scheme,
e.g. socks5://user:pass@127.0.0.1:1080/.
Pass in an empty string (--proxy "") for
direct connection
```
<https://github.com/hanxi/xiaomusic/issues/2><https://github.com/hanxi/xiaomusic/issues/11>
### 本地编译Docker Image
```shell
docker build -t xiaomusic .
```
### docker compose 示例
使用哔哩哔哩下载歌曲:
```yaml
version: '3'
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
MI_DID: 00000
MI_HARDWARE: 'L07A'
XIAOMUSIC_SEARCH: 'bilisearch:'
XIAOMUSIC_HOSTNAME: '192.168.2.5'
```
使用 youtobe 下载歌曲:
```yaml
version: '3'
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
MI_DID: 00000
MI_HARDWARE: 'L07A'
XIAOMUSIC_SEARCH: 'ytsearch:'
XIAOMUSIC_PROXY: 'http://192.168.2.5:8080'
XIAOMUSIC_HOSTNAME: '192.168.2.5'
```
如果想让 setting.json 文件不存储到 music 目录,可以这样配,下面的示例会把 setting.json 文件放到容器的 /app/conf 目录且映射到本地的 ./conf 目录:
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
- ./conf:/app/conf
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
XIAOMUSIC_CONF_PATH: '/app/conf'
```
> 已知 L05B L05C LX06 L16A 不支持 flac 格式。
> 如果格式不能播放可以打开【转换为MP3】和【触屏版兼容模式】选项。具体见 <https://github.com/hanxi/xiaomusic/issues/153#issuecomment-2328168689>
## 简易的控制面板
@@ -297,16 +199,9 @@ services:
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER
- MI_PASS
- XIAOMUSIC_HOSTNAME
采用新的设置页面之后,没有必须在启动前配置的环境变量了,除非是改默认的 8090 端口才需要配置环境变量。
其他的这些可以在网页里配置:
- MI_DID
- MI_HARDWARE
- XIAOMUSIC_SEARCH
- XIAOMUSIC_PROXY
后台的
## 网络歌单功能
@@ -316,22 +211,28 @@ services:
## 更多其他可选配置
- XIAOMUSIC_ACTIVE_CMD 环境变量,用于唤醒口令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,具体见 <https://github.com/hanxi/xiaomusic/issues/76>
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台具体见 <https://github.com/hanxi/xiaomusic/issues/47>
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,记得把目录映射到主机,默认为 `/app/config` ,具体见 <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 接口播放音乐,用于兼容不能播放的型号,如果发现需要设置这个选项的时候请告知我加一下设备型号,方便以后不用设置。 见 <https://github.com/hanxi/xiaomusic/issues/30>
- 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>
- XIAOMUSIC_ACTIVE_CMD 环境变量,对应后台的 【允许唤醒的命令】,用于唤醒口令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录,对应后台的 【忽略目录】
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,对应后台的 【目录深度】,具体见 <https://github.com/hanxi/xiaomusic/issues/76>
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台对应后台的 【关闭密码验证】,具体见 <https://github.com/hanxi/xiaomusic/issues/47>
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户,对应后台的 【web控制台账户】
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码,对应后台的 【web控制台密码】
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,对应后台的 【配置文件目录】,记得把目录映射到主机,默认为 `/app/config` ,具体见 <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 接口播放音乐,对应后台的 【触屏版兼容模式】,用于兼容不能播放的型号,如果发现需要设置这个选项的时候请告知我加一下设备型号,方便以后不用设置。 见 <https://github.com/hanxi/xiaomusic/issues/30>
- 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>
- XIAOMUSIC_PROXY 用于配置国内使用 youtube 源下载歌曲时使用的代理,参数格式参考 yt-dlp 文档说明。 见 <https://github.com/hanxi/xiaomusic/issues/2><https://github.com/hanxi/xiaomusic/issues/11>
### :warning: 安全提醒
- 如果配置了公网访问 xiaomusic ,请一定要开启密码登陆,并设置复杂的密码。且不要在公共场所的 WiFi 环境下使用,否则可能造成小米账号密码泄露。
## 高级篇
@@ -355,6 +256,7 @@ services:
- [NAS部署教程](https://post.m.smzdm.com/p/avpe7n99/)
- [群晖部署教程](https://post.m.smzdm.com/p/a7px7dol/)
- [QNAS部署教程](https://post.smzdm.com/p/a5xz5x63/)
- [视频教程](https://www.bilibili.com/video/BV1ZZpweHEtT/)
- 所有帮忙调试和测试的朋友
- 所有反馈问题和建议的朋友
@@ -364,6 +266,6 @@ services:
## 赞赏
- 爱发电 <https://afdian.net/a/imhanxi>
- 点个 Star
- 谢谢 ❤️
- :moneybag: 爱发电 <https://afdian.com/a/imhanxi>
- 点个 Star :star:
- 谢谢 :heart:

View File

@@ -77,5 +77,6 @@
},
"enable_force_stop": false,
"devices": {},
"group_list": ""
}
"group_list": "",
"convert_to_mp3": false
}

47
pdm.lock generated
View File

@@ -5,7 +5,7 @@
groups = ["default", "dev", "lint"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:662037dc1982f7f3592bd691dffc2ffd7b7d7f7b464fa74f716403a03e3a7725"
content_hash = "sha256:0a0b1f63fdd9dd2c4ca2a777f12d294126a880631c1b3d48108d1df283ba14a8"
[[metadata.targets]]
requires_python = "==3.10.12"
@@ -25,14 +25,27 @@ files = [
{file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
]
[[package]]
name = "aiohappyeyeballs"
version = "2.3.4"
requires_python = "<4.0,>=3.8"
summary = "Happy Eyeballs for asyncio"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "aiohappyeyeballs-2.3.4-py3-none-any.whl", hash = "sha256:40a16ceffcf1fc9e142fd488123b2e218abc4188cf12ac20c67200e1579baa42"},
{file = "aiohappyeyeballs-2.3.4.tar.gz", hash = "sha256:7e1ae8399c320a8adec76f6c919ed5ceae6edd4c3672f4d9eae2b27e37c80ff6"},
]
[[package]]
name = "aiohttp"
version = "3.9.5"
version = "3.10.0"
requires_python = ">=3.8"
summary = "Async http client/server framework (asyncio)"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
dependencies = [
"aiohappyeyeballs>=2.3.0",
"aiosignal>=1.1.2",
"async-timeout<5.0,>=4.0; python_version < \"3.11\"",
"attrs>=17.3.0",
@@ -41,8 +54,8 @@ dependencies = [
"yarl<2.0,>=1.0",
]
files = [
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
{file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
{file = "aiohttp-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06ef0135d7ab7fb0284342fbbf8e8ddf73b7fee8ecc55f5c3a3d0a6b765e6d8b"},
{file = "aiohttp-3.10.0.tar.gz", hash = "sha256:e8dd7da2609303e3574c95b0ec9f1fd49647ef29b94701a2862cceae76382e1d"},
]
[[package]]
@@ -976,14 +989,14 @@ files = [
[[package]]
name = "ruff"
version = "0.5.3"
version = "0.5.5"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
groups = ["lint"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "ruff-0.5.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bfe9ab5bdc0b08470c3b261643ad54ea86edc32b64d1e080892d7953add3ad"},
{file = "ruff-0.5.3.tar.gz", hash = "sha256:2a3eb4f1841771fa5b67a56be9c2d16fd3cc88e378bd86aaeaec2f7e6bcdd0a2"},
{file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"},
{file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"},
]
[[package]]
@@ -1094,7 +1107,7 @@ files = [
[[package]]
name = "uvicorn"
version = "0.30.1"
version = "0.30.4"
requires_python = ">=3.8"
summary = "The lightning-fast ASGI server."
groups = ["default"]
@@ -1105,13 +1118,13 @@ dependencies = [
"typing-extensions>=4.0; python_version < \"3.11\"",
]
files = [
{file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"},
{file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"},
{file = "uvicorn-0.30.4-py3-none-any.whl", hash = "sha256:06b00e3087e58c6865c284143c0c42f810b32ff4f265ab19d08c566f74a08728"},
{file = "uvicorn-0.30.4.tar.gz", hash = "sha256:00db9a9e3711a5fa59866e2b02fac69d8dc70ce0814aaec9a66d1d9e5c832a30"},
]
[[package]]
name = "uvicorn"
version = "0.30.1"
version = "0.30.4"
extras = ["standard"]
requires_python = ">=3.8"
summary = "The lightning-fast ASGI server."
@@ -1122,14 +1135,14 @@ dependencies = [
"httptools>=0.5.0",
"python-dotenv>=0.13",
"pyyaml>=5.1",
"uvicorn==0.30.1",
"uvicorn==0.30.4",
"uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"",
"watchfiles>=0.13",
"websockets>=10.4",
]
files = [
{file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"},
{file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"},
{file = "uvicorn-0.30.4-py3-none-any.whl", hash = "sha256:06b00e3087e58c6865c284143c0c42f810b32ff4f265ab19d08c566f74a08728"},
{file = "uvicorn-0.30.4.tar.gz", hash = "sha256:00db9a9e3711a5fa59866e2b02fac69d8dc70ce0814aaec9a66d1d9e5c832a30"},
]
[[package]]
@@ -1345,7 +1358,7 @@ files = [
[[package]]
name = "yt-dlp"
version = "2024.7.16"
version = "2024.8.1"
requires_python = ">=3.8"
summary = "A feature-rich command-line audio/video downloader"
groups = ["default"]
@@ -1361,6 +1374,6 @@ dependencies = [
"websockets>=12.0",
]
files = [
{file = "yt_dlp-2024.7.16-py3-none-any.whl", hash = "sha256:424805a112e757b141e767bc938d49db56d13d6415a92fa4cd8acadd55790be0"},
{file = "yt_dlp-2024.7.16.tar.gz", hash = "sha256:c5bd517a49dea1923ec8e14f51858f10fd89dfece14cb701392b480b41b2f516"},
{file = "yt_dlp-2024.8.1-py3-none-any.whl", hash = "sha256:d0d927038e30a05f6eab26ff6189628456ea21bb159a3d9dc2e855eef2810eac"},
{file = "yt_dlp-2024.8.1.tar.gz", hash = "sha256:4318aa523694611562f01419c8d526b662a72df34ef8ba454016b34c8366c158"},
]

View File

@@ -1,4 +1,5 @@
async def code1(arg1):
global log, xiaomusic
log.info(f"code1:{arg1}")
await xiaomusic.do_tts("你好,我是自定义的测试口令")
did = xiaomusic._cur_did
await xiaomusic.do_tts(did, "你好,我是自定义的测试口令")

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.3.13"
version = "0.3.31"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},
@@ -15,7 +15,7 @@ dependencies = [
"starlette>=0.37.2",
"aiofiles>=24.1.0",
]
requires-python = ">=3.10"
requires-python = ">=3.10,<3.12"
readme = "README.md"
license = {text = "MIT"}

View File

@@ -0,0 +1,32 @@
import math
from xiaomusic.const import (
SUPPORT_MUSIC_TYPE,
)
from xiaomusic.utils import (
get_local_music_duration,
traverse_music_directory,
)
async def test_one_music(filename):
# 获取播放时长
duration = await get_local_music_duration(filename)
sec = math.ceil(duration)
print(f"本地歌曲 : {filename} 的时长 {duration} {sec}")
async def main(directory):
# 获取所有歌曲文件
local_musics = traverse_music_directory(directory, 10, [], SUPPORT_MUSIC_TYPE)
print(local_musics)
for _, files in local_musics.items():
for file in files:
await test_one_music(file)
if __name__ == "__main__":
import asyncio
directory = "./music" # 替换为你的音乐目录路径
asyncio.run(main(directory))

View File

@@ -1 +1 @@
__version__ = "0.3.13"
__version__ = "0.3.31"

View File

@@ -1,5 +1,8 @@
#!/usr/bin/env python3
import argparse
import json
import os
import signal
import uvicorn
@@ -75,29 +78,89 @@ def main():
options = parser.parse_args()
config = Config.from_options(options)
xiaomusic = XiaoMusic(config)
HttpInit(xiaomusic)
from uvicorn.config import LOGGING_CONFIG
LOGGING_CONFIG["formatters"]["access"] = {
"format": f"%(asctime)s [{__version__}] [%(levelname)s] %(filename)s:%(lineno)d: %(message)s",
"datefmt": "[%X]",
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
"datefmt": "[%X]",
"use_colors": False,
},
"access": {
"format": f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
"datefmt": "[%X]",
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
"file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"formatter": "access",
"filename": config.log_file,
"maxBytes": 10 * 1024 * 1024,
"backupCount": 1,
},
},
"loggers": {
"uvicorn": {
"handlers": [
"default",
"file",
],
"level": "INFO",
},
"uvicorn.error": {
"level": "INFO",
},
"uvicorn.access": {
"handlers": [
"access",
"file",
],
"level": "INFO",
"propagate": False,
},
},
}
LOGGING_CONFIG["handlers"]["access"] = {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"formatter": "access",
"filename": config.log_file,
"maxBytes": 10 * 1024 * 1024,
"backupCount": 1,
}
uvicorn.run(
HttpApp,
host=["::", "0.0.0.0"],
port=config.port,
log_config=LOGGING_CONFIG,
)
try:
filename = config.getsettingfile()
with open(filename, encoding="utf-8") as f:
data = json.loads(f.read())
config.update_config(data)
except Exception as e:
print(f"Execption {e}")
def run_server(port):
xiaomusic = XiaoMusic(config)
HttpInit(xiaomusic)
uvicorn.run(
HttpApp,
host=["0.0.0.0", "::"],
port=port,
log_config=LOGGING_CONFIG,
)
def signal_handler(sig, frame):
print("主进程收到退出信号,准备退出...")
os._exit(0) # 退出主进程
# 捕获主进程的退出信号
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
port = int(config.port)
run_server(port)
if __name__ == "__main__":

View File

@@ -16,12 +16,15 @@ def default_key_word_dict():
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"上一首": "play_prev",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "set_random_play",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"加入收藏": "add_to_favorites",
"取消收藏": "del_from_favorites",
}
@@ -44,12 +47,15 @@ def default_key_match_order():
"分钟后关机",
"播放歌曲",
"下一首",
"上一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"刷新列表",
"播放列表",
"加入收藏",
"取消收藏",
]
@@ -74,7 +80,7 @@ class Config:
music_path: str = os.getenv(
"XIAOMUSIC_MUSIC_PATH", "music"
) # 只能是music目录下的子目录
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "")
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "music/download")
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", "conf")
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
@@ -96,6 +102,7 @@ class Config:
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", "")
custom_play_list_json: str = os.getenv("XIAOMUSIC_CUSTOM_PLAY_LIST_JSON", "")
disable_download: bool = (
os.getenv("XIAOMUSIC_DISABLE_DOWNLOAD", "false").lower() == "true"
)
@@ -136,6 +143,8 @@ class Config:
remove_id3tag: bool = (
os.getenv("XIAOMUSIC_REMOVE_ID3TAG", "false").lower() == "true"
)
convert_to_mp3: bool = os.getenv("CONVERT_TO_MP3", "false").lower() == "true"
delay_sec: int = int(os.getenv("XIAOMUSIC_DELAY_SEC", 3)) # 下一首歌延迟播放秒数
def append_keyword(self, keys, action):
for key in keys.split(","):
@@ -146,9 +155,12 @@ class Config:
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)
if k not in self.key_match_order:
self.key_match_order.append(k)
def init_keyword(self):
self.key_match_order = default_key_match_order()
self.key_word_dict = default_key_word_dict()
self.append_keyword(self.keywords_playlocal, "playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_stop, "stop")
@@ -216,3 +228,13 @@ class Config:
if converted_value is not None:
setattr(self, k, converted_value)
self.init_keyword()
# 获取设置文件
def getsettingfile(self):
# 兼容旧配置空的情况
if not self.conf_path:
self.conf_path = "conf"
if not os.path.exists(self.conf_path):
os.makedirs(self.conf_path)
filename = os.path.join(self.conf_path, "setting.json")
return filename

View File

@@ -1,6 +1,8 @@
import asyncio
import json
import mimetypes
import os
import re
import secrets
import shutil
import tempfile
@@ -8,12 +10,14 @@ from contextlib import asynccontextmanager
from dataclasses import asdict
from typing import Annotated
import aiofiles
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.responses import StreamingResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from starlette.background import BackgroundTask
from starlette.responses import FileResponse
from starlette.responses import FileResponse, Response
from xiaomusic import __version__
from xiaomusic.utils import (
@@ -29,9 +33,11 @@ log = None
@asynccontextmanager
async def app_lifespan(app):
if xiaomusic is not None:
task = asyncio.create_task(xiaomusic.run_forever())
asyncio.create_task(xiaomusic.run_forever())
try:
yield
task.cancel()
except Exception as e:
log.exception(f"Execption {e}")
security = HTTPBasic()
@@ -66,7 +72,6 @@ def no_verification():
app = FastAPI(
lifespan=app_lifespan,
version=__version__,
dependencies=[Depends(verification)],
)
@@ -77,14 +82,6 @@ def reset_http_server():
else:
app.dependency_overrides = {}
# 更新 music 链接
app.router.routes = [route for route in app.router.routes if route.path != "/music"]
app.mount(
"/music",
StaticFiles(directory=config.music_path, follow_symlink=True),
name="music",
)
def HttpInit(_xiaomusic):
global xiaomusic, config, log
@@ -98,19 +95,19 @@ def HttpInit(_xiaomusic):
@app.get("/")
async def read_index():
async def read_index(Verifcation=Depends(verification)):
folder = os.path.dirname(__file__)
return FileResponse(f"{folder}/static/index.html")
@app.get("/getversion")
def getversion():
def getversion(Verifcation=Depends(verification)):
log.debug("getversion %s", __version__)
return {"version": __version__}
@app.get("/getvolume")
async def getvolume(did: str = ""):
async def getvolume(did: str = "", Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
return {"volume": 0}
@@ -124,7 +121,7 @@ class DidVolume(BaseModel):
@app.post("/setvolume")
async def setvolume(data: DidVolume):
async def setvolume(data: DidVolume, Verifcation=Depends(verification)):
did = data.did
volume = data.volume
if not xiaomusic.did_exist(did):
@@ -136,12 +133,12 @@ async def setvolume(data: DidVolume):
@app.get("/searchmusic")
def searchmusic(name: str = ""):
def searchmusic(name: str = "", Verifcation=Depends(verification)):
return xiaomusic.searchmusic(name)
@app.get("/playingmusic")
def playingmusic(did: str = ""):
def playingmusic(did: str = "", Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
@@ -160,7 +157,7 @@ class DidCmd(BaseModel):
@app.post("/cmd")
async def do_cmd(data: DidCmd):
async def do_cmd(data: DidCmd, Verifcation=Depends(verification)):
did = data.did
cmd = data.cmd
log.info(f"docmd. did:{did} cmd:{cmd}")
@@ -168,13 +165,18 @@ async def do_cmd(data: DidCmd):
return {"ret": "Did not exist"}
if len(cmd) > 0:
asyncio.create_task(xiaomusic.do_check_cmd(did=did, query=cmd))
try:
await xiaomusic.cancel_all_tasks()
task = asyncio.create_task(xiaomusic.do_check_cmd(did=did, query=cmd))
xiaomusic.append_running_task(task)
except Exception as e:
log.warning(f"Execption {e}")
return {"ret": "OK"}
return {"ret": "Unknow cmd"}
@app.get("/getsetting")
async def getsetting(need_device_list: bool = False):
async def getsetting(need_device_list: bool = False, Verifcation=Depends(verification)):
config = xiaomusic.getconfig()
data = asdict(config)
if need_device_list:
@@ -185,7 +187,7 @@ async def getsetting(need_device_list: bool = False):
@app.post("/savesetting")
async def savesetting(request: Request):
async def savesetting(request: Request, Verifcation=Depends(verification)):
try:
data_json = await request.body()
data = json.loads(data_json.decode("utf-8"))
@@ -203,8 +205,18 @@ async def musiclist(Verifcation=Depends(verification)):
return xiaomusic.get_music_list()
@app.get("/musicinfo")
async def musicinfo(name: str, Verifcation=Depends(verification)):
url = xiaomusic.get_music_url(name)
return {
"ret": "OK",
"name": name,
"url": url,
}
@app.get("/curplaylist")
async def curplaylist(did: str = ""):
async def curplaylist(did: str = "", Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
return ""
return xiaomusic.get_cur_play_list(did)
@@ -215,7 +227,7 @@ class MusicItem(BaseModel):
@app.post("/delmusic")
def delmusic(data: MusicItem):
def delmusic(data: MusicItem, Verifcation=Depends(verification)):
log.info(data)
xiaomusic.del_music(data.name)
return "success"
@@ -226,7 +238,7 @@ class UrlInfo(BaseModel):
@app.post("/downloadjson")
async def downloadjson(data: UrlInfo):
async def downloadjson(data: UrlInfo, Verifcation=Depends(verification)):
log.info(data)
url = data.url
content = ""
@@ -274,7 +286,7 @@ def downloadlog(Verifcation=Depends(verification)):
@app.get("/playurl")
async def playurl(did: str, url: str):
async def playurl(did: str, url: str, Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
@@ -283,7 +295,7 @@ async def playurl(did: str, url: str):
@app.post("/debug_play_by_music_url")
async def debug_play_by_music_url(request: Request):
async def debug_play_by_music_url(request: Request, Verifcation=Depends(verification)):
try:
data = await request.body()
data_dict = json.loads(data.decode("utf-8"))
@@ -291,3 +303,66 @@ async def debug_play_by_music_url(request: Request):
return await xiaomusic.debug_play_by_music_url(arg1=data_dict)
except json.JSONDecodeError as err:
raise HTTPException(status_code=400, detail="Invalid JSON") from err
async def file_iterator(file_path, start, end):
async with aiofiles.open(file_path, mode="rb") as file:
await file.seek(start)
chunk_size = 1024
while start <= end:
read_size = min(chunk_size, end - start + 1)
data = await file.read(read_size)
if not data:
break
start += len(data)
yield data
range_pattern = re.compile(r"bytes=(\d+)-(\d*)")
@app.get("/music/{file_path:path}")
async def music_file(request: Request, file_path: str):
absolute_path = os.path.abspath(config.music_path)
absolute_file_path = os.path.normpath(os.path.join(absolute_path, file_path))
if not absolute_file_path.startswith(absolute_path):
raise HTTPException(status_code=404, detail="File not found")
if not os.path.exists(absolute_file_path):
raise HTTPException(status_code=404, detail="File not found")
file_size = os.path.getsize(absolute_file_path)
range_start, range_end = 0, file_size - 1
range_header = request.headers.get("Range")
log.info(f"music_file range_header {range_header}")
if range_header:
range_match = range_pattern.match(range_header)
if range_match:
range_start = int(range_match.group(1))
if range_match.group(2):
range_end = int(range_match.group(2))
log.info(f"music_file in range {absolute_file_path}")
log.info(f"music_file {range_start} {range_end} {absolute_file_path}")
headers = {
"Content-Range": f"bytes {range_start}-{range_end}/{file_size}",
"Accept-Ranges": "bytes",
}
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
mime_type = "application/octet-stream"
return StreamingResponse(
file_iterator(absolute_file_path, range_start, range_end),
headers=headers,
status_code=206 if range_header else 200,
media_type=mime_type,
)
@app.options("/music/{file_path:path}")
async def music_options():
headers = {
"Accept-Ranges": "bytes",
}
return Response(headers=headers)

View File

@@ -1,6 +1,9 @@
$(function(){
$container=$("#cmds");
append_op_button_name("加入收藏");
append_op_button_name("取消收藏");
const PLAY_TYPE_ONE = 0; // 单曲循环
const PLAY_TYPE_ALL = 1; // 全部循环
const PLAY_TYPE_RND = 2; // 随机播放
@@ -8,9 +11,11 @@ $(function(){
append_op_button("play_type_one", "单曲循环", "单曲循环");
append_op_button("play_type_rnd", "随机播放", "随机播放");
append_op_button_name("刷新列表");
append_op_button_name("下一首");
append_op_button_name("上一首");
append_op_button_name("关机");
append_op_button_name("下一首");
append_op_button_name("刷新列表");
$container.append($("<hr>"));
@@ -53,15 +58,17 @@ $(function(){
.prop('selected', value === did);
$("#did").append(option);
if (cur_device.play_type == PLAY_TYPE_ALL) {
$("#play_type_all").css('background-color', '#b1a8f3');
$("#play_type_all").text('✔️ 全部循环');
} else if (cur_device.play_type == PLAY_TYPE_ONE) {
$("#play_type_one").css('background-color', '#b1a8f3');
$("#play_type_one").text('✔️ 单曲循环');
} else if (cur_device.play_type == PLAY_TYPE_RND) {
$("#play_type_rnd").css('background-color', '#b1a8f3');
$("#play_type_rnd").text('✔️ 随机播放');
if (value === did) {
if (cur_device.play_type == PLAY_TYPE_ALL) {
$("#play_type_all").css('background-color', '#b1a8f3');
$("#play_type_all").text('✔️ 全部循环');
} else if (cur_device.play_type == PLAY_TYPE_ONE) {
$("#play_type_one").css('background-color', '#b1a8f3');
$("#play_type_one").text('✔️ 单曲循环');
} else if (cur_device.play_type == PLAY_TYPE_RND) {
$("#play_type_rnd").css('background-color', '#b1a8f3');
$("#play_type_rnd").text('✔️ 随机播放');
}
}
}
});
@@ -72,6 +79,7 @@ $(function(){
localStorage.setItem('cur_did', did);
window.did = did;
console.log('cur_did', did);
location.reload();
})
});
@@ -92,9 +100,9 @@ $(function(){
$('#music_list').change(function() {
const selectedValue = $(this).val();
localStorage.setItem('cur_playlist', selectedValue);
$('#music_name').empty();
const sorted_musics = data[selectedValue].sort(custom_sort_key);
$.each(sorted_musics, function(index, item) {
$.each(data[selectedValue], function(index, item) {
$('#music_name').append($('<option></option>').val(item).text(item));
});
});
@@ -102,10 +110,17 @@ $(function(){
$('#music_list').trigger('change');
// 获取当前播放列表
$.get(`curplaylist?did=${did}`, function(data, status) {
if (data != "") {
$('#music_list').val(data);
$.get(`curplaylist?did=${did}`, function(playlist, status) {
if (playlist != "") {
$('#music_list').val(playlist);
$('#music_list').trigger('change');
} else {
// 使用本地记录的
playlist = localStorage.getItem('cur_playlist');
if (data.includes(playlist)) {
$('#music_list').val(playlist);
$('#music_list').trigger('change');
}
}
})
})
@@ -124,6 +139,17 @@ $(function(){
sendcmd(cmd);
});
$("#web_play").on("click", () => {
const music_name = $("#music_name").val();
$.get(`/musicinfo?name=${music_name}`, function(data, status) {
console.log(data);
if (data.ret == "OK") {
const music = new Audio(data.url);
music.play();
}
});
});
$("#del_music").on("click", () => {
var del_music_name = $("#music_name").val();
if (confirm(`确定删除歌曲 ${del_music_name} 吗?`)) {
@@ -248,23 +274,4 @@ $(function(){
}
});
}
function custom_sort_key(a, b) {
// 使用正则表达式提取数字前缀
const numericPrefixA = a.match(/^(\d+)/) ? parseInt(a.match(/^(\d+)/)[1], 10) : null;
const numericPrefixB = b.match(/^(\d+)/) ? parseInt(b.match(/^(\d+)/)[1], 10) : null;
// 如果两个键都有数字前缀,则按数字大小排序
if (numericPrefixA !== null && numericPrefixB !== null) {
return numericPrefixA - numericPrefixB;
}
// 如果一个键有数字前缀而另一个没有,则有数字前缀的键排在前面
if (numericPrefixA !== null) return -1;
if (numericPrefixB !== null) return 1;
// 如果两个键都没有数字前缀,则按照常规字符串排序
return a.localeCompare(b);
}
});

View File

@@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width">
<title>Debug For XiaoMusic</title>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1721862430">
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725986534">
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="/static/jquery-3.7.1.min.js?version=1721862430"></script>
<script src="/static/jquery-3.7.1.min.js?version=1725986534"></script>
<script>
var vConsole = new window.VConsole();

View File

@@ -3,9 +3,9 @@
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="/static/jquery-3.7.1.min.js?version=1721862430"></script>
<script src="/static/app.js?version=1721862430"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1721862430">
<script src="/static/jquery-3.7.1.min.js?version=1725986534"></script>
<script src="/static/app.js?version=1725986534"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725986534">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
@@ -29,15 +29,14 @@ var vConsole = new window.VConsole();
</div>
<div id="cmds">
<a class="button" href="/static/setting.html">设置</a>
</div>
<hr>
<div style="margin: 20px;">
<div style="display: flex; align-items: center;">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#8e43e7" style="height: 48px; width: 48px;"><path d="M550.826667 154.666667a47.786667 47.786667 0 0 0-19.84 4.48L298.666667 298.666667H186.453333A80 80 0 0 0 106.666667 378.453333v267.093334A80 80 0 0 0 186.453333 725.333333H298.666667l232.32 139.52a47.786667 47.786667 0 0 0 19.84 4.48A46.506667 46.506667 0 0 0 597.333333 822.826667V201.173333a46.506667 46.506667 0 0 0-46.506666-46.506666zM554.666667 822.826667c0 3.413333-3.84 3.84-3.84 3.84L320 688.853333l-9.6-6.186666H186.453333A37.12 37.12 0 0 1 149.333333 645.546667V378.453333A37.12 37.12 0 0 1 186.453333 341.333333h123.946667l10.24-6.186666 229.546667-137.6s3.84 0 3.84 3.84zM667.52 346.026667a21.333333 21.333333 0 0 0 0 30.293333 192 192 0 0 1 0 271.36 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 234.666667 234.666667 0 0 0 0-331.946666 21.333333 21.333333 0 0 0-30.293333 0z"></path><path d="M804.48 219.52a21.333333 21.333333 0 0 0-30.293333 30.293333 370.986667 370.986667 0 0 1 0 524.373334 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 414.08 414.08 0 0 0 0-584.96z"></path></svg>
<input id="volume" type="range"></input>
<a href="/static/setting.html">
<svg fill="#8e43e7" height="48px" width="48px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-11.88 -11.88 77.76 77.76" xml:space="preserve" stroke="#8e43e7" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)" stroke-width="0.00054"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)"><rect x="-11.88" y="-11.88" width="77.76" height="77.76" rx="18.6624" fill="#addcff" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.512"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M51.22,21h-5.052c-0.812,0-1.481-0.447-1.792-1.197s-0.153-1.54,0.42-2.114l3.572-3.571 c0.525-0.525,0.814-1.224,0.814-1.966c0-0.743-0.289-1.441-0.814-1.967l-4.553-4.553c-1.05-1.05-2.881-1.052-3.933,0l-3.571,3.571 c-0.574,0.573-1.366,0.733-2.114,0.421C33.447,9.313,33,8.644,33,7.832V2.78C33,1.247,31.753,0,30.22,0H23.78 C22.247,0,21,1.247,21,2.78v5.052c0,0.812-0.447,1.481-1.197,1.792c-0.748,0.313-1.54,0.152-2.114-0.421l-3.571-3.571 c-1.052-1.052-2.883-1.05-3.933,0l-4.553,4.553c-0.525,0.525-0.814,1.224-0.814,1.967c0,0.742,0.289,1.44,0.814,1.966l3.572,3.571 c0.573,0.574,0.73,1.364,0.42,2.114S8.644,21,7.832,21H2.78C1.247,21,0,22.247,0,23.78v6.439C0,31.753,1.247,33,2.78,33h5.052 c0.812,0,1.481,0.447,1.792,1.197s0.153,1.54-0.42,2.114l-3.572,3.571c-0.525,0.525-0.814,1.224-0.814,1.966 c0,0.743,0.289,1.441,0.814,1.967l4.553,4.553c1.051,1.051,2.881,1.053,3.933,0l3.571-3.572c0.574-0.573,1.363-0.731,2.114-0.42 c0.75,0.311,1.197,0.98,1.197,1.792v5.052c0,1.533,1.247,2.78,2.78,2.78h6.439c1.533,0,2.78-1.247,2.78-2.78v-5.052 c0-0.812,0.447-1.481,1.197-1.792c0.751-0.312,1.54-0.153,2.114,0.42l3.571,3.572c1.052,1.052,2.883,1.05,3.933,0l4.553-4.553 c0.525-0.525,0.814-1.224,0.814-1.967c0-0.742-0.289-1.44-0.814-1.966l-3.572-3.571c-0.573-0.574-0.73-1.364-0.42-2.114 S45.356,33,46.168,33h5.052c1.533,0,2.78-1.247,2.78-2.78V23.78C54,22.247,52.753,21,51.22,21z M52,30.22 C52,30.65,51.65,31,51.22,31h-5.052c-1.624,0-3.019,0.932-3.64,2.432c-0.622,1.5-0.295,3.146,0.854,4.294l3.572,3.571 c0.305,0.305,0.305,0.8,0,1.104l-4.553,4.553c-0.304,0.304-0.799,0.306-1.104,0l-3.571-3.572c-1.149-1.149-2.794-1.474-4.294-0.854 c-1.5,0.621-2.432,2.016-2.432,3.64v5.052C31,51.65,30.65,52,30.22,52H23.78C23.35,52,23,51.65,23,51.22v-5.052 c0-1.624-0.932-3.019-2.432-3.64c-0.503-0.209-1.021-0.311-1.533-0.311c-1.014,0-1.997,0.4-2.761,1.164l-3.571,3.572 c-0.306,0.306-0.801,0.304-1.104,0l-4.553-4.553c-0.305-0.305-0.305-0.8,0-1.104l3.572-3.571c1.148-1.148,1.476-2.794,0.854-4.294 C10.851,31.932,9.456,31,7.832,31H2.78C2.35,31,2,30.65,2,30.22V23.78C2,23.35,2.35,23,2.78,23h5.052 c1.624,0,3.019-0.932,3.64-2.432c0.622-1.5,0.295-3.146-0.854-4.294l-3.572-3.571c-0.305-0.305-0.305-0.8,0-1.104l4.553-4.553 c0.304-0.305,0.799-0.305,1.104,0l3.571,3.571c1.147,1.147,2.792,1.476,4.294,0.854C22.068,10.851,23,9.456,23,7.832V2.78 C23,2.35,23.35,2,23.78,2h6.439C30.65,2,31,2.35,31,2.78v5.052c0,1.624,0.932,3.019,2.432,3.64 c1.502,0.622,3.146,0.294,4.294-0.854l3.571-3.571c0.306-0.305,0.801-0.305,1.104,0l4.553,4.553c0.305,0.305,0.305,0.8,0,1.104 l-3.572,3.571c-1.148,1.148-1.476,2.794-0.854,4.294c0.621,1.5,2.016,2.432,3.64,2.432h5.052C51.65,23,52,23.35,52,23.78V30.22z"></path> <path d="M27,18c-4.963,0-9,4.037-9,9s4.037,9,9,9s9-4.037,9-9S31.963,18,27,18z M27,34c-3.859,0-7-3.141-7-7s3.141-7,7-7 s7,3.141,7,7S30.859,34,27,34z"></path> </g> </g></svg>
</a>
</div>
</div>
<hr>
@@ -60,6 +59,7 @@ var vConsole = new window.VConsole();
<div>
<button id="play_music_list">播放列表歌曲</button>
<button id="del_music">删除选中歌曲</button>
<button id="web_play">网页播放</button>
</div>
</div>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>M3U to JSON Converter</title>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1721862430">
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725986534">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>

View File

@@ -3,9 +3,9 @@
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="/static/jquery-3.7.1.min.js?version=1721862430"></script>
<script src="/static/setting.js?version=1721862430"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1721862430">
<script src="/static/jquery-3.7.1.min.js?version=1725986534"></script>
<script src="/static/setting.js?version=1725986534"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725986534">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
@@ -90,18 +90,23 @@ var vConsole = new window.VConsole();
<label for="proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080"></input>
<label for="remove_id3tag">去除MP3 ID3v2和填充减少播放前延迟:</label>
<select id="remove_id3tag">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="convert_to_mp3">转换为MP3</label>
<select id="convert_to_mp3">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="disable_httpauth">关闭密码验证:</label>
<select id="disable_httpauth">
<option value="true" selected>true</option>
<option value="false">false</option>
</select>
<label for="remove_id3tag">去除MP3 ID3v2和填充减少播放前延迟:</label>
<select id="remove_id3tag">
<option value="true" >true</option>
<option value="false" selected>false</option>
</select>
<label for="httpauth_username">web控制台账户:</label>
<input id="httpauth_username" type="text" value=""></input>
<label for="httpauth_password">web控制台密码:</label>
@@ -127,9 +132,18 @@ var vConsole = new window.VConsole();
<option value="false">false</option>
</select>
<label for="use_music_api">触屏版兼容模式:</label>
<select id="use_music_api">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="public_port">外网访问端口(0表示跟监听端口一致):</label>
<input id="public_port" type="number" value="0"></input>
<label for="delay_sec">下一首歌延迟播放秒数:</label>
<input id="delay_sec" type="number" value="3"></input>
<label for="stop_tts_msg">停止提示音:</label>
<input id="stop_tts_msg" type="text" value="收到,再见"></input>
<label for="keywords_playlocal">播放本地歌曲口令:</label>
@@ -152,6 +166,7 @@ var vConsole = new window.VConsole();
<button id="get_music_list">获取歌单</button>
<button class="save-button">保存</button>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<button onclick="location.href='/docs';">查看接口文档</button>
<hr>
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>

View File

@@ -68,9 +68,8 @@ $(function(){
// 初始化显示
for (const key in data) {
if (data.hasOwnProperty(key)) {
const $element = $("#" + key);
if ($element.length && data[key] !== '') {
if ($element.length) {
if (data[key] === true) {
$element.val('true');
} else if (data[key] === false) {
@@ -79,7 +78,6 @@ $(function(){
$element.val(data[key]);
}
}
}
}
autoSelectOne();

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python3
from __future__ import annotations
import asyncio
import copy
import difflib
import io
import json
import logging
import mimetypes
import os
@@ -11,12 +12,12 @@ import random
import re
import shutil
import string
import subprocess
import tempfile
from collections.abc import AsyncIterator
from http.cookies import SimpleCookie
from urllib.parse import urlparse
import aiofiles
import aiohttp
import mutagen
from mutagen.id3 import ID3
@@ -142,9 +143,7 @@ def _append_files_result(result, root, joinpath, files, support_extension):
result[dir_name].append(os.path.join(joinpath, file))
def traverse_music_directory(
directory, depth=10, exclude_dirs=None, support_extension=None
):
def traverse_music_directory(directory, depth, exclude_dirs, support_extension):
result = {}
for root, dirs, files in os.walk(directory, followlinks=True):
# 忽略排除的目录
@@ -191,7 +190,11 @@ def is_mp3(url):
return False
async def _get_web_music_duration(session, url, start=0, end=500):
def is_m4a(url):
return url.endswith(".m4a")
async def _get_web_music_duration(session, url, ffmpeg_location, start=0, end=500):
duration = 0
headers = {"Range": f"bytes={start}-{end}"}
async with session.get(url, headers=headers) as response:
@@ -201,6 +204,8 @@ async def _get_web_music_duration(session, url, start=0, end=500):
try:
if is_mp3(url):
m = mutagen.mp3.MP3(tmp)
elif is_m4a(url):
return get_duration_by_ffprobe(tmp, ffmpeg_location)
else:
m = mutagen.File(tmp)
duration = m.info.length
@@ -209,7 +214,7 @@ async def _get_web_music_duration(session, url, start=0, end=500):
return duration
async def get_web_music_duration(url):
async def get_web_music_duration(url, ffmpeg_location="./ffmpeg/bin"):
duration = 0
try:
parsed_url = urlparse(url)
@@ -229,10 +234,12 @@ async def get_web_music_duration(url):
# 设置总超时时间为3秒
timeout = aiohttp.ClientTimeout(total=3)
async with aiohttp.ClientSession(timeout=timeout) as session:
duration = await _get_web_music_duration(session, url, start=0, end=500)
duration = await _get_web_music_duration(
session, url, ffmpeg_location, start=0, end=500
)
if duration <= 0:
duration = await _get_web_music_duration(
session, url, start=0, end=3000
session, url, ffmpeg_location, start=0, end=3000
)
except Exception as e:
logging.error(f"Error get_web_music_duration: {e}")
@@ -240,19 +247,47 @@ async def get_web_music_duration(url):
# 获取文件播放时长
async def get_local_music_duration(filename):
async def get_local_music_duration(filename, ffmpeg_location="./ffmpeg/bin"):
loop = asyncio.get_event_loop()
duration = 0
try:
async with aiofiles.open(filename, "rb") as f:
buffer = io.BytesIO(await f.read())
if is_mp3(filename):
m = mutagen.mp3.MP3(buffer)
m = await loop.run_in_executor(None, mutagen.mp3.MP3, filename)
elif is_m4a(filename):
duration = get_duration_by_ffprobe(filename, ffmpeg_location)
return duration
else:
m = mutagen.File(buffer)
if m and m.info:
duration = m.info.length
m = await loop.run_in_executor(None, mutagen.File, filename)
duration = m.info.length
except Exception as e:
logging.error(f"Error getting local music duration: {e}")
logging.error(f"Error getting local music {filename} duration: {e}")
return duration
def get_duration_by_ffprobe(file_path, ffmpeg_location):
# 使用 ffprobe 获取文件的元数据,并以 JSON 格式输出
result = subprocess.run(
[
os.path.join(ffmpeg_location, "ffprobe"),
"-v",
"error", # 只输出错误信息,避免混杂在其他输出中
"-show_entries",
"format=duration", # 仅显示时长
"-of",
"json", # 以 JSON 格式输出
file_path,
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
# 解析 JSON 输出
ffprobe_output = json.loads(result.stdout)
# 获取时长
duration = float(ffprobe_output["format"]["duration"])
return duration
@@ -335,3 +370,35 @@ def remove_id3_tags(file_path):
change = True
return change
def convert_file_to_mp3(input_file: str, ffmpeg_location: str, music_path: str) -> str:
"""
Convert the music file to MP3 format and return the path of the temporary MP3 file.
"""
# 指定临时文件的目录为 music_path 目录下的 tmp 文件夹
temp_dir = os.path.join(music_path, "tmp")
if not os.path.exists(temp_dir):
os.makedirs(temp_dir) # 确保目录存在
out_file_name = os.path.splitext(os.path.basename(input_file))[0]
out_file_path = os.path.join(temp_dir, f"{out_file_name}.mp3")
command = [
os.path.join(ffmpeg_location, "ffmpeg"),
"-i",
input_file,
"-f",
"mp3",
"-y",
out_file_path,
]
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
print(f"Error during conversion: {e}")
return None
relative_path = os.path.relpath(out_file_path, music_path)
return relative_path

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import asyncio
import copy
import json
import logging
import math
@@ -32,6 +33,7 @@ from xiaomusic.const import (
)
from xiaomusic.plugin import PluginManager
from xiaomusic.utils import (
convert_file_to_mp3,
custom_sort_key,
deepcopy_data_no_sensitive_info,
find_best_match,
@@ -62,6 +64,7 @@ class XiaoMusic:
self._all_radio = {} # 电台列表
self.music_list = {} # 播放列表 key 为目录名, value 为 play_list
self.devices = {} # key 为 did
self.running_task = []
# 初始化配置
self.init_config()
@@ -84,15 +87,11 @@ class XiaoMusic:
debug_config = deepcopy_data_no_sensitive_info(self.config)
self.log.info(f"Startup OK. {debug_config}")
if self.conf_path == self.music_path:
if self.config.conf_path == self.music_path:
self.log.warning("配置文件目录和音乐目录建议设置为不同的目录")
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 = "conf"
self.download_path = self.config.download_path
if not self.download_path:
self.download_path = self.music_path
@@ -110,6 +109,7 @@ class XiaoMusic:
self.exclude_dirs = set(self.config.exclude_dirs.split(","))
self.music_path_depth = self.config.music_path_depth
self.remove_id3tag = self.config.remove_id3tag
self.convert_to_mp3 = self.config.convert_to_mp3
def update_devices(self):
self.device_id_did = {} # key 为 device_id
@@ -225,7 +225,7 @@ class XiaoMusic:
self.log.error(f"{self.mi_token_home} file not exist")
return None
with open(self.mi_token_home) as f:
with open(self.mi_token_home, encoding="utf-8") as f:
user_data = json.loads(f.read())
user_id = user_data.get("userId")
service_token = user_data.get("micoapi")[1]
@@ -313,10 +313,10 @@ class XiaoMusic:
def get_filename(self, name):
if name not in self.all_music:
self.log.debug(f"get_filename not in. name:{name}")
self.log.info(f"get_filename not in. name:{name}")
return ""
filename = self.all_music[name]
self.log.debug(f"try get_filename. filename:{filename}")
self.log.info(f"try get_filename. filename:{filename}")
if os.path.exists(filename):
return filename
return ""
@@ -347,18 +347,24 @@ class XiaoMusic:
async def get_music_sec_url(self, name):
sec = 0
url = self.get_music_url(name)
self.log.info(f"get_music_sec_url. name:{name} url:{url}")
if self.is_web_radio_music(name):
self.log.info("电台不会有播放时长")
return 0, url
if self.is_web_music(name):
origin_url = url
duration, url = await get_web_music_duration(url)
duration, url = await get_web_music_duration(
url, self.config.ffmpeg_location
)
sec = math.ceil(duration)
self.log.info(f"网络歌曲 {name} : {origin_url} {url} 的时长 {sec}")
else:
filename = self.get_filename(name)
duration = await get_local_music_duration(filename)
self.log.info(f"get_music_sec_url. name:{name} filename:{filename}")
duration = await get_local_music_duration(
filename, self.config.ffmpeg_location
)
sec = math.ceil(duration)
self.log.info(f"本地歌曲 {name} : {filename} {url} 的时长 {sec}")
@@ -369,7 +375,7 @@ class XiaoMusic:
def get_music_url(self, name):
if self.is_web_music(name):
url = self.all_music[name]
self.log.debug(f"get_music_url web music. name:{name}, url:{url}")
self.log.info(f"get_music_url web music. name:{name}, url:{url}")
return url
filename = self.get_filename(name)
@@ -382,12 +388,27 @@ class XiaoMusic:
else:
self.log.info("No ID3 tag remove needed")
# 如果开启了MP3转换功能且文件不是MP3格式则进行转换
if self.convert_to_mp3 and not is_mp3(filename):
self.log.info(f"convert_to_mp3 is enabled. Checking file: {filename}")
temp_mp3_file = convert_file_to_mp3(
filename, self.config.ffmpeg_location, self.config.music_path
)
if temp_mp3_file:
self.log.info(f"Converted file: {filename} to {temp_mp3_file}")
filename = temp_mp3_file
else:
self.log.warning(f"Failed to convert file to MP3 format: {filename}")
# 构造音乐文件的URL
filename = filename.replace("\\", "/")
if filename.startswith(self.config.music_path):
filename = filename[len(self.config.music_path) :]
if filename.startswith("/"):
filename = filename[1:]
self.log.debug(f"get_music_url local music. name:{name}, filename:{filename}")
self.log.info(f"get_music_url local music. name:{name}, filename:{filename}")
encoded_name = urllib.parse.quote(filename)
return f"http://{self.hostname}:{self.public_port}/music/{encoded_name}"
@@ -433,6 +454,11 @@ class XiaoMusic:
self.log.exception(f"Execption {e}")
self.music_list["全部"] = list(self.all_music.keys())
self.music_list["所有歌曲"] = [
name for name in self.all_music.keys() if name not in self._all_radio
]
self._append_custom_play_list()
# 歌单排序
for _, play_list in self.music_list.items():
@@ -442,6 +468,16 @@ class XiaoMusic:
for device in self.devices.values():
device.update_playlist()
def _append_custom_play_list(self):
if not self.config.custom_play_list_json:
return
try:
custom_play_list = json.loads(self.config.custom_play_list_json)
self.music_list["收藏"] = list(custom_play_list["收藏"])
except Exception as e:
self.log.exception(f"Execption {e}")
# 给歌单里补充网络歌单
def _append_music_list(self):
if not self.config.music_list_json:
@@ -509,6 +545,19 @@ class XiaoMusic:
except Exception as e:
self.log.exception(f"Execption {e}")
def append_running_task(self, task):
self.running_task.append(task)
async def cancel_all_tasks(self):
if len(self.running_task) == 0:
self.log.info("cancel_all_tasks no task")
return
for task in self.running_task:
self.log.info(f"cancel_all_tasks {task}")
task.cancel()
await asyncio.gather(*self.running_task, return_exceptions=True)
self.running_task = []
async def check_replay(self, did):
return await self.devices[did].check_replay()
@@ -673,6 +722,9 @@ class XiaoMusic:
async def play_next(self, did="", **kwargs):
return await self.devices[did].play_next()
async def play_prev(self, did="", **kwargs):
return await self.devices[did].play_prev()
# 停止
async def stop(self, did="", arg1="", **kwargs):
return await self.devices[did].stop(arg1=arg1)
@@ -682,6 +734,47 @@ class XiaoMusic:
minute = int(arg1)
return await self.devices[did].stop_after_minute(minute)
# 添加歌曲到收藏列表
async def add_to_favorites(self, did="", arg1="", **kwargs):
name = arg1 if arg1 else self.playingmusic(did)
if not name:
return
favorites = self.music_list.get("收藏", [])
if name in favorites:
return
favorites.append(name)
self.save_favorites(favorites)
# 从收藏列表中移除
async def del_from_favorites(self, did="", arg1="", **kwargs):
name = arg1 if arg1 else self.playingmusic(did)
if not name:
return
favorites = self.music_list.get("收藏", [])
if name not in favorites:
return
favorites.remove(name)
self.save_favorites(favorites)
def save_favorites(self, favorites):
self.music_list["收藏"] = favorites
custom_play_list = {}
if self.config.custom_play_list_json:
custom_play_list = json.loads(self.config.custom_play_list_json)
custom_play_list["收藏"] = favorites
self.config.custom_play_list_json = json.dumps(
custom_play_list, ensure_ascii=False
)
self.save_cur_config()
# 更新每个设备的歌单
for device in self.devices.values():
device.update_playlist()
# 获取音量
async def get_volume(self, did="", **kwargs):
return await self.devices[did].get_volume()
@@ -708,7 +801,7 @@ class XiaoMusic:
# 正在播放中的音乐
def playingmusic(self, did):
cur_music = self.devices[did].cur_music
cur_music = self.devices[did].get_cur_music()
self.log.debug(f"playingmusic. cur_music:{cur_music}")
return cur_music
@@ -720,17 +813,10 @@ class XiaoMusic:
def getconfig(self):
return self.config
# 获取设置文件
def getsettingfile(self):
if not os.path.exists(self.conf_path):
os.makedirs(self.conf_path)
filename = os.path.join(self.conf_path, "setting.json")
return filename
def try_init_setting(self):
try:
filename = self.getsettingfile()
with open(filename) as f:
filename = self.config.getsettingfile()
with open(filename, encoding="utf-8") as f:
data = json.loads(f.read())
self.update_config_from_setting(data)
except FileNotFoundError:
@@ -751,13 +837,16 @@ class XiaoMusic:
# 配置文件落地
def do_saveconfig(self, data):
# 默认暂时配置保存到 music 目录下
filename = self.getsettingfile()
filename = self.config.getsettingfile()
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# 把当前配置落地
def save_cur_config(self):
for did in self.config.devices.keys():
deviceobj = self.devices.get(did)
if deviceobj is not None:
self.config.devices[did] = deviceobj.device
data = asdict(self.config)
self.do_saveconfig(data)
self.log.info("save_cur_config ok")
@@ -831,7 +920,6 @@ class XiaoMusicDevice:
self.ffmpeg_location = self.config.ffmpeg_location
self._download_proc = None # 下载对象
self.cur_music = self.device.cur_music
self._next_timer = None
self._timeout = 0
self._playing = False
@@ -840,12 +928,21 @@ class XiaoMusicDevice:
self._last_cmd = None
self.update_playlist()
def get_cur_music(self):
return self.device.cur_music
# 初始化播放列表
def update_playlist(self):
self._cur_play_list = self.device.cur_playlist
if self._cur_play_list not in self.xiaomusic.music_list:
self._cur_play_list = "全部"
self._play_list = self.xiaomusic.music_list.get(self._cur_play_list)
if self.device.cur_playlist not in self.xiaomusic.music_list:
self.device.cur_playlist = "全部"
list_name = self.device.cur_playlist
self._play_list = copy.copy(self.xiaomusic.music_list[list_name])
if self.device.play_type == PLAY_TYPE_RND:
random.shuffle(self._play_list)
self.log.info(f"随机打乱 {list_name} {self._play_list}")
else:
self.log.info(f"没打乱 {list_name} {self._play_list}")
# 播放歌曲
async def play(self, name="", search_key=""):
@@ -855,10 +952,10 @@ class XiaoMusicDevice:
async def _play(self, name="", search_key=""):
if search_key == "" and name == "":
if self.check_play_next():
await self.play_next()
await self._play_next()
return
else:
name = self.cur_music
name = self.get_cur_music()
self.log.info(f"play. search_key:{search_key} name:{name}")
# 本地歌曲不存在时下载
@@ -880,7 +977,7 @@ class XiaoMusicDevice:
async def _play_next(self):
self.log.info("开始播放下一首")
name = self.cur_music
name = self.get_cur_music()
if (
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
@@ -888,7 +985,27 @@ class XiaoMusicDevice:
or (name not in self._play_list)
):
name = self.get_next_music()
self.log.info(f"play_next. name:{name}, cur_music:{self.cur_music}")
self.log.info(f"_play_next. name:{name}, cur_music:{self.get_cur_music()}")
if name == "":
await self.do_tts("本地没有歌曲")
return
await self._play(name)
# 上一首
async def play_prev(self):
return await self._play_prev()
async def _play_prev(self):
self.log.info("开始播放上一首")
name = self.get_cur_music()
if (
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
or name == ""
or (name not in self._play_list)
):
name = self.get_prev_music()
self.log.info(f"_play_prev. name:{name}, cur_music:{self.get_cur_music()}")
if name == "":
await self.do_tts("本地没有歌曲")
return
@@ -899,10 +1016,10 @@ class XiaoMusicDevice:
self._last_cmd = "playlocal"
if name == "":
if self.check_play_next():
await self.play_next()
await self._play_next()
return
else:
name = self.cur_music
name = self.get_cur_music()
self.log.info(f"playlocal. name:{name}")
@@ -914,13 +1031,17 @@ class XiaoMusicDevice:
await self._playmusic(name)
async def _playmusic(self, name):
# 取消组内所有的下一首歌曲的定时器
self.cancel_group_next_timer()
self._playing = True
self.cur_music = name
self.log.info(f"cur_music {self.cur_music}")
self.device.cur_music = name
self.log.info(f"cur_music {self.get_cur_music()}")
sec, url = await self.xiaomusic.get_music_sec_url(name)
await self.group_force_stop_xiaoai()
self.log.info(f"播放 {url}")
results = await self.group_player_play(url)
results = await self.group_player_play(url, name)
if all(ele is None for ele in results):
self.log.info(f"播放 {name} 失败")
await asyncio.sleep(1)
@@ -928,12 +1049,15 @@ class XiaoMusicDevice:
await self._play_next()
return
self.log.info("已经开始播放了")
self.log.info(f"{name}已经开始播放了")
# 取消组内所有的下一首歌曲的定时器
self.cancel_group_next_timer()
# 设置下一首歌曲的播放定时器
if sec <= 1:
self.log.info(f"{name}】不会设置下一首歌的定时器")
return
sec = sec + self.config.delay_sec
await self.set_next_music_timeout(sec)
self.xiaomusic.save_cur_config()
async def do_tts(self, value):
self.log.info(f"try do_tts value:{value}")
@@ -941,13 +1065,13 @@ class XiaoMusicDevice:
self.log.info("do_tts no value")
return
await self.group_force_stop_xiaoai()
# await self.group_force_stop_xiaoai()
await self.text_to_speech(value)
# 最大等8秒
sec = min(8, int(len(value) / 3))
await asyncio.sleep(sec)
self.log.info(f"do_tts ok. cur_music:{self.cur_music}")
self.log.info(f"do_tts ok. cur_music:{self.get_cur_music()}")
await self.check_replay()
async def force_stop_xiaoai(self, device_id):
@@ -1049,48 +1173,62 @@ class XiaoMusicDevice:
self.log.info(f"add_download_music add_music {name}")
self.log.debug(self._play_list)
# 获取下一首
def get_next_music(self):
def get_music(self, direction="next"):
play_list_len = len(self._play_list)
if play_list_len == 0:
self.log.warning("当前播放列表没有歌曲")
return ""
index = 0
try:
index = self._play_list.index(self.cur_music)
index = self._play_list.index(self.get_cur_music())
except ValueError:
pass
if play_list_len == 1:
next_index = index # 当只有一首歌曲时保持当前索引不变
new_index = index # 当只有一首歌曲时保持当前索引不变
else:
# 顺序往后找1个
next_index = index + 1
if next_index >= play_list_len:
next_index = 0
# 排除当前歌曲随机找1个
if self.device.play_type == PLAY_TYPE_RND:
indices = list(range(play_list_len))
indices.remove(index)
next_index = random.choice(indices)
name = self._play_list[next_index]
if direction == "next":
new_index = index + 1
if new_index >= play_list_len:
new_index = 0
elif direction == "prev":
new_index = index - 1
if new_index < 0:
new_index = play_list_len - 1
else:
self.log.error("无效的方向参数")
return ""
name = self._play_list[new_index]
if not self.xiaomusic.is_music_exist(name):
self._play_list.pop(next_index)
self.log.info(f"pop not exist music:{name}")
return self.get_next_music()
self._play_list.pop(new_index)
self.log.info(f"pop not exist music: {name}")
return self.get_music(direction)
return name
# 获取下一首
def get_next_music(self):
return self.get_music(direction="next")
# 获取上一首
def get_prev_music(self):
return self.get_music(direction="prev")
# 判断是否播放下一首歌曲
def check_play_next(self):
# 当前歌曲不在当前播放列表
if self.cur_music not in self._play_list:
if self.get_cur_music() not in self._play_list:
self.log.info(f"当前歌曲 {self.get_cur_music()} 不在当前播放列表")
return True
# 当前没我在播放的歌曲
if self.cur_music == "":
if self.get_cur_music() == "":
self.log.info("当前没我在播放的歌曲")
return True
else:
# 当前播放的歌曲不存在了
if not self.xiaomusic.is_music_exist(self.cur_music):
if not self.xiaomusic.is_music_exist(self.get_cur_music()):
self.log.info(f"当前播放的歌曲 {self.get_cur_music()} 不存在了")
return True
return False
@@ -1101,22 +1239,25 @@ class XiaoMusicDevice:
self.log.exception(f"Execption {e}")
# 同一组设备播放
async def group_player_play(self, url):
async def group_player_play(self, url, name=""):
device_id_list = self.xiaomusic.get_group_device_id_list(self.group_name)
tasks = [self.play_one_url(device_id, url) for device_id in device_id_list]
tasks = [
self.play_one_url(device_id, url, name) for device_id in device_id_list
]
results = await asyncio.gather(*tasks)
self.log.info(f"group_player_play {url} {device_id_list} {results}")
return results
async def play_one_url(self, device_id, url):
async def play_one_url(self, device_id, url, name):
ret = None
try:
audio_id = await self._get_audio_id(name)
if self.config.use_music_api:
ret = await self.xiaomusic.mina_service.play_by_music_url(
device_id, url
device_id, url, audio_id=audio_id
)
self.log.info(
f"play_one_url play_by_music_url device_id:{device_id} ret:{ret} url:{url}"
f"play_one_url play_by_music_url device_id:{device_id} ret:{ret} url:{url} audio_id:{audio_id}"
)
else:
ret = await self.xiaomusic.mina_service.play_by_url(device_id, url)
@@ -1127,25 +1268,42 @@ class XiaoMusicDevice:
self.log.exception(f"Execption {e}")
return ret
async def _get_audio_id(self, name):
audio_id = 1582971365183456177
try:
params = {
"query": name,
"queryType": 1,
"offset": 0,
"count": 6,
"timestamp": int(time.time_ns() / 1000),
}
response = await self.xiaomusic.mina_service.mina_request(
"/music/search", params
)
audio_id = response["data"]["songList"][5][
"audioID"
] # QQ音乐为搜索结果的第6首歌
self.log.debug(f"_get_audio_id. name: {name} songId:{audio_id}")
except Exception as e:
self.log.error(f"_get_audio_id {e}")
return str(audio_id)
# 设置下一首歌曲的播放定时器
async def set_next_music_timeout(self, sec):
if sec <= 0:
return
if self._next_timer:
self._next_timer.cancel()
self.log.info("旧定时器已取消")
self.cancel_next_timer()
self._timeout = sec
async def _do_next():
await asyncio.sleep(self._timeout)
try:
await self.play_next()
self.log.info("定时器时间到了")
self._next_timer = None
await self._play_next()
except Exception as e:
self.log.error(f"Execption {e}")
self._next_timer = asyncio.ensure_future(_do_next())
self._next_timer = asyncio.create_task(_do_next())
self.log.info(f"{sec} 秒后将会播放下一首歌曲")
async def set_volume(self, volume: int):
@@ -1172,11 +1330,12 @@ class XiaoMusicDevice:
self.xiaomusic.save_cur_config()
tts = PLAY_TYPE_TTS[play_type]
await self.do_tts(tts)
self.update_playlist()
async def play_music_list(self, list_name, music_name):
self._last_cmd = "play_music_list"
self._cur_play_list = list_name
self._play_list = self.xiaomusic.music_list[list_name]
self.device.cur_playlist = list_name
self.update_playlist()
self.log.info(f"开始播放列表{list_name}")
await self._play(music_name)
@@ -1185,10 +1344,8 @@ class XiaoMusicDevice:
self._playing = False
if arg1 != "notts":
await self.do_tts(self.config.stop_tts_msg)
if self._next_timer:
self._next_timer.cancel()
self._next_timer = None
self.log.info("定时器已取消")
# 取消组内所有的下一首歌曲的定时器
self.cancel_group_next_timer()
await self.group_force_stop_xiaoai()
self.log.info("stop now")
@@ -1213,13 +1370,14 @@ class XiaoMusicDevice:
except Exception as e:
self.log.exception(f"Execption {e}")
self._stop_timer = asyncio.ensure_future(_do_stop())
self._stop_timer = asyncio.create_task(_do_stop())
await self.do_tts(f"收到,{minute}分钟后将关机")
def cancel_next_timer(self):
if self._next_timer:
self._next_timer.cancel()
self.log.info("下一曲定时器已取消")
self.log.info(f"下一曲定时器已取消 {self.device_id}")
self._next_timer = None
def cancel_group_next_timer(self):
devices = self.xiaomusic.get_group_devices(self.group_name)
@@ -1227,17 +1385,19 @@ class XiaoMusicDevice:
device.cancel_next_timer()
def get_cur_play_list(self):
return self._cur_play_list
return self.device.cur_playlist
# 清空所有定时器
def cancel_all_timer(self):
self.log.info("in cancel_all_timer")
if self._next_timer:
self._next_timer.cancel()
self._next_timer = None
self.log.info("cancel_all_timer _next_timer.cancel")
if self._stop_timer:
self._stop_timer.cancel()
self._stop_timer = None
self.log.info("cancel_all_timer _stop_timer.cancel")
@classmethod