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

Compare commits

...

120 Commits

Author SHA1 Message Date
涵曦
4de032e193 bump: version 0.3.19 → 0.3.20 2024-07-30 06:09:27 +00:00
涵曦
d655157e1d build: update static version 2024-07-30 06:09:26 +00:00
涵曦
ff06958ab3 fix: 尝试修复延迟问题,修复播放停止不了的问题 2024-07-30 06:09:18 +00:00
涵曦
6e98b5ee2b bump: version 0.3.18 → 0.3.19 2024-07-30 01:59:32 +00:00
涵曦
da90fe2633 build: update static version 2024-07-30 01:59:31 +00:00
涵曦
eaedef452c fix: 调整配置,优化获取歌曲时长接口 2024-07-30 01:09:01 +00:00
涵曦
2d5f3799a3 bump: version 0.3.17 → 0.3.18 2024-07-29 10:16:10 +00:00
涵曦
b52bfe0848 build: update static version 2024-07-29 10:16:09 +00:00
涵曦
e2261b2d19 fix: #135 修复获取不到播放时长时只播放3秒的问题 2024-07-29 10:15:49 +00:00
涵曦
2443444165 Update release.yml 2024-07-29 05:51:04 +08:00
涵曦
7c912a51be bump: version 0.3.16 → 0.3.17 2024-07-28 21:42:49 +00:00
涵曦
bda55a1faa build: update static version 2024-07-28 21:42:48 +00:00
涵曦
5b5f957f8e fix: 优化日志输出,尝试排查延迟播放的问题 2024-07-28 21:42:23 +00:00
涵曦
12f54e3ad4 Update release.yml 2024-07-29 04:48:39 +08:00
quxiaozha
d6594e1270 Fix docker run command 2024-07-29 04:42:02 +08:00
涵曦
b678447417 bump: version 0.3.15 → 0.3.16 2024-07-28 13:58:52 +00:00
涵曦
02508f6997 build: update static version 2024-07-28 13:58:52 +00:00
涵曦
d50fff9e31 build: 修复版本打包问题 2024-07-28 13:58:44 +00:00
涵曦
2d7d7ddc95 bump: version 0.3.14 → 0.3.15 2024-07-28 13:07:36 +00:00
涵曦
9c9825d423 build: update static version 2024-07-28 13:07:35 +00:00
涵曦
f788c0f37b fix: 修复自定义口令重复的问题 2024-07-28 13:07:14 +00:00
涵曦
36d72d1eca fix: 修复日志输出问题 2024-07-28 12:59:32 +00:00
涵曦
6b17779c59 Update release.yml 2024-07-28 17:33:43 +08:00
涵曦
759130e38d fix: 修复退出异常问题 2024-07-28 08:48:39 +00:00
涵曦
49f727477e style: 优化日志输出,方便排查问题 2024-07-28 08:09:34 +00:00
涵曦
43886116c1 bump: version 0.3.13 → 0.3.14 2024-07-28 02:02:09 +00:00
涵曦
a38194027d build: update static version 2024-07-28 02:02:08 +00:00
涵曦
eab4f4bd46 feat: 优化播放延迟问题,并新增配置下一首播放的延迟秒数 2024-07-28 00:51:20 +00:00
涵曦
6b38676766 build: 打包优化 2024-07-26 14:21:43 +00:00
涵曦
3830f58c0b bump: version 0.3.12 → 0.3.13 2024-07-24 23:07:11 +00:00
涵曦
34cdea1731 build: update static version 2024-07-24 23:07:10 +00:00
涵曦
22f545b99c fix: 解决 docker 镜像问题 2024-07-24 21:58:28 +00:00
涵曦
37f73dc31f bump: version 0.3.11 → 0.3.12 2024-07-24 13:34:37 +00:00
涵曦
10e52f0b63 build: update static version 2024-07-24 13:34:36 +00:00
涵曦
b0ac1034d2 feat: 优化获取文件播放时长接口,尝试解决播放延迟和操作面板卡顿的问题 2024-07-24 01:47:46 +00:00
涵曦
48d663a764 bump: version 0.3.10 → 0.3.11 2024-07-22 15:30:55 +00:00
涵曦
08ab75b390 build: update static version 2024-07-22 15:30:54 +00:00
涵曦
71dfc6d468 fix: #130 单曲循环的模式下,播放列表的指令不生效 2024-07-22 15:30:30 +00:00
涵曦
7877775495 refactor: 优化代码 2024-07-19 14:25:56 +00:00
涵曦
7744a75773 build: 更新依赖库 2024-07-19 14:14:49 +00:00
bowji
a28a0267e4 feat: Add remove mp3 id3 tag function
Signed-off-by: bowji <bowji@bowji-m-dnpx.dclife.fun>

Signed-off-by: Bowen Ji <jibwf@hotmail.com>
2024-07-19 14:09:05 +00:00
涵曦
417a5c924a bump: version 0.3.9 → 0.3.10 2024-07-19 12:18:50 +00:00
涵曦
d4bc1c49ea build: update static version 2024-07-19 12:18:49 +00:00
涵曦
51ef9e08fe fix: 修复软连接目录不能播放的问题 2024-07-19 06:10:14 +00:00
涵曦
285203a342 style: 优化日志输出 2024-07-18 09:54:31 +00:00
涵曦
af6077693e fix: 修复自定义语音口令设置不生效的问题 2024-07-18 01:52:04 +00:00
涵曦
40ac67cce0 Update FUNDING.yml 2024-07-18 00:15:38 +08:00
涵曦
96d03c5c29 feat: 支持软连接的接口直接用os.walk即可 2024-07-17 14:58:03 +00:00
涵曦
82aa453e50 bump: version 0.3.8 → 0.3.9 2024-07-17 10:59:13 +00:00
涵曦
1718211619 build: update static version 2024-07-17 10:59:13 +00:00
涵曦
09310675fc feat: #119 音乐目录支持软连接 2024-07-17 09:57:24 +00:00
涵曦
ca711bbdb8 fix: 修复日志下载报错问题 2024-07-17 06:05:15 +00:00
涵曦
fb44f88df2 fix: 兼容旧的setting.json文件中conf_path为空的情况 2024-07-17 06:05:15 +00:00
涵曦
e5b32b2831 fix: 修复设置页面可能打不开的问题 2024-07-17 06:05:15 +00:00
涵曦
19ddbb7ca9 Update README.md 2024-07-17 00:32:42 +08:00
涵曦
3e82d7acdc build: 默认映射目录设置 2024-07-16 16:25:01 +00:00
涵曦
3921c70c86 bump: version 0.3.7 → 0.3.8 2024-07-16 13:58:04 +00:00
涵曦
aea9333e57 build: update static version 2024-07-16 13:58:03 +00:00
涵曦
7043ca31cf fix: 修复播放url接口问题 2024-07-16 13:57:37 +00:00
涵曦
963d86de7c bump: version 0.3.6 → 0.3.7 2024-07-16 09:38:51 +00:00
涵曦
d6b0e974b7 build: update static version 2024-07-16 09:38:50 +00:00
涵曦
8a8340a159 feat: 播放链接按钮对应给个默认的链接用于测试 2024-07-16 09:38:36 +00:00
涵曦
20d3c9fce9 feat: Uvicorn 的日志信息合并到 xiaomusic 日志里显示 2024-07-16 05:01:50 +00:00
涵曦
729549a7a9 bump: version 0.3.5 → 0.3.6 2024-07-15 17:22:30 +00:00
涵曦
d28614177c build: update static version 2024-07-15 17:22:30 +00:00
涵曦
db53517784 fix: #126 修复pip安装时主页打不开的问题 2024-07-15 17:22:24 +00:00
涵曦
186e9c1417 bump: version 0.3.4 → 0.3.5 2024-07-15 17:17:26 +00:00
涵曦
7498016d61 build: update static version 2024-07-15 17:17:26 +00:00
涵曦
7114ea2e6e fix: #116 播放失败自动切下首歌 2024-07-15 17:17:08 +00:00
涵曦
e8ca1f8678 bump: version 0.3.3 → 0.3.4 2024-07-15 16:46:06 +00:00
涵曦
2275bc2600 build: update static version 2024-07-15 16:46:06 +00:00
涵曦
45a94f4bfe fix: #125 修复本地英文歌曲匹大小写字母配不到的问题 2024-07-15 16:45:27 +00:00
涵曦
5cb2c84715 build: update code 2024-07-15 15:40:16 +00:00
涵曦
a3bf8d8aaa build: 静态文件版本自动更新脚本修改 2024-07-15 15:37:45 +00:00
涵曦
0a88b7be26 build: update static version 2024-07-15 15:24:26 +00:00
涵曦
05da3e8fc4 bump: version 0.3.2 → 0.3.3 2024-07-15 15:19:40 +00:00
涵曦
560ea1aeca fix: 尝试修复播放卡顿问题 see #124 2024-07-15 15:16:38 +00:00
涵曦
5ef1f2d940 ci: 发版本自动更新静态文件版本号 2024-07-15 14:53:25 +00:00
涵曦
e01fcdcecd bump: version 0.3.1 → 0.3.2 2024-07-15 14:51:09 +00:00
涵曦
8ff04dd0f6 fix: #122 pip安装方式下,static目录找不到报错 2024-07-15 14:44:57 +00:00
涵曦
4a7b5ac2b0 fix: 版本更新时更新页面缓存 2024-07-15 07:07:16 +00:00
涵曦
ac74ef3c15 Update README.md 2024-07-15 10:15:35 +08:00
涵曦
8913057c27 bump: version 0.3.0 → 0.3.1 2024-07-15 00:59:55 +00:00
涵曦
a75030a73c fix: 修复主页选择设备不生效的问题 see #120 2024-07-15 00:58:59 +00:00
涵曦
e2e74f9459 Update README.md 2024-07-14 23:14:16 +08:00
涵曦
09febb66dc bump: version 0.2.0 → 0.3.0 2024-07-14 14:00:05 +00:00
涵曦
ad0db8c9a9 Update README.md 2024-07-14 21:49:01 +08:00
涵曦
90b0fecd3b feat: 建议音乐目录和配置目录分开不同目录 2024-07-14 13:46:22 +00:00
涵曦
a8f88e8bfc fix: #114 修复部分 mp3 文件长度识别错误 2024-07-14 13:20:38 +00:00
涵曦
d8035f0713 fix: 删除 armv6 的支持 2024-07-14 12:48:05 +00:00
涵曦
76c1a8952f fix: 修复编译问题 2024-07-14 12:38:48 +00:00
涵曦
d8a66ca152 fix: 修复音乐路径设置后找不到音乐的问题 2024-07-14 09:52:14 +00:00
涵曦
dd77176035 fix: 修复启动报错的问题 2024-07-14 08:39:16 +00:00
涵曦
6d7d90d642 feat: 优化后台网络设置,同时支持ipv4和ipv6 2024-07-14 07:24:23 +00:00
涵曦
514bf9b8b2 Update README.md 2024-07-13 12:28:48 +08:00
涵曦
cd5869ff84 feat: 使用fastapi替换flask,解决多线程问题 2024-07-13 01:56:40 +00:00
涵曦
ee6f4ee4e4 feat: #106 网页上显示音箱当前状态(播放中or空闲中)以及当前的播放模式 2024-07-11 11:09:12 +00:00
涵曦
0189a00155 feat: 优化首页加载慢的问题 2024-07-11 10:34:23 +00:00
涵曦
e3d60d3f2e fix: 修复CI警告问题 2024-07-11 04:03:27 +00:00
涵曦
27fecd788b feat: 优化设置页面布局,方便配置必须项 2024-07-11 03:54:25 +00:00
涵曦
e9b1d94fb3 feat: 优化配置界面,支持配置分组 2024-07-11 03:40:10 +00:00
涵曦
043a9303a5 feat: 支持多设备分开播放 see #65 2024-07-11 03:05:56 +00:00
涵曦
aa698667c9 bump: version 0.1.101 → 0.2.0 2024-07-09 15:12:14 +00:00
涵曦
700e17854c Update README.md 2024-07-09 23:11:18 +08:00
涵曦
40258c9fa1 feat: 触屏版可以不用设置 XIAOMUSIC_USE_MUSIC_API 2024-07-09 15:07:46 +00:00
涵曦
eb59bf0db5 feat: 升级依赖库 2024-07-09 15:03:20 +00:00
涵曦
a9df78af97 Update README.md 2024-07-08 18:00:02 +08:00
涵曦
901506a32d feat: 唤醒口令配置支持配语音词,简化自定义口令配置 see #105 2024-07-08 01:06:01 +00:00
涵曦
0ddbe58fbd bump: version 0.1.100 → 0.1.101 2024-07-07 08:47:33 +00:00
涵曦
350d82184f fix: #81 修复播放列表时,当前歌曲不在列表没有更换歌曲的问题 2024-07-07 08:43:55 +00:00
涵曦
5b8054abd9 fix: #110 修复配置加载问题 2024-07-07 08:28:09 +00:00
涵曦
c5c691b653 bump: version 0.1.99 → 0.1.100 2024-07-07 06:59:44 +00:00
涵曦
7a44c8587c fix: 日志代码写错 2024-07-07 06:41:45 +00:00
涵曦
5092ffc91a bump: version 0.1.98 → 0.1.99 2024-07-07 06:39:08 +00:00
涵曦
2da12e12d5 fix: #81 修复播放列表没有继续播放上次播放的歌曲,并把随机播放,全部循环,单曲循环状态落地 2024-07-07 06:21:15 +00:00
涵曦
5aff72dbb6 bump: version 0.1.97 → 0.1.98 2024-07-07 05:48:40 +00:00
涵曦
043f452e71 Update README.md 2024-07-07 13:47:30 +08:00
涵曦
5cedf8a907 fix: 修复多设备获取不到对话记录的问题 see #65 2024-07-07 04:46:33 +00:00
涵曦
ae77c7232e Update README.md 2024-07-07 11:42:05 +08:00
涵曦
d559413d46 fix: #93 修复目录深度设置后导致目录下的歌曲无法加到播放列表里的问题 2024-07-07 02:03:17 +00:00
29 changed files with 2822 additions and 2237 deletions

2
.github/FUNDING.yml vendored
View File

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

View File

@@ -2,7 +2,8 @@ name: ci
on:
push:
branches: [ main ]
branches:
- "*"
workflow_dispatch:
jobs:
@@ -25,7 +26,7 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}
- name: Docker Hub Description

View File

@@ -10,31 +10,25 @@ on:
workflow_dispatch:
jobs:
release-pypi:
name: Build and Release PyPI
pypi-publish:
name: upload release to PyPI
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-python@v4
with:
python-version: "3.10"
- uses: pdm-project/setup-pdm@v3
- name: Publish package distributions to PyPI
run: pdm publish
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Build artifacts
run: |
pip install build
python -m build
- uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
node-version: 16.x
- run: npx changelogithub
continue-on-error: true
@@ -61,6 +55,6 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable

2
.gitignore vendored
View File

@@ -165,3 +165,5 @@ ffmpeg
music
test.sh
conf
setting.json
.DS_Store

View File

@@ -1,3 +1,198 @@
## v0.3.20 (2024-07-30)
### Fix
- 尝试修复延迟问题,修复播放停止不了的问题
## v0.3.19 (2024-07-30)
### Fix
- 调整配置,优化获取歌曲时长接口
## v0.3.18 (2024-07-29)
### Fix
- #135 修复获取不到播放时长时只播放3秒的问题
## v0.3.17 (2024-07-28)
### Fix
- 优化日志输出,尝试排查延迟播放的问题
## v0.3.16 (2024-07-28)
## v0.3.15 (2024-07-28)
### Fix
- 修复自定义口令重复的问题
- 修复日志输出问题
- 修复退出异常问题
## v0.3.14 (2024-07-28)
### Feat
- 优化播放延迟问题,并新增配置下一首播放的延迟秒数
## v0.3.13 (2024-07-24)
### Fix
- 解决 docker 镜像问题
## v0.3.12 (2024-07-24)
### Feat
- 优化获取文件播放时长接口,尝试解决播放延迟和操作面板卡顿的问题
## v0.3.11 (2024-07-22)
### Feat
- Add remove mp3 id3 tag function
### Fix
- #130 单曲循环的模式下,播放列表的指令不生效
### Refactor
- 优化代码
## v0.3.10 (2024-07-19)
### Feat
- 支持软连接的接口直接用os.walk即可
### Fix
- 修复软连接目录不能播放的问题
- 修复自定义语音口令设置不生效的问题
## v0.3.9 (2024-07-17)
### Feat
- #119 音乐目录支持软连接
### Fix
- 修复日志下载报错问题
- 兼容旧的setting.json文件中conf_path为空的情况
- 修复设置页面可能打不开的问题
## v0.3.8 (2024-07-16)
### Fix
- 修复播放url接口问题
## v0.3.7 (2024-07-16)
### Feat
- 播放链接按钮对应给个默认的链接用于测试
- Uvicorn 的日志信息合并到 xiaomusic 日志里显示
## v0.3.6 (2024-07-15)
### Fix
- #126 修复pip安装时主页打不开的问题
## v0.3.5 (2024-07-15)
### Fix
- #116 播放失败自动切下首歌
## v0.3.4 (2024-07-15)
### Fix
- #125 修复本地英文歌曲匹大小写字母配不到的问题
## v0.3.3 (2024-07-15)
### Fix
- 尝试修复播放卡顿问题 see #124
## v0.3.2 (2024-07-15)
### Fix
- #122 pip安装方式下static目录找不到报错
- 版本更新时更新页面缓存
## v0.3.1 (2024-07-15)
### Fix
- 修复主页选择设备不生效的问题 see #120
## v0.3.0 (2024-07-14)
### Feat
- 建议音乐目录和配置目录分开不同目录
- 优化后台网络设置同时支持ipv4和ipv6
- 使用fastapi替换flask,解决多线程问题
- #106 网页上显示音箱当前状态播放中or空闲中以及当前的播放模式
- 优化首页加载慢的问题
- 优化设置页面布局,方便配置必须项
- 优化配置界面,支持配置分组
- 支持多设备分开播放 see #65
### Fix
- #114 修复部分 mp3 文件长度识别错误
- 删除 armv6 的支持
- 修复编译问题
- 修复音乐路径设置后找不到音乐的问题
- 修复启动报错的问题
- 修复CI警告问题
## v0.2.0 (2024-07-09)
### Feat
- 触屏版可以不用设置 XIAOMUSIC_USE_MUSIC_API
- 升级依赖库
- 唤醒口令配置支持配语音词,简化自定义口令配置 see #105
## v0.1.101 (2024-07-07)
### Fix
- #81 修复播放列表时,当前歌曲不在列表没有更换歌曲的问题
- #110 修复配置加载问题
## v0.1.100 (2024-07-07)
### Fix
- 日志代码写错
## v0.1.99 (2024-07-07)
### Fix
- #81 修复播放列表没有继续播放上次播放的歌曲,并把随机播放,全部循环,单曲循环状态落地
## v0.1.98 (2024-07-07)
### Fix
- 修复多设备获取不到对话记录的问题 see #65
- #93 修复目录深度设置后导致目录下的歌曲无法加到播放列表里的问题
## v0.1.97 (2024-07-06)
### Fix

View File

@@ -1,8 +1,13 @@
FROM python:3.10 AS builder
ENV DEBIAN_FRONTEND=noninteractive
RUN pip install -U pdm
ENV PDM_CHECK_UPDATE=false
WORKDIR /app
COPY requirements.txt .
RUN python3 -m venv .venv && .venv/bin/pip install --no-cache-dir -r requirements.txt
COPY pyproject.toml README.md .
COPY xiaomusic/ ./xiaomusic/
COPY plugins/ ./plugins/
COPY xiaomusic.py .
RUN pdm install --prod --no-editable
COPY install_dependencies.sh .
RUN bash install_dependencies.sh
@@ -13,10 +18,10 @@ COPY --from=builder /app/ffmpeg /app/ffmpeg
COPY xiaomusic/ ./xiaomusic/
COPY plugins/ ./plugins/
COPY xiaomusic.py .
ENV XDG_CONFIG_HOME=/config
ENV XIAOMUSIC_HOSTNAME=192.168.2.5
ENV XIAOMUSIC_PORT=8090
VOLUME /config
VOLUME /app/conf
VOLUME /app/music
EXPOSE 8090
ENV PATH=/app/.venv/bin:$PATH
ENTRYPOINT [".venv/bin/python3","xiaomusic.py"]

View File

@@ -29,6 +29,7 @@ services:
- 8090:8090
volumes:
- ./music:/app/music
- ./conf:/app/conf
```
对应的 docker 启动命令如下:
@@ -36,12 +37,15 @@ services:
```yaml
docker run -p 8090:8090 \
-v ./music:/app/music \
-v ./conf:/app/conf \
hanxi/xiaomusic
```
启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改
其中 conf 目录为配置文件存放目录music 目录为音乐存放目录,建议分开配置为不同的目录
### ✨ 修改8090端口
启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
### ✨✨✨ 修改默认8090端口映射 ✨✨✨
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
@@ -66,7 +70,7 @@ services:
## pip 方式安装运行
```shell
> pip install xiaomusic
> pip install -U xiaomusic
> xiaomusic --help
__ __ _ __ __ _
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
@@ -116,6 +120,8 @@ export XIAOMUSIC_SEARCH='bilisearch:'
pdm run xiaomusic.py
````
如果是开发前端界面,可以通过 <http://localhost:8090/docs> 查看有什么接口。
### 支持口令
- **播放歌曲**
@@ -146,11 +152,13 @@ pdm run xiaomusic.py
| 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) |
| LX04 X10A X08A | 已经支持的触屏版 |
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
> 目前应该所有设备类型都已经支持播放,有问题随时反馈。
> 其他触屏版不能播放可以设置 XIAOMUSIC_USE_MUSIC_API 为 true 试试。
## 支持音乐格式
@@ -314,9 +322,9 @@ services:
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台具体见 <https://github.com/hanxi/xiaomusic/issues/47>
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,记得把目录映射到主机,默认情况会把配置存放在music目录,具体见 <https://github.com/hanxi/xiaomusic/issues/74>
- XIAOMUSIC_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 接口播放音乐,用于兼容不能播放的型号,触屏版目前还需要设置这个为 true 。
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号,如果发现需要设置这个选项的时候请告知我加一下设备型号,方便以后不用设置。 见 <https://github.com/hanxi/xiaomusic/issues/30>
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。

View File

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

9
newpatch.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
./update-static-version.py
git add xiaomusic/static
git commit -m 'build: update static version'
cz bump --check-consistency --increment patch
git push -u origin main --tags

View File

@@ -1,5 +1,9 @@
#!/bin/bash
cz bump --check-consistency --increment patch
./update-static-version.py
git add xiaomusic/static
git commit -m 'build: update static version'
cz bump --check-consistency
git push -u origin main --tags

1358
pdm.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,19 @@
[project]
name = "xiaomusic"
version = "0.1.97"
version = "0.3.20"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},
]
dependencies = [
"aiohttp>=3.8.6",
"miservice-fork>=2.5.0",
"miservice-fork>=2.7.0",
"mutagen>=1.47.0",
"yt-dlp>=2024.07.01",
"flask[async]>=3.0.1",
"waitress>=3.0.0",
"flask-HTTPAuth>=4.8.0",
"uvicorn>=0.30.1",
"fastapi>=0.111.0",
"starlette>=0.37.2",
"aiofiles>=24.1.0",
]
requires-python = ">=3.10"
readme = "README.md"
@@ -55,6 +56,9 @@ include = ["**/*.py", "**/*.pyi", "**/pyproject.toml"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = ["fastapi.Depends", "fastapi.params.Depends", "fastapi.Query", "fastapi.params.Query"]
[tool.pdm.scripts]
lint = "ruff check ."
fmt = "ruff format ."

View File

@@ -1,477 +0,0 @@
# This file is @generated by PDM.
# Please do not edit it manually.
aiohttp==3.9.5 \
--hash=sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c \
--hash=sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf \
--hash=sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81 \
--hash=sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f \
--hash=sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a \
--hash=sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771 \
--hash=sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb \
--hash=sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430 \
--hash=sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233 \
--hash=sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9 \
--hash=sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59 \
--hash=sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888 \
--hash=sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c \
--hash=sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c \
--hash=sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424 \
--hash=sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2 \
--hash=sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb \
--hash=sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a \
--hash=sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10 \
--hash=sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0 \
--hash=sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4 \
--hash=sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3 \
--hash=sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa \
--hash=sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a \
--hash=sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a \
--hash=sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323 \
--hash=sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6 \
--hash=sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832 \
--hash=sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75 \
--hash=sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6 \
--hash=sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d \
--hash=sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72 \
--hash=sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db \
--hash=sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da \
--hash=sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678 \
--hash=sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b \
--hash=sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f \
--hash=sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58 \
--hash=sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342 \
--hash=sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558 \
--hash=sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551 \
--hash=sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595 \
--hash=sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee \
--hash=sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d \
--hash=sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7 \
--hash=sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f
aiosignal==1.3.1 \
--hash=sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc \
--hash=sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17
asgiref==3.7.2 \
--hash=sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e \
--hash=sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed
async-timeout==4.0.3; python_version < "3.11" \
--hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \
--hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028
attrs==23.1.0 \
--hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
--hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
blinker==1.7.0 \
--hash=sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9 \
--hash=sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182
brotli==1.1.0; implementation_name == "cpython" \
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
--hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \
--hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \
--hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \
--hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \
--hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \
--hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \
--hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \
--hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \
--hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \
--hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \
--hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \
--hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \
--hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \
--hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \
--hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \
--hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \
--hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \
--hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \
--hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \
--hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \
--hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \
--hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \
--hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \
--hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \
--hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \
--hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \
--hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \
--hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \
--hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \
--hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \
--hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \
--hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \
--hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \
--hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \
--hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e
brotlicffi==1.1.0.0; implementation_name != "cpython" \
--hash=sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b \
--hash=sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171 \
--hash=sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb \
--hash=sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979 \
--hash=sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33 \
--hash=sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca \
--hash=sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f \
--hash=sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6 \
--hash=sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca \
--hash=sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112 \
--hash=sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391 \
--hash=sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8 \
--hash=sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0 \
--hash=sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35 \
--hash=sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820 \
--hash=sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838 \
--hash=sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613 \
--hash=sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5 \
--hash=sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851 \
--hash=sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814 \
--hash=sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc \
--hash=sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13 \
--hash=sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990 \
--hash=sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6 \
--hash=sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d \
--hash=sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808 \
--hash=sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14
certifi==2023.7.22 \
--hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \
--hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9
cffi==1.16.0; implementation_name != "cpython" \
--hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \
--hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \
--hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \
--hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \
--hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \
--hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \
--hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \
--hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \
--hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \
--hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \
--hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \
--hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \
--hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \
--hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \
--hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \
--hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \
--hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \
--hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \
--hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \
--hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \
--hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \
--hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \
--hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \
--hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \
--hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \
--hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \
--hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \
--hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \
--hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \
--hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \
--hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \
--hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \
--hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357
charset-normalizer==3.3.0 \
--hash=sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786 \
--hash=sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e \
--hash=sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8 \
--hash=sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa \
--hash=sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d \
--hash=sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382 \
--hash=sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678 \
--hash=sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b \
--hash=sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e \
--hash=sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596 \
--hash=sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69 \
--hash=sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c \
--hash=sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459 \
--hash=sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7 \
--hash=sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908 \
--hash=sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a \
--hash=sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8 \
--hash=sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d \
--hash=sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d \
--hash=sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34 \
--hash=sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6 \
--hash=sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e \
--hash=sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c \
--hash=sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078 \
--hash=sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4 \
--hash=sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403 \
--hash=sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0 \
--hash=sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9 \
--hash=sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05 \
--hash=sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec \
--hash=sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56 \
--hash=sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e \
--hash=sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455 \
--hash=sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65 \
--hash=sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78 \
--hash=sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df \
--hash=sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1 \
--hash=sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989 \
--hash=sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63 \
--hash=sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649 \
--hash=sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2 \
--hash=sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd \
--hash=sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5 \
--hash=sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe \
--hash=sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293 \
--hash=sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e \
--hash=sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
colorama==0.4.6; platform_system == "Windows" \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
flask==3.0.3 \
--hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3 \
--hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842
flask-HTTPAuth==4.8.0 \
--hash=sha256:66568a05bc73942c65f1e2201ae746295816dc009edd84b482c44c758d75097a \
--hash=sha256:a58fedd09989b9975448eef04806b096a3964a7feeebc0a78831ff55685b62b0
frozenlist==1.4.0 \
--hash=sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01 \
--hash=sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251 \
--hash=sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9 \
--hash=sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b \
--hash=sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0 \
--hash=sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b \
--hash=sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c \
--hash=sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467 \
--hash=sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1 \
--hash=sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300 \
--hash=sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea \
--hash=sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab \
--hash=sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb \
--hash=sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8 \
--hash=sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62 \
--hash=sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326 \
--hash=sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c \
--hash=sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431 \
--hash=sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3 \
--hash=sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956 \
--hash=sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472 \
--hash=sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc \
--hash=sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839 \
--hash=sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b \
--hash=sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f \
--hash=sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559 \
--hash=sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b \
--hash=sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95 \
--hash=sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb \
--hash=sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963 \
--hash=sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f
idna==3.4 \
--hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
--hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
itsdangerous==2.1.2 \
--hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \
--hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a
Jinja2==3.1.3 \
--hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \
--hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90
markdown-it-py==3.0.0 \
--hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
--hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
MarkupSafe==2.1.4 \
--hash=sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69 \
--hash=sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0 \
--hash=sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d \
--hash=sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74 \
--hash=sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d \
--hash=sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f \
--hash=sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6 \
--hash=sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656 \
--hash=sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc \
--hash=sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56 \
--hash=sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc \
--hash=sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250 \
--hash=sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc \
--hash=sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863 \
--hash=sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8 \
--hash=sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f \
--hash=sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2 \
--hash=sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e \
--hash=sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e \
--hash=sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb \
--hash=sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26 \
--hash=sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131 \
--hash=sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858 \
--hash=sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e \
--hash=sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84 \
--hash=sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7 \
--hash=sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea \
--hash=sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b \
--hash=sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6 \
--hash=sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475 \
--hash=sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
miservice-fork==2.6.1 \
--hash=sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c \
--hash=sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39
multidict==6.0.4 \
--hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \
--hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \
--hash=sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03 \
--hash=sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710 \
--hash=sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569 \
--hash=sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636 \
--hash=sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49 \
--hash=sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93 \
--hash=sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0 \
--hash=sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4 \
--hash=sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc \
--hash=sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8 \
--hash=sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed \
--hash=sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98 \
--hash=sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3 \
--hash=sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe \
--hash=sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988 \
--hash=sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c \
--hash=sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c \
--hash=sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0 \
--hash=sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5 \
--hash=sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a \
--hash=sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b \
--hash=sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982 \
--hash=sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7 \
--hash=sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461 \
--hash=sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc \
--hash=sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547 \
--hash=sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0 \
--hash=sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171 \
--hash=sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba
mutagen==1.47.0 \
--hash=sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99 \
--hash=sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719
pycparser==2.21; implementation_name != "cpython" \
--hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
--hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
pycryptodomex==3.19.0 \
--hash=sha256:09c9401dc06fb3d94cb1ec23b4ea067a25d1f4c6b7b118ff5631d0b5daaab3cc \
--hash=sha256:0b2f1982c5bc311f0aab8c293524b861b485d76f7c9ab2c3ac9a25b6f7655975 \
--hash=sha256:136b284e9246b4ccf4f752d435c80f2c44fc2321c198505de1d43a95a3453b3c \
--hash=sha256:2126bc54beccbede6eade00e647106b4f4c21e5201d2b0a73e9e816a01c50905 \
--hash=sha256:263de9a96d2fcbc9f5bd3a279f14ea0d5f072adb68ebd324987576ec25da084d \
--hash=sha256:50cb18d4dd87571006fd2447ccec85e6cec0136632a550aa29226ba075c80644 \
--hash=sha256:5b883e1439ab63af976656446fb4839d566bb096f15fc3c06b5a99cde4927188 \
--hash=sha256:5d73e9fa3fe830e7b6b42afc49d8329b07a049a47d12e0ef9225f2fd220f19b2 \
--hash=sha256:67c8eb79ab33d0fbcb56842992298ddb56eb6505a72369c20f60bc1d2b6fb002 \
--hash=sha256:7cb51096a6a8d400724104db8a7e4f2206041a1f23e58924aa3d8d96bcb48338 \
--hash=sha256:800a2b05cfb83654df80266692f7092eeefe2a314fa7901dcefab255934faeec \
--hash=sha256:a3866d68e2fc345162b1b9b83ef80686acfe5cec0d134337f3b03950a0a8bf56 \
--hash=sha256:a588a1cb7781da9d5e1c84affd98c32aff9c89771eac8eaa659d2760666f7139 \
--hash=sha256:a77b79852175064c822b047fee7cf5a1f434f06ad075cc9986aa1c19a0c53eb0 \
--hash=sha256:af83a554b3f077564229865c45af0791be008ac6469ef0098152139e6bd4b5b6 \
--hash=sha256:b801216c48c0886742abf286a9a6b117e248ca144d8ceec1f931ce2dd0c9cb40 \
--hash=sha256:bfb040b5dda1dff1e197d2ef71927bd6b8bfcb9793bc4dfe0bb6df1e691eaacb \
--hash=sha256:c01678aee8ac0c1a461cbc38ad496f953f9efcb1fa19f5637cbeba7544792a53 \
--hash=sha256:c74eb1f73f788facece7979ce91594dc177e1a9b5d5e3e64697dd58299e5cb4d \
--hash=sha256:d4dd3b381ff5a5907a3eb98f5f6d32c64d319a840278ceea1dcfcc65063856f3 \
--hash=sha256:edbe083c299835de7e02c8aa0885cb904a75087d35e7bab75ebe5ed336e8c3e2
pygments==2.16.1 \
--hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
--hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
rich==13.7.1 \
--hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \
--hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432
typing-extensions==4.9.0; python_version < "3.11" \
--hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \
--hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd
urllib3==2.0.6 \
--hash=sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2 \
--hash=sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564
waitress==3.0.0 \
--hash=sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1 \
--hash=sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669
websockets==12.0 \
--hash=sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b \
--hash=sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6 \
--hash=sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df \
--hash=sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b \
--hash=sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2 \
--hash=sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed \
--hash=sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd \
--hash=sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b \
--hash=sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931 \
--hash=sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30 \
--hash=sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370 \
--hash=sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be \
--hash=sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf \
--hash=sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b \
--hash=sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402 \
--hash=sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f \
--hash=sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123 \
--hash=sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603 \
--hash=sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45 \
--hash=sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558 \
--hash=sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4 \
--hash=sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480 \
--hash=sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447 \
--hash=sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8 \
--hash=sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04 \
--hash=sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c \
--hash=sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb \
--hash=sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b \
--hash=sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c \
--hash=sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92 \
--hash=sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113 \
--hash=sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f \
--hash=sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468 \
--hash=sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611 \
--hash=sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d \
--hash=sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca \
--hash=sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f \
--hash=sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2 \
--hash=sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077 \
--hash=sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2 \
--hash=sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374 \
--hash=sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc \
--hash=sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e \
--hash=sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53 \
--hash=sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399 \
--hash=sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547 \
--hash=sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3 \
--hash=sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870 \
--hash=sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5 \
--hash=sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7
Werkzeug==3.0.1 \
--hash=sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc \
--hash=sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10
yarl==1.9.2 \
--hash=sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571 \
--hash=sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7 \
--hash=sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191 \
--hash=sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea \
--hash=sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4 \
--hash=sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095 \
--hash=sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde \
--hash=sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0 \
--hash=sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528 \
--hash=sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6 \
--hash=sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be \
--hash=sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a \
--hash=sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8 \
--hash=sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6 \
--hash=sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608 \
--hash=sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82 \
--hash=sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3 \
--hash=sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d \
--hash=sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8 \
--hash=sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac \
--hash=sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8 \
--hash=sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0 \
--hash=sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb \
--hash=sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2 \
--hash=sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7 \
--hash=sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051 \
--hash=sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9 \
--hash=sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5 \
--hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \
--hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \
--hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560
yt-dlp==2024.7.1.232715.dev0 \
--hash=sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8 \
--hash=sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503

View File

@@ -1 +0,0 @@
pdm export --prod -o requirements.txt

57
update-static-version.py Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
import re
from pathlib import Path
def get_html_files(directory):
"""
获取指定目录下所有HTML文件的列表。
:param directory: 搜索HTML文件的目录。
:return: 搜索到的HTML文件的路径列表。
"""
return list(Path(directory).rglob("*.html"))
def update_html_version(html_files, version):
"""
更新HTML文件中所有以 /static/ 开头的CSS和JS文件引用的版本号。
:param html_files: 需要更新的HTML文件路径的列表。
:param version: 新的版本号字符串。
"""
pattern = re.compile(r'(/static/.*(css|js))\?version=[^"]*"')
# pattern = re.compile(r'(/static/.*html)\?version=[^"]*"')
for html_file in html_files:
if not html_file.exists():
print(f"文件 {html_file} 不存在。")
continue
html_content = html_file.read_text()
# 更新CSS和JS版本号
html_content = pattern.sub(rf'\g<1>?version={version}"', html_content)
# html_content = pattern.sub(fr'\g<1>"', html_content)
# 保存更改到HTML文件
html_file.write_text(html_content)
print(f"文件 {html_file} 已更新为使用新的版本号: {version}")
# 使用案例
if __name__ == "__main__":
import time
t = str(int(time.time()))
# 指定目录
html_directory = "xiaomusic/static" # 修改为实际的HTML文件目录路径
# 获取HTML文件列表
html_files_to_update = get_html_files(html_directory)
# 执行更新
update_html_version(html_files_to_update, t)

View File

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

View File

@@ -1,10 +1,12 @@
#!/usr/bin/env python3
import argparse
import asyncio
from xiaomusic import (
__version__,
)
import uvicorn
from xiaomusic import __version__
from xiaomusic.config import Config
from xiaomusic.httpserver import HttpInit
from xiaomusic.httpserver import app as HttpApp
from xiaomusic.xiaomusic import XiaoMusic
LOGO = r"""
@@ -74,8 +76,69 @@ def main():
config = Config.from_options(options)
xiaomusic = XiaoMusic(config)
loop = asyncio.get_event_loop()
loop.run_until_complete(xiaomusic.run_forever())
HttpInit(xiaomusic)
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
"datefmt": "[%X]",
"use_colors": False,
},
"access": {
"format": f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
"datefmt": "[%X]",
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
"file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"formatter": "access",
"filename": config.log_file,
"maxBytes": 10 * 1024 * 1024,
"backupCount": 1,
},
},
"loggers": {
"uvicorn": {
"handlers": [
"default",
"file",
],
"level": "INFO",
},
"uvicorn.error": {
"level": "INFO",
},
"uvicorn.access": {
"handlers": [
"access",
"file",
],
"level": "INFO",
"propagate": False,
},
},
}
uvicorn.run(
HttpApp,
host=["::", "0.0.0.0"],
port=config.port,
log_config=LOGGING_CONFIG,
)
if __name__ == "__main__":

View File

@@ -18,12 +18,10 @@ def default_key_word_dict():
"下一首": "play_next",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "random_play",
"随机播放": "set_random_play",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"set_volume#": "set_volume",
"get_volume#": "get_volume",
}
@@ -43,8 +41,6 @@ KEY_WORD_ARG_BEFORE_DICT = {
# 口令匹配优先级
def default_key_match_order():
return [
"set_volume#",
"get_volume#",
"分钟后关机",
"播放歌曲",
"下一首",
@@ -57,29 +53,39 @@ def default_key_match_order():
]
@dataclass
class Device:
did: str = ""
device_id: str = ""
hardware: str = ""
name: str = ""
play_type: int = ""
cur_music: str = ""
cur_playlist: str = ""
@dataclass
class Config:
account: str = os.getenv("MI_USER", "")
password: str = os.getenv("MI_PASS", "")
mi_did: str = os.getenv("MI_DID", "") # 逗号分割支持多设备
hardware: str = os.getenv("MI_HARDWARE", "L07A") # 逗号分割支持多设备
cookie: str = ""
verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
music_path: str = os.getenv(
"XIAOMUSIC_MUSIC_PATH", "music"
) # 只能是music目录下的子目录
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "")
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", None)
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")) # 监听端口
public_port: int = int(os.getenv("XIAOMUSIC_PUBLIC_PORT", 0)) # 歌曲访问端口
proxy: str | None = os.getenv("XIAOMUSIC_PROXY", None)
proxy: str = os.getenv("XIAOMUSIC_PROXY", None)
search_prefix: str = os.getenv(
"XIAOMUSIC_SEARCH", "bilisearch:"
) # "bilisearch:" or "ytsearch:"
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
active_cmd: str = os.getenv(
"XIAOMUSIC_ACTIVE_CMD", "play,random_play,playlocal,play_music_list,stop"
"XIAOMUSIC_ACTIVE_CMD", "play,set_random_play,playlocal,play_music_list,stop"
)
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir")
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
@@ -123,6 +129,14 @@ class Config:
enable_force_stop: bool = (
os.getenv("XIAOMUSIC_ENABLE_FORCE_STOP", "false").lower() == "true"
)
devices: dict[str, Device] = field(default_factory=dict)
group_list: str = os.getenv(
"XIAOMUSIC_GROUP_LIST", ""
) # did1:group_name,did2:group_name
remove_id3tag: bool = (
os.getenv("XIAOMUSIC_REMOVE_ID3TAG", "false").lower() == "true"
)
delay_sec: int = int(os.getenv("XIAOMUSIC_DELAY_SEC", 3)) # 下一首歌延迟播放秒数
def append_keyword(self, keys, action):
for key in keys.split(","):
@@ -133,23 +147,26 @@ class Config:
def append_user_keyword(self):
for k, v in self.user_key_word_dict.items():
self.key_word_dict[k] = v
self.key_match_order.append(k)
if k not in self.key_match_order:
self.key_match_order.append(k)
def init_keyword(self):
self.key_match_order = default_key_match_order()
self.append_keyword(self.keywords_playlocal, "playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_stop, "stop")
self.append_user_keyword()
def __post_init__(self) -> None:
if self.proxy:
validate_proxy(self.proxy)
self.append_keyword(self.keywords_playlocal, "playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_stop, "stop")
self.append_user_keyword()
self.init_keyword()
# 保存配置到 config-example.json 文件
if self.enable_config_example:
with open("config-example.json", "w") as f:
data = asdict(self)
json.dump(data, f, ensure_ascii=False, indent=4)
json.dump(data, f, ensure_ascii=False, indent=2)
@classmethod
def from_options(cls, options: argparse.Namespace) -> Config:
@@ -161,43 +178,54 @@ class Config:
config[key] = value
return cls(**config)
@classmethod
def convert_value(cls, k, v, type_hints):
if v is not None and k in type_hints:
expected_type = type_hints[k]
try:
if expected_type is bool:
converted_value = False
if str(v).lower() == "true":
converted_value = True
elif expected_type == dict[str, Device]:
converted_value = {}
for kk, vv in v.items():
converted_value[kk] = Device(**vv)
else:
converted_value = expected_type(v)
return converted_value
except (ValueError, TypeError) as e:
print(f"Error converting {k}:{v} to {expected_type}: {e}")
return None
@classmethod
def read_from_file(cls, config_path: str) -> dict:
result = {}
with open(config_path, "rb") as f:
config = json.load(f)
for key, value in config.items():
if value is not None and key in cls.__dataclass_fields__:
result[key] = value
data = json.load(f)
type_hints = get_type_hints(cls)
for k, v in data.items():
converted_value = cls.convert_value(k, v, type_hints)
if converted_value is not None:
result[k] = converted_value
return result
def update_config(self, data):
# 获取类型提示
type_hints = get_type_hints(self)
type_hints = get_type_hints(self, globals(), locals())
for k, v in data.items():
if v and k in type_hints:
# 获取字段的类型
expected_type = type_hints[k]
converted_value = self.convert_value(k, v, type_hints)
if converted_value is not None:
setattr(self, k, converted_value)
self.init_keyword()
# 根据期望的类型进行转换
if isinstance(v, expected_type):
# 如果v已经是正确的类型则直接赋值
setattr(self, k, v)
else:
# 尝试转换类型
try:
# 特殊情况处理(例如对布尔值的转换)
if expected_type is bool:
converted_value = False
if v and v.lower() == "true":
converted_value = True
else:
# 使用期望类型的构造函数进行转换
converted_value = expected_type(v)
except (ValueError, TypeError) as e:
print(f"Error converting {v} to {expected_type}: {e}")
continue
# 设置转换后的值
setattr(self, k, converted_value)
# 获取设置文件
def getsettingfile(self):
# 兼容旧配置空的情况
if not self.conf_path:
self.conf_path = "conf"
if not os.path.exists(self.conf_path):
os.makedirs(self.conf_path)
filename = os.path.join(self.conf_path, "setting.json")
return filename

View File

@@ -9,3 +9,13 @@ SUPPORT_MUSIC_TYPE = [
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}&timestamp={timestamp}&limit=2"
COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}"
PLAY_TYPE_ONE = 0 # 单曲循环
PLAY_TYPE_ALL = 1 # 全部循环
PLAY_TYPE_RND = 2 # 随机播放
PLAY_TYPE_TTS = {
PLAY_TYPE_ONE: "已经设置为单曲循环",
PLAY_TYPE_ALL: "已经设置为全部循环",
PLAY_TYPE_RND: "已经设置为随机播放",
}

View File

@@ -1,155 +1,244 @@
#!/usr/bin/env python3
import asyncio
import json
import os
import secrets
import shutil
import tempfile
from contextlib import asynccontextmanager
from dataclasses import asdict
from threading import Thread
from typing import Annotated
from flask import Flask, request, send_file, send_from_directory
from flask_httpauth import HTTPBasicAuth
from waitress import serve
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from starlette.background import BackgroundTask
from starlette.responses import FileResponse
from xiaomusic import (
__version__,
)
from xiaomusic import __version__
from xiaomusic.utils import (
deepcopy_data_no_sensitive_info,
downloadfile,
)
app = Flask(__name__)
auth = HTTPBasicAuth()
host = "0.0.0.0"
port = 8090
static_path = "music"
xiaomusic = None
config = None
log = None
@auth.verify_password
def verify_password(username, password):
if xiaomusic.config.disable_httpauth:
return True
if (
xiaomusic.config.httpauth_username == username
and xiaomusic.config.httpauth_password == password
):
return username
@asynccontextmanager
async def app_lifespan(app):
if xiaomusic is not None:
asyncio.create_task(xiaomusic.run_forever())
try:
yield
except Exception as e:
log.exception(f"Execption {e}")
@app.route("/allcmds")
@auth.login_required
def allcmds():
return xiaomusic.config.key_word_dict
security = HTTPBasic()
@app.route("/getversion", methods=["GET"])
def verification(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = config.httpauth_username.encode("utf8")
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = config.httpauth_password.encode("utf8")
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return True
def no_verification():
return True
app = FastAPI(
lifespan=app_lifespan,
version=__version__,
dependencies=[Depends(verification)],
)
def reset_http_server():
log.info(f"disable_httpauth:{config.disable_httpauth}")
if config.disable_httpauth:
app.dependency_overrides[verification] = no_verification
else:
app.dependency_overrides = {}
# 更新 music 链接
app.router.routes = [route for route in app.router.routes if route.path != "/music"]
app.mount(
"/music",
StaticFiles(directory=config.music_path, follow_symlink=True),
name="music",
)
def HttpInit(_xiaomusic):
global xiaomusic, config, log
xiaomusic = _xiaomusic
config = xiaomusic.config
log = xiaomusic.log
folder = os.path.dirname(__file__)
app.mount("/static", StaticFiles(directory=f"{folder}/static"), name="static")
reset_http_server()
@app.get("/")
async def read_index():
folder = os.path.dirname(__file__)
return FileResponse(f"{folder}/static/index.html")
@app.get("/getversion")
def getversion():
log.debug("getversion %s", __version__)
return {
"version": __version__,
}
return {"version": __version__}
@app.route("/getvolume", methods=["GET"])
@auth.login_required
def getvolume():
volume = xiaomusic.get_volume_ret()
return {
"volume": volume,
}
@app.get("/getvolume")
async def getvolume(did: str = ""):
if not xiaomusic.did_exist(did):
return {"volume": 0}
volume = await xiaomusic.get_volume(did=did)
return {"volume": volume}
@app.route("/searchmusic", methods=["GET"])
@auth.login_required
def searchmusic():
name = request.args.get("name")
class DidVolume(BaseModel):
did: str
volume: int = 0
@app.post("/setvolume")
async def setvolume(data: DidVolume):
did = data.did
volume = data.volume
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
log.info(f"set_volume {did} {volume}")
await xiaomusic.set_volume(did=did, arg1=volume)
return {"ret": "OK", "volume": volume}
@app.get("/searchmusic")
def searchmusic(name: str = ""):
return xiaomusic.searchmusic(name)
@app.route("/playingmusic", methods=["GET"])
@auth.login_required
def playingmusic():
return xiaomusic.playingmusic()
@app.get("/playingmusic")
def playingmusic(did: str = ""):
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
is_playing = xiaomusic.isplaying(did)
cur_music = xiaomusic.playingmusic(did)
return {
"ret": "OK",
"is_playing": is_playing,
"cur_music": cur_music,
}
@app.route("/isplaying", methods=["GET"])
@auth.login_required
def isplaying():
return xiaomusic.isplaying()
class DidCmd(BaseModel):
did: str
cmd: str
@app.route("/", methods=["GET"])
def index():
return send_from_directory("static", "index.html")
@app.post("/cmd")
async def do_cmd(data: DidCmd):
did = data.did
cmd = data.cmd
log.info(f"docmd. did:{did} cmd:{cmd}")
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
@app.route("/cmd", methods=["POST"])
@auth.login_required
async def do_cmd():
data = request.get_json()
cmd = data.get("cmd")
if len(cmd) > 0:
log.debug("docmd. cmd:%s", cmd)
xiaomusic.set_last_record(cmd)
await xiaomusic.cancel_all_tasks()
task = asyncio.create_task(xiaomusic.do_check_cmd(did=did, query=cmd))
xiaomusic.append_running_task(task)
return {"ret": "OK"}
return {"ret": "Unknow cmd"}
@app.route("/getsetting", methods=["GET"])
@auth.login_required
async def getsetting():
@app.get("/getsetting")
async def getsetting(need_device_list: bool = False):
config = xiaomusic.getconfig()
data = asdict(config)
alldevices = await xiaomusic.call_main_thread_function(xiaomusic.getalldevices)
log.info(f"getsetting alldevices: {alldevices}")
data["mi_did_list"] = alldevices["did_list"]
data["mi_hardware_list"] = alldevices["hardware_list"]
if need_device_list:
device_list = await xiaomusic.getalldevices()
log.info(f"getsetting device_list: {device_list}")
data["device_list"] = device_list
return data
@app.route("/savesetting", methods=["POST"])
@auth.login_required
async def savesetting():
data = request.get_json()
debug_data = deepcopy_data_no_sensitive_info(data)
log.info(f"saveconfig: {debug_data}")
await xiaomusic.saveconfig(data)
return "save success"
@app.post("/savesetting")
async def savesetting(request: Request):
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}")
await xiaomusic.saveconfig(data)
reset_http_server()
return "save success"
except json.JSONDecodeError as err:
raise HTTPException(status_code=400, detail="Invalid JSON") from err
@app.route("/musiclist", methods=["GET"])
@auth.login_required
async def musiclist():
@app.get("/musiclist")
async def musiclist(Verifcation=Depends(verification)):
return xiaomusic.get_music_list()
@app.route("/curplaylist", methods=["GET"])
@auth.login_required
async def curplaylist():
return xiaomusic.get_cur_play_list()
@app.get("/curplaylist")
async def curplaylist(did: str = ""):
if not xiaomusic.did_exist(did):
return ""
return xiaomusic.get_cur_play_list(did)
@app.route("/delmusic", methods=["POST"])
@auth.login_required
def delmusic():
data = request.get_json()
class MusicItem(BaseModel):
name: str
@app.post("/delmusic")
def delmusic(data: MusicItem):
log.info(data)
xiaomusic.del_music(data["name"])
xiaomusic.del_music(data.name)
return "success"
@app.route("/downloadjson", methods=["POST"])
@auth.login_required
def downloadjson():
data = request.get_json()
class UrlInfo(BaseModel):
url: str
@app.post("/downloadjson")
async def downloadjson(data: UrlInfo):
log.info(data)
url = data["url"]
url = data.url
content = ""
try:
ret = "OK"
content = downloadfile(url)
content = await downloadfile(url)
except Exception as e:
log.warning(f"downloadjson failed. url:{url} e:{e}")
log.exception(f"Execption {e}")
ret = "Download JSON file failed."
return {
"ret": ret,
@@ -157,54 +246,52 @@ def downloadjson():
}
@app.route("/downloadlog", methods=["GET"])
@auth.login_required
def downloadlog():
return send_file(xiaomusic.config.log_file, as_attachment=True)
@app.get("/downloadlog")
def downloadlog(Verifcation=Depends(verification)):
file_path = xiaomusic.config.log_file
if os.path.exists(file_path):
# 创建一个临时文件来保存日志的快照
temp_file = tempfile.NamedTemporaryFile(delete=False)
try:
with open(file_path, "rb") as f:
shutil.copyfileobj(f, temp_file)
temp_file.close()
# 使用BackgroundTask在响应发送完毕后删除临时文件
def cleanup_temp_file(tmp_file_path):
os.remove(tmp_file_path)
background_task = BackgroundTask(cleanup_temp_file, temp_file.name)
return FileResponse(
temp_file.name,
media_type="text/plain",
filename="xiaomusic.txt",
background=background_task,
)
except Exception as e:
os.remove(temp_file.name)
raise HTTPException(
status_code=500, detail="Error capturing log file"
) from e
else:
return {"message": "File not found."}
@app.route("/playurl", methods=["GET"])
@auth.login_required
async def playurl():
url = request.args.get("url")
log.info(f"play_url:{url}")
return await xiaomusic.call_main_thread_function(xiaomusic.play_url, arg1=url)
@app.get("/playurl")
async def playurl(did: str, url: str):
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)
@app.route("/debug_play_by_music_url", methods=["POST"])
@auth.login_required
async def debug_play_by_music_url():
data = request.get_json()
log.info(f"data:{data}")
return await xiaomusic.call_main_thread_function(
xiaomusic.debug_play_by_music_url, arg1=data
)
def static_path_handler(filename):
log.debug(filename)
log.debug(static_path)
absolute_path = os.path.abspath(static_path)
log.debug(absolute_path)
return send_from_directory(absolute_path, filename)
def run_app():
serve(app, host=host, port=port)
def StartHTTPServer(_port, _static_path, _xiaomusic):
global port, static_path, xiaomusic, log
port = _port
static_path = _static_path
xiaomusic = _xiaomusic
log = xiaomusic.log
app.add_url_rule(
f"/{static_path}/<path:filename>", "static_path_handler", static_path_handler
)
server_thread = Thread(target=run_app)
server_thread.daemon = True
server_thread.start()
xiaomusic.log.info(f"Serving on {host}:{port}")
@app.post("/debug_play_by_music_url")
async def debug_play_by_music_url(request: Request):
try:
data = await request.body()
data_dict = json.loads(data.decode("utf-8"))
log.info(f"data:{data_dict}")
return await xiaomusic.debug_play_by_music_url(arg1=data_dict)
except json.JSONDecodeError as err:
raise HTTPException(status_code=400, detail="Invalid JSON") from err

View File

@@ -1,8 +1,13 @@
$(function(){
$container=$("#cmds");
append_op_button_name("全部循环");
append_op_button_name("单曲循环");
append_op_button_name("随机播放");
const PLAY_TYPE_ONE = 0; // 单曲循环
const PLAY_TYPE_ALL = 1; // 全部循环
const PLAY_TYPE_RND = 2; // 随机播放
append_op_button("play_type_all", "全部循环", "全部循环");
append_op_button("play_type_one", "单曲循环", "单曲循环");
append_op_button("play_type_rnd", "随机播放", "随机播放");
append_op_button_name("刷新列表");
append_op_button_name("下一首");
append_op_button_name("关机");
@@ -13,11 +18,61 @@ $(function(){
append_op_button_name("30分钟后关机");
append_op_button_name("60分钟后关机");
// 拉取声音
sendcmd("get_volume#");
$.get("/getvolume", function(data, status) {
console.log(data, status, data["volume"]);
$("#volume").val(data.volume);
// 拉取现有配置
$.get("/getsetting", function(data, status) {
console.log(data, status);
localStorage.setItem('mi_did', data.mi_did);
var did = localStorage.getItem('cur_did');
var dids = [];
if (data.mi_did != null) {
dids = data.mi_did.split(',');
}
console.log('cur_did', did);
console.log('dids', dids);
if ((dids.length > 0) && (did == null || did == "" || !dids.includes(did))) {
did = dids[0];
localStorage.setItem('cur_did', did);
}
window.did = did;
$.get(`/getvolume?did=${did}`, function(data, status) {
console.log(data, status, data["volume"]);
$("#volume").val(data.volume);
});
refresh_music_list();
$("#did").empty();
var dids = data.mi_did.split(',');
$.each(dids, function(index, value) {
var cur_device = Object.values(data.devices).find(device => device.did === value);
if (cur_device) {
var option = $('<option></option>')
.val(value)
.text(cur_device.name)
.prop('selected', value === did);
$("#did").append(option);
if (cur_device.play_type == PLAY_TYPE_ALL) {
$("#play_type_all").css('background-color', '#b1a8f3');
$("#play_type_all").text('✔️ 全部循环');
} else if (cur_device.play_type == PLAY_TYPE_ONE) {
$("#play_type_one").css('background-color', '#b1a8f3');
$("#play_type_one").text('✔️ 单曲循环');
} else if (cur_device.play_type == PLAY_TYPE_RND) {
$("#play_type_rnd").css('background-color', '#b1a8f3');
$("#play_type_rnd").text('✔️ 随机播放');
}
}
});
console.log('cur_did', did);
$('#did').change(function() {
did = $(this).val();
localStorage.setItem('cur_did', did);
window.did = did;
console.log('cur_did', did);
})
});
// 拉取版本
@@ -47,13 +102,20 @@ $(function(){
$('#music_list').trigger('change');
// 获取当前播放列表
$.get("curplaylist", function(data, status) {
$('#music_list').val(data);
$('#music_list').trigger('change');
$.get(`curplaylist?did=${did}`, function(data, status) {
if (data != "") {
$('#music_list').val(data);
$('#music_list').trigger('change');
}
})
})
// 每3秒获取下正在播放的音乐
get_playing_music();
setInterval(() => {
get_playing_music();
}, 3000);
}
refresh_music_list();
$("#play_music_list").on("click", () => {
var music_list = $("#music_list").val();
@@ -84,20 +146,23 @@ $(function(){
$("#playurl").on("click", () => {
var url = $("#music-url").val();
$.get(`/playurl?url=${url}`, function(data, status) {
$.get(`/playurl?url=${url}&did=${did}`, function(data, status) {
console.log(data);
});
});
function append_op_button_name(name) {
append_op_button(name, name);
append_op_button(null, name, name);
}
function append_op_button(name, cmd) {
function append_op_button(id, name, cmd) {
// 创建按钮
const $button = $("<button>");
$button.text(name);
$button.attr("type", "button");
if (id !== null) {
$button.attr("id", id);
}
// 设置按钮点击事件
$button.on("click", () => {
@@ -115,9 +180,18 @@ $(function(){
sendcmd(cmd);
});
$("#volume").on('input', function () {
$("#volume").on('change', function () {
var value = $(this).val();
sendcmd("set_volume#"+value);
$.ajax({
type: "POST",
url: "/setvolume",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({did: did, volume: value}),
success: () => {
},
error: () => {
}
});
});
function sendcmd(cmd) {
@@ -125,11 +199,14 @@ $(function(){
type: "POST",
url: "/cmd",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({cmd: cmd}),
data: JSON.stringify({did: did, cmd: cmd}),
success: () => {
if (cmd == "刷新列表") {
setTimeout(refresh_music_list, 3000);
}
if (["全部循环", "单曲循环", "随机播放"].includes(cmd)) {
location.reload();
}
},
error: () => {
// 请求失败时执行的操作
@@ -160,18 +237,18 @@ $(function(){
});
function get_playing_music() {
$.get("/playingmusic", function(data, status) {
$.get(`/playingmusic?did=${did}`, function(data, status) {
console.log(data);
$("#playering-music").text(data);
if (data.ret == "OK") {
if (data.is_playing) {
$("#playering-music").text(`【播放中】 ${data.cur_music}`);
} else {
$("#playering-music").text(`【空闲中】 ${data.cur_music}`);
}
}
});
}
// 每3秒获取下正在播放的音乐
get_playing_music();
setInterval(() => {
get_playing_music();
}, 3000);
function custom_sort_key(a, b) {
// 使用正则表达式提取数字前缀
const numericPrefixA = a.match(/^(\d+)/) ? parseInt(a.match(/^(\d+)/)[1], 10) : null;

View File

@@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width">
<title>Debug For XiaoMusic</title>
<link rel="stylesheet" type="text/css" href="/static/style.css">
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722319766">
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="/static/jquery-3.7.1.min.js"></script>
<script src="/static/jquery-3.7.1.min.js?version=1722319766"></script>
<script>
var vConsole = new window.VConsole();
@@ -30,11 +30,12 @@ function postJSON() {
function sendDebugCmd() {
var cmd = $("#cmd").val();
var did = localStorage.getItem('cur_did');
$.ajax({
type: "POST",
url: "/cmd",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({cmd: cmd}),
data: JSON.stringify({did: did, cmd: cmd}),
success: () => {
},
error: () => {

View File

@@ -3,9 +3,17 @@
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="/static/jquery-3.7.1.min.js"></script>
<script src="/static/app.js"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css">
<script src="/static/jquery-3.7.1.min.js?version=1722319766"></script>
<script src="/static/app.js?version=1722319766"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722319766">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
-->
</head>
<body>
<h2>小爱音箱操控面板
@@ -14,6 +22,12 @@
</a>)
</h2>
<hr>
<div class="rows">
<select id="did">
</select>
</div>
<div id="cmds">
</div>
<hr>
@@ -27,14 +41,14 @@
</div>
</div>
<hr>
<div>
<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 class="container">
<div id="playering-music" class="text"></div>
<div>
<button id="play">播放</button>
<div id="playering-music" class="text"></div>
</div>
</div>
<hr>
@@ -43,15 +57,17 @@
<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>
<button id="play_music_list">播放列表歌曲</button>
<button id="del_music">删除选中歌曲</button>
<hr>
<div>
<input id="music-url" type="text" placeholder="链接(http://ngcdn001.cnr.cn/live/zgzs/index.m3u8)"></input>
<div class="rows">
<input id="music-url" type="text" value="https://lhttp.qtfm.cn/live/4915/64k.mp3"></input>
<button id="playurl">播放链接</button>
</div>
<button id="playurl">播放链接</button>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>

View File

@@ -4,7 +4,7 @@
<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">
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722319766">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>

View File

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

View File

@@ -16,23 +16,22 @@ $(function(){
});
};
function updateCheckbox(selector, mi_did_list, mi_did, mi_hardware_list) {
function updateCheckbox(selector, mi_did, device_list) {
// 清除现有的内容
$(selector).empty();
// 将 mi_did 字符串通过逗号分割转换为数组,以便于判断默认选中项
var selected_dids = mi_did.split(',');
// 遍历传入的 mi_did_list 和 mi_hardware_list
$.each(mi_did_list, function(index, did) {
// 获取硬件标识,假定列表是一一对应的
var hardware = mi_hardware_list[index];
$.each(device_list, function(index, device) {
var did = device.miotDID;
var hardware = device.hardware;
var name = device.name;
// 创建复选框元素
var checkbox = $('<input>', {
type: 'checkbox',
id: did,
value: `${did}|${hardware}`,
value: `${did}`,
class: 'custom-checkbox', // 添加样式类
// 如果mi_did中包含了该did则默认选中
checked: selected_dids.indexOf(did) !== -1
@@ -42,7 +41,7 @@ $(function(){
var label = $('<label>', {
for: did,
class: 'checkbox-label', // 添加样式类
text: `${hardware} ${did}` // 设定标签内容为did和hardware的拼接
text: `${hardware} ${did}${name}` // 设定标签内容
});
// 将复选框和标签添加到目标选择器元素中
@@ -50,35 +49,27 @@ $(function(){
});
}
function getSelectedDidsAndHardware(containerSelector) {
function getSelectedDids(containerSelector) {
var selectedDids = [];
var selectedHardware = [];
// 仅选择给定容器中选中的复选框
$(containerSelector + ' .custom-checkbox:checked').each(function() {
// 解析当前复选框的值(值中包含了 did 和 hardware使用 '|' 分割)
var parts = this.value.split('|');
selectedDids.push(parts[0]);
selectedHardware.push(parts[1]);
var did = this.value;
selectedDids.push(did);
});
// 返回包含 did_list 和 hardware_list 的对象
return {
did_list: selectedDids.join(','),
hardware_list: selectedHardware.join(',')
};
return selectedDids.join(',');
}
// 拉取现有配置
$.get("/getsetting", function(data, status) {
$.get("/getsetting?need_device_list=true", function(data, status) {
console.log(data, status);
updateCheckbox("#mi_did_hardware", data.mi_did_list, data.mi_did, data.mi_hardware_list);
updateCheckbox("#mi_did", data.mi_did, data.device_list);
// 初始化显示
for (const key in data) {
if (data.hasOwnProperty(key)) {
const $element = $("#" + key);
if ($element.length && data[key] !== '') {
if ($element.length) {
if (data[key] === true) {
$element.val('true');
} else if (data[key] === false) {
@@ -87,13 +78,12 @@ $(function(){
$element.val(data[key]);
}
}
}
}
autoSelectOne();
});
$("#save").on("click", () => {
$(".save-button").on("click", () => {
var setting = $('#setting');
var inputs = setting.find('input, select, textarea');
var data = {};
@@ -103,9 +93,8 @@ $(function(){
data[id] = $(this).val();
}
});
var selectedData = getSelectedDidsAndHardware("#mi_did_hardware");
data["mi_did"] = selectedData.did_list;
data["hardware"] = selectedData.hardware_list;
var did_list = getSelectedDids("#mi_did");
data["mi_did"] = did_list;
console.log(data)
$.ajax({
@@ -115,6 +104,7 @@ $(function(){
data: JSON.stringify(data),
success: (msg) => {
alert(msg);
location.reload();
},
error: (msg) => {
alert(msg);

View File

@@ -29,35 +29,6 @@ input,select {
height: 40px;
}
.container{
width: 280px;
overflow: hidden;
display: inline-block;
}
@keyframes text-scroll {
0% {
left: 100%;
}
25% {
left: 50%;
}
50% {
left: 0%;
}
75% {
left: -50%;
}
100% {
left: -100%;
}
}
.text {
white-space: nowrap;
font-weight: bold;
position: relative;
animation: text-scroll 10s linear infinite;
}
.rows {
display: flex;
flex-direction: column;
@@ -78,40 +49,31 @@ footer {
height: 200px;
}
/* 隐藏原生复选框 */
.custom-checkbox {
display: none;
display: inline-block;
margin: 10px;
width: 20px;
height: 20px;
vertical-align: middle; /* 确保与标签垂直居中对齐 */
}
/* 修改后的自定义复选框外观 */
.checkbox-label {
display: inline-block;
width: 200px; /* 宽度 */
height: 20px; /* 高度 */
background-color: #fff; /* 背景颜色 */
border: 0px solid #ccc; /* 边框 */
border-radius: 3px; /* 圆角边框 */
position: relative; /* 设置相对定位 */
cursor: pointer; /* 鼠标形状 */
padding-left: 40px; /* 给左边的复选框图标留下空位 */
width: 260px;
background-color: #fff;
border: 0px solid #ccc;
border-radius: 3px;
position: relative;
cursor: pointer;
vertical-align: middle; /* 确保与复选框垂直居中对齐 */
margin-left: 1px; /* 给复选框和标签之间一些距离,如果需要的话 */
}
/* 对勾的样式 */
.custom-checkbox:checked + .checkbox-label::after {
content: '✔';
position: absolute;
left: 10px; /* 对勾图标靠左侧位置 */
color: #000; /* 对勾颜色 */
font-size: 18px; /* 对勾字体大小,视清晰度需调整 */
.text {
margin: 10px;
width: 150px;
height: 50px;
text-align: center;
text-decoration: none;
display: inline-block;
}
/* 标签文本样式,使用 ::before 伪元素表示复选框未选中时的样式 */
.custom-checkbox + .checkbox-label::before {
content: '⬜'; /* 表示未选中时的复选框样式,这里用了白色方块 */
position: absolute;
left: 8px; /* 方块图标靠左侧位置 */
top: 1px; /* 方块图标顶部位置 */
color: #000; /* 方块颜色 */
font-size: 18px; /* 方块字体大小,视清晰度需调整 */
}

View File

@@ -1,11 +1,15 @@
#!/usr/bin/env python3
from __future__ import annotations
import asyncio
import copy
import difflib
import logging
import mimetypes
import os
import random
import re
import shutil
import string
import tempfile
from collections.abc import AsyncIterator
@@ -14,7 +18,8 @@ from urllib.parse import urlparse
import aiohttp
import mutagen
import requests
from mutagen.id3 import ID3
from mutagen.mp3 import MP3
from requests.utils import cookiejar_from_dict
from xiaomusic.const import SUPPORT_MUSIC_TYPE
@@ -73,12 +78,21 @@ def validate_proxy(proxy_str: str) -> bool:
# 模糊搜索
def fuzzyfinder(user_input, collection):
return difflib.get_close_matches(user_input, collection, n=10, cutoff=0.1)
lower_collection = {item.lower(): item for item in collection}
user_input = user_input.lower()
matches = difflib.get_close_matches(
user_input, lower_collection.keys(), n=10, cutoff=0.1
)
return [lower_collection[match] for match in matches]
def find_best_match(user_input, collection, cutoff=0.6):
matches = difflib.get_close_matches(user_input, collection, n=1, cutoff=cutoff)
return matches[0] if matches else None
lower_collection = {item.lower(): item for item in collection}
user_input = user_input.lower()
matches = difflib.get_close_matches(
user_input, lower_collection.keys(), n=1, cutoff=cutoff
)
return lower_collection[matches[0]] if matches else None
# 歌曲排序
@@ -101,83 +115,79 @@ def custom_sort_key(s):
return (2, s)
# fork from https://gist.github.com/dougthor42/001355248518bc64d2f8
def walk_to_depth(root, depth=None, *args, **kwargs):
"""
Wrapper around os.walk that stops after going down `depth` folders.
I had my own version, but it wasn't as efficient as
http://stackoverflow.com/a/234329/1354930, so I modified to be more
similar to nosklo's answer.
However, nosklo's answer doesn't work if topdown=False, so I kept my
version.
"""
# Let people use this as a standard `os.walk` function.
if depth is None:
return os.walk(root, *args, **kwargs)
def _get_depth_path(root, directory, depth):
# 计算当前目录的深度
relative_path = root[len(directory) :].strip(os.sep)
path_parts = relative_path.split(os.sep)
if len(path_parts) >= depth:
return os.path.join(directory, *path_parts[:depth])
else:
return root
# remove any trailing separators so that our counts are correct.
root = root.rstrip(os.path.sep)
def main_func(root, depth, *args, **kwargs):
"""Faster because it skips traversing dirs that are too deep."""
root_depth = root.count(os.path.sep)
for dirpath, dirnames, filenames in os.walk(root, *args, **kwargs):
yield (dirpath, dirnames, filenames)
def _append_files_result(result, root, joinpath, files, support_extension):
dir_name = os.path.basename(root)
if dir_name not in result:
result[dir_name] = []
for file in files:
# 过滤隐藏文件
if file.startswith("."):
continue
# 过滤文件后缀
(name, extension) = os.path.splitext(file)
if extension.lower() not in support_extension:
continue
# calculate how far down we are.
current_folder_depth = dirpath.count(os.path.sep)
if current_folder_depth >= root_depth + depth:
del dirnames[:]
result[dir_name].append(os.path.join(joinpath, file))
def fallback_func(root, depth, *args, **kwargs):
"""Slower, but works when topdown is False"""
root_depth = root.count(os.path.sep)
for dirpath, dirnames, filenames in os.walk(root, *args, **kwargs):
current_folder_depth = dirpath.count(os.path.sep)
if current_folder_depth <= root_depth + depth:
yield (dirpath, dirnames, filenames)
# there's gotta be a better way do do this...
try:
if args[0] is False:
yield from fallback_func(root, depth, *args, **kwargs)
return
def traverse_music_directory(
directory, depth=10, exclude_dirs=None, support_extension=None
):
result = {}
for root, dirs, files in os.walk(directory, followlinks=True):
# 忽略排除的目录
dirs[:] = [d for d in dirs if d not in exclude_dirs]
# 计算当前目录的深度
current_depth = root[len(directory) :].count(os.sep) + 1
if current_depth > depth:
depth_path = _get_depth_path(root, directory, depth - 1)
_append_files_result(result, depth_path, root, files, support_extension)
else:
yield from main_func(root, depth, *args, **kwargs)
return
except IndexError:
pass
try:
if kwargs["topdown"] is False:
yield from fallback_func(root, depth, *args, **kwargs)
return
else:
yield from main_func(root, depth, *args, **kwargs)
return
except KeyError:
yield from main_func(root, depth, *args, **kwargs)
return
_append_files_result(result, root, root, files, support_extension)
return result
def downloadfile(url):
async def downloadfile(url):
# 清理和验证URL
# 解析URL
parsed_url = urlparse(url)
# 基础验证仅允许HTTP和HTTPS协议
if parsed_url.scheme not in ("http", "https"):
raise Warning(
f"Invalid URL scheme: {parsed_url.scheme}. Only HTTP and HTTPS are allowed."
)
# 构建目标URL
cleaned_url = parsed_url.geturl()
# 发起请求
response = requests.get(cleaned_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
return response.text
# 使用 aiohttp 创建一个客户端会话来发起请求
async with aiohttp.ClientSession() as session:
async with session.get(
cleaned_url, timeout=5
) as response: # 增加超时以避免长时间挂起
# 如果响应不是200引发异常
response.raise_for_status()
# 读取响应文本
text = await response.text()
return text
def is_mp3(url):
mt = mimetypes.guess_type(url)
if mt and mt[0] == "audio/mpeg":
return True
return False
async def _get_web_music_duration(session, url, start=0, end=500):
@@ -185,20 +195,20 @@ async def _get_web_music_duration(session, url, start=0, end=500):
headers = {"Range": f"bytes={start}-{end}"}
async with session.get(url, headers=headers) as response:
array_buffer = await response.read()
with tempfile.NamedTemporaryFile(delete=False) as tmp:
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(array_buffer)
name = tmp.name
try:
m = mutagen.File(name)
duration = m.info.length
except Exception:
pass
os.remove(name)
try:
if is_mp3(url):
m = mutagen.mp3.MP3(tmp)
else:
m = mutagen.File(tmp)
duration = m.info.length
except Exception as e:
logging.error(f"Error _get_web_music_duration: {e}")
return duration
async def get_web_music_duration(url, start=0, end=500):
async def get_web_music_duration(url):
duration = 0
try:
parsed_url = urlparse(url)
@@ -221,21 +231,26 @@ async def get_web_music_duration(url, start=0, end=500):
duration = await _get_web_music_duration(session, url, start=0, end=500)
if duration <= 0:
duration = await _get_web_music_duration(
session, url, start=0, end=1000
session, url, start=0, end=3000
)
except Exception:
pass
except Exception as e:
logging.error(f"Error get_web_music_duration: {e}")
return duration, url
# 获取文件播放时长
def get_local_music_duration(filename):
async def get_local_music_duration(filename):
loop = asyncio.get_event_loop()
duration = 0
try:
m = mutagen.File(filename)
duration = m.info.length
except Exception:
pass
if is_mp3(filename):
m = await loop.run_in_executor(None, mutagen.mp3.MP3, filename)
else:
m = await loop.run_in_executor(None, mutagen.File, filename)
if m and m.info:
duration = m.info.length
except Exception as e:
logging.error(f"Error getting local music duration: {e}")
return duration
@@ -268,3 +283,53 @@ def deepcopy_data_no_sensitive_info(data, fields_to_anonymize=None):
setattr(copy_data, field, "******")
return copy_data
# k1:v1,k2:v2
def parse_str_to_dict(s, d1=",", d2=":"):
# 初始化一个空字典
result = {}
parts = s.split(d1)
for part in parts:
# 根据冒号切割
subparts = part.split(d2)
if len(subparts) == 2: # 防止数据不是成对出现
k, v = subparts
result[k] = v
return result
# remove mp3 file id3 tag and padding to reduce delay
def no_padding(info):
# this will remove all padding
return 0
def remove_id3_tags(file_path):
audio = MP3(file_path, ID3=ID3)
change = False
# 检查是否存在ID3 v2.3或v2.4标签
if audio.tags and (
audio.tags.version == (2, 3, 0) or audio.tags.version == (2, 4, 0)
):
# 构造新文件的路径
new_file_path = file_path + ".bak"
# 备份原始文件为新文件
shutil.copy(file_path, new_file_path)
# 删除ID3标签
audio.delete()
# 删除padding
audio.save(padding=no_padding)
# 保存修改后的文件
audio.save()
change = True
return change

File diff suppressed because it is too large Load Diff