mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4efa0d879 | ||
|
|
2ebc0f11d2 | ||
|
|
180f28800e | ||
|
|
7f5692d6cd | ||
|
|
8d7b5337eb | ||
|
|
dcbf4330be | ||
|
|
2e53f20d80 | ||
|
|
2914ddcc36 | ||
|
|
442756e3cc | ||
|
|
b55a2a67c9 | ||
|
|
09545c7015 | ||
|
|
1f82efa2a1 | ||
|
|
e509052242 | ||
|
|
046cdade5a | ||
|
|
7f8e639b86 | ||
|
|
bfbd36f7f9 | ||
|
|
c5d623547c | ||
|
|
31c61675bf | ||
|
|
9900bd9ee9 | ||
|
|
8459494c61 | ||
|
|
9852feec81 | ||
|
|
ce79c0f0f7 | ||
|
|
51037fc714 | ||
|
|
f6b9178688 | ||
|
|
7ccbd6ce79 | ||
|
|
30926c6b79 | ||
|
|
270076b9a7 | ||
|
|
ba58d45d8b | ||
|
|
0543c92f37 | ||
|
|
f40a4c5c7b | ||
|
|
cc05933992 | ||
|
|
896eae92ff | ||
|
|
07676e8c5d | ||
|
|
4ec70a210b | ||
|
|
0b395f26ed | ||
|
|
965a8be5bb | ||
|
|
36ddfc8885 | ||
|
|
f82957c73f | ||
|
|
f1625e7d92 | ||
|
|
48797ddf8f | ||
|
|
6f67f515b2 | ||
|
|
9a0146b04e | ||
|
|
7fdb28c352 | ||
|
|
5619584481 | ||
|
|
3f0a1cb8f5 | ||
|
|
2f6105843b | ||
|
|
d71f99de53 | ||
|
|
d7385405d9 | ||
|
|
781e5ebb2f | ||
|
|
980772bf9c | ||
|
|
632e411c6e | ||
|
|
789d442029 | ||
|
|
0452d49930 | ||
|
|
19d781fa1f | ||
|
|
ad82d13a7e | ||
|
|
9c4d757dc0 | ||
|
|
e369d80875 | ||
|
|
9b0c8510a3 | ||
|
|
94fb158d7d | ||
|
|
11df6e9f0c | ||
|
|
4e8550a56c | ||
|
|
7f349410a0 | ||
|
|
6610c29fe4 | ||
|
|
3da1b8eac1 | ||
|
|
003068e62c | ||
|
|
1d12f0d508 | ||
|
|
1ee4667a79 | ||
|
|
521605e9c8 | ||
|
|
9add14408c | ||
|
|
e554ace7ae | ||
|
|
cdd0cdd237 | ||
|
|
c713d32230 | ||
|
|
8ee4e88f82 | ||
|
|
ca7679e9d3 | ||
|
|
b8c18ef33b | ||
|
|
1ce324151e | ||
|
|
faa452253c | ||
|
|
ae41ae57b3 | ||
|
|
6d3fe9381d | ||
|
|
f879c0aeb9 | ||
|
|
58ffb93d3f | ||
|
|
8e5df7094c | ||
|
|
64c2f54ff0 | ||
|
|
d1b869ae43 | ||
|
|
d3895f2632 | ||
|
|
5bf62c4b1a | ||
|
|
406e09922c | ||
|
|
ae34572d13 | ||
|
|
1e3c69ea90 | ||
|
|
3c232505f8 | ||
|
|
44177db9b6 | ||
|
|
e72ae973bc | ||
|
|
4ab3c5cbee | ||
|
|
4e532d298d | ||
|
|
3372440f4e | ||
|
|
1255239912 | ||
|
|
e401a73595 |
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -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.
|
||||
|
||||
130
CHANGELOG.md
130
CHANGELOG.md
@@ -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
263
README.md
@@ -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 | [小爱音箱Play(2019款)](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:
|
||||
- 
|
||||
|
||||
@@ -77,5 +77,6 @@
|
||||
},
|
||||
"enable_force_stop": false,
|
||||
"devices": {},
|
||||
"group_list": ""
|
||||
}
|
||||
"group_list": "",
|
||||
"convert_to_mp3": false
|
||||
}
|
||||
|
||||
14
pdm.lock
generated
14
pdm.lock
generated
@@ -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"
|
||||
|
||||
@@ -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, "你好,我是自定义的测试口令")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.3.25"
|
||||
__version__ = "0.3.36"
|
||||
|
||||
82
xiaomusic/analytics.py
Normal file
82
xiaomusic/analytics.py
Normal 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)
|
||||
@@ -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__":
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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"))
|
||||
|
||||
@@ -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')}`;
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
92
xiaomusic/static/default/index.html
Normal file
92
xiaomusic/static/default/index.html
Normal 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>
|
||||
@@ -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>
|
||||
BIN
xiaomusic/static/default/qrcode.png
Normal file
BIN
xiaomusic/static/default/qrcode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
BIN
xiaomusic/static/pure/assets/guidance-BdU7g-Gp.png
Normal file
BIN
xiaomusic/static/pure/assets/guidance-BdU7g-Gp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
40
xiaomusic/static/pure/assets/index-C9BDvfvm.js
Normal file
40
xiaomusic/static/pure/assets/index-C9BDvfvm.js
Normal file
File diff suppressed because one or more lines are too long
1
xiaomusic/static/pure/assets/index-DUo8JY-R.css
Normal file
1
xiaomusic/static/pure/assets/index-DUo8JY-R.css
Normal file
File diff suppressed because one or more lines are too long
BIN
xiaomusic/static/pure/favicon.ico
Normal file
BIN
xiaomusic/static/pure/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
26
xiaomusic/static/pure/index.html
Normal file
26
xiaomusic/static/pure/index.html
Normal 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
BIN
xiaomusic/static/xiaoai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 528 KiB |
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user