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

Compare commits

...

27 Commits

Author SHA1 Message Date
涵曦
0ddbe58fbd bump: version 0.1.100 → 0.1.101 2024-07-07 08:47:33 +00:00
涵曦
350d82184f fix: #81 修复播放列表时,当前歌曲不在列表没有更换歌曲的问题 2024-07-07 08:43:55 +00:00
涵曦
5b8054abd9 fix: #110 修复配置加载问题 2024-07-07 08:28:09 +00:00
涵曦
c5c691b653 bump: version 0.1.99 → 0.1.100 2024-07-07 06:59:44 +00:00
涵曦
7a44c8587c fix: 日志代码写错 2024-07-07 06:41:45 +00:00
涵曦
5092ffc91a bump: version 0.1.98 → 0.1.99 2024-07-07 06:39:08 +00:00
涵曦
2da12e12d5 fix: #81 修复播放列表没有继续播放上次播放的歌曲,并把随机播放,全部循环,单曲循环状态落地 2024-07-07 06:21:15 +00:00
涵曦
5aff72dbb6 bump: version 0.1.97 → 0.1.98 2024-07-07 05:48:40 +00:00
涵曦
043f452e71 Update README.md 2024-07-07 13:47:30 +08:00
涵曦
5cedf8a907 fix: 修复多设备获取不到对话记录的问题 see #65 2024-07-07 04:46:33 +00:00
涵曦
ae77c7232e Update README.md 2024-07-07 11:42:05 +08:00
涵曦
d559413d46 fix: #93 修复目录深度设置后导致目录下的歌曲无法加到播放列表里的问题 2024-07-07 02:03:17 +00:00
涵曦
8b185d8768 bump: version 0.1.96 → 0.1.97 2024-07-06 16:05:28 +00:00
涵曦
202105a11f fix: 修复网页控制台设置页面保存报错 2024-07-06 16:04:07 +00:00
涵曦
7da80594e3 bump: version 0.1.95 → 0.1.96 2024-07-06 15:46:36 +00:00
涵曦
a032a1d50a feat: 使用commitizen管理版本号 2024-07-06 15:46:15 +00:00
涵曦
be62d8abc8 feat: 页面版本号链接到CHANGELOG页面 2024-07-06 15:06:51 +00:00
涵曦
aaf9f4b6a7 feat: 规范版本管理 2024-07-06 14:49:41 +00:00
涵曦
bb5d82097e new version v0.1.95 2024-07-06 11:25:57 +00:00
涵曦
d6ba656641 new version v0.1.94 2024-07-06 11:25:33 +00:00
涵曦
742cae0543 style: 调整调试日志输出 2024-07-06 11:19:50 +00:00
涵曦
a4ab1af160 fix: 新增参数配置强制打断小爱说话 2024-07-06 11:16:42 +00:00
涵曦
86f158532a fix: 修复多设备获取对话记录的问题 2024-07-06 11:00:47 +00:00
Zhang Tinmix
9ea7935cfb fix: 修复windows下路径分隔符被视为转移符导致音箱无法播放音乐的问题
由于Windows路径中使用反斜杠(\)作为目录分隔符,而在URL编码中反斜杠被视为特殊字符所导致windows下拼接得到的url不符合预期
示例
filename:music\outside.mp3
预期:
url: /music/outside.mp3
实际:
url:/music%5Coutside.mp3

需要一个办法来处理windows下的\\路径分隔符,我给出了一种简单的解法,作者可以考虑看看怎么处理会完全一点
2024-07-06 11:00:40 +00:00
涵曦
20c7e14076 fix: 修复播放链接报错 2024-07-06 03:50:47 +00:00
涵曦
e10f5b89b6 fix: 修复配置页面默认配置被置空的问题 2024-07-06 00:32:10 +00:00
涵曦
cb0bae5ae7 feat: 优化多设备接口执行效果,尽量做到同时执行 2024-07-05 17:06:13 +00:00
13 changed files with 790 additions and 220 deletions

8
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,8 @@
repos:
- hooks:
- id: commitizen
- id: commitizen-branch
stages:
- push
repo: https://github.com/commitizen-tools/commitizen
rev: v3.27.0

381
CHANGELOG.md Normal file
View File

@@ -0,0 +1,381 @@
## v0.1.101 (2024-07-07)
### Fix
- #81 修复播放列表时,当前歌曲不在列表没有更换歌曲的问题
- #110 修复配置加载问题
## v0.1.100 (2024-07-07)
### Fix
- 日志代码写错
## v0.1.99 (2024-07-07)
### Fix
- #81 修复播放列表没有继续播放上次播放的歌曲,并把随机播放,全部循环,单曲循环状态落地
## v0.1.98 (2024-07-07)
### Fix
- 修复多设备获取不到对话记录的问题 see #65
- #93 修复目录深度设置后导致目录下的歌曲无法加到播放列表里的问题
## v0.1.97 (2024-07-06)
### Fix
- 修复网页控制台设置页面保存报错
## v0.1.96 (2024-07-06)
### Feat
- 使用commitizen管理版本号
- 页面版本号链接到CHANGELOG页面
- 规范版本管理
## v0.1.95 (2024-07-06)
## v0.1.94 (2024-07-06)
### Feat
- 优化多设备接口执行效果,尽量做到同时执行
### Fix
- 新增参数配置强制打断小爱说话
- 修复多设备获取对话记录的问题
- 修复windows下路径分隔符被视为转移符导致音箱无法播放音乐的问题
- 修复播放链接报错
- 修复配置页面默认配置被置空的问题
## v0.1.93 (2024-07-05)
### Feat
- 访问账号密码默认为空
- 支持下载的目录与本地音乐目录分开 see #98
- 新增m4a文件格式支持
- 设置页面支持配置多设备
- 默认用空的后台账号和密码
- 支持多个设备同时播放 see #65
- 新增自定义口令功能 #105
### Fix
- 修复设置页面没成功初始化设置问题
- 修复镜像缺少文件问题
- 尝试解决插件路径问题
- 设置页面日志路径写错了
- 修复口令导致异常关闭的问题
## v0.1.92 (2024-07-04)
### Feat
- 启动参数新增 --port 配置监听端口
- 外网访问端口可独立配置
- 优化设置页面,新增更多配置项
- 首次保存设置后不需要重启容器
### Fix
- 日志文件配置的环境变量写错了
## v0.1.91 (2024-07-03)
### Fix
- 尝试解决触屏版不能播放的问题
## v0.1.90 (2024-07-02)
### Feat
- 优化触屏版播放页面显示歌曲
## v0.1.89 (2024-07-02)
### Feat
- 尝试解决触屏版无法播放的问题
### Fix
- 播放歌曲写成固定的了
- 播放歌曲时被其他指令打断后没有继续播放
## v0.1.88 (2024-07-02)
### Feat
- 日志里不要输出敏感信息
- 优化下载 ffmpeg 脚本,尝试解决 armv7 环境问题
- 优化日志输出信息
- 尝试解决触屏版无法播放的问题
### Fix
- 是否下载中判断错误导致播放无法自动重新开始播放
- 升级yt-dlp到2024.07.01
- 修复部分型号关机失败的问题
## v0.1.87 (2024-07-01)
### Fix
- 修复XIAOMUSIC_USE_MUSIC_API=true时播放不了的问题
## v0.1.86 (2024-07-01)
### Feat
- 优化 ffmpeg 安装脚本
- 新增调试工具用来调试 player_play_music 接口
- 升级依赖库 MiService
### Fix
- 尝试修复 armv7 的 ffmpeg 问题
- 尝试修复关机失败的问题
- 修复口令不能播放的问题
## v0.1.85 (2024-06-30)
### Feat
- 版本号链接到github的release页面方便查看版本更新日志
### Fix
- 修复电台删除后没有从电台列表中删除的问题
## v0.1.84 (2024-06-30)
### Feat
- config.json 支持更多配置选项
- 新增 XIAOMUSIC_STOP_TTS_MSG 配置关机提示音
## v0.1.83 (2024-06-30)
## v0.1.82 (2024-06-30)
### Feat
- 优化指令匹配规则
## v0.1.81 (2024-06-30)
## v0.1.80 (2024-06-30)
### Fix
- #91 修复下载歌曲报错
## v0.1.79 (2024-06-29)
## v0.1.77 (2024-06-29)
### Fix
- #52 支持配置模糊匹配本地歌曲
## v0.1.76 (2024-06-28)
## v0.1.75 (2024-06-28)
## v0.1.74 (2024-06-28)
## v0.1.73 (2024-06-28)
## v0.1.72 (2024-06-28)
## v0.1.71 (2024-06-28)
### Fix
- #83
## v0.1.70 (2024-06-27)
## v0.1.69 (2024-06-26)
## v0.1.67 (2024-06-26)
## v0.1.66 (2024-06-26)
## v0.1.65 (2024-06-26)
## v0.1.64 (2024-06-26)
## v0.1.62 (2024-06-25)
## v0.1.61 (2024-06-25)
## v0.1.60 (2024-06-25)
## v0.1.58 (2024-06-25)
### Fix
- 登陆失败不阻塞启动
## v0.1.57 (2024-06-24)
## v0.1.56 (2024-06-24)
## v0.1.55 (2024-06-23)
### Fix
- #47 支持配置基础的BaseAuth登录
## v0.1.54 (2024-06-23)
### Fix
- #76 新增XIAOMUSIC_MUSIC_PATH_DEPTH配置生成播放列表的目录深度默认10
- #74 配置目录可以和下载目录分开配置, 新增XIAOMUSIC_CONF_PATH用来设置配置目录不配置时使用下载目录
## v0.1.53 (2024-06-23)
## v0.1.52 (2024-06-21)
## v0.1.51 (2024-06-20)
## v0.1.49 (2024-06-20)
## v0.1.48 (2024-06-16)
## v0.1.47 (2024-06-16)
## v0.1.46 (2024-06-15)
## v0.1.45 (2024-06-15)
## v0.1.44 (2024-06-14)
## v0.1.43 (2024-06-14)
## v0.1.41 (2024-06-14)
## v0.1.40 (2024-06-12)
## v0.1.39 (2024-06-12)
## v0.1.38 (2024-06-12)
### Fix
- #70 下一首歌曲不存在时从播放列表中删除并继续找下一首
## v0.1.37 (2024-06-04)
## v0.1.36 (2024-05-30)
## v0.1.35 (2024-05-30)
### Fix
- #67 没配置did时也允许启动 http 服务
## v0.1.34 (2024-05-19)
## v0.1.33 (2024-05-19)
### Fix
- #50 新增配置页面
- #62
## v0.1.32 (2024-05-17)
## v0.1.31 (2024-05-16)
## v0.1.30 (2024-05-16)
### Fix
- 控制台显示版本号 #59
## v0.1.29 (2024-05-16)
### Fix
- #57 #55
## v0.1.28 (2024-05-16)
## v0.1.27 (2024-05-16)
## v0.1.26 (2024-05-08)
## v0.1.25 (2024-05-06)
## v0.1.24 (2024-04-30)
## v0.1.23 (2024-04-30)
## v0.1.22 (2024-04-30)
## v0.1.21 (2024-04-08)
## v0.1.20 (2024-04-08)
## v0.1.19 (2024-04-04)
## v0.1.18 (2024-02-24)
## v0.1.16 (2024-02-24)
## v0.1.15 (2024-02-03)
## v0.1.14 (2024-02-03)
## v0.1.13 (2024-02-02)
## v0.1.12 (2024-01-30)
### Fix
- set volume failed
## v0.1.11 (2024-01-29)
## v0.1.10 (2024-01-29)
## v0.1.9 (2024-01-28)
### Fix
- arg1 漏修改
## v0.1.8 (2024-01-28)
### Fix
- http server listen host
## v0.1.7 (2024-01-28)
## v0.1.6 (2024-01-28)
## v0.1.5 (2024-01-27)
## v0.1.4 (2024-01-27)
### Fix
- error when play next
## v0.1.3 (2023-10-15)
## v0.1.2 (2023-10-15)
## v0.1.1 (2023-10-14)

View File

@@ -39,9 +39,9 @@ docker run -p 8090:8090 \
hanxi/xiaomusic hanxi/xiaomusic
``` ```
启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。 启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
### ✨ 修改8090端口 ### ✨✨✨ 修改默认8090端口映射 ✨✨✨
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19> 如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>

View File

@@ -1,37 +1,5 @@
#!/bin/bash #!/bin/bash
set -e cz bump --check-consistency --increment patch
version_file=./pyproject.toml
init_file=./xiaomusic/__init__.py
# 获取当前版本号
current_version=$(grep -oE "version = \"[0-9]+\.[0-9]+\.[0-9]+\"" $version_file | cut -d'"' -f2)
echo "当前版本号: "$current_version
# 将版本号分割成三部分
major=$(echo $current_version | cut -d'.' -f1)
minor=$(echo $current_version | cut -d'.' -f2)
patch=$(echo $current_version | cut -d'.' -f3)
echo "major: $major"
echo "minor: $minor"
echo "patch: $patch"
# 将补丁号加1
patch=$((patch + 1))
# 生成新版本号
new_version="$major.$minor.$patch"
# 将新版本号写入文件
sed -i "s/version.*/version = \"$new_version\"/g" $version_file
sed -i "s/__version__.*/__version__ = \"$new_version\"/g" $init_file
echo "新版本号:$new_version"
git diff
git add $version_file
git add $init_file
git commit -m "new version v$new_version"
git tag v$new_version
git push -u origin main --tags git push -u origin main --tags

167
pdm.lock generated
View File

@@ -2,10 +2,10 @@
# It is not intended for manual editing. # It is not intended for manual editing.
[metadata] [metadata]
groups = ["default", "lint"] groups = ["default", "lint", "dev"]
strategy = ["cross_platform"] strategy = ["cross_platform"]
lock_version = "4.4.1" lock_version = "4.4.1"
content_hash = "sha256:ac53cf6421de7aded8475907adc40a716a3e5c6429c614b93e5cfbddea36d048" content_hash = "sha256:3631f504ea2c9e450ff20fe555e2ec0143bc315c22ff257e17a992d1e6d3c39d"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@@ -82,6 +82,16 @@ files = [
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
] ]
[[package]]
name = "argcomplete"
version = "3.3.0"
requires_python = ">=3.8"
summary = "Bash tab completion for argparse"
files = [
{file = "argcomplete-3.3.0-py3-none-any.whl", hash = "sha256:c168c3723482c031df3c207d4ba8fa702717ccb9fc0bfe4117166c1f537b4a54"},
{file = "argcomplete-3.3.0.tar.gz", hash = "sha256:fd03ff4a5b9e6580569d34b273f741e85cd9e072f3feeeee3eba4891c70eda62"},
]
[[package]] [[package]]
name = "asgiref" name = "asgiref"
version = "3.7.2" version = "3.7.2"
@@ -339,6 +349,39 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "commitizen"
version = "3.27.0"
requires_python = ">=3.8"
summary = "Python commitizen client tool"
dependencies = [
"argcomplete<3.4,>=1.12.1",
"charset-normalizer<4,>=2.1.0",
"colorama<0.5.0,>=0.4.1",
"decli<0.7.0,>=0.6.0",
"importlib-metadata<8,>=4.13",
"jinja2>=2.10.3",
"packaging>=19",
"pyyaml>=3.08",
"questionary<3.0,>=2.0",
"termcolor<3,>=1.1",
"tomlkit<1.0.0,>=0.5.3",
]
files = [
{file = "commitizen-3.27.0-py3-none-any.whl", hash = "sha256:11948fa563d5ad5464baf09eaacff3cf8cbade1ca029ed9c4978f2227f033130"},
{file = "commitizen-3.27.0.tar.gz", hash = "sha256:5874d0c7e8e1be3b75b1b0a2269cffe3dd5c843b860d84b0bdbb9ea86e3474b8"},
]
[[package]]
name = "decli"
version = "0.6.2"
requires_python = ">=3.7"
summary = "Minimal, easy-to-use, declarative cli tool"
files = [
{file = "decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed"},
{file = "decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f"},
]
[[package]] [[package]]
name = "flask" name = "flask"
version = "3.0.3" version = "3.0.3"
@@ -432,6 +475,19 @@ files = [
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
] ]
[[package]]
name = "importlib-metadata"
version = "7.2.1"
requires_python = ">=3.8"
summary = "Read metadata from Python packages"
dependencies = [
"zipp>=0.5",
]
files = [
{file = "importlib_metadata-7.2.1-py3-none-any.whl", hash = "sha256:ffef94b0b66046dd8ea2d619b701fe978d9264d38f3998bc4c27ec3b146a87c8"},
{file = "importlib_metadata-7.2.1.tar.gz", hash = "sha256:509ecb2ab77071db5137c655e24ceb3eee66e7bbc6574165d0d114d9fc4bbe68"},
]
[[package]] [[package]]
name = "itsdangerous" name = "itsdangerous"
version = "2.1.2" version = "2.1.2"
@@ -581,6 +637,29 @@ files = [
{file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"},
] ]
[[package]]
name = "packaging"
version = "24.1"
requires_python = ">=3.8"
summary = "Core utilities for Python packages"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "prompt-toolkit"
version = "3.0.36"
requires_python = ">=3.6.2"
summary = "Library for building powerful interactive command lines in Python"
dependencies = [
"wcwidth",
]
files = [
{file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"},
{file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"},
]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.21" version = "2.21"
@@ -630,6 +709,51 @@ files = [
{file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
] ]
[[package]]
name = "pyyaml"
version = "6.0.1"
requires_python = ">=3.6"
summary = "YAML parser and emitter for Python"
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "questionary"
version = "2.0.1"
requires_python = ">=3.8"
summary = "Python library to build pretty command line user prompts ⭐️"
dependencies = [
"prompt-toolkit<=3.0.36,>=2.0",
]
files = [
{file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"},
{file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"},
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"
@@ -686,6 +810,26 @@ files = [
{file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"}, {file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"},
] ]
[[package]]
name = "termcolor"
version = "2.4.0"
requires_python = ">=3.8"
summary = "ANSI color formatting for output in terminal"
files = [
{file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"},
{file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"},
]
[[package]]
name = "tomlkit"
version = "0.12.5"
requires_python = ">=3.7"
summary = "Style preserving TOML library"
files = [
{file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"},
{file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"},
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.9.0" version = "4.9.0"
@@ -716,6 +860,15 @@ files = [
{file = "waitress-3.0.0.tar.gz", hash = "sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1"}, {file = "waitress-3.0.0.tar.gz", hash = "sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1"},
] ]
[[package]]
name = "wcwidth"
version = "0.2.13"
summary = "Measures the displayed width of unicode strings in a terminal"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "12.0" version = "12.0"
@@ -849,3 +1002,13 @@ files = [
{file = "yt_dlp-2024.7.1.232715.dev0-py3-none-any.whl", hash = "sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503"}, {file = "yt_dlp-2024.7.1.232715.dev0-py3-none-any.whl", hash = "sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503"},
{file = "yt_dlp-2024.7.1.232715.dev0.tar.gz", hash = "sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8"}, {file = "yt_dlp-2024.7.1.232715.dev0.tar.gz", hash = "sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8"},
] ]
[[package]]
name = "zipp"
version = "3.19.2"
requires_python = ">=3.8"
summary = "Backport of pathlib-compatible object wrapper for zip files"
files = [
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
]

View File

@@ -1,9 +1,9 @@
[project] [project]
name = "xiaomusic" name = "xiaomusic"
version = "0.1.93" version = "0.1.101"
description = "Play Music with xiaomi AI speaker" description = "Play Music with xiaomi AI speaker"
authors = [ authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"}, {name = "涵曦", email = "im.hanxi@gmail.com"},
] ]
dependencies = [ dependencies = [
"aiohttp>=3.8.6", "aiohttp>=3.8.6",
@@ -33,19 +33,22 @@ build-backend = "pdm.backend"
lint = [ lint = [
"ruff>=0.4.8", "ruff>=0.4.8",
] ]
dev = [
"commitizen>=3.27.0",
]
[tool.ruff] [tool.ruff]
lint.select = [ lint.select = [
"B", # flake8-bugbear "B", # flake8-bugbear
"C4", # flake8-comprehensions "C4", # flake8-comprehensions
"E", # pycodestyle - Error "E", # pycodestyle - Error
"F", # Pyflakes "F", # Pyflakes
"I", # isort "I", # isort
"W", # pycodestyle - Warning "W", # pycodestyle - Warning
"UP", # pyupgrade "UP", # pyupgrade
] ]
lint.ignore = [ lint.ignore = [
"E501", # line-too-long "E501", # line-too-long
"W191", # tab-indentation "W191", # tab-indentation
] ]
include = ["**/*.py", "**/*.pyi", "**/pyproject.toml"] include = ["**/*.py", "**/*.pyi", "**/pyproject.toml"]
@@ -55,3 +58,14 @@ convention = "google"
[tool.pdm.scripts] [tool.pdm.scripts]
lint = "ruff check ." lint = "ruff check ."
fmt = "ruff format ." fmt = "ruff format ."
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "v$version"
version_scheme = "pep440"
version_provider = "pep621"
update_changelog_on_bump = true
major_version_zero = true
version_files = [
"xiaomusic/__init__.py",
]

View File

@@ -1 +1 @@
__version__ = "0.1.93" __version__ = "0.1.101"

View File

@@ -73,7 +73,7 @@ class Config:
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5") hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口 port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
public_port: int = int(os.getenv("XIAOMUSIC_PUBLIC_PORT", 0)) # 歌曲访问端口 public_port: int = int(os.getenv("XIAOMUSIC_PUBLIC_PORT", 0)) # 歌曲访问端口
proxy: str | None = os.getenv("XIAOMUSIC_PROXY", None) proxy: str = os.getenv("XIAOMUSIC_PROXY", None)
search_prefix: str = os.getenv( search_prefix: str = os.getenv(
"XIAOMUSIC_SEARCH", "bilisearch:" "XIAOMUSIC_SEARCH", "bilisearch:"
) # "bilisearch:" or "ytsearch:" ) # "bilisearch:" or "ytsearch:"
@@ -120,6 +120,10 @@ class Config:
user_key_word_dict: dict[str, str] = field( user_key_word_dict: dict[str, str] = field(
default_factory=default_user_key_word_dict default_factory=default_user_key_word_dict
) )
enable_force_stop: bool = (
os.getenv("XIAOMUSIC_ENABLE_FORCE_STOP", "false").lower() == "true"
)
play_type: int = int(os.getenv("XIAOMUSIC_PLAY_TYPE", "2"))
def append_keyword(self, keys, action): def append_keyword(self, keys, action):
for key in keys.split(","): for key in keys.split(","):
@@ -158,43 +162,39 @@ class Config:
config[key] = value config[key] = value
return cls(**config) return cls(**config)
@classmethod
def convert_value(cls, k, v, type_hints):
if v is not None and k in type_hints:
expected_type = type_hints[k]
try:
if expected_type is bool:
converted_value = False
if str(v).lower() == "true":
converted_value = True
else:
converted_value = expected_type(v)
return converted_value
except (ValueError, TypeError) as e:
print(f"Error converting {k}:{v} to {expected_type}: {e}")
return None
@classmethod @classmethod
def read_from_file(cls, config_path: str) -> dict: def read_from_file(cls, config_path: str) -> dict:
result = {} result = {}
with open(config_path, "rb") as f: with open(config_path, "rb") as f:
config = json.load(f) data = json.load(f)
for key, value in config.items(): type_hints = get_type_hints(cls)
if value is not None and key in cls.__dataclass_fields__:
result[key] = value for k, v in data.items():
converted_value = cls.convert_value(k, v, type_hints)
if converted_value is not None:
result[k] = converted_value
return result return result
def update_config(self, data): def update_config(self, data):
# 获取类型提示
type_hints = get_type_hints(self) type_hints = get_type_hints(self)
for k, v in data.items(): for k, v in data.items():
if v and k in type_hints: converted_value = self.convert_value(k, v, type_hints)
# 获取字段的类型 if converted_value is not None:
expected_type = type_hints[k] setattr(self, k, converted_value)
# 根据期望的类型进行转换
if isinstance(v, expected_type):
# 如果v已经是正确的类型则直接赋值
setattr(self, k, v)
else:
# 尝试转换类型
try:
# 特殊情况处理(例如对布尔值的转换)
if expected_type is bool:
converted_value = False
if v and v.lower() == "true":
converted_value = True
else:
# 使用期望类型的构造函数进行转换
converted_value = expected_type(v)
except (ValueError, TypeError) as e:
print(f"Error converting {v} to {expected_type}: {e}")
continue
# 设置转换后的值
setattr(self, k, converted_value)

View File

@@ -9,7 +9,7 @@
</head> </head>
<body> <body>
<h2>小爱音箱操控面板 <h2>小爱音箱操控面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/releases"> (<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
版本未知 版本未知
</a>) </a>)
</h2> </h2>

View File

@@ -17,7 +17,7 @@ var vConsole = new window.VConsole();
</head> </head>
<body> <body>
<h2>小爱音箱设置面板 <h2>小爱音箱设置面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/releases"> (<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
版本未知 版本未知
</a>) </a>)
</h2> </h2>
@@ -50,7 +50,7 @@ var vConsole = new window.VConsole();
<input id="music_path" type="text" value="music"></input> <input id="music_path" type="text" value="music"></input>
<label for="download_path">音乐下载目录(必须是music的子目录):</label> <label for="download_path">音乐下载目录(必须是music的子目录):</label>
<input id="download_path" type="text"></input> <input id="download_path" type="text" value='music/download'></input>
<label for="conf_path">配置文件目录:</label> <label for="conf_path">配置文件目录:</label>
<input id="conf_path" type="text"></input> <input id="conf_path" type="text"></input>

View File

@@ -78,7 +78,7 @@ $(function(){
for (const key in data) { for (const key in data) {
if (data.hasOwnProperty(key)) { if (data.hasOwnProperty(key)) {
const $element = $("#" + key); const $element = $("#" + key);
if ($element.length) { if ($element.length && data[key] !== '') {
if (data[key] === true) { if (data[key] === true) {
$element.val('true'); $element.val('true');
} else if (data[key] === false) { } else if (data[key] === false) {

View File

@@ -101,63 +101,48 @@ def custom_sort_key(s):
return (2, s) return (2, s)
# fork from https://gist.github.com/dougthor42/001355248518bc64d2f8 def _get_depth_path(root, directory, depth):
def walk_to_depth(root, depth=None, *args, **kwargs): # 计算当前目录的深度
""" relative_path = root[len(directory) :].strip(os.sep)
Wrapper around os.walk that stops after going down `depth` folders. path_parts = relative_path.split(os.sep)
I had my own version, but it wasn't as efficient as if len(path_parts) >= depth:
http://stackoverflow.com/a/234329/1354930, so I modified to be more return os.path.join(directory, *path_parts[:depth])
similar to nosklo's answer. else:
However, nosklo's answer doesn't work if topdown=False, so I kept my return root
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): def _append_files_result(result, root, joinpath, files, support_extension):
"""Faster because it skips traversing dirs that are too deep.""" dir_name = os.path.basename(root)
root_depth = root.count(os.path.sep) if dir_name not in result:
for dirpath, dirnames, filenames in os.walk(root, *args, **kwargs): result[dir_name] = []
yield (dirpath, dirnames, filenames) for file in files:
# 过滤隐藏文件
if file.startswith("."):
continue
# 过滤文件后缀
(name, extension) = os.path.splitext(file)
if extension.lower() not in support_extension:
continue
# calculate how far down we are. result[dir_name].append(os.path.join(joinpath, file))
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... def traverse_music_directory(
try: directory, depth=10, exclude_dirs=None, support_extension=None
if args[0] is False: ):
yield from fallback_func(root, depth, *args, **kwargs) result = {}
return for root, dirs, files in os.walk(directory):
# 忽略排除的目录
dirs[:] = [d for d in dirs if d not in exclude_dirs]
# 计算当前目录的深度
current_depth = root[len(directory) :].count(os.sep) + 1
if current_depth > depth:
depth_path = _get_depth_path(root, directory, depth - 1)
_append_files_result(result, depth_path, root, files, support_extension)
else: else:
yield from main_func(root, depth, *args, **kwargs) _append_files_result(result, root, root, files, support_extension)
return return result
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): def downloadfile(url):

View File

@@ -9,6 +9,7 @@ import re
import time import time
import traceback import traceback
import urllib.parse import urllib.parse
from dataclasses import asdict
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from pathlib import Path from pathlib import Path
@@ -37,7 +38,7 @@ from xiaomusic.utils import (
get_local_music_duration, get_local_music_duration,
get_web_music_duration, get_web_music_duration,
parse_cookie_string, parse_cookie_string,
walk_to_depth, traverse_music_directory,
) )
EOF = object() EOF = object()
@@ -52,7 +53,7 @@ class XiaoMusic:
self.config = config self.config = config
self.mi_token_home = Path.home() / ".mi.token" self.mi_token_home = Path.home() / ".mi.token"
self.last_timestamp = int(time.time() * 1000) # timestamp last call mi speaker self.last_timestamp = {} # timestamp last call mi speaker
self.last_record = None self.last_record = None
self.cookie_jar = None self.cookie_jar = None
self.mina_service = None self.mina_service = None
@@ -64,8 +65,6 @@ class XiaoMusic:
# 下载对象 # 下载对象
self.download_proc = None self.download_proc = None
# 单曲循环,全部循环
self.play_type = PLAY_TYPE_RND
self.cur_music = "" self.cur_music = ""
self._next_timer = None self._next_timer = None
self._timeout = 0 self._timeout = 0
@@ -102,6 +101,8 @@ class XiaoMusic:
self.log.info(f"Startup OK. {debug_config}") self.log.info(f"Startup OK. {debug_config}")
def init_config(self): def init_config(self):
# 单曲循环,全部循环
self.play_type = self.config.play_type
self.music_path = self.config.music_path self.music_path = self.config.music_path
self.conf_path = self.config.conf_path self.conf_path = self.config.conf_path
if not self.conf_path: if not self.conf_path:
@@ -153,18 +154,23 @@ class XiaoMusic:
async with ClientSession() as session: async with ClientSession() as session:
while True: while True:
self.log.debug( self.log.debug(
"Listening new message, timestamp: %s", self.last_timestamp f"Listening new message, timestamp: {self.last_timestamp}"
) )
session._cookie_jar = self.cookie_jar session._cookie_jar = self.cookie_jar
# 拉取所有音箱的对话记录 # 拉取所有音箱的对话记录
for device_id in self.device2hardware: tasks = [
await self.get_latest_ask_from_xiaoai(session, device_id) self.get_latest_ask_from_xiaoai(session, device_id)
for device_id in self.device2hardware
]
await asyncio.gather(*tasks)
start = time.perf_counter() start = time.perf_counter()
self.log.debug("Polling_event, timestamp: %s", self.last_timestamp) self.log.debug(f"Polling_event, timestamp: {self.last_timestamp}")
await self.polling_event.wait() await self.polling_event.wait()
if (d := time.perf_counter() - start) < 1: if (d := time.perf_counter() - start) < 1:
# sleep to avoid too many request # sleep to avoid too many request
self.log.debug("Sleep %f, timestamp: %s", d, self.last_timestamp) self.log.debug(f"Sleep {d}, timestamp: {self.last_timestamp}")
await asyncio.sleep(1 - d) await asyncio.sleep(1 - d)
async def init_all_data(self, session): async def init_all_data(self, session):
@@ -193,12 +199,12 @@ class XiaoMusic:
self.device2hardware = {} self.device2hardware = {}
self.did2device = {} self.did2device = {}
for h in hardware_data: for h in hardware_data:
device = h.get("deviceID", "") device_id = h.get("deviceID", "")
hardware = h.get("hardware", "") hardware = h.get("hardware", "")
did = h.get("miotDID", "") did = h.get("miotDID", "")
if device and hardware and did and (did in mi_dids): if device_id and hardware and did and (did in mi_dids):
self.device2hardware[device] = hardware self.device2hardware[device_id] = hardware
self.did2device[did] = device self.did2device[did] = device_id
except Exception as e: except Exception as e:
self.log.error(f"Execption {e}") self.log.error(f"Execption {e}")
@@ -231,8 +237,9 @@ class XiaoMusic:
for i in range(retries): for i in range(retries):
try: try:
timeout = ClientTimeout(total=15) timeout = ClientTimeout(total=15)
hardware = self.device2hardware[device_id]
url = LATEST_ASK_API.format( url = LATEST_ASK_API.format(
hardware=self.config.hardware, hardware=hardware,
timestamp=str(int(time.time() * 1000)), timestamp=str(int(time.time() * 1000)),
) )
self.log.debug(f"url:{url}") self.log.debug(f"url:{url}")
@@ -251,18 +258,21 @@ class XiaoMusic:
self.log.info("Maybe outof date trying to re init it") self.log.info("Maybe outof date trying to re init it")
await self.init_all_data(self.session) await self.init_all_data(self.session)
else: else:
return self._get_last_query(data) return self._get_last_query(device_id, data)
def _get_last_query(self, data): def _get_last_query(self, device_id, data):
self.log.debug(f"_get_last_query:{data}") self.log.debug(f"_get_last_query device_id:{device_id} data:{data}")
if d := data.get("data"): if d := data.get("data"):
records = json.loads(d).get("records") records = json.loads(d).get("records")
if not records: if not records:
return return
last_record = records[0] last_record = records[0]
timestamp = last_record.get("time") timestamp = last_record.get("time")
if timestamp > self.last_timestamp: # 首次用当前时间初始化
self.last_timestamp = timestamp if device_id not in self.last_timestamp:
self.last_timestamp[device_id] = int(time.time() * 1000)
if timestamp > self.last_timestamp[device_id]:
self.last_timestamp[device_id] = timestamp
self.last_record = last_record self.last_record = last_record
self.new_record_event.set() self.new_record_event.set()
@@ -289,13 +299,19 @@ class XiaoMusic:
self.log.info(f"do_tts ok. cur_music:{self.cur_music}") self.log.info(f"do_tts ok. cur_music:{self.cur_music}")
await self.check_replay() await self.check_replay()
async def text_to_speech(self, value): async def text_to_speech_one(self, device_id, value):
try: try:
for device_id in self.device2hardware: await self.mina_service.text_to_speech(device_id, value)
await self.mina_service.text_to_speech(device_id, value)
except Exception as e: except Exception as e:
self.log.error(f"Execption {e}") self.log.error(f"Execption {e}")
async def text_to_speech(self, value):
tasks = [
self.text_to_speech_one(device_id, value)
for device_id in self.device2hardware
]
await asyncio.gather(*tasks)
# 继续播放被打断的歌曲 # 继续播放被打断的歌曲
async def check_replay(self): async def check_replay(self):
if self.isplaying() and not self.isdownloading(): if self.isplaying() and not self.isdownloading():
@@ -332,24 +348,29 @@ class XiaoMusic:
async def stop_if_xiaoai_is_playing(self, device_id): async def stop_if_xiaoai_is_playing(self, device_id):
is_playing = await self.get_if_xiaoai_is_playing(device_id) is_playing = await self.get_if_xiaoai_is_playing(device_id)
if is_playing: if is_playing or self.config.enable_force_stop:
# stop it # stop it
ret = await self.mina_service.player_stop(device_id) ret = await self.mina_service.player_stop(device_id)
self.log.info( self.log.info(
f"force_stop_xiaoai player_stop device_id:{device_id} ret:{ret}" f"stop_if_xiaoai_is_playing player_stop device_id:{device_id} enable_force_stop:{self.config.enable_force_stop} ret:{ret}"
) )
async def force_stop_xiaoai(self): async def force_stop_one_xiaoai(self, device_id):
try: try:
for device_id in self.device2hardware: ret = await self.mina_service.player_pause(device_id)
ret = await self.mina_service.player_pause(device_id) self.log.info(
self.log.info( f"force_stop_one_xiaoai player_pause device_id:{device_id} ret:{ret}"
f"force_stop_xiaoai player_pause device_id:{device_id} ret:{ret}" )
) await self.stop_if_xiaoai_is_playing(device_id)
await self.stop_if_xiaoai_is_playing(device_id)
except Exception as e: except Exception as e:
self.log.error(f"Execption {e}") self.log.error(f"Execption {e}")
async def force_stop_xiaoai(self):
tasks = [
self.force_stop_one_xiaoai(device_id) for device_id in self.device2hardware
]
await asyncio.gather(*tasks)
# 是否在下载中 # 是否在下载中
def isdownloading(self): def isdownloading(self):
if not self.download_proc: if not self.download_proc:
@@ -455,46 +476,41 @@ class XiaoMusic:
self.log.debug("get_music_url web music. name:%s, url:%s", name, url) self.log.debug("get_music_url web music. name:%s, url:%s", name, url)
return url return url
filename = self.get_filename(name) filename = self.get_filename(name).replace("\\", "/")
self.log.debug( self.log.debug(
"get_music_url local music. name:%s, filename:%s", name, filename "get_music_url local music. name:%s, filename:%s", name, filename
) )
encoded_name = urllib.parse.quote(filename) encoded_name = urllib.parse.quote(filename)
return f"http://{self.hostname}:{self.public_port}/{encoded_name}" return f"http://{self.hostname}:{self.public_port}/{encoded_name}"
# 递归获取目录下所有歌曲,生成随机播放列表 # 获取目录下所有歌曲,生成随机播放列表
def _gen_all_music_list(self): def _gen_all_music_list(self):
self._all_music = {} self._all_music = {}
all_music_by_dir = {} all_music_by_dir = {}
for root, dirs, filenames in walk_to_depth( local_musics = traverse_music_directory(
self.music_path, depth=self.music_path_depth self.music_path,
): depth=self.music_path_depth,
dirs[:] = [d for d in dirs if d not in self.exclude_dirs] exclude_dirs=self.exclude_dirs,
self.log.debug("root:%s dirs:%s music_path:%s", root, dirs, self.music_path) support_extension=SUPPORT_MUSIC_TYPE,
dir_name = os.path.basename(root) )
if self.music_path == root: for dir_name, files in local_musics.items():
if len(files) == 0:
continue
if dir_name == os.path.basename(self.music_path):
dir_name = "其他" dir_name = "其他"
if self.music_path != self.download_path and dir_name == os.path.basename(
self.download_path
):
dir_name = "下载"
if dir_name not in all_music_by_dir: if dir_name not in all_music_by_dir:
all_music_by_dir[dir_name] = {} all_music_by_dir[dir_name] = {}
for filename in filenames: for file in files:
self.log.debug("gen_all_music_list. filename:%s", filename)
# 过滤隐藏文件
if filename.startswith("."):
continue
# 过滤非音乐文件
(name, extension) = os.path.splitext(filename)
self.log.debug(
"gen_all_music_list. filename:%s, name:%s, extension:%s",
filename,
name,
extension,
)
if extension.lower() not in SUPPORT_MUSIC_TYPE:
continue
# 歌曲名字相同会覆盖 # 歌曲名字相同会覆盖
self._all_music[name] = os.path.join(root, filename) filename = os.path.basename(file)
(name, _) = os.path.splitext(filename)
self._all_music[name] = file
all_music_by_dir[dir_name][name] = True all_music_by_dir[dir_name][name] = True
self._play_list = list(self._all_music.keys()) self._play_list = list(self._all_music.keys())
self._cur_play_list = "全部" self._cur_play_list = "全部"
self._gen_play_list() self._gen_play_list()
@@ -698,7 +714,7 @@ class XiaoMusic:
opvalue = self.config.key_word_dict.get(opkey) opvalue = self.config.key_word_dict.get(opkey)
if not ctrl_panel and not self.isplaying(): if not ctrl_panel and not self.isplaying():
if self.active_cmd and opvalue not in self.active_cmd: if self.active_cmd and opvalue not in self.active_cmd:
self.log.ifno(f"不在激活命令中 {opvalue}") self.log.info(f"不在激活命令中 {opvalue}")
continue continue
self.log.info(f"匹配到指令. opkey:{opkey} opvalue:{opvalue} oparg:{oparg}") self.log.info(f"匹配到指令. opkey:{opkey} opvalue:{opvalue} oparg:{oparg}")
return (opvalue, oparg) return (opvalue, oparg)
@@ -707,6 +723,10 @@ class XiaoMusic:
# 判断是否播放下一首歌曲 # 判断是否播放下一首歌曲
def check_play_next(self): def check_play_next(self):
# 当前歌曲不在当前播放列表
if self.cur_music not in self._play_list:
return True
# 当前没我在播放的歌曲 # 当前没我在播放的歌曲
if self.cur_music == "": if self.cur_music == "":
return True return True
@@ -718,23 +738,31 @@ class XiaoMusic:
async def play_url(self, **kwargs): async def play_url(self, **kwargs):
url = kwargs.get("arg1", "") url = kwargs.get("arg1", "")
await self.all_player_play(url) return await self.all_player_play(url)
async def all_player_play(self, url): async def play_one_url(self, device_id, url):
try: try:
for device_id in self.device2hardware: if self.config.use_music_api:
if self.config.use_music_api: ret = await self.play_by_music_url(device_id, url)
ret = await self.play_by_music_url(device_id, url) self.log.info(
self.log.info( f"play_one_url play_by_music_url device_id:{device_id} ret:{ret} url:{url}"
f"player_play play_by_music_url device_id:{device_id} ret:{ret} url:{url}" )
) else:
else: ret = await self.mina_service.play_by_url(device_id, url)
ret = await self.mina_service.play_by_url(device_id, url) self.log.info(
self.log.info( f"play_one_url play_by_url device_id:{device_id} ret:{ret} url:{url}"
f"player_play play_by_url device_id:{device_id} ret:{ret} url:{url}" )
)
except Exception as e: except Exception as e:
self.log.error(f"Execption {e}") self.log.error(f"Execption {e}")
return ret
async def all_player_play(self, url):
tasks = [
self.play_one_url(device_id, url) for device_id in self.device2hardware
]
results = await asyncio.gather(*tasks)
self.log.info(f"all_player_play {url} {results}")
return results
def find_real_music_name(self, name): def find_real_music_name(self, name):
if not self.config.enable_fuzzy_match: if not self.config.enable_fuzzy_match:
@@ -814,7 +842,7 @@ class XiaoMusic:
# 下一首 # 下一首
async def play_next(self, **kwargs): async def play_next(self, **kwargs):
self.log.info("下一首") self.log.info("开始播放下一首")
name = self.cur_music name = self.cur_music
self.log.debug("play_next. name:%s, cur_music:%s", name, self.cur_music) self.log.debug("play_next. name:%s, cur_music:%s", name, self.cur_music)
if ( if (
@@ -831,18 +859,21 @@ class XiaoMusic:
# 单曲循环 # 单曲循环
async def set_play_type_one(self, **kwargs): async def set_play_type_one(self, **kwargs):
self.play_type = PLAY_TYPE_ONE self.play_type = PLAY_TYPE_ONE
self.save_play_type()
await self.do_tts("已经设置为单曲循环") await self.do_tts("已经设置为单曲循环")
# 全部循环 # 全部循环
async def set_play_type_all(self, **kwargs): async def set_play_type_all(self, **kwargs):
self.play_type = PLAY_TYPE_ALL self.play_type = PLAY_TYPE_ALL
self._gen_play_list() self._gen_play_list()
self.save_play_type()
await self.do_tts("已经设置为全部循环") await self.do_tts("已经设置为全部循环")
# 随机播放 # 随机播放
async def random_play(self, **kwargs): async def random_play(self, **kwargs):
self.play_type = PLAY_TYPE_RND self.play_type = PLAY_TYPE_RND
self._gen_play_list() self._gen_play_list()
self.save_play_type()
await self.do_tts("已经设置为随机播放") await self.do_tts("已经设置为随机播放")
# 刷新列表 # 刷新列表
@@ -874,7 +905,8 @@ class XiaoMusic:
if real_name: if real_name:
self.log.info(f"根据【{list_name}】找到播放列表【{real_name}") self.log.info(f"根据【{list_name}】找到播放列表【{real_name}")
list_name = real_name list_name = real_name
self.log.info(f"没找到播放列表【{list_name}") else:
self.log.info(f"没找到播放列表【{list_name}")
return list_name return list_name
# 播放一个播放列表 # 播放一个播放列表
@@ -894,8 +926,6 @@ class XiaoMusic:
music_name = "" music_name = ""
if len(parts) > 1: if len(parts) > 1:
music_name = parts[1] music_name = parts[1]
else:
music_name = self.get_next_music()
await self.play(arg1=music_name) await self.play(arg1=music_name)
async def stop(self, **kwargs): async def stop(self, **kwargs):
@@ -991,12 +1021,30 @@ class XiaoMusic:
# 保存配置并重新启动 # 保存配置并重新启动
async def saveconfig(self, data): async def saveconfig(self, data):
# 配置文件落地
self.do_saveconfig(data)
# 更新配置
self.update_config_from_setting(data)
# 重新初始化
await self.call_main_thread_function(self.reinit)
# 配置文件落地
def do_saveconfig(self, data):
# 默认暂时配置保存到 music 目录下 # 默认暂时配置保存到 music 目录下
filename = self.getsettingfile() filename = self.getsettingfile()
with open(filename, "w", encoding="utf-8") as f: with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4) json.dump(data, f, ensure_ascii=False, indent=4)
self.update_config_from_setting(data)
await self.call_main_thread_function(self.reinit) # 把当前配置落地
def save_cur_config(self):
data = asdict(self.config)
self.do_saveconfig(data)
self.log.info("save_cur_config ok")
# 播放类型落地
def save_play_type(self):
self.config.play_type = self.play_type
self.save_cur_config()
def update_config_from_setting(self, data): def update_config_from_setting(self, data):
# 兼容旧配置:一段时间后清理这里的旧代码 # 兼容旧配置:一段时间后清理这里的旧代码
@@ -1024,14 +1072,17 @@ class XiaoMusic:
async def getalldevices(self, **kwargs): async def getalldevices(self, **kwargs):
did_list = [] did_list = []
hardware_list = [] hardware_list = []
hardware_data = await self.mina_service.device_list() try:
for h in hardware_data: hardware_data = await self.mina_service.device_list()
did = h.get("miotDID", "") for h in hardware_data:
if did != "": did = h.get("miotDID", "")
did_list.append(did) if did != "":
hardware = h.get("hardware", "") did_list.append(did)
if h.get("hardware", "") != "": hardware = h.get("hardware", "")
hardware_list.append(hardware) if h.get("hardware", "") != "":
hardware_list.append(hardware)
except Exception as e:
self.log.error(f"Execption {e}")
alldevices = { alldevices = {
"did_list": did_list, "did_list": did_list,
"hardware_list": hardware_list, "hardware_list": hardware_list,