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

Compare commits

...

63 Commits

Author SHA1 Message Date
涵曦
5c88c79ac6 new version v0.1.87 2024-07-01 14:43:59 +00:00
涵曦
5df91e7a59 fix: 修复XIAOMUSIC_USE_MUSIC_API=true时播放不了的问题 2024-07-01 14:42:20 +00:00
涵曦
71e9c15b5d new version v0.1.86 2024-07-01 13:34:38 +00:00
涵曦
c151144a5a Update README.md 2024-07-01 20:28:45 +08:00
涵曦
82a3373e72 Update README.md 2024-07-01 20:24:43 +08:00
涵曦
29ef5f238f feat: 优化 ffmpeg 安装脚本 2024-07-01 11:47:31 +00:00
涵曦
1809a2ab54 fix: 尝试修复 armv7 的 ffmpeg 问题 2024-07-01 11:14:55 +00:00
涵曦
3b1684f553 feat: 新增调试工具用来调试 player_play_music 接口 2024-07-01 11:08:54 +00:00
涵曦
d088374333 Update README.md 2024-07-01 09:15:07 +08:00
涵曦
80da6bd1e6 fix: 尝试修复关机失败的问题 2024-07-01 01:11:35 +00:00
涵曦
619bb9c853 fix: 修复口令不能播放的问题 2024-07-01 01:11:35 +00:00
涵曦
5add7b7a5c feat: 升级依赖库 MiService 2024-07-01 01:11:35 +00:00
涵曦
f61f14e16c Update README.md 2024-07-01 05:30:21 +08:00
leic4u
125421db22 为支持的设备增加产品名称和官方产品百科链接
为支持的设备增加产品名称和官方产品百科链接
2024-07-01 05:23:47 +08:00
涵曦
98b73f72df new version v0.1.85 2024-06-30 10:20:06 +00:00
涵曦
e68bc3b937 fix: 修复电台删除后没有从电台列表中删除的问题 2024-06-30 10:19:53 +00:00
涵曦
ab447a4633 feat: 版本号链接到github的release页面,方便查看版本更新日志 2024-06-30 10:16:58 +00:00
涵曦
23d321a722 new version v0.1.84 2024-06-30 09:44:25 +00:00
涵曦
20945954b1 feat: config.json 支持更多配置选项 2024-06-30 09:43:51 +00:00
涵曦
d6c2078917 docs: 文档更新 2024-06-30 09:43:28 +00:00
涵曦
a5b8dc639c feat: 新增 XIAOMUSIC_STOP_TTS_MSG 配置关机提示音 2024-06-30 07:32:33 +00:00
涵曦
84751e0d68 new version v0.1.83 2024-06-30 06:38:09 +00:00
涵曦
e759658481 bugfix: pip安装运行名字错误 2024-06-30 06:38:04 +00:00
涵曦
d83100588f new version v0.1.82 2024-06-30 06:34:03 +00:00
涵曦
83d0e02eb4 feat: 优化指令匹配规则 2024-06-30 06:33:53 +00:00
涵曦
20f1f33b6c update readme 2024-06-30 05:53:54 +00:00
涵曦
fbb5d26c28 new version v0.1.81 2024-06-30 05:42:26 +00:00
涵曦
2c21778675 update project 2024-06-30 05:40:08 +00:00
涵曦
959acd8fb7 优化关机提示 2024-06-30 05:27:44 +00:00
涵曦
148c5b7621 命令行新增LOGO 2024-06-30 05:19:38 +00:00
涵曦
d7a2afba48 Update README.md 2024-06-30 12:06:01 +08:00
涵曦
559ed23214 Update README.md 2024-06-30 12:05:10 +08:00
涵曦
1d1e63df8a Update README.md 2024-06-30 12:04:29 +08:00
涵曦
27e9d92a0a 提交config.json模板文件 2024-06-30 03:54:39 +00:00
涵曦
69573f3fa4 new version v0.1.80 2024-06-30 01:07:29 +00:00
涵曦
edafd79140 fix: #91 修复下载歌曲报错 2024-06-30 01:07:25 +00:00
涵曦
7c45d93fea new version v0.1.79 2024-06-29 15:37:38 +00:00
涵曦
f19a7e1080 优化关机的问题 2024-06-29 15:37:08 +00:00
涵曦
5e0ae07978 new version v0.1.78 2024-06-29 15:35:15 +00:00
涵曦
116a05ce4b 修复播放列表的问题 2024-06-29 15:35:15 +00:00
涵曦
d6fdee5905 Update README.md 2024-06-29 21:33:09 +08:00
涵曦
af25300917 new version v0.1.77 2024-06-29 13:24:37 +00:00
涵曦
f18b2f49bf fix: #52 支持配置模糊匹配本地歌曲 2024-06-29 13:21:37 +00:00
涵曦
db1e4e6fc4 整理代码 2024-06-29 12:42:15 +00:00
涵曦
dc49f63a37 新增直接播放链接的接口 2024-06-29 12:41:29 +00:00
涵曦
6837841872 Update README.md 2024-06-29 09:32:41 +08:00
涵曦
637347ae0c Update README.md 2024-06-29 09:18:26 +08:00
涵曦
ff968c4db4 Update README.md 2024-06-29 00:14:52 +08:00
涵曦
ca547e0d81 new version v0.1.76 2024-06-28 16:10:50 +00:00
涵曦
f0931c447b 下载日志链接改为按钮样式 2024-06-28 16:10:34 +00:00
涵曦
d098b5eb60 Update README.md 2024-06-28 23:45:18 +08:00
涵曦
09111e849d Update README.md 2024-06-28 23:43:48 +08:00
涵曦
49a76dee60 Update README.md 2024-06-28 23:20:45 +08:00
涵曦
637672473e Update README.md 2024-06-28 23:14:16 +08:00
涵曦
e4e1d13b69 Update README.md 2024-06-28 23:00:01 +08:00
涵曦
7736f8c5b4 Update README.md 2024-06-28 22:55:31 +08:00
涵曦
29db69b52f Update README.md 2024-06-28 22:52:04 +08:00
涵曦
37fe51771b Update README.md 2024-06-28 22:48:42 +08:00
涵曦
77ece713dc new version v0.1.75 2024-06-28 14:46:51 +00:00
涵曦
4436cc3a15 新增ogg格式文件 2024-06-28 14:46:46 +00:00
涵曦
c875350112 new version v0.1.74 2024-06-28 14:34:36 +00:00
涵曦
d6df2f6bfe 支持在设置页面下载日志 2024-06-28 14:34:12 +00:00
涵曦
03e3312218 新增日志文件 2024-06-28 14:03:09 +00:00
20 changed files with 469 additions and 121 deletions

View File

@@ -1,4 +1,5 @@
FROM python:3.10 AS builder FROM python:3.10 AS builder
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN python3 -m venv .venv && .venv/bin/pip install --no-cache-dir -r requirements.txt RUN python3 -m venv .venv && .venv/bin/pip install --no-cache-dir -r requirements.txt

115
README.md
View File

@@ -11,6 +11,10 @@
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。 使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
<https://github.com/hanxi/xiaomusic>
> 初次安装遇到问题请查阅 <https://github.com/hanxi/xiaomusic/issues/99> 上是否已经有解决办法。
## 最简配置运行 ## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下: 已经支持在 web 页面配置其他参数docker compose 配置如下:
@@ -28,11 +32,28 @@ services:
environment: environment:
MI_USER: '小米账号' MI_USER: '小米账号'
MI_PASS: '小米密码' MI_PASS: '小米密码'
XIAOMUSIC_VERBOSE: 'true'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip' XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
``` ```
对应的 docker 启动命令如下:
```yaml
docker run -e MI_USER='小米账号' \
-e MI_PASS='小米密码' \
-e XIAOMUSIC_VERBOSE='true' \
-e XIAOMUSIC_HOSTNAME='docker 主机 ip' \
-p 8090:8090 \
-v ./music:/app/music \
hanxi/xiaomusic
```
启动成功后,在 web 页面可以配置 MI_DID, MI_HARDWARE, XIAOMUSIC_SEARCH, XIAOMUSIC_PROXY 参数。 启动成功后,在 web 页面可以配置 MI_DID, MI_HARDWARE, XIAOMUSIC_SEARCH, XIAOMUSIC_PROXY 参数。
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 ### ✨ 修改8090端口
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
```yaml ```yaml
services: services:
xiaomusic: xiaomusic:
@@ -46,10 +67,44 @@ services:
environment: environment:
MI_USER: '小米账号' MI_USER: '小米账号'
MI_PASS: '小米密码' MI_PASS: '小米密码'
XIAOMUSIC_VERBOSE: 'true'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip' XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
XIAOMUSIC_PORT: 5678 XIAOMUSIC_PORT: 5678
``` ```
其中 XIAOMUSIC_VERBOSE 设置为 'true' 时表示开启 debug 日志,遇到问题可以去 web 设置页面底部【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
## pip 方式安装运行
```shell
> pip install xiaomusic
> xiaomusic --help
__ __ _ __ __ _
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
XiaoMusic v0.1.81 by: github.com/hanxi
usage: xiaomusic.py [-h] [--hardware HARDWARE] [--account ACCOUNT]
[--password PASSWORD] [--cookie COOKIE] [--verbose]
[--config CONFIG] [--ffmpeg_location FFMPEG_LOCATION]
options:
-h, --help show this help message and exit
--hardware HARDWARE 小爱 hardware
--account ACCOUNT xiaomi account
--password PASSWORD xiaomi password
--cookie COOKIE xiaomi cookie
--verbose show info
--config CONFIG config file path
--ffmpeg_location FFMPEG_LOCATION
ffmpeg bin path
> xiaomusic --config config.json
```
其中 `config.json` 文件可以参考 `config-example.json` 文件配置。见 <https://github.com/hanxi/xiaomusic/issues/94>
## 开发环境运行 ## 开发环境运行
- 使用 install_dependencies.sh 下载依赖 - 使用 install_dependencies.sh 下载依赖
@@ -86,29 +141,35 @@ pdm run xiaomusic.py
## 已测试支持的设备 ## 已测试支持的设备
```txt | 型号 | 名称 |
- L07A | ---- | ---------------------------------------------------------------------------------------------- |
- S12 | L06A | [小爱音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l06a) |
- LX5A | L07A | [Redmi小爱音箱 Play](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l7a) |
- LX05 | S12 | [小米AI音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.s12) |
- L16A | S12A | - |
- LX06 | LX5A | [小爱音箱 万能遥控版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx5a) |
- LX01 | LX05 | [小爱音箱Play2019款](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx05) |
- L05B | L16A | [Xiaomi Sound](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l16a) |
- L05C | L17A | [Xiaomi Sound Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l17a) |
```` | LX06 | [小爱音箱Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx06) |
| LX01 | [小爱音箱mini](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx01) |
| L05B | [小爱音箱Play](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05b) |
| L05C | [小米小爱音箱Play 增强版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05c) |
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
## 支持音乐格式 ## 支持音乐格式
- mp3 - mp3
- flac - flac
- wav - wav
- ape - ape
- ogg
> 本地音乐会搜索 mp3 和 flac 格式的文件,下载的歌曲是 mp3 格式的。 > 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
## 其他参数
- XIAOMUSIC_ACTIVE_CMD 环境变量,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。
## 在 Docker 里使用 ## 在 Docker 里使用
@@ -199,7 +260,7 @@ services:
XIAOMUSIC_HOSTNAME: '192.168.2.5' XIAOMUSIC_HOSTNAME: '192.168.2.5'
``` ```
setting.json 文件不存到 music 可以这样写,会把 setting.json 文件放到容器的 /app/conf 目录且映射到本地的 ./conf 目录: 如果想让 setting.json 文件不存到 music 目录,可以这样配,下面的示例会把 setting.json 文件放到容器的 /app/conf 目录且映射到本地的 ./conf 目录:
```yaml ```yaml
services: services:
@@ -226,11 +287,14 @@ services:
- ip 是 XIAOMUSIC_HOSTNAME 设置的 - ip 是 XIAOMUSIC_HOSTNAME 设置的
- 8090 是默认端口 - 8090 是默认端口
- 功能 - 支持功能
- 显示正在播放的歌曲 - 显示正在播放的歌曲
- 模糊搜索本地歌曲 - 模糊搜索本地歌曲
- 播放列表
- 删除歌曲
- 设置页面 - 设置页面
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,必须在启动前配置的环境变量只剩下: 采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER - MI_USER
@@ -245,23 +309,26 @@ services:
## 网络歌单功能 ## 网络歌单功能
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78> 可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
> 欢迎有想法的朋友们制作更多的歌单转换工具。
## 更多其他可选配置 ## 更多其他可选配置
- XIAOMUSIC_ACTIVE_CMD 配置唤醒令,具体见 <https://github.com/hanxi/xiaomusic/pull/43> - XIAOMUSIC_ACTIVE_CMD 环境变量,用于唤醒令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录 - XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,具体见 <https://github.com/hanxi/xiaomusic/issues/76> - XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,具体见 <https://github.com/hanxi/xiaomusic/issues/76>
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台具体见 <https://github.com/hanxi/xiaomusic/issues/47> - XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台具体见 <https://github.com/hanxi/xiaomusic/issues/47>
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户 - XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码 - XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录记得把目录映射到主机默认情况会把配置存放在music目录具体见 <https://github.com/hanxi/xiaomusic/issues/74> - XIAOMUSIC_CONF_PATH 用来存放配置文件的目录记得把目录映射到主机默认情况会把配置存放在music目录具体见 <https://github.com/hanxi/xiaomusic/issues/74>
- XIAOMUSIC_VERBOSE 设置为 true 时开启 debug 日志,用于排查问题
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82> - XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82>
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号 - XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号v0.1.86 之后的版本可以不用设置。
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个 - XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。 - XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。 - XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_ENABLE_FUZZY_MATCH 设为 true 时开启模糊匹配(默认),设为 false 时关闭模糊匹配,支持模糊匹配歌名和歌单名。 具体见 <https://github.com/hanxi/xiaomusic/issues/52>
- XIAOMUSIC_FUZZY_MATCH_CUTOFF 设置模糊搜索匹配的最低相似度阈值默认0.6可以配0到1直接的小数越小越模糊越大越精准。具体见 <https://github.com/hanxi/xiaomusic/issues/52>
## 讨论区 ## 讨论区

32
config-example.json Normal file
View File

@@ -0,0 +1,32 @@
{
"hardware": "L07A",
"account": "",
"password": "",
"mi_did": "",
"cookie": "",
"verbose": false,
"music_path": "music",
"conf_path": null,
"hostname": "192.168.2.5",
"port": 8090,
"proxy": null,
"search_prefix": "ytsearch:",
"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,
"use_music_api": false,
"log_file": "/tmp/xiaomusic.txt",
"fuzzy_match_cutoff": 0.6,
"enable_fuzzy_match": true,
"stop_tts_msg": "收到,再见",
"keywords_playlocal": "播放本地歌曲,本地播放歌曲",
"keywords_play": "播放歌曲,放歌曲",
"keywords_stop": "关机,暂停,停止,停止播放"
}

View File

@@ -4,14 +4,41 @@
# https://github.com/yt-dlp/yt-dlp#dependencies # https://github.com/yt-dlp/yt-dlp#dependencies
# 判断系统架构 # 判断系统架构
arch=$(arch) arch=$(uname -m)
pkg=ffmpeg-master-latest-linuxarm64-gpl # 输出架构信息
if [[ "${arch}" == "x86_64" ]]; then echo "当前系统架构是:$arch"
install_from_build() {
pkg=$1
wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz
tar -xvJf $pkg.tar.xz
mv $pkg ffmpeg
}
install_from_apt() {
apt-get update
apt-get install -y ffmpeg
rm -rf /var/lib/apt/lists/*
mkdir -p /app/ffmpeg/bin
ln -s /usr/bin/ffmpeg /app/ffmpeg/bin/ffmpeg
ln -s /usr/bin/ffprobe /app/ffmpeg/bin/ffprobe
}
# 基于架构执行不同的操作
case "$arch" in
x86_64)
echo "64位 x86 架构"
pkg=ffmpeg-master-latest-linux64-gpl pkg=ffmpeg-master-latest-linux64-gpl
fi install_from_build "$pkg"
;;
#export ALL_PROXY=http://192.168.2.5:8080 arm64 | aarch64)
wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz echo "64位 ARM 架构"
tar -xvJf $pkg.tar.xz pkg=ffmpeg-master-latest-linuxarm64-gpl
mv $pkg ffmpeg install_from_build "$pkg"
;;
*)
echo "未知架构 $arch"
install_from_apt
;;
esac

24
pdm.lock generated
View File

@@ -5,7 +5,7 @@
groups = ["default", "lint"] groups = ["default", "lint"]
strategy = ["cross_platform"] strategy = ["cross_platform"]
lock_version = "4.4.1" lock_version = "4.4.1"
content_hash = "sha256:813253734c7d7835a76cd87fe8fe0329e02ad067f535aee6a9e11cb106569dd2" content_hash = "sha256:e7455b13bf13306ccf5ad11781191edb62991d9fbe8f8ce1e61a2f35c713cc2a"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@@ -519,7 +519,7 @@ files = [
[[package]] [[package]]
name = "miservice-fork" name = "miservice-fork"
version = "2.6.0" version = "2.6.1"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "XiaoMi Cloud Service fork from https://github.com/Yonsm/MiService" summary = "XiaoMi Cloud Service fork from https://github.com/Yonsm/MiService"
dependencies = [ dependencies = [
@@ -528,8 +528,8 @@ dependencies = [
"rich", "rich",
] ]
files = [ files = [
{file = "miservice_fork-2.6.0-py3-none-any.whl", hash = "sha256:98169a77ea41a7b9392e1b1fab8cb80a4165fed8a9e882d9ada9a16dd1120347"}, {file = "miservice_fork-2.6.1-py3-none-any.whl", hash = "sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39"},
{file = "miservice_fork-2.6.0.tar.gz", hash = "sha256:a59d337d1f7a92566aa147e96595a8d2f5bf3f7000ae5e7dd9ed451f18d6e2fd"}, {file = "miservice_fork-2.6.1.tar.gz", hash = "sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c"},
] ]
[[package]] [[package]]
@@ -632,8 +632,8 @@ files = [
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.31.0"
requires_python = ">=3.8" requires_python = ">=3.7"
summary = "Python HTTP for Humans." summary = "Python HTTP for Humans."
dependencies = [ dependencies = [
"certifi>=2017.4.17", "certifi>=2017.4.17",
@@ -642,8 +642,8 @@ dependencies = [
"urllib3<3,>=1.21.1", "urllib3<3,>=1.21.1",
] ]
files = [ files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
] ]
[[package]] [[package]]
@@ -832,7 +832,7 @@ files = [
[[package]] [[package]]
name = "yt-dlp" name = "yt-dlp"
version = "2024.6.24.232830.dev0" version = "2024.5.30.232720.dev0"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "A feature-rich command-line audio/video downloader" summary = "A feature-rich command-line audio/video downloader"
dependencies = [ dependencies = [
@@ -841,11 +841,11 @@ dependencies = [
"certifi", "certifi",
"mutagen", "mutagen",
"pycryptodomex", "pycryptodomex",
"requests<3,>=2.32.2", "requests<3,>=2.31.0",
"urllib3<3,>=1.26.17", "urllib3<3,>=1.26.17",
"websockets>=12.0", "websockets>=12.0",
] ]
files = [ files = [
{file = "yt_dlp-2024.6.24.232830.dev0-py3-none-any.whl", hash = "sha256:efffecef44ce688e9ee3c02226eb1ba4ad64b37744726e9e4df5c2bd04ea93c5"}, {file = "yt_dlp-2024.5.30.232720.dev0-py3-none-any.whl", hash = "sha256:d6e563a2923807392325722028e7792e35affb694a505617b008195d0d212d2c"},
{file = "yt_dlp-2024.6.24.232830.dev0.tar.gz", hash = "sha256:0e89b46958984954393692a8c41e0f6d76a773be2df381c3d3a4ff24ce89aa32"}, {file = "yt_dlp-2024.5.30.232720.dev0.tar.gz", hash = "sha256:9e2b177c5b13ea6f54cee1c56a69dd7832d506fba73a2247c6470e7d1952f959"},
] ]

View File

@@ -1,12 +1,12 @@
[project] [project]
name = "xiaomusic" name = "xiaomusic"
version = "0.1.73" version = "0.1.87"
description = "Play Music with xiaomi AI speaker" description = "Play Music with xiaomi AI speaker"
authors = [ authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"}, {name = "涵曦", email = "im.hanxi@gmail.com"},
] ]
dependencies = [ dependencies = [
"requests>=2.31.0", "requests==2.31.0",
"aiohttp>=3.8.6", "aiohttp>=3.8.6",
"miservice-fork>=2.5.0", "miservice-fork>=2.5.0",
"mutagen>=1.47.0", "mutagen>=1.47.0",
@@ -19,6 +19,12 @@ requires-python = ">=3.10"
readme = "README.md" readme = "README.md"
license = {text = "MIT"} license = {text = "MIT"}
[project.urls]
Homepage = "https://github.com/hanxi/xiaomusic"
[project.scripts]
xiaomusic = "xiaomusic.cli:main"
[build-system] [build-system]
requires = ["pdm-backend"] requires = ["pdm-backend"]
build-backend = "pdm.backend" build-backend = "pdm.backend"

View File

@@ -305,9 +305,9 @@ MarkupSafe==2.1.4 \
mdurl==0.1.2 \ mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
miservice-fork==2.6.0 \ miservice-fork==2.6.1 \
--hash=sha256:98169a77ea41a7b9392e1b1fab8cb80a4165fed8a9e882d9ada9a16dd1120347 \ --hash=sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c \
--hash=sha256:a59d337d1f7a92566aa147e96595a8d2f5bf3f7000ae5e7dd9ed451f18d6e2fd --hash=sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39
multidict==6.0.4 \ multidict==6.0.4 \
--hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \ --hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \
--hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \ --hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \
@@ -371,9 +371,9 @@ pycryptodomex==3.19.0 \
pygments==2.16.1 \ pygments==2.16.1 \
--hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \ --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
--hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29 --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
requests==2.32.3 \ requests==2.31.0 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
rich==13.7.1 \ rich==13.7.1 \
--hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \ --hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \
--hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432 --hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432
@@ -472,6 +472,6 @@ yarl==1.9.2 \
--hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \ --hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \
--hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \ --hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \
--hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560 --hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560
yt-dlp==2024.6.24.232830.dev0 \ yt-dlp==2024.5.30.232720.dev0 \
--hash=sha256:0e89b46958984954393692a8c41e0f6d76a773be2df381c3d3a4ff24ce89aa32 \ --hash=sha256:9e2b177c5b13ea6f54cee1c56a69dd7832d506fba73a2247c6470e7d1952f959 \
--hash=sha256:efffecef44ce688e9ee3c02226eb1ba4ad64b37744726e9e4df5c2bd04ea93c5 --hash=sha256:d6e563a2923807392325722028e7792e35affb694a505617b008195d0d212d2c

View File

@@ -1 +1 @@
__version__ = "0.1.73" __version__ = "0.1.87"

View File

@@ -1,9 +1,21 @@
import argparse import argparse
import asyncio import asyncio
from xiaomusic import (
__version__,
)
from xiaomusic.config import Config from xiaomusic.config import Config
from xiaomusic.xiaomusic import XiaoMusic from xiaomusic.xiaomusic import XiaoMusic
LOGO = r"""
__ __ _ __ __ _
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
{}
"""
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -45,6 +57,8 @@ def main():
help="ffmpeg bin path", help="ffmpeg bin path",
) )
print(LOGO.format(f"XiaoMusic v{__version__} by: github.com/hanxi"))
options = parser.parse_args() options = parser.parse_args()
config = Config.from_options(options) config = Config.from_options(options)

View File

@@ -61,7 +61,9 @@ class Config:
"XIAOMUSIC_SEARCH", "ytsearch:" "XIAOMUSIC_SEARCH", "ytsearch:"
) # "bilisearch:" or "ytsearch:" ) # "bilisearch:" or "ytsearch:"
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin") 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") active_cmd: str = os.getenv(
"XIAOMUSIC_ACTIVE_CMD", "play,random_play,playlocal,play_music_list,stop"
)
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir") exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir")
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10")) music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
disable_httpauth: bool = ( disable_httpauth: bool = (
@@ -79,6 +81,20 @@ class Config:
use_music_api: bool = ( use_music_api: bool = (
os.getenv("XIAOMUSIC_USE_MUSIC_API", "false").lower() == "true" os.getenv("XIAOMUSIC_USE_MUSIC_API", "false").lower() == "true"
) )
log_file: str = os.getenv("XIAOMUSIC_MUSIC_LOG_FILE", "/tmp/xiaomusic.txt")
# 模糊搜索匹配的最低相似度阈值
fuzzy_match_cutoff: float = float(os.getenv("XIAOMUSIC_FUZZY_MATCH_CUTOFF", "0.6"))
# 开启模糊搜索
enable_fuzzy_match: bool = (
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
)
stop_tts_msg: str = os.getenv("XIAOMUSIC_STOP_TTS_MSG", "收到,再见")
keywords_playlocal: str = os.getenv(
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
)
keywords_play: str = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
keywords_stop: str = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止,停止播放")
def append_keyword(self, keys, action): def append_keyword(self, keys, action):
for key in keys.split(","): for key in keys.split(","):
@@ -89,14 +105,14 @@ class Config:
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.proxy: if self.proxy:
validate_proxy(self.proxy) validate_proxy(self.proxy)
keywords_playlocal = os.getenv( self.append_keyword(self.keywords_playlocal, "playlocal")
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲" self.append_keyword(self.keywords_play, "play")
) self.append_keyword(self.keywords_stop, "stop")
self.append_keyword(keywords_playlocal, "playlocal")
keywords_play = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲") # 保存配置到 config-example.json 文件
self.append_keyword(keywords_play, "play") # with open("config-example.json", "w") as f:
keywords_stop = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止") # data = asdict(self)
self.append_keyword(keywords_stop, "stop") # json.dump(data, f, ensure_ascii=False, indent=4)
@classmethod @classmethod
def from_options(cls, options: argparse.Namespace) -> Config: def from_options(cls, options: argparse.Namespace) -> Config:

View File

@@ -3,6 +3,7 @@ SUPPORT_MUSIC_TYPE = [
".flac", ".flac",
".wav", ".wav",
".ape", ".ape",
".ogg",
] ]
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}&timestamp={timestamp}&limit=2" LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}&timestamp={timestamp}&limit=2"

View File

@@ -2,7 +2,7 @@
import os import os
from threading import Thread from threading import Thread
from flask import Flask, request, send_from_directory from flask import Flask, request, send_file, send_from_directory
from flask_httpauth import HTTPBasicAuth from flask_httpauth import HTTPBasicAuth
from waitress import serve from waitress import serve
@@ -163,6 +163,30 @@ def downloadjson():
} }
@app.route("/downloadlog", methods=["GET"])
@auth.login_required
def downloadlog():
return send_file(xiaomusic.config.log_file, as_attachment=True)
@app.route("/playurl", methods=["GET"])
@auth.login_required
async def playurl():
url = request.args.get("url")
log.info(f"play_url:{url}")
return await xiaomusic.call_main_thread_function(xiaomusic.play_url, arg1=url)
@app.route("/debug_play_by_music_url", methods=["POST"])
@auth.login_required
async def debug_play_by_music_url():
data = request.get_json()
log.info(f"data:{data}")
return await xiaomusic.call_main_thread_function(
xiaomusic.debug_play_by_music_url, arg1=data
)
def static_path_handler(filename): def static_path_handler(filename):
log.debug(filename) log.debug(filename)
log.debug(static_path) log.debug(static_path)

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,11 @@
<link rel="stylesheet" type="text/css" href="/static/style.css"> <link rel="stylesheet" type="text/css" href="/static/style.css">
</head> </head>
<body> <body>
<h2>小爱音箱设置面板<span id="version">(版本未知)</span></h2> <h2>小爱音箱设置面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/releases">
版本未知
</a>)
</h2>
<hr> <hr>
<div class="rows"> <div class="rows">
<label for="mi_did">MI_DID:</label> <label for="mi_did">MI_DID:</label>
@@ -31,9 +35,12 @@
<button onclick="location.href='/';">返回首页</button> <button onclick="location.href='/';">返回首页</button>
<button id="get_music_list">获取歌单</button> <button id="get_music_list">获取歌单</button>
<button id="save">保存</button> <button id="save">保存</button>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<hr> <hr>
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a> <a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>
<hr>
<a href="/static/debug.html" target="_blank">调试工具</a>
<footer> <footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p> <p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>

View File

@@ -2,7 +2,7 @@ $(function(){
// 拉取版本 // 拉取版本
$.get("/getversion", function(data, status) { $.get("/getversion", function(data, status) {
console.log(data, status, data["version"]); console.log(data, status, data["version"]);
$("#version").text(`(${data.version})`); $("#version").text(`${data.version}`);
}); });
const updateSelectOptions = (selectId, optionsList, selectedOption) => { const updateSelectOptions = (selectId, optionsList, selectedOption) => {

View File

@@ -1,4 +1,8 @@
button { .button {
line-height: 50px;
font-size: 14px;
}
button, .button {
margin: 10px; margin: 10px;
width: 100px; width: 100px;
height: 50px; height: 50px;
@@ -10,7 +14,7 @@ button {
border-radius: 10px; border-radius: 10px;
background-color: #008CBA; background-color: #008CBA;
} }
button:active { button:active, .button:active {
font-weight:bold; font-weight:bold;
background-color: #007CBA; background-color: #007CBA;
transform: translateY(2px); transform: translateY(2px);

View File

@@ -72,7 +72,12 @@ def validate_proxy(proxy_str: str) -> bool:
# 模糊搜索 # 模糊搜索
def fuzzyfinder(user_input, collection): def fuzzyfinder(user_input, collection):
return difflib.get_close_matches(user_input, collection, 10, cutoff=0.1) return difflib.get_close_matches(user_input, collection, n=10, cutoff=0.1)
def find_best_match(user_input, collection, cutoff=0.6):
matches = difflib.get_close_matches(user_input, collection, n=1, cutoff=cutoff)
return matches[0] if matches else None
# 歌曲排序 # 歌曲排序

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import asyncio import asyncio
import copy
import json import json
import logging import logging
import os import os
@@ -9,6 +10,7 @@ import re
import time import time
import traceback import traceback
import urllib.parse import urllib.parse
from logging.handlers import RotatingFileHandler
from pathlib import Path from pathlib import Path
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
@@ -29,9 +31,9 @@ from xiaomusic.const import (
from xiaomusic.httpserver import StartHTTPServer from xiaomusic.httpserver import StartHTTPServer
from xiaomusic.utils import ( from xiaomusic.utils import (
custom_sort_key, custom_sort_key,
find_best_match,
fuzzyfinder, fuzzyfinder,
get_local_music_duration, get_local_music_duration,
get_random,
get_web_music_duration, get_web_music_duration,
parse_cookie_string, parse_cookie_string,
walk_to_depth, walk_to_depth,
@@ -91,14 +93,8 @@ class XiaoMusic:
# 关机定时器 # 关机定时器
self._stop_timer = None self._stop_timer = None
# setup logger # 初始化日志
logging.basicConfig( self.setup_logger()
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
datefmt="[%X]",
)
self.log = logging.getLogger("xiaomusic")
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
self.log.debug(config)
# 尝试从设置里加载配置 # 尝试从设置里加载配置
self.try_init_setting() self.try_init_setting()
@@ -109,7 +105,30 @@ class XiaoMusic:
# 启动时初始化获取声音 # 启动时初始化获取声音
self.set_last_record("get_volume#") self.set_last_record("get_volume#")
self.log.info("ffmpeg_location: %s", self.ffmpeg_location) def setup_logger(self):
logging.basicConfig(
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
datefmt="[%X]",
)
log_file = self.config.log_file
log_path = os.path.dirname(log_file)
if not os.path.exists(log_path):
os.makedirs(log_path)
if os.path.exists(log_file):
os.remove(log_file)
handler = RotatingFileHandler(
self.config.log_file, maxBytes=10 * 1024 * 1024, backupCount=1
)
self.log = logging.getLogger("xiaomusic")
self.log.addHandler(handler)
self.log.setLevel(logging.DEBUG if self.config.verbose else logging.INFO)
debug_config = copy.deepcopy(self.config)
debug_config.account = "******"
debug_config.password = "******"
debug_config.httpauth_username = "******"
debug_config.httpauth_password = "******"
self.log.info(debug_config)
async def poll_latest_ask(self): async def poll_latest_ask(self):
async with ClientSession() as session: async with ClientSession() as session:
@@ -270,10 +289,13 @@ class XiaoMusic:
await self.mina_service.text_to_speech(self.device_id, value) await self.mina_service.text_to_speech(self.device_id, value)
except Exception as e: except Exception as e:
self.log.error(f"Execption {e}") self.log.error(f"Execption {e}")
# 最大等8秒
sec = min(8, int(len(value) / 3))
await asyncio.sleep(sec)
self.log.debug(f"do_tts. cur_music:{self.cur_music}") self.log.debug(f"do_tts. cur_music:{self.cur_music}")
if self._playing and not self.is_downloading(): if self._playing and not self.is_downloading():
# 继续播放歌曲 # 继续播放歌曲
self.log.info("继续播放歌曲")
await self.play() await self.play()
async def do_set_volume(self, value): async def do_set_volume(self, value):
@@ -285,8 +307,25 @@ class XiaoMusic:
except Exception as e: except Exception as e:
self.log.error(f"Execption {e}") self.log.error(f"Execption {e}")
async def get_if_xiaoai_is_playing(self):
playing_info = await self.mina_service.player_get_status(self.device_id)
# WTF xiaomi api
is_playing = (
json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1)
== 1
)
return is_playing
async def stop_if_xiaoai_is_playing(self):
is_playing = await self.get_if_xiaoai_is_playing()
if is_playing:
# stop it
ret = await self.mina_service.player_stop(self.device_id)
self.log.debug(f"force_stop_xiaoai player_stop ret:{ret}")
async def force_stop_xiaoai(self): async def force_stop_xiaoai(self):
await self.mina_service.player_stop(self.device_id) ret = await self.mina_service.player_pause(self.device_id)
self.log.debug(f"force_stop_xiaoai player_pause ret:{ret}")
# 是否在下载中 # 是否在下载中
def is_downloading(self): def is_downloading(self):
@@ -450,6 +489,7 @@ class XiaoMusic:
if not self.config.music_list_json: if not self.config.music_list_json:
return return
self._all_radio = {}
music_list = json.loads(self.config.music_list_json) music_list = json.loads(self.config.music_list_json)
try: try:
for item in music_list: for item in music_list:
@@ -570,7 +610,7 @@ class XiaoMusic:
self.polling_event.clear() # stop polling when processing the question self.polling_event.clear() # stop polling when processing the question
query = new_record.get("query", "").strip() query = new_record.get("query", "").strip()
ctrl_panel = new_record.get("ctrl_panel", False) ctrl_panel = new_record.get("ctrl_panel", False)
self.log.debug("收到消息:%s 控制面板:%s", query, ctrl_panel) self.log.info("收到消息:%s 控制面板:%s", query, ctrl_panel)
# 匹配命令 # 匹配命令
opvalue, oparg = self.match_cmd(query, ctrl_panel) opvalue, oparg = self.match_cmd(query, ctrl_panel)
@@ -584,8 +624,26 @@ class XiaoMusic:
except Exception as e: except Exception as e:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}") self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
# 检查是否匹配到完全一样的指令
def check_full_match_cmd(self, query, ctrl_panel):
if query in self.config.key_match_order:
opkey = query
opvalue = self.config.key_word_dict.get(opkey)
if ctrl_panel or self._playing:
return opvalue
else:
if not self.active_cmd or opvalue in self.active_cmd:
return opvalue
return None
# 匹配命令 # 匹配命令
def match_cmd(self, query, ctrl_panel): def match_cmd(self, query, ctrl_panel):
# 优先处理完全匹配
opvalue = self.check_full_match_cmd(query, ctrl_panel)
if opvalue:
self.log.info(f"完全匹配指令. query:{query} opvalue:{opvalue}")
return (opvalue, "")
for opkey in self.config.key_match_order: for opkey in self.config.key_match_order:
patternarg = rf"(.*){opkey}(.*)" patternarg = rf"(.*){opkey}(.*)"
# 匹配参数 # 匹配参数
@@ -603,20 +661,18 @@ class XiaoMusic:
argafter, argafter,
) )
oparg = argafter oparg = argafter
if opkey in KEY_WORD_ARG_BEFORE_DICT:
oparg = argpre
opvalue = self.config.key_word_dict.get(opkey) opvalue = self.config.key_word_dict.get(opkey)
if not ctrl_panel and not self._playing: if not ctrl_panel and not self._playing:
if self.active_cmd and opvalue not in self.active_cmd: if self.active_cmd and opvalue not in self.active_cmd:
self.log.debug(f"不在激活命令中 {opvalue}") self.log.ifno(f"不在激活命令中 {opvalue}")
continue continue
if opkey in KEY_WORD_ARG_BEFORE_DICT: self.log.info(f"匹配到指令. opkey:{opkey} opvalue:{opvalue} oparg:{oparg}")
oparg = argpre
self.log.info(
"匹配到指令. opkey:%s opvalue:%s oparg:%s", opkey, opvalue, oparg
)
return (opvalue, oparg) return (opvalue, oparg)
if self._playing: if self._playing:
self.log.info("未匹配到指令,自动停止") self.log.info("未匹配到指令,自动停止")
return ("stop", {}) return ("stop", "notts")
return (None, None) return (None, None)
# 判断是否播放下一首歌曲 # 判断是否播放下一首歌曲
@@ -630,29 +686,10 @@ class XiaoMusic:
return True return True
return False return False
async def _play_by_music_url(self, device_id, url): async def play_url(self, **kwargs):
audio_id = get_random(30) url = kwargs.get("arg1", "")
audio_type = ""
if self.config.hardware in ["LX04", "X10A", "X08A"]:
audio_type = "MUSIC"
music = {
"payload": {
"audio_items": [
{"item_id": {"audio_id": audio_id}, "stream": {"url": url}}
],
"audio_type": audio_type,
}
}
return await self.mina_service.ubus_request(
device_id,
"player_play_music",
"mediaplayer",
{"startaudioid": audio_id, "music": json.dumps(music)},
)
async def play_url(self, url):
if self.config.use_music_api: if self.config.use_music_api:
ret = await self._play_by_music_url(self.device_id, url) ret = await self.mina_service.play_by_music_url(self.device_id, url, _type=2)
self.log.debug( self.log.debug(
f"play_url play_by_music_url {self.config.hardware}. ret:{ret} url:{url}" f"play_url play_by_music_url {self.config.hardware}. ret:{ret} url:{url}"
) )
@@ -661,6 +698,22 @@ class XiaoMusic:
self.log.debug( self.log.debug(
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}" f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
) )
return ret
def find_real_music_name(self, name):
if not self.config.enable_fuzzy_match:
self.log.debug("没开启模糊匹配")
return name
all_music_list = list(self._all_music.keys())
real_name = find_best_match(
name, all_music_list, cutoff=self.config.fuzzy_match_cutoff
)
if real_name:
self.log.info(f"根据【{name}】找到歌曲【{real_name}")
return real_name
self.log.info(f"没找到歌曲【{name}")
return name
# 播放本地歌曲 # 播放本地歌曲
async def playlocal(self, **kwargs): async def playlocal(self, **kwargs):
@@ -675,6 +728,7 @@ class XiaoMusic:
self.log.info(f"playlocal. name:{name}") self.log.info(f"playlocal. name:{name}")
# 本地歌曲不存在时下载 # 本地歌曲不存在时下载
name = self.find_real_music_name(name)
if not self.is_music_exist(name): if not self.is_music_exist(name):
await self.do_tts(f"本地不存在歌曲{name}") await self.do_tts(f"本地不存在歌曲{name}")
return return
@@ -687,7 +741,7 @@ class XiaoMusic:
sec, url = await self.get_music_sec_url(name) sec, url = await self.get_music_sec_url(name)
self.log.info(f"播放 {url}") self.log.info(f"播放 {url}")
await self.force_stop_xiaoai() await self.force_stop_xiaoai()
await self.play_url(url) await self.play_url(arg1=url)
self.log.info("已经开始播放了") self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器 # 设置下一首歌曲的播放定时器
await self.set_next_music_timeout(sec) await self.set_next_music_timeout(sec)
@@ -710,12 +764,13 @@ class XiaoMusic:
self.log.info("play. search_key:%s name:%s", search_key, name) self.log.info("play. search_key:%s name:%s", search_key, name)
# 本地歌曲不存在时下载 # 本地歌曲不存在时下载
name = self.find_real_music_name(name)
if not self.is_music_exist(name): if not self.is_music_exist(name):
if self.config.disable_download: if self.config.disable_download:
await self.do_tts(f"本地不存在歌曲{name}") await self.do_tts(f"本地不存在歌曲{name}")
return return
await self.download(search_key, name) await self.download(search_key, name)
self.log.info("正在下载中 %s", search_key + ":" + name) self.log.info(f"正在下载中 {search_key} {name}")
await self.download_proc.wait() await self.download_proc.wait()
# 把文件插入到播放列表里 # 把文件插入到播放列表里
self.add_download_music(name) self.add_download_music(name)
@@ -771,10 +826,27 @@ class XiaoMusic:
self.log.error(f"del ${filename} failed") self.log.error(f"del ${filename} failed")
self._gen_all_music_list() self._gen_all_music_list()
def find_real_music_list_name(self, list_name):
if not self.config.enable_fuzzy_match:
self.log.debug("没开启模糊匹配")
return list_name
# 模糊搜一个播放列表
real_name = find_best_match(
list_name, self._music_list, cutoff=self.config.fuzzy_match_cutoff
)
if real_name:
self.log.info(f"根据【{list_name}】找到播放列表【{real_name}")
list_name = real_name
self.log.info(f"没找到播放列表【{list_name}")
return list_name
# 播放一个播放列表 # 播放一个播放列表
async def play_music_list(self, **kwargs): async def play_music_list(self, **kwargs):
parts = kwargs.get("arg1").split("|") parts = kwargs.get("arg1").split("|")
list_name = parts[0] list_name = parts[0]
list_name = self.find_real_music_list_name(list_name)
if list_name not in self._music_list: if list_name not in self._music_list:
await self.do_tts(f"播放列表{list_name}不存在") await self.do_tts(f"播放列表{list_name}不存在")
return return
@@ -792,6 +864,9 @@ class XiaoMusic:
async def stop(self, **kwargs): async def stop(self, **kwargs):
self._playing = False self._playing = False
if kwargs.get("arg1", "") != "notts":
if self.config.stop_tts_msg:
await self.do_tts(self.config.stop_tts_msg)
if self._next_timer: if self._next_timer:
self._next_timer.cancel() self._next_timer.cancel()
self.log.info("定时器已取消") self.log.info("定时器已取消")
@@ -806,12 +881,12 @@ class XiaoMusic:
async def _do_stop(): async def _do_stop():
await asyncio.sleep(minute * 60) await asyncio.sleep(minute * 60)
try: try:
await self.stop() await self.stop(arg1="notts")
except Exception as e: except Exception as e:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}") self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
self._stop_timer = asyncio.ensure_future(_do_stop()) self._stop_timer = asyncio.ensure_future(_do_stop())
self.log.info(f"{minute}分钟后将关机") await self.do_tts(f"收到,{minute}分钟后将关机")
async def set_volume(self, **kwargs): async def set_volume(self, **kwargs):
value = kwargs.get("arg1", 0) value = kwargs.get("arg1", 0)
@@ -821,7 +896,7 @@ class XiaoMusic:
playing_info = await self.mina_service.player_get_status(self.device_id) playing_info = await self.mina_service.player_get_status(self.device_id)
self.log.debug("get_volume. playing_info:%s", playing_info) self.log.debug("get_volume. playing_info:%s", playing_info)
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get( self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
"volume", 5 "volume", 0
) )
self.log.info("get_volume. volume:%s", self._volume) self.log.info("get_volume. volume:%s", self._volume)
@@ -936,3 +1011,15 @@ class XiaoMusic:
self.new_record_event.set() self.new_record_event.set()
result = await future result = await future
return result return result
async def debug_play_by_music_url(self, arg1=None):
if arg1 is None:
arg1 = {}
data = arg1
self.log.info(f"debug_play_by_music_url: {data}")
return await self.mina_service.ubus_request(
self.device_id,
"player_play_music",
"mediaplayer",
data,
)