1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2025-12-10 15:28:15 +08:00

Compare commits

..

5 Commits

Author SHA1 Message Date
涵曦
f3658e0368 new version v0.1.63 2024-06-26 01:24:09 +00:00
涵曦
d28f4e1473 修复自动播放的问题 2024-06-26 01:18:58 +00:00
涵曦
1efdcf2144 修复自动播放的问题 2024-06-26 01:10:26 +00:00
涵曦
0f1516ae7e 优化日志输出 2024-06-26 00:22:23 +00:00
涵曦
f111d67819 优化不定长参数arg1的用法 2024-06-26 00:17:29 +00:00
16 changed files with 160 additions and 474 deletions

2
.github/FUNDING.yml vendored
View File

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

116
README.md
View File

@@ -1,13 +1,4 @@
# 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 下载。
@@ -28,45 +19,10 @@ 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 下载依赖
@@ -101,30 +57,21 @@ pdm run xiaomusic.py
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
## 已测试支持的设备
## 已测试设备
```txt
- L07A
- S12
- LX5A
- LX05
- L16A
- L17A
- LX06
- LX01
- L05B
- L05C
"L07A": ("5-1", "5-5"), # Redmi小爱音箱Play(l7a)
````
## 支持音乐格式
- mp3
- flac
- wav
- ape
- ogg
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
> 本地音乐会搜索 mp3 和 flac 格式的文件,下载的歌曲是 mp3 格式的。
## 其他参数
- XIAOMUSIC_ACTIVE_CMD 环境变量,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。
## 在 Docker 里使用
@@ -215,26 +162,6 @@ 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'
```
## 简易的控制面板
@@ -242,14 +169,11 @@ services:
- ip 是 XIAOMUSIC_HOSTNAME 设置的
- 8090 是默认端口
- 支持功能
- 功能
- 显示正在播放的歌曲
- 模糊搜索本地歌曲
- 播放列表
- 删除歌曲
- 设置页面
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER
@@ -262,33 +186,22 @@ services:
- XIAOMUSIC_SEARCH
- XIAOMUSIC_PROXY
## 网络歌单功能
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
> 欢迎有想法的朋友们制作更多的歌单转换工具。
## 更多其他可选配置
- XIAOMUSIC_ACTIVE_CMD 环境变量,用于唤醒令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_ACTIVE_CMD 配置唤醒令,具体见 <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 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_VERBOSE为 true 时开启 debug 日志,用于排查问题
## 讨论区
- [点击链接加入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)
- https://github.com/hanxi/xiaomusic/issues
## 感谢
@@ -306,7 +219,4 @@ 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.6.0"
version = "2.5.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.6.0-py3-none-any.whl", hash = "sha256:98169a77ea41a7b9392e1b1fab8cb80a4165fed8a9e882d9ada9a16dd1120347"},
{file = "miservice_fork-2.6.0.tar.gz", hash = "sha256:a59d337d1f7a92566aa147e96595a8d2f5bf3f7000ae5e7dd9ed451f18d6e2fd"},
{file = "miservice_fork-2.5.0-py3-none-any.whl", hash = "sha256:97b6360ea53c34fe035ac9d94e8705f305b8fa7fc2b44a7aea182449a76cb622"},
{file = "miservice_fork-2.5.0.tar.gz", hash = "sha256:8ca2d370d5b32f7e330add38aa1912d734aefa7880f16cef9eac110a5a3029e2"},
]
[[package]]
@@ -662,28 +662,27 @@ files = [
[[package]]
name = "ruff"
version = "0.5.0"
version = "0.4.9"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [
{file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"},
{file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"},
{file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"},
{file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"},
{file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"},
{file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"},
{file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"},
{file = "ruff-0.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"},
]
[[package]]
@@ -832,7 +831,7 @@ files = [
[[package]]
name = "yt-dlp"
version = "2024.6.24.232830.dev0"
version = "2024.6.17.232743.dev0"
requires_python = ">=3.8"
summary = "A feature-rich command-line audio/video downloader"
dependencies = [
@@ -846,6 +845,6 @@ dependencies = [
"websockets>=12.0",
]
files = [
{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"},
{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"},
]

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.1.76"
version = "0.1.63"
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.6.0 \
--hash=sha256:98169a77ea41a7b9392e1b1fab8cb80a4165fed8a9e882d9ada9a16dd1120347 \
--hash=sha256:a59d337d1f7a92566aa147e96595a8d2f5bf3f7000ae5e7dd9ed451f18d6e2fd
miservice-fork==2.5.0 \
--hash=sha256:8ca2d370d5b32f7e330add38aa1912d734aefa7880f16cef9eac110a5a3029e2 \
--hash=sha256:97b6360ea53c34fe035ac9d94e8705f305b8fa7fc2b44a7aea182449a76cb622
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.24.232830.dev0 \
--hash=sha256:0e89b46958984954393692a8c41e0f6d76a773be2df381c3d3a4ff24ce89aa32 \
--hash=sha256:efffecef44ce688e9ee3c02226eb1ba4ad64b37744726e9e4df5c2bd04ea93c5
yt-dlp==2024.6.17.232743.dev0 \
--hash=sha256:2f6f44eff755a7b051cdcd3c4375771033dbeb64d6164351022efdc67cce0c52 \
--hash=sha256:dd6e7e194b96e778691f58a0cb6b42956cf956b22f6bb1a12bdef5ab3ac0c9ad

View File

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

View File

@@ -7,15 +7,18 @@ from dataclasses import dataclass
from xiaomusic.utils import validate_proxy
# 默认口令
DEFAULT_KEY_WORD_DICT = {
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 = {
"播放歌曲": "play",
"播放本地歌曲": "playlocal",
"关机": "stop",
"歌曲": "play",
"下一首": "play_next",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "random_play",
"关机": "stop",
"停止播放": "stop",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
@@ -28,21 +31,30 @@ KEY_WORD_ARG_BEFORE_DICT = {
"分钟后关机": True,
}
# 口令匹配优先级
DEFAULT_KEY_MATCH_ORDER = [
# 匹配优先级
KEY_MATCH_ORDER = [
"set_volume#",
"get_volume#",
"分钟后关机",
"播放歌曲",
"放歌曲",
"下一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"停止播放",
"刷新列表",
"播放列表",
]
SUPPORT_MUSIC_TYPE = [
".mp3",
".flac",
".wav",
".ape",
]
@dataclass
class Config:
@@ -61,7 +73,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,playlocal,play_music_list")
active_cmd: str = os.getenv("XIAOMUSIC_ACTIVE_CMD", "play,random_play")
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir")
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
disable_httpauth: bool = (
@@ -71,33 +83,10 @@ class Config:
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:

View File

@@ -1,10 +0,0 @@
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,13 +2,16 @@
import os
from threading import Thread
from flask import Flask, request, send_from_directory, send_file
from flask import Flask, request, send_from_directory
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,
)
@@ -38,7 +41,7 @@ def verify_password(username, password):
@app.route("/allcmds")
@auth.login_required
def allcmds():
return xiaomusic.config.key_word_dict
return KEY_WORD_DICT
@app.route("/getversion", methods=["GET"])
@@ -150,22 +153,12 @@ def delmusic():
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."
ret, content = downloadfile(data["url"])
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)

View File

@@ -29,7 +29,7 @@
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)"></input>
</div>
<button id="play">播放</button>
<div class="container">
<div class="cawait get_web_music_duration(url)ontainer">
<div id="playering-music" class="text"></div>
</div>

View File

@@ -1,64 +0,0 @@
<!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" disabled></select>
<label for="mi_hardware">MI_HARDWARE:</label>
<select id="mi_hardware"></select>
<label for="xiaomusic_search">XIAOMUSIC_SEARCH:</label>
<select id="xiaomusic_search">
<option value="ytsearch:">ytsearch:</option>
@@ -31,10 +31,6 @@
<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,49 +5,33 @@ $(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);
updateSelectOptions("#mi_did", data.mi_did_list, data.mi_did);
updateSelectOptions("#mi_hardware", data.mi_hardware_list, data.mi_hardware);
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);
}
});
// 初始化联动
linkSelects('#mi_did', data.mi_did_list, '#mi_hardware', data.mi_hardware_list);
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);
}
});
if (data.xiaomusic_search != "") {
$("#xiaomusic_search").val(data.xiaomusic_search);

View File

@@ -1,8 +1,4 @@
.button {
line-height: 50px;
font-size: 14px;
}
button, .button {
button {
margin: 10px;
width: 100px;
height: 50px;
@@ -14,7 +10,7 @@ button, .button {
border-radius: 10px;
background-color: #008CBA;
}
button:active, .button:active {
button:active {
font-weight:bold;
background-color: #007CBA;
transform: translateY(2px);

View File

@@ -3,9 +3,7 @@ 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
@@ -16,8 +14,6 @@ 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):
@@ -155,23 +151,19 @@ def walk_to_depth(root, depth=None, *args, **kwargs):
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
try:
response = requests.get(url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
return ("OK", response.text)
except requests.exceptions.HTTPError as errh:
return (f"HTTP Error: {errh}", "")
except requests.exceptions.ConnectionError as errc:
return (f"Error Connecting: {errc}", "")
except requests.exceptions.Timeout as errt:
return (f"Timeout Error: {errt}", "")
except requests.exceptions.RequestException as err:
return (f"Oops: Something Else, {err}", "")
return ("Unknow Error", "")
async def _get_web_music_duration(session, url, start=0, end=500):
@@ -195,20 +187,6 @@ 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:
@@ -219,7 +197,7 @@ async def get_web_music_duration(url, start=0, end=500):
)
except Exception:
pass
return duration, url
return duration
# 获取文件播放时长
@@ -231,7 +209,3 @@ def get_local_music_duration(filename):
except Exception:
pass
return duration
def get_random(length):
return "".join(random.sample(string.ascii_letters + string.digits, length))

View File

@@ -10,9 +10,7 @@ import time
import traceback
import urllib.parse
from pathlib import Path
import copy
from logging.handlers import RotatingFileHandler
from aiohttp import ClientSession, ClientTimeout
from miservice import MiAccount, MiIOService, MiNAService
@@ -20,20 +18,19 @@ from xiaomusic import (
__version__,
)
from xiaomusic.config import (
KEY_WORD_ARG_BEFORE_DICT,
Config,
)
from xiaomusic.const import (
COOKIE_TEMPLATE,
KEY_MATCH_ORDER,
KEY_WORD_ARG_BEFORE_DICT,
KEY_WORD_DICT,
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,
@@ -93,8 +90,14 @@ class XiaoMusic:
# 关机定时器
self._stop_timer = None
# 初始化日志
self.setup_logger()
# 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.try_init_setting()
@@ -105,28 +108,7 @@ class XiaoMusic:
# 启动时初始化获取声音
self.set_last_record("get_volume#")
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)
self.log.info("ffmpeg_location: %s", self.ffmpeg_location)
async def poll_latest_ask(self):
async with ClientSession() as session:
@@ -378,28 +360,7 @@ 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]
@@ -487,7 +448,7 @@ class XiaoMusic:
# 处理电台列表
if music_type == "radio":
self._all_radio[name] = url
self.log.debug(one_music_list)
self.log.info(one_music_list)
# 歌曲名字相同会覆盖
self._music_list[list_name] = one_music_list
if self._all_radio:
@@ -536,14 +497,30 @@ class XiaoMusic:
return name
# 设置下一首歌曲的播放定时器
async def set_next_music_timeout(self, sec):
if sec <= 0:
async def set_next_music_timeout(self):
name = self.cur_music
if self.is_web_radio_music(name):
self.log.info("电台不会有下一首的定时器")
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():
@@ -554,7 +531,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)
@@ -564,11 +541,10 @@ 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 self.config.key_match_order if "#" not in keyword
keyword for keyword in 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()
@@ -603,7 +579,7 @@ class XiaoMusic:
# 匹配命令
def match_cmd(self, query, ctrl_panel):
for opkey in self.config.key_match_order:
for opkey in KEY_MATCH_ORDER:
patternarg = rf"(.*){opkey}(.*)"
# 匹配参数
matcharg = re.match(patternarg, query)
@@ -620,7 +596,7 @@ class XiaoMusic:
argafter,
)
oparg = argafter
opvalue = self.config.key_word_dict.get(opkey)
opvalue = KEY_WORD_DICT[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}")
@@ -647,68 +623,6 @@ class XiaoMusic:
return True
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):
parts = kwargs.get("arg1", "").split("|")
@@ -728,15 +642,22 @@ class XiaoMusic:
# 本地歌曲不存在时下载
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)
await self._playmusic(name)
self._playing = True
self.cur_music = name
self.log.info("cur_music %s", self.cur_music)
url = self.get_music_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("已经开始播放了")
# 设置下一首歌曲的播放定时器
await self.set_next_music_timeout()
# 下一首
async def play_next(self, **kwargs):
@@ -912,7 +833,7 @@ class XiaoMusic:
self.search_prefix = self.config.search_prefix
self.proxy = self.config.proxy
self.log.debug("update_config_from_setting ok. data:%s", data)
self.log.info("update_config_from_setting ok. data:%s", data)
# 重新初始化
async def reinit(self, **kwargs):