mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b32e2e29d8 | ||
|
|
8f22e2c66e | ||
|
|
8801bb73f4 | ||
|
|
82a215e3bc | ||
|
|
88e5d98c68 | ||
|
|
c7ab57c06a | ||
|
|
f1938b9096 | ||
|
|
5242deea33 | ||
|
|
d06b3cd2a5 | ||
|
|
2a59d1f69c | ||
|
|
358f7ccb98 | ||
|
|
1043b6f32f | ||
|
|
8f2d26ac10 | ||
|
|
8e8a605816 | ||
|
|
63eb0c22cb | ||
|
|
ca07ed0dd3 | ||
|
|
7d158ff40e | ||
|
|
90243b395a | ||
|
|
653bd417e5 | ||
|
|
6d89a24b28 | ||
|
|
0ca2adb014 | ||
|
|
a8a9e2bc45 | ||
|
|
7e7bb256b5 | ||
|
|
35c43646cb |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -63,4 +63,4 @@ jobs:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: hanxi/xiaomusic:${{ github.ref_name }}
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable
|
||||
|
||||
18
README.md
18
README.md
@@ -33,14 +33,21 @@ pdm run xiaomusic.py
|
||||
```txt
|
||||
"L07A": ("5-1", "5-5"), # Redmi小爱音箱Play(l7a)
|
||||
````
|
||||
## 支持音乐格式
|
||||
|
||||
- mp3
|
||||
- flac
|
||||
|
||||
> 本地音乐会搜索 mp3 和 flac 格式的文件,下载的歌曲是 mp3 格式的。
|
||||
|
||||
## 在 Docker 里使用
|
||||
|
||||
```shell
|
||||
docker run -e MI_USER=<your-xiaomi-account> -e MI_PASS=<your-xiaomi-password> -e MI_DID=<your-xiaomi-speaker-mid> -e XIAOMUSIC_PROXY=<proxy-for-yt-dlp> -e XIAOMUSIC_HOSTNAME=192.168.2.5 -p 8090:8090 -v ./music:/app/music hanxi/xiaomusic --hardware=<L07A> -
|
||||
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 -p 8090:8090 -v ./music:/app/music hanxi/xiaomusic
|
||||
```
|
||||
|
||||
- XIAOMUSIC_PROXY 用于配置代理,yt-dlp 工具下载歌曲会用到。
|
||||
- XIAOMUSIC_PROXY 用于配置代理,默认为空,yt-dlp 工具下载歌曲会用到。
|
||||
- MI_HARDWARE 是小米音箱的型号,默认为'L07A'
|
||||
- 注意端口必须映射为与容器内一致,XIAOMUSIC_HOSTNAME 需要设置为宿主机的 IP 地址,否则小爱无法正常播放。
|
||||
- 可以把 /app/music 目录映射到本地,用于保存下载的歌曲。
|
||||
|
||||
@@ -50,6 +57,13 @@ docker run -e MI_USER=<your-xiaomi-account> -e MI_PASS=<your-xiaomi-password> -e
|
||||
docker build -t xiaomusic .
|
||||
```
|
||||
|
||||
## 简易的控制面板
|
||||
|
||||
浏览器进入 <http://192.168.2.5:8090>
|
||||
|
||||
- ip 是 XIAOMUSIC_HOSTNAME 设置的
|
||||
- 8090 是默认端口
|
||||
|
||||
## 感谢
|
||||
|
||||
- [xiaomi](https://www.mi.com/)
|
||||
|
||||
165
pdm.lock
generated
165
pdm.lock
generated
@@ -6,7 +6,7 @@ groups = ["default"]
|
||||
cross_platform = true
|
||||
static_urls = false
|
||||
lock_version = "4.3"
|
||||
content_hash = "sha256:c123354af86c15de519dfa703c59361f09898eb635df198f3dcf9ef6ed41ffba"
|
||||
content_hash = "sha256:091ebbfd2c575745f3dae0440e7aaa775c6e2121050f4c101b0676a0bcef7606"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
@@ -69,6 +69,19 @@ files = [
|
||||
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.7.2"
|
||||
requires_python = ">=3.7"
|
||||
summary = "ASGI specs, helper code, and adapters"
|
||||
dependencies = [
|
||||
"typing-extensions>=4; python_version < \"3.11\"",
|
||||
]
|
||||
files = [
|
||||
{file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"},
|
||||
{file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "4.0.3"
|
||||
@@ -89,6 +102,16 @@ files = [
|
||||
{file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.7.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Fast, simple object-to-object and broadcast signaling"
|
||||
files = [
|
||||
{file = "blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9"},
|
||||
{file = "blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "1.1.0"
|
||||
@@ -280,6 +303,61 @@ files = [
|
||||
{file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
requires_python = ">=3.7"
|
||||
summary = "Composable command line interface toolkit"
|
||||
dependencies = [
|
||||
"colorama; platform_system == \"Windows\"",
|
||||
]
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
summary = "Cross-platform colored terminal text."
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.0.1"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A simple framework for building complex web applications."
|
||||
dependencies = [
|
||||
"Jinja2>=3.1.2",
|
||||
"Werkzeug>=3.0.0",
|
||||
"blinker>=1.6.2",
|
||||
"click>=8.1.3",
|
||||
"itsdangerous>=2.1.2",
|
||||
]
|
||||
files = [
|
||||
{file = "flask-3.0.1-py3-none-any.whl", hash = "sha256:ca631a507f6dfe6c278ae20112cea3ff54ff2216390bf8880f6b035a5354af13"},
|
||||
{file = "flask-3.0.1.tar.gz", hash = "sha256:6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.0.1"
|
||||
extras = ["async"]
|
||||
requires_python = ">=3.8"
|
||||
summary = "A simple framework for building complex web applications."
|
||||
dependencies = [
|
||||
"asgiref>=3.2",
|
||||
"flask==3.0.1",
|
||||
]
|
||||
files = [
|
||||
{file = "flask-3.0.1-py3-none-any.whl", hash = "sha256:ca631a507f6dfe6c278ae20112cea3ff54ff2216390bf8880f6b035a5354af13"},
|
||||
{file = "flask-3.0.1.tar.gz", hash = "sha256:6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.4.0"
|
||||
@@ -329,6 +407,29 @@ files = [
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.1.2"
|
||||
requires_python = ">=3.7"
|
||||
summary = "Safely pass data to untrusted environments and back."
|
||||
files = [
|
||||
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.3"
|
||||
requires_python = ">=3.7"
|
||||
summary = "A very fast and expressive template engine."
|
||||
dependencies = [
|
||||
"MarkupSafe>=2.0",
|
||||
]
|
||||
files = [
|
||||
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
|
||||
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
@@ -342,6 +443,45 @@ files = [
|
||||
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.4"
|
||||
requires_python = ">=3.7"
|
||||
summary = "Safely add untrusted strings to HTML/XML markup."
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"},
|
||||
{file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
@@ -493,6 +633,16 @@ files = [
|
||||
{file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.9.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
files = [
|
||||
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
|
||||
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.0.6"
|
||||
@@ -550,6 +700,19 @@ files = [
|
||||
{file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.0.1"
|
||||
requires_python = ">=3.8"
|
||||
summary = "The comprehensive WSGI web application library."
|
||||
dependencies = [
|
||||
"MarkupSafe>=2.1.1",
|
||||
]
|
||||
files = [
|
||||
{file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"},
|
||||
{file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.9.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.1.3"
|
||||
version = "0.1.7"
|
||||
description = "Play Music with xiaomi AI speaker"
|
||||
authors = [
|
||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||
@@ -12,6 +12,7 @@ dependencies = [
|
||||
"miservice-fork>=2.2.1",
|
||||
"mutagen>=1.47.0",
|
||||
"yt-dlp>=2023.10.13",
|
||||
"flask[async]>=3.0.1",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -36,12 +36,18 @@ aiohttp==3.8.6 \
|
||||
aiosignal==1.3.1 \
|
||||
--hash=sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc \
|
||||
--hash=sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17
|
||||
asgiref==3.7.2 \
|
||||
--hash=sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e \
|
||||
--hash=sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed
|
||||
async-timeout==4.0.3 \
|
||||
--hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \
|
||||
--hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028
|
||||
attrs==23.1.0 \
|
||||
--hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
|
||||
--hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
|
||||
blinker==1.7.0 \
|
||||
--hash=sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9 \
|
||||
--hash=sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182
|
||||
brotli==1.1.0 \
|
||||
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
|
||||
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
|
||||
@@ -131,6 +137,12 @@ charset-normalizer==3.3.0 \
|
||||
--hash=sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293 \
|
||||
--hash=sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e \
|
||||
--hash=sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e
|
||||
click==8.1.7 \
|
||||
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
|
||||
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
|
||||
flask==3.0.1 \
|
||||
--hash=sha256:6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403 \
|
||||
--hash=sha256:ca631a507f6dfe6c278ae20112cea3ff54ff2216390bf8880f6b035a5354af13
|
||||
frozenlist==1.4.0 \
|
||||
--hash=sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01 \
|
||||
--hash=sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251 \
|
||||
@@ -166,9 +178,47 @@ frozenlist==1.4.0 \
|
||||
idna==3.4 \
|
||||
--hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
|
||||
--hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
|
||||
itsdangerous==2.1.2 \
|
||||
--hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \
|
||||
--hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a
|
||||
Jinja2==3.1.3 \
|
||||
--hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \
|
||||
--hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90
|
||||
markdown-it-py==3.0.0 \
|
||||
--hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
|
||||
--hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
|
||||
MarkupSafe==2.1.4 \
|
||||
--hash=sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69 \
|
||||
--hash=sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0 \
|
||||
--hash=sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d \
|
||||
--hash=sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74 \
|
||||
--hash=sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d \
|
||||
--hash=sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f \
|
||||
--hash=sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6 \
|
||||
--hash=sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656 \
|
||||
--hash=sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc \
|
||||
--hash=sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56 \
|
||||
--hash=sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc \
|
||||
--hash=sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250 \
|
||||
--hash=sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc \
|
||||
--hash=sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863 \
|
||||
--hash=sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8 \
|
||||
--hash=sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f \
|
||||
--hash=sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2 \
|
||||
--hash=sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e \
|
||||
--hash=sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e \
|
||||
--hash=sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb \
|
||||
--hash=sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26 \
|
||||
--hash=sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131 \
|
||||
--hash=sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858 \
|
||||
--hash=sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e \
|
||||
--hash=sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84 \
|
||||
--hash=sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7 \
|
||||
--hash=sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea \
|
||||
--hash=sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b \
|
||||
--hash=sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6 \
|
||||
--hash=sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475 \
|
||||
--hash=sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74
|
||||
mdurl==0.1.2 \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
@@ -241,6 +291,9 @@ requests==2.31.0 \
|
||||
rich==13.6.0 \
|
||||
--hash=sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245 \
|
||||
--hash=sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef
|
||||
typing-extensions==4.9.0 \
|
||||
--hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \
|
||||
--hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd
|
||||
urllib3==2.0.6 \
|
||||
--hash=sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2 \
|
||||
--hash=sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564
|
||||
@@ -284,6 +337,9 @@ websockets==11.0.3 \
|
||||
--hash=sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602 \
|
||||
--hash=sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf \
|
||||
--hash=sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564
|
||||
Werkzeug==3.0.1 \
|
||||
--hash=sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc \
|
||||
--hash=sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10
|
||||
yarl==1.9.2 \
|
||||
--hash=sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571 \
|
||||
--hash=sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7 \
|
||||
|
||||
1
update-requirements.sh
Normal file
1
update-requirements.sh
Normal file
@@ -0,0 +1 @@
|
||||
pdm export -o requirements.txt
|
||||
@@ -37,15 +37,41 @@ KEY_WORD_DICT = {
|
||||
"播放歌曲": "play",
|
||||
"放歌曲": "play",
|
||||
"下一首": "play_next",
|
||||
"单曲循环":"set_play_type_one",
|
||||
"全部循环":"set_play_type_all",
|
||||
"关机":"stop",
|
||||
"停止播放":"stop",
|
||||
"单曲循环": "set_play_type_one",
|
||||
"全部循环": "set_play_type_all",
|
||||
"随机播放": "random_play",
|
||||
"关机": "stop",
|
||||
"停止播放": "stop",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
}
|
||||
|
||||
# 命令参数在前面
|
||||
KEY_WORD_ARG_BEFORE_DICT = {
|
||||
"分钟后关机": True,
|
||||
}
|
||||
|
||||
# 匹配优先级
|
||||
KEY_MATCH_ORDER = [
|
||||
"分钟后关机",
|
||||
"播放歌曲",
|
||||
"放歌曲",
|
||||
"下一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"停止播放",
|
||||
]
|
||||
|
||||
SUPPORT_MUSIC_TYPE = [
|
||||
"mp3",
|
||||
"flac",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
hardware: str = "L07A"
|
||||
hardware: str = os.getenv("MI_HARDWARE", "L07A")
|
||||
account: str = os.getenv("MI_USER", "")
|
||||
password: str = os.getenv("MI_PASS", "")
|
||||
mi_did: str = os.getenv("MI_DID", "")
|
||||
@@ -56,7 +82,7 @@ class Config:
|
||||
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
|
||||
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
|
||||
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090"))
|
||||
proxy: str = os.getenv("XIAOMUSIC_PROXY", "http://192.168.2.5:8080")
|
||||
proxy: str | None = os.getenv("XIAOMUSIC_PROXY", None)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.proxy:
|
||||
|
||||
68
xiaomusic/httpserver.py
Normal file
68
xiaomusic/httpserver.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from flask import Flask, request, send_from_directory
|
||||
from threading import Thread
|
||||
|
||||
from xiaomusic.config import (
|
||||
KEY_WORD_DICT,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
host = "0.0.0.0"
|
||||
port = 8090
|
||||
static_path = "music"
|
||||
xiaomusic = None
|
||||
log = None
|
||||
|
||||
|
||||
@app.route("/allcmds")
|
||||
def allcmds():
|
||||
return KEY_WORD_DICT
|
||||
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
def redirect_to_index():
|
||||
return send_from_directory("static", "index.html")
|
||||
|
||||
|
||||
@app.route("/cmd", methods=["POST"])
|
||||
async def do_cmd():
|
||||
data = request.get_json()
|
||||
cmd = data.get("cmd")
|
||||
if len(cmd) > 0:
|
||||
log.debug("docmd. cmd:%s", cmd)
|
||||
xiaomusic.set_last_record(cmd)
|
||||
return {"ret": "OK"}
|
||||
return {"ret": "Unknow cmd"}
|
||||
|
||||
|
||||
def static_path_handler(filename):
|
||||
log.debug(filename)
|
||||
log.debug(static_path)
|
||||
absolute_path = os.path.abspath(static_path)
|
||||
log.debug(absolute_path)
|
||||
return send_from_directory(absolute_path, filename)
|
||||
|
||||
|
||||
def run_app():
|
||||
app.run(host=host, port=port)
|
||||
|
||||
|
||||
def StartHTTPServer(_host, _port, _static_path, _xiaomusic):
|
||||
global host, port, static_path, xiaomusic, log
|
||||
host = _host
|
||||
port = _port
|
||||
static_path = _static_path
|
||||
xiaomusic = _xiaomusic
|
||||
log = xiaomusic.log
|
||||
|
||||
app.add_url_rule(
|
||||
f"/{static_path}/<path:filename>", "static_path_handler", static_path_handler
|
||||
)
|
||||
|
||||
server_thread = Thread(target=run_app)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
xiaomusic.log.info(f"Serving on {host}:{port}")
|
||||
63
xiaomusic/static/app.js
Normal file
63
xiaomusic/static/app.js
Normal file
@@ -0,0 +1,63 @@
|
||||
$(function(){
|
||||
// 拉取所有可操作的命令
|
||||
$.get("/allcmds", function(data, status) {
|
||||
console.log(data, status);
|
||||
|
||||
$container=$("#cmds");
|
||||
// 遍历数据
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (key != "分钟后关机" && key != "放歌曲") {
|
||||
append_op_button(key);
|
||||
}
|
||||
}
|
||||
|
||||
append_op_button("5分钟后关机");
|
||||
append_op_button("10分钟后关机");
|
||||
append_op_button("30分钟后关机");
|
||||
append_op_button("60分钟后关机");
|
||||
});
|
||||
|
||||
function append_op_button(name) {
|
||||
// 创建按钮
|
||||
const $button = $("<button>");
|
||||
$button.text(name);
|
||||
$button.attr("type", "button");
|
||||
|
||||
// 设置按钮点击事件
|
||||
$button.on("click", () => {
|
||||
// 发起post请求
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/cmd",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({cmd: name}),
|
||||
success: () => {
|
||||
// 请求成功时执行的操作
|
||||
},
|
||||
error: () => {
|
||||
// 请求失败时执行的操作
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 添加按钮到容器
|
||||
$container.append($button);
|
||||
}
|
||||
|
||||
$("#play").on("click", () => {
|
||||
name = $("#music-name").val();
|
||||
let cmd = "播放歌曲"+name;
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/cmd",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({cmd: cmd}),
|
||||
success: () => {
|
||||
// 请求成功时执行的操作
|
||||
},
|
||||
error: () => {
|
||||
// 请求失败时执行的操作
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
30
xiaomusic/static/index.html
Normal file
30
xiaomusic/static/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="/static/jquery-3.7.1.min.js"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
<style>
|
||||
button {
|
||||
margin: 10px;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
}
|
||||
input {
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>小爱音箱操控面板</h2>
|
||||
<hr>
|
||||
<div id="cmds">
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<input id="music-name" type="text" placeholder="请输入歌曲名称"></input>
|
||||
<button id="play">播放</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2
xiaomusic/static/jquery-3.7.1.min.js
vendored
Normal file
2
xiaomusic/static/jquery-3.7.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,22 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import functools
|
||||
import http.server
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import socketserver
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import traceback
|
||||
import mutagen.mp3
|
||||
import mutagen
|
||||
from xiaomusic.httpserver import StartHTTPServer
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -29,6 +22,9 @@ 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 (
|
||||
@@ -38,28 +34,9 @@ from xiaomusic.utils import (
|
||||
|
||||
EOF = object()
|
||||
|
||||
PLAY_TYPE_ONE = 0 # 单曲循环
|
||||
PLAY_TYPE_ALL = 1 # 全部循环
|
||||
PLAY_TYPE_ONE = 0 # 单曲循环
|
||||
PLAY_TYPE_ALL = 1 # 全部循环
|
||||
|
||||
class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
logger = logging.getLogger("xiaomusic")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
self.logger.debug(f"{self.address_string()} - {format}", *args)
|
||||
|
||||
def log_error(self, format, *args):
|
||||
self.logger.error(f"{self.address_string()} - {format}", *args)
|
||||
|
||||
def copyfile(self, source, outputfile):
|
||||
try:
|
||||
super().copyfile(source, outputfile)
|
||||
except (socket.error, ConnectionResetError, BrokenPipeError):
|
||||
# ignore this or TODO find out why the error later
|
||||
pass
|
||||
|
||||
class XiaoMusic:
|
||||
def __init__(self, config: Config):
|
||||
@@ -83,11 +60,14 @@ class XiaoMusic:
|
||||
# 下载对象
|
||||
self.download_proc = None
|
||||
# 单曲循环,全部循环
|
||||
self.play_type = PLAY_TYPE_ONE
|
||||
self.play_type = PLAY_TYPE_ALL
|
||||
self.cur_music = ""
|
||||
self._next_timer = None
|
||||
self._timeout = 0
|
||||
|
||||
# 关机定时器
|
||||
self._stop_timer = None
|
||||
|
||||
# setup logger
|
||||
self.log = logging.getLogger("xiaomusic")
|
||||
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
|
||||
@@ -98,16 +78,16 @@ class XiaoMusic:
|
||||
async with ClientSession() as session:
|
||||
session._cookie_jar = self.cookie_jar
|
||||
while True:
|
||||
self.log.debug(
|
||||
"Listening new message, timestamp: %s", self.last_timestamp
|
||||
)
|
||||
# self.log.debug(
|
||||
# "Listening new message, timestamp: %s", self.last_timestamp
|
||||
# )
|
||||
await self.get_latest_ask_from_xiaoai(session)
|
||||
start = time.perf_counter()
|
||||
self.log.debug("Polling_event, timestamp: %s", self.last_timestamp)
|
||||
# self.log.debug("Polling_event, timestamp: %s", self.last_timestamp)
|
||||
await self.polling_event.wait()
|
||||
if (d := time.perf_counter() - start) < 1:
|
||||
# sleep to avoid too many request
|
||||
self.log.debug("Sleep %f, timestamp: %s", d, self.last_timestamp)
|
||||
# self.log.debug("Sleep %f, timestamp: %s", d, self.last_timestamp)
|
||||
await asyncio.sleep(1 - d)
|
||||
|
||||
async def init_all_data(self, session):
|
||||
@@ -115,7 +95,7 @@ class XiaoMusic:
|
||||
await self._init_data_hardware()
|
||||
session.cookie_jar.update_cookies(self.get_cookie())
|
||||
self.cookie_jar = session.cookie_jar
|
||||
self.start_http_server()
|
||||
StartHTTPServer(self.hostname, self.port, self.music_path, self)
|
||||
|
||||
async def login_miboy(self, session):
|
||||
account = MiAccount(
|
||||
@@ -225,6 +205,13 @@ class XiaoMusic:
|
||||
self.last_record = last_record
|
||||
self.new_record_event.set()
|
||||
|
||||
# 手动发消息
|
||||
def set_last_record(self, query):
|
||||
self.last_record = {
|
||||
"query": query,
|
||||
}
|
||||
self.new_record_event.set()
|
||||
|
||||
async def do_tts(self, value, wait_for_finish=False):
|
||||
self.log.info("do_tts: %s", value)
|
||||
if not self.config.use_command:
|
||||
@@ -249,17 +236,6 @@ class XiaoMusic:
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def start_http_server(self):
|
||||
# create the server
|
||||
handler = functools.partial(HTTPRequestHandler, directory=self.music_path)
|
||||
httpd = ThreadedHTTPServer(("", self.port), handler)
|
||||
# start the server in a new thread
|
||||
server_thread = threading.Thread(target=httpd.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
self.log.info(f"Serving on {self.hostname}:{self.port}")
|
||||
|
||||
async def get_if_xiaoai_is_playing(self):
|
||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
||||
# WTF xiaomi api
|
||||
@@ -286,63 +262,87 @@ 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 != None and self.download_proc.returncode < 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
# 下载歌曲
|
||||
async def download(self, name):
|
||||
if self.download_proc:
|
||||
self.download_proc.kill()
|
||||
try:
|
||||
self.download_proc.kill()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
self.download_proc = await asyncio.create_subprocess_exec(
|
||||
"yt-dlp", f"ytsearch:{name}",
|
||||
"-x", "--audio-format", "mp3",
|
||||
"--paths", self.music_path,
|
||||
"-o", f"{name}.mp3",
|
||||
"--proxy", f"{self.proxy}",
|
||||
"--ffmpeg-location", "./ffmpeg/bin")
|
||||
sbp_args = (
|
||||
"yt-dlp",
|
||||
f"ytsearch:{name}",
|
||||
"-x",
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"--paths",
|
||||
self.music_path,
|
||||
"-o",
|
||||
f"{name}.mp3",
|
||||
"--ffmpeg-location",
|
||||
"./ffmpeg/bin",
|
||||
)
|
||||
|
||||
if self.proxy:
|
||||
sbp_args += ("--proxy", f"{self.proxy}")
|
||||
|
||||
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
|
||||
await self.do_tts(f"正在下载歌曲{name}")
|
||||
|
||||
def get_filename(self, name):
|
||||
filename = os.path.join(self.music_path, f"{name}.mp3")
|
||||
filename = os.path.join(self.music_path, name)
|
||||
return filename
|
||||
|
||||
# 本地是否存在歌曲
|
||||
def local_exist(self, name):
|
||||
filename = self.get_filename(name)
|
||||
self.log.debug("local_exist. filename:%s", filename)
|
||||
return os.path.exists(filename)
|
||||
for tp in SUPPORT_MUSIC_TYPE:
|
||||
filename = self.get_filename(f"{name}.{tp}")
|
||||
self.log.debug("try local_exist. filename:%s", filename)
|
||||
if os.path.exists(filename):
|
||||
return filename
|
||||
return ""
|
||||
|
||||
# 获取歌曲播放地址
|
||||
def get_file_url(self, name):
|
||||
encoded_name = urllib.parse.quote(os.path.basename(name))
|
||||
return f"http://{self.hostname}:{self.port}/{encoded_name}.mp3"
|
||||
def get_file_url(self, filename):
|
||||
self.log.debug("get_file_url. filename:%s", filename)
|
||||
encoded_name = urllib.parse.quote(filename)
|
||||
return f"http://{self.hostname}:{self.port}/{encoded_name}"
|
||||
|
||||
# 随机获取一首音乐
|
||||
def random_music(self):
|
||||
files = os.listdir(self.music_path)
|
||||
# 过滤 mp3 文件
|
||||
mp3_files = [file for file in files if file.endswith(".mp3")]
|
||||
if len(mp3_files) == 0:
|
||||
# 过滤音乐文件
|
||||
music_files = []
|
||||
for file in files:
|
||||
for tp in SUPPORT_MUSIC_TYPE:
|
||||
if file.endswith(f".{tp}"):
|
||||
music_files.append(file)
|
||||
|
||||
if len(music_files) == 0:
|
||||
self.log.warning(f"没有随机到歌曲")
|
||||
return ""
|
||||
# 随机选择一个文件
|
||||
mp3_file = random.choice(mp3_files)
|
||||
name = mp3_file[:-4]
|
||||
self.log.info(f"随机到歌曲{name}")
|
||||
return name
|
||||
music_file = random.choice(music_files)
|
||||
(filename, extension) = os.path.splitext(music_file)
|
||||
self.log.info(f"随机到歌曲{filename}{extension}")
|
||||
return filename
|
||||
|
||||
# 获取mp3文件播放时长
|
||||
def get_mp3_duration(self, name):
|
||||
filename = self.get_filename(name)
|
||||
audio = mutagen.mp3.MP3(filename)
|
||||
return audio.info.length
|
||||
# 获取文件播放时长
|
||||
def get_file_duration(self, filename):
|
||||
# 获取音频文件对象
|
||||
audio = mutagen.File(filename)
|
||||
# 获取播放时长
|
||||
duration = audio.info.length
|
||||
return duration
|
||||
|
||||
# 设置下一首歌曲的播放定时器
|
||||
def set_next_music_timeout(self):
|
||||
sec = int(self.get_mp3_duration(self.cur_music))
|
||||
sec = int(self.get_file_duration(self.cur_music))
|
||||
self.log.info(f"歌曲{self.cur_music}的时长{sec}秒")
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
@@ -351,7 +351,10 @@ class XiaoMusic:
|
||||
|
||||
async def _do_next():
|
||||
await asyncio.sleep(self._timeout)
|
||||
await self.play_next()
|
||||
try:
|
||||
await self.play_next()
|
||||
except Exception as e:
|
||||
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
|
||||
|
||||
self._next_timer = asyncio.ensure_future(_do_next())
|
||||
self.log.info(f"{sec}秒后将会播放下一首")
|
||||
@@ -362,7 +365,10 @@ 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
|
||||
self.log.info(f"Running xiaomusic now, 用`{'/'.join(KEY_WORD_DICT.keys())}`开头来控制")
|
||||
self.log.info(
|
||||
f"Running xiaomusic now, 用`{'/'.join(KEY_WORD_DICT.keys())}`开头来控制"
|
||||
)
|
||||
|
||||
while True:
|
||||
self.polling_event.set()
|
||||
await self.new_record_event.wait()
|
||||
@@ -373,8 +379,8 @@ class XiaoMusic:
|
||||
self.log.debug("收到消息:%s", query)
|
||||
|
||||
# 匹配命令
|
||||
match = re.match(rf"^({'|'.join(KEY_WORD_DICT.keys())})", query)
|
||||
if not match:
|
||||
opvalue, oparg = self.match_cmd(query)
|
||||
if not opvalue:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
@@ -384,32 +390,53 @@ class XiaoMusic:
|
||||
# waiting for xiaoai speaker done
|
||||
await asyncio.sleep(8)
|
||||
|
||||
opkey = match.groups()[0]
|
||||
opvalue = KEY_WORD_DICT[opkey]
|
||||
oparg = query[len(opkey):]
|
||||
self.log.info("收到指令:%s %s", opkey, oparg)
|
||||
|
||||
try:
|
||||
func = getattr(self, opvalue)
|
||||
await func(name = oparg)
|
||||
await func(arg1=oparg)
|
||||
except Exception as e:
|
||||
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
|
||||
|
||||
# 匹配命令
|
||||
def match_cmd(self, query):
|
||||
for opkey in KEY_MATCH_ORDER:
|
||||
patternarg = rf"(.*){opkey}(.*)"
|
||||
# 匹配参数
|
||||
matcharg = re.match(patternarg, query)
|
||||
if not matcharg:
|
||||
continue
|
||||
|
||||
argpre = matcharg.groups()[0]
|
||||
argafter = matcharg.groups()[1]
|
||||
self.log.debug(
|
||||
"matcharg. opkey:%s, argpre:%s, argafter:%s",
|
||||
opkey,
|
||||
argpre,
|
||||
argafter,
|
||||
)
|
||||
oparg = argafter
|
||||
opvalue = KEY_WORD_DICT[opkey]
|
||||
if opkey in KEY_WORD_ARG_BEFORE_DICT:
|
||||
oparg = argpre
|
||||
self.log.info("匹配到指令. opkey:%s opvalue:%s oparg:%s", opkey, opvalue, oparg)
|
||||
return (opvalue, oparg)
|
||||
return (None, None)
|
||||
|
||||
# 播放歌曲
|
||||
async def play(self, **kwargs):
|
||||
name = kwargs["name"]
|
||||
name = kwargs["arg1"]
|
||||
if name == "":
|
||||
await self.play_next()
|
||||
return
|
||||
|
||||
await self.do_tts(f"即将播放{name}")
|
||||
if not self.local_exist(name):
|
||||
filename = self.local_exist(name)
|
||||
if len(filename) <= 0:
|
||||
await self.download(name)
|
||||
self.log.info("正在下载中 %s", name)
|
||||
filename = self.get_filename(f"{name}.mp3")
|
||||
await self.download_proc.wait()
|
||||
|
||||
self.cur_music = name
|
||||
url = self.get_file_url(name)
|
||||
self.cur_music = filename
|
||||
url = self.get_file_url(filename)
|
||||
self.log.info("播放 %s", url)
|
||||
await self.stop_if_xiaoai_is_playing()
|
||||
await self.mina_service.play_by_url(self.device_id, url)
|
||||
@@ -420,7 +447,8 @@ class XiaoMusic:
|
||||
# 下一首
|
||||
async def play_next(self, **kwargs):
|
||||
self.log.info("下一首")
|
||||
name = self.cur_music
|
||||
(name, _) = os.path.splitext(os.path.basename(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 == "":
|
||||
name = self.random_music()
|
||||
if name == "":
|
||||
@@ -438,8 +466,30 @@ class XiaoMusic:
|
||||
self.play_type = PLAY_TYPE_ALL
|
||||
await self.do_tts(f"已经设置为全部循环")
|
||||
|
||||
# 随机播放
|
||||
async def random_play(self, **kwargs):
|
||||
self.play_type = PLAY_TYPE_ALL
|
||||
await self.do_tts(f"已经设置为全部循环并随机播放")
|
||||
await self.play_next()
|
||||
|
||||
async def stop(self, **kwargs):
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info(f"定时器已取消")
|
||||
await self.stop_if_xiaoai_is_playing()
|
||||
|
||||
async def stop_after_minute(self, **kwargs):
|
||||
if self._stop_timer:
|
||||
self._stop_timer.cancel()
|
||||
self.log.info(f"关机定时器已取消")
|
||||
minute = int(kwargs["arg1"])
|
||||
|
||||
async def _do_stop():
|
||||
await asyncio.sleep(minute * 60)
|
||||
try:
|
||||
await self.stop()
|
||||
except Exception as e:
|
||||
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
|
||||
|
||||
self._stop_timer = asyncio.ensure_future(_do_stop())
|
||||
self.log.info(f"{minute}分钟后将关机")
|
||||
|
||||
Reference in New Issue
Block a user