mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-08 15:08:12 +08:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bd399b654 | ||
|
|
228d89f1f8 | ||
|
|
e97639302f | ||
|
|
7f4e51be08 | ||
|
|
cdab5fc92d | ||
|
|
6efe498f2a | ||
|
|
0f3f2e47f5 | ||
|
|
3b720b7367 | ||
|
|
9a3e513b6c | ||
|
|
5a8e5dfa82 | ||
|
|
70d9ad93cb | ||
|
|
87b3411f5e | ||
|
|
5c88c79ac6 | ||
|
|
5df91e7a59 | ||
|
|
71e9c15b5d | ||
|
|
c151144a5a | ||
|
|
82a3373e72 | ||
|
|
29ef5f238f | ||
|
|
1809a2ab54 | ||
|
|
3b1684f553 | ||
|
|
d088374333 | ||
|
|
80da6bd1e6 | ||
|
|
619bb9c853 | ||
|
|
5add7b7a5c | ||
|
|
f61f14e16c | ||
|
|
125421db22 | ||
|
|
98b73f72df | ||
|
|
e68bc3b937 | ||
|
|
ab447a4633 | ||
|
|
23d321a722 | ||
|
|
20945954b1 | ||
|
|
d6c2078917 | ||
|
|
a5b8dc639c | ||
|
|
84751e0d68 | ||
|
|
e759658481 | ||
|
|
d83100588f | ||
|
|
83d0e02eb4 | ||
|
|
20f1f33b6c | ||
|
|
fbb5d26c28 | ||
|
|
2c21778675 | ||
|
|
959acd8fb7 | ||
|
|
148c5b7621 | ||
|
|
d7a2afba48 | ||
|
|
559ed23214 | ||
|
|
1d1e63df8a | ||
|
|
27e9d92a0a | ||
|
|
69573f3fa4 | ||
|
|
edafd79140 | ||
|
|
7c45d93fea | ||
|
|
f19a7e1080 | ||
|
|
5e0ae07978 | ||
|
|
116a05ce4b | ||
|
|
d6fdee5905 | ||
|
|
af25300917 | ||
|
|
f18b2f49bf | ||
|
|
db1e4e6fc4 | ||
|
|
dc49f63a37 | ||
|
|
6837841872 | ||
|
|
637347ae0c | ||
|
|
ff968c4db4 |
@@ -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
|
||||||
|
|||||||
75
README.md
75
README.md
@@ -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 配置如下:
|
||||||
@@ -46,7 +50,10 @@ docker run -e MI_USER='小米账号' \
|
|||||||
|
|
||||||
启动成功后,在 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:
|
||||||
@@ -67,6 +74,37 @@ services:
|
|||||||
|
|
||||||
其中 XIAOMUSIC_VERBOSE 设置为 'true' 时表示开启 debug 日志,遇到问题可以去 web 设置页面底部【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
|
其中 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 下载依赖
|
||||||
@@ -103,18 +141,25 @@ 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 | - |
|
||||||
- L17A
|
| LX5A | [小爱音箱 万能遥控版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx5a) |
|
||||||
- LX06
|
| LX05 | [小爱音箱Play(2019款)](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx05) |
|
||||||
- LX01
|
| L16A | [Xiaomi Sound](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l16a) |
|
||||||
- L05B
|
| L17A | [Xiaomi Sound Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l17a) |
|
||||||
- L05C
|
| 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
|
||||||
@@ -278,10 +323,12 @@ services:
|
|||||||
- 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_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
32
config-example.json
Normal 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": "关机,暂停,停止,停止播放"
|
||||||
|
}
|
||||||
@@ -4,14 +4,47 @@
|
|||||||
# 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"
|
||||||
pkg=ffmpeg-master-latest-linux64-gpl
|
|
||||||
fi
|
|
||||||
|
|
||||||
#export ALL_PROXY=http://192.168.2.5:8080
|
install_from_github() {
|
||||||
wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz
|
pkg=$1
|
||||||
tar -xvJf $pkg.tar.xz
|
wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz
|
||||||
mv $pkg ffmpeg
|
tar -xvJf $pkg.tar.xz
|
||||||
|
mkdir -p ffmpeg/bin
|
||||||
|
mv $pkg/bin/ffmpeg ffmpeg/bin/
|
||||||
|
mv $pkg/bin/ffprobe ffmpeg/bin/
|
||||||
|
}
|
||||||
|
|
||||||
|
install_from_ffmpeg() {
|
||||||
|
pkg=$1
|
||||||
|
wget https://johnvansickle.com/ffmpeg/builds/$pkg.tar.xz
|
||||||
|
mkdir -p $pkg
|
||||||
|
tar -xvJf $pkg.tar.xz -C $pkg
|
||||||
|
mkdir -p ffmpeg/bin
|
||||||
|
mv $pkg/*/ffmpeg ffmpeg/bin/
|
||||||
|
mv $pkg/*/ffprobe ffmpeg/bin/
|
||||||
|
}
|
||||||
|
|
||||||
|
# 基于架构执行不同的操作
|
||||||
|
case "$arch" in
|
||||||
|
x86_64)
|
||||||
|
echo "64位 x86 架构"
|
||||||
|
install_from_github ffmpeg-master-latest-linux64-gpl
|
||||||
|
#install_from_ffmpeg ffmpeg-git-amd64-static
|
||||||
|
;;
|
||||||
|
arm64 | aarch64)
|
||||||
|
echo "64位 ARM 架构"
|
||||||
|
install_from_github ffmpeg-master-latest-linuxarm64-gpl
|
||||||
|
#install_from_ffmpeg ffmpeg-git-arm64-static
|
||||||
|
;;
|
||||||
|
armv7l)
|
||||||
|
echo "armv7l 架构"
|
||||||
|
install_from_ffmpeg ffmpeg-git-armhf-static
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "未知架构 $arch"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
14
pdm.lock
generated
14
pdm.lock
generated
@@ -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:ac53cf6421de7aded8475907adc40a716a3e5c6429c614b93e5cfbddea36d048"
|
||||||
|
|
||||||
[[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]]
|
||||||
@@ -832,7 +832,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yt-dlp"
|
name = "yt-dlp"
|
||||||
version = "2024.6.24.232830.dev0"
|
version = "2024.7.1.232715.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 = [
|
||||||
@@ -846,6 +846,6 @@ dependencies = [
|
|||||||
"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.7.1.232715.dev0-py3-none-any.whl", hash = "sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503"},
|
||||||
{file = "yt_dlp-2024.6.24.232830.dev0.tar.gz", hash = "sha256:0e89b46958984954393692a8c41e0f6d76a773be2df381c3d3a4ff24ce89aa32"},
|
{file = "yt_dlp-2024.7.1.232715.dev0.tar.gz", hash = "sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8"},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "xiaomusic"
|
name = "xiaomusic"
|
||||||
version = "0.1.76"
|
version = "0.1.89"
|
||||||
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",
|
|
||||||
"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",
|
||||||
"yt-dlp>=2024.04.09",
|
"yt-dlp>=2024.07.01",
|
||||||
"flask[async]>=3.0.1",
|
"flask[async]>=3.0.1",
|
||||||
"waitress>=3.0.0",
|
"waitress>=3.0.0",
|
||||||
"flask-HTTPAuth>=4.8.0",
|
"flask-HTTPAuth>=4.8.0",
|
||||||
@@ -19,6 +18,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"
|
||||||
|
|||||||
@@ -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 \
|
||||||
@@ -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.7.1.232715.dev0 \
|
||||||
--hash=sha256:0e89b46958984954393692a8c41e0f6d76a773be2df381c3d3a4ff24ce89aa32 \
|
--hash=sha256:4f1ab25318c9156cca0b7308bdd2aeb3e7f01e8d9fb83916b4719010038170c8 \
|
||||||
--hash=sha256:efffecef44ce688e9ee3c02226eb1ba4ad64b37744726e9e4df5c2bd04ea93c5
|
--hash=sha256:e9ab443353da0c8f01587b031fb84b2cc42eae82aeaa03a9ce5ed6edc301b503
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.1.76"
|
__version__ = "0.1.89"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 = (
|
||||||
@@ -80,6 +82,19 @@ class Config:
|
|||||||
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")
|
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(","):
|
||||||
@@ -90,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:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from flask import Flask, request, send_from_directory, send_file
|
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
|
||||||
|
|
||||||
@@ -98,7 +98,6 @@ async def do_cmd():
|
|||||||
@auth.login_required
|
@auth.login_required
|
||||||
async def getsetting():
|
async def getsetting():
|
||||||
config = xiaomusic.getconfig()
|
config = xiaomusic.getconfig()
|
||||||
log.debug(config)
|
|
||||||
|
|
||||||
alldevices = await xiaomusic.call_main_thread_function(xiaomusic.getalldevices)
|
alldevices = await xiaomusic.call_main_thread_function(xiaomusic.getalldevices)
|
||||||
log.info(alldevices)
|
log.info(alldevices)
|
||||||
@@ -162,11 +161,31 @@ def downloadjson():
|
|||||||
"content": content,
|
"content": content,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.route("/downloadlog", methods=["GET"])
|
@app.route("/downloadlog", methods=["GET"])
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
def downloadlog():
|
def downloadlog():
|
||||||
return send_file(xiaomusic.config.log_file, as_attachment=True)
|
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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
40
xiaomusic/static/debug.html
Normal file
40
xiaomusic/static/debug.html
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -35,6 +39,8 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
# 歌曲排序
|
# 歌曲排序
|
||||||
|
|||||||
@@ -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,10 +10,9 @@ import re
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from pathlib import Path
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from aiohttp import ClientSession, ClientTimeout
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
from miservice import MiAccount, MiIOService, MiNAService
|
from miservice import MiAccount, MiIOService, MiNAService
|
||||||
|
|
||||||
@@ -31,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,
|
||||||
@@ -106,9 +106,12 @@ class XiaoMusic:
|
|||||||
self.set_last_record("get_volume#")
|
self.set_last_record("get_volume#")
|
||||||
|
|
||||||
def setup_logger(self):
|
def setup_logger(self):
|
||||||
|
log_format = f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s"
|
||||||
|
date_format = "[%X]"
|
||||||
|
formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
|
format=log_format,
|
||||||
datefmt="[%X]",
|
datefmt=date_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
log_file = self.config.log_file
|
log_file = self.config.log_file
|
||||||
@@ -117,15 +120,18 @@ class XiaoMusic:
|
|||||||
os.makedirs(log_path)
|
os.makedirs(log_path)
|
||||||
if os.path.exists(log_file):
|
if os.path.exists(log_file):
|
||||||
os.remove(log_file)
|
os.remove(log_file)
|
||||||
handler = RotatingFileHandler(self.config.log_file, maxBytes=10*1024*1024, backupCount=1)
|
handler = RotatingFileHandler(
|
||||||
|
self.config.log_file, maxBytes=10 * 1024 * 1024, backupCount=1
|
||||||
|
)
|
||||||
|
handler.setFormatter(formatter)
|
||||||
self.log = logging.getLogger("xiaomusic")
|
self.log = logging.getLogger("xiaomusic")
|
||||||
self.log.addHandler(handler)
|
self.log.addHandler(handler)
|
||||||
self.log.setLevel(logging.DEBUG if self.config.verbose else logging.INFO)
|
self.log.setLevel(logging.DEBUG if self.config.verbose else logging.INFO)
|
||||||
debug_config = copy.deepcopy(self.config)
|
debug_config = copy.deepcopy(self.config)
|
||||||
debug_config.account = '******'
|
debug_config.account = "******"
|
||||||
debug_config.password = '******'
|
debug_config.password = "******"
|
||||||
debug_config.httpauth_username = '******'
|
debug_config.httpauth_username = "******"
|
||||||
debug_config.httpauth_password = '******'
|
debug_config.httpauth_password = "******"
|
||||||
self.log.info(debug_config)
|
self.log.info(debug_config)
|
||||||
|
|
||||||
async def poll_latest_ask(self):
|
async def poll_latest_ask(self):
|
||||||
@@ -281,17 +287,32 @@ class XiaoMusic:
|
|||||||
self.new_record_event.set()
|
self.new_record_event.set()
|
||||||
|
|
||||||
async def do_tts(self, value):
|
async def do_tts(self, value):
|
||||||
self.log.info("do_tts: %s", value)
|
self.log.info(f"try do_tts value:{value}")
|
||||||
|
if not value:
|
||||||
|
self.log.info("do_tts no value")
|
||||||
|
return
|
||||||
|
|
||||||
await self.force_stop_xiaoai()
|
await self.force_stop_xiaoai()
|
||||||
try:
|
try:
|
||||||
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.info(f"do_tts ok. cur_music:{self.cur_music}")
|
||||||
|
await self.check_replay()
|
||||||
|
|
||||||
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
|
# 继续播放被打断的歌曲
|
||||||
if self._playing and not self.is_downloading():
|
async def check_replay(self):
|
||||||
|
if self.isplaying() and not self.isdownloading():
|
||||||
# 继续播放歌曲
|
# 继续播放歌曲
|
||||||
|
self.log.info("现在继续播放歌曲")
|
||||||
await self.play()
|
await self.play()
|
||||||
|
else:
|
||||||
|
self.log.info(
|
||||||
|
f"不会继续播放歌曲. isplaying:{self.isplaying()} isdownloading:{self.isdownloading()}"
|
||||||
|
)
|
||||||
|
|
||||||
async def do_set_volume(self, value):
|
async def do_set_volume(self, value):
|
||||||
value = int(value)
|
value = int(value)
|
||||||
@@ -302,18 +323,40 @@ 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)
|
||||||
|
self.log.info(playing_info)
|
||||||
|
# WTF xiaomi api
|
||||||
|
is_playing = (
|
||||||
|
json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
return is_playing
|
||||||
|
|
||||||
|
async def stop_if_xiaoai_is_playing(self):
|
||||||
|
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.info(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.info(f"force_stop_xiaoai player_pause ret:{ret}")
|
||||||
|
await self.stop_if_xiaoai_is_playing()
|
||||||
|
|
||||||
# 是否在下载中
|
# 是否在下载中
|
||||||
def is_downloading(self):
|
def isdownloading(self):
|
||||||
if not self.download_proc:
|
if not self.download_proc:
|
||||||
return False
|
return False
|
||||||
if (
|
|
||||||
self.download_proc.returncode is not None
|
if self.download_proc.returncode is not None:
|
||||||
and self.download_proc.returncode < 0
|
self.log.info(
|
||||||
):
|
f"Process exited with returncode:{self.download_proc.returncode}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
self.log.info("Download Process is still running.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 下载歌曲
|
# 下载歌曲
|
||||||
@@ -342,7 +385,8 @@ class XiaoMusic:
|
|||||||
if self.proxy:
|
if self.proxy:
|
||||||
sbp_args += ("--proxy", f"{self.proxy}")
|
sbp_args += ("--proxy", f"{self.proxy}")
|
||||||
|
|
||||||
self.log.info(f"download: {sbp_args}")
|
cmd = " ".join(sbp_args)
|
||||||
|
self.log.info(f"download cmd: {cmd}")
|
||||||
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
|
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
|
||||||
await self.do_tts(f"正在下载歌曲{search_key}")
|
await self.do_tts(f"正在下载歌曲{search_key}")
|
||||||
|
|
||||||
@@ -467,6 +511,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:
|
||||||
@@ -542,7 +587,7 @@ class XiaoMusic:
|
|||||||
|
|
||||||
if self._next_timer:
|
if self._next_timer:
|
||||||
self._next_timer.cancel()
|
self._next_timer.cancel()
|
||||||
self.log.info("定时器已取消")
|
self.log.info("旧定时器已取消")
|
||||||
|
|
||||||
self._timeout = sec
|
self._timeout = sec
|
||||||
|
|
||||||
@@ -587,12 +632,13 @@ 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)
|
||||||
if not opvalue:
|
if not opvalue:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
await self.check_replay()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -601,8 +647,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.isplaying():
|
||||||
|
return opvalue
|
||||||
|
else:
|
||||||
|
if not self.active_cmd or opvalue in self.active_cmd:
|
||||||
|
return opvalue
|
||||||
|
return None
|
||||||
|
|
||||||
# 匹配命令
|
# 匹配命令
|
||||||
def match_cmd(self, query, ctrl_panel):
|
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}(.*)"
|
||||||
# 匹配参数
|
# 匹配参数
|
||||||
@@ -620,20 +684,16 @@ class XiaoMusic:
|
|||||||
argafter,
|
argafter,
|
||||||
)
|
)
|
||||||
oparg = argafter
|
oparg = argafter
|
||||||
opvalue = self.config.key_word_dict.get(opkey)
|
|
||||||
if not ctrl_panel and not self._playing:
|
|
||||||
if self.active_cmd and opvalue not in self.active_cmd:
|
|
||||||
self.log.debug(f"不在激活命令中 {opvalue}")
|
|
||||||
continue
|
|
||||||
if opkey in KEY_WORD_ARG_BEFORE_DICT:
|
if opkey in KEY_WORD_ARG_BEFORE_DICT:
|
||||||
oparg = argpre
|
oparg = argpre
|
||||||
self.log.info(
|
opvalue = self.config.key_word_dict.get(opkey)
|
||||||
"匹配到指令. opkey:%s opvalue:%s oparg:%s", opkey, opvalue, oparg
|
if not ctrl_panel and not self.isplaying():
|
||||||
)
|
if self.active_cmd and opvalue not in self.active_cmd:
|
||||||
|
self.log.ifno(f"不在激活命令中 {opvalue}")
|
||||||
|
continue
|
||||||
|
self.log.info(f"匹配到指令. opkey:{opkey} opvalue:{opvalue} oparg:{oparg}")
|
||||||
return (opvalue, oparg)
|
return (opvalue, oparg)
|
||||||
if self._playing:
|
self.log.info(f"未匹配到指令 {query} {ctrl_panel}")
|
||||||
self.log.info("未匹配到指令,自动停止")
|
|
||||||
return ("stop", {})
|
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
# 判断是否播放下一首歌曲
|
# 判断是否播放下一首歌曲
|
||||||
@@ -647,37 +707,34 @@ 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.play_by_music_url(self.device_id, url)
|
||||||
self.log.debug(
|
self.log.info(
|
||||||
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}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ret = await self.mina_service.play_by_url(self.device_id, url)
|
ret = await self.mina_service.play_by_url(self.device_id, url)
|
||||||
self.log.debug(
|
self.log.info(
|
||||||
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):
|
||||||
@@ -692,6 +749,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
|
||||||
@@ -702,9 +760,9 @@ class XiaoMusic:
|
|||||||
self.cur_music = name
|
self.cur_music = name
|
||||||
self.log.info(f"cur_music {self.cur_music}")
|
self.log.info(f"cur_music {self.cur_music}")
|
||||||
sec, url = await self.get_music_sec_url(name)
|
sec, url = await self.get_music_sec_url(name)
|
||||||
self.log.info(f"播放 {url}")
|
|
||||||
await self.force_stop_xiaoai()
|
await self.force_stop_xiaoai()
|
||||||
await self.play_url(url)
|
self.log.info(f"播放 {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)
|
||||||
@@ -727,12 +785,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)
|
||||||
@@ -788,10 +847,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
|
||||||
@@ -809,10 +885,13 @@ class XiaoMusic:
|
|||||||
|
|
||||||
async def stop(self, **kwargs):
|
async def stop(self, **kwargs):
|
||||||
self._playing = False
|
self._playing = False
|
||||||
|
if kwargs.get("arg1", "") != "notts":
|
||||||
|
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("定时器已取消")
|
||||||
await self.force_stop_xiaoai()
|
await self.force_stop_xiaoai()
|
||||||
|
self.log.info("stop now")
|
||||||
|
|
||||||
async def stop_after_minute(self, **kwargs):
|
async def stop_after_minute(self, **kwargs):
|
||||||
if self._stop_timer:
|
if self._stop_timer:
|
||||||
@@ -823,12 +902,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)
|
||||||
@@ -838,7 +917,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)
|
||||||
|
|
||||||
@@ -953,3 +1032,50 @@ class XiaoMusic:
|
|||||||
self.new_record_event.set()
|
self.new_record_event.set()
|
||||||
result = await future
|
result = await future
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def play_by_music_url(self, deviceId, url, _type=2):
|
||||||
|
self.log.info(f"play_by_music_url url:{url}, type:{_type}")
|
||||||
|
audio_type = ""
|
||||||
|
if _type == 1:
|
||||||
|
# If set to MUSIC, the light will be on
|
||||||
|
audio_type = "MUSIC"
|
||||||
|
audio_id = "1741636975854617441"
|
||||||
|
music = {
|
||||||
|
"payload": {
|
||||||
|
"audio_type": audio_type,
|
||||||
|
"audio_items": [
|
||||||
|
{
|
||||||
|
"item_id": {
|
||||||
|
"audio_id": audio_id,
|
||||||
|
"cp": {
|
||||||
|
"album_id": "-1",
|
||||||
|
"episode_index": 0,
|
||||||
|
"id": "372639235",
|
||||||
|
"name": "xiaowei",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"stream": {"url": url},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = {"startaudioid": audio_id, "music": json.dumps(music)}
|
||||||
|
self.log.info(json.dumps(data))
|
||||||
|
return await self.mina_service.ubus_request(
|
||||||
|
deviceId,
|
||||||
|
"player_play_music",
|
||||||
|
"mediaplayer",
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def debug_play_by_music_url(self, arg1=None):
|
||||||
|
if arg1 is None:
|
||||||
|
arg1 = {}
|
||||||
|
data = arg1
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user