mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44df1134a8 | ||
|
|
243c1673db | ||
|
|
82eab4810c | ||
|
|
e0a59b5729 | ||
|
|
ad3bad85db | ||
|
|
743d85de32 | ||
|
|
8bd32f878f | ||
|
|
eccb52c197 | ||
|
|
603d60d8b8 | ||
|
|
3ef04f4159 | ||
|
|
7888ee7938 | ||
|
|
b2a3cda7b5 | ||
|
|
80c6d29079 | ||
|
|
0b020deaef | ||
|
|
74c8bea756 | ||
|
|
af10d6261f | ||
|
|
a178278576 | ||
|
|
474fea8434 | ||
|
|
d271f7b0f7 | ||
|
|
7e2af515ed | ||
|
|
2d403ff18c | ||
|
|
f08244a990 | ||
|
|
4869e5cf80 | ||
|
|
b887504f9f | ||
|
|
ad43a4f732 | ||
|
|
8699938b61 | ||
|
|
40bd099153 | ||
|
|
750923d5ca | ||
|
|
6d99b30e2d | ||
|
|
a155f16560 | ||
|
|
d4aa045487 | ||
|
|
8080dd9822 | ||
|
|
2eab8d8113 | ||
|
|
b51e56718a | ||
|
|
088d448e10 | ||
|
|
4a89b4bce5 | ||
|
|
b351b4bcd4 | ||
|
|
b2edaf48e4 | ||
|
|
33e02cee82 | ||
|
|
91a8c9eb50 | ||
|
|
50da8a0554 | ||
|
|
42b5978d89 | ||
|
|
bf29fc67b4 | ||
|
|
dbc68d6b56 | ||
|
|
5dabf66e7c | ||
|
|
d6e4478eb6 | ||
|
|
23ef4719ba | ||
|
|
d799a85ab9 | ||
|
|
4ad6bcc636 | ||
|
|
d2473ec7e8 | ||
|
|
28797edc7c | ||
|
|
be1a643071 | ||
|
|
ee6b9778ac | ||
|
|
881c34bcb5 | ||
|
|
c22fc99235 | ||
|
|
0874efe58b | ||
|
|
f01665c998 | ||
|
|
ac23080f6a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -164,3 +164,4 @@ cython_debug/
|
||||
ffmpeg
|
||||
music
|
||||
test.sh
|
||||
conf
|
||||
|
||||
@@ -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
|
||||
|
||||
22
README.md
22
README.md
@@ -1,6 +1,6 @@
|
||||
# xiaomusic
|
||||
|
||||
使用小爱/红米音箱播放音乐,音乐使用 yt-dlp 下载。
|
||||
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
|
||||
|
||||
## 最简配置运行
|
||||
|
||||
@@ -76,7 +76,7 @@ pdm run xiaomusic.py
|
||||
## 在 Docker 里使用
|
||||
|
||||
```shell
|
||||
docker run -e MI_USER=<your-xiaomi-account> \
|
||||
docker run -e MI_USER='your-xiaomi-account' \
|
||||
-e MI_PASS='your-xiaomi-password' \
|
||||
-e MI_DID='your-xiaomi-speaker-mid' \
|
||||
-e MI_HARDWARE='L07A' \
|
||||
@@ -186,6 +186,21 @@ services:
|
||||
- XIAOMUSIC_SEARCH
|
||||
- XIAOMUSIC_PROXY
|
||||
|
||||
## 网络歌单功能
|
||||
|
||||
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
|
||||
|
||||
## 更多其他可选配置
|
||||
|
||||
- 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)
|
||||
@@ -206,3 +221,6 @@ services:
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#hanxi/xiaomusic&Date)
|
||||
|
||||
## 赞赏
|
||||
谢谢就够了
|
||||
|
||||
66
pdm.lock
generated
66
pdm.lock
generated
@@ -5,7 +5,7 @@
|
||||
groups = ["default", "lint"]
|
||||
strategy = ["cross_platform"]
|
||||
lock_version = "4.4.1"
|
||||
content_hash = "sha256:fa83f8134ccb4a432304dead5fe1303899418558634abaa0824e8d9fdfb1f490"
|
||||
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]]
|
||||
@@ -650,27 +662,27 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.4.8"
|
||||
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.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"},
|
||||
{file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"},
|
||||
{file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"},
|
||||
{file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"},
|
||||
{file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"},
|
||||
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"},
|
||||
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"},
|
||||
{file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"},
|
||||
{file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"},
|
||||
{file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"},
|
||||
{file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"},
|
||||
{file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"},
|
||||
{file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"},
|
||||
{file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"},
|
||||
{file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"},
|
||||
{file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"},
|
||||
{file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"},
|
||||
{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]]
|
||||
@@ -819,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 = [
|
||||
@@ -828,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"},
|
||||
]
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.1.44"
|
||||
version = "0.1.64"
|
||||
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"
|
||||
@@ -29,7 +29,7 @@ lint = [
|
||||
"ruff>=0.4.8",
|
||||
]
|
||||
[tool.ruff]
|
||||
select = [
|
||||
lint.select = [
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"E", # pycodestyle - Error
|
||||
@@ -38,13 +38,13 @@ select = [
|
||||
"W", # pycodestyle - Warning
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
ignore = [
|
||||
lint.ignore = [
|
||||
"E501", # line-too-long
|
||||
"W191", # tab-indentation
|
||||
]
|
||||
include = ["**/*.py", "**/*.pyi", "**/pyproject.toml"]
|
||||
|
||||
[tool.ruff.pydocstyle]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.pdm.scripts]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.44"
|
||||
__version__ = "0.1.64"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,28 +9,6 @@ from xiaomusic.utils import validate_proxy
|
||||
|
||||
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={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",
|
||||
@@ -73,6 +51,8 @@ KEY_MATCH_ORDER = [
|
||||
SUPPORT_MUSIC_TYPE = [
|
||||
".mp3",
|
||||
".flac",
|
||||
".wav",
|
||||
".ape",
|
||||
]
|
||||
|
||||
|
||||
@@ -82,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 = 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)
|
||||
@@ -95,23 +74,20 @@ 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")
|
||||
music_list_url: str = os.getenv("XIAOMUSIC_MUSIC_LIST_URL", "")
|
||||
music_list_json: str = os.getenv("XIAOMUSIC_MUSIC_LIST_JSON", "")
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
from threading import Thread
|
||||
|
||||
from flask import Flask, request, send_from_directory
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from waitress import serve
|
||||
|
||||
from xiaomusic import (
|
||||
@@ -11,13 +12,13 @@ from xiaomusic import (
|
||||
from xiaomusic.config import (
|
||||
KEY_WORD_DICT,
|
||||
)
|
||||
|
||||
# 隐藏 flask 启动告警
|
||||
# https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356
|
||||
# from flask import cli
|
||||
# cli.show_server_banner = lambda *_: None
|
||||
from xiaomusic.utils import (
|
||||
downloadfile,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
auth = HTTPBasicAuth()
|
||||
|
||||
host = "0.0.0.0"
|
||||
port = 8090
|
||||
static_path = "music"
|
||||
@@ -25,7 +26,20 @@ 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
|
||||
|
||||
@@ -39,6 +53,7 @@ def getversion():
|
||||
|
||||
|
||||
@app.route("/getvolume", methods=["GET"])
|
||||
@auth.login_required
|
||||
def getvolume():
|
||||
volume = xiaomusic.get_volume_ret()
|
||||
return {
|
||||
@@ -47,22 +62,31 @@ def getvolume():
|
||||
|
||||
|
||||
@app.route("/searchmusic", methods=["GET"])
|
||||
@auth.login_required
|
||||
def searchmusic():
|
||||
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")
|
||||
@@ -74,6 +98,7 @@ async def do_cmd():
|
||||
|
||||
|
||||
@app.route("/getsetting", methods=["GET"])
|
||||
@auth.login_required
|
||||
async def getsetting():
|
||||
config = xiaomusic.getconfig()
|
||||
log.debug(config)
|
||||
@@ -87,11 +112,14 @@ async def getsetting():
|
||||
"mi_hardware_list": alldevices["hardware_list"],
|
||||
"xiaomusic_search": config.search_prefix,
|
||||
"xiaomusic_proxy": config.proxy,
|
||||
"xiaomusic_music_list_url": config.music_list_url,
|
||||
"xiaomusic_music_list_json": config.music_list_json,
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
@app.route("/savesetting", methods=["POST"])
|
||||
@auth.login_required
|
||||
async def savesetting():
|
||||
data = request.get_json()
|
||||
log.info(data)
|
||||
@@ -100,16 +128,19 @@ async def savesetting():
|
||||
|
||||
|
||||
@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)
|
||||
@@ -117,6 +148,18 @@ def delmusic():
|
||||
return "success"
|
||||
|
||||
|
||||
@app.route("/downloadjson", methods=["POST"])
|
||||
@auth.login_required
|
||||
def downloadjson():
|
||||
data = request.get_json()
|
||||
log.info(data)
|
||||
ret, content = downloadfile(data["url"])
|
||||
return {
|
||||
"ret": ret,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
|
||||
def static_path_handler(filename):
|
||||
log.debug(filename)
|
||||
log.debug(static_path)
|
||||
|
||||
@@ -38,7 +38,8 @@ $(function(){
|
||||
$('#music_list').change(function() {
|
||||
const selectedValue = $(this).val();
|
||||
$('#music_name').empty();
|
||||
$.each(data[selectedValue], function(index, item) {
|
||||
const sorted_musics = data[selectedValue].sort(custom_sort_key);
|
||||
$.each(sorted_musics, function(index, item) {
|
||||
$('#music_name').append($('<option></option>').val(item).text(item));
|
||||
});
|
||||
});
|
||||
@@ -163,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);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#8e43e7" style="height: 48px; width: 48px;"><path d="M550.826667 154.666667a47.786667 47.786667 0 0 0-19.84 4.48L298.666667 298.666667H186.453333A80 80 0 0 0 106.666667 378.453333v267.093334A80 80 0 0 0 186.453333 725.333333H298.666667l232.32 139.52a47.786667 47.786667 0 0 0 19.84 4.48A46.506667 46.506667 0 0 0 597.333333 822.826667V201.173333a46.506667 46.506667 0 0 0-46.506666-46.506666zM554.666667 822.826667c0 3.413333-3.84 3.84-3.84 3.84L320 688.853333l-9.6-6.186666H186.453333A37.12 37.12 0 0 1 149.333333 645.546667V378.453333A37.12 37.12 0 0 1 186.453333 341.333333h123.946667l10.24-6.186666 229.546667-137.6s3.84 0 3.84 3.84zM667.52 346.026667a21.333333 21.333333 0 0 0 0 30.293333 192 192 0 0 1 0 271.36 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 234.666667 234.666667 0 0 0 0-331.946666 21.333333 21.333333 0 0 0-30.293333 0z"></path><path d="M804.48 219.52a21.333333 21.333333 0 0 0-30.293333 30.293333 370.986667 370.986667 0 0 1 0 524.373334 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 414.08 414.08 0 0 0 0-584.96z"></path></svg>
|
||||
<input id="volume" type="range"></input>
|
||||
<a href="/static/setting.html">
|
||||
<svg fill="#8e43e7" height="64px" width="64px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-11.88 -11.88 77.76 77.76" xml:space="preserve" stroke="#8e43e7" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)" stroke-width="0.00054"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)"><rect x="-11.88" y="-11.88" width="77.76" height="77.76" rx="18.6624" fill="#addcff" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.512"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M51.22,21h-5.052c-0.812,0-1.481-0.447-1.792-1.197s-0.153-1.54,0.42-2.114l3.572-3.571 c0.525-0.525,0.814-1.224,0.814-1.966c0-0.743-0.289-1.441-0.814-1.967l-4.553-4.553c-1.05-1.05-2.881-1.052-3.933,0l-3.571,3.571 c-0.574,0.573-1.366,0.733-2.114,0.421C33.447,9.313,33,8.644,33,7.832V2.78C33,1.247,31.753,0,30.22,0H23.78 C22.247,0,21,1.247,21,2.78v5.052c0,0.812-0.447,1.481-1.197,1.792c-0.748,0.313-1.54,0.152-2.114-0.421l-3.571-3.571 c-1.052-1.052-2.883-1.05-3.933,0l-4.553,4.553c-0.525,0.525-0.814,1.224-0.814,1.967c0,0.742,0.289,1.44,0.814,1.966l3.572,3.571 c0.573,0.574,0.73,1.364,0.42,2.114S8.644,21,7.832,21H2.78C1.247,21,0,22.247,0,23.78v6.439C0,31.753,1.247,33,2.78,33h5.052 c0.812,0,1.481,0.447,1.792,1.197s0.153,1.54-0.42,2.114l-3.572,3.571c-0.525,0.525-0.814,1.224-0.814,1.966 c0,0.743,0.289,1.441,0.814,1.967l4.553,4.553c1.051,1.051,2.881,1.053,3.933,0l3.571-3.572c0.574-0.573,1.363-0.731,2.114-0.42 c0.75,0.311,1.197,0.98,1.197,1.792v5.052c0,1.533,1.247,2.78,2.78,2.78h6.439c1.533,0,2.78-1.247,2.78-2.78v-5.052 c0-0.812,0.447-1.481,1.197-1.792c0.751-0.312,1.54-0.153,2.114,0.42l3.571,3.572c1.052,1.052,2.883,1.05,3.933,0l4.553-4.553 c0.525-0.525,0.814-1.224,0.814-1.967c0-0.742-0.289-1.44-0.814-1.966l-3.572-3.571c-0.573-0.574-0.73-1.364-0.42-2.114 S45.356,33,46.168,33h5.052c1.533,0,2.78-1.247,2.78-2.78V23.78C54,22.247,52.753,21,51.22,21z M52,30.22 C52,30.65,51.65,31,51.22,31h-5.052c-1.624,0-3.019,0.932-3.64,2.432c-0.622,1.5-0.295,3.146,0.854,4.294l3.572,3.571 c0.305,0.305,0.305,0.8,0,1.104l-4.553,4.553c-0.304,0.304-0.799,0.306-1.104,0l-3.571-3.572c-1.149-1.149-2.794-1.474-4.294-0.854 c-1.5,0.621-2.432,2.016-2.432,3.64v5.052C31,51.65,30.65,52,30.22,52H23.78C23.35,52,23,51.65,23,51.22v-5.052 c0-1.624-0.932-3.019-2.432-3.64c-0.503-0.209-1.021-0.311-1.533-0.311c-1.014,0-1.997,0.4-2.761,1.164l-3.571,3.572 c-0.306,0.306-0.801,0.304-1.104,0l-4.553-4.553c-0.305-0.305-0.305-0.8,0-1.104l3.572-3.571c1.148-1.148,1.476-2.794,0.854-4.294 C10.851,31.932,9.456,31,7.832,31H2.78C2.35,31,2,30.65,2,30.22V23.78C2,23.35,2.35,23,2.78,23h5.052 c1.624,0,3.019-0.932,3.64-2.432c0.622-1.5,0.295-3.146-0.854-4.294l-3.572-3.571c-0.305-0.305-0.305-0.8,0-1.104l4.553-4.553 c0.304-0.305,0.799-0.305,1.104,0l3.571,3.571c1.147,1.147,2.792,1.476,4.294,0.854C22.068,10.851,23,9.456,23,7.832V2.78 C23,2.35,23.35,2,23.78,2h6.439C30.65,2,31,2.35,31,2.78v5.052c0,1.624,0.932,3.019,2.432,3.64 c1.502,0.622,3.146,0.294,4.294-0.854l3.571-3.571c0.306-0.305,0.801-0.305,1.104,0l4.553,4.553c0.305,0.305,0.305,0.8,0,1.104 l-3.572,3.571c-1.148,1.148-1.476,2.794-0.854,4.294c0.621,1.5,2.016,2.432,3.64,2.432h5.052C51.65,23,52,23.35,52,23.78V30.22z"></path> <path d="M27,18c-4.963,0-9,4.037-9,9s4.037,9,9,9s9-4.037,9-9S31.963,18,27,18z M27,34c-3.859,0-7-3.141-7-7s3.141-7,7-7 s7,3.141,7,7S30.859,34,27,34z"></path> </g> </g></svg>
|
||||
<svg fill="#8e43e7" height="48px" width="48px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-11.88 -11.88 77.76 77.76" xml:space="preserve" stroke="#8e43e7" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)" stroke-width="0.00054"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)"><rect x="-11.88" y="-11.88" width="77.76" height="77.76" rx="18.6624" fill="#addcff" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.512"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M51.22,21h-5.052c-0.812,0-1.481-0.447-1.792-1.197s-0.153-1.54,0.42-2.114l3.572-3.571 c0.525-0.525,0.814-1.224,0.814-1.966c0-0.743-0.289-1.441-0.814-1.967l-4.553-4.553c-1.05-1.05-2.881-1.052-3.933,0l-3.571,3.571 c-0.574,0.573-1.366,0.733-2.114,0.421C33.447,9.313,33,8.644,33,7.832V2.78C33,1.247,31.753,0,30.22,0H23.78 C22.247,0,21,1.247,21,2.78v5.052c0,0.812-0.447,1.481-1.197,1.792c-0.748,0.313-1.54,0.152-2.114-0.421l-3.571-3.571 c-1.052-1.052-2.883-1.05-3.933,0l-4.553,4.553c-0.525,0.525-0.814,1.224-0.814,1.967c0,0.742,0.289,1.44,0.814,1.966l3.572,3.571 c0.573,0.574,0.73,1.364,0.42,2.114S8.644,21,7.832,21H2.78C1.247,21,0,22.247,0,23.78v6.439C0,31.753,1.247,33,2.78,33h5.052 c0.812,0,1.481,0.447,1.792,1.197s0.153,1.54-0.42,2.114l-3.572,3.571c-0.525,0.525-0.814,1.224-0.814,1.966 c0,0.743,0.289,1.441,0.814,1.967l4.553,4.553c1.051,1.051,2.881,1.053,3.933,0l3.571-3.572c0.574-0.573,1.363-0.731,2.114-0.42 c0.75,0.311,1.197,0.98,1.197,1.792v5.052c0,1.533,1.247,2.78,2.78,2.78h6.439c1.533,0,2.78-1.247,2.78-2.78v-5.052 c0-0.812,0.447-1.481,1.197-1.792c0.751-0.312,1.54-0.153,2.114,0.42l3.571,3.572c1.052,1.052,2.883,1.05,3.933,0l4.553-4.553 c0.525-0.525,0.814-1.224,0.814-1.967c0-0.742-0.289-1.44-0.814-1.966l-3.572-3.571c-0.573-0.574-0.73-1.364-0.42-2.114 S45.356,33,46.168,33h5.052c1.533,0,2.78-1.247,2.78-2.78V23.78C54,22.247,52.753,21,51.22,21z M52,30.22 C52,30.65,51.65,31,51.22,31h-5.052c-1.624,0-3.019,0.932-3.64,2.432c-0.622,1.5-0.295,3.146,0.854,4.294l3.572,3.571 c0.305,0.305,0.305,0.8,0,1.104l-4.553,4.553c-0.304,0.304-0.799,0.306-1.104,0l-3.571-3.572c-1.149-1.149-2.794-1.474-4.294-0.854 c-1.5,0.621-2.432,2.016-2.432,3.64v5.052C31,51.65,30.65,52,30.22,52H23.78C23.35,52,23,51.65,23,51.22v-5.052 c0-1.624-0.932-3.019-2.432-3.64c-0.503-0.209-1.021-0.311-1.533-0.311c-1.014,0-1.997,0.4-2.761,1.164l-3.571,3.572 c-0.306,0.306-0.801,0.304-1.104,0l-4.553-4.553c-0.305-0.305-0.305-0.8,0-1.104l3.572-3.571c1.148-1.148,1.476-2.794,0.854-4.294 C10.851,31.932,9.456,31,7.832,31H2.78C2.35,31,2,30.65,2,30.22V23.78C2,23.35,2.35,23,2.78,23h5.052 c1.624,0,3.019-0.932,3.64-2.432c0.622-1.5,0.295-3.146-0.854-4.294l-3.572-3.571c-0.305-0.305-0.305-0.8,0-1.104l4.553-4.553 c0.304-0.305,0.799-0.305,1.104,0l3.571,3.571c1.147,1.147,2.792,1.476,4.294,0.854C22.068,10.851,23,9.456,23,7.832V2.78 C23,2.35,23.35,2,23.78,2h6.439C30.65,2,31,2.35,31,2.78v5.052c0,1.624,0.932,3.019,2.432,3.64 c1.502,0.622,3.146,0.294,4.294-0.854l3.571-3.571c0.306-0.305,0.801-0.305,1.104,0l4.553,4.553c0.305,0.305,0.305,0.8,0,1.104 l-3.572,3.571c-1.148,1.148-1.476,2.794-0.854,4.294c0.621,1.5,2.016,2.432,3.64,2.432h5.052C51.65,23,52,23.35,52,23.78V30.22z"></path> <path d="M27,18c-4.963,0-9,4.037-9,9s4.037,9,9,9s9-4.037,9-9S31.963,18,27,18z M27,34c-3.859,0-7-3.141-7-7s3.141-7,7-7 s7,3.141,7,7S30.859,34,27,34z"></path> </g> </g></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -22,9 +22,14 @@
|
||||
</select>
|
||||
<label for="xiaomusic_proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
|
||||
<input id="xiaomusic_proxy" type="text" placeholder="http://192.168.2.5:8080"></input>
|
||||
<label for="xiaomusic_music_list_url">歌单地址:</label>
|
||||
<input id="xiaomusic_music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json"></input>
|
||||
<label for="xiaomusic_music_list_json">歌单内容:</label>
|
||||
<textarea id="xiaomusic_music_list_json" type="text"></textarea>
|
||||
</div>
|
||||
<hr>
|
||||
<button onclick="location.href='/';">返回首页</button>
|
||||
<button id="get_music_list">获取歌单</button>
|
||||
<button id="save">保存</button>
|
||||
|
||||
<footer>
|
||||
|
||||
@@ -40,6 +40,14 @@ $(function(){
|
||||
if (data.xiaomusic_proxy != "") {
|
||||
$("#xiaomusic_proxy").val(data.xiaomusic_proxy);
|
||||
}
|
||||
|
||||
if (data.xiaomusic_music_list_url != "") {
|
||||
$("#xiaomusic_music_list_url").val(data.xiaomusic_music_list_url);
|
||||
}
|
||||
|
||||
if (data.xiaomusic_music_list_json != "") {
|
||||
$("#xiaomusic_music_list_json").val(data.xiaomusic_music_list_json);
|
||||
}
|
||||
});
|
||||
|
||||
$("#save").on("click", () => {
|
||||
@@ -47,15 +55,21 @@ $(function(){
|
||||
var mi_hardware = $("#mi_hardware").val();
|
||||
var xiaomusic_search = $("#xiaomusic_search").val();
|
||||
var xiaomusic_proxy = $("#xiaomusic_proxy").val();
|
||||
var xiaomusic_music_list_url = $("#xiaomusic_music_list_url").val();
|
||||
var xiaomusic_music_list_json = $("#xiaomusic_music_list_json").val();
|
||||
console.log("mi_did", mi_did);
|
||||
console.log("mi_hardware", mi_hardware);
|
||||
console.log("xiaomusic_search", xiaomusic_search);
|
||||
console.log("xiaomusic_proxy", xiaomusic_proxy);
|
||||
console.log("xiaomusic_music_list_url", xiaomusic_music_list_url);
|
||||
console.log("xiaomusic_music_list_json", xiaomusic_music_list_json);
|
||||
var data = {
|
||||
mi_did: mi_did,
|
||||
mi_hardware: mi_hardware,
|
||||
xiaomusic_search: xiaomusic_search,
|
||||
xiaomusic_proxy: xiaomusic_proxy,
|
||||
xiaomusic_music_list_url: xiaomusic_music_list_url,
|
||||
xiaomusic_music_list_json: xiaomusic_music_list_json,
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
@@ -70,4 +84,30 @@ $(function(){
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#get_music_list").on("click", () => {
|
||||
var xiaomusic_music_list_url = $("#xiaomusic_music_list_url").val();
|
||||
console.log("xiaomusic_music_list_url", xiaomusic_music_list_url);
|
||||
var data = {
|
||||
url: xiaomusic_music_list_url,
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/downloadjson",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(data),
|
||||
success: (res) => {
|
||||
if (res.ret == "OK") {
|
||||
$("#xiaomusic_music_list_json").val(res.content);
|
||||
} else {
|
||||
console.log(res);
|
||||
alert(res.ret);
|
||||
}
|
||||
},
|
||||
error: (res) => {
|
||||
console.log(res);
|
||||
alert(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,3 +67,9 @@ footer {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
textarea{
|
||||
margin: 10px;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from collections.abc import AsyncIterator
|
||||
from http.cookies import SimpleCookie
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import aiohttp
|
||||
import mutagen
|
||||
import requests
|
||||
from requests.utils import cookiejar_from_dict
|
||||
|
||||
|
||||
@@ -64,3 +69,143 @@ def validate_proxy(proxy_str: str) -> bool:
|
||||
# 模糊搜索
|
||||
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
|
||||
|
||||
|
||||
def downloadfile(url):
|
||||
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):
|
||||
duration = 0
|
||||
headers = {"Range": f"bytes={start}-{end}"}
|
||||
async with session.get(url, headers=headers) as response:
|
||||
array_buffer = await response.read()
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||
tmp.write(array_buffer)
|
||||
name = tmp.name
|
||||
|
||||
try:
|
||||
m = mutagen.File(name)
|
||||
duration = m.info.length
|
||||
except Exception:
|
||||
pass
|
||||
os.remove(name)
|
||||
return duration
|
||||
|
||||
|
||||
async def get_web_music_duration(url, start=0, end=500):
|
||||
duration = 0
|
||||
try:
|
||||
# 设置总超时时间为3秒
|
||||
timeout = aiohttp.ClientTimeout(total=3)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
duration = await _get_web_music_duration(session, url, start=0, end=500)
|
||||
if duration <= 0:
|
||||
duration = await _get_web_music_duration(
|
||||
session, url, start=0, end=1000
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return duration
|
||||
|
||||
|
||||
# 获取文件播放时长
|
||||
def get_local_music_duration(filename):
|
||||
duration = 0
|
||||
try:
|
||||
m = mutagen.File(filename)
|
||||
duration = m.info.length
|
||||
except Exception:
|
||||
pass
|
||||
return duration
|
||||
|
||||
@@ -11,10 +11,8 @@ import traceback
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
import mutagen
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from miservice import MiAccount, MiIOService, MiNAService, miio_command
|
||||
from rich.logging import RichHandler
|
||||
from miservice import MiAccount, MiIOService, MiNAService
|
||||
|
||||
from xiaomusic import (
|
||||
__version__,
|
||||
@@ -30,14 +28,19 @@ from xiaomusic.config import (
|
||||
)
|
||||
from xiaomusic.httpserver import StartHTTPServer
|
||||
from xiaomusic.utils import (
|
||||
custom_sort_key,
|
||||
fuzzyfinder,
|
||||
get_local_music_duration,
|
||||
get_web_music_duration,
|
||||
parse_cookie_string,
|
||||
walk_to_depth,
|
||||
)
|
||||
|
||||
EOF = object()
|
||||
|
||||
PLAY_TYPE_ONE = 0 # 单曲循环
|
||||
PLAY_TYPE_ALL = 1 # 全部循环
|
||||
PLAY_TYPE_RND = 2 # 随机播放
|
||||
|
||||
|
||||
class XiaoMusic:
|
||||
@@ -56,22 +59,29 @@ 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._all_radio = {} # 电台列表
|
||||
self._play_list = []
|
||||
self._cur_play_list = ""
|
||||
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
|
||||
@@ -82,9 +92,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)
|
||||
@@ -99,7 +108,7 @@ class XiaoMusic:
|
||||
# 启动时初始化获取声音
|
||||
self.set_last_record("get_volume#")
|
||||
|
||||
self.log.debug("ffmpeg_location: %s", self.ffmpeg_location)
|
||||
self.log.info("ffmpeg_location: %s", self.ffmpeg_location)
|
||||
|
||||
async def poll_latest_ask(self):
|
||||
async with ClientSession() as session:
|
||||
@@ -120,7 +129,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 +147,30 @@ class XiaoMusic:
|
||||
self.miio_service = MiIOService(account)
|
||||
|
||||
async def try_update_device_id(self):
|
||||
hardware_data = await self.mina_service.device_list()
|
||||
# fix multi xiaoai problems we check did first
|
||||
# why we use this way to fix?
|
||||
# some videos and articles already in the Internet
|
||||
# we do not want to change old way, so we check if miotDID in `env` first
|
||||
# to set device id
|
||||
|
||||
for h in hardware_data:
|
||||
if did := self.config.mi_did:
|
||||
if h.get("miotDID", "") == str(did):
|
||||
try:
|
||||
hardware_data = await self.mina_service.device_list()
|
||||
for h in hardware_data:
|
||||
if did := self.config.mi_did:
|
||||
if h.get("miotDID", "") == str(did):
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if h.get("hardware", "") == self.config.hardware:
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if h.get("hardware", "") == self.config.hardware:
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
self.log.error(
|
||||
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
|
||||
)
|
||||
else:
|
||||
self.log.error(
|
||||
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
async def _init_data_hardware(self):
|
||||
if self.config.cookie:
|
||||
@@ -176,6 +190,8 @@ class XiaoMusic:
|
||||
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 +200,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 +232,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 +242,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 +264,25 @@ class XiaoMusic:
|
||||
|
||||
async def do_tts(self, value):
|
||||
self.log.info("do_tts: %s", 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}")
|
||||
|
||||
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}",
|
||||
)
|
||||
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
|
||||
if self._playing and not self.is_downloading():
|
||||
# 继续播放歌曲
|
||||
await self.play()
|
||||
|
||||
async def do_set_volume(self, value):
|
||||
value = int(value)
|
||||
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}")
|
||||
|
||||
async def force_stop_xiaoai(self):
|
||||
await self.mina_service.player_stop(self.device_id)
|
||||
@@ -321,10 +324,10 @@ class XiaoMusic:
|
||||
if self.proxy:
|
||||
sbp_args += ("--proxy", f"{self.proxy}")
|
||||
|
||||
self.log.info(f"download: {sbp_args}")
|
||||
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
|
||||
await self.do_tts(f"正在下载歌曲{search_key}")
|
||||
|
||||
# 本地是否存在歌曲
|
||||
def get_filename(self, name):
|
||||
if name not in self._all_music:
|
||||
self.log.debug("get_filename not in. name:%s", name)
|
||||
@@ -335,10 +338,39 @@ class XiaoMusic:
|
||||
return filename
|
||||
return ""
|
||||
|
||||
# 获取歌曲播放地址
|
||||
def get_file_url(self, name):
|
||||
# 判断本地音乐是否存在,网络歌曲不判断
|
||||
def is_music_exist(self, name):
|
||||
if name not in self._all_music:
|
||||
return False
|
||||
if self.is_web_music(name):
|
||||
return True
|
||||
filename = self.get_filename(name)
|
||||
self.log.debug("get_file_url. name:%s, filename:%s", name, filename)
|
||||
if filename:
|
||||
return True
|
||||
return False
|
||||
|
||||
# 是否是网络电台
|
||||
def is_web_radio_music(self, name):
|
||||
return name in self._all_radio
|
||||
|
||||
# 是否是网络歌曲
|
||||
def is_web_music(self, name):
|
||||
if name not in self._all_music:
|
||||
return False
|
||||
url = self._all_music[name]
|
||||
return url.startswith(("http://", "https://"))
|
||||
|
||||
# 获取歌曲播放地址
|
||||
def get_music_url(self, name):
|
||||
if self.is_web_music(name):
|
||||
url = self._all_music[name]
|
||||
self.log.debug("get_music_url web music. name:%s, url:%s", name, url)
|
||||
return url
|
||||
|
||||
filename = self.get_filename(name)
|
||||
self.log.debug(
|
||||
"get_music_url local music. name:%s, filename:%s", name, filename
|
||||
)
|
||||
encoded_name = urllib.parse.quote(filename)
|
||||
return f"http://{self.hostname}:{self.port}/{encoded_name}"
|
||||
|
||||
@@ -346,7 +378,10 @@ class XiaoMusic:
|
||||
def _gen_all_music_list(self):
|
||||
self._all_music = {}
|
||||
all_music_by_dir = {}
|
||||
for root, dirs, filenames in os.walk(self.music_path):
|
||||
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:
|
||||
@@ -366,16 +401,15 @@ 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())
|
||||
self._cur_play_list = "全部"
|
||||
random.shuffle(self._play_list)
|
||||
self._gen_play_list()
|
||||
self.log.debug(self._all_music)
|
||||
|
||||
self._music_list = {}
|
||||
@@ -383,7 +417,54 @@ class XiaoMusic:
|
||||
for dir_name, musics in all_music_by_dir.items():
|
||||
self._music_list[dir_name] = list(musics.keys())
|
||||
self.log.debug("dir_name:%s, list:%s", dir_name, self._music_list[dir_name])
|
||||
pass
|
||||
|
||||
try:
|
||||
self._append_music_list()
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption _append_music_list {e}")
|
||||
|
||||
# 给歌单里补充网络歌单
|
||||
def _append_music_list(self):
|
||||
if not self.config.music_list_json:
|
||||
return
|
||||
|
||||
music_list = json.loads(self.config.music_list_json)
|
||||
try:
|
||||
for item in music_list:
|
||||
list_name = item.get("name")
|
||||
musics = item.get("musics")
|
||||
if (not list_name) or (not musics):
|
||||
continue
|
||||
one_music_list = []
|
||||
for music in musics:
|
||||
name = music.get("name")
|
||||
url = music.get("url")
|
||||
music_type = music.get("type")
|
||||
if (not name) or (not url):
|
||||
continue
|
||||
self._all_music[name] = url
|
||||
one_music_list.append(name)
|
||||
|
||||
# 处理电台列表
|
||||
if music_type == "radio":
|
||||
self._all_radio[name] = url
|
||||
self.log.info(one_music_list)
|
||||
# 歌曲名字相同会覆盖
|
||||
self._music_list[list_name] = one_music_list
|
||||
if self._all_radio:
|
||||
self._music_list["所有电台"] = list(self._all_radio.keys())
|
||||
self.log.debug(self._all_music)
|
||||
self.log.debug(self._music_list)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption music_list:{music_list} {e}")
|
||||
|
||||
# 歌曲排序或者打乱顺序
|
||||
def _gen_play_list(self):
|
||||
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):
|
||||
@@ -409,29 +490,37 @@ class XiaoMusic:
|
||||
if next_index >= play_list_len:
|
||||
next_index = 0
|
||||
name = self._play_list[next_index]
|
||||
filename = self.get_filename(name)
|
||||
if len(filename) <= 0:
|
||||
if not self.is_music_exist(name):
|
||||
self._play_list.pop(next_index)
|
||||
self.log.info(f"pop not exist music:{name}")
|
||||
return self.get_next_music()
|
||||
return name
|
||||
|
||||
# 获取文件播放时长
|
||||
def get_file_duration(self, filename):
|
||||
# 获取音频文件对象
|
||||
audio = mutagen.File(filename)
|
||||
# 获取播放时长
|
||||
duration = audio.info.length
|
||||
return duration
|
||||
|
||||
# 设置下一首歌曲的播放定时器
|
||||
def set_next_music_timeout(self):
|
||||
filename = self.get_filename(self.cur_music)
|
||||
sec = int(self.get_file_duration(filename))
|
||||
self.log.info(f"歌曲 {self.cur_music} : {filename} 的时长 {sec} 秒")
|
||||
async def set_next_music_timeout(self):
|
||||
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():
|
||||
@@ -523,43 +612,63 @@ class XiaoMusic:
|
||||
return ("stop", {})
|
||||
return (None, None)
|
||||
|
||||
# 判断是否播放下一首歌曲
|
||||
def check_play_next(self):
|
||||
# 当前没我在播放的歌曲
|
||||
if self.cur_music == "":
|
||||
return True
|
||||
else:
|
||||
# 当前播放的歌曲不存在了
|
||||
if not self.is_music_exist(self.cur_music):
|
||||
return True
|
||||
return False
|
||||
|
||||
# 播放歌曲
|
||||
async def play(self, **kwargs):
|
||||
self._playing = True
|
||||
parts = kwargs["arg1"].split("|")
|
||||
parts = kwargs.get("arg1", "").split("|")
|
||||
search_key = parts[0]
|
||||
name = parts[1] if len(parts) > 1 else search_key
|
||||
if search_key == "" and name == "":
|
||||
await self.play_next()
|
||||
return
|
||||
if name == "":
|
||||
name = search_key
|
||||
self.log.debug("play. search_key:%s name:%s", search_key, name)
|
||||
filename = self.get_filename(name)
|
||||
|
||||
if len(filename) <= 0:
|
||||
if search_key == "" and name == "":
|
||||
if self.check_play_next():
|
||||
await self.play_next()
|
||||
return
|
||||
else:
|
||||
name = self.cur_music
|
||||
|
||||
self.log.info("play. search_key:%s name:%s", search_key, name)
|
||||
|
||||
# 本地歌曲不存在时下载
|
||||
if not self.is_music_exist(name):
|
||||
await self.download(search_key, name)
|
||||
self.log.info("正在下载中 %s", search_key + ":" + name)
|
||||
await self.download_proc.wait()
|
||||
# 把文件插入到播放列表里
|
||||
self.add_download_music(name)
|
||||
|
||||
self._playing = True
|
||||
self.cur_music = name
|
||||
self.log.info("cur_music %s", self.cur_music)
|
||||
url = self.get_file_url(name)
|
||||
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("已经开始播放了")
|
||||
# 设置下一首歌曲的播放定时器
|
||||
self.set_next_music_timeout()
|
||||
await self.set_next_music_timeout()
|
||||
|
||||
# 下一首
|
||||
async def play_next(self, **kwargs):
|
||||
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("本地没有歌曲")
|
||||
@@ -574,18 +683,18 @@ class XiaoMusic:
|
||||
# 全部循环
|
||||
async def set_play_type_all(self, **kwargs):
|
||||
self.play_type = PLAY_TYPE_ALL
|
||||
self._gen_play_list()
|
||||
await self.do_tts("已经设置为全部循环")
|
||||
|
||||
# 随机播放
|
||||
async def random_play(self, **kwargs):
|
||||
self.play_type = PLAY_TYPE_ALL
|
||||
random.shuffle(self._play_list)
|
||||
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()
|
||||
await self.do_tts("生成播放列表完毕")
|
||||
|
||||
# 删除歌曲
|
||||
def del_music(self, name):
|
||||
@@ -598,18 +707,18 @@ class XiaoMusic:
|
||||
self.log.info(f"del ${filename} success")
|
||||
except OSError:
|
||||
self.log.error(f"del ${filename} failed")
|
||||
pass
|
||||
self._gen_all_music_list()
|
||||
|
||||
# 播放一个播放列表
|
||||
async def play_music_list(self, **kwargs):
|
||||
parts = kwargs["arg1"].split("|")
|
||||
parts = kwargs.get("arg1").split("|")
|
||||
list_name = parts[0]
|
||||
if list_name not in self._music_list:
|
||||
await self.do_tts(f"播放列表{list_name}不存在")
|
||||
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 = ""
|
||||
@@ -624,14 +733,13 @@ class XiaoMusic:
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info("定时器已取消")
|
||||
self.cur_music = ""
|
||||
await self.force_stop_xiaoai()
|
||||
|
||||
async def stop_after_minute(self, **kwargs):
|
||||
if self._stop_timer:
|
||||
self._stop_timer.cancel()
|
||||
self.log.info("关机定时器已取消")
|
||||
minute = int(kwargs["arg1"])
|
||||
minute = int(kwargs.get("arg1", 0))
|
||||
|
||||
async def _do_stop():
|
||||
await asyncio.sleep(minute * 60)
|
||||
@@ -644,7 +752,7 @@ class XiaoMusic:
|
||||
self.log.info(f"{minute}分钟后将关机")
|
||||
|
||||
async def set_volume(self, **kwargs):
|
||||
value = kwargs["arg1"]
|
||||
value = kwargs.get("arg1", 0)
|
||||
await self.do_set_volume(value)
|
||||
|
||||
async def get_volume(self, **kwargs):
|
||||
@@ -678,13 +786,24 @@ class XiaoMusic:
|
||||
self.log.debug("playingmusic. cur_music:%s", self.cur_music)
|
||||
return self.cur_music
|
||||
|
||||
# 当前是否正在播放歌曲
|
||||
def isplaying(self):
|
||||
return self._playing
|
||||
|
||||
# 获取当前配置
|
||||
def getconfig(self):
|
||||
return self.config
|
||||
|
||||
# 获取设置文件
|
||||
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)
|
||||
@@ -692,21 +811,25 @@ 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")
|
||||
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)
|
||||
|
||||
def update_config_from_setting(self, data):
|
||||
self.config.mi_did = data["mi_did"]
|
||||
self.config.hardware = data["mi_hardware"]
|
||||
self.config.search_prefix = data["xiaomusic_search"]
|
||||
self.config.proxy = data["xiaomusic_proxy"]
|
||||
self.config.mi_did = data.get("mi_did")
|
||||
self.config.hardware = data.get("mi_hardware")
|
||||
self.config.search_prefix = data.get("xiaomusic_search")
|
||||
self.config.proxy = data.get("xiaomusic_proxy")
|
||||
self.config.music_list_url = data.get("xiaomusic_music_list_url")
|
||||
self.config.music_list_json = data.get("xiaomusic_music_list_json")
|
||||
|
||||
self.search_prefix = self.config.search_prefix
|
||||
self.proxy = self.config.proxy
|
||||
@@ -715,12 +838,11 @@ class XiaoMusic:
|
||||
# 重新初始化
|
||||
async def reinit(self, **kwargs):
|
||||
await self.try_update_device_id()
|
||||
self._gen_all_music_list()
|
||||
self.log.info("reinit success")
|
||||
|
||||
# 获取所有设备
|
||||
async def getalldevices(self, **kwargs):
|
||||
arg1 = kwargs["arg1"]
|
||||
self.log.debug("getalldevices. arg1:%s", arg1)
|
||||
did_list = []
|
||||
hardware_list = []
|
||||
hardware_data = await self.mina_service.device_list()
|
||||
|
||||
Reference in New Issue
Block a user