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

Compare commits

...

156 Commits

Author SHA1 Message Date
涵曦
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
涵曦
6a583119d0 new version v0.1.93 2024-07-05 15:28:51 +00:00
涵曦
9349070e8b Update README.md 2024-07-05 23:21:43 +08:00
涵曦
01d99dc699 feat: 访问账号密码默认为空 2024-07-05 15:15:31 +00:00
涵曦
f4d9a6c1fd feat: 支持下载的目录与本地音乐目录分开 see #98 2024-07-05 15:12:36 +00:00
涵曦
1919bc84d9 feat: 新增m4a文件格式支持 2024-07-05 14:55:47 +00:00
涵曦
4c1761468f fix: 修复设置页面没成功初始化设置问题 2024-07-05 14:52:14 +00:00
涵曦
c75230a67d Update ci.yml 2024-07-05 22:15:14 +08:00
涵曦
ee7ffa55cb fix: 修复镜像缺少文件问题 2024-07-05 13:45:44 +00:00
涵曦
45bbc8af42 Update ci.yml 2024-07-05 21:22:36 +08:00
涵曦
ab8bf8fa62 fix: 尝试解决插件路径问题 2024-07-05 13:12:03 +00:00
涵曦
493cad080e Update README.md 2024-07-05 18:43:06 +08:00
涵曦
eaa159c5cb feat: 设置页面支持配置多设备 2024-07-05 10:34:38 +00:00
涵曦
96e3b8c2ff feat: 默认用空的后台账号和密码 2024-07-05 09:10:48 +00:00
涵曦
794e8dcd06 feat: 支持多个设备同时播放 see #65 2024-07-05 09:10:01 +00:00
涵曦
f2675e4340 Update README.md 2024-07-05 13:08:18 +08:00
涵曦
e5059840fb fix: 设置页面日志路径写错了 2024-07-05 04:57:20 +00:00
涵曦
f3e57789fa feat: 新增自定义口令功能 #105 2024-07-05 04:26:21 +00:00
涵曦
c151b826f7 Update README.md 2024-07-04 23:31:43 +08:00
涵曦
1b3ed3b35a fix: 修复口令导致异常关闭的问题 2024-07-04 13:07:34 +00:00
涵曦
77a37a9438 new version v0.1.92 2024-07-04 11:12:00 +00:00
涵曦
f22de9f906 Update README.md 2024-07-04 19:11:03 +08:00
涵曦
090e8c3f4c feat: 启动参数新增 --port 配置监听端口 2024-07-04 10:42:31 +00:00
涵曦
aef51fb65d Update README.md 2024-07-04 18:32:15 +08:00
涵曦
a5a3a2dc62 Update README.md 2024-07-04 18:26:38 +08:00
涵曦
ce9adcee7f feat: 外网访问端口可独立配置 2024-07-04 10:15:48 +00:00
涵曦
485a42a9a0 feat: 优化设置页面,新增更多配置项 2024-07-04 09:36:06 +00:00
涵曦
3754970c84 feat: 首次保存设置后不需要重启容器 2024-07-04 04:42:19 +00:00
涵曦
924fbc208b fix: 日志文件配置的环境变量写错了 2024-07-04 04:03:23 +00:00
涵曦
c1a2ee4577 new version v0.1.91 2024-07-03 16:08:00 +00:00
涵曦
e84ee5de1e feat:触屏版显示界面的歌曲id支持配置 2024-07-03 13:48:52 +00:00
涵曦
0414830539 fix: 尝试解决触屏版不能播放的问题 2024-07-03 13:48:52 +00:00
涵曦
385f23842d Update README.md 2024-07-03 12:05:01 +08:00
涵曦
1deceaa5a5 Update README.md 2024-07-03 01:02:26 +08:00
涵曦
b8f1157e27 new version v0.1.90 2024-07-02 16:33:30 +00:00
涵曦
06558c24b7 feat: 优化触屏版播放页面显示歌曲 2024-07-02 16:33:06 +00:00
涵曦
6bd399b654 new version v0.1.89 2024-07-02 16:12:14 +00:00
涵曦
228d89f1f8 fix: 播放歌曲写成固定的了 2024-07-02 15:55:02 +00:00
涵曦
e97639302f feat: 尝试解决触屏版无法播放的问题 2024-07-02 13:40:56 +00:00
涵曦
7f4e51be08 fix: 播放歌曲时被其他指令打断后没有继续播放 2024-07-02 13:25:38 +00:00
涵曦
cdab5fc92d new version v0.1.88 2024-07-02 09:40:22 +00:00
涵曦
6efe498f2a feat: 日志里不要输出敏感信息 2024-07-02 05:27:17 +00:00
涵曦
0f3f2e47f5 feat: 优化下载 ffmpeg 脚本,尝试解决 armv7 环境问题 2024-07-02 05:24:29 +00:00
涵曦
3b720b7367 fix: 是否下载中判断错误导致播放无法自动重新开始播放 2024-07-02 03:29:01 +00:00
涵曦
9a3e513b6c fix: 升级yt-dlp到2024.07.01 2024-07-02 03:28:31 +00:00
涵曦
5a8e5dfa82 feat: 优化日志输出信息 2024-07-02 03:10:33 +00:00
涵曦
70d9ad93cb fix: 修复部分型号关机失败的问题 2024-07-01 16:57:04 +00:00
涵曦
87b3411f5e feat: 尝试解决触屏版无法播放的问题 2024-07-01 15:28:57 +00:00
涵曦
5c88c79ac6 new version v0.1.87 2024-07-01 14:43:59 +00:00
涵曦
5df91e7a59 fix: 修复XIAOMUSIC_USE_MUSIC_API=true时播放不了的问题 2024-07-01 14:42:20 +00:00
涵曦
71e9c15b5d new version v0.1.86 2024-07-01 13:34:38 +00:00
涵曦
c151144a5a Update README.md 2024-07-01 20:28:45 +08:00
涵曦
82a3373e72 Update README.md 2024-07-01 20:24:43 +08:00
涵曦
29ef5f238f feat: 优化 ffmpeg 安装脚本 2024-07-01 11:47:31 +00:00
涵曦
1809a2ab54 fix: 尝试修复 armv7 的 ffmpeg 问题 2024-07-01 11:14:55 +00:00
涵曦
3b1684f553 feat: 新增调试工具用来调试 player_play_music 接口 2024-07-01 11:08:54 +00:00
涵曦
d088374333 Update README.md 2024-07-01 09:15:07 +08:00
涵曦
80da6bd1e6 fix: 尝试修复关机失败的问题 2024-07-01 01:11:35 +00:00
涵曦
619bb9c853 fix: 修复口令不能播放的问题 2024-07-01 01:11:35 +00:00
涵曦
5add7b7a5c feat: 升级依赖库 MiService 2024-07-01 01:11:35 +00:00
涵曦
f61f14e16c Update README.md 2024-07-01 05:30:21 +08:00
leic4u
125421db22 为支持的设备增加产品名称和官方产品百科链接
为支持的设备增加产品名称和官方产品百科链接
2024-07-01 05:23:47 +08:00
涵曦
98b73f72df new version v0.1.85 2024-06-30 10:20:06 +00:00
涵曦
e68bc3b937 fix: 修复电台删除后没有从电台列表中删除的问题 2024-06-30 10:19:53 +00:00
涵曦
ab447a4633 feat: 版本号链接到github的release页面,方便查看版本更新日志 2024-06-30 10:16:58 +00:00
涵曦
23d321a722 new version v0.1.84 2024-06-30 09:44:25 +00:00
涵曦
20945954b1 feat: config.json 支持更多配置选项 2024-06-30 09:43:51 +00:00
涵曦
d6c2078917 docs: 文档更新 2024-06-30 09:43:28 +00:00
涵曦
a5b8dc639c feat: 新增 XIAOMUSIC_STOP_TTS_MSG 配置关机提示音 2024-06-30 07:32:33 +00:00
涵曦
84751e0d68 new version v0.1.83 2024-06-30 06:38:09 +00:00
涵曦
e759658481 bugfix: pip安装运行名字错误 2024-06-30 06:38:04 +00:00
涵曦
d83100588f new version v0.1.82 2024-06-30 06:34:03 +00:00
涵曦
83d0e02eb4 feat: 优化指令匹配规则 2024-06-30 06:33:53 +00:00
涵曦
20f1f33b6c update readme 2024-06-30 05:53:54 +00:00
涵曦
fbb5d26c28 new version v0.1.81 2024-06-30 05:42:26 +00:00
涵曦
2c21778675 update project 2024-06-30 05:40:08 +00:00
涵曦
959acd8fb7 优化关机提示 2024-06-30 05:27:44 +00:00
涵曦
148c5b7621 命令行新增LOGO 2024-06-30 05:19:38 +00:00
涵曦
d7a2afba48 Update README.md 2024-06-30 12:06:01 +08:00
涵曦
559ed23214 Update README.md 2024-06-30 12:05:10 +08:00
涵曦
1d1e63df8a Update README.md 2024-06-30 12:04:29 +08:00
涵曦
27e9d92a0a 提交config.json模板文件 2024-06-30 03:54:39 +00:00
涵曦
69573f3fa4 new version v0.1.80 2024-06-30 01:07:29 +00:00
涵曦
edafd79140 fix: #91 修复下载歌曲报错 2024-06-30 01:07:25 +00:00
涵曦
7c45d93fea new version v0.1.79 2024-06-29 15:37:38 +00:00
涵曦
f19a7e1080 优化关机的问题 2024-06-29 15:37:08 +00:00
涵曦
5e0ae07978 new version v0.1.78 2024-06-29 15:35:15 +00:00
涵曦
116a05ce4b 修复播放列表的问题 2024-06-29 15:35:15 +00:00
涵曦
d6fdee5905 Update README.md 2024-06-29 21:33:09 +08:00
涵曦
af25300917 new version v0.1.77 2024-06-29 13:24:37 +00:00
涵曦
f18b2f49bf fix: #52 支持配置模糊匹配本地歌曲 2024-06-29 13:21:37 +00:00
涵曦
db1e4e6fc4 整理代码 2024-06-29 12:42:15 +00:00
涵曦
dc49f63a37 新增直接播放链接的接口 2024-06-29 12:41:29 +00:00
涵曦
6837841872 Update README.md 2024-06-29 09:32:41 +08:00
涵曦
637347ae0c Update README.md 2024-06-29 09:18:26 +08:00
涵曦
ff968c4db4 Update README.md 2024-06-29 00:14:52 +08:00
涵曦
ca547e0d81 new version v0.1.76 2024-06-28 16:10:50 +00:00
涵曦
f0931c447b 下载日志链接改为按钮样式 2024-06-28 16:10:34 +00:00
涵曦
d098b5eb60 Update README.md 2024-06-28 23:45:18 +08:00
涵曦
09111e849d Update README.md 2024-06-28 23:43:48 +08:00
涵曦
49a76dee60 Update README.md 2024-06-28 23:20:45 +08:00
涵曦
637672473e Update README.md 2024-06-28 23:14:16 +08:00
涵曦
e4e1d13b69 Update README.md 2024-06-28 23:00:01 +08:00
涵曦
7736f8c5b4 Update README.md 2024-06-28 22:55:31 +08:00
涵曦
29db69b52f Update README.md 2024-06-28 22:52:04 +08:00
涵曦
37fe51771b Update README.md 2024-06-28 22:48:42 +08:00
涵曦
77ece713dc new version v0.1.75 2024-06-28 14:46:51 +00:00
涵曦
4436cc3a15 新增ogg格式文件 2024-06-28 14:46:46 +00:00
涵曦
c875350112 new version v0.1.74 2024-06-28 14:34:36 +00:00
涵曦
d6df2f6bfe 支持在设置页面下载日志 2024-06-28 14:34:12 +00:00
涵曦
03e3312218 新增日志文件 2024-06-28 14:03:09 +00:00
涵曦
5d7451c3f2 new version v0.1.73 2024-06-28 10:14:12 +00:00
涵曦
551dfa0c7f 播放列表口令加到默认唤醒词里 2024-06-28 10:14:08 +00:00
涵曦
3cc35e8f97 Update README.md 2024-06-28 15:32:19 +08:00
涵曦
01c68ba64e Create FUNDING.yml 2024-06-28 13:43:16 +08:00
涵曦
61d167d347 Update README.md 2024-06-28 12:18:18 +08:00
涵曦
9393e9f1ca new version v0.1.72 2024-06-28 02:43:35 +00:00
涵曦
c08ef030e9 优化配置页面,只允许选did see #83 2024-06-28 02:41:56 +00:00
涵曦
bad13f01f4 优化获取播放时长的问题 2024-06-28 02:33:12 +00:00
涵曦
049e1a2c38 测试触屏版本不能播放的问题 2024-06-28 01:47:27 +00:00
涵曦
a9fb829563 new version v0.1.71 2024-06-28 00:57:31 +00:00
涵曦
e66f731301 fix: #83 2024-06-28 00:56:39 +00:00
涵曦
538ac1d485 优化获取播放时长的逻辑 2024-06-27 23:20:43 +00:00
涵曦
88fbc503e7 加点调试日志 2024-06-27 16:36:53 +00:00
涵曦
1dc3ccbc16 更新依赖库 2024-06-27 15:34:03 +00:00
涵曦
df007d8e1b Update README.md 2024-06-27 23:15:53 +08:00
涵曦
0876551795 Update README.md 2024-06-27 23:09:40 +08:00
涵曦
51acb3ac8e Update README.md 2024-06-27 23:06:34 +08:00
涵曦
786af1c79e Update README.md 2024-06-27 22:46:10 +08:00
涵曦
7fba78e44b Update README.md 2024-06-27 22:03:06 +08:00
涵曦
9434cf3216 Update README.md 2024-06-27 14:17:00 +08:00
涵曦
f71ab25407 new version v0.1.70 2024-06-27 05:38:12 +00:00
涵曦
a634813c21 update readme 2024-06-27 05:33:58 +00:00
涵曦
17279aaae0 新增XIAOMUSIC_USE_MUSIC_API=true时使用player_play_music接口,兼容部分不能播放的设备型号 see #30 2024-06-27 05:33:53 +00:00
涵曦
4a234e8829 优化m3u文件转换工具 2024-06-27 05:16:33 +00:00
涵曦
805b3c41c8 优化下载文件的逻辑 2024-06-27 05:07:34 +00:00
涵曦
7a154fd847 修复停止播放导致退出问题 see #80 2024-06-26 23:33:15 +00:00
涵曦
3cdc836e9e 播放本地歌曲口令也作为唤醒词 see #80 2024-06-26 17:41:38 +00:00
涵曦
d232627796 优化播放歌曲口令和关机口令,新增播放本地歌曲口令,且都支持自定义配置这三个口令 see #80 2024-06-26 17:34:23 +00:00
涵曦
579820e606 new version v0.1.69 2024-06-26 16:24:12 +00:00
涵曦
a24a5166f9 new version v0.1.68 2024-06-26 16:24:09 +00:00
涵曦
ad67894244 Update README.md 2024-06-27 00:23:22 +08:00
30 changed files with 2026 additions and 468 deletions

2
.github/FUNDING.yml vendored Normal file
View File

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

View File

@@ -28,3 +28,9 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: hanxi/xiaomusic

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

355
CHANGELOG.md Normal file
View File

@@ -0,0 +1,355 @@
## 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

@@ -1,4 +1,5 @@
FROM python:3.10 AS builder
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /app
COPY requirements.txt .
RUN python3 -m venv .venv && .venv/bin/pip install --no-cache-dir -r requirements.txt
@@ -10,6 +11,7 @@ WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/ffmpeg /app/ffmpeg
COPY xiaomusic/ ./xiaomusic/
COPY plugins/ ./plugins/
COPY xiaomusic.py .
ENV XDG_CONFIG_HOME=/config
ENV XIAOMUSIC_HOSTNAME=192.168.2.5

168
README.md
View File

@@ -11,6 +11,10 @@
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
<https://github.com/hanxi/xiaomusic>
> 初次安装遇到问题请查阅 <https://github.com/hanxi/xiaomusic/issues/99> 上是否已经有解决办法。
## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下:
@@ -25,12 +29,73 @@ services:
- 8090:8090
volumes:
- ./music:/app/music
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
```
启动成功后,在 web 页面可以配置 MI_DID, MI_HARDWARE, XIAOMUSIC_SEARCH, XIAOMUSIC_PROXY 参数。
对应的 docker 启动命令如下:
```yaml
docker run -p 8090:8090 \
-v ./music:/app/music \
hanxi/xiaomusic
```
启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。
### ✨ 修改8090端口
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 5678:5678
volumes:
- ./music:/app/music
environment:
XIAOMUSIC_PORT: 5678
```
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
> 目前除了 XIAOMUSIC_PORT 只能在启动前配置,其他参数都可以在 web 网页里配置。
## pip 方式安装运行
```shell
> pip install xiaomusic
> xiaomusic --help
__ __ _ __ __ _
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
XiaoMusic v0.1.92 by: github.com/hanxi
usage: xiaomusic [-h] [--port PORT] [--hardware HARDWARE] [--account ACCOUNT]
[--password PASSWORD] [--cookie COOKIE] [--verbose]
[--config CONFIG] [--ffmpeg_location FFMPEG_LOCATION]
options:
-h, --help show this help message and exit
--port PORT 监听端口
--hardware HARDWARE 小爱音箱型号
--account ACCOUNT xiaomi account
--password PASSWORD xiaomi password
--cookie COOKIE xiaomi cookie
--verbose show info
--config CONFIG config file path
--ffmpeg_location FFMPEG_LOCATION
ffmpeg bin path
> xiaomusic --config config.json
```
其中 `config.json` 文件可以参考 `config-example.json` 文件配置。见 <https://github.com/hanxi/xiaomusic/issues/94>
不修改默认端口 8090 的情况下,只需要执行 `xiaomusic` 即可启动。
## 开发环境运行
@@ -66,21 +131,38 @@ pdm run xiaomusic.py
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
## 已测试设备
## 已测试支持的设备
| 型号 | 名称 |
| ---- | ---------------------------------------------------------------------------------------------- |
| L06A | [小爱音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l06a) |
| L07A | [Redmi小爱音箱 Play](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l7a) |
| S12/S12A/MDZ-25-DA | [小米AI音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.s12) |
| LX5A | [小爱音箱 万能遥控版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx5a) |
| LX05 | [小爱音箱Play2019款](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx05) |
| L16A | [Xiaomi Sound](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l16a) |
| L17A | [Xiaomi Sound Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l17a) |
| LX06 | [小爱音箱Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx06) |
| LX01 | [小爱音箱mini](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx01) |
| L05B | [小爱音箱Play](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05b) |
| L05C | [小米小爱音箱Play 增强版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05c) |
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
> 目前应该所有设备类型都已经支持播放,有问题随时反馈。
```txt
"L07A": ("5-1", "5-5"), # Redmi小爱音箱Play(l7a)
````
## 支持音乐格式
- mp3
- flac
- wav
- ape
- ogg
- m4a
> 本地音乐会搜索 mp3 和 flac 格式的文件,下载的歌曲是 mp3 格式的。
## 其他参数
- XIAOMUSIC_ACTIVE_CMD 环境变量,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
## 在 Docker 里使用
@@ -171,6 +253,26 @@ services:
XIAOMUSIC_HOSTNAME: '192.168.2.5'
```
如果想让 setting.json 文件不存储到 music 目录,可以这样配,下面的示例会把 setting.json 文件放到容器的 /app/conf 目录且映射到本地的 ./conf 目录:
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
- ./conf:/app/conf
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
XIAOMUSIC_CONF_PATH: '/app/conf'
```
## 简易的控制面板
@@ -178,11 +280,14 @@ services:
- ip 是 XIAOMUSIC_HOSTNAME 设置的
- 8090 是默认端口
- 功能
- 支持功能
- 显示正在播放的歌曲
- 模糊搜索本地歌曲
- 播放列表
- 删除歌曲
- 设置页面
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER
@@ -197,24 +302,40 @@ services:
## 网络歌单功能
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
> 欢迎有想法的朋友们制作更多的歌单转换工具。
## 更多其他可选配置
- XIAOMUSIC_ACTIVE_CMD 配置唤醒令,具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_ACTIVE_CMD 环境变量,用于唤醒令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体见 <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 日志,用于排查问题
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82>
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号,触屏版目前还需要设置这个为 true 。
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_ENABLE_FUZZY_MATCH 设为 true 时开启模糊匹配(默认),设为 false 时关闭模糊匹配,支持模糊匹配歌名和歌单名。 具体见 <https://github.com/hanxi/xiaomusic/issues/52>
- XIAOMUSIC_FUZZY_MATCH_CUTOFF 设置模糊搜索匹配的最低相似度阈值默认0.6可以配0到1直接的小数越小越模糊越大越精准。具体见 <https://github.com/hanxi/xiaomusic/issues/52>
- XIAOMUSIC_PUBLIC_PORT 用于设置外网端口当使用反向代理时可以设置为外网端口XIAOMUSIC_HOSTNAME 设为外网IP或者域名即可。
- XIAOMUSIC_DOWNLOAD_PATH 变量可以配置下载目录,默认为空,表示使用 music 目录为下载目录。设置这个目录必须是 music 的子目录,否则刷新列表后会找不到歌曲。具体见 <https://github.com/hanxi/xiaomusic/issues/98>
## 高级篇
- 自定义口令功能 <https://github.com/hanxi/xiaomusic/issues/105>
- [ ] 缺少一篇教程 [如何写自定义插件](https://github.com/hanxi/xiaomusic/issues/105)
## 讨论区
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
- [点击链接加入群聊【xiaomusic】 604526973](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=13St5PLVcTxYlWTAs_iAawazjtdD1l-a&authKey=dJWEpaT2fDBDpdUUOWj%2FLt6NS1ePBfShDfz7a6seNURi05VvVnAGQzXF%2FM%2F5HgIm&noverify=0&group_code=604526973)
- https://github.com/hanxi/xiaomusic/issues
- <https://github.com/hanxi/xiaomusic/issues>
- [微信群二维码](https://github.com/hanxi/xiaomusic/issues/86)
## 感谢
@@ -226,10 +347,15 @@ services:
- [NAS部署教程](https://post.m.smzdm.com/p/avpe7n99/)
- [群晖部署教程](https://post.m.smzdm.com/p/a7px7dol/)
- [QNAS部署教程](https://post.smzdm.com/p/a5xz5x63/)
- 所有帮忙调试和测试的朋友
- 所有反馈问题和建议的朋友
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=hanxi/xiaomusic&type=Date)](https://star-history.com/#hanxi/xiaomusic&Date)
## 赞赏
谢谢就够了
- 爱发电 <https://afdian.net/a/imhanxi>
- 点个 Star ⭐
- 谢谢 ❤️

81
config-example.json Normal file
View File

@@ -0,0 +1,81 @@
{
"hardware": "L07A",
"account": "",
"password": "",
"mi_did": "",
"cookie": "",
"verbose": false,
"music_path": "music",
"conf_path": null,
"hostname": "192.168.2.5",
"port": 8090,
"public_port": 0,
"proxy": null,
"search_prefix": "bilisearch:",
"ffmpeg_location": "./ffmpeg/bin",
"active_cmd": "play,random_play,playlocal,play_music_list,stop",
"exclude_dirs": "@eaDir",
"music_path_depth": 10,
"disable_httpauth": true,
"httpauth_username": "admin",
"httpauth_password": "admin",
"music_list_url": "",
"music_list_json": "",
"disable_download": false,
"key_word_dict": {
"播放歌曲": "play",
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "random_play",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"set_volume#": "set_volume",
"get_volume#": "get_volume",
"本地播放歌曲": "playlocal",
"放歌曲": "play",
"暂停": "stop",
"停止": "stop",
"停止播放": "stop",
"测试自定义口令": "exec#code1(\"hello\")",
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
},
"key_match_order": [
"set_volume#",
"get_volume#",
"分钟后关机",
"播放歌曲",
"下一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"刷新列表",
"播放列表",
"播放本地歌曲",
"本地播放歌曲",
"放歌曲",
"暂停",
"停止",
"停止播放",
"测试自定义口令",
"测试链接"
],
"use_music_api": false,
"use_music_audio_id": "1582971365183456177",
"use_music_id": "355454500",
"log_file": "/tmp/xiaomusic.txt",
"fuzzy_match_cutoff": 0.6,
"enable_fuzzy_match": true,
"stop_tts_msg": "收到,再见",
"keywords_playlocal": "播放本地歌曲,本地播放歌曲",
"keywords_play": "播放歌曲,放歌曲",
"keywords_stop": "关机,暂停,停止,停止播放",
"user_key_word_dict": {
"测试自定义口令": "exec#code1(\"hello\")",
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
}
}

View File

@@ -4,14 +4,47 @@
# https://github.com/yt-dlp/yt-dlp#dependencies
# 判断系统架构
arch=$(arch)
arch=$(uname -m)
pkg=ffmpeg-master-latest-linuxarm64-gpl
if [[ "${arch}" == "x86_64" ]]; then
pkg=ffmpeg-master-latest-linux64-gpl
fi
# 输出架构信息
echo "当前系统架构是:$arch"
#export ALL_PROXY=http://192.168.2.5:8080
wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz
tar -xvJf $pkg.tar.xz
mv $pkg ffmpeg
install_from_github() {
pkg=$1
wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz
tar -xvJf $pkg.tar.xz
mkdir -p ffmpeg/bin
mv $pkg/bin/ffmpeg ffmpeg/bin/
mv $pkg/bin/ffprobe ffmpeg/bin/
}
install_from_ffmpeg() {
pkg=$1
wget https://johnvansickle.com/ffmpeg/builds/$pkg.tar.xz
mkdir -p $pkg
tar -xvJf $pkg.tar.xz -C $pkg
mkdir -p ffmpeg/bin
mv $pkg/*/ffmpeg ffmpeg/bin/
mv $pkg/*/ffprobe ffmpeg/bin/
}
# 基于架构执行不同的操作
case "$arch" in
x86_64)
echo "64位 x86 架构"
install_from_github ffmpeg-master-latest-linux64-gpl
#install_from_ffmpeg ffmpeg-git-amd64-static
;;
arm64 | aarch64)
echo "64位 ARM 架构"
install_from_github ffmpeg-master-latest-linuxarm64-gpl
#install_from_ffmpeg ffmpeg-git-arm64-static
;;
armv7l)
echo "armv7l 架构"
install_from_ffmpeg ffmpeg-git-armhf-static
;;
*)
echo "未知架构 $arch"
;;
esac

View File

@@ -1,37 +1,5 @@
#!/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

216
pdm.lock generated
View File

@@ -2,10 +2,10 @@
# It is not intended for manual editing.
[metadata]
groups = ["default", "lint"]
groups = ["default", "lint", "dev"]
strategy = ["cross_platform"]
lock_version = "4.4.1"
content_hash = "sha256:813253734c7d7835a76cd87fe8fe0329e02ad067f535aee6a9e11cb106569dd2"
content_hash = "sha256:3631f504ea2c9e450ff20fe555e2ec0143bc315c22ff257e17a992d1e6d3c39d"
[[package]]
name = "aiohttp"
@@ -82,6 +82,16 @@ files = [
{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]]
name = "asgiref"
version = "3.7.2"
@@ -339,6 +349,39 @@ files = [
{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]]
name = "flask"
version = "3.0.3"
@@ -432,6 +475,19 @@ files = [
{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]]
name = "itsdangerous"
version = "2.1.2"
@@ -519,7 +575,7 @@ files = [
[[package]]
name = "miservice-fork"
version = "2.5.0"
version = "2.6.1"
requires_python = ">=3.8"
summary = "XiaoMi Cloud Service fork from https://github.com/Yonsm/MiService"
dependencies = [
@@ -528,8 +584,8 @@ dependencies = [
"rich",
]
files = [
{file = "miservice_fork-2.5.0-py3-none-any.whl", hash = "sha256:97b6360ea53c34fe035ac9d94e8705f305b8fa7fc2b44a7aea182449a76cb622"},
{file = "miservice_fork-2.5.0.tar.gz", hash = "sha256:8ca2d370d5b32f7e330add38aa1912d734aefa7880f16cef9eac110a5a3029e2"},
{file = "miservice_fork-2.6.1-py3-none-any.whl", hash = "sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39"},
{file = "miservice_fork-2.6.1.tar.gz", hash = "sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c"},
]
[[package]]
@@ -581,6 +637,29 @@ files = [
{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]]
name = "pycparser"
version = "2.21"
@@ -630,6 +709,51 @@ files = [
{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]]
name = "requests"
version = "2.32.3"
@@ -662,27 +786,48 @@ files = [
[[package]]
name = "ruff"
version = "0.4.9"
version = "0.5.0"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [
{file = "ruff-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b262ed08d036ebe162123170b35703aaf9daffecb698cd367a8d585157732991"},
{file = "ruff-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:98ec2775fd2d856dc405635e5ee4ff177920f2141b8e2d9eb5bd6efd50e80317"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4555056049d46d8a381f746680db1c46e67ac3b00d714606304077682832998e"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e91175fbe48f8a2174c9aad70438fe9cb0a5732c4159b2a10a3565fea2d94cde"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8e7b95673f22e0efd3571fb5b0cf71a5eaaa3cc8a776584f3b2cc878e46bff"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2d45ddc6d82e1190ea737341326ecbc9a61447ba331b0a8962869fcada758505"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78de3fdb95c4af084087628132336772b1c5044f6e710739d440fc0bccf4d321"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06b60f91bfa5514bb689b500a25ba48e897d18fea14dce14b48a0c40d1635893"},
{file = "ruff-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88bffe9c6a454bf8529f9ab9091c99490578a593cc9f9822b7fc065ee0712a06"},
{file = "ruff-0.4.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:673bddb893f21ab47a8334c8e0ea7fd6598ecc8e698da75bcd12a7b9d0a3206e"},
{file = "ruff-0.4.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c1aff58c31948cc66d0b22951aa19edb5af0a3af40c936340cd32a8b1ab7438"},
{file = "ruff-0.4.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:784d3ec9bd6493c3b720a0b76f741e6c2d7d44f6b2be87f5eef1ae8cc1d54c84"},
{file = "ruff-0.4.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:732dd550bfa5d85af8c3c6cbc47ba5b67c6aed8a89e2f011b908fc88f87649db"},
{file = "ruff-0.4.9-py3-none-win32.whl", hash = "sha256:8064590fd1a50dcf4909c268b0e7c2498253273309ad3d97e4a752bb9df4f521"},
{file = "ruff-0.4.9-py3-none-win_amd64.whl", hash = "sha256:e0a22c4157e53d006530c902107c7f550b9233e9706313ab57b892d7197d8e52"},
{file = "ruff-0.4.9-py3-none-win_arm64.whl", hash = "sha256:5d5460f789ccf4efd43f265a58538a2c24dbce15dbf560676e430375f20a8198"},
{file = "ruff-0.4.9.tar.gz", hash = "sha256:f1cb0828ac9533ba0135d148d214e284711ede33640465e706772645483427e3"},
{file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"},
{file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"},
{file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"},
{file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"},
{file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"},
{file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"},
{file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"},
]
[[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]]
@@ -715,6 +860,15 @@ files = [
{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]]
name = "websockets"
version = "12.0"
@@ -831,7 +985,7 @@ files = [
[[package]]
name = "yt-dlp"
version = "2024.6.17.232743.dev0"
version = "2024.7.1.232715.dev0"
requires_python = ">=3.8"
summary = "A feature-rich command-line audio/video downloader"
dependencies = [
@@ -845,6 +999,16 @@ dependencies = [
"websockets>=12.0",
]
files = [
{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"},
{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"},
]
[[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"},
]

0
plugins/__init__.py Normal file
View File

4
plugins/code1.py Normal file
View File

@@ -0,0 +1,4 @@
async def code1(arg1):
global log, xiaomusic
log.info(f"code1:{arg1}")
await xiaomusic.do_tts("你好,我是自定义的测试口令")

10
plugins/httpget.py Normal file
View File

@@ -0,0 +1,10 @@
import requests
def httpget(url):
global log
# 发起请求
response = requests.get(url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
log.info(f"httpget url:{url} response:{response.text}")

View File

@@ -1,16 +1,15 @@
[project]
name = "xiaomusic"
version = "0.1.67"
version = "0.1.97"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},
{name = "涵曦", email = "im.hanxi@gmail.com"},
]
dependencies = [
"requests>=2.31.0",
"aiohttp>=3.8.6",
"miservice-fork>=2.5.0",
"mutagen>=1.47.0",
"yt-dlp>=2024.04.09",
"yt-dlp>=2024.07.01",
"flask[async]>=3.0.1",
"waitress>=3.0.0",
"flask-HTTPAuth>=4.8.0",
@@ -19,6 +18,12 @@ requires-python = ">=3.10"
readme = "README.md"
license = {text = "MIT"}
[project.urls]
Homepage = "https://github.com/hanxi/xiaomusic"
[project.scripts]
xiaomusic = "xiaomusic.cli:main"
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
@@ -28,19 +33,22 @@ build-backend = "pdm.backend"
lint = [
"ruff>=0.4.8",
]
dev = [
"commitizen>=3.27.0",
]
[tool.ruff]
lint.select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"E", # pycodestyle - Error
"F", # Pyflakes
"I", # isort
"W", # pycodestyle - Warning
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"E", # pycodestyle - Error
"F", # Pyflakes
"I", # isort
"W", # pycodestyle - Warning
"UP", # pyupgrade
]
lint.ignore = [
"E501", # line-too-long
"W191", # tab-indentation
"E501", # line-too-long
"W191", # tab-indentation
]
include = ["**/*.py", "**/*.pyi", "**/pyproject.toml"]
@@ -50,3 +58,14 @@ convention = "google"
[tool.pdm.scripts]
lint = "ruff check ."
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

@@ -305,9 +305,9 @@ MarkupSafe==2.1.4 \
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
miservice-fork==2.5.0 \
--hash=sha256:8ca2d370d5b32f7e330add38aa1912d734aefa7880f16cef9eac110a5a3029e2 \
--hash=sha256:97b6360ea53c34fe035ac9d94e8705f305b8fa7fc2b44a7aea182449a76cb622
miservice-fork==2.6.1 \
--hash=sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c \
--hash=sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39
multidict==6.0.4 \
--hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \
--hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \
@@ -472,6 +472,6 @@ yarl==1.9.2 \
--hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \
--hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \
--hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560
yt-dlp==2024.6.17.232743.dev0 \
--hash=sha256:2f6f44eff755a7b051cdcd3c4375771033dbeb64d6164351022efdc67cce0c52 \
--hash=sha256:dd6e7e194b96e778691f58a0cb6b42956cf956b22f6bb1a12bdef5ab3ac0c9ad
yt-dlp==2024.7.1.232715.dev0 \
--hash=sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8 \
--hash=sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503

View File

@@ -1 +1 @@
__version__ = "0.1.67"
__version__ = "0.1.97"

View File

@@ -1,16 +1,33 @@
import argparse
import asyncio
from xiaomusic import (
__version__,
)
from xiaomusic.config import Config
from xiaomusic.xiaomusic import XiaoMusic
LOGO = r"""
__ __ _ __ __ _
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
{}
"""
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--port",
dest="port",
help="监听端口",
)
parser.add_argument(
"--hardware",
dest="hardware",
help="小爱 hardware",
help="小爱音箱型号",
)
parser.add_argument(
"--account",
@@ -44,6 +61,14 @@ def main():
dest="ffmpeg_location",
help="ffmpeg bin path",
)
parser.add_argument(
"--enable_config_example",
dest="enable_config_example",
help="是否输出示例配置文件",
action="store_true",
)
print(LOGO.format(f"XiaoMusic v{__version__} by: github.com/hanxi"))
options = parser.parse_args()
config = Config.from_options(options)

View File

@@ -3,94 +3,154 @@ from __future__ import annotations
import argparse
import json
import os
from dataclasses import dataclass
from dataclasses import asdict, dataclass, field
from typing import get_type_hints
from xiaomusic.utils import validate_proxy
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}&timestamp={timestamp}&limit=2"
COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}"
KEY_WORD_DICT = {
"播放歌曲": "play",
"放歌曲": "play",
"下一首": "play_next",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "random_play",
"关机": "stop",
"停止播放": "stop",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"set_volume#": "set_volume",
"get_volume#": "get_volume",
}
# 默认口令
def default_key_word_dict():
return {
"播放歌曲": "play",
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "random_play",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"set_volume#": "set_volume",
"get_volume#": "get_volume",
}
def default_user_key_word_dict():
return {
"测试自定义口令": 'exec#code1("hello")',
"测试链接": 'exec#httpget("https://github.com/hanxi/xiaomusic")',
}
# 命令参数在前面
KEY_WORD_ARG_BEFORE_DICT = {
"分钟后关机": True,
}
# 匹配优先级
KEY_MATCH_ORDER = [
"set_volume#",
"get_volume#",
"分钟后关机",
"播放歌曲",
"放歌曲",
"下一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"停止播放",
"刷新列表",
"播放列表",
]
SUPPORT_MUSIC_TYPE = [
".mp3",
".flac",
".wav",
".ape",
]
# 口令匹配优先级
def default_key_match_order():
return [
"set_volume#",
"get_volume#",
"分钟后关机",
"播放歌曲",
"下一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"刷新列表",
"播放列表",
]
@dataclass
class Config:
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", "")
mi_did: str = os.getenv("MI_DID", "") # 逗号分割支持多设备
hardware: str = os.getenv("MI_HARDWARE", "L07A") # 逗号分割支持多设备
cookie: str = ""
verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
music_path: str = os.getenv(
"XIAOMUSIC_MUSIC_PATH", "music"
) # 只能是music目录下的子目录
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "")
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"))
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
public_port: int = int(os.getenv("XIAOMUSIC_PUBLIC_PORT", 0)) # 歌曲访问端口
proxy: str | None = os.getenv("XIAOMUSIC_PROXY", None)
search_prefix: str = os.getenv(
"XIAOMUSIC_SEARCH", "ytsearch:"
"XIAOMUSIC_SEARCH", "bilisearch:"
) # "bilisearch:" or "ytsearch:"
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
active_cmd: str = os.getenv("XIAOMUSIC_ACTIVE_CMD", "play,random_play")
active_cmd: str = os.getenv(
"XIAOMUSIC_ACTIVE_CMD", "play,random_play,playlocal,play_music_list,stop"
)
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")
httpauth_username: str = os.getenv("XIAOMUSIC_HTTPAUTH_USERNAME", "")
httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "")
music_list_url: str = os.getenv("XIAOMUSIC_MUSIC_LIST_URL", "")
music_list_json: str = os.getenv("XIAOMUSIC_MUSIC_LIST_JSON", "")
disable_download: bool = (
os.getenv("XIAOMUSIC_DISABLE_DOWNLOAD", "false").lower() == "true"
)
key_word_dict: dict[str, str] = field(default_factory=default_key_word_dict)
key_match_order: list[str] = field(default_factory=default_key_match_order)
use_music_api: bool = (
os.getenv("XIAOMUSIC_USE_MUSIC_API", "false").lower() == "true"
)
use_music_audio_id: str = os.getenv(
"XIAOMUSIC_USE_MUSIC_AUDIO_ID", "1582971365183456177"
)
use_music_id: str = os.getenv("XIAOMUSIC_USE_MUSIC_ID", "355454500")
log_file: str = os.getenv("XIAOMUSIC_LOG_FILE", "/tmp/xiaomusic.txt")
# 模糊搜索匹配的最低相似度阈值
fuzzy_match_cutoff: float = float(os.getenv("XIAOMUSIC_FUZZY_MATCH_CUTOFF", "0.6"))
# 开启模糊搜索
enable_fuzzy_match: bool = (
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
)
stop_tts_msg: str = os.getenv("XIAOMUSIC_STOP_TTS_MSG", "收到,再见")
enable_config_example: bool = False
keywords_playlocal: str = os.getenv(
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
)
keywords_play: str = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
keywords_stop: str = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止,停止播放")
user_key_word_dict: dict[str, str] = field(
default_factory=default_user_key_word_dict
)
enable_force_stop: bool = (
os.getenv("XIAOMUSIC_ENABLE_FORCE_STOP", "false").lower() == "true"
)
def append_keyword(self, keys, action):
for key in keys.split(","):
self.key_word_dict[key] = action
if key not in self.key_match_order:
self.key_match_order.append(key)
def append_user_keyword(self):
for k, v in self.user_key_word_dict.items():
self.key_word_dict[k] = v
self.key_match_order.append(k)
def __post_init__(self) -> None:
if self.proxy:
validate_proxy(self.proxy)
self.append_keyword(self.keywords_playlocal, "playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_stop, "stop")
self.append_user_keyword()
# 保存配置到 config-example.json 文件
if self.enable_config_example:
with open("config-example.json", "w") as f:
data = asdict(self)
json.dump(data, f, ensure_ascii=False, indent=4)
@classmethod
def from_options(cls, options: argparse.Namespace) -> Config:
config = {}
@@ -110,3 +170,34 @@ class Config:
if value is not None and key in cls.__dataclass_fields__:
result[key] = value
return result
def update_config(self, data):
# 获取类型提示
type_hints = get_type_hints(self)
for k, v in data.items():
if v and k in type_hints:
# 获取字段的类型
expected_type = type_hints[k]
# 根据期望的类型进行转换
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)

11
xiaomusic/const.py Normal file
View File

@@ -0,0 +1,11 @@
SUPPORT_MUSIC_TYPE = [
".mp3",
".flac",
".wav",
".ape",
".ogg",
".m4a",
]
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}&timestamp={timestamp}&limit=2"
COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}"

View File

@@ -1,18 +1,17 @@
#!/usr/bin/env python3
import os
from dataclasses import asdict
from threading import Thread
from flask import Flask, request, send_from_directory
from flask import Flask, request, send_file, send_from_directory
from flask_httpauth import HTTPBasicAuth
from waitress import serve
from xiaomusic import (
__version__,
)
from xiaomusic.config import (
KEY_WORD_DICT,
)
from xiaomusic.utils import (
deepcopy_data_no_sensitive_info,
downloadfile,
)
@@ -41,7 +40,7 @@ def verify_password(username, password):
@app.route("/allcmds")
@auth.login_required
def allcmds():
return KEY_WORD_DICT
return xiaomusic.config.key_word_dict
@app.route("/getversion", methods=["GET"])
@@ -101,20 +100,11 @@ async def do_cmd():
@auth.login_required
async def getsetting():
config = xiaomusic.getconfig()
log.debug(config)
data = asdict(config)
alldevices = await xiaomusic.call_main_thread_function(xiaomusic.getalldevices)
log.info(alldevices)
data = {
"mi_did": config.mi_did,
"mi_did_list": alldevices["did_list"],
"mi_hardware": config.hardware,
"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,
}
log.info(f"getsetting alldevices: {alldevices}")
data["mi_did_list"] = alldevices["did_list"]
data["mi_hardware_list"] = alldevices["hardware_list"]
return data
@@ -122,7 +112,8 @@ async def getsetting():
@auth.login_required
async def savesetting():
data = request.get_json()
log.info(data)
debug_data = deepcopy_data_no_sensitive_info(data)
log.info(f"saveconfig: {debug_data}")
await xiaomusic.saveconfig(data)
return "save success"
@@ -154,10 +145,11 @@ def downloadjson():
data = request.get_json()
log.info(data)
url = data["url"]
retmsg, content = downloadfile(url)
ret = "OK"
if retmsg != "OK":
log.warning(f"downloadjson failed. url:{url} ret:{ret}")
try:
ret = "OK"
content = downloadfile(url)
except Exception as e:
log.warning(f"downloadjson failed. url:{url} e:{e}")
ret = "Download JSON file failed."
return {
"ret": ret,
@@ -165,6 +157,30 @@ def downloadjson():
}
@app.route("/downloadlog", methods=["GET"])
@auth.login_required
def downloadlog():
return send_file(xiaomusic.config.log_file, as_attachment=True)
@app.route("/playurl", methods=["GET"])
@auth.login_required
async def playurl():
url = request.args.get("url")
log.info(f"play_url:{url}")
return await xiaomusic.call_main_thread_function(xiaomusic.play_url, arg1=url)
@app.route("/debug_play_by_music_url", methods=["POST"])
@auth.login_required
async def debug_play_by_music_url():
data = request.get_json()
log.info(f"data:{data}")
return await xiaomusic.call_main_thread_function(
xiaomusic.debug_play_by_music_url, arg1=data
)
def static_path_handler(filename):
log.debug(filename)
log.debug(static_path)

69
xiaomusic/plugin.py Normal file
View File

@@ -0,0 +1,69 @@
import importlib
import inspect
import pkgutil
class PluginManager:
def __init__(self, xiaomusic, plugin_dir="plugins"):
self.xiaomusic = xiaomusic
self.log = xiaomusic.log
self._funcs = {}
self._load_plugins(plugin_dir)
def _load_plugins(self, plugin_dir):
# 假设 plugins 已经在搜索路径上
package_name = plugin_dir
package = importlib.import_module(package_name)
# 遍历 package 中所有模块并动态导入它们
for _, modname, _ in pkgutil.iter_modules(package.__path__, package_name + "."):
# 跳过__init__文件
if modname.endswith("__init__"):
continue
module = importlib.import_module(modname)
# 将 log 和 xiaomusic 注入模块的命名空间
module.log = self.log
module.xiaomusic = self.xiaomusic
# 动态获取模块中与文件名同名的函数
function_name = modname.split(".")[-1] # 从模块全名提取函数名
if hasattr(module, function_name):
self._funcs[function_name] = getattr(module, function_name)
else:
self.log.error(
f"No function named '{function_name}' found in module {modname}"
)
def get_func(self, plugin_name):
"""根据插件名获取插件函数"""
return self._funcs.get(plugin_name)
def get_local_namespace(self):
"""返回包含所有插件函数的字典,可以用作 exec 要执行的代码的命名空间"""
return self._funcs.copy()
async def execute_plugin(self, code):
"""
执行指定的插件代码。插件函数可以是同步或异步。
:param code: 需要执行的插件函数代码(例如 'plugin1("hello")'
"""
# 分解代码字符串以获取函数名
func_name = code.split("(")[0]
# 根据解析出的函数名从插件字典中获取函数
plugin_func = self.get_func(func_name)
if not plugin_func:
raise ValueError(f"No plugin function named '{func_name}' found.")
# 检查函数是否是异步函数
global_namespace = globals().copy()
local_namespace = self.get_local_namespace()
if inspect.iscoroutinefunction(plugin_func):
# 如果是异步函数,构建执行用的协程对象
coroutine = eval(code, global_namespace, local_namespace)
# 等待协程执行
await coroutine
else:
# 如果是普通函数,直接执行代码
eval(code, global_namespace, local_namespace)

View File

@@ -23,7 +23,7 @@ $(function(){
// 拉取版本
$.get("/getversion", function(data, status) {
console.log(data, status, data["version"]);
$("#version").text(`(${data.version})`);
$("#version").text(`${data.version}`);
});
// 拉取播放列表
@@ -82,6 +82,13 @@ $(function(){
}
});
$("#playurl").on("click", () => {
var url = $("#music-url").val();
$.get(`/playurl?url=${url}`, function(data, status) {
console.log(data);
});
});
function append_op_button_name(name) {
append_op_button(name, name);
}

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>Debug For XiaoMusic</title>
<link rel="stylesheet" type="text/css" href="/static/style.css">
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="/static/jquery-3.7.1.min.js"></script>
<script>
var vConsole = new window.VConsole();
function postJSON() {
var data = $('#post-input').val();
$.ajax({
type: 'POST',
url: '/debug_play_by_music_url',
data: data,
contentType: "application/json; charset=utf-8",
success: (err) => {
console.log("succ", res);
},
error: (res) => {
console.log("error", res);
}
});
}
function sendDebugCmd() {
var cmd = $("#cmd").val();
$.ajax({
type: "POST",
url: "/cmd",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({cmd: cmd}),
success: () => {
},
error: () => {
// 请求失败时执行的操作
}
});
}
</script>
</head>
<body>
<h1>Debug For XiaoMusic</h1>
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
<button onclick="postJSON()">提交</button><br>
<hr>
<input id="cmd" type="text"></input>
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
</body>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>
</html>

View File

@@ -8,7 +8,11 @@
<link rel="stylesheet" type="text/css" href="/static/style.css">
</head>
<body>
<h2>小爱音箱操控面板<span id="version">(版本未知)</span></h2>
<h2>小爱音箱操控面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
版本未知
</a>)
</h2>
<hr>
<div id="cmds">
</div>
@@ -43,6 +47,12 @@
<button id="play_music_list">播放列表歌曲</button>
<button id="del_music">删除选中歌曲</button>
<hr>
<div>
<input id="music-url" type="text" placeholder="链接(http://ngcdn001.cnr.cn/live/zgzs/index.m3u8)"></input>
</div>
<button id="playurl">播放链接</button>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>

View File

@@ -33,10 +33,11 @@ function convertToJSON() {
var musicsArray = [];
var currentName = '';
lines.forEach(function(line) {
if (line.startsWith('#EXTINF:-1,')) {
currentName = line.substring(11).trim();
line = line.trim();
if (line.startsWith('#EXTINF:')) {
currentName = line.replace(/.*,/g, '');
} else if (line.startsWith('http') && currentName !== '') {
musicsArray.push({"name": currentName, "type": "radio", "url": line.trim()});
musicsArray.push({"name": currentName, "type": "radio", "url": line});
currentName = ''; // Reset the name for the next entry
}
});

View File

@@ -6,34 +6,138 @@
<script src="/static/jquery-3.7.1.min.js"></script>
<script src="/static/setting.js"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
-->
</head>
<body>
<h2>小爱音箱设置面板<span id="version">(版本未知)</span></h2>
<h2>小爱音箱设置面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
版本未知
</a>)
</h2>
<hr>
<div class="rows">
<label for="mi_did">MI_DID:</label>
<select id="mi_did"></select>
<label for="mi_hardware">MI_HARDWARE:</label>
<select id="mi_hardware"></select>
<label for="xiaomusic_search">XIAOMUSIC_SEARCH:</label>
<select id="xiaomusic_search">
<option value="ytsearch:">ytsearch:</option>
<option value="bilisearch:">bilisearch:</option>
<label for="mi_did_hardware">*勾选设备(至少勾选1个):</label>
<div id="mi_did_hardware">
</div>
</div>
<br>
<div id="setting" class="rows">
<label for="account">*小米账号:</label>
<input id="account" type="text" placeholder="填写小米登录账号"></input>
<label for="password">*小米密码:</label>
<input id="password" type="password" placeholder="填写小米登录密码"></input>
<label for="hostname">*XIAOMUSIC_HOSTNAME(IP或域名):</label>
<input id="hostname" type="text"></input>
<label for="verbose">是否开启调试日志:</label>
<select id="verbose">
<option value="true" selected>true</option>
<option value="false">false</option>
</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>
<label for="music_path">音乐目录:</label>
<input id="music_path" type="text" value="music"></input>
<label for="download_path">音乐下载目录(必须是music的子目录):</label>
<input id="download_path" type="text" value='music/download'></input>
<label for="conf_path">配置文件目录:</label>
<input id="conf_path" type="text"></input>
<label for="ffmpeg_location">ffmpeg路径:</label>
<input id="ffmpeg_location" type="text" value="./ffmpeg/bin"></input>
<label for="log_file">日志路径:</label>
<input id="log_file" type="text" value="/tmp/xiaomusic.txt"></input>
<label for="active_cmd">允许唤醒的命令:</label>
<input id="active_cmd" type="text" value="play,random_play,playlocal,play_music_list,stop"></input>
<label for="exclude_dirs">忽略目录(逗号分割):</label>
<input id="exclude_dirs" type="text" value="@eaDir"></input>
<label for="music_path_depth">目录深度:</label>
<input id="music_path_depth" type="number" value="10"></input>
<label for="search_prefix">XIAOMUSIC_SEARCH(歌曲下载方式):</label>
<select id="search_prefix">
<option value="bilisearch:">bilisearch:</option>
<option value="ytsearch:">ytsearch:</option>
</select>
<label for="proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080"></input>
<label for="disable_httpauth">关闭密码验证:</label>
<select id="disable_httpauth">
<option value="true" selected>true</option>
<option value="false">false</option>
</select>
<label for="httpauth_username">web控制台账户:</label>
<input id="httpauth_username" type="text" value=""></input>
<label for="httpauth_password">web控制台密码:</label>
<input id="httpauth_password" type="password" value=""></input>
<label for="disable_download">关闭下载功能:</label>
<select id="disable_download">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="use_music_audio_id">触屏版显示歌曲ID:</label>
<input id="use_music_audio_id" type="text" value="1582971365183456177"></input>
<label for="use_music_id">触屏版显示歌曲分段ID:</label>
<input id="use_music_id" type="text" value="355454500"></input>
<label for="fuzzy_match_cutoff">模糊匹配阈值(0.1~0.9):</label>
<input id="fuzzy_match_cutoff" type="number" value="0.6"></input>
<label for="enable_fuzzy_match">开启模糊搜索:</label>
<select id="enable_fuzzy_match">
<option value="true" selected>true</option>
<option value="false">false</option>
</select>
<label for="public_port">外网访问端口(0表示跟监听端口一致):</label>
<input id="public_port" type="number" value="0"></input>
<label for="stop_tts_msg">停止提示音:</label>
<input id="stop_tts_msg" type="text" value="收到,再见"></input>
<label for="keywords_playlocal">播放本地歌曲口令:</label>
<input id="keywords_playlocal" type="text" value="播放本地歌曲,本地播放歌曲"></input>
<label for="keywords_play">播放歌曲口令:</label>
<input id="keywords_play" type="text" value="播放歌曲,放歌曲"></input>
<label for="keywords_stop">停止口令:</label>
<input id="keywords_stop" type="text" value="关机,暂停,停止,停止播放"></input>
<label for="music_list_url">歌单地址:</label>
<input id="music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json"></input>
<label for="music_list_json">歌单内容:</label>
<textarea id="music_list_json" type="text"></textarea>
</div>
<hr>
<button onclick="location.href='/';">返回首页</button>
<button id="get_music_list">获取歌单</button>
<button id="save">保存</button>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<hr>
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>
<hr>
<a href="/static/debug.html" target="_blank">调试工具</a>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>

View File

@@ -2,75 +2,112 @@ $(function(){
// 拉取版本
$.get("/getversion", function(data, status) {
console.log(data, status, data["version"]);
$("#version").text(`(${data.version})`);
$("#version").text(`${data.version}`);
});
// 遍历所有的select元素默认选中只有1个选项的
const autoSelectOne = () => {
$('select').each(function() {
// 如果select元素仅有一个option子元素
if ($(this).children('option').length === 1) {
// 选中这个option
$(this).find('option').prop('selected', true);
}
});
};
function updateCheckbox(selector, mi_did_list, mi_did, mi_hardware_list) {
// 清除现有的内容
$(selector).empty();
// 将 mi_did 字符串通过逗号分割转换为数组,以便于判断默认选中项
var selected_dids = mi_did.split(',');
// 遍历传入的 mi_did_list 和 mi_hardware_list
$.each(mi_did_list, function(index, did) {
// 获取硬件标识,假定列表是一一对应的
var hardware = mi_hardware_list[index];
// 创建复选框元素
var checkbox = $('<input>', {
type: 'checkbox',
id: did,
value: `${did}|${hardware}`,
class: 'custom-checkbox', // 添加样式类
// 如果mi_did中包含了该did则默认选中
checked: selected_dids.indexOf(did) !== -1
});
// 创建标签元素
var label = $('<label>', {
for: did,
class: 'checkbox-label', // 添加样式类
text: `${hardware}${did}` // 设定标签内容为did和hardware的拼接
});
// 将复选框和标签添加到目标选择器元素中
$(selector).append(checkbox).append(label);
});
}
function getSelectedDidsAndHardware(containerSelector) {
var selectedDids = [];
var selectedHardware = [];
// 仅选择给定容器中选中的复选框
$(containerSelector + ' .custom-checkbox:checked').each(function() {
// 解析当前复选框的值(值中包含了 did 和 hardware使用 '|' 分割)
var parts = this.value.split('|');
selectedDids.push(parts[0]);
selectedHardware.push(parts[1]);
});
// 返回包含 did_list 和 hardware_list 的对象
return {
did_list: selectedDids.join(','),
hardware_list: selectedHardware.join(',')
};
}
// 拉取现有配置
$.get("/getsetting", function(data, status) {
console.log(data, status);
updateCheckbox("#mi_did_hardware", data.mi_did_list, data.mi_did, data.mi_hardware_list);
var mi_did_div = $("#mi_did")
mi_did_div.empty();
$.each(data.mi_did_list, function(index, option){
mi_did_div.append($('<option>', {
value:option,
text:option,
}));
if (data.mi_did == option) {
mi_did_div.val(option);
// 初始化显示
for (const key in data) {
if (data.hasOwnProperty(key)) {
const $element = $("#" + key);
if ($element.length && data[key] !== '') {
if (data[key] === true) {
$element.val('true');
} else if (data[key] === false) {
$element.val('false');
} else {
$element.val(data[key]);
}
}
}
});
var mi_hardware_div = $("#mi_hardware")
mi_hardware_div.empty();
$.each(data.mi_hardware_list, function(index, option){
mi_hardware_div.append($('<option>', {
value:option,
text:option,
}));
if (data.mi_hardware == option) {
mi_hardware_div.val(option);
}
});
if (data.xiaomusic_search != "") {
$("#xiaomusic_search").val(data.xiaomusic_search);
}
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);
}
autoSelectOne();
});
$("#save").on("click", () => {
var mi_did = $("#mi_did").val();
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,
};
var setting = $('#setting');
var inputs = setting.find('input, select, textarea');
var data = {};
inputs.each(function() {
var id = this.id;
if (id) {
data[id] = $(this).val();
}
});
var selectedData = getSelectedDidsAndHardware("#mi_did_hardware");
data["mi_did"] = selectedData.did_list;
data["hardware"] = selectedData.hardware_list;
console.log(data)
$.ajax({
type: "POST",
url: "/savesetting",
@@ -86,10 +123,10 @@ $(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 music_list_url = $("#music_list_url").val();
console.log("music_list_url", music_list_url);
var data = {
url: xiaomusic_music_list_url,
url: music_list_url,
};
$.ajax({
type: "POST",
@@ -98,7 +135,7 @@ $(function(){
data: JSON.stringify(data),
success: (res) => {
if (res.ret == "OK") {
$("#xiaomusic_music_list_json").val(res.content);
$("#music_list_json").val(res.content);
} else {
console.log(res);
alert(res.ret);

View File

@@ -1,4 +1,8 @@
button {
.button {
line-height: 50px;
font-size: 14px;
}
button, .button {
margin: 10px;
width: 100px;
height: 50px;
@@ -10,7 +14,7 @@ button {
border-radius: 10px;
background-color: #008CBA;
}
button:active {
button:active, .button:active {
font-weight:bold;
background-color: #007CBA;
transform: translateY(2px);
@@ -73,3 +77,41 @@ footer {
width: 300px;
height: 200px;
}
/* 隐藏原生复选框 */
.custom-checkbox {
display: none;
}
/* 修改后的自定义复选框外观 */
.checkbox-label {
display: inline-block;
width: 200px; /* 宽度 */
height: 20px; /* 高度 */
background-color: #fff; /* 背景颜色 */
border: 0px solid #ccc; /* 边框 */
border-radius: 3px; /* 圆角边框 */
position: relative; /* 设置相对定位 */
cursor: pointer; /* 鼠标形状 */
padding-left: 40px; /* 给左边的复选框图标留下空位 */
}
/* 对勾的样式 */
.custom-checkbox:checked + .checkbox-label::after {
content: '✔';
position: absolute;
left: 10px; /* 对勾图标靠左侧位置 */
color: #000; /* 对勾颜色 */
font-size: 18px; /* 对勾字体大小,视清晰度需调整 */
}
/* 标签文本样式,使用 ::before 伪元素表示复选框未选中时的样式 */
.custom-checkbox + .checkbox-label::before {
content: '⬜'; /* 表示未选中时的复选框样式,这里用了白色方块 */
position: absolute;
left: 8px; /* 方块图标靠左侧位置 */
top: 1px; /* 方块图标顶部位置 */
color: #000; /* 方块颜色 */
font-size: 18px; /* 方块字体大小,视清晰度需调整 */
}

View File

@@ -1,9 +1,12 @@
#!/usr/bin/env python3
from __future__ import annotations
import copy
import difflib
import os
import random
import re
import string
import tempfile
from collections.abc import AsyncIterator
from http.cookies import SimpleCookie
@@ -14,6 +17,8 @@ import mutagen
import requests
from requests.utils import cookiejar_from_dict
from xiaomusic.const import SUPPORT_MUSIC_TYPE
### HELP FUNCTION ###
def parse_cookie_string(cookie_string):
@@ -68,7 +73,12 @@ def validate_proxy(proxy_str: str) -> bool:
# 模糊搜索
def fuzzyfinder(user_input, collection):
return difflib.get_close_matches(user_input, collection, 10, cutoff=0.1)
return difflib.get_close_matches(user_input, collection, n=10, cutoff=0.1)
def find_best_match(user_input, collection, cutoff=0.6):
matches = difflib.get_close_matches(user_input, collection, n=1, cutoff=cutoff)
return matches[0] if matches else None
# 歌曲排序
@@ -152,36 +162,22 @@ def walk_to_depth(root, depth=None, *args, **kwargs):
def downloadfile(url):
# 清理和验证URL
try:
# 解析URL
parsed_url = urlparse(url)
# 解析URL
parsed_url = urlparse(url)
# 基础验证仅允许HTTP和HTTPS协议
if parsed_url.scheme not in ("http", "https"):
return (
f"Invalid URL scheme: {parsed_url.scheme}. Only HTTP and HTTPS are allowed.",
"",
)
# 基础验证仅允许HTTP和HTTPS协议
if parsed_url.scheme not in ("http", "https"):
raise Warning(
f"Invalid URL scheme: {parsed_url.scheme}. Only HTTP and HTTPS are allowed."
)
# 构建目标URL
cleaned_url = parsed_url.geturl()
except Exception as e:
return (f"Invalid URL: {e}", "")
# 构建目标URL
cleaned_url = parsed_url.geturl()
# 发起请求
try:
response = requests.get(cleaned_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 ("Unknown Error", "")
response = requests.get(cleaned_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
return response.text
async def _get_web_music_duration(session, url, start=0, end=500):
@@ -205,6 +201,20 @@ async def _get_web_music_duration(session, url, start=0, end=500):
async def get_web_music_duration(url, start=0, end=500):
duration = 0
try:
parsed_url = urlparse(url)
file_path = parsed_url.path
_, extension = os.path.splitext(file_path)
if extension.lower() not in SUPPORT_MUSIC_TYPE:
cleaned_url = parsed_url.geturl()
async with aiohttp.ClientSession() as session:
async with session.get(
cleaned_url,
allow_redirects=True,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
},
) as response:
url = str(response.url)
# 设置总超时时间为3秒
timeout = aiohttp.ClientTimeout(total=3)
async with aiohttp.ClientSession(timeout=timeout) as session:
@@ -215,7 +225,7 @@ async def get_web_music_duration(url, start=0, end=500):
)
except Exception:
pass
return duration
return duration, url
# 获取文件播放时长
@@ -227,3 +237,34 @@ def get_local_music_duration(filename):
except Exception:
pass
return duration
def get_random(length):
return "".join(random.sample(string.ascii_letters + string.digits, length))
# 深拷贝把敏感数据设置位*
def deepcopy_data_no_sensitive_info(data, fields_to_anonymize=None):
if fields_to_anonymize is None:
fields_to_anonymize = [
"account",
"password",
"httpauth_username",
"httpauth_password",
]
copy_data = copy.deepcopy(data)
# 检查copy_data是否是字典或具有属性的对象
if isinstance(copy_data, dict):
# 对字典进行处理
for field in fields_to_anonymize:
if field in copy_data:
copy_data[field] = "******"
else:
# 对对象进行处理
for field in fields_to_anonymize:
if hasattr(copy_data, field):
setattr(copy_data, field, "******")
return copy_data

View File

@@ -9,26 +9,30 @@ import re
import time
import traceback
import urllib.parse
from logging.handlers import RotatingFileHandler
from pathlib import Path
from aiohttp import ClientSession, ClientTimeout
from miservice import MiAccount, MiIOService, MiNAService
from miservice import MiAccount, MiNAService
from xiaomusic import (
__version__,
)
from xiaomusic.config import (
COOKIE_TEMPLATE,
KEY_MATCH_ORDER,
KEY_WORD_ARG_BEFORE_DICT,
KEY_WORD_DICT,
LATEST_ASK_API,
SUPPORT_MUSIC_TYPE,
Config,
)
from xiaomusic.const import (
COOKIE_TEMPLATE,
LATEST_ASK_API,
SUPPORT_MUSIC_TYPE,
)
from xiaomusic.httpserver import StartHTTPServer
from xiaomusic.plugin import PluginManager
from xiaomusic.utils import (
custom_sort_key,
deepcopy_data_no_sensitive_info,
find_best_match,
fuzzyfinder,
get_local_music_duration,
get_web_music_duration,
@@ -48,29 +52,15 @@ class XiaoMusic:
self.config = config
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.cookie_jar = None
self.device_id = ""
self.mina_service = None
self.miio_service = None
self.polling_event = asyncio.Event()
self.new_record_event = asyncio.Event()
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.device2hardware = {}
self.did2device = {}
# 下载对象
self.download_proc = None
@@ -90,14 +80,11 @@ class XiaoMusic:
# 关机定时器
self._stop_timer = None
# setup logger
logging.basicConfig(
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
datefmt="[%X]",
)
self.log = logging.getLogger("xiaomusic")
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
self.log.debug(config)
# 初始化配置
self.init_config()
# 初始化日志
self.setup_logger()
# 尝试从设置里加载配置
self.try_init_setting()
@@ -108,27 +95,86 @@ class XiaoMusic:
# 启动时初始化获取声音
self.set_last_record("get_volume#")
self.log.info("ffmpeg_location: %s", self.ffmpeg_location)
# 初始化插件
self.plugin_manager = PluginManager(self)
debug_config = deepcopy_data_no_sensitive_info(self.config)
self.log.info(f"Startup OK. {debug_config}")
def init_config(self):
self.music_path = self.config.music_path
self.conf_path = self.config.conf_path
if not self.conf_path:
self.conf_path = self.config.music_path
self.download_path = self.config.download_path
if not self.download_path:
self.download_path = self.music_path
if not os.path.exists(self.download_path):
os.makedirs(self.download_path)
self.hostname = self.config.hostname
self.port = self.config.port
self.public_port = self.config.public_port
if self.public_port == 0:
self.public_port = self.port
self.proxy = self.config.proxy
self.search_prefix = self.config.search_prefix
self.ffmpeg_location = self.config.ffmpeg_location
self.active_cmd = self.config.active_cmd.split(",")
self.exclude_dirs = set(self.config.exclude_dirs.split(","))
self.music_path_depth = self.config.music_path_depth
def setup_logger(self):
log_format = f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s"
date_format = "[%X]"
formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
logging.basicConfig(
format=log_format,
datefmt=date_format,
)
log_file = self.config.log_file
log_path = os.path.dirname(log_file)
if not os.path.exists(log_path):
os.makedirs(log_path)
if os.path.exists(log_file):
os.remove(log_file)
handler = RotatingFileHandler(
self.config.log_file, maxBytes=10 * 1024 * 1024, backupCount=1
)
handler.setFormatter(formatter)
self.log = logging.getLogger("xiaomusic")
self.log.addHandler(handler)
self.log.setLevel(logging.DEBUG if self.config.verbose else logging.INFO)
async def poll_latest_ask(self):
async with ClientSession() as session:
session._cookie_jar = self.cookie_jar
while True:
self.log.debug(
"Listening new message, timestamp: %s", self.last_timestamp
f"Listening new message, timestamp: {self.last_timestamp}"
)
await self.get_latest_ask_from_xiaoai(session)
session._cookie_jar = self.cookie_jar
# 拉取所有音箱的对话记录
tasks = [
self.get_latest_ask_from_xiaoai(session, device_id)
for device_id in self.device2hardware
]
await asyncio.gather(*tasks)
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()
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(f"Sleep {d}, timestamp: {self.last_timestamp}")
await asyncio.sleep(1 - d)
async def init_all_data(self, session):
await self.login_miboy(session)
await self._init_data_hardware()
await self.try_update_device_id()
cookie_jar = self.get_cookie()
if cookie_jar:
session.cookie_jar.update_cookies(cookie_jar)
@@ -144,61 +190,26 @@ class XiaoMusic:
# Forced login to refresh to refresh token
await account.login("micoapi")
self.mina_service = MiNAService(account)
self.miio_service = MiIOService(account)
async def try_update_device_id(self):
# 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
try:
mi_dids = self.config.mi_did.split(",")
hardware_data = await self.mina_service.device_list()
self.device2hardware = {}
self.did2device = {}
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:
self.log.error(
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
)
device = h.get("deviceID", "")
hardware = h.get("hardware", "")
did = h.get("miotDID", "")
if device and hardware and did and (did in mi_dids):
self.device2hardware[device] = hardware
self.did2device[did] = device
except Exception as e:
self.log.error(f"Execption {e}")
async def _init_data_hardware(self):
if self.config.cookie:
# if use cookie do not need init
return
await self.try_update_device_id()
if not self.config.mi_did:
devices = await self.miio_service.device_list()
try:
self.config.mi_did = next(
d["did"]
for d in devices
if d["model"].endswith(self.config.hardware.lower())
)
except StopIteration:
self.log.error(
f"cannot find did for hardware: {self.config.hardware} "
"please set it via MI_DID env"
)
except Exception as e:
self.log.error(f"Execption init hardware {e}")
def get_cookie(self):
if self.config.cookie:
cookie_jar = parse_cookie_string(self.config.cookie)
# set attr from cookie fix #134
cookie_dict = cookie_jar.get_dict()
self.device_id = cookie_dict["deviceId"]
return cookie_jar
if not os.path.exists(self.mi_token_home):
@@ -209,12 +220,18 @@ class XiaoMusic:
user_data = json.loads(f.read())
user_id = user_data.get("userId")
service_token = user_data.get("micoapi")[1]
device_id = self.get_one_device()
cookie_string = COOKIE_TEMPLATE.format(
device_id=self.device_id, service_token=service_token, user_id=user_id
device_id=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):
def get_one_device(self):
device_id = next(iter(self.device2hardware), "")
return device_id
async def get_latest_ask_from_xiaoai(self, session, device_id):
cookies = {"deviceId": device_id}
retries = 3
for i in range(retries):
try:
@@ -224,7 +241,7 @@ class XiaoMusic:
timestamp=str(int(time.time() * 1000)),
)
self.log.debug(f"url:{url}")
r = await session.get(url, timeout=timeout)
r = await session.get(url, timeout=timeout, cookies=cookies)
except Exception as e:
self.log.warning(
"Execption when get latest ask from xiaoai: %s", str(e)
@@ -239,18 +256,21 @@ class XiaoMusic:
self.log.info("Maybe outof date trying to re init it")
await self.init_all_data(self.session)
else:
return self._get_last_query(data)
return self._get_last_query(device_id, data)
def _get_last_query(self, data):
self.log.debug(f"_get_last_query:{data}")
def _get_last_query(self, device_id, data):
self.log.debug(f"_get_last_query device_id:{device_id} data:{data}")
if d := data.get("data"):
records = json.loads(d).get("records")
if not records:
return
last_record = records[0]
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.new_record_event.set()
@@ -263,39 +283,104 @@ class XiaoMusic:
self.new_record_event.set()
async def do_tts(self, value):
self.log.info("do_tts: %s", value)
self.log.info(f"try do_tts value:{value}")
if not value:
self.log.info("do_tts no value")
return
await self.force_stop_xiaoai()
await self.text_to_speech(value)
# 最大等8秒
sec = min(8, int(len(value) / 3))
await asyncio.sleep(sec)
self.log.info(f"do_tts ok. cur_music:{self.cur_music}")
await self.check_replay()
async def text_to_speech_one(self, device_id, value):
try:
await self.mina_service.text_to_speech(self.device_id, value)
await self.mina_service.text_to_speech(device_id, value)
except Exception as e:
self.log.error(f"Execption {e}")
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
if self._playing and not self.is_downloading():
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):
if self.isplaying() and not self.isdownloading():
# 继续播放歌曲
self.log.info("现在继续播放歌曲")
await self.play()
else:
self.log.info(
f"不会继续播放歌曲. isplaying:{self.isplaying()} isdownloading:{self.isdownloading()}"
)
async def do_set_volume(self, value):
value = int(value)
self._volume = value
self.log.info(f"声音设置为{value}")
await self.player_set_volume(value)
async def player_set_volume(self, value):
try:
await self.mina_service.player_set_volume(self.device_id, value)
for device_id in self.device2hardware:
await self.mina_service.player_set_volume(device_id, value)
except Exception as e:
self.log.error(f"Execption {e}")
async def get_if_xiaoai_is_playing(self, device_id):
playing_info = await self.mina_service.player_get_status(device_id)
self.log.info(playing_info)
# WTF xiaomi api
is_playing = (
json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1)
== 1
)
return is_playing
async def stop_if_xiaoai_is_playing(self, device_id):
is_playing = await self.get_if_xiaoai_is_playing(device_id)
if is_playing or self.config.enable_force_stop:
# stop it
ret = await self.mina_service.player_stop(device_id)
self.log.info(
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_one_xiaoai(self, device_id):
try:
ret = await self.mina_service.player_pause(device_id)
self.log.info(
f"force_stop_one_xiaoai player_pause device_id:{device_id} ret:{ret}"
)
await self.stop_if_xiaoai_is_playing(device_id)
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)
tasks = [
self.force_stop_one_xiaoai(device_id) for device_id in self.device2hardware
]
await asyncio.gather(*tasks)
# 是否在下载中
def is_downloading(self):
def isdownloading(self):
if not self.download_proc:
return False
if (
self.download_proc.returncode is not None
and self.download_proc.returncode < 0
):
if self.download_proc.returncode is not None:
self.log.info(
f"Process exited with returncode:{self.download_proc.returncode}"
)
return False
self.log.info("Download Process is still running.")
return True
# 下载歌曲
@@ -313,7 +398,7 @@ class XiaoMusic:
"--audio-format",
"mp3",
"--paths",
self.music_path,
self.download_path,
"-o",
f"{name}.mp3",
"--ffmpeg-location",
@@ -324,7 +409,8 @@ class XiaoMusic:
if self.proxy:
sbp_args += ("--proxy", f"{self.proxy}")
self.log.info(f"download: {sbp_args}")
cmd = " ".join(sbp_args)
self.log.info(f"download cmd: {cmd}")
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
await self.do_tts(f"正在下载歌曲{search_key}")
@@ -360,19 +446,40 @@ class XiaoMusic:
url = self._all_music[name]
return url.startswith(("http://", "https://"))
# 获取歌曲播放地址
# 获取歌曲播放时长,播放地址
async def get_music_sec_url(self, name):
sec = 0
url = self.get_music_url(name)
if self.is_web_radio_music(name):
self.log.info("电台不会有播放时长")
return 0, url
if self.is_web_music(name):
origin_url = url
duration, url = await get_web_music_duration(url)
sec = int(duration)
self.log.info(f"网络歌曲 {name} : {origin_url} {url} 的时长 {sec}")
else:
filename = self.get_filename(name)
sec = int(get_local_music_duration(filename))
self.log.info(f"本地歌曲 {name} : {filename} {url} 的时长 {sec}")
if sec <= 0:
self.log.warning(f"获取歌曲时长失败 {name} {url}")
return sec, url
def get_music_url(self, name):
if self.is_web_music(name):
url = self._all_music[name]
self.log.debug("get_music_url web music. name:%s, url:%s", name, url)
return url
filename = self.get_filename(name)
filename = self.get_filename(name).replace("\\", "/")
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}"
return f"http://{self.hostname}:{self.public_port}/{encoded_name}"
# 递归获取目录下所有歌曲,生成随机播放列表
def _gen_all_music_list(self):
@@ -428,6 +535,7 @@ class XiaoMusic:
if not self.config.music_list_json:
return
self._all_radio = {}
music_list = json.loads(self.config.music_list_json)
try:
for item in music_list:
@@ -448,7 +556,7 @@ class XiaoMusic:
# 处理电台列表
if music_type == "radio":
self._all_radio[name] = url
self.log.info(one_music_list)
self.log.debug(one_music_list)
# 歌曲名字相同会覆盖
self._music_list[list_name] = one_music_list
if self._all_radio:
@@ -468,7 +576,7 @@ class XiaoMusic:
# 把下载的音乐加入播放列表
def add_download_music(self, name):
self._all_music[name] = os.path.join(self.music_path, f"{name}.mp3")
self._all_music[name] = os.path.join(self.download_path, f"{name}.mp3")
if name not in self._play_list:
self._play_list.append(name)
self.log.debug("add_music %s", name)
@@ -497,29 +605,13 @@ class XiaoMusic:
return name
# 设置下一首歌曲的播放定时器
async def set_next_music_timeout(self):
name = self.cur_music
if self.is_web_radio_music(name):
self.log.info("电台不会有下一首的定时器")
async def set_next_music_timeout(self, sec):
if sec <= 0:
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.log.info("定时器已取消")
self._timeout = sec
@@ -531,7 +623,7 @@ class XiaoMusic:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
self._next_timer = asyncio.ensure_future(_do_next())
self.log.info(f"{sec}秒后将会播放下一首")
self.log.info(f"{sec}秒后将会播放下一首歌曲")
async def run_forever(self):
StartHTTPServer(self.port, self.music_path, self)
@@ -541,10 +633,11 @@ class XiaoMusic:
task = asyncio.create_task(self.poll_latest_ask())
assert task is not None # to keep the reference to task, do not remove this
filtered_keywords = [
keyword for keyword in KEY_MATCH_ORDER if "#" not in keyword
keyword for keyword in self.config.key_match_order if "#" not in keyword
]
joined_keywords = "/".join(filtered_keywords)
self.log.info(f"Running xiaomusic now, 用`{joined_keywords}`开头来控制")
self.log.info(self.config.key_word_dict)
while True:
self.polling_event.set()
@@ -563,23 +656,46 @@ class XiaoMusic:
self.polling_event.clear() # stop polling when processing the question
query = new_record.get("query", "").strip()
ctrl_panel = new_record.get("ctrl_panel", False)
self.log.debug("收到消息:%s 控制面板:%s", query, ctrl_panel)
self.log.info("收到消息:%s 控制面板:%s", query, ctrl_panel)
# 匹配命令
opvalue, oparg = self.match_cmd(query, ctrl_panel)
if not opvalue:
await asyncio.sleep(1)
continue
try:
opvalue, oparg = self.match_cmd(query, ctrl_panel)
if not opvalue:
await asyncio.sleep(1)
await self.check_replay()
continue
func = getattr(self, opvalue)
await func(arg1=oparg)
except Exception as e:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
# 检查是否匹配到完全一样的指令
def check_full_match_cmd(self, query, ctrl_panel):
if query in self.config.key_match_order:
opkey = query
opvalue = self.config.key_word_dict.get(opkey)
if ctrl_panel or self.isplaying():
return opvalue
else:
if not self.active_cmd or opvalue in self.active_cmd:
return opvalue
return None
# 匹配命令
def match_cmd(self, query, ctrl_panel):
for opkey in KEY_MATCH_ORDER:
# 优先处理完全匹配
opvalue = self.check_full_match_cmd(query, ctrl_panel)
if opvalue:
self.log.info(f"完全匹配指令. query:{query} opvalue:{opvalue}")
# 自定义口令
if opvalue.startswith("exec#"):
code = opvalue.split("#", 1)[1]
return ("exec", code)
return (opvalue, "")
for opkey in self.config.key_match_order:
patternarg = rf"(.*){opkey}(.*)"
# 匹配参数
matcharg = re.match(patternarg, query)
@@ -596,20 +712,16 @@ class XiaoMusic:
argafter,
)
oparg = argafter
opvalue = KEY_WORD_DICT[opkey]
if not ctrl_panel and not self._playing:
if self.active_cmd and opvalue not in self.active_cmd:
self.log.debug(f"不在激活命令中 {opvalue}")
continue
if opkey in KEY_WORD_ARG_BEFORE_DICT:
oparg = argpre
self.log.info(
"匹配到指令. opkey:%s opvalue:%s oparg:%s", opkey, opvalue, oparg
)
opvalue = self.config.key_word_dict.get(opkey)
if not ctrl_panel and not self.isplaying():
if self.active_cmd and opvalue not in self.active_cmd:
self.log.ifno(f"不在激活命令中 {opvalue}")
continue
self.log.info(f"匹配到指令. opkey:{opkey} opvalue:{opvalue} oparg:{oparg}")
return (opvalue, oparg)
if self._playing:
self.log.info("未匹配到指令,自动停止")
return ("stop", {})
self.log.info(f"未匹配到指令 {query} {ctrl_panel}")
return (None, None)
# 判断是否播放下一首歌曲
@@ -623,6 +735,80 @@ class XiaoMusic:
return True
return False
async def play_url(self, **kwargs):
url = kwargs.get("arg1", "")
return await self.all_player_play(url)
async def play_one_url(self, device_id, url):
try:
if self.config.use_music_api:
ret = await self.play_by_music_url(device_id, url)
self.log.info(
f"play_one_url play_by_music_url device_id:{device_id} ret:{ret} url:{url}"
)
else:
ret = await self.mina_service.play_by_url(device_id, url)
self.log.info(
f"play_one_url play_by_url device_id:{device_id} ret:{ret} url:{url}"
)
except Exception as 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):
if not self.config.enable_fuzzy_match:
self.log.debug("没开启模糊匹配")
return name
all_music_list = list(self._all_music.keys())
real_name = find_best_match(
name, all_music_list, cutoff=self.config.fuzzy_match_cutoff
)
if real_name:
self.log.info(f"根据【{name}】找到歌曲【{real_name}")
return real_name
self.log.info(f"没找到歌曲【{name}")
return name
# 播放本地歌曲
async def playlocal(self, **kwargs):
name = kwargs.get("arg1", "")
if name == "":
if self.check_play_next():
await self.play_next()
return
else:
name = self.cur_music
self.log.info(f"playlocal. name:{name}")
# 本地歌曲不存在时下载
name = self.find_real_music_name(name)
if not self.is_music_exist(name):
await self.do_tts(f"本地不存在歌曲{name}")
return
await self._playmusic(name)
async def _playmusic(self, name):
self._playing = True
self.cur_music = name
self.log.info(f"cur_music {self.cur_music}")
sec, url = await self.get_music_sec_url(name)
await self.force_stop_xiaoai()
self.log.info(f"播放 {url}")
await self.play_url(arg1=url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
await self.set_next_music_timeout(sec)
# 播放歌曲
async def play(self, **kwargs):
parts = kwargs.get("arg1", "").split("|")
@@ -641,26 +827,17 @@ class XiaoMusic:
self.log.info("play. search_key:%s name:%s", search_key, name)
# 本地歌曲不存在时下载
name = self.find_real_music_name(name)
if not self.is_music_exist(name):
if self.config.disable_download:
await self.do_tts(f"本地不存在歌曲{name}")
return
await self.download(search_key, name)
self.log.info("正在下载中 %s", search_key + ":" + name)
self.log.info(f"正在下载中 {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_music_url(name)
self.log.info("播放 %s", url)
await self.force_stop_xiaoai()
await self.mina_service.play_by_url(self.device_id, url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
await self.set_next_music_timeout()
await self._playmusic(name)
# 下一首
async def play_next(self, **kwargs):
@@ -712,10 +889,27 @@ class XiaoMusic:
self.log.error(f"del ${filename} failed")
self._gen_all_music_list()
def find_real_music_list_name(self, list_name):
if not self.config.enable_fuzzy_match:
self.log.debug("没开启模糊匹配")
return list_name
# 模糊搜一个播放列表
real_name = find_best_match(
list_name, self._music_list, cutoff=self.config.fuzzy_match_cutoff
)
if real_name:
self.log.info(f"根据【{list_name}】找到播放列表【{real_name}")
list_name = real_name
self.log.info(f"没找到播放列表【{list_name}")
return list_name
# 播放一个播放列表
async def play_music_list(self, **kwargs):
parts = kwargs.get("arg1").split("|")
list_name = parts[0]
list_name = self.find_real_music_list_name(list_name)
if list_name not in self._music_list:
await self.do_tts(f"播放列表{list_name}不存在")
return
@@ -733,10 +927,13 @@ class XiaoMusic:
async def stop(self, **kwargs):
self._playing = False
if kwargs.get("arg1", "") != "notts":
await self.do_tts(self.config.stop_tts_msg)
if self._next_timer:
self._next_timer.cancel()
self.log.info("定时器已取消")
await self.force_stop_xiaoai()
self.log.info("stop now")
async def stop_after_minute(self, **kwargs):
if self._stop_timer:
@@ -747,22 +944,24 @@ class XiaoMusic:
async def _do_stop():
await asyncio.sleep(minute * 60)
try:
await self.stop()
await self.stop(arg1="notts")
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}分钟后将关机")
await self.do_tts(f"收到,{minute}分钟后将关机")
async def set_volume(self, **kwargs):
value = kwargs.get("arg1", 0)
await self.do_set_volume(value)
async def get_volume(self, **kwargs):
playing_info = await self.mina_service.player_get_status(self.device_id)
# 取一个音箱的声音
device_id = self.get_one_device()
playing_info = await self.mina_service.player_get_status(device_id)
self.log.debug("get_volume. playing_info:%s", playing_info)
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
"volume", 5
"volume", 0
)
self.log.info("get_volume. volume:%s", self._volume)
@@ -827,20 +1026,24 @@ class XiaoMusic:
await self.call_main_thread_function(self.reinit)
def update_config_from_setting(self, data):
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
self.log.info("update_config_from_setting ok. data:%s", data)
# 自动赋值相同字段的配置
self.config.update_config(data)
self.init_config()
debug_config = deepcopy_data_no_sensitive_info(self.config)
self.log.info("update_config_from_setting ok. data:%s", debug_config)
# 重新初始化
async def reinit(self, **kwargs):
await self.try_update_device_id()
self.setup_logger()
await self.init_all_data(self.session)
self._gen_all_music_list()
self.log.info("reinit success")
@@ -848,14 +1051,17 @@ class XiaoMusic:
async def getalldevices(self, **kwargs):
did_list = []
hardware_list = []
hardware_data = await self.mina_service.device_list()
for h in hardware_data:
did = h.get("miotDID", "")
if did != "":
did_list.append(did)
hardware = h.get("hardware", "")
if h.get("hardware", "") != "":
hardware_list.append(hardware)
try:
hardware_data = await self.mina_service.device_list()
for h in hardware_data:
did = h.get("miotDID", "")
if did != "":
did_list.append(did)
hardware = h.get("hardware", "")
if h.get("hardware", "") != "":
hardware_list.append(hardware)
except Exception as e:
self.log.error(f"Execption {e}")
alldevices = {
"did_list": did_list,
"hardware_list": hardware_list,
@@ -877,3 +1083,63 @@ class XiaoMusic:
self.new_record_event.set()
result = await future
return result
async def play_by_music_url(self, deviceId, url, _type=2):
self.log.info(f"play_by_music_url url:{url}, type:{_type}")
audio_type = ""
if _type == 1:
# If set to MUSIC, the light will be on
audio_type = "MUSIC"
audio_id = self.config.use_music_audio_id
id = self.config.use_music_id
music = {
"payload": {
"audio_type": audio_type,
"audio_items": [
{
"item_id": {
"audio_id": audio_id,
"cp": {
"album_id": "-1",
"episode_index": 0,
"id": id,
"name": "xiaowei",
},
},
"stream": {"url": url},
}
],
"list_params": {
"listId": "-1",
"loadmore_offset": 0,
"origin": "xiaowei",
"type": "MUSIC",
},
},
"play_behavior": "REPLACE_ALL",
}
data = {"startaudioid": audio_id, "music": json.dumps(music)}
self.log.info(json.dumps(data))
return await self.mina_service.ubus_request(
deviceId,
"player_play_music",
"mediaplayer",
data,
)
async def debug_play_by_music_url(self, arg1=None):
if arg1 is None:
arg1 = {}
data = arg1
device_id = self.get_one_device()
self.log.info(f"debug_play_by_music_url: {data} {device_id}")
return await self.mina_service.ubus_request(
device_id,
"player_play_music",
"mediaplayer",
data,
)
async def exec(self, arg1=None):
code = arg1 if arg1 else 'code1("hello")'
await self.plugin_manager.execute_plugin(code)