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

Compare commits

..

97 Commits

Author SHA1 Message Date
涵曦
e4efa0d879 bump: version 0.3.35 → 0.3.36 2024-09-19 07:08:47 +08:00
涵曦
2ebc0f11d2 build: update static version 2024-09-19 07:08:46 +08:00
52fisher
180f28800e feat: Pure 主题更新 (#178)
* 使用hash路由以改进后端服务器无法返回正确* 路由的结果
* 更新关于界面
* 播放列表界面优化
* 增加欢迎页的功能说明
2024-09-19 06:48:19 +08:00
涵曦
7f5692d6cd feat: 支持配置获取对话记录间隔时间 #169 2024-09-19 00:45:08 +08:00
涵曦
8d7b5337eb feat: 允许在后台设置监听端口 2024-09-18 23:32:00 +08:00
hui
dcbf4330be fix: 修复开启继续播放时歌曲播放不完整问题 (#177) 2024-09-18 20:14:55 +08:00
涵曦
2e53f20d80 Update README.md 2024-09-18 12:01:02 +08:00
涵曦
2914ddcc36 bump: version 0.3.34 → 0.3.35 2024-09-18 07:17:12 +08:00
涵曦
442756e3cc build: update static version 2024-09-18 07:17:11 +08:00
涵曦
b55a2a67c9 feat: 允许跨域访问 #172 2024-09-18 06:57:49 +08:00
52fisher
09545c7015 fix: 修复 Pure 主题白屏无法打开的问题 (#176)
* 修复 白屏无法打开的问题
* 增加清空已缓存的设置和拉取最新设置功能
* 增加刷新页面按钮及说明
2024-09-18 06:48:38 +08:00
涵曦
1f82efa2a1 bump: version 0.3.33 → 0.3.34 2024-09-18 01:08:13 +08:00
涵曦
e509052242 build: update static version 2024-09-18 01:08:12 +08:00
涵曦
046cdade5a fix: 主页适配移动端 2024-09-18 00:32:53 +08:00
涵曦
7f8e639b86 Update README.md 2024-09-17 19:46:02 +08:00
涵曦
bfbd36f7f9 feat: 新增 pure 主题 vue + elementUI (#172)
* allow CROS

* feat: vue + elementUI

* fixed 设置后打开主页白屏的问题

* fixed 设置中获取device_list失败的问题

* Update Theme

* Update Theme

---------

Co-authored-by: 52fisher <32198215+52fisher@users.noreply.github.com>
Co-authored-by: fisher <i@qnmlgb.trade>
2024-09-17 19:42:58 +08:00
涵曦
c5d623547c Update README.md 2024-09-17 01:53:22 +08:00
涵曦
31c61675bf refactor: 优化代码:输入框处理抖动问题,网页播放修改实现方式 see #166 2024-09-17 01:17:38 +08:00
涵曦
9900bd9ee9 fix: 修复网页播放点击后没有关闭旧声音的问题 #166 2024-09-16 22:35:36 +08:00
涵曦
8459494c61 fix: 修复单曲循环的情况下歌曲不在当前播放列表时失效的情况 2024-09-16 21:43:49 +08:00
涵曦
9852feec81 bump: version 0.3.32 → 0.3.33 2024-09-15 23:55:45 +08:00
涵曦
ce79c0f0f7 build: update static version 2024-09-15 23:55:44 +08:00
涵曦
51037fc714 Update README.md 2024-09-15 23:47:15 +08:00
涵曦
f6b9178688 Update README.md 2024-09-15 23:40:12 +08:00
涵曦
7ccbd6ce79 refactor: 优化谷歌统计 2024-09-15 23:10:00 +08:00
涵曦
30926c6b79 feat: 调整页面布局 2024-09-15 22:53:59 +08:00
涵曦
270076b9a7 fix: #168 安全优化: 设置数据接口密码隐藏处理 2024-09-15 15:51:44 +08:00
hui
ba58d45d8b feat: 支持继续播放 (#171) 2024-09-15 14:44:47 +08:00
涵曦
0543c92f37 Update README.md 2024-09-15 10:03:29 +08:00
涵曦
f40a4c5c7b Update README.md 2024-09-15 09:13:07 +08:00
涵曦
cc05933992 Update README.md 2024-09-14 19:59:05 +08:00
涵曦
896eae92ff fix: 修复谷歌统计报错问题 2024-09-14 15:08:38 +08:00
涵曦
07676e8c5d Update README.md 2024-09-14 12:13:12 +08:00
涵曦
4ec70a210b bump: version 0.3.31 → 0.3.32 2024-09-14 08:37:30 +08:00
涵曦
0b395f26ed build: update static version 2024-09-14 08:37:30 +08:00
涵曦
965a8be5bb Update README.md 2024-09-13 20:42:30 +08:00
hui
36ddfc8885 fix: 优化audio_id查询方式 (#165) 2024-09-13 20:14:40 +08:00
涵曦
f82957c73f Update README.md 2024-09-13 18:49:17 +08:00
涵曦
f1625e7d92 fix: 播放链接接口支持复杂的链接 2024-09-13 08:55:33 +08:00
涵曦
48797ddf8f feat: 新增谷歌统计 2024-09-12 20:06:32 +08:00
hui
6f67f515b2 feat: 增加播放进度 (#160)
* fix: windows下保存配置失败

* feat: 增加播放进度
2024-09-12 17:46:21 +08:00
涵曦
9a0146b04e Update README.md 2024-09-12 08:45:20 +08:00
涵曦
7fdb28c352 Update README.md 2024-09-11 17:36:50 +08:00
涵曦
5619584481 bump: version 0.3.30 → 0.3.31 2024-09-10 16:42:15 +00:00
涵曦
3f0a1cb8f5 build: update static version 2024-09-10 16:42:14 +00:00
涵曦
2f6105843b fix: 修复插件示例报错 #105 2024-09-10 16:24:11 +00:00
涵曦
d71f99de53 feat: 新增播放上一首歌曲功能 #90 2024-09-10 16:13:44 +00:00
涵曦
d7385405d9 fix: 修复当前播放歌曲没保存的问题 #90 2024-09-10 14:38:14 +00:00
涵曦
781e5ebb2f feat: 新增所有歌曲列表 2024-09-10 06:42:16 +00:00
Yan Zenghui
980772bf9c feat: 触屏版显示歌曲名称 (#156)
* feat:触屏版显示歌曲名称

* fix:修改日志级别

* fix:修改歌曲名称获取方式
2024-09-09 17:32:02 +08:00
涵曦
632e411c6e Update README.md 2024-09-08 16:43:51 +08:00
涵曦
789d442029 Update README.md 2024-09-08 16:41:02 +08:00
涵曦
0452d49930 Update README.md 2024-09-08 16:39:28 +08:00
涵曦
19d781fa1f Update README.md 2024-09-08 14:30:40 +08:00
涵曦
ad82d13a7e Update README.md 2024-09-08 14:28:28 +08:00
涵曦
9c4d757dc0 bump: version 0.3.29 → 0.3.30 2024-09-07 14:34:42 +00:00
涵曦
e369d80875 build: update static version 2024-09-07 14:34:41 +00:00
涵曦
9b0c8510a3 feat: 修改设置按钮位置 2024-09-07 13:58:20 +00:00
涵曦
94fb158d7d Update README.md 2024-09-07 10:02:47 +08:00
涵曦
11df6e9f0c Update README.md 2024-09-07 09:51:21 +08:00
涵曦
4e8550a56c Update README.md 2024-09-07 09:39:56 +08:00
涵曦
7f349410a0 feat: 新增网页播放接口 #138 2024-09-07 00:16:55 +00:00
涵曦
6610c29fe4 Update README.md 2024-09-07 07:42:10 +08:00
涵曦
3da1b8eac1 Update README.md 2024-09-07 07:38:07 +08:00
涵曦
003068e62c Update README.md 2024-09-07 07:34:50 +08:00
涵曦
1d12f0d508 Update README.md 2024-09-07 07:29:56 +08:00
涵曦
1ee4667a79 Update README.md 2024-09-07 02:36:16 +08:00
涵曦
521605e9c8 Update README.md 2024-09-07 02:31:35 +08:00
涵曦
9add14408c bump: version 0.3.28 → 0.3.29 2024-09-06 18:07:48 +00:00
涵曦
e554ace7ae build: update static version 2024-09-06 18:07:47 +00:00
涵曦
cdd0cdd237 Update README.md 2024-09-07 01:40:18 +08:00
涵曦
c713d32230 Update README.md 2024-09-07 01:37:49 +08:00
涵曦
8ee4e88f82 Update README.md 2024-09-07 01:34:47 +08:00
涵曦
ca7679e9d3 Update README.md 2024-09-07 01:19:09 +08:00
涵曦
b8c18ef33b feat: 设置页面新增接口文档入口 2024-09-06 17:17:17 +00:00
涵曦
1ce324151e fix: 修复网页开启秘密验证无法播歌的问题 #149 2024-09-06 17:13:35 +00:00
涵曦
faa452253c Update README.md 2024-09-07 00:29:47 +08:00
涵曦
ae41ae57b3 Update README.md 2024-09-04 18:52:57 +08:00
涵曦
6d3fe9381d Update README.md 2024-09-04 18:43:48 +08:00
涵曦
f879c0aeb9 Update release.yml 2024-09-04 06:56:59 +08:00
涵曦
58ffb93d3f Update release.yml 2024-09-04 06:56:38 +08:00
涵曦
8e5df7094c bump: version 0.3.27 → 0.3.28 2024-09-03 16:35:08 +00:00
涵曦
64c2f54ff0 build: update static version 2024-09-03 16:35:07 +00:00
涵曦
d1b869ae43 feat: 新增歌曲收藏功能 #87 2024-09-03 15:55:19 +00:00
涵曦
d3895f2632 refactor: ffmpeg_location 从配置里读取 2024-09-03 14:33:20 +00:00
hui
5bf62c4b1a fix: docker下minetypes无法判断m4a 2024-09-03 18:33:57 +08:00
hui
406e09922c fix:m4a无法正确获取播放时长 2024-09-03 18:33:57 +08:00
hui
ae34572d13 fix:指定文件编码,修复windows下的文件读取错误 2024-09-03 18:33:57 +08:00
涵曦
1e3c69ea90 Update release.yml 2024-09-02 09:09:49 +08:00
涵曦
3c232505f8 bump: version 0.3.26 → 0.3.27 2024-09-02 00:37:48 +00:00
涵曦
44177db9b6 build: update static version 2024-09-02 00:37:47 +00:00
涵曦
e72ae973bc refactor: 处理 code review 问题' 2024-09-01 16:09:33 +00:00
Hi-Jiajun
4ab3c5cbee feat: Add feature as requested in issue #143 2024-09-01 16:09:20 +00:00
涵曦
4e532d298d fix: 默认下载目录修改 2024-08-18 02:39:22 +00:00
涵曦
3372440f4e bump: version 0.3.25 → 0.3.26 2024-08-17 15:00:11 +00:00
涵曦
1255239912 build: update static version 2024-08-17 15:00:10 +00:00
涵曦
e401a73595 feat: 删除网关模式 2024-08-17 11:22:23 +00:00
33 changed files with 1140 additions and 564 deletions

View File

@@ -6,7 +6,7 @@ permissions:
on:
push:
tags:
- "*"
- "v*"
workflow_dispatch:
jobs:
@@ -17,24 +17,25 @@ jobs:
contents: write
id-token: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pdm-project/setup-pdm@v3
- name: Publish package distributions to PyPI
run: pdm publish
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: 16.x
registry-url: https://registry.npmjs.org/
node-version: lts/*
- run: npx changelogithub
continue-on-error: true
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- uses: pdm-project/setup-pdm@v3
- name: Publish package distributions to PyPI
run: pdm publish
build-image:
runs-on: ubuntu-latest
#needs: release-pypi

2
.gitignore vendored
View File

@@ -25,7 +25,7 @@ share/python-wheels/
.installed.cfg
*.egg
MANIFEST
*_bak/
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.

View File

@@ -1,3 +1,133 @@
## v0.3.36 (2024-09-19)
### Feat
- Pure 主题更新 (#178)
- 支持配置获取对话记录间隔时间 #169
- 允许在后台设置监听端口
### Fix
- 修复开启继续播放时歌曲播放不完整问题 (#177)
## v0.3.35 (2024-09-18)
### Feat
- 允许跨域访问 #172
### Fix
- 修复 Pure 主题白屏无法打开的问题 (#176)
## v0.3.34 (2024-09-18)
### Feat
- 新增 pure 主题 vue + elementUI (#172)
### Fix
- 主页适配移动端
- 修复网页播放点击后没有关闭旧声音的问题 #166
- 修复单曲循环的情况下歌曲不在当前播放列表时失效的情况
### Refactor
- 优化代码:输入框处理抖动问题,网页播放修改实现方式 see #166
## v0.3.33 (2024-09-15)
### Feat
- 调整页面布局
- 支持继续播放 (#171)
### Fix
- #168 安全优化: 设置数据接口密码隐藏处理
- 修复谷歌统计报错问题
### Refactor
- 优化谷歌统计
## v0.3.32 (2024-09-14)
### Feat
- 新增谷歌统计
- 增加播放进度 (#160)
### Fix
- 优化audio_id查询方式 (#165)
- 播放链接接口支持复杂的链接
## v0.3.31 (2024-09-10)
### Feat
- 新增播放上一首歌曲功能 #90
- 新增所有歌曲列表
- 触屏版显示歌曲名称 (#156)
### Fix
- 修复插件示例报错 #105
- 修复当前播放歌曲没保存的问题 #90
## v0.3.30 (2024-09-07)
### Feat
- 修改设置按钮位置
- 新增网页播放接口 #138
## v0.3.29 (2024-09-06)
### Feat
- 设置页面新增接口文档入口
### Fix
- 修复网页开启秘密验证无法播歌的问题 #149
## v0.3.28 (2024-09-03)
### Feat
- 新增歌曲收藏功能 #87
### Fix
- docker下minetypes无法判断m4a
### Refactor
- ffmpeg_location 从配置里读取
## v0.3.27 (2024-09-02)
### Feat
- Add feature as requested in issue #143
### Fix
- 默认下载目录修改
### Refactor
- 处理 code review 问题'
## v0.3.26 (2024-08-17)
### Feat
- 删除网关模式
## v0.3.25 (2024-08-16)
### Feat

263
README.md
View File

@@ -13,11 +13,17 @@
<https://github.com/hanxi/xiaomusic>
> 初次安装遇到问题请查阅 <https://github.com/hanxi/xiaomusic/issues/99> 上是否已经有解决办法。
> 初次安装遇到问题请查阅 FAQ: <https://github.com/hanxi/xiaomusic/issues/99> 上是否已经有解决办法。
## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下
已经支持在 web 页面配置其他参数docker 启动命令如下:
```bash
docker run -p 8090:8090 -v /xiaomusic/music:/app/music -v /xiaomusic/conf:/app/conf hanxi/xiaomusic
```
对应的 docker compose 配置如下:
```yaml
services:
@@ -28,24 +34,19 @@ services:
ports:
- 8090:8090
volumes:
- ./music:/app/music
- ./conf:/app/conf
```
对应的 docker 启动命令如下:
```yaml
docker run -p 8090:8090 \
-v ./music:/app/music \
-v ./conf:/app/conf \
hanxi/xiaomusic
- /xiaomusic/music:/app/music
- /xiaomusic/conf:/app/conf
```
其中 conf 目录为配置文件存放目录music 目录为音乐存放目录,建议分开配置为不同的目录。
启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表
> 上面配置的 /xiaomusic/music 和 /xiaomusic/conf 是 docker 主机里的 /xiaomusic 目录下的,可以修改为其他目录。如果报错找不到 /xiaomusic/music 目录,可以先执行 `mkdir -p /xiaomusic/{music,conf}` 命令新建目录
### ✨✨✨ 修改默认8090端口映射 ✨✨✨
docker 和 docker compose 二选一即可,启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
> 目前安装步骤已经是最简化了,如果还是嫌安装麻烦,可以微信或者 QQ 约我远程安装,我一般周末和晚上才有时间,收个辛苦费 :moneybag: 50 元一次,安装失败不收费。
### 修改默认8090端口映射
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
@@ -58,12 +59,13 @@ services:
ports:
- 5678:5678
volumes:
- ./music:/app/music
- /xiaomusic/music:/app/music
- /xiaomusic/conf:/app/conf
environment:
XIAOMUSIC_PORT: 5678
```
如果不是首次修改端口,还需要修改 setting.json 文件里的端口。
如果不是首次修改端口,还需要修改 /xiaomusic/conf/setting.json 文件里的端口。
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
@@ -79,7 +81,7 @@ services:
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
XiaoMusic v0.1.92 by: github.com/hanxi
XiaoMusic v0.3.29 by: github.com/hanxi
usage: xiaomusic [-h] [--port PORT] [--hardware HARDWARE] [--account ACCOUNT]
[--password PASSWORD] [--cookie COOKIE] [--verbose]
@@ -107,22 +109,31 @@ options:
- 使用 install_dependencies.sh 下载依赖
- 使用 pdm 安装环境
- 参考 [xiaogpt](https://github.com/yihong0618/xiaogpt) 设置好环境变量
```shell
export MI_USER="xxxxx"
export MI_PASS="xxxx"
export MI_DID=00000
export XIAOMUSIC_SEARCH='bilisearch:'
```
然后启动即可。默认监听了端口 8090 , 使用其他端口自行修改。
- 默认监听了端口 8090 , 使用其他端口自行修改。
```shell
pdm run xiaomusic.py
````
如果是开发前端界面,可以通过 <http://localhost:8090/docs> 查看有什么接口。
如果是开发前端界面,可以通过 <http://localhost:8090/docs>
查看有什么接口。目前的 web 控制台非常简陋,欢迎有兴趣的朋友帮忙实现一个漂亮的前端,需要什么接口可以随时提需求。
### 代码提交规范
提交前请执行
```
pdm fmt
pdm lint --fix
```
用于检查代码和格式化代码。
### 本地编译 Docker Image
```shell
docker build -t xiaomusic .
```
### 支持口令
@@ -136,6 +147,10 @@ pdm run xiaomusic.py
- 停止播放
- 刷新列表
- 播放列表+列表名 比如:播放列表其他
- 加入收藏
- 取消收藏
- 播放列表收藏
- 播放本地歌曲+歌名
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
@@ -148,19 +163,21 @@ pdm run xiaomusic.py
| 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) |
| L15A | [小米AI音箱第二代](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l15a#/) |
| 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) |
| L09A | [小米音箱Art](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l09a) |
| LX04 X10A X08A | 已经支持的触屏版 |
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
> 目前应该所有设备类型都已经支持播放,有问题随时反馈。
> 其他触屏版不能播放可以设置 XIAOMUSIC_USE_MUSIC_API true 试试。
> 其他触屏版不能播放可以设置【触屏版兼容模式】选项为 true 试试。见 <https://github.com/hanxi/xiaomusic/issues/30>
## 支持音乐格式
@@ -172,116 +189,8 @@ pdm run xiaomusic.py
- m4a
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
## 在 Docker 里使用
```shell
docker run -e MI_USER='your-xiaomi-account' \
-e MI_PASS='your-xiaomi-password' \
-e MI_DID='your-xiaomi-speaker-mid' \
-e MI_HARDWARE='L07A' \
-e XIAOMUSIC_PROXY='proxy-for-yt-dlp' \
-e XIAOMUSIC_HOSTNAME=192.168.2.5 \
-e XIAOMUSIC_SEARCH='bilisearch:' \
-p 8090:8090 \
-v ./music:/app/music hanxi/xiaomusic
```
- XIAOMUSIC_SEARCH 可以配置为 'bilisearch:' 表示歌曲从哔哩哔哩下载;
- 配置为 'ytsearch:' 表示歌曲从 youtube 下载。
- XIAOMUSIC_PROXY 用于配置代理,默认为空;
- 当 XIAOMUSIC_SEARCH 配置为 'ytsearch:' 时在国内需要用到。
- MI_HARDWARE 是小米音箱的型号,默认为'L07A'
- 注意端口必须映射为与容器内一致, XIAOMUSIC_HOSTNAME 需要设置为宿主机的 IP 地址,否则小爱无法正常播放。
- 可以把 /app/music 目录映射到本地,用于保存下载的歌曲。
XIAOMUSIC_PROXY 参数格式参考 yt-dlp 文档说明:
```
Use the specified HTTP/HTTPS/SOCKS proxy. To
enable SOCKS proxy, specify a proper scheme,
e.g. socks5://user:pass@127.0.0.1:1080/.
Pass in an empty string (--proxy "") for
direct connection
```
<https://github.com/hanxi/xiaomusic/issues/2><https://github.com/hanxi/xiaomusic/issues/11>
### 本地编译Docker Image
```shell
docker build -t xiaomusic .
```
### docker compose 示例
使用哔哩哔哩下载歌曲:
```yaml
version: '3'
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
MI_DID: 00000
MI_HARDWARE: 'L07A'
XIAOMUSIC_SEARCH: 'bilisearch:'
XIAOMUSIC_HOSTNAME: '192.168.2.5'
```
使用 youtobe 下载歌曲:
```yaml
version: '3'
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
MI_DID: 00000
MI_HARDWARE: 'L07A'
XIAOMUSIC_SEARCH: 'ytsearch:'
XIAOMUSIC_PROXY: 'http://192.168.2.5:8080'
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'
```
> 已知 L05B L05C LX06 L16A 不支持 flac 格式。
> 如果格式不能播放可以打开【转换为MP3】和【触屏版兼容模式】选项。具体见 <https://github.com/hanxi/xiaomusic/issues/153#issuecomment-2328168689>
## 简易的控制面板
@@ -299,16 +208,7 @@ services:
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER
- MI_PASS
- XIAOMUSIC_HOSTNAME
其他的这些可以在网页里配置:
- MI_DID
- MI_HARDWARE
- XIAOMUSIC_SEARCH
- XIAOMUSIC_PROXY
采用新的设置页面之后,没有必须在启动前配置的环境变量了,除非是改默认的 8090 端口才需要配置环境变量。
## 网络歌单功能
@@ -318,22 +218,28 @@ services:
## 更多其他可选配置
- 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 用来存放配置文件的目录,记得把目录映射到主机,默认为 `/app/config` ,具体见 <https://github.com/hanxi/xiaomusic/issues/74>
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82>
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号,如果发现需要设置这个选项的时候请告知我加一下设备型号,方便以后不用设置。 见 <https://github.com/hanxi/xiaomusic/issues/30>
- 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>
- 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 控制台用户,对应后台的 【web控制台账户】
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码,对应后台的 【web控制台密码】
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,对应后台的 【配置文件目录】,记得把目录映射到主机,默认为 `/app/config` ,具体见 <https://github.com/hanxi/xiaomusic/issues/74>
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,对应后台的 【关闭下载功能】,<https://github.com/hanxi/xiaomusic/issues/82>
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,对应后台的 【触屏版兼容模式】,用于兼容不能播放的型号,如果发现需要设置这个选项的时候请告知我加一下设备型号,方便以后不用设置。 见 <https://github.com/hanxi/xiaomusic/issues/30>
- 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>
- XIAOMUSIC_PROXY 用于配置国内使用 youtube 源下载歌曲时使用的代理,参数格式参考 yt-dlp 文档说明。 见 <https://github.com/hanxi/xiaomusic/issues/2><https://github.com/hanxi/xiaomusic/issues/11>
### :warning: 安全提醒
- 如果配置了公网访问 xiaomusic ,请一定要开启密码登陆,并设置复杂的密码。且不要在公共场所的 WiFi 环境下使用,否则可能造成小米账号密码泄露。
## 高级篇
@@ -353,12 +259,26 @@ services:
- [PDM](https://pdm.fming.dev/latest/)
- [xiaogpt](https://github.com/yihong0618/xiaogpt)
- [MiService](https://github.com/yihong0618/MiService)
- [实现原理](https://github.com/yihong0618/gitblog/issues/258)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [awesome-xiaoai](https://github.com/zzz6519003/awesome-xiaoai)
- [微信小程序: XIAO晓音](https://github.com/F-loat/xiaoplayer)
- [pure 主题 xiaomusicUI](https://github.com/52fisher/xiaomusicUI)
- 所有帮忙调试和测试的朋友
- 所有反馈问题和建议的朋友
### 其他教程
> 下面教程可能比较旧,只供参考
- [NAS部署教程](https://post.m.smzdm.com/p/avpe7n99/)
- [群晖部署教程](https://post.m.smzdm.com/p/a7px7dol/)
- [QNAS部署教程](https://post.smzdm.com/p/a5xz5x63/)
- 所有帮忙调试和测试的朋友
- 所有反馈问题和建议的朋友
- [视频教程](https://www.bilibili.com/video/BV1ZZpweHEtT/)
- [TechHive](https://mp.weixin.qq.com/s/4a41muFtPaFKtHeZYu795w)
- [弹个AI](https://mp.weixin.qq.com/s/sIsKxB7Y8b83AhnvaWiMog)
- [简单免费教你用绿联NAS联动小爱音箱私人音乐库也能语音点播](https://post.m.smzdm.com/p/a8pldgg7/)
## Star History
@@ -366,6 +286,7 @@ services:
## 赞赏
- 爱发电 <https://afdian.net/a/imhanxi>
- 点个 Star
- 谢谢 ❤️
- :moneybag: 爱发电 <https://afdian.com/a/imhanxi>
- 点个 Star :star:
- 谢谢 :heart:
- ![喝杯奶茶](https://i.v2ex.co/7Q03axO5l.png)

View File

@@ -77,5 +77,6 @@
},
"enable_force_stop": false,
"devices": {},
"group_list": ""
}
"group_list": "",
"convert_to_mp3": false
}

14
pdm.lock generated
View File

@@ -5,7 +5,7 @@
groups = ["default", "dev", "lint"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:0a0b1f63fdd9dd2c4ca2a777f12d294126a880631c1b3d48108d1df283ba14a8"
content_hash = "sha256:d78c6aed8ee11387663e36ade149f06fd493f984e253a1936163f85542ab5a52"
[[metadata.targets]]
requires_python = "==3.10.12"
@@ -464,6 +464,18 @@ files = [
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
]
[[package]]
name = "ga4mp"
version = "2.0.4"
requires_python = ">=3.6,<4.0"
summary = "Google Analytics 4 Measurement Protocol Python Module"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "ga4mp-2.0.4-py3-none-any.whl", hash = "sha256:11e5072b33a93917bbfcf5b44ee48d21e45124ef18e2a0f1275e1529df340de8"},
{file = "ga4mp-2.0.4.tar.gz", hash = "sha256:2fdcf275e643c5c3ab2c3a03e82edc3551109f7e9175b3604aea8c8e015c15ad"},
]
[[package]]
name = "h11"
version = "0.14.0"

View File

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

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.3.25"
version = "0.3.36"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},
@@ -14,6 +14,7 @@ dependencies = [
"fastapi>=0.111.0",
"starlette>=0.37.2",
"aiofiles>=24.1.0",
"ga4mp>=2.0.4",
]
requires-python = ">=3.10,<3.12"
readme = "README.md"

View File

@@ -16,13 +16,12 @@ def get_html_files(directory):
def update_html_version(html_files, version):
"""
更新HTML文件中所有以 /static/ 开头的CSS和JS文件引用的版本号。
更新HTML文件中所有以 ./ 开头的CSS和JS文件引用的版本号。
:param html_files: 需要更新的HTML文件路径的列表。
:param version: 新的版本号字符串。
"""
pattern = re.compile(r'(/static/.*(css|js))\?version=[^"]*"')
# pattern = re.compile(r'(/static/.*html)\?version=[^"]*"')
pattern = re.compile(r'(\./.*(css|js))\?version=[^"]*"')
for html_file in html_files:
if not html_file.exists():
@@ -48,7 +47,7 @@ if __name__ == "__main__":
t = str(int(time.time()))
# 指定目录
html_directory = "xiaomusic/static" # 修改为实际的HTML文件目录路径
html_directory = "xiaomusic/static/default" # 修改为实际的HTML文件目录路径
# 获取HTML文件列表
html_files_to_update = get_html_files(html_directory)

View File

@@ -1 +1 @@
__version__ = "0.3.25"
__version__ = "0.3.36"

82
xiaomusic/analytics.py Normal file
View File

@@ -0,0 +1,82 @@
import asyncio
from datetime import datetime
from ga4mp import GtagMP
from xiaomusic import __version__
class Analytics:
def __init__(self, log):
self.gtag = None
self.current_date = None
self.log = log
self.init()
def init(self):
if self.gtag is not None:
return
gtag = GtagMP(
api_secret="sVRsf3T9StuWc-ZiWZxDVA",
measurement_id="G-Z09NC1K7ZW",
client_id="",
)
gtag.client_id = gtag.random_client_id()
gtag.store.set_user_property(name="version", value=__version__)
self.gtag = gtag
self.log.info("analytics init ok")
async def run_with_timeout(self, func, *args, **kwargs):
try:
return await asyncio.wait_for(func(*args, **kwargs), 3)
except asyncio.TimeoutError as e:
self.log.warning(f"analytics run_with_timeout failed {e}")
return None
async def send_startup_event(self):
try:
await self.run_with_timeout(self._send_startup_event)
except Exception as e:
self.log.warning(f"analytics send_startup_event failed {e}")
self.init()
async def _send_startup_event(self):
event = self.gtag.create_new_event(name="startup")
event.set_event_param(name="version", value=__version__)
event_list = [event]
self.gtag.send(events=event_list)
async def send_daily_event(self):
try:
await self.run_with_timeout(self._send_daily_event)
except Exception as e:
self.log.warning(f"analytics send_daily_event failed {e}")
self.init()
async def _send_daily_event(self):
current_date = datetime.now().strftime("%Y-%m-%d")
if self.current_date == current_date:
return
event = self.gtag.create_new_event(name="daily_active_user")
event.set_event_param(name="version", value=__version__)
event.set_event_param(name="date", value=current_date)
event_list = [event]
self.gtag.send(events=event_list)
self.current_date = current_date
async def send_play_event(self, name, sec):
try:
await self.run_with_timeout(self._send_play_event, name, sec)
except Exception as e:
self.log.warning(f"analytics send_play_event failed {e}")
self.init()
async def _send_play_event(self, name, sec):
event = self.gtag.create_new_event(name="play")
event.set_event_param(name="version", value=__version__)
event.set_event_param(name="music", value=name)
event.set_event_param(name="sec", value=sec)
event_list = [event]
self.gtag.send(events=event_list)

View File

@@ -3,7 +3,6 @@ import argparse
import json
import os
import signal
import subprocess
import uvicorn
@@ -137,7 +136,7 @@ def main():
try:
filename = config.getsettingfile()
with open(filename) as f:
with open(filename, encoding="utf-8") as f:
data = json.loads(f.read())
config.update_config(data)
except Exception as e:
@@ -153,39 +152,15 @@ def main():
log_config=LOGGING_CONFIG,
)
process = None
def run_gate():
command = [
"uvicorn",
"xiaomusic.gate:app",
"--workers",
"4",
"--host",
"0.0.0.0",
"--port",
str(config.port),
]
global process
process = subprocess.Popen(command)
def signal_handler(sig, frame):
print("主进程收到退出信号,准备退出...")
if process is not None:
process.terminate() # 终止子进程
process.wait() # 等待子进程退出
print("子进程已退出")
os._exit(0) # 退出主进程
# 捕获主进程的退出信号
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
port = int(config.port)
if config.enable_gate:
run_gate()
run_server(port + 1)
else:
run_server(port)
run_server(port)
if __name__ == "__main__":

View File

@@ -16,12 +16,15 @@ def default_key_word_dict():
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"上一首": "play_prev",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "set_random_play",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"加入收藏": "add_to_favorites",
"取消收藏": "del_from_favorites",
}
@@ -44,12 +47,15 @@ def default_key_match_order():
"分钟后关机",
"播放歌曲",
"下一首",
"上一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"刷新列表",
"播放列表",
"加入收藏",
"取消收藏",
]
@@ -74,7 +80,7 @@ class Config:
music_path: str = os.getenv(
"XIAOMUSIC_MUSIC_PATH", "music"
) # 只能是music目录下的子目录
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "")
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "music/download")
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", "conf")
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
@@ -96,6 +102,7 @@ class Config:
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", "")
custom_play_list_json: str = os.getenv("XIAOMUSIC_CUSTOM_PLAY_LIST_JSON", "")
disable_download: bool = (
os.getenv("XIAOMUSIC_DISABLE_DOWNLOAD", "false").lower() == "true"
)
@@ -136,8 +143,12 @@ class Config:
remove_id3tag: bool = (
os.getenv("XIAOMUSIC_REMOVE_ID3TAG", "false").lower() == "true"
)
convert_to_mp3: bool = os.getenv("CONVERT_TO_MP3", "false").lower() == "true"
delay_sec: int = int(os.getenv("XIAOMUSIC_DELAY_SEC", 3)) # 下一首歌延迟播放秒数
enable_gate: bool = os.getenv("XIAOMUSIC_ENABLE_GATE", "false").lower() == "true"
continue_play: bool = (
os.getenv("XIAOMUSIC_CONTINUE_PLAY", "false").lower() == "true"
)
pull_ask_sec: int = int(os.getenv("XIAOMUSIC_PULL_ASK_SEC", "1"))
def append_keyword(self, keys, action):
for key in keys.split(","):
@@ -153,6 +164,7 @@ class Config:
def init_keyword(self):
self.key_match_order = default_key_match_order()
self.key_word_dict = default_key_word_dict()
self.append_keyword(self.keywords_playlocal, "playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_stop, "stop")

View File

@@ -1,124 +0,0 @@
import json
import logging
import mimetypes
import os
import re
from contextlib import asynccontextmanager
import aiofiles
import httpx
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import Response, StreamingResponse
from fastapi.staticfiles import StaticFiles
from xiaomusic import __version__
from xiaomusic.config import Config
config = Config()
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
@asynccontextmanager
async def app_lifespan(app):
global config
try:
filename = config.getsettingfile()
with open(filename) as f:
data = json.loads(f.read())
config.update_config(data)
except Exception as e:
log.exception(f"Execption {e}")
yield
app = FastAPI(
lifespan=app_lifespan,
version=__version__,
)
folder = os.path.dirname(__file__)
app.mount("/static", StaticFiles(directory=f"{folder}/static"), name="static")
async def file_iterator(file_path, start, end):
async with aiofiles.open(file_path, mode="rb") as file:
await file.seek(start)
chunk_size = 1024
while start <= end:
read_size = min(chunk_size, end - start + 1)
data = await file.read(read_size)
if not data:
break
start += len(data)
yield data
range_pattern = re.compile(r"bytes=(\d+)-(\d*)")
@app.get("/music/{file_path:path}")
async def music_file(request: Request, file_path: str):
absolute_path = os.path.abspath(config.music_path)
absolute_file_path = os.path.normpath(os.path.join(absolute_path, file_path))
if not absolute_file_path.startswith(absolute_path):
raise HTTPException(status_code=404, detail="File not found")
if not os.path.exists(absolute_file_path):
raise HTTPException(status_code=404, detail="File not found")
file_size = os.path.getsize(absolute_file_path)
range_start, range_end = 0, file_size - 1
range_header = request.headers.get("Range")
log.info(f"music_file range_header {range_header}")
if range_header:
range_match = range_pattern.match(range_header)
if range_match:
range_start = int(range_match.group(1))
if range_match.group(2):
range_end = int(range_match.group(2))
log.info(f"music_file in range {absolute_file_path}")
log.info(f"music_file {range_start} {range_end} {absolute_file_path}")
headers = {
"Content-Range": f"bytes {range_start}-{range_end}/{file_size}",
"Accept-Ranges": "bytes",
}
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
mime_type = "application/octet-stream"
return StreamingResponse(
file_iterator(absolute_file_path, range_start, range_end),
headers=headers,
status_code=206 if range_header else 200,
media_type=mime_type,
)
@app.options("/music/{file_path:path}")
async def music_options():
headers = {
"Accept-Ranges": "bytes",
}
return Response(headers=headers)
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(path: str, request: Request):
async with httpx.AsyncClient() as client:
port = config.port + 1
url = f"http://127.0.0.1:{port}/{path}"
response = await client.request(
method=request.method,
url=url,
headers=request.headers,
params=request.query_params,
content=await request.body() if request.method in ["POST", "PUT"] else None,
)
return Response(
content=response.content,
status_code=response.status_code,
headers=dict(response.headers),
)

View File

@@ -6,12 +6,14 @@ import re
import secrets
import shutil
import tempfile
import urllib.parse
from contextlib import asynccontextmanager
from dataclasses import asdict
from typing import Annotated
import aiofiles
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
@@ -72,7 +74,14 @@ def no_verification():
app = FastAPI(
lifespan=app_lifespan,
version=__version__,
dependencies=[Depends(verification)],
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许访问的源
allow_credentials=False, # 支持 cookie
allow_methods=["*"], # 允许使用的请求方法
allow_headers=["*"], # 允许携带的 Headers
)
@@ -96,19 +105,19 @@ def HttpInit(_xiaomusic):
@app.get("/")
async def read_index():
async def read_index(Verifcation=Depends(verification)):
folder = os.path.dirname(__file__)
return FileResponse(f"{folder}/static/index.html")
@app.get("/getversion")
def getversion():
def getversion(Verifcation=Depends(verification)):
log.debug("getversion %s", __version__)
return {"version": __version__}
@app.get("/getvolume")
async def getvolume(did: str = ""):
async def getvolume(did: str = "", Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
return {"volume": 0}
@@ -122,7 +131,7 @@ class DidVolume(BaseModel):
@app.post("/setvolume")
async def setvolume(data: DidVolume):
async def setvolume(data: DidVolume, Verifcation=Depends(verification)):
did = data.did
volume = data.volume
if not xiaomusic.did_exist(did):
@@ -134,21 +143,25 @@ async def setvolume(data: DidVolume):
@app.get("/searchmusic")
def searchmusic(name: str = ""):
def searchmusic(name: str = "", Verifcation=Depends(verification)):
return xiaomusic.searchmusic(name)
@app.get("/playingmusic")
def playingmusic(did: str = ""):
def playingmusic(did: str = "", Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
is_playing = xiaomusic.isplaying(did)
cur_music = xiaomusic.playingmusic(did)
# 播放进度
offset, duration = xiaomusic.get_offset_duration(did)
return {
"ret": "OK",
"is_playing": is_playing,
"cur_music": cur_music,
"offset": offset,
"duration": duration,
}
@@ -158,7 +171,7 @@ class DidCmd(BaseModel):
@app.post("/cmd")
async def do_cmd(data: DidCmd):
async def do_cmd(data: DidCmd, Verifcation=Depends(verification)):
did = data.did
cmd = data.cmd
log.info(f"docmd. did:{did} cmd:{cmd}")
@@ -177,9 +190,11 @@ async def do_cmd(data: DidCmd):
@app.get("/getsetting")
async def getsetting(need_device_list: bool = False):
async def getsetting(need_device_list: bool = False, Verifcation=Depends(verification)):
config = xiaomusic.getconfig()
data = asdict(config)
data["password"] = "******"
data["httpauth_password"] = "******"
if need_device_list:
device_list = await xiaomusic.getalldevices()
log.info(f"getsetting device_list: {device_list}")
@@ -188,12 +203,17 @@ async def getsetting(need_device_list: bool = False):
@app.post("/savesetting")
async def savesetting(request: Request):
async def savesetting(request: Request, Verifcation=Depends(verification)):
try:
data_json = await request.body()
data = json.loads(data_json.decode("utf-8"))
debug_data = deepcopy_data_no_sensitive_info(data)
log.info(f"saveconfig: {debug_data}")
config = xiaomusic.getconfig()
if data["password"] == "******" or data["password"] == "":
data["password"] = config.password
if data["httpauth_password"] == "******" or data["httpauth_password"] == "":
data["httpauth_password"] = config.httpauth_password
await xiaomusic.saveconfig(data)
reset_http_server()
return "save success"
@@ -206,8 +226,18 @@ async def musiclist(Verifcation=Depends(verification)):
return xiaomusic.get_music_list()
@app.get("/musicinfo")
async def musicinfo(name: str, Verifcation=Depends(verification)):
url = xiaomusic.get_music_url(name)
return {
"ret": "OK",
"name": name,
"url": url,
}
@app.get("/curplaylist")
async def curplaylist(did: str = ""):
async def curplaylist(did: str = "", Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
return ""
return xiaomusic.get_cur_play_list(did)
@@ -218,7 +248,7 @@ class MusicItem(BaseModel):
@app.post("/delmusic")
def delmusic(data: MusicItem):
def delmusic(data: MusicItem, Verifcation=Depends(verification)):
log.info(data)
xiaomusic.del_music(data.name)
return "success"
@@ -229,7 +259,7 @@ class UrlInfo(BaseModel):
@app.post("/downloadjson")
async def downloadjson(data: UrlInfo):
async def downloadjson(data: UrlInfo, Verifcation=Depends(verification)):
log.info(data)
url = data.url
content = ""
@@ -277,16 +307,16 @@ def downloadlog(Verifcation=Depends(verification)):
@app.get("/playurl")
async def playurl(did: str, url: str):
async def playurl(did: str, url: str, Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
log.info(f"playurl did: {did} url: {url}")
return await xiaomusic.play_url(did=did, arg1=url)
decoded_url = urllib.parse.unquote(url)
log.info(f"playurl did: {did} url: {decoded_url}")
return await xiaomusic.play_url(did=did, arg1=decoded_url)
@app.post("/debug_play_by_music_url")
async def debug_play_by_music_url(request: Request):
async def debug_play_by_music_url(request: Request, Verifcation=Depends(verification)):
try:
data = await request.body()
data_dict = json.loads(data.decode("utf-8"))

View File

@@ -1,6 +1,9 @@
$(function(){
$container=$("#cmds");
append_op_button_name("加入收藏");
append_op_button_name("取消收藏");
const PLAY_TYPE_ONE = 0; // 单曲循环
const PLAY_TYPE_ALL = 1; // 全部循环
const PLAY_TYPE_RND = 2; // 随机播放
@@ -8,9 +11,11 @@ $(function(){
append_op_button("play_type_one", "单曲循环", "单曲循环");
append_op_button("play_type_rnd", "随机播放", "随机播放");
append_op_button_name("刷新列表");
append_op_button_name("下一首");
append_op_button_name("上一首");
append_op_button_name("关机");
append_op_button_name("下一首");
append_op_button_name("刷新列表");
$container.append($("<hr>"));
@@ -18,6 +23,9 @@ $(function(){
append_op_button_name("30分钟后关机");
append_op_button_name("60分钟后关机");
var offset = 0;
var duration = 0;
// 拉取现有配置
$.get("/getsetting", function(data, status) {
console.log(data, status);
@@ -95,9 +103,9 @@ $(function(){
$('#music_list').change(function() {
const selectedValue = $(this).val();
localStorage.setItem('cur_playlist', selectedValue);
$('#music_name').empty();
const sorted_musics = data[selectedValue].sort(custom_sort_key);
$.each(sorted_musics, function(index, item) {
$.each(data[selectedValue], function(index, item) {
$('#music_name').append($('<option></option>').val(item).text(item));
});
});
@@ -105,10 +113,17 @@ $(function(){
$('#music_list').trigger('change');
// 获取当前播放列表
$.get(`curplaylist?did=${did}`, function(data, status) {
if (data != "") {
$('#music_list').val(data);
$.get(`curplaylist?did=${did}`, function(playlist, status) {
if (playlist != "") {
$('#music_list').val(playlist);
$('#music_list').trigger('change');
} else {
// 使用本地记录的
playlist = localStorage.getItem('cur_playlist');
if (data.includes(playlist)) {
$('#music_list').val(playlist);
$('#music_list').trigger('change');
}
}
})
})
@@ -127,6 +142,16 @@ $(function(){
sendcmd(cmd);
});
$("#web_play").on("click", () => {
const music_name = $("#music_name").val();
$.get(`/musicinfo?name=${music_name}`, function(data, status) {
console.log(data);
if (data.ret == "OK") {
$('audio').attr('src',data.url);
}
});
});
$("#del_music").on("click", () => {
var del_music_name = $("#music_name").val();
if (confirm(`确定删除歌曲 ${del_music_name} 吗?`)) {
@@ -149,7 +174,8 @@ $(function(){
$("#playurl").on("click", () => {
var url = $("#music-url").val();
$.get(`/playurl?url=${url}&did=${did}`, function(data, status) {
const encoded_url = encodeURIComponent(url);
$.get(`/playurl?url=${encoded_url}&did=${did}`, function(data, status) {
console.log(data);
});
});
@@ -217,8 +243,15 @@ $(function(){
});
}
// 监听输入框的输入事件
$("#music-name").on('input', function() {
// 监听输入框的输入事件
function debounce(func, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
$("#music-name").on('input', debounce(function() {
var inputValue = $(this).val();
// 发送Ajax请求
$.ajax({
@@ -237,7 +270,7 @@ $(function(){
});
}
});
});
},300));
function get_playing_music() {
$.get(`/playingmusic?did=${did}`, function(data, status) {
@@ -248,26 +281,23 @@ $(function(){
} else {
$("#playering-music").text(`【空闲中】 ${data.cur_music}`);
}
offset = data.offset;
duration = data.duration;
}
});
}
function custom_sort_key(a, b) {
// 使用正则表达式提取数字前缀
const numericPrefixA = a.match(/^(\d+)/) ? parseInt(a.match(/^(\d+)/)[1], 10) : null;
const numericPrefixB = b.match(/^(\d+)/) ? parseInt(b.match(/^(\d+)/)[1], 10) : null;
// 如果两个键都有数字前缀,则按数字大小排序
if (numericPrefixA !== null && numericPrefixB !== null) {
return numericPrefixA - numericPrefixB;
}
// 如果一个键有数字前缀而另一个没有,则有数字前缀的键排在前面
if (numericPrefixA !== null) return -1;
if (numericPrefixB !== null) return 1;
// 如果两个键都没有数字前缀,则按照常规字符串排序
return a.localeCompare(b);
}
});
setInterval(()=>{
if (duration > 0) {
offset++;
$("#progress").val(offset / duration * 100);
$("#play-time").text(`${formatTime(offset)}/${formatTime(duration)}`)
}else{
$("#play-time").text(`${formatTime(0)}/${formatTime(0)}`)
}
},1000)
function formatTime(seconds) {
var minutes = Math.floor(seconds / 60);
var remainingSeconds =Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
});

View File

@@ -5,9 +5,18 @@
<meta name="viewport" content="width=device-width">
<title>Debug For XiaoMusic</title>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1723807100">
<link rel="stylesheet" type="text/css" href="./style.css?version=1726700926">
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="/static/jquery-3.7.1.min.js?version=1723807100"></script>
<script src="./jquery-3.7.1.min.js?version=1726700926"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<script>
var vConsole = new window.VConsole();

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="./jquery-3.7.1.min.js?version=1726700926"></script>
<script src="./app.js?version=1726700926"></script>
<link rel="stylesheet" type="text/css" href="./style.css?version=1726700926">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
-->
</head>
<body>
<h2>小爱音箱操控面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
版本未知
</a>)
</h2>
<hr>
<div class="rows">
<select id="did">
</select>
</div>
<div id="cmds">
<a class="button" href="./setting.html">设置</a>
</div>
<hr>
<div style="margin: 20px;">
<div style="display: flex; align-items: center;">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#8e43e7" style="height: 48px; width: 48px;"><path d="M550.826667 154.666667a47.786667 47.786667 0 0 0-19.84 4.48L298.666667 298.666667H186.453333A80 80 0 0 0 106.666667 378.453333v267.093334A80 80 0 0 0 186.453333 725.333333H298.666667l232.32 139.52a47.786667 47.786667 0 0 0 19.84 4.48A46.506667 46.506667 0 0 0 597.333333 822.826667V201.173333a46.506667 46.506667 0 0 0-46.506666-46.506666zM554.666667 822.826667c0 3.413333-3.84 3.84-3.84 3.84L320 688.853333l-9.6-6.186666H186.453333A37.12 37.12 0 0 1 149.333333 645.546667V378.453333A37.12 37.12 0 0 1 186.453333 341.333333h123.946667l10.24-6.186666 229.546667-137.6s3.84 0 3.84 3.84zM667.52 346.026667a21.333333 21.333333 0 0 0 0 30.293333 192 192 0 0 1 0 271.36 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 234.666667 234.666667 0 0 0 0-331.946666 21.333333 21.333333 0 0 0-30.293333 0z"></path><path d="M804.48 219.52a21.333333 21.333333 0 0 0-30.293333 30.293333 370.986667 370.986667 0 0 1 0 524.373334 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 414.08 414.08 0 0 0 0-584.96z"></path></svg>
<input id="volume" type="range"></input>
</div>
</div>
<hr>
<div class="rows">
<datalist id="autocomplete-list"></datalist>
<input id="music-name" type="text" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)" list="autocomplete-list"></input>
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)"></input>
<div style="display: flex; align-items: center">
<progress id="progress" value="0" max="100" style="width: 270px"></progress>
<div id="play-time" style="margin-left: 10px">00:00/00:00</div>
</div>
<div>
<button id="play">播放</button>
<div id="playering-music" class="text"></div>
</div>
</div>
<hr>
<div class="rows">
<label for="music_list">播放列表:</label>
<select id="music_list"></select>
<label for="music_name">歌曲:</label>
<select id="music_name"></select>
<div>
<button id="play_music_list">播放列表歌曲</button>
<button id="del_music">删除选中歌曲</button>
<button id="web_play">网页播放</button>
</div>
<div class="play_pannel">
<audio autoplay controls src=""></audio>
</div>
</div>
<hr>
<div class="rows">
<input id="music-url" type="text" value="https://lhttp.qtfm.cn/live/4915/64k.mp3"></input>
<button id="playurl">播放链接</button>
</div>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>
</body>
</html>

View File

@@ -4,7 +4,17 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>M3U to JSON Converter</title>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1723807100">
<link rel="stylesheet" type="text/css" href="./style.css?version=1726700926">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -3,9 +3,18 @@
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="/static/jquery-3.7.1.min.js?version=1723807100"></script>
<script src="/static/setting.js?version=1723807100"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1723807100">
<script src="./jquery-3.7.1.min.js?version=1726700926"></script>
<script src="./setting.js?version=1726700926"></script>
<link rel="stylesheet" type="text/css" href="./style.css?version=1726700926">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
@@ -40,20 +49,15 @@ var vConsole = new window.VConsole();
<label for="hostname">*XIAOMUSIC_HOSTNAME(IP或域名):</label>
<input id="hostname" type="text"></input>
</div>
<hr>
<div class="rows">
<label for="verbose">是否开启调试日志:</label>
<select id="verbose">
<option value="true" selected>true</option>
<option value="false">false</option>
</select>
<div>
<button class="save-button">保存</button>
<button onclick="location.href='/';">返回首页</button>
</div>
</div>
<hr>
<div class="rows">
<label for="group_list">设备分组配置:</label>
<input id="group_list" type="text" placeholder="did1:组名1,did2:组名1,did3:组名2"></input>
@@ -90,18 +94,23 @@ var vConsole = new window.VConsole();
<label for="proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080"></input>
<label for="remove_id3tag">去除MP3 ID3v2和填充减少播放前延迟:</label>
<select id="remove_id3tag">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="convert_to_mp3">转换为MP3</label>
<select id="convert_to_mp3">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="disable_httpauth">关闭密码验证:</label>
<select id="disable_httpauth">
<option value="true" selected>true</option>
<option value="false">false</option>
</select>
<label for="remove_id3tag">去除MP3 ID3v2和填充减少播放前延迟:</label>
<select id="remove_id3tag">
<option value="true" >true</option>
<option value="false" selected>false</option>
</select>
<label for="httpauth_username">web控制台账户:</label>
<input id="httpauth_username" type="text" value=""></input>
<label for="httpauth_password">web控制台密码:</label>
@@ -127,21 +136,27 @@ var vConsole = new window.VConsole();
<option value="false">false</option>
</select>
<label for="enable_gate">开启网关(重启生效):</label>
<select id="enable_gate">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="use_music_api">触屏版兼容模式:</label>
<select id="use_music_api">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="continue_play">启用继续播放(可能导致兼容性问题):</label>
<select id="continue_play">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="port">监听端口(修改后需要重启):</label>
<input id="port" type="number" value="8090"></input>
<label for="public_port">外网访问端口(0表示跟监听端口一致):</label>
<input id="public_port" type="number" value="0"></input>
<label for="pull_ask_sec">获取对话记录间隔(秒):</label>
<input id="pull_ask_sec" type="number" value="1"></input>
<label for="delay_sec">下一首歌延迟播放秒数:</label>
<input id="delay_sec" type="number" value="3"></input>
@@ -163,16 +178,24 @@ var vConsole = new window.VConsole();
</div>
</div>
<hr>
<button onclick="location.href='/';">返回首页</button>
<button id="get_music_list">获取歌单</button>
<button class="save-button">保存</button>
<hr>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<button onclick="location.href='/docs';">查看接口文档</button>
<a class="button" href="./m3u.html" target="_blank">m3u文件转换</a>
<hr>
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>
<hr>
<a href="/static/debug.html" target="_blank">调试工具</a>
<a class="button" href="./debug.html" target="_blank">调试工具</a>
<a class="button" href="https://afdian.com/a/imhanxi" target="_blank">💰 爱发电</a>
<a class="button" href="https://github.com/hanxi/xiaomusic" target="_blank">点个 Star ⭐</a>
<div class="rows">
<img class="qrcode" src="./qrcode.png" alt="请涵曦喝奶茶🧋">
</div>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>

View File

@@ -24,16 +24,19 @@ label {
width: 300px;
}
input,select {
margin: 10px;
width: 300px;
margin-left: 5%;
margin-right: 5%;
margin-top: 10px;
margin-bottom: 10px;
width: 90%;
max-width: 400px;
height: 40px;
}
.rows {
display: flex;
flex-direction: column;
margin-left: 20px;
margin-right: 20px;
justify-content: center;
}
footer {
@@ -44,8 +47,12 @@ footer {
}
textarea{
margin: 10px;
width: 300px;
margin-left: 5%;
margin-right: 5%;
margin-top: 10px;
margin-bottom: 10px;
width: 90%;
max-width: 400px;
height: 200px;
}
@@ -77,3 +84,9 @@ footer {
text-decoration: none;
display: inline-block;
}
.qrcode {
width: 100%;
max-width: 480px;
height: auto;
}

View File

@@ -1,76 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="/static/jquery-3.7.1.min.js?version=1723807100"></script>
<script src="/static/app.js?version=1723807100"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1723807100">
<html lang="en">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
-->
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小爱音箱操控面板</title>
</head>
<body>
<h2>小爱音箱操控面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
版本未知
</a>)
</h2>
<hr>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<div class="rows">
<select id="did">
</select>
</head>
<body>
<div class="container_wrapper">
<div class="logo">
<img src="/static/xiaoai.png" alt="">
</div>
<div class="desc">
<h1>谁家灯火夜通明</h1>
<p class="call">小爱同学?</p>
<p class="answer">哎,我在</p>
</div>
<div class="options">
<!-- 选择主题 /static/[theme] -->
<a href="/static/default/index.html" class="href">默认主题</a>
<a href="/static/pure/index.html" class="href">Pure主题</a>
<a href="https://afdian.com/a/imhanxi" target="_blank">爱发电</a>
<a href="https://github.com/hanxi/xiaomusic" target="_blank">GitHub</a>
</div>
</div>
<footer>
power by <a href="https://github.com/hanxi/xiaomusic">XiaoMusic</a>
</footer>
<style>
@font-face{ font-family: "得意黑 斜体"; font-weight: 400; src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/gJk2ny0v51vn.woff2") format("woff2"), url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/e2C1wSBHH86h.woff") format("woff"); font-display: swap;} @font-face{ font-family: "阿里妈妈数黑体 Bold"; font-weight: 700; src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/4DWYdFK3dz5J.woff2") format("woff2"), url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/V7EBEKlNSdxC.woff") format("woff"); font-display: swap;} body{ background-color: rgb(47, 44, 67); height: 100%; overflow: hidden;}
.container_wrapper{display: flex; justify-content: space-around; align-items: center; flex-wrap: wrap; height: 90vh; cursor: default;}
h1{ font-weight: bold; color: #a2a9af; max-width: 600px; font-family: '得意黑 斜体', 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-size: 2.5em; border-bottom: 1px solid #a2a9af;}
.container_wrapper .logo img{ width: 140px; height: auto; filter: drop-shadow(10px 10px 10px rgba(0, 0, 0, 0.5));}
.desc{ text-align: center; color: #fff; margin: auto 30px; backdrop-filter: blur(5px);}
.desc p{ font-size: 1.2em; margin: 0; padding: 0; font-family: '阿里妈妈数黑体 Bold'; font-weight: 800;}
p.call{ letter-spacing: 0.4em; font-size: 2.2em; line-height: 1.5; font-style: normal;}
p.answer{ letter-spacing: 0.23em; line-height: 1.5; font-style: normal; color: #a2a9af; margin-top: 10px;}
.desc p::before, .desc p::after{ font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-size: 1.5em; color: #4c5870;}
.desc p::before{ content: "“";} .desc p::after{ content: "”";}
.options{ display: flex; flex-direction: column;}
.options a{ color: #a2a9af; text-decoration: none; font-size: 1.1em; position: relative; display: inline; margin: 10px auto;}
.options a::before{ content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background-color: #ebedec; transform-origin: bottom right; transform: scaleX(0); transition: transform 0.3s ease;}
.options a:hover::before{ transform-origin: bottom left; transform: scaleX(1);} .options a:hover{ color:#ebedec;}
footer{ display: flex; justify-content: center; color: #4c5870;} footer a{ color:inherit; text-decoration: none; margin: auto 10px;}
</style>
</body>
<div id="cmds">
</div>
<hr>
<div style="margin: 20px;">
<div style="display: flex; align-items: center;">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#8e43e7" style="height: 48px; width: 48px;"><path d="M550.826667 154.666667a47.786667 47.786667 0 0 0-19.84 4.48L298.666667 298.666667H186.453333A80 80 0 0 0 106.666667 378.453333v267.093334A80 80 0 0 0 186.453333 725.333333H298.666667l232.32 139.52a47.786667 47.786667 0 0 0 19.84 4.48A46.506667 46.506667 0 0 0 597.333333 822.826667V201.173333a46.506667 46.506667 0 0 0-46.506666-46.506666zM554.666667 822.826667c0 3.413333-3.84 3.84-3.84 3.84L320 688.853333l-9.6-6.186666H186.453333A37.12 37.12 0 0 1 149.333333 645.546667V378.453333A37.12 37.12 0 0 1 186.453333 341.333333h123.946667l10.24-6.186666 229.546667-137.6s3.84 0 3.84 3.84zM667.52 346.026667a21.333333 21.333333 0 0 0 0 30.293333 192 192 0 0 1 0 271.36 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 234.666667 234.666667 0 0 0 0-331.946666 21.333333 21.333333 0 0 0-30.293333 0z"></path><path d="M804.48 219.52a21.333333 21.333333 0 0 0-30.293333 30.293333 370.986667 370.986667 0 0 1 0 524.373334 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 414.08 414.08 0 0 0 0-584.96z"></path></svg>
<input id="volume" type="range"></input>
<a href="/static/setting.html">
<svg fill="#8e43e7" height="48px" width="48px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-11.88 -11.88 77.76 77.76" xml:space="preserve" stroke="#8e43e7" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)" stroke-width="0.00054"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)"><rect x="-11.88" y="-11.88" width="77.76" height="77.76" rx="18.6624" fill="#addcff" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.512"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M51.22,21h-5.052c-0.812,0-1.481-0.447-1.792-1.197s-0.153-1.54,0.42-2.114l3.572-3.571 c0.525-0.525,0.814-1.224,0.814-1.966c0-0.743-0.289-1.441-0.814-1.967l-4.553-4.553c-1.05-1.05-2.881-1.052-3.933,0l-3.571,3.571 c-0.574,0.573-1.366,0.733-2.114,0.421C33.447,9.313,33,8.644,33,7.832V2.78C33,1.247,31.753,0,30.22,0H23.78 C22.247,0,21,1.247,21,2.78v5.052c0,0.812-0.447,1.481-1.197,1.792c-0.748,0.313-1.54,0.152-2.114-0.421l-3.571-3.571 c-1.052-1.052-2.883-1.05-3.933,0l-4.553,4.553c-0.525,0.525-0.814,1.224-0.814,1.967c0,0.742,0.289,1.44,0.814,1.966l3.572,3.571 c0.573,0.574,0.73,1.364,0.42,2.114S8.644,21,7.832,21H2.78C1.247,21,0,22.247,0,23.78v6.439C0,31.753,1.247,33,2.78,33h5.052 c0.812,0,1.481,0.447,1.792,1.197s0.153,1.54-0.42,2.114l-3.572,3.571c-0.525,0.525-0.814,1.224-0.814,1.966 c0,0.743,0.289,1.441,0.814,1.967l4.553,4.553c1.051,1.051,2.881,1.053,3.933,0l3.571-3.572c0.574-0.573,1.363-0.731,2.114-0.42 c0.75,0.311,1.197,0.98,1.197,1.792v5.052c0,1.533,1.247,2.78,2.78,2.78h6.439c1.533,0,2.78-1.247,2.78-2.78v-5.052 c0-0.812,0.447-1.481,1.197-1.792c0.751-0.312,1.54-0.153,2.114,0.42l3.571,3.572c1.052,1.052,2.883,1.05,3.933,0l4.553-4.553 c0.525-0.525,0.814-1.224,0.814-1.967c0-0.742-0.289-1.44-0.814-1.966l-3.572-3.571c-0.573-0.574-0.73-1.364-0.42-2.114 S45.356,33,46.168,33h5.052c1.533,0,2.78-1.247,2.78-2.78V23.78C54,22.247,52.753,21,51.22,21z M52,30.22 C52,30.65,51.65,31,51.22,31h-5.052c-1.624,0-3.019,0.932-3.64,2.432c-0.622,1.5-0.295,3.146,0.854,4.294l3.572,3.571 c0.305,0.305,0.305,0.8,0,1.104l-4.553,4.553c-0.304,0.304-0.799,0.306-1.104,0l-3.571-3.572c-1.149-1.149-2.794-1.474-4.294-0.854 c-1.5,0.621-2.432,2.016-2.432,3.64v5.052C31,51.65,30.65,52,30.22,52H23.78C23.35,52,23,51.65,23,51.22v-5.052 c0-1.624-0.932-3.019-2.432-3.64c-0.503-0.209-1.021-0.311-1.533-0.311c-1.014,0-1.997,0.4-2.761,1.164l-3.571,3.572 c-0.306,0.306-0.801,0.304-1.104,0l-4.553-4.553c-0.305-0.305-0.305-0.8,0-1.104l3.572-3.571c1.148-1.148,1.476-2.794,0.854-4.294 C10.851,31.932,9.456,31,7.832,31H2.78C2.35,31,2,30.65,2,30.22V23.78C2,23.35,2.35,23,2.78,23h5.052 c1.624,0,3.019-0.932,3.64-2.432c0.622-1.5,0.295-3.146-0.854-4.294l-3.572-3.571c-0.305-0.305-0.305-0.8,0-1.104l4.553-4.553 c0.304-0.305,0.799-0.305,1.104,0l3.571,3.571c1.147,1.147,2.792,1.476,4.294,0.854C22.068,10.851,23,9.456,23,7.832V2.78 C23,2.35,23.35,2,23.78,2h6.439C30.65,2,31,2.35,31,2.78v5.052c0,1.624,0.932,3.019,2.432,3.64 c1.502,0.622,3.146,0.294,4.294-0.854l3.571-3.571c0.306-0.305,0.801-0.305,1.104,0l4.553,4.553c0.305,0.305,0.305,0.8,0,1.104 l-3.572,3.571c-1.148,1.148-1.476,2.794-0.854,4.294c0.621,1.5,2.016,2.432,3.64,2.432h5.052C51.65,23,52,23.35,52,23.78V30.22z"></path> <path d="M27,18c-4.963,0-9,4.037-9,9s4.037,9,9,9s9-4.037,9-9S31.963,18,27,18z M27,34c-3.859,0-7-3.141-7-7s3.141-7,7-7 s7,3.141,7,7S30.859,34,27,34z"></path> </g> </g></svg>
</a>
</div>
</div>
<hr>
<div class="rows">
<datalist id="autocomplete-list"></datalist>
<input id="music-name" type="text" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)" list="autocomplete-list"></input>
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)"></input>
<div>
<button id="play">播放</button>
<div id="playering-music" class="text"></div>
</div>
</div>
<hr>
<div class="rows">
<label for="music_list">播放列表:</label>
<select id="music_list"></select>
<label for="music_name">歌曲:</label>
<select id="music_name"></select>
<div>
<button id="play_music_list">播放列表歌曲</button>
<button id="del_music">删除选中歌曲</button>
</div>
</div>
<hr>
<div class="rows">
<input id="music-url" type="text" value="https://lhttp.qtfm.cn/live/4915/64k.mp3"></input>
<button id="playurl">播放链接</button>
</div>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/static/pure/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小爱音箱操控面板</title>
<script type="module" crossorigin src="/static/pure/assets/index-C9BDvfvm.js"></script>
<link rel="stylesheet" crossorigin href="/static/pure/assets/index-DUo8JY-R.css">
</head>
<body>
<div id="app"></div>
<!-- 作者的统计代码 -->
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments) };
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
</body>
</html>

BIN
xiaomusic/static/xiaoai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
import copy
import difflib
import json
import logging
import mimetypes
import os
@@ -11,6 +12,7 @@ import random
import re
import shutil
import string
import subprocess
import tempfile
from collections.abc import AsyncIterator
from http.cookies import SimpleCookie
@@ -188,7 +190,11 @@ def is_mp3(url):
return False
async def _get_web_music_duration(session, url, start=0, end=500):
def is_m4a(url):
return url.endswith(".m4a")
async def _get_web_music_duration(session, url, ffmpeg_location, start=0, end=500):
duration = 0
headers = {"Range": f"bytes={start}-{end}"}
async with session.get(url, headers=headers) as response:
@@ -198,6 +204,8 @@ async def _get_web_music_duration(session, url, start=0, end=500):
try:
if is_mp3(url):
m = mutagen.mp3.MP3(tmp)
elif is_m4a(url):
return get_duration_by_ffprobe(tmp, ffmpeg_location)
else:
m = mutagen.File(tmp)
duration = m.info.length
@@ -206,7 +214,7 @@ async def _get_web_music_duration(session, url, start=0, end=500):
return duration
async def get_web_music_duration(url):
async def get_web_music_duration(url, ffmpeg_location="./ffmpeg/bin"):
duration = 0
try:
parsed_url = urlparse(url)
@@ -226,10 +234,12 @@ async def get_web_music_duration(url):
# 设置总超时时间为3秒
timeout = aiohttp.ClientTimeout(total=3)
async with aiohttp.ClientSession(timeout=timeout) as session:
duration = await _get_web_music_duration(session, url, start=0, end=500)
duration = await _get_web_music_duration(
session, url, ffmpeg_location, start=0, end=500
)
if duration <= 0:
duration = await _get_web_music_duration(
session, url, start=0, end=3000
session, url, ffmpeg_location, start=0, end=3000
)
except Exception as e:
logging.error(f"Error get_web_music_duration: {e}")
@@ -237,12 +247,15 @@ async def get_web_music_duration(url):
# 获取文件播放时长
async def get_local_music_duration(filename):
async def get_local_music_duration(filename, ffmpeg_location="./ffmpeg/bin"):
loop = asyncio.get_event_loop()
duration = 0
try:
if is_mp3(filename):
m = await loop.run_in_executor(None, mutagen.mp3.MP3, filename)
elif is_m4a(filename):
duration = get_duration_by_ffprobe(filename, ffmpeg_location)
return duration
else:
m = await loop.run_in_executor(None, mutagen.File, filename)
duration = m.info.length
@@ -251,6 +264,33 @@ async def get_local_music_duration(filename):
return duration
def get_duration_by_ffprobe(file_path, ffmpeg_location):
# 使用 ffprobe 获取文件的元数据,并以 JSON 格式输出
result = subprocess.run(
[
os.path.join(ffmpeg_location, "ffprobe"),
"-v",
"error", # 只输出错误信息,避免混杂在其他输出中
"-show_entries",
"format=duration", # 仅显示时长
"-of",
"json", # 以 JSON 格式输出
file_path,
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
# 解析 JSON 输出
ffprobe_output = json.loads(result.stdout)
# 获取时长
duration = float(ffprobe_output["format"]["duration"])
return duration
def get_random(length):
return "".join(random.sample(string.ascii_letters + string.digits, length))
@@ -330,3 +370,35 @@ def remove_id3_tags(file_path):
change = True
return change
def convert_file_to_mp3(input_file: str, ffmpeg_location: str, music_path: str) -> str:
"""
Convert the music file to MP3 format and return the path of the temporary MP3 file.
"""
# 指定临时文件的目录为 music_path 目录下的 tmp 文件夹
temp_dir = os.path.join(music_path, "tmp")
if not os.path.exists(temp_dir):
os.makedirs(temp_dir) # 确保目录存在
out_file_name = os.path.splitext(os.path.basename(input_file))[0]
out_file_path = os.path.join(temp_dir, f"{out_file_name}.mp3")
command = [
os.path.join(ffmpeg_location, "ffmpeg"),
"-i",
input_file,
"-f",
"mp3",
"-y",
out_file_path,
]
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
print(f"Error during conversion: {e}")
return None
relative_path = os.path.relpath(out_file_path, music_path)
return relative_path

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import asyncio
import copy
import json
import logging
import math
@@ -16,6 +17,7 @@ from aiohttp import ClientSession, ClientTimeout
from miservice import MiAccount, MiNAService
from xiaomusic import __version__
from xiaomusic.analytics import Analytics
from xiaomusic.config import (
KEY_WORD_ARG_BEFORE_DICT,
Config,
@@ -32,6 +34,7 @@ from xiaomusic.const import (
)
from xiaomusic.plugin import PluginManager
from xiaomusic.utils import (
convert_file_to_mp3,
custom_sort_key,
deepcopy_data_no_sensitive_info,
find_best_match,
@@ -82,6 +85,9 @@ class XiaoMusic:
# 更新设备列表
self.update_devices()
# 启动统计
self.analytics = Analytics(self.log)
debug_config = deepcopy_data_no_sensitive_info(self.config)
self.log.info(f"Startup OK. {debug_config}")
@@ -107,6 +113,8 @@ class XiaoMusic:
self.exclude_dirs = set(self.config.exclude_dirs.split(","))
self.music_path_depth = self.config.music_path_depth
self.remove_id3tag = self.config.remove_id3tag
self.convert_to_mp3 = self.config.convert_to_mp3
self.continue_play = self.config.continue_play
def update_devices(self):
self.device_id_did = {} # key 为 device_id
@@ -150,9 +158,9 @@ class XiaoMusic:
async def poll_latest_ask(self):
async with ClientSession() as session:
while True:
# self.log.debug(
# f"Listening new message, timestamp: {self.last_timestamp}"
# )
self.log.debug(
f"Listening new message, timestamp: {self.last_timestamp}"
)
session._cookie_jar = self.cookie_jar
# 拉取所有音箱的对话记录
@@ -163,12 +171,17 @@ class XiaoMusic:
await asyncio.gather(*tasks)
start = time.perf_counter()
# 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(f"Sleep {d}, timestamp: {self.last_timestamp}")
await asyncio.sleep(1 - d)
if self.config.pull_ask_sec <= 1:
if (d := time.perf_counter() - start) < 1:
await asyncio.sleep(1 - d)
else:
sleep_sec = 0
while True:
await asyncio.sleep(1)
sleep_sec = sleep_sec + 1
if sleep_sec >= self.config.pull_ask_sec:
break
async def init_all_data(self, session):
await self.login_miboy(session)
@@ -222,7 +235,7 @@ class XiaoMusic:
self.log.error(f"{self.mi_token_home} file not exist")
return None
with open(self.mi_token_home) as f:
with open(self.mi_token_home, encoding="utf-8") as f:
user_data = json.loads(f.read())
user_id = user_data.get("userId")
service_token = user_data.get("micoapi")[1]
@@ -351,13 +364,17 @@ class XiaoMusic:
if self.is_web_music(name):
origin_url = url
duration, url = await get_web_music_duration(url)
duration, url = await get_web_music_duration(
url, self.config.ffmpeg_location
)
sec = math.ceil(duration)
self.log.info(f"网络歌曲 {name} : {origin_url} {url} 的时长 {sec}")
else:
filename = self.get_filename(name)
self.log.info(f"get_music_sec_url. name:{name} filename:{filename}")
duration = await get_local_music_duration(filename)
duration = await get_local_music_duration(
filename, self.config.ffmpeg_location
)
sec = math.ceil(duration)
self.log.info(f"本地歌曲 {name} : {filename} {url} 的时长 {sec}")
@@ -381,12 +398,27 @@ class XiaoMusic:
else:
self.log.info("No ID3 tag remove needed")
# 如果开启了MP3转换功能且文件不是MP3格式则进行转换
if self.convert_to_mp3 and not is_mp3(filename):
self.log.info(f"convert_to_mp3 is enabled. Checking file: {filename}")
temp_mp3_file = convert_file_to_mp3(
filename, self.config.ffmpeg_location, self.config.music_path
)
if temp_mp3_file:
self.log.info(f"Converted file: {filename} to {temp_mp3_file}")
filename = temp_mp3_file
else:
self.log.warning(f"Failed to convert file to MP3 format: {filename}")
# 构造音乐文件的URL
filename = filename.replace("\\", "/")
if filename.startswith(self.config.music_path):
filename = filename[len(self.config.music_path) :]
if filename.startswith("/"):
filename = filename[1:]
self.log.info(f"get_music_url local music. name:{name}, filename:{filename}")
encoded_name = urllib.parse.quote(filename)
return f"http://{self.hostname}:{self.public_port}/music/{encoded_name}"
@@ -432,6 +464,11 @@ class XiaoMusic:
self.log.exception(f"Execption {e}")
self.music_list["全部"] = list(self.all_music.keys())
self.music_list["所有歌曲"] = [
name for name in self.all_music.keys() if name not in self._all_radio
]
self._append_custom_play_list()
# 歌单排序
for _, play_list in self.music_list.items():
@@ -441,6 +478,16 @@ class XiaoMusic:
for device in self.devices.values():
device.update_playlist()
def _append_custom_play_list(self):
if not self.config.custom_play_list_json:
return
try:
custom_play_list = json.loads(self.config.custom_play_list_json)
self.music_list["收藏"] = list(custom_play_list["收藏"])
except Exception as e:
self.log.exception(f"Execption {e}")
# 给歌单里补充网络歌单
def _append_music_list(self):
if not self.config.music_list_json:
@@ -477,7 +524,17 @@ class XiaoMusic:
except Exception as e:
self.log.exception(f"Execption {e}")
async def analytics_task_daily(self):
while True:
await self.analytics.send_daily_event()
await asyncio.sleep(3600)
async def run_forever(self):
await self.analytics.send_startup_event()
analytics_task = asyncio.create_task(self.analytics_task_daily())
assert (
analytics_task is not None
) # to keep the reference to task, do not remove this
async with ClientSession() as session:
self.session = session
await self.init_all_data(session)
@@ -492,6 +549,11 @@ class XiaoMusic:
query = new_record.get("query", "").strip()
did = new_record.get("did", "").strip()
await self.do_check_cmd(did, query, False)
answers = new_record.get("answers", [{}])
if answers:
answer = answers[0].get("tts", {}).get("text", "").strip()
await self.reset_timer_when_answer(len(answer), did)
self.log.debug(f"query:{query} did:{did} answer:{answer}")
# 匹配命令
async def do_check_cmd(self, did="", query="", ctrl_panel=True, **kwargs):
@@ -508,6 +570,10 @@ class XiaoMusic:
except Exception as e:
self.log.exception(f"Execption {e}")
# 重置计时器
async def reset_timer_when_answer(self, answer_length, did):
await self.devices[did].reset_timer_when_answer(answer_length)
def append_running_task(self, task):
self.running_task.append(task)
@@ -685,6 +751,9 @@ class XiaoMusic:
async def play_next(self, did="", **kwargs):
return await self.devices[did].play_next()
async def play_prev(self, did="", **kwargs):
return await self.devices[did].play_prev()
# 停止
async def stop(self, did="", arg1="", **kwargs):
return await self.devices[did].stop(arg1=arg1)
@@ -694,6 +763,47 @@ class XiaoMusic:
minute = int(arg1)
return await self.devices[did].stop_after_minute(minute)
# 添加歌曲到收藏列表
async def add_to_favorites(self, did="", arg1="", **kwargs):
name = arg1 if arg1 else self.playingmusic(did)
if not name:
return
favorites = self.music_list.get("收藏", [])
if name in favorites:
return
favorites.append(name)
self.save_favorites(favorites)
# 从收藏列表中移除
async def del_from_favorites(self, did="", arg1="", **kwargs):
name = arg1 if arg1 else self.playingmusic(did)
if not name:
return
favorites = self.music_list.get("收藏", [])
if name not in favorites:
return
favorites.remove(name)
self.save_favorites(favorites)
def save_favorites(self, favorites):
self.music_list["收藏"] = favorites
custom_play_list = {}
if self.config.custom_play_list_json:
custom_play_list = json.loads(self.config.custom_play_list_json)
custom_play_list["收藏"] = favorites
self.config.custom_play_list_json = json.dumps(
custom_play_list, ensure_ascii=False
)
self.save_cur_config()
# 更新每个设备的歌单
for device in self.devices.values():
device.update_playlist()
# 获取音量
async def get_volume(self, did="", **kwargs):
return await self.devices[did].get_volume()
@@ -720,10 +830,13 @@ class XiaoMusic:
# 正在播放中的音乐
def playingmusic(self, did):
cur_music = self.devices[did].cur_music
cur_music = self.devices[did].get_cur_music()
self.log.debug(f"playingmusic. cur_music:{cur_music}")
return cur_music
def get_offset_duration(self, did):
return self.devices[did].get_offset_duration()
# 当前是否正在播放歌曲
def isplaying(self, did):
return self.devices[did].isplaying()
@@ -735,7 +848,7 @@ class XiaoMusic:
def try_init_setting(self):
try:
filename = self.config.getsettingfile()
with open(filename) as f:
with open(filename, encoding="utf-8") as f:
data = json.loads(f.read())
self.update_config_from_setting(data)
except FileNotFoundError:
@@ -762,6 +875,10 @@ class XiaoMusic:
# 把当前配置落地
def save_cur_config(self):
for did in self.config.devices.keys():
deviceobj = self.devices.get(did)
if deviceobj is not None:
self.config.devices[did] = deviceobj.device
data = asdict(self.config)
self.do_saveconfig(data)
self.log.info("save_cur_config ok")
@@ -780,6 +897,8 @@ class XiaoMusic:
# 重新初始化
async def reinit(self, **kwargs):
for handler in self.log.handlers:
handler.close()
self.setup_logger()
await self.init_all_data(self.session)
self._gen_all_music_list()
@@ -835,21 +954,41 @@ class XiaoMusicDevice:
self.ffmpeg_location = self.config.ffmpeg_location
self._download_proc = None # 下载对象
self.cur_music = self.device.cur_music
self._next_timer = None
self._timeout = 0
self._playing = False
# 播放进度
self._start_time = 0
self._duration = 0
self._paused_time = 0
# 关机定时器
self._stop_timer = None
self._last_cmd = None
self.update_playlist()
def get_cur_music(self):
return self.device.cur_music
def get_offset_duration(self):
if not self.isplaying():
return -1, -1
offset = time.time() - self._start_time - self._paused_time
duration = self._duration
return offset, duration
# 初始化播放列表
def update_playlist(self):
self._cur_play_list = self.device.cur_playlist
if self._cur_play_list not in self.xiaomusic.music_list:
self._cur_play_list = "全部"
self._play_list = self.xiaomusic.music_list.get(self._cur_play_list)
if self.device.cur_playlist not in self.xiaomusic.music_list:
self.device.cur_playlist = "全部"
list_name = self.device.cur_playlist
self._play_list = copy.copy(self.xiaomusic.music_list[list_name])
if self.device.play_type == PLAY_TYPE_RND:
random.shuffle(self._play_list)
self.log.info(f"随机打乱 {list_name} {self._play_list}")
else:
self.log.info(f"没打乱 {list_name} {self._play_list}")
# 播放歌曲
async def play(self, name="", search_key=""):
@@ -862,7 +1001,7 @@ class XiaoMusicDevice:
await self._play_next()
return
else:
name = self.cur_music
name = self.get_cur_music()
self.log.info(f"play. search_key:{search_key} name:{name}")
# 本地歌曲不存在时下载
@@ -884,15 +1023,37 @@ class XiaoMusicDevice:
async def _play_next(self):
self.log.info("开始播放下一首")
name = self.cur_music
name = self.get_cur_music()
if (
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
or name == ""
or (
(name not in self._play_list) and self.device.play_type != PLAY_TYPE_ONE
)
):
name = self.get_next_music()
self.log.info(f"_play_next. name:{name}, cur_music:{self.get_cur_music()}")
if name == "":
await self.do_tts("本地没有歌曲")
return
await self._play(name)
# 上一首
async def play_prev(self):
return await self._play_prev()
async def _play_prev(self):
self.log.info("开始播放上一首")
name = self.get_cur_music()
if (
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
or name == ""
or (name not in self._play_list)
):
name = self.get_next_music()
self.log.info(f"_play_next. name:{name}, cur_music:{self.cur_music}")
name = self.get_prev_music()
self.log.info(f"_play_prev. name:{name}, cur_music:{self.get_cur_music()}")
if name == "":
await self.do_tts("本地没有歌曲")
return
@@ -906,7 +1067,7 @@ class XiaoMusicDevice:
await self._play_next()
return
else:
name = self.cur_music
name = self.get_cur_music()
self.log.info(f"playlocal. name:{name}")
@@ -922,12 +1083,13 @@ class XiaoMusicDevice:
self.cancel_group_next_timer()
self._playing = True
self.cur_music = name
self.log.info(f"cur_music {self.cur_music}")
self.device.cur_music = name
self.log.info(f"cur_music {self.get_cur_music()}")
sec, url = await self.xiaomusic.get_music_sec_url(name)
await self.group_force_stop_xiaoai()
self.log.info(f"播放 {url}")
results = await self.group_player_play(url)
results = await self.group_player_play(url, name)
if all(ele is None for ele in results):
self.log.info(f"播放 {name} 失败")
await asyncio.sleep(1)
@@ -936,13 +1098,18 @@ class XiaoMusicDevice:
return
self.log.info(f"{name}】已经开始播放了")
await self.xiaomusic.analytics.send_play_event(name, sec)
# 设置下一首歌曲的播放定时器
if sec <= 1:
self.log.info(f"{name}】不会设置下一首歌的定时器")
return
sec = sec + self.config.delay_sec
self._start_time = time.time()
self._duration = sec
self._paused_time = 0
await self.set_next_music_timeout(sec)
self.xiaomusic.save_cur_config()
async def do_tts(self, value):
self.log.info(f"try do_tts value:{value}")
@@ -956,7 +1123,7 @@ class XiaoMusicDevice:
# 最大等8秒
sec = min(8, int(len(value) / 3))
await asyncio.sleep(sec)
self.log.info(f"do_tts ok. cur_music:{self.cur_music}")
self.log.info(f"do_tts ok. cur_music:{self.get_cur_music()}")
await self.check_replay()
async def force_stop_xiaoai(self, device_id):
@@ -1038,9 +1205,14 @@ class XiaoMusicDevice:
# 继续播放被打断的歌曲
async def check_replay(self):
if self.isplaying() and not self.isdownloading():
# 继续播放歌曲
self.log.info("现在继续播放歌曲")
await self._play()
if not self.config.continue_play:
# 重新播放歌曲
self.log.info("现在重新播放歌曲")
await self._play()
else:
self.log.info(
f"继续播放歌曲. self.config.continue_play:{self.config.continue_play}"
)
else:
self.log.info(
f"不会继续播放歌曲. isplaying:{self.isplaying()} isdownloading:{self.isdownloading()}"
@@ -1058,51 +1230,62 @@ class XiaoMusicDevice:
self.log.info(f"add_download_music add_music {name}")
self.log.debug(self._play_list)
# 获取下一首
def get_next_music(self):
def get_music(self, direction="next"):
play_list_len = len(self._play_list)
if play_list_len == 0:
self.log.warning("当前播放列表没有歌曲")
return ""
index = 0
try:
index = self._play_list.index(self.cur_music)
index = self._play_list.index(self.get_cur_music())
except ValueError:
pass
if play_list_len == 1:
next_index = index # 当只有一首歌曲时保持当前索引不变
new_index = index # 当只有一首歌曲时保持当前索引不变
else:
# 顺序往后找1个
next_index = index + 1
if next_index >= play_list_len:
next_index = 0
# 排除当前歌曲随机找1个
if self.device.play_type == PLAY_TYPE_RND:
indices = list(range(play_list_len))
indices.remove(index)
next_index = random.choice(indices)
name = self._play_list[next_index]
if direction == "next":
new_index = index + 1
if new_index >= play_list_len:
new_index = 0
elif direction == "prev":
new_index = index - 1
if new_index < 0:
new_index = play_list_len - 1
else:
self.log.error("无效的方向参数")
return ""
name = self._play_list[new_index]
if not self.xiaomusic.is_music_exist(name):
self._play_list.pop(next_index)
self.log.info(f"pop not exist music:{name}")
return self.get_next_music()
self._play_list.pop(new_index)
self.log.info(f"pop not exist music: {name}")
return self.get_music(direction)
return name
# 获取下一首
def get_next_music(self):
return self.get_music(direction="next")
# 获取上一首
def get_prev_music(self):
return self.get_music(direction="prev")
# 判断是否播放下一首歌曲
def check_play_next(self):
# 当前歌曲不在当前播放列表
if self.cur_music not in self._play_list:
self.log.info(f"当前歌曲 {self.cur_music} 不在当前播放列表")
if self.get_cur_music() not in self._play_list:
self.log.info(f"当前歌曲 {self.get_cur_music()} 不在当前播放列表")
return True
# 当前没我在播放的歌曲
if self.cur_music == "":
if self.get_cur_music() == "":
self.log.info("当前没我在播放的歌曲")
return True
else:
# 当前播放的歌曲不存在了
if not self.xiaomusic.is_music_exist(self.cur_music):
self.log.info(f"当前播放的歌曲 {self.cur_music} 不存在了")
if not self.xiaomusic.is_music_exist(self.get_cur_music()):
self.log.info(f"当前播放的歌曲 {self.get_cur_music()} 不存在了")
return True
return False
@@ -1113,22 +1296,32 @@ class XiaoMusicDevice:
self.log.exception(f"Execption {e}")
# 同一组设备播放
async def group_player_play(self, url):
async def group_player_play(self, url, name=""):
device_id_list = self.xiaomusic.get_group_device_id_list(self.group_name)
tasks = [self.play_one_url(device_id, url) for device_id in device_id_list]
tasks = [
self.play_one_url(device_id, url, name) for device_id in device_id_list
]
results = await asyncio.gather(*tasks)
self.log.info(f"group_player_play {url} {device_id_list} {results}")
return results
async def play_one_url(self, device_id, url):
async def play_one_url(self, device_id, url, name):
ret = None
try:
if self.config.use_music_api:
audio_id = await self._get_audio_id(name)
if self.config.continue_play:
ret = await self.xiaomusic.mina_service.play_by_music_url(
device_id, url
device_id, url, _type=1, audio_id=audio_id
)
self.log.info(
f"play_one_url play_by_music_url device_id:{device_id} ret:{ret} url:{url}"
f"play_one_url continue_play device_id:{device_id} ret:{ret} url:{url} audio_id:{audio_id}"
)
elif self.config.use_music_api:
ret = await self.xiaomusic.mina_service.play_by_music_url(
device_id, url, audio_id=audio_id
)
self.log.info(
f"play_one_url play_by_music_url device_id:{device_id} ret:{ret} url:{url} audio_id:{audio_id}"
)
else:
ret = await self.xiaomusic.mina_service.play_by_url(device_id, url)
@@ -1139,6 +1332,46 @@ class XiaoMusicDevice:
self.log.exception(f"Execption {e}")
return ret
async def _get_audio_id(self, name):
audio_id = 1582971365183456177
if not (self.config.use_music_api or self.config.continue_play):
return str(audio_id)
try:
params = {
"query": name,
"queryType": 1,
"offset": 0,
"count": 6,
"timestamp": int(time.time_ns() / 1000),
}
response = await self.xiaomusic.mina_service.mina_request(
"/music/search", params
)
for song in response["data"]["songList"]:
if song["originName"] == "QQ音乐":
audio_id = song["audioID"]
break
# 没找到QQ音乐的歌曲取第一个
if audio_id == 1582971365183456177:
audio_id = response["data"]["songList"][0]["audioID"]
self.log.debug(f"_get_audio_id. name: {name} songId:{audio_id}")
except Exception as e:
self.log.error(f"_get_audio_id {e}")
return str(audio_id)
# 重置计时器
async def reset_timer_when_answer(self, answer_length):
if not (self.isplaying() and self.config.continue_play):
return
pause_time = answer_length / 5 + 1
offset, duration = self.get_offset_duration()
self._paused_time += pause_time
new_time = duration - offset + pause_time
await self.set_next_music_timeout(new_time)
self.log.info(
f"reset_timer 延长定时器. answer_length:{answer_length} pause_time:{pause_time}"
)
# 设置下一首歌曲的播放定时器
async def set_next_music_timeout(self, sec):
self.cancel_next_timer()
@@ -1180,11 +1413,12 @@ class XiaoMusicDevice:
self.xiaomusic.save_cur_config()
tts = PLAY_TYPE_TTS[play_type]
await self.do_tts(tts)
self.update_playlist()
async def play_music_list(self, list_name, music_name):
self._last_cmd = "play_music_list"
self._cur_play_list = list_name
self._play_list = self.xiaomusic.music_list[list_name]
self.device.cur_playlist = list_name
self.update_playlist()
self.log.info(f"开始播放列表{list_name}")
await self._play(music_name)
@@ -1234,7 +1468,7 @@ class XiaoMusicDevice:
device.cancel_next_timer()
def get_cur_play_list(self):
return self._cur_play_list
return self.device.cur_playlist
# 清空所有定时器
def cancel_all_timer(self):