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

Compare commits

...

80 Commits

Author SHA1 Message Date
涵曦
ca547e0d81 new version v0.1.76 2024-06-28 16:10:50 +00:00
涵曦
f0931c447b 下载日志链接改为按钮样式 2024-06-28 16:10:34 +00:00
涵曦
d098b5eb60 Update README.md 2024-06-28 23:45:18 +08:00
涵曦
09111e849d Update README.md 2024-06-28 23:43:48 +08:00
涵曦
49a76dee60 Update README.md 2024-06-28 23:20:45 +08:00
涵曦
637672473e Update README.md 2024-06-28 23:14:16 +08:00
涵曦
e4e1d13b69 Update README.md 2024-06-28 23:00:01 +08:00
涵曦
7736f8c5b4 Update README.md 2024-06-28 22:55:31 +08:00
涵曦
29db69b52f Update README.md 2024-06-28 22:52:04 +08:00
涵曦
37fe51771b Update README.md 2024-06-28 22:48:42 +08:00
涵曦
77ece713dc new version v0.1.75 2024-06-28 14:46:51 +00:00
涵曦
4436cc3a15 新增ogg格式文件 2024-06-28 14:46:46 +00:00
涵曦
c875350112 new version v0.1.74 2024-06-28 14:34:36 +00:00
涵曦
d6df2f6bfe 支持在设置页面下载日志 2024-06-28 14:34:12 +00:00
涵曦
03e3312218 新增日志文件 2024-06-28 14:03:09 +00:00
涵曦
5d7451c3f2 new version v0.1.73 2024-06-28 10:14:12 +00:00
涵曦
551dfa0c7f 播放列表口令加到默认唤醒词里 2024-06-28 10:14:08 +00:00
涵曦
3cc35e8f97 Update README.md 2024-06-28 15:32:19 +08:00
涵曦
01c68ba64e Create FUNDING.yml 2024-06-28 13:43:16 +08:00
涵曦
61d167d347 Update README.md 2024-06-28 12:18:18 +08:00
涵曦
9393e9f1ca new version v0.1.72 2024-06-28 02:43:35 +00:00
涵曦
c08ef030e9 优化配置页面,只允许选did see #83 2024-06-28 02:41:56 +00:00
涵曦
bad13f01f4 优化获取播放时长的问题 2024-06-28 02:33:12 +00:00
涵曦
049e1a2c38 测试触屏版本不能播放的问题 2024-06-28 01:47:27 +00:00
涵曦
a9fb829563 new version v0.1.71 2024-06-28 00:57:31 +00:00
涵曦
e66f731301 fix: #83 2024-06-28 00:56:39 +00:00
涵曦
538ac1d485 优化获取播放时长的逻辑 2024-06-27 23:20:43 +00:00
涵曦
88fbc503e7 加点调试日志 2024-06-27 16:36:53 +00:00
涵曦
1dc3ccbc16 更新依赖库 2024-06-27 15:34:03 +00:00
涵曦
df007d8e1b Update README.md 2024-06-27 23:15:53 +08:00
涵曦
0876551795 Update README.md 2024-06-27 23:09:40 +08:00
涵曦
51acb3ac8e Update README.md 2024-06-27 23:06:34 +08:00
涵曦
786af1c79e Update README.md 2024-06-27 22:46:10 +08:00
涵曦
7fba78e44b Update README.md 2024-06-27 22:03:06 +08:00
涵曦
9434cf3216 Update README.md 2024-06-27 14:17:00 +08:00
涵曦
f71ab25407 new version v0.1.70 2024-06-27 05:38:12 +00:00
涵曦
a634813c21 update readme 2024-06-27 05:33:58 +00:00
涵曦
17279aaae0 新增XIAOMUSIC_USE_MUSIC_API=true时使用player_play_music接口,兼容部分不能播放的设备型号 see #30 2024-06-27 05:33:53 +00:00
涵曦
4a234e8829 优化m3u文件转换工具 2024-06-27 05:16:33 +00:00
涵曦
805b3c41c8 优化下载文件的逻辑 2024-06-27 05:07:34 +00:00
涵曦
7a154fd847 修复停止播放导致退出问题 see #80 2024-06-26 23:33:15 +00:00
涵曦
3cdc836e9e 播放本地歌曲口令也作为唤醒词 see #80 2024-06-26 17:41:38 +00:00
涵曦
d232627796 优化播放歌曲口令和关机口令,新增播放本地歌曲口令,且都支持自定义配置这三个口令 see #80 2024-06-26 17:34:23 +00:00
涵曦
579820e606 new version v0.1.69 2024-06-26 16:24:12 +00:00
涵曦
a24a5166f9 new version v0.1.68 2024-06-26 16:24:09 +00:00
涵曦
ad67894244 Update README.md 2024-06-27 00:23:22 +08:00
涵曦
725d4c4ab3 新增XIAOMUSIC_DISABLE_DOWNLOAD=true时关闭音乐下载功能 see #82 2024-06-26 16:14:41 +00:00
涵曦
6a0310fe05 new version v0.1.67 2024-06-26 15:57:07 +00:00
涵曦
463fd9dd38 新增m3u文件转电台歌单工具 2024-06-26 15:56:19 +00:00
涵曦
34fe19abd1 new version v0.1.66 2024-06-26 13:41:24 +00:00
涵曦
3365f082f7 修复主页滚动问题 2024-06-26 13:41:05 +00:00
涵曦
13361c57b8 new version v0.1.65 2024-06-26 13:35:43 +00:00
涵曦
2a22b00d53 优化下载json文件逻辑 2024-06-26 10:14:11 +00:00
涵曦
925b52d979 优化下载文件的接口 2024-06-26 09:09:55 +00:00
涵曦
468efb63fb 优化下载文件的接口 2024-06-26 08:56:43 +00:00
涵曦
38aae7eca3 Update README.md 2024-06-26 12:53:05 +08:00
涵曦
44df1134a8 new version v0.1.64 2024-06-26 01:24:30 +00:00
涵曦
243c1673db new version v0.1.63 2024-06-26 01:24:27 +00:00
涵曦
82eab4810c 修复自动播放的问题 2024-06-26 01:24:27 +00:00
涵曦
e0a59b5729 修复自动播放的问题 2024-06-26 01:24:27 +00:00
涵曦
ad3bad85db 优化日志输出 2024-06-26 01:24:27 +00:00
涵曦
743d85de32 优化不定长参数arg1的用法 2024-06-26 01:24:27 +00:00
涵曦
8bd32f878f Update README.md 2024-06-26 02:03:38 +08:00
涵曦
eccb52c197 new version v0.1.62 2024-06-25 17:34:47 +00:00
涵曦
603d60d8b8 优化图标大小 2024-06-25 17:30:50 +00:00
涵曦
3ef04f4159 优化获取音乐时长接口 2024-06-25 17:27:32 +00:00
涵曦
7888ee7938 new version v0.1.61 2024-06-25 11:13:57 +00:00
涵曦
b2a3cda7b5 #78 支持配置自定义网络歌单 2024-06-25 11:13:46 +00:00
涵曦
80c6d29079 new version v0.1.60 2024-06-25 06:20:28 +00:00
涵曦
0b020deaef new version v0.1.59 2024-06-25 06:17:41 +00:00
涵曦
74c8bea756 cue不是音乐文件,排除下 2024-06-25 06:17:41 +00:00
涵曦
af10d6261f Update README.md 2024-06-25 10:50:32 +08:00
涵曦
a178278576 new version v0.1.58 2024-06-25 01:03:13 +00:00
涵曦
474fea8434 优化播放被打断的问题 2024-06-25 01:03:01 +00:00
涵曦
d271f7b0f7 代码优化 2024-06-24 16:03:10 +00:00
涵曦
7e2af515ed fix: 登陆失败不阻塞启动 2024-06-24 15:38:41 +00:00
涵曦
2d403ff18c new version v0.1.57 2024-06-24 14:28:08 +00:00
涵曦
f08244a990 update readme 2024-06-24 14:28:05 +00:00
涵曦
4869e5cf80 新增ape和cue格式文件 2024-06-24 14:27:31 +00:00
涵曦
b887504f9f Update README.md 2024-06-24 12:24:47 +08:00
16 changed files with 742 additions and 194 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [hanxi]
custom: ['https://afdian.net/a/imhanxi']

119
README.md
View File

@@ -1,6 +1,15 @@
# xiaomusic
[![GitHub License](https://img.shields.io/github/license/hanxi/xiaomusic)](https://github.com/hanxi/xiaomusic)
[![Docker Image Version](https://img.shields.io/docker/v/hanxi/xiaomusic?sort=semver&label=docker%20image)](https://hub.docker.com/r/hanxi/xiaomusic)
[![Docker Pulls](https://img.shields.io/docker/pulls/hanxi/xiaomusic)](https://hub.docker.com/r/hanxi/xiaomusic)
[![PyPI - Version](https://img.shields.io/pypi/v/xiaomusic)](https://pypi.org/project/xiaomusic/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/xiaomusic)](https://pypi.org/project/xiaomusic/)
[![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fhanxi%2Fxiaomusic%2Fmain%2Fpyproject.toml)](https://pypi.org/project/xiaomusic/)
[![GitHub Release](https://img.shields.io/github/v/release/hanxi/xiaomusic)](https://github.com/hanxi/xiaomusic/releases)
使用小爱/红米音箱播放音乐,音乐使用 yt-dlp 下载。
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
## 最简配置运行
@@ -19,10 +28,45 @@ services:
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 \
-v ./music:/app/music \
hanxi/xiaomusic
```
启动成功后,在 web 页面可以配置 MI_DID, MI_HARDWARE, XIAOMUSIC_SEARCH, XIAOMUSIC_PROXY 参数。
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 5678:5678
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 反馈问题时把下载的日志文件带上。
## 开发环境运行
- 使用 install_dependencies.sh 下载依赖
@@ -57,26 +101,35 @@ pdm run xiaomusic.py
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
## 已测试设备
## 已测试支持的设备
```txt
"L07A": ("5-1", "5-5"), # Redmi小爱音箱Play(l7a)
- L07A
- S12
- LX5A
- LX05
- L16A
- L17A
- LX06
- LX01
- L05B
- L05C
````
## 支持音乐格式
- mp3
- flac
- wav
- ape
- ogg
> 本地音乐会搜索 mp3 和 flac 格式的文件,下载的歌曲是 mp3 格式的。
## 其他参数
- XIAOMUSIC_ACTIVE_CMD 环境变量,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
## 在 Docker 里使用
```shell
docker run -e MI_USER=<your-xiaomi-account> \
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' \
@@ -162,6 +215,26 @@ services:
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'
```
## 简易的控制面板
@@ -169,11 +242,14 @@ services:
- ip 是 XIAOMUSIC_HOSTNAME 设置的
- 8090 是默认端口
- 功能
- 支持功能
- 显示正在播放的歌曲
- 模糊搜索本地歌曲
- 播放列表
- 删除歌曲
- 设置页面
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER
@@ -186,21 +262,33 @@ services:
- XIAOMUSIC_SEARCH
- XIAOMUSIC_PROXY
## 网络歌单功能
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
> 欢迎有想法的朋友们制作更多的歌单转换工具。
## 更多其他可选配置
- XIAOMUSIC_ACTIVE_CMD 配置唤醒令,具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_ACTIVE_CMD 环境变量,用于唤醒令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录
- XIAOMUSIC_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 用来存放配置文件的目录记得把目录映射到主机默认情况会把配置存放在music目录具体见 <https://github.com/hanxi/xiaomusic/issues/74>
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82>
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
## 讨论区
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
- [点击链接加入群聊【xiaomusic】 604526973](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=13St5PLVcTxYlWTAs_iAawazjtdD1l-a&authKey=dJWEpaT2fDBDpdUUOWj%2FLt6NS1ePBfShDfz7a6seNURi05VvVnAGQzXF%2FM%2F5HgIm&noverify=0&group_code=604526973)
- https://github.com/hanxi/xiaomusic/issues
- <https://github.com/hanxi/xiaomusic/issues>
- [微信群二维码](https://github.com/hanxi/xiaomusic/issues/86)
## 感谢
@@ -218,4 +306,7 @@ services:
[![Star History Chart](https://api.star-history.com/svg?repos=hanxi/xiaomusic&type=Date)](https://star-history.com/#hanxi/xiaomusic&Date)
## 赞赏
谢谢就够了
- 爱发电 <https://afdian.net/a/imhanxi>
- 点个 Star ⭐
- 谢谢 ❤️

49
pdm.lock generated
View File

@@ -519,7 +519,7 @@ files = [
[[package]]
name = "miservice-fork"
version = "2.5.0"
version = "2.6.0"
requires_python = ">=3.8"
summary = "XiaoMi Cloud Service fork from https://github.com/Yonsm/MiService"
dependencies = [
@@ -528,8 +528,8 @@ dependencies = [
"rich",
]
files = [
{file = "miservice_fork-2.5.0-py3-none-any.whl", hash = "sha256:97b6360ea53c34fe035ac9d94e8705f305b8fa7fc2b44a7aea182449a76cb622"},
{file = "miservice_fork-2.5.0.tar.gz", hash = "sha256:8ca2d370d5b32f7e330add38aa1912d734aefa7880f16cef9eac110a5a3029e2"},
{file = "miservice_fork-2.6.0-py3-none-any.whl", hash = "sha256:98169a77ea41a7b9392e1b1fab8cb80a4165fed8a9e882d9ada9a16dd1120347"},
{file = "miservice_fork-2.6.0.tar.gz", hash = "sha256:a59d337d1f7a92566aa147e96595a8d2f5bf3f7000ae5e7dd9ed451f18d6e2fd"},
]
[[package]]
@@ -662,27 +662,28 @@ files = [
[[package]]
name = "ruff"
version = "0.4.9"
version = "0.5.0"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [
{file = "ruff-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b262ed08d036ebe162123170b35703aaf9daffecb698cd367a8d585157732991"},
{file = "ruff-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:98ec2775fd2d856dc405635e5ee4ff177920f2141b8e2d9eb5bd6efd50e80317"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4555056049d46d8a381f746680db1c46e67ac3b00d714606304077682832998e"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e91175fbe48f8a2174c9aad70438fe9cb0a5732c4159b2a10a3565fea2d94cde"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8e7b95673f22e0efd3571fb5b0cf71a5eaaa3cc8a776584f3b2cc878e46bff"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2d45ddc6d82e1190ea737341326ecbc9a61447ba331b0a8962869fcada758505"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78de3fdb95c4af084087628132336772b1c5044f6e710739d440fc0bccf4d321"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06b60f91bfa5514bb689b500a25ba48e897d18fea14dce14b48a0c40d1635893"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88bffe9c6a454bf8529f9ab9091c99490578a593cc9f9822b7fc065ee0712a06"},
{file = "ruff-0.4.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:673bddb893f21ab47a8334c8e0ea7fd6598ecc8e698da75bcd12a7b9d0a3206e"},
{file = "ruff-0.4.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c1aff58c31948cc66d0b22951aa19edb5af0a3af40c936340cd32a8b1ab7438"},
{file = "ruff-0.4.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:784d3ec9bd6493c3b720a0b76f741e6c2d7d44f6b2be87f5eef1ae8cc1d54c84"},
{file = "ruff-0.4.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:732dd550bfa5d85af8c3c6cbc47ba5b67c6aed8a89e2f011b908fc88f87649db"},
{file = "ruff-0.4.9-py3-none-win32.whl", hash = "sha256:8064590fd1a50dcf4909c268b0e7c2498253273309ad3d97e4a752bb9df4f521"},
{file = "ruff-0.4.9-py3-none-win_amd64.whl", hash = "sha256:e0a22c4157e53d006530c902107c7f550b9233e9706313ab57b892d7197d8e52"},
{file = "ruff-0.4.9-py3-none-win_arm64.whl", hash = "sha256:5d5460f789ccf4efd43f265a58538a2c24dbce15dbf560676e430375f20a8198"},
{file = "ruff-0.4.9.tar.gz", hash = "sha256:f1cb0828ac9533ba0135d148d214e284711ede33640465e706772645483427e3"},
{file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"},
{file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"},
{file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"},
{file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"},
{file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"},
{file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"},
{file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"},
]
[[package]]
@@ -831,7 +832,7 @@ files = [
[[package]]
name = "yt-dlp"
version = "2024.6.17.232743.dev0"
version = "2024.6.24.232830.dev0"
requires_python = ">=3.8"
summary = "A feature-rich command-line audio/video downloader"
dependencies = [
@@ -845,6 +846,6 @@ dependencies = [
"websockets>=12.0",
]
files = [
{file = "yt_dlp-2024.6.17.232743.dev0-py3-none-any.whl", hash = "sha256:dd6e7e194b96e778691f58a0cb6b42956cf956b22f6bb1a12bdef5ab3ac0c9ad"},
{file = "yt_dlp-2024.6.17.232743.dev0.tar.gz", hash = "sha256:2f6f44eff755a7b051cdcd3c4375771033dbeb64d6164351022efdc67cce0c52"},
{file = "yt_dlp-2024.6.24.232830.dev0-py3-none-any.whl", hash = "sha256:efffecef44ce688e9ee3c02226eb1ba4ad64b37744726e9e4df5c2bd04ea93c5"},
{file = "yt_dlp-2024.6.24.232830.dev0.tar.gz", hash = "sha256:0e89b46958984954393692a8c41e0f6d76a773be2df381c3d3a4ff24ce89aa32"},
]

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.1.56"
version = "0.1.76"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},

View File

@@ -305,9 +305,9 @@ MarkupSafe==2.1.4 \
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
miservice-fork==2.5.0 \
--hash=sha256:8ca2d370d5b32f7e330add38aa1912d734aefa7880f16cef9eac110a5a3029e2 \
--hash=sha256:97b6360ea53c34fe035ac9d94e8705f305b8fa7fc2b44a7aea182449a76cb622
miservice-fork==2.6.0 \
--hash=sha256:98169a77ea41a7b9392e1b1fab8cb80a4165fed8a9e882d9ada9a16dd1120347 \
--hash=sha256:a59d337d1f7a92566aa147e96595a8d2f5bf3f7000ae5e7dd9ed451f18d6e2fd
multidict==6.0.4 \
--hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \
--hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \
@@ -472,6 +472,6 @@ yarl==1.9.2 \
--hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \
--hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \
--hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560
yt-dlp==2024.6.17.232743.dev0 \
--hash=sha256:2f6f44eff755a7b051cdcd3c4375771033dbeb64d6164351022efdc67cce0c52 \
--hash=sha256:dd6e7e194b96e778691f58a0cb6b42956cf956b22f6bb1a12bdef5ab3ac0c9ad
yt-dlp==2024.6.24.232830.dev0 \
--hash=sha256:0e89b46958984954393692a8c41e0f6d76a773be2df381c3d3a4ff24ce89aa32 \
--hash=sha256:efffecef44ce688e9ee3c02226eb1ba4ad64b37744726e9e4df5c2bd04ea93c5

View File

@@ -1 +1 @@
__version__ = "0.1.56"
__version__ = "0.1.76"

View File

@@ -7,18 +7,15 @@ from dataclasses import dataclass
from xiaomusic.utils import validate_proxy
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}&timestamp={timestamp}&limit=2"
COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}"
KEY_WORD_DICT = {
# 默认口令
DEFAULT_KEY_WORD_DICT = {
"播放歌曲": "play",
"歌曲": "play",
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "random_play",
"关机": "stop",
"停止播放": "stop",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
@@ -31,29 +28,21 @@ KEY_WORD_ARG_BEFORE_DICT = {
"分钟后关机": True,
}
# 匹配优先级
KEY_MATCH_ORDER = [
# 口令匹配优先级
DEFAULT_KEY_MATCH_ORDER = [
"set_volume#",
"get_volume#",
"分钟后关机",
"播放歌曲",
"放歌曲",
"下一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"停止播放",
"刷新列表",
"播放列表",
]
SUPPORT_MUSIC_TYPE = [
".mp3",
".flac",
".wav",
]
@dataclass
class Config:
@@ -72,7 +61,7 @@ class Config:
"XIAOMUSIC_SEARCH", "ytsearch:"
) # "bilisearch:" or "ytsearch:"
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
active_cmd: str = os.getenv("XIAOMUSIC_ACTIVE_CMD", "play,random_play")
active_cmd: str = os.getenv("XIAOMUSIC_ACTIVE_CMD", "play,random_play,playlocal,play_music_list")
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir")
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
disable_httpauth: bool = (
@@ -80,10 +69,35 @@ class Config:
)
httpauth_username: str = os.getenv("XIAOMUSIC_HTTPAUTH_USERNAME", "admin")
httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "admin")
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()
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")
def append_keyword(self, keys, action):
for key in keys.split(","):
self.key_word_dict[key] = action
if key not in self.key_match_order:
self.key_match_order.append(key)
def __post_init__(self) -> None:
if self.proxy:
validate_proxy(self.proxy)
keywords_playlocal = os.getenv(
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
)
self.append_keyword(keywords_playlocal, "playlocal")
keywords_play = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
self.append_keyword(keywords_play, "play")
keywords_stop = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止")
self.append_keyword(keywords_stop, "stop")
@classmethod
def from_options(cls, options: argparse.Namespace) -> Config:

10
xiaomusic/const.py Normal file
View File

@@ -0,0 +1,10 @@
SUPPORT_MUSIC_TYPE = [
".mp3",
".flac",
".wav",
".ape",
".ogg",
]
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}&timestamp={timestamp}&limit=2"
COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}"

View File

@@ -2,15 +2,15 @@
import os
from threading import Thread
from flask import Flask, request, send_from_directory
from flask import Flask, request, send_from_directory, send_file
from flask_httpauth import HTTPBasicAuth
from waitress import serve
from xiaomusic import (
__version__,
)
from xiaomusic.config import (
KEY_WORD_DICT,
from xiaomusic.utils import (
downloadfile,
)
app = Flask(__name__)
@@ -38,7 +38,7 @@ def verify_password(username, password):
@app.route("/allcmds")
@auth.login_required
def allcmds():
return KEY_WORD_DICT
return xiaomusic.config.key_word_dict
@app.route("/getversion", methods=["GET"])
@@ -71,6 +71,12 @@ def playingmusic():
return xiaomusic.playingmusic()
@app.route("/isplaying", methods=["GET"])
@auth.login_required
def isplaying():
return xiaomusic.isplaying()
@app.route("/", methods=["GET"])
def index():
return send_from_directory("static", "index.html")
@@ -103,6 +109,8 @@ async def getsetting():
"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,
}
return data
@@ -137,6 +145,28 @@ def delmusic():
return "success"
@app.route("/downloadjson", methods=["POST"])
@auth.login_required
def downloadjson():
data = request.get_json()
log.info(data)
url = data["url"]
try:
ret = "OK"
content = downloadfile(url)
except Exception as e:
log.warning(f"downloadjson failed. url:{url} e:{e}")
ret = "Download JSON file failed."
return {
"ret": ret,
"content": content,
}
@app.route("/downloadlog", methods=["GET"])
@auth.login_required
def downloadlog():
return send_file(xiaomusic.config.log_file, as_attachment=True)
def static_path_handler(filename):
log.debug(filename)
log.debug(static_path)

View File

@@ -18,7 +18,7 @@
<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="64px" width="64px" 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>
<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>

64
xiaomusic/static/m3u.html Normal file
View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<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">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole 默认会挂载到 `window.VConsole` 上
var vConsole = new window.VConsole();
</script>
-->
<script>
function handleFileSelect(evt) {
var file = evt.target.files[0];
if (file) {
var reader = new FileReader();
reader.onload = function(e) {
document.getElementById('m3u-input').value = e.target.result;
};
reader.readAsText(file);
} else {
alert('无法加载文件');
}
}
function convertToJSON() {
var m3uContent = document.getElementById('m3u-input').value;
var lines = m3uContent.split('\n');
console.log(lines);
var musicsArray = [];
var currentName = '';
lines.forEach(function(line) {
line = line.trim();
if (line.startsWith('#EXTINF:')) {
currentName = line.replace(/.*,/g, '');
} else if (line.startsWith('http') && currentName !== '') {
musicsArray.push({"name": currentName, "type": "radio", "url": line});
currentName = ''; // Reset the name for the next entry
}
});
var output = [{
"name": "m3u电台",
"musics": musicsArray
}];
document.getElementById('json-output').value = JSON.stringify(output, null, 2);
}
</script>
</head>
<body>
<h1>M3U to JSON Converter</h1>
<input type="file" id="file-input" accept=".m3u" onchange="handleFileSelect(event)"/><br>
<textarea id="m3u-input" rows="10" cols="50" placeholder="粘贴m3u内容或上传文件..."></textarea><br>
<button onclick="convertToJSON()">转换</button><br>
<textarea id="json-output" rows="10" cols="50" placeholder="转换后的JSON..."></textarea>
</body>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>
</html>

View File

@@ -13,8 +13,8 @@
<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"></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>
@@ -22,10 +22,19 @@
</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>
</div>
<hr>
<button onclick="location.href='/';">返回首页</button>
<button id="get_music_list">获取歌单</button>
<button id="save">保存</button>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<hr>
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>

View File

@@ -5,33 +5,49 @@ $(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]);
}
}
});
};
// 拉取现有配置
$.get("/getsetting", function(data, status) {
console.log(data, status);
var mi_did_div = $("#mi_did")
mi_did_div.empty();
$.each(data.mi_did_list, function(index, option){
mi_did_div.append($('<option>', {
value:option,
text:option,
}));
if (data.mi_did == option) {
mi_did_div.val(option);
}
});
updateSelectOptions("#mi_did", data.mi_did_list, data.mi_did);
updateSelectOptions("#mi_hardware", data.mi_hardware_list, data.mi_hardware);
var mi_hardware_div = $("#mi_hardware")
mi_hardware_div.empty();
$.each(data.mi_hardware_list, function(index, option){
mi_hardware_div.append($('<option>', {
value:option,
text:option,
}));
if (data.mi_hardware == option) {
mi_hardware_div.val(option);
}
});
// 初始化联动
linkSelects('#mi_did', data.mi_did_list, '#mi_hardware', data.mi_hardware_list);
if (data.xiaomusic_search != "") {
$("#xiaomusic_search").val(data.xiaomusic_search);
@@ -40,6 +56,14 @@ $(function(){
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);
}
});
$("#save").on("click", () => {
@@ -47,15 +71,21 @@ $(function(){
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,
};
$.ajax({
type: "POST",
@@ -70,4 +100,30 @@ $(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 data = {
url: xiaomusic_music_list_url,
};
$.ajax({
type: "POST",
url: "/downloadjson",
contentType: "application/json",
data: JSON.stringify(data),
success: (res) => {
if (res.ret == "OK") {
$("#xiaomusic_music_list_json").val(res.content);
} else {
console.log(res);
alert(res.ret);
}
},
error: (res) => {
console.log(res);
alert(res);
}
});
});
});

View File

@@ -1,4 +1,8 @@
button {
.button {
line-height: 50px;
font-size: 14px;
}
button, .button {
margin: 10px;
width: 100px;
height: 50px;
@@ -10,7 +14,7 @@ button {
border-radius: 10px;
background-color: #008CBA;
}
button:active {
button:active, .button:active {
font-weight:bold;
background-color: #007CBA;
transform: translateY(2px);
@@ -67,3 +71,9 @@ footer {
text-align: center;
padding: 10px 0;
}
textarea{
margin: 10px;
width: 300px;
height: 200px;
}

View File

@@ -3,13 +3,21 @@ from __future__ import annotations
import difflib
import os
import random
import re
import string
import tempfile
from collections.abc import AsyncIterator
from http.cookies import SimpleCookie
from urllib.parse import urlparse
import aiohttp
import mutagen
import requests
from requests.utils import cookiejar_from_dict
from xiaomusic.const import SUPPORT_MUSIC_TYPE
### HELP FUNCTION ###
def parse_cookie_string(cookie_string):
@@ -144,3 +152,86 @@ def walk_to_depth(root, depth=None, *args, **kwargs):
except KeyError:
yield from main_func(root, depth, *args, **kwargs)
return
def downloadfile(url):
# 清理和验证URL
# 解析URL
parsed_url = urlparse(url)
# 基础验证仅允许HTTP和HTTPS协议
if parsed_url.scheme not in ("http", "https"):
raise Warning(
f"Invalid URL scheme: {parsed_url.scheme}. Only HTTP and HTTPS are allowed."
)
# 构建目标URL
cleaned_url = parsed_url.geturl()
# 发起请求
response = requests.get(cleaned_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
return response.text
async def _get_web_music_duration(session, url, start=0, end=500):
duration = 0
headers = {"Range": f"bytes={start}-{end}"}
async with session.get(url, headers=headers) as response:
array_buffer = await response.read()
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp.write(array_buffer)
name = tmp.name
try:
m = mutagen.File(name)
duration = m.info.length
except Exception:
pass
os.remove(name)
return duration
async def get_web_music_duration(url, start=0, end=500):
duration = 0
try:
parsed_url = urlparse(url)
file_path = parsed_url.path
_, extension = os.path.splitext(file_path)
if extension.lower() not in SUPPORT_MUSIC_TYPE:
cleaned_url = parsed_url.geturl()
async with aiohttp.ClientSession() as session:
async with session.get(
cleaned_url,
allow_redirects=True,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
},
) as response:
url = str(response.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)
if duration <= 0:
duration = await _get_web_music_duration(
session, url, start=0, end=1000
)
except Exception:
pass
return duration, url
# 获取文件播放时长
def get_local_music_duration(filename):
duration = 0
try:
m = mutagen.File(filename)
duration = m.info.length
except Exception:
pass
return duration
def get_random(length):
return "".join(random.sample(string.ascii_letters + string.digits, length))

View File

@@ -10,8 +10,9 @@ import time
import traceback
import urllib.parse
from pathlib import Path
import copy
import mutagen
from logging.handlers import RotatingFileHandler
from aiohttp import ClientSession, ClientTimeout
from miservice import MiAccount, MiIOService, MiNAService
@@ -19,18 +20,21 @@ from xiaomusic import (
__version__,
)
from xiaomusic.config import (
COOKIE_TEMPLATE,
KEY_MATCH_ORDER,
KEY_WORD_ARG_BEFORE_DICT,
KEY_WORD_DICT,
Config,
)
from xiaomusic.const import (
COOKIE_TEMPLATE,
LATEST_ASK_API,
SUPPORT_MUSIC_TYPE,
Config,
)
from xiaomusic.httpserver import StartHTTPServer
from xiaomusic.utils import (
custom_sort_key,
fuzzyfinder,
get_local_music_duration,
get_random,
get_web_music_duration,
parse_cookie_string,
walk_to_depth,
)
@@ -80,6 +84,7 @@ class XiaoMusic:
self._timeout = 0
self._volume = 0
self._all_music = {}
self._all_radio = {} # 电台列表
self._play_list = []
self._cur_play_list = ""
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
@@ -88,14 +93,8 @@ class XiaoMusic:
# 关机定时器
self._stop_timer = None
# setup logger
logging.basicConfig(
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
datefmt="[%X]",
)
self.log = logging.getLogger("xiaomusic")
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
self.log.debug(config)
# 初始化日志
self.setup_logger()
# 尝试从设置里加载配置
self.try_init_setting()
@@ -106,7 +105,28 @@ class XiaoMusic:
# 启动时初始化获取声音
self.set_last_record("get_volume#")
self.log.debug("ffmpeg_location: %s", self.ffmpeg_location)
def setup_logger(self):
logging.basicConfig(
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
datefmt="[%X]",
)
log_file = self.config.log_file
log_path = os.path.dirname(log_file)
if not os.path.exists(log_path):
os.makedirs(log_path)
if os.path.exists(log_file):
os.remove(log_file)
handler = RotatingFileHandler(self.config.log_file, maxBytes=10*1024*1024, backupCount=1)
self.log = logging.getLogger("xiaomusic")
self.log.addHandler(handler)
self.log.setLevel(logging.DEBUG if self.config.verbose else logging.INFO)
debug_config = copy.deepcopy(self.config)
debug_config.account = '******'
debug_config.password = '******'
debug_config.httpauth_username = '******'
debug_config.httpauth_password = '******'
self.log.info(debug_config)
async def poll_latest_ask(self):
async with ClientSession() as session:
@@ -127,7 +147,9 @@ class XiaoMusic:
async def init_all_data(self, session):
await self.login_miboy(session)
await self._init_data_hardware()
session.cookie_jar.update_cookies(self.get_cookie())
cookie_jar = self.get_cookie()
if cookie_jar:
session.cookie_jar.update_cookies(cookie_jar)
self.cookie_jar = session.cookie_jar
async def login_miboy(self, session):
@@ -143,27 +165,30 @@ class XiaoMusic:
self.miio_service = MiIOService(account)
async def try_update_device_id(self):
hardware_data = await self.mina_service.device_list()
# 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
for h in hardware_data:
if did := self.config.mi_did:
if h.get("miotDID", "") == str(did):
try:
hardware_data = await self.mina_service.device_list()
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:
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"
)
else:
self.log.error(
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
)
except Exception as e:
self.log.error(f"Execption {e}")
async def _init_data_hardware(self):
if self.config.cookie:
@@ -193,15 +218,19 @@ class XiaoMusic:
cookie_dict = cookie_jar.get_dict()
self.device_id = cookie_dict["deviceId"]
return cookie_jar
else:
with open(self.mi_token_home) as f:
user_data = json.loads(f.read())
user_id = user_data.get("userId")
service_token = user_data.get("micoapi")[1]
cookie_string = COOKIE_TEMPLATE.format(
device_id=self.device_id, service_token=service_token, user_id=user_id
)
return parse_cookie_string(cookie_string)
if not os.path.exists(self.mi_token_home):
self.log.error(f"{self.mi_token_home} file not exist")
return None
with open(self.mi_token_home) as f:
user_data = json.loads(f.read())
user_id = user_data.get("userId")
service_token = user_data.get("micoapi")[1]
cookie_string = COOKIE_TEMPLATE.format(
device_id=self.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):
retries = 3
@@ -221,8 +250,8 @@ class XiaoMusic:
continue
try:
data = await r.json()
except Exception:
self.log.warning("get latest ask from xiaoai error, retry")
except Exception as e:
self.log.warning(f"get latest ask from xiaoai error {e}, retry")
if i == 2:
# tricky way to fix #282 #272 # if it is the third time we re init all data
self.log.info("Maybe outof date trying to re init it")
@@ -256,8 +285,13 @@ class XiaoMusic:
await self.force_stop_xiaoai()
try:
await self.mina_service.text_to_speech(self.device_id, value)
except Exception:
pass
except Exception as e:
self.log.error(f"Execption {e}")
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
if self._playing and not self.is_downloading():
# 继续播放歌曲
await self.play()
async def do_set_volume(self, value):
value = int(value)
@@ -265,8 +299,8 @@ class XiaoMusic:
self.log.info(f"声音设置为{value}")
try:
await self.mina_service.player_set_volume(self.device_id, value)
except Exception:
pass
except Exception as e:
self.log.error(f"Execption {e}")
async def force_stop_xiaoai(self):
await self.mina_service.player_stop(self.device_id)
@@ -308,11 +342,10 @@ class XiaoMusic:
if self.proxy:
sbp_args += ("--proxy", f"{self.proxy}")
self.log.debug(f"download: {sbp_args}")
self.log.info(f"download: {sbp_args}")
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
await self.do_tts(f"正在下载歌曲{search_key}")
# 本地是否存在歌曲
def get_filename(self, name):
if name not in self._all_music:
self.log.debug("get_filename not in. name:%s", name)
@@ -323,10 +356,60 @@ class XiaoMusic:
return filename
return ""
# 获取歌曲播放地址
def get_file_url(self, name):
# 判断本地音乐是否存在,网络歌曲不判断
def is_music_exist(self, name):
if name not in self._all_music:
return False
if self.is_web_music(name):
return True
filename = self.get_filename(name)
self.log.debug("get_file_url. name:%s, filename:%s", name, filename)
if filename:
return True
return False
# 是否是网络电台
def is_web_radio_music(self, name):
return name in self._all_radio
# 是否是网络歌曲
def is_web_music(self, name):
if name not in self._all_music:
return False
url = self._all_music[name]
return url.startswith(("http://", "https://"))
# 获取歌曲播放时长,播放地址
async def get_music_sec_url(self, name):
sec = 0
url = self.get_music_url(name)
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)
sec = int(duration)
self.log.info(f"网络歌曲 {name} : {origin_url} {url} 的时长 {sec}")
else:
filename = self.get_filename(name)
sec = int(get_local_music_duration(filename))
self.log.info(f"本地歌曲 {name} : {filename} {url} 的时长 {sec}")
if sec <= 0:
self.log.warning(f"获取歌曲时长失败 {name} {url}")
return sec, url
def get_music_url(self, name):
if self.is_web_music(name):
url = self._all_music[name]
self.log.debug("get_music_url web music. name:%s, url:%s", name, url)
return url
filename = self.get_filename(name)
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}"
@@ -363,7 +446,6 @@ class XiaoMusic:
# 歌曲名字相同会覆盖
self._all_music[name] = os.path.join(root, filename)
all_music_by_dir[dir_name][name] = True
pass
self._play_list = list(self._all_music.keys())
self._cur_play_list = "全部"
self._gen_play_list()
@@ -374,7 +456,46 @@ class XiaoMusic:
for dir_name, musics in all_music_by_dir.items():
self._music_list[dir_name] = list(musics.keys())
self.log.debug("dir_name:%s, list:%s", dir_name, self._music_list[dir_name])
pass
try:
self._append_music_list()
except Exception as e:
self.log.error(f"Execption _append_music_list {e}")
# 给歌单里补充网络歌单
def _append_music_list(self):
if not self.config.music_list_json:
return
music_list = json.loads(self.config.music_list_json)
try:
for item in music_list:
list_name = item.get("name")
musics = item.get("musics")
if (not list_name) or (not musics):
continue
one_music_list = []
for music in musics:
name = music.get("name")
url = music.get("url")
music_type = music.get("type")
if (not name) or (not url):
continue
self._all_music[name] = url
one_music_list.append(name)
# 处理电台列表
if music_type == "radio":
self._all_radio[name] = url
self.log.debug(one_music_list)
# 歌曲名字相同会覆盖
self._music_list[list_name] = one_music_list
if self._all_radio:
self._music_list["所有电台"] = list(self._all_radio.keys())
self.log.debug(self._all_music)
self.log.debug(self._music_list)
except Exception as e:
self.log.error(f"Execption music_list:{music_list} {e}")
# 歌曲排序或者打乱顺序
def _gen_play_list(self):
@@ -408,29 +529,21 @@ class XiaoMusic:
if next_index >= play_list_len:
next_index = 0
name = self._play_list[next_index]
filename = self.get_filename(name)
if len(filename) <= 0:
if not self.is_music_exist(name):
self._play_list.pop(next_index)
self.log.info(f"pop not exist music:{name}")
return self.get_next_music()
return name
# 获取文件播放时长
def get_file_duration(self, filename):
# 获取音频文件对象
audio = mutagen.File(filename)
# 获取播放时长
duration = audio.info.length
return duration
# 设置下一首歌曲的播放定时器
def set_next_music_timeout(self):
filename = self.get_filename(self.cur_music)
sec = int(self.get_file_duration(filename))
self.log.info(f"歌曲 {self.cur_music} : {filename} 的时长 {sec}")
async def set_next_music_timeout(self, sec):
if sec <= 0:
return
if self._next_timer:
self._next_timer.cancel()
self.log.info("定时器已取消")
self._timeout = sec
async def _do_next():
@@ -441,7 +554,7 @@ class XiaoMusic:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
self._next_timer = asyncio.ensure_future(_do_next())
self.log.info(f"{sec}秒后将会播放下一首")
self.log.info(f"{sec}秒后将会播放下一首歌曲")
async def run_forever(self):
StartHTTPServer(self.port, self.music_path, self)
@@ -451,10 +564,11 @@ class XiaoMusic:
task = asyncio.create_task(self.poll_latest_ask())
assert task is not None # to keep the reference to task, do not remove this
filtered_keywords = [
keyword for keyword in KEY_MATCH_ORDER if "#" not in keyword
keyword for keyword in self.config.key_match_order if "#" not in keyword
]
joined_keywords = "/".join(filtered_keywords)
self.log.info(f"Running xiaomusic now, 用`{joined_keywords}`开头来控制")
self.log.info(self.config.key_word_dict)
while True:
self.polling_event.set()
@@ -489,7 +603,7 @@ class XiaoMusic:
# 匹配命令
def match_cmd(self, query, ctrl_panel):
for opkey in KEY_MATCH_ORDER:
for opkey in self.config.key_match_order:
patternarg = rf"(.*){opkey}(.*)"
# 匹配参数
matcharg = re.match(patternarg, query)
@@ -506,7 +620,7 @@ class XiaoMusic:
argafter,
)
oparg = argafter
opvalue = KEY_WORD_DICT[opkey]
opvalue = self.config.key_word_dict.get(opkey)
if not ctrl_panel and not self._playing:
if self.active_cmd and opvalue not in self.active_cmd:
self.log.debug(f"不在激活命令中 {opvalue}")
@@ -522,23 +636,82 @@ class XiaoMusic:
return ("stop", {})
return (None, None)
# 判断是否播放一下私募歌曲
# 判断是否播放下一首歌曲
def check_play_next(self):
# 当前没我在播放的歌曲
if self.cur_music == "":
return True
else:
filename = self.get_filename(self.cur_music)
# 当前播放的歌曲不存在了
if len(filename) <= 0:
if not self.is_music_exist(self.cur_music):
return True
pass
return False
async def _play_by_music_url(self, device_id, url):
audio_id = get_random(30)
audio_type = ""
if self.config.hardware in ["LX04", "X10A", "X08A"]:
audio_type = "MUSIC"
music = {
"payload": {
"audio_items": [
{"item_id": {"audio_id": audio_id}, "stream": {"url": url}}
],
"audio_type": audio_type,
}
}
return await self.mina_service.ubus_request(
device_id,
"player_play_music",
"mediaplayer",
{"startaudioid": audio_id, "music": json.dumps(music)},
)
async def play_url(self, url):
if self.config.use_music_api:
ret = await self._play_by_music_url(self.device_id, url)
self.log.debug(
f"play_url play_by_music_url {self.config.hardware}. ret:{ret} url:{url}"
)
else:
ret = await self.mina_service.play_by_url(self.device_id, url)
self.log.debug(
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
)
# 播放本地歌曲
async def playlocal(self, **kwargs):
name = kwargs.get("arg1", "")
if name == "":
if self.check_play_next():
await self.play_next()
return
else:
name = self.cur_music
self.log.info(f"playlocal. name:{name}")
# 本地歌曲不存在时下载
if not self.is_music_exist(name):
await self.do_tts(f"本地不存在歌曲{name}")
return
await self._playmusic(name)
async def _playmusic(self, name):
self._playing = True
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()
await self.play_url(url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
await self.set_next_music_timeout(sec)
# 播放歌曲
async def play(self, **kwargs):
self._playing = True
parts = kwargs["arg1"].split("|")
parts = kwargs.get("arg1", "").split("|")
search_key = parts[0]
name = parts[1] if len(parts) > 1 else search_key
if name == "":
@@ -551,25 +724,19 @@ class XiaoMusic:
else:
name = self.cur_music
self.log.debug("play. search_key:%s name:%s", search_key, name)
filename = self.get_filename(name)
self.log.info("play. search_key:%s name:%s", search_key, name)
if len(filename) <= 0:
# 本地歌曲不存在时下载
if not self.is_music_exist(name):
if self.config.disable_download:
await self.do_tts(f"本地不存在歌曲{name}")
return
await self.download(search_key, name)
self.log.info("正在下载中 %s", search_key + ":" + name)
await self.download_proc.wait()
# 把文件插入到播放列表里
self.add_download_music(name)
self.cur_music = name
self.log.info("cur_music %s", self.cur_music)
url = self.get_file_url(name)
self.log.info("播放 %s", url)
await self.force_stop_xiaoai()
await self.mina_service.play_by_url(self.device_id, url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
self.set_next_music_timeout()
await self._playmusic(name)
# 下一首
async def play_next(self, **kwargs):
@@ -607,7 +774,6 @@ class XiaoMusic:
# 刷新列表
async def gen_music_list(self, **kwargs):
self._gen_all_music_list()
await self.do_tts("生成播放列表完毕")
# 删除歌曲
def del_music(self, name):
@@ -620,12 +786,11 @@ class XiaoMusic:
self.log.info(f"del ${filename} success")
except OSError:
self.log.error(f"del ${filename} failed")
pass
self._gen_all_music_list()
# 播放一个播放列表
async def play_music_list(self, **kwargs):
parts = kwargs["arg1"].split("|")
parts = kwargs.get("arg1").split("|")
list_name = parts[0]
if list_name not in self._music_list:
await self.do_tts(f"播放列表{list_name}不存在")
@@ -653,7 +818,7 @@ class XiaoMusic:
if self._stop_timer:
self._stop_timer.cancel()
self.log.info("关机定时器已取消")
minute = int(kwargs["arg1"])
minute = int(kwargs.get("arg1", 0))
async def _do_stop():
await asyncio.sleep(minute * 60)
@@ -666,7 +831,7 @@ class XiaoMusic:
self.log.info(f"{minute}分钟后将关机")
async def set_volume(self, **kwargs):
value = kwargs["arg1"]
value = kwargs.get("arg1", 0)
await self.do_set_volume(value)
async def get_volume(self, **kwargs):
@@ -700,6 +865,10 @@ class XiaoMusic:
self.log.debug("playingmusic. cur_music:%s", self.cur_music)
return self.cur_music
# 当前是否正在播放歌曲
def isplaying(self):
return self._playing
# 获取当前配置
def getconfig(self):
return self.config
@@ -734,24 +903,25 @@ class XiaoMusic:
await self.call_main_thread_function(self.reinit)
def update_config_from_setting(self, data):
self.config.mi_did = data["mi_did"]
self.config.hardware = data["mi_hardware"]
self.config.search_prefix = data["xiaomusic_search"]
self.config.proxy = data["xiaomusic_proxy"]
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.info("update_config_from_setting ok. data:%s", data)
self.log.debug("update_config_from_setting ok. data:%s", data)
# 重新初始化
async def reinit(self, **kwargs):
await self.try_update_device_id()
self._gen_all_music_list()
self.log.info("reinit success")
# 获取所有设备
async def getalldevices(self, **kwargs):
arg1 = kwargs["arg1"]
self.log.debug("getalldevices. arg1:%s", arg1)
did_list = []
hardware_list = []
hardware_data = await self.mina_service.device_list()