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

Compare commits

..

70 Commits

Author SHA1 Message Date
涵曦
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
涵曦
ad43a4f732 new version v0.1.56 2024-06-24 01:04:13 +00:00
涵曦
8699938b61 删除用不上的配置参数 2024-06-24 00:45:41 +00:00
涵曦
40bd099153 支持wav格式文件 2024-06-24 00:19:49 +00:00
涵曦
750923d5ca Update README.md 2024-06-24 00:38:33 +08:00
涵曦
6d99b30e2d Update README.md 2024-06-24 00:35:02 +08:00
涵曦
a155f16560 Update README.md 2024-06-24 00:30:42 +08:00
涵曦
d4aa045487 Update README.md 2024-06-23 20:05:20 +08:00
涵曦
8080dd9822 Update Dockerfile 2024-06-23 19:05:13 +08:00
涵曦
2eab8d8113 update dockerfile 2024-06-23 10:23:46 +00:00
涵曦
b51e56718a update dockerfile 2024-06-23 10:18:07 +00:00
涵曦
088d448e10 优化 Dockerfile 2024-06-23 17:40:28 +08:00
涵曦
4a89b4bce5 Update README.md 2024-06-23 16:27:22 +08:00
涵曦
b351b4bcd4 new version v0.1.55 2024-06-23 07:09:42 +00:00
涵曦
b2edaf48e4 fix: #47 支持配置基础的BaseAuth登录 2024-06-23 07:07:59 +00:00
涵曦
33e02cee82 new version v0.1.54 2024-06-23 04:17:03 +00:00
涵曦
91a8c9eb50 fix: #76 新增XIAOMUSIC_MUSIC_PATH_DEPTH配置生成播放列表的目录深度,默认10 2024-06-23 03:30:32 +00:00
涵曦
50da8a0554 fix: #74 配置目录可以和下载目录分开配置, 新增XIAOMUSIC_CONF_PATH用来设置配置目录,不配置时使用下载目录 2024-06-23 02:49:13 +00:00
涵曦
42b5978d89 new version v0.1.53 2024-06-23 01:52:42 +00:00
涵曦
bf29fc67b4 增加调试日志 2024-06-23 01:52:38 +00:00
涵曦
dbc68d6b56 new version v0.1.52 2024-06-21 14:16:52 +00:00
涵曦
5dabf66e7c 增加日志 2024-06-21 14:16:28 +00:00
涵曦
d6e4478eb6 new version v0.1.51 2024-06-20 23:10:21 +00:00
涵曦
23ef4719ba update log 2024-06-20 15:45:23 +00:00
涵曦
d799a85ab9 new version v0.1.49 2024-06-20 04:12:33 +00:00
涵曦
4ad6bcc636 播放列表排序显示,修复顺序播放问题 2024-06-20 04:12:23 +00:00
涵曦
d2473ec7e8 全部循环为顺序播放,和随机播放区分开 2024-06-18 07:06:16 +00:00
涵曦
28797edc7c new version v0.1.48 2024-06-16 06:14:37 +00:00
涵曦
be1a643071 忽略目录默认值修改 2024-06-16 06:14:33 +00:00
涵曦
ee6b9778ac new version v0.1.47 2024-06-16 05:40:38 +00:00
涵曦
881c34bcb5 新增忽略目录的环境变量 2024-06-16 05:40:30 +00:00
涵曦
c22fc99235 new version v0.1.46 2024-06-15 15:56:22 +00:00
涵曦
0874efe58b 播放歌曲指令默认播放最后一次播放的歌曲 2024-06-15 15:56:15 +00:00
涵曦
f01665c998 new version v0.1.45 2024-06-15 15:04:07 +00:00
涵曦
ac23080f6a 播放列表歌曲前打乱顺序 2024-06-15 15:03:56 +00:00
涵曦
15ee6c4dd1 new version v0.1.44 2024-06-14 15:47:02 +00:00
涵曦
aeaa8f8925 fmt 2024-06-14 15:46:47 +00:00
涵曦
59d7e056c4 new version v0.1.43 2024-06-14 15:14:56 +00:00
涵曦
e79afa46b3 新增删除歌曲按钮 2024-06-14 15:14:34 +00:00
涵曦
9714f3d064 new version v0.1.41 2024-06-14 14:11:00 +00:00
涵曦
2c35c6cfd6 add XIAOMUSIC_VERBOSE env 2024-06-14 14:10:56 +00:00
涵曦
88f0ce7e51 use ruff lint and fmt code 2024-06-14 01:58:10 +00:00
涵曦
e484164fad 修复刷新列表问题 2024-06-13 14:49:36 +00:00
涵曦
aa6bce75cd update readme 2024-06-12 17:26:00 +00:00
涵曦
512efb595a new version v0.1.40 2024-06-12 17:21:13 +00:00
涵曦
e5dea8e693 新增刷新列表指令 2024-06-12 17:21:09 +00:00
涵曦
a704f8003c new version v0.1.39 2024-06-12 17:13:00 +00:00
涵曦
349a25ad58 新增播放列表功能 #51 2024-06-12 17:12:07 +00:00
涵曦
746f46edb3 new version v0.1.38 2024-06-12 15:39:23 +00:00
涵曦
4a29c7a124 fix: #70 下一首歌曲不存在时从播放列表中删除并继续找下一首 2024-06-12 01:18:08 +00:00
涵曦
0e1e412ee9 new version v0.1.37 2024-06-04 12:17:50 +00:00
涵曦
2e84f7c830 Update ci.yml 2024-06-04 18:46:17 +08:00
涵曦
61a0d68b6a Update release.yml 2024-06-04 18:45:53 +08:00
涵曦
ae90029d8e Update README.md 2024-06-04 15:14:14 +08:00
涵曦
ccc83a518c new version v0.1.36 2024-05-30 14:42:32 +00:00
涵曦
7884a5769f 继续修复启动失败问题 2024-05-30 14:42:12 +00:00
涵曦
a663bb330e new version v0.1.35 2024-05-30 13:49:52 +00:00
涵曦
346f0af543 fix: #67 没配置did时也允许启动 http 服务 2024-05-27 14:47:40 +00:00
涵曦
49ec1bb7c0 Update README.md 2024-05-20 00:03:27 +08:00
18 changed files with 642 additions and 228 deletions

View File

@@ -25,6 +25,6 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}

View File

@@ -61,6 +61,6 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable

1
.gitignore vendored
View File

@@ -164,3 +164,4 @@ cython_debug/
ffmpeg
music
test.sh
conf

View File

@@ -6,7 +6,6 @@ COPY install_dependencies.sh .
RUN bash install_dependencies.sh
FROM python:3.10-slim
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/ffmpeg /app/ffmpeg

View File

@@ -1,8 +1,29 @@
# xiaomusic
使用小爱/红米音箱播放音乐,音乐使用 yt-dlp 下载。
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
## 运行
## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下:
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
```
启动成功后,在 web 页面可以配置 MI_DID, MI_HARDWARE, XIAOMUSIC_SEARCH, XIAOMUSIC_PROXY 参数。
## 开发环境运行
- 使用 install_dependencies.sh 下载依赖
- 使用 pdm 安装环境
@@ -28,6 +49,11 @@ pdm run xiaomusic.py
- 下一首
- 单曲循环
- 全部循环
- 随机播放
- 关机
- 停止播放
- 刷新列表
- 播放列表+列表名 比如:播放列表其他
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
@@ -50,8 +76,17 @@ pdm run xiaomusic.py
## 在 Docker 里使用
```shell
docker run -e MI_USER=<your-xiaomi-account> -e MI_PASS=<your-xiaomi-password> -e MI_DID=<your-xiaomi-speaker-mid> -e MI_HARDWARE='L07A' -e XIAOMUSIC_PROXY=<proxy-for-yt-dlp> -e XIAOMUSIC_HOSTNAME=192.168.2.5 -e XIAOMUSIC_SEARCH='bilisearch:' -p 8090:8090 -v ./music:/app/music hanxi/xiaomusic
docker run -e MI_USER='your-xiaomi-account' \
-e MI_PASS='your-xiaomi-password' \
-e MI_DID='your-xiaomi-speaker-mid' \
-e MI_HARDWARE='L07A' \
-e XIAOMUSIC_PROXY='proxy-for-yt-dlp' \
-e XIAOMUSIC_HOSTNAME=192.168.2.5 \
-e XIAOMUSIC_SEARCH='bilisearch:' \
-p 8090:8090 \
-v ./music:/app/music hanxi/xiaomusic
```
- XIAOMUSIC_SEARCH 可以配置为 'bilisearch:' 表示歌曲从哔哩哔哩下载;
- 配置为 'ytsearch:' 表示歌曲从 youtube 下载。
- XIAOMUSIC_PROXY 用于配置代理,默认为空;
@@ -151,6 +186,17 @@ services:
- XIAOMUSIC_SEARCH
- XIAOMUSIC_PROXY
## 更多其他可选配置
- 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_VERBOSE 设置为 true 时开启 debug 日志,用于排查问题
## 讨论区
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
@@ -171,3 +217,6 @@ services:
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=hanxi/xiaomusic&type=Date)](https://star-history.com/#hanxi/xiaomusic&Date)
## 赞赏
谢谢就够了

57
pdm.lock generated
View File

@@ -2,10 +2,10 @@
# It is not intended for manual editing.
[metadata]
groups = ["default"]
groups = ["default", "lint"]
strategy = ["cross_platform"]
lock_version = "4.4.1"
content_hash = "sha256:d771311a452ca58665efe3b74af341cb202d75d83a250896c293ea9c696e5696"
content_hash = "sha256:813253734c7d7835a76cd87fe8fe0329e02ad067f535aee6a9e11cb106569dd2"
[[package]]
name = "aiohttp"
@@ -356,6 +356,18 @@ files = [
{file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"},
]
[[package]]
name = "flask-httpauth"
version = "4.8.0"
summary = "HTTP authentication for Flask routes"
dependencies = [
"flask",
]
files = [
{file = "Flask-HTTPAuth-4.8.0.tar.gz", hash = "sha256:66568a05bc73942c65f1e2201ae746295816dc009edd84b482c44c758d75097a"},
{file = "Flask_HTTPAuth-4.8.0-py3-none-any.whl", hash = "sha256:a58fedd09989b9975448eef04806b096a3964a7feeebc0a78831ff55685b62b0"},
]
[[package]]
name = "flask"
version = "3.0.3"
@@ -620,8 +632,8 @@ files = [
[[package]]
name = "requests"
version = "2.31.0"
requires_python = ">=3.7"
version = "2.32.3"
requires_python = ">=3.8"
summary = "Python HTTP for Humans."
dependencies = [
"certifi>=2017.4.17",
@@ -630,8 +642,8 @@ dependencies = [
"urllib3<3,>=1.21.1",
]
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[[package]]
@@ -648,6 +660,31 @@ files = [
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
]
[[package]]
name = "ruff"
version = "0.4.9"
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"},
]
[[package]]
name = "typing-extensions"
version = "4.9.0"
@@ -794,7 +831,7 @@ files = [
[[package]]
name = "yt-dlp"
version = "2024.5.16.232713.dev0"
version = "2024.6.17.232743.dev0"
requires_python = ">=3.8"
summary = "A feature-rich command-line audio/video downloader"
dependencies = [
@@ -803,11 +840,11 @@ dependencies = [
"certifi",
"mutagen",
"pycryptodomex",
"requests<3,>=2.31.0",
"requests<3,>=2.32.2",
"urllib3<3,>=1.26.17",
"websockets>=12.0",
]
files = [
{file = "yt_dlp-2024.5.16.232713.dev0-py3-none-any.whl", hash = "sha256:42d3c27ab77583ff67ee2ddc94e376ea2a76a561ed8b1836ee04fd1cd23ad88c"},
{file = "yt_dlp-2024.5.16.232713.dev0.tar.gz", hash = "sha256:d431187fa703c9f52225080ae56471272679e44d9363f97b7b3187d37a5e6480"},
{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,12 +1,11 @@
[project]
name = "xiaomusic"
version = "0.1.34"
version = "0.1.60"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},
]
dependencies = [
"rich>=13.6.0",
"requests>=2.31.0",
"aiohttp>=3.8.6",
"miservice-fork>=2.5.0",
@@ -14,6 +13,7 @@ dependencies = [
"yt-dlp>=2024.04.09",
"flask[async]>=3.0.1",
"waitress>=3.0.0",
"flask-HTTPAuth>=4.8.0",
]
requires-python = ">=3.10"
readme = "README.md"
@@ -24,3 +24,29 @@ requires = ["pdm-backend"]
build-backend = "pdm.backend"
[tool.pdm]
[tool.pdm.dev-dependencies]
lint = [
"ruff>=0.4.8",
]
[tool.ruff]
lint.select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"E", # pycodestyle - Error
"F", # Pyflakes
"I", # isort
"W", # pycodestyle - Warning
"UP", # pyupgrade
]
lint.ignore = [
"E501", # line-too-long
"W191", # tab-indentation
]
include = ["**/*.py", "**/*.pyi", "**/pyproject.toml"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.pdm.scripts]
lint = "ruff check ."
fmt = "ruff format ."

View File

@@ -223,6 +223,9 @@ colorama==0.4.6; platform_system == "Windows" \
flask==3.0.3 \
--hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3 \
--hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842
flask-HTTPAuth==4.8.0 \
--hash=sha256:66568a05bc73942c65f1e2201ae746295816dc009edd84b482c44c758d75097a \
--hash=sha256:a58fedd09989b9975448eef04806b096a3964a7feeebc0a78831ff55685b62b0
frozenlist==1.4.0 \
--hash=sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01 \
--hash=sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251 \
@@ -368,9 +371,9 @@ pycryptodomex==3.19.0 \
pygments==2.16.1 \
--hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
--hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
requests==2.31.0 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
rich==13.7.1 \
--hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \
--hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432
@@ -469,6 +472,6 @@ yarl==1.9.2 \
--hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \
--hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \
--hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560
yt-dlp==2024.5.16.232713.dev0 \
--hash=sha256:42d3c27ab77583ff67ee2ddc94e376ea2a76a561ed8b1836ee04fd1cd23ad88c \
--hash=sha256:d431187fa703c9f52225080ae56471272679e44d9363f97b7b3187d37a5e6480
yt-dlp==2024.6.17.232743.dev0 \
--hash=sha256:2f6f44eff755a7b051cdcd3c4375771033dbeb64d6164351022efdc67cce0c52 \
--hash=sha256:dd6e7e194b96e778691f58a0cb6b42956cf956b22f6bb1a12bdef5ab3ac0c9ad

View File

@@ -1 +1 @@
pdm export -o requirements.txt
pdm export --prod -o requirements.txt

View File

@@ -1 +1 @@
__version__ = "0.1.34"
__version__ = "0.1.60"

View File

@@ -27,20 +27,6 @@ def main():
dest="cookie",
help="xiaomi cookie",
)
parser.add_argument(
"--use_command",
dest="use_command",
action="store_true",
default=None,
help="use command to tts",
)
parser.add_argument(
"--mute_xiaoai",
dest="mute_xiaoai",
action="store_true",
default=None,
help="try to mute xiaoai answer",
)
parser.add_argument(
"--verbose",
dest="verbose",

View File

@@ -3,35 +3,12 @@ from __future__ import annotations
import argparse
import json
import os
from dataclasses import dataclass, field
from typing import Any, Iterable
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}"
HARDWARE_COMMAND_DICT = {
# hardware: (tts_command, wakeup_command, volume_command)
"LX06": ("5-1", "5-5", "2-1"),
"L05B": ("5-3", "5-4", "2-1"),
"S12": ("5-1", "5-5", "2-1"), # 第一代小爱型号MDZ-25-DA
"S12A": ("5-1", "5-5", "2-1"),
"LX01": ("5-1", "5-5", "2-1"),
"L06A": ("5-1", "5-5", "2-1"),
"LX04": ("5-1", "5-4", "2-1"),
"L05C": ("5-3", "5-4", "2-1"),
"L17A": ("7-3", "7-4", "2-1"),
"X08E": ("7-3", "7-4", "2-1"),
"LX05A": ("5-1", "5-5", "2-1"), # 小爱红外版
"LX5A": ("5-1", "5-5", "2-1"), # 小爱红外版
"L07A": ("5-1", "5-5", "2-1"), # Redmi小爱音箱Play(l7a)
"L15A": ("7-3", "7-4", "2-1"),
"X6A": ("7-3", "7-4", "2-1"), # 小米智能家庭屏6
"X10A": ("7-3", "7-4", "2-1"), # 小米智能家庭屏10
# add more here
}
DEFAULT_COMMAND = ("5-1", "5-5", "2-1")
KEY_WORD_DICT = {
"播放歌曲": "play",
@@ -43,6 +20,8 @@ KEY_WORD_DICT = {
"关机": "stop",
"停止播放": "stop",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"set_volume#": "set_volume",
"get_volume#": "get_volume",
}
@@ -65,11 +44,15 @@ KEY_MATCH_ORDER = [
"随机播放",
"关机",
"停止播放",
"刷新列表",
"播放列表",
]
SUPPORT_MUSIC_TYPE = [
".mp3",
".flac",
".wav",
".ape",
]
@@ -79,11 +62,10 @@ class Config:
account: str = os.getenv("MI_USER", "")
password: str = os.getenv("MI_PASS", "")
mi_did: str = os.getenv("MI_DID", "")
mute_xiaoai: bool = True
cookie: str = ""
use_command: bool = False
verbose: bool = False
verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", None)
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090"))
proxy: str | None = os.getenv("XIAOMUSIC_PROXY", None)
@@ -92,23 +74,18 @@ class Config:
) # "bilisearch:" or "ytsearch:"
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
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 = (
os.getenv("XIAOMUSIC_DISABLE_HTTPAUTH", "true").lower() == "true"
)
httpauth_username: str = os.getenv("XIAOMUSIC_HTTPAUTH_USERNAME", "admin")
httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "admin")
def __post_init__(self) -> None:
if self.proxy:
validate_proxy(self.proxy)
@property
def tts_command(self) -> str:
return HARDWARE_COMMAND_DICT.get(self.hardware, DEFAULT_COMMAND)[0]
@property
def wakeup_command(self) -> str:
return HARDWARE_COMMAND_DICT.get(self.hardware, DEFAULT_COMMAND)[1]
@property
def volume_command(self) -> str:
return HARDWARE_COMMAND_DICT.get(self.hardware, DEFAULT_COMMAND)[2]
@classmethod
def from_options(cls, options: argparse.Namespace) -> Config:
config = {}

View File

@@ -1,27 +1,21 @@
#!/usr/bin/env python3
import os
import sys
import traceback
import asyncio
from flask import Flask, request, send_from_directory
from waitress import serve
from threading import Thread
from xiaomusic.config import (
KEY_WORD_DICT,
)
from flask import Flask, request, send_from_directory
from flask_httpauth import HTTPBasicAuth
from waitress import serve
from xiaomusic import (
__version__,
)
# 隐藏 flask 启动告警
# https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356
#from flask import cli
#cli.show_server_banner = lambda *_: None
from xiaomusic.config import (
KEY_WORD_DICT,
)
app = Flask(__name__)
auth = HTTPBasicAuth()
host = "0.0.0.0"
port = 8090
static_path = "music"
@@ -29,10 +23,24 @@ xiaomusic = None
log = None
@auth.verify_password
def verify_password(username, password):
if xiaomusic.config.disable_httpauth:
return True
if (
xiaomusic.config.httpauth_username == username
and xiaomusic.config.httpauth_password == password
):
return username
@app.route("/allcmds")
@auth.login_required
def allcmds():
return KEY_WORD_DICT
@app.route("/getversion", methods=["GET"])
def getversion():
log.debug("getversion %s", __version__)
@@ -40,28 +48,42 @@ def getversion():
"version": __version__,
}
@app.route("/getvolume", methods=["GET"])
@auth.login_required
def getvolume():
volume = xiaomusic.get_volume_ret()
return {
"volume": volume,
}
@app.route("/searchmusic", methods=["GET"])
@auth.login_required
def searchmusic():
name = request.args.get('name')
name = request.args.get("name")
return xiaomusic.searchmusic(name)
@app.route("/playingmusic", methods=["GET"])
@auth.login_required
def playingmusic():
return xiaomusic.playingmusic()
@app.route("/isplaying", methods=["GET"])
@auth.login_required
def isplaying():
return xiaomusic.isplaying()
@app.route("/", methods=["GET"])
def redirect_to_index():
def index():
return send_from_directory("static", "index.html")
@app.route("/cmd", methods=["POST"])
@auth.login_required
async def do_cmd():
data = request.get_json()
cmd = data.get("cmd")
@@ -71,7 +93,9 @@ async def do_cmd():
return {"ret": "OK"}
return {"ret": "Unknow cmd"}
@app.route("/getsetting", methods=["GET"])
@auth.login_required
async def getsetting():
config = xiaomusic.getconfig()
log.debug(config)
@@ -88,13 +112,37 @@ async def getsetting():
}
return data
@app.route("/savesetting", methods=["POST"])
@auth.login_required
async def savesetting():
data = request.get_json()
log.info(data)
await xiaomusic.saveconfig(data)
return "save success"
@app.route("/musiclist", methods=["GET"])
@auth.login_required
async def musiclist():
return xiaomusic.get_music_list()
@app.route("/curplaylist", methods=["GET"])
@auth.login_required
async def curplaylist():
return xiaomusic.get_cur_play_list()
@app.route("/delmusic", methods=["POST"])
@auth.login_required
def delmusic():
data = request.get_json()
log.info(data)
xiaomusic.del_music(data["name"])
return "success"
def static_path_handler(filename):
log.debug(filename)
log.debug(static_path)
@@ -102,9 +150,11 @@ def static_path_handler(filename):
log.debug(absolute_path)
return send_from_directory(absolute_path, filename)
def run_app():
serve(app, host=host, port=port)
def StartHTTPServer(_port, _static_path, _xiaomusic):
global port, static_path, xiaomusic, log
port = _port

View File

@@ -1,11 +1,11 @@
$(function(){
$container=$("#cmds");
append_op_button_name("下一首");
append_op_button_name("全部循环");
append_op_button_name("关机");
append_op_button_name("单曲循环");
append_op_button_name("播放歌曲");
append_op_button_name("随机播放");
append_op_button_name("刷新列表");
append_op_button_name("下一首");
append_op_button_name("关机");
$container.append($("<hr>"));
@@ -26,6 +26,61 @@ $(function(){
$("#version").text(`(${data.version})`);
});
// 拉取播放列表
function refresh_music_list() {
$('#music_list').empty();
$.get("/musiclist", function(data, status) {
console.log(data, status);
$.each(data, function(key, value) {
$('#music_list').append($('<option></option>').val(key).text(key));
});
$('#music_list').change(function() {
const selectedValue = $(this).val();
$('#music_name').empty();
const sorted_musics = data[selectedValue].sort(custom_sort_key);
$.each(sorted_musics, function(index, item) {
$('#music_name').append($('<option></option>').val(item).text(item));
});
});
$('#music_list').trigger('change');
// 获取当前播放列表
$.get("curplaylist", function(data, status) {
$('#music_list').val(data);
$('#music_list').trigger('change');
})
})
}
refresh_music_list();
$("#play_music_list").on("click", () => {
var music_list = $("#music_list").val();
var music_name = $("#music_name").val();
let cmd = "播放列表" + music_list + "|" + music_name;
sendcmd(cmd);
});
$("#del_music").on("click", () => {
var del_music_name = $("#music_name").val();
if (confirm(`确定删除歌曲 ${del_music_name} 吗?`)) {
console.log(`删除歌曲 ${del_music_name}`);
$.ajax({
type: 'POST',
url: '/delmusic',
data: JSON.stringify({"name": del_music_name}),
contentType: "application/json; charset=utf-8",
success: () => {
alert(`删除 ${del_music_name} 成功`);
refresh_music_list();
},
error: () => {
alert(`删除 ${del_music_name} 失败`);
}
});
}
});
function append_op_button_name(name) {
append_op_button(name, name);
@@ -48,8 +103,8 @@ $(function(){
$("#play").on("click", () => {
var search_key = $("#music-name").val();
var filename=$("#music-filename").val();
let cmd = "播放歌曲"+search_key+"|"+filename;
var filename = $("#music-filename").val();
let cmd = "播放歌曲" + search_key + "|" + filename;
sendcmd(cmd);
});
@@ -62,10 +117,12 @@ $(function(){
$.ajax({
type: "POST",
url: "/cmd",
contentType: "application/json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({cmd: cmd}),
success: () => {
// 请求成功时执行的操作
if (cmd == "刷新列表") {
setTimeout(refresh_music_list, 3000);
}
},
error: () => {
// 请求失败时执行的操作
@@ -107,4 +164,23 @@ $(function(){
setInterval(() => {
get_playing_music();
}, 3000);
function custom_sort_key(a, b) {
// 使用正则表达式提取数字前缀
const numericPrefixA = a.match(/^(\d+)/) ? parseInt(a.match(/^(\d+)/)[1], 10) : null;
const numericPrefixB = b.match(/^(\d+)/) ? parseInt(b.match(/^(\d+)/)[1], 10) : null;
// 如果两个键都有数字前缀,则按数字大小排序
if (numericPrefixA !== null && numericPrefixB !== null) {
return numericPrefixA - numericPrefixB;
}
// 如果一个键有数字前缀而另一个没有,则有数字前缀的键排在前面
if (numericPrefixA !== null) return -1;
if (numericPrefixB !== null) return 1;
// 如果两个键都没有数字前缀,则按照常规字符串排序
return a.localeCompare(b);
}
});

View File

@@ -33,6 +33,16 @@
<div id="playering-music" class="text"></div>
</div>
<hr>
<div class="rows">
<label for="music_list">播放列表:</label>
<select id="music_list"></select>
<label for="music_name">歌曲:</label>
<select id="music_name"></select>
</div>
<button id="play_music_list">播放列表歌曲</button>
<button id="del_music">删除选中歌曲</button>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>

View File

@@ -62,7 +62,6 @@ input,select {
}
footer {
position: fixed;
bottom: 0;
width: 100%;
text-align: center;

View File

@@ -1,13 +1,12 @@
#!/usr/bin/env python3
from __future__ import annotations
import difflib
import os
import re
import socket
from collections.abc import AsyncIterator
from http.cookies import SimpleCookie
from typing import AsyncIterator
from urllib.parse import urlparse
import difflib
from requests.utils import cookiejar_from_dict
@@ -62,6 +61,86 @@ def validate_proxy(proxy_str: str) -> bool:
return True
# 模糊搜索
def fuzzyfinder(user_input, collection):
return difflib.get_close_matches(user_input, collection, 10, cutoff=0.1)
# 歌曲排序
def custom_sort_key(s):
# 使用正则表达式分别提取字符串的数字前缀和数字后缀
prefix_match = re.match(r"^(\d+)", s)
suffix_match = re.search(r"(\d+)$", s)
numeric_prefix = int(prefix_match.group(0)) if prefix_match else None
numeric_suffix = int(suffix_match.group(0)) if suffix_match else None
if numeric_prefix is not None:
# 如果前缀是数字,先按前缀数字排序,再按整个字符串排序
return (0, numeric_prefix, s)
elif numeric_suffix is not None:
# 如果后缀是数字,先按前缀字符排序,再按后缀数字排序
return (1, s[: suffix_match.start()], numeric_suffix)
else:
# 如果前缀和后缀都不是数字,按字典序排序
return (2, s)
# fork from https://gist.github.com/dougthor42/001355248518bc64d2f8
def walk_to_depth(root, depth=None, *args, **kwargs):
"""
Wrapper around os.walk that stops after going down `depth` folders.
I had my own version, but it wasn't as efficient as
http://stackoverflow.com/a/234329/1354930, so I modified to be more
similar to nosklo's answer.
However, nosklo's answer doesn't work if topdown=False, so I kept my
version.
"""
# Let people use this as a standard `os.walk` function.
if depth is None:
return os.walk(root, *args, **kwargs)
# remove any trailing separators so that our counts are correct.
root = root.rstrip(os.path.sep)
def main_func(root, depth, *args, **kwargs):
"""Faster because it skips traversing dirs that are too deep."""
root_depth = root.count(os.path.sep)
for dirpath, dirnames, filenames in os.walk(root, *args, **kwargs):
yield (dirpath, dirnames, filenames)
# calculate how far down we are.
current_folder_depth = dirpath.count(os.path.sep)
if current_folder_depth >= root_depth + depth:
del dirnames[:]
def fallback_func(root, depth, *args, **kwargs):
"""Slower, but works when topdown is False"""
root_depth = root.count(os.path.sep)
for dirpath, dirnames, filenames in os.walk(root, *args, **kwargs):
current_folder_depth = dirpath.count(os.path.sep)
if current_folder_depth <= root_depth + depth:
yield (dirpath, dirnames, filenames)
# there's gotta be a better way do do this...
try:
if args[0] is False:
yield from fallback_func(root, depth, *args, **kwargs)
return
else:
yield from main_func(root, depth, *args, **kwargs)
return
except IndexError:
pass
try:
if kwargs["topdown"] is False:
yield from fallback_func(root, depth, *args, **kwargs)
return
else:
yield from main_func(root, depth, *args, **kwargs)
return
except KeyError:
yield from main_func(root, depth, *args, **kwargs)
return

View File

@@ -3,44 +3,44 @@ import asyncio
import json
import logging
import os
import queue
import random
import re
import time
import urllib.parse
import traceback
import mutagen
import queue
from xiaomusic.httpserver import StartHTTPServer
import urllib.parse
from pathlib import Path
import mutagen
from aiohttp import ClientSession, ClientTimeout
from miservice import MiAccount, MiIOService, MiNAService, miio_command
from rich import print
from rich.logging import RichHandler
from xiaomusic.config import (
COOKIE_TEMPLATE,
LATEST_ASK_API,
KEY_WORD_DICT,
KEY_WORD_ARG_BEFORE_DICT,
KEY_MATCH_ORDER,
SUPPORT_MUSIC_TYPE,
Config,
)
from xiaomusic.utils import (
parse_cookie_string,
fuzzyfinder,
)
from miservice import MiAccount, MiIOService, MiNAService
from xiaomusic import (
__version__,
)
from xiaomusic.config 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,
parse_cookie_string,
walk_to_depth,
)
EOF = object()
PLAY_TYPE_ONE = 0 # 单曲循环
PLAY_TYPE_ALL = 1 # 全部循环
PLAY_TYPE_RND = 2 # 随机播放
class XiaoMusic:
def __init__(self, config: Config):
@@ -58,23 +58,31 @@ class XiaoMusic:
self.queue = queue.Queue()
self.music_path = config.music_path
self.conf_path = config.conf_path
if not self.conf_path:
self.conf_path = config.music_path
self.hostname = config.hostname
self.port = config.port
self.proxy = config.proxy
self.search_prefix = config.search_prefix
self.ffmpeg_location = config.ffmpeg_location
self.active_cmd = config.active_cmd.split(",")
self.exclude_dirs = set(config.exclude_dirs.split(","))
self.music_path_depth = config.music_path_depth
# 下载对象
self.download_proc = None
# 单曲循环,全部循环
self.play_type = PLAY_TYPE_ALL
self.play_type = PLAY_TYPE_RND
self.cur_music = ""
self._next_timer = None
self._timeout = 0
self._volume = 0
self._all_music = {}
self._play_list = []
self._cur_play_list = ""
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
self._playing = False
# 关机定时器
@@ -82,9 +90,8 @@ class XiaoMusic:
# setup logger
logging.basicConfig(
format=f"[{__version__}]\t%(message)s",
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)]
)
self.log = logging.getLogger("xiaomusic")
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
@@ -94,7 +101,7 @@ class XiaoMusic:
self.try_init_setting()
# 启动时重新生成一次播放列表
self.gen_all_music_list()
self._gen_all_music_list()
# 启动时初始化获取声音
self.set_last_record("get_volume#")
@@ -120,7 +127,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):
@@ -136,27 +145,31 @@ 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:
raise Exception(
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}")
pass
async def _init_data_hardware(self):
if self.config.cookie:
@@ -172,10 +185,12 @@ class XiaoMusic:
if d["model"].endswith(self.config.hardware.lower())
)
except StopIteration:
raise Exception(
self.log.error(
f"cannot find did for hardware: {self.config.hardware} "
"please set it via MI_DID env"
)
except Exception as e:
self.log.error(f"Execption init hardware {e}")
def get_cookie(self):
if self.config.cookie:
@@ -184,28 +199,31 @@ 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
for i in range(retries):
try:
timeout = ClientTimeout(total=15)
r = await session.get(
LATEST_ASK_API.format(
hardware=self.config.hardware,
timestamp=str(int(time.time() * 1000)),
),
timeout=timeout,
url = LATEST_ASK_API.format(
hardware=self.config.hardware,
timestamp=str(int(time.time() * 1000)),
)
self.log.debug(f"url:{url}")
r = await session.get(url, timeout=timeout)
except Exception as e:
self.log.warning(
"Execption when get latest ask from xiaoai: %s", str(e)
@@ -213,8 +231,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")
@@ -223,6 +241,7 @@ class XiaoMusic:
return self._get_last_query(data)
def _get_last_query(self, data):
self.log.debug(f"_get_last_query:{data}")
if d := data.get("data"):
records = json.loads(d).get("records")
if not records:
@@ -244,42 +263,25 @@ class XiaoMusic:
async def do_tts(self, value):
self.log.info("do_tts: %s", value)
if self.config.mute_xiaoai:
await self.force_stop_xiaoai()
else:
# waiting for xiaoai speaker done
await asyncio.sleep(8)
if not self.config.use_command:
try:
await self.mina_service.text_to_speech(self.device_id, value)
except Exception:
pass
else:
await miio_command(
self.miio_service,
self.config.mi_did,
f"{self.config.tts_command} {value}",
)
await self.force_stop_xiaoai()
try:
await self.mina_service.text_to_speech(self.device_id, value)
except Exception as e:
self.log.error(f"Execption {e}")
pass
if self._playing:
# 继续播放歌曲
await self.play()
async def do_set_volume(self, value):
value = int(value)
self._volume = value
self.log.info(f"声音设置为{value}")
if not self.config.use_command:
try:
self.log.debug("do_set_volume not use_command value:%d", value)
await self.mina_service.player_set_volume(self.device_id, value)
except Exception:
pass
else:
self.log.debug("do_set_volume use_command value:%d", value)
await miio_command(
self.miio_service,
self.config.mi_did,
f"{self.config.volume_command}=#{value}",
)
try:
await self.mina_service.player_set_volume(self.device_id, value)
except Exception as e:
self.log.error(f"Execption {e}")
pass
async def force_stop_xiaoai(self):
await self.mina_service.player_stop(self.device_id)
@@ -288,7 +290,10 @@ class XiaoMusic:
def is_downloading(self):
if not self.download_proc:
return False
if self.download_proc.returncode != None and self.download_proc.returncode < 0:
if (
self.download_proc.returncode is not None
and self.download_proc.returncode < 0
):
return False
return True
@@ -318,6 +323,7 @@ class XiaoMusic:
if self.proxy:
sbp_args += ("--proxy", f"{self.proxy}")
self.log.debug(f"download: {sbp_args}")
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
await self.do_tts(f"正在下载歌曲{search_key}")
@@ -340,9 +346,19 @@ class XiaoMusic:
return f"http://{self.hostname}:{self.port}/{encoded_name}"
# 递归获取目录下所有歌曲,生成随机播放列表
def gen_all_music_list(self):
def _gen_all_music_list(self):
self._all_music = {}
for root, dirs, filenames in os.walk(self.music_path):
all_music_by_dir = {}
for root, dirs, filenames in walk_to_depth(
self.music_path, depth=self.music_path_depth
):
dirs[:] = [d for d in dirs if d not in self.exclude_dirs]
self.log.debug("root:%s dirs:%s music_path:%s", root, dirs, self.music_path)
dir_name = os.path.basename(root)
if self.music_path == root:
dir_name = "其他"
if dir_name not in all_music_by_dir:
all_music_by_dir[dir_name] = {}
for filename in filenames:
self.log.debug("gen_all_music_list. filename:%s", filename)
# 过滤隐藏文件
@@ -356,15 +372,33 @@ class XiaoMusic:
name,
extension,
)
if extension not in SUPPORT_MUSIC_TYPE:
if extension.lower() not in SUPPORT_MUSIC_TYPE:
continue
# 歌曲名字相同会覆盖
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())
random.shuffle(self._play_list)
self._cur_play_list = "全部"
self._gen_play_list()
self.log.debug(self._all_music)
self._music_list = {}
self._music_list["全部"] = self._play_list
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
# 歌曲排序或者打乱顺序
def _gen_play_list(self):
if self.play_type == PLAY_TYPE_RND:
random.shuffle(self._play_list)
else:
self._play_list.sort(key=custom_sort_key)
self.log.debug("play_list:%s", self._play_list)
# 把下载的音乐加入播放列表
def add_download_music(self, name):
self._all_music[name] = os.path.join(self.music_path, f"{name}.mp3")
@@ -377,7 +411,7 @@ class XiaoMusic:
def get_next_music(self):
play_list_len = len(self._play_list)
if play_list_len == 0:
self.log.warning(f"没有随机到歌曲")
self.log.warning("没有随机到歌曲")
return ""
# 随机选择一个文件
index = 0
@@ -388,8 +422,13 @@ class XiaoMusic:
next_index = index + 1
if next_index >= play_list_len:
next_index = 0
filename = self._play_list[next_index]
return filename
name = self._play_list[next_index]
filename = self.get_filename(name)
if len(filename) <= 0:
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):
@@ -406,7 +445,7 @@ class XiaoMusic:
self.log.info(f"歌曲 {self.cur_music} : {filename} 的时长 {sec}")
if self._next_timer:
self._next_timer.cancel()
self.log.info(f"定时器已取消")
self.log.info("定时器已取消")
self._timeout = sec
async def _do_next():
@@ -426,11 +465,11 @@ class XiaoMusic:
await self.init_all_data(session)
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]
filtered_keywords = [
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(f"Running xiaomusic now, 用`{joined_keywords}`开头来控制")
while True:
self.polling_event.set()
@@ -498,17 +537,35 @@ 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:
return True
pass
return False
# 播放歌曲
async def play(self, **kwargs):
self._playing = True
parts = kwargs["arg1"].split("|")
search_key = parts[0]
name = parts[1] if len(parts) > 1 else search_key
if search_key == "" and name == "":
await self.play_next()
return
if name == "":
name = search_key
if search_key == "" and name == "":
if self.check_play_next():
await self.play_next()
return
else:
name = self.cur_music
self.log.debug("play. search_key:%s name:%s", search_key, name)
filename = self.get_filename(name)
@@ -534,42 +591,82 @@ class XiaoMusic:
self.log.info("下一首")
name = self.cur_music
self.log.debug("play_next. name:%s, cur_music:%s", name, self.cur_music)
if self.play_type == PLAY_TYPE_ALL or name == "":
if (
self.play_type == PLAY_TYPE_ALL
or self.play_type == PLAY_TYPE_RND
or name == ""
):
name = self.get_next_music()
if name == "":
await self.do_tts(f"本地没有歌曲")
await self.do_tts("本地没有歌曲")
return
await self.play(arg1=name)
# 单曲循环
async def set_play_type_one(self, **kwargs):
self.play_type = PLAY_TYPE_ONE
await self.do_tts(f"已经设置为单曲循环")
await self.do_tts("已经设置为单曲循环")
# 全部循环
async def set_play_type_all(self, **kwargs):
self.play_type = PLAY_TYPE_ALL
await self.do_tts(f"已经设置为全部循环")
self._gen_play_list()
await self.do_tts("已经设置为全部循环")
# 随机播放
async def random_play(self, **kwargs):
self.play_type = PLAY_TYPE_ALL
await self.do_tts(f"已经设置为全部循环并随机播放")
# 重新生成随机播放列表
self.gen_all_music_list()
await self.play_next()
self.play_type = PLAY_TYPE_RND
self._gen_play_list()
await self.do_tts("已经设置为随机播放")
# 刷新列表
async def gen_music_list(self, **kwargs):
self._gen_all_music_list()
# 删除歌曲
def del_music(self, name):
filename = self.get_filename(name)
if filename == "":
self.log.info(f"${name} not exist")
return
try:
os.remove(filename)
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("|")
list_name = parts[0]
if list_name not in self._music_list:
await self.do_tts(f"播放列表{list_name}不存在")
return
self._play_list = self._music_list[list_name]
self._cur_play_list = list_name
self._gen_play_list()
self.log.info(f"开始播放列表{list_name}")
music_name = ""
if len(parts) > 1:
music_name = parts[1]
else:
music_name = self.get_next_music()
await self.play(arg1=music_name)
async def stop(self, **kwargs):
self._playing = False
if self._next_timer:
self._next_timer.cancel()
self.log.info(f"定时器已取消")
self.log.info("定时器已取消")
await self.force_stop_xiaoai()
async def stop_after_minute(self, **kwargs):
if self._stop_timer:
self._stop_timer.cancel()
self.log.info(f"关机定时器已取消")
self.log.info("关机定时器已取消")
minute = int(kwargs["arg1"])
async def _do_stop():
@@ -589,7 +686,9 @@ class XiaoMusic:
async def get_volume(self, **kwargs):
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)
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
"volume", 5
)
self.log.info("get_volume. volume:%s", self._volume)
def get_volume_ret(self):
@@ -597,22 +696,42 @@ class XiaoMusic:
# 搜索音乐
def searchmusic(self, name):
search_list = fuzzyfinder(name, self._play_list)
all_music_list = list(self._all_music.keys())
search_list = fuzzyfinder(name, all_music_list)
self.log.debug("searchmusic. name:%s search_list:%s", name, search_list)
return search_list
# 获取播放列表
def get_music_list(self):
return self._music_list
# 获取当前的播放列表
def get_cur_play_list(self):
return self._cur_play_list
# 正在播放中的音乐
def playingmusic(self):
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
# 获取设置文件
def getsettingfile(self):
if not os.path.exists(self.conf_path):
os.makedirs(self.conf_path)
filename = os.path.join(self.conf_path, "setting.json")
return filename
def try_init_setting(self):
try:
filename = os.path.join(self.music_path, "setting.json")
filename = self.getsettingfile()
with open(filename) as f:
data = json.loads(f.read())
self.update_config_from_setting(data)
@@ -620,12 +739,14 @@ class XiaoMusic:
self.log.info(f"The file {filename} does not exist.")
except json.JSONDecodeError:
self.log.warning(f"The file {filename} contains invalid JSON.")
except Exception as e:
self.log.error(f"Execption init setting {e}")
# 保存配置并重新启动
async def saveconfig(self, data):
# 默认暂时配置保存到 music 目录下
filename = os.path.join(self.music_path, "setting.json")
with open(filename, 'w', encoding='utf-8') as f:
filename = self.getsettingfile()
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
self.update_config_from_setting(data)
await self.call_main_thread_function(self.reinit)
@@ -670,12 +791,13 @@ class XiaoMusic:
async def call_main_thread_function(self, func, arg1=None):
loop = asyncio.get_event_loop()
future = loop.create_future()
def callback(ret):
nonlocal future
loop.call_soon_threadsafe(future.set_result, ret)
self.queue.put((func, callback, arg1))
self.last_record = None
self.new_record_event.set()
result = await future
return result