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

Compare commits

...

44 Commits

Author SHA1 Message Date
涵曦
b5ab8e398a new version v0.1.78 2024-06-29 15:34:41 +00:00
涵曦
8907e41818 修复播放列表的问题 2024-06-29 15:34:38 +00:00
涵曦
af25300917 new version v0.1.77 2024-06-29 13:24:37 +00:00
涵曦
f18b2f49bf fix: #52 支持配置模糊匹配本地歌曲 2024-06-29 13:21:37 +00:00
涵曦
db1e4e6fc4 整理代码 2024-06-29 12:42:15 +00:00
涵曦
dc49f63a37 新增直接播放链接的接口 2024-06-29 12:41:29 +00:00
涵曦
6837841872 Update README.md 2024-06-29 09:32:41 +08:00
涵曦
637347ae0c Update README.md 2024-06-29 09:18:26 +08:00
涵曦
ff968c4db4 Update README.md 2024-06-29 00:14:52 +08:00
涵曦
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
16 changed files with 379 additions and 139 deletions

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

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

View File

@@ -11,6 +11,8 @@
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
<https://github.com/hanxi/xiaomusic>
## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下:
@@ -28,10 +30,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 下载依赖
@@ -69,26 +106,34 @@ pdm run xiaomusic.py
## 已测试支持的设备
```txt
- L06A
- L07A
- S12
- S12A
- LX5A
- LX05
- L16A
- L17A
- LX06
- LX01
- L05B
- L05C
````
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
## 支持音乐格式
- mp3
- flac
- wav
- ape
- ogg
> 本地音乐会搜索 mp3 和 flac 格式的文件,下载的歌曲是 mp3 格式的。
## 其他参数
- XIAOMUSIC_ACTIVE_CMD 环境变量,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
## 在 Docker 里使用
@@ -179,6 +224,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'
```
## 简易的控制面板
@@ -186,11 +251,14 @@ services:
- ip 是 XIAOMUSIC_HOSTNAME 设置的
- 8090 是默认端口
- 功能
- 支持功能
- 显示正在播放的歌曲
- 模糊搜索本地歌曲
- 播放列表
- 删除歌曲
- 设置页面
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER
@@ -205,26 +273,31 @@ services:
## 网络歌单功能
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
可以配置一个 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_VERBOSE 设置为 true 时开启 debug 日志,用于排查问题
- 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/86)
## 感谢
@@ -242,4 +315,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.70"
version = "0.1.78"
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.70"
__version__ = "0.1.78"

View File

@@ -7,9 +7,6 @@ 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}"
# 默认口令
DEFAULT_KEY_WORD_DICT = {
"播放歌曲": "play",
@@ -46,13 +43,6 @@ DEFAULT_KEY_MATCH_ORDER = [
"播放列表",
]
SUPPORT_MUSIC_TYPE = [
".mp3",
".flac",
".wav",
".ape",
]
@dataclass
class Config:
@@ -71,7 +61,9 @@ 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,playlocal")
active_cmd: str = os.getenv(
"XIAOMUSIC_ACTIVE_CMD", "play,random_play,playlocal,play_music_list,stop"
)
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir")
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
disable_httpauth: bool = (
@@ -89,6 +81,13 @@ class Config:
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")
# 模糊搜索匹配的最低相似度阈值
fuzzy_match_cutoff: float = float(os.getenv("XIAOMUSIC_FUZZY_MATCH_CUTOFF", "0.6"))
# 开启模糊搜索
enable_fuzzy_match: bool = (
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
)
def append_keyword(self, keys, action):
for key in keys.split(","):

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,7 +2,7 @@
import os
from threading import Thread
from flask import Flask, request, send_from_directory
from flask import Flask, request, send_file, send_from_directory
from flask_httpauth import HTTPBasicAuth
from waitress import serve
@@ -163,6 +163,20 @@ def downloadjson():
}
@app.route("/downloadlog", methods=["GET"])
@auth.login_required
def downloadlog():
return send_file(xiaomusic.config.log_file, as_attachment=True)
@app.route("/playurl", methods=["GET"])
@auth.login_required
async def playurl():
url = request.args.get("url")
log.info(f"play_url:{url}")
return await xiaomusic.call_main_thread_function(xiaomusic.play_url, arg1=url)
def static_path_handler(filename):
log.debug(filename)
log.debug(static_path)

View File

@@ -82,6 +82,13 @@ $(function(){
}
});
$("#playurl").on("click", () => {
var url = $("#music-url").val();
$.get(`/playurl?url=${url}`, function(data, status) {
console.log(data);
});
});
function append_op_button_name(name) {
append_op_button(name, name);
}

View File

@@ -43,6 +43,12 @@
<button id="play_music_list">播放列表歌曲</button>
<button id="del_music">删除选中歌曲</button>
<hr>
<div>
<input id="music-url" type="text" placeholder="链接(http://ngcdn001.cnr.cn/live/zgzs/index.m3u8)"></input>
</div>
<button id="playurl">播放链接</button>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>

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>
@@ -31,6 +31,7 @@
<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>

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);

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);

View File

@@ -16,6 +16,8 @@ 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):
@@ -70,7 +72,12 @@ def validate_proxy(proxy_str: str) -> bool:
# 模糊搜索
def fuzzyfinder(user_input, collection):
return difflib.get_close_matches(user_input, collection, 10, cutoff=0.1)
return difflib.get_close_matches(user_input, collection, n=10, cutoff=0.1)
def find_best_match(user_input, collection, cutoff=0.6):
matches = difflib.get_close_matches(user_input, collection, n=1, cutoff=cutoff)
return matches[0] if matches else None
# 歌曲排序
@@ -193,6 +200,20 @@ async def _get_web_music_duration(session, url, start=0, end=500):
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:
@@ -203,7 +224,7 @@ async def get_web_music_duration(url, start=0, end=500):
)
except Exception:
pass
return duration
return duration, url
# 获取文件播放时长

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import asyncio
import copy
import json
import logging
import os
@@ -9,6 +10,7 @@ import re
import time
import traceback
import urllib.parse
from logging.handlers import RotatingFileHandler
from pathlib import Path
from aiohttp import ClientSession, ClientTimeout
@@ -18,15 +20,18 @@ from xiaomusic import (
__version__,
)
from xiaomusic.config import (
COOKIE_TEMPLATE,
KEY_WORD_ARG_BEFORE_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,
find_best_match,
fuzzyfinder,
get_local_music_duration,
get_random,
@@ -89,14 +94,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()
@@ -107,7 +106,30 @@ class XiaoMusic:
# 启动时初始化获取声音
self.set_last_record("get_volume#")
self.log.info("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:
@@ -268,10 +290,13 @@ class XiaoMusic:
await self.mina_service.text_to_speech(self.device_id, value)
except Exception as e:
self.log.error(f"Execption {e}")
# 最大等8秒
sec = min(8, int(len(value) / 3.3))
await asyncio.sleep(sec)
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
if self._playing and not self.is_downloading():
# 继续播放歌曲
self.log.info("继续播放歌曲")
await self.play()
async def do_set_volume(self, value):
@@ -284,7 +309,10 @@ class XiaoMusic:
self.log.error(f"Execption {e}")
async def force_stop_xiaoai(self):
await self.mina_service.player_stop(self.device_id)
ret = await self.mina_service.player_pause(self.device_id)
self.log.debug(f"force_stop_xiaoai player_pause ret:{ret}")
ret = await self.mina_service.player_stop(self.device_id)
self.log.debug(f"force_stop_xiaoai player_stop ret:{ret}")
# 是否在下载中
def is_downloading(self):
@@ -359,7 +387,28 @@ class XiaoMusic:
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]
@@ -496,30 +545,14 @@ class XiaoMusic:
return name
# 设置下一首歌曲的播放定时器
async def set_next_music_timeout(self):
name = self.cur_music
if self.is_web_radio_music(name):
self.log.info("电台不会有下一首的定时器")
async def set_next_music_timeout(self, sec):
if sec <= 0:
return
if self.is_web_music(name):
url = self._all_music[name]
duration = await get_web_music_duration(url)
sec = int(duration)
self.log.info(f"网络歌曲 {name} : {url} 的时长 {sec}")
else:
filename = self.get_filename(name)
sec = int(get_local_music_duration(filename))
self.log.info(f"本地歌曲 {name} : {filename} 的时长 {sec}")
if self._next_timer:
self._next_timer.cancel()
self.log.info("定时器已取消")
if sec <= 0:
self.log.warning("获取歌曲时长失败,不会开启下一首歌曲的定时器")
return
self._timeout = sec
async def _do_next():
@@ -530,7 +563,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)
@@ -563,7 +596,7 @@ class XiaoMusic:
self.polling_event.clear() # stop polling when processing the question
query = new_record.get("query", "").strip()
ctrl_panel = new_record.get("ctrl_panel", False)
self.log.debug("收到消息:%s 控制面板:%s", query, ctrl_panel)
self.log.info("收到消息:%s 控制面板:%s", query, ctrl_panel)
# 匹配命令
opvalue, oparg = self.match_cmd(query, ctrl_panel)
@@ -609,7 +642,7 @@ class XiaoMusic:
return (opvalue, oparg)
if self._playing:
self.log.info("未匹配到指令,自动停止")
return ("stop", {})
return ("stop", "notts")
return (None, None)
# 判断是否播放下一首歌曲
@@ -623,24 +656,53 @@ class XiaoMusic:
return True
return False
async def play_url(self, url):
if self.config.use_music_api:
audio_id = get_random(30)
music = {
"payload": {
"audio_items": [
{"item_id": {"audio_id": audio_id}, "stream": {"url": url}}
],
}
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(
self.device_id,
"player_play_music",
"mediaplayer",
{"startaudioid": audio_id, "music": json.dumps(music)},
}
return await self.mina_service.ubus_request(
device_id,
"player_play_music",
"mediaplayer",
{"startaudioid": audio_id, "music": json.dumps(music)},
)
async def play_url(self, **kwargs):
url = kwargs.get("arg1", "")
if self.config.use_music_api:
ret = await self._play_by_music_url(self.device_id, url)
self.log.debug(
f"play_url play_by_music_url {self.config.hardware}. ret:{ret} url:{url}"
)
else:
await self.mina_service.play_by_url(self.device_id, url)
ret = await self.mina_service.play_by_url(self.device_id, url)
self.log.debug(
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
)
return ret
def find_real_music_name(self, name):
if not self.config.enable_fuzzy_match:
self.log.debug("没开启模糊匹配")
return name
all_music_list = list(self._all_music.keys())
real_name = find_best_match(
name, all_music_list, cutoff=self.config.fuzzy_match_cutoff
)
if real_name:
self.log.info(f"根据【{name}】找到歌曲【{real_name}")
return real_name
self.log.info(f"没找到歌曲【{name}")
# 播放本地歌曲
async def playlocal(self, **kwargs):
@@ -655,6 +717,7 @@ class XiaoMusic:
self.log.info(f"playlocal. name:{name}")
# 本地歌曲不存在时下载
name = self.find_real_music_name(name)
if not self.is_music_exist(name):
await self.do_tts(f"本地不存在歌曲{name}")
return
@@ -664,13 +727,13 @@ class XiaoMusic:
self._playing = True
self.cur_music = name
self.log.info(f"cur_music {self.cur_music}")
url = self.get_music_url(name)
sec, url = await self.get_music_sec_url(name)
self.log.info(f"播放 {url}")
await self.force_stop_xiaoai()
await self.play_url(url)
await self.play_url(arg1=url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
await self.set_next_music_timeout()
await self.set_next_music_timeout(sec)
# 播放歌曲
async def play(self, **kwargs):
@@ -690,6 +753,7 @@ class XiaoMusic:
self.log.info("play. search_key:%s name:%s", search_key, name)
# 本地歌曲不存在时下载
name = self.find_real_music_name(name)
if not self.is_music_exist(name):
if self.config.disable_download:
await self.do_tts(f"本地不存在歌曲{name}")
@@ -751,10 +815,27 @@ class XiaoMusic:
self.log.error(f"del ${filename} failed")
self._gen_all_music_list()
def find_real_music_list_name(self, list_name):
if not self.config.enable_fuzzy_match:
self.log.debug("没开启模糊匹配")
return list_name
# 模糊搜一个播放列表
real_name = find_best_match(
list_name, self._music_list, cutoff=self.config.fuzzy_match_cutoff
)
if real_name:
self.log.info(f"根据【{list_name}】找到播放列表【{real_name}")
list_name = real_name
self.log.info(f"没找到播放列表【{list_name}")
return list_name
# 播放一个播放列表
async def play_music_list(self, **kwargs):
parts = kwargs.get("arg1").split("|")
list_name = parts[0]
list_name = self.find_real_music_list_name(list_name)
if list_name not in self._music_list:
await self.do_tts(f"播放列表{list_name}不存在")
return
@@ -772,6 +853,8 @@ class XiaoMusic:
async def stop(self, **kwargs):
self._playing = False
if kwargs.get("arg1", "") != "notts":
await self.do_tts("收到关机口令,再见")
if self._next_timer:
self._next_timer.cancel()
self.log.info("定时器已取消")
@@ -786,12 +869,12 @@ class XiaoMusic:
async def _do_stop():
await asyncio.sleep(minute * 60)
try:
await self.stop()
await self.stop(arg1="notts")
except Exception as e:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
self._stop_timer = asyncio.ensure_future(_do_stop())
self.log.info(f"{minute}分钟后将关机")
await self.do_tts(f"收到,{minute}分钟后将关机")
async def set_volume(self, **kwargs):
value = kwargs.get("arg1", 0)
@@ -801,7 +884,7 @@ class XiaoMusic:
playing_info = await self.mina_service.player_get_status(self.device_id)
self.log.debug("get_volume. playing_info:%s", playing_info)
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
"volume", 5
"volume", 0
)
self.log.info("get_volume. volume:%s", self._volume)