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

Compare commits

..

5 Commits

Author SHA1 Message Date
涵曦
f3658e0368 new version v0.1.63 2024-06-26 01:24:09 +00:00
涵曦
d28f4e1473 修复自动播放的问题 2024-06-26 01:18:58 +00:00
涵曦
1efdcf2144 修复自动播放的问题 2024-06-26 01:10:26 +00:00
涵曦
0f1516ae7e 优化日志输出 2024-06-26 00:22:23 +00:00
涵曦
f111d67819 优化不定长参数arg1的用法 2024-06-26 00:17:29 +00:00
22 changed files with 200 additions and 814 deletions

2
.github/FUNDING.yml vendored
View File

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

View File

@@ -1,5 +1,4 @@
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

167
README.md
View File

@@ -1,20 +1,7 @@
# xiaomusic # xiaomusic
[![GitHub License](https://img.shields.io/github/license/hanxi/xiaomusic)](https://github.com/hanxi/xiaomusic)
[![Docker Image Version](https://img.shields.io/docker/v/hanxi/xiaomusic?sort=semver&label=docker%20image)](https://hub.docker.com/r/hanxi/xiaomusic)
[![Docker Pulls](https://img.shields.io/docker/pulls/hanxi/xiaomusic)](https://hub.docker.com/r/hanxi/xiaomusic)
[![PyPI - Version](https://img.shields.io/pypi/v/xiaomusic)](https://pypi.org/project/xiaomusic/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/xiaomusic)](https://pypi.org/project/xiaomusic/)
[![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fhanxi%2Fxiaomusic%2Fmain%2Fpyproject.toml)](https://pypi.org/project/xiaomusic/)
[![GitHub Release](https://img.shields.io/github/v/release/hanxi/xiaomusic)](https://github.com/hanxi/xiaomusic/releases)
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。 使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
<https://github.com/hanxi/xiaomusic>
> 初次安装遇到问题请查阅 <https://github.com/hanxi/xiaomusic/issues/99> 上是否已经有解决办法。
## 最简配置运行 ## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下: 已经支持在 web 页面配置其他参数docker compose 配置如下:
@@ -32,79 +19,10 @@ 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端口
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 5678:5678
volumes:
- ./music:/app/music
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
XIAOMUSIC_VERBOSE: 'true'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
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 下载依赖
@@ -139,37 +57,21 @@ pdm run xiaomusic.py
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。 > 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
## 已测试支持的设备 ## 已测试设备
| 型号 | 名称 |
| ---- | ---------------------------------------------------------------------------------------------- |
| L06A | [小爱音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l06a) |
| L07A | [Redmi小爱音箱 Play](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l7a) |
| S12 | [小米AI音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.s12) |
| S12A | - |
| LX5A | [小爱音箱 万能遥控版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx5a) |
| LX05 | [小爱音箱Play2019款](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx05) |
| L16A | [Xiaomi Sound](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l16a) |
| L17A | [Xiaomi Sound Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l17a) |
| LX06 | [小爱音箱Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx06) |
| LX01 | [小爱音箱mini](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx01) |
| L05B | [小爱音箱Play](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05b) |
| L05C | [小米小爱音箱Play 增强版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05c) |
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
```txt
"L07A": ("5-1", "5-5"), # Redmi小爱音箱Play(l7a)
````
## 支持音乐格式 ## 支持音乐格式
- mp3 - mp3
- flac - flac
- wav
- ape
- ogg
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。 > 本地音乐会搜索 mp3 和 flac 格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
## 其他参数
- XIAOMUSIC_ACTIVE_CMD 环境变量,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。
## 在 Docker 里使用 ## 在 Docker 里使用
@@ -260,26 +162,6 @@ services:
XIAOMUSIC_HOSTNAME: '192.168.2.5' XIAOMUSIC_HOSTNAME: '192.168.2.5'
``` ```
如果想让 setting.json 文件不存储到 music 目录,可以这样配,下面的示例会把 setting.json 文件放到容器的 /app/conf 目录且映射到本地的 ./conf 目录:
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
- ./conf:/app/conf
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
XIAOMUSIC_CONF_PATH: '/app/conf'
```
## 简易的控制面板 ## 简易的控制面板
@@ -287,14 +169,11 @@ services:
- ip 是 XIAOMUSIC_HOSTNAME 设置的 - ip 是 XIAOMUSIC_HOSTNAME 设置的
- 8090 是默认端口 - 8090 是默认端口
- 支持功能 - 功能
- 显示正在播放的歌曲 - 显示正在播放的歌曲
- 模糊搜索本地歌曲 - 模糊搜索本地歌曲
- 播放列表
- 删除歌曲
- 设置页面 - 设置页面
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,必须在启动前配置的环境变量只剩下: 采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER - MI_USER
@@ -307,35 +186,22 @@ services:
- XIAOMUSIC_SEARCH - XIAOMUSIC_SEARCH
- XIAOMUSIC_PROXY - XIAOMUSIC_PROXY
## 网络歌单功能
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
> 欢迎有想法的朋友们制作更多的歌单转换工具。
## 更多其他可选配置 ## 更多其他可选配置
- XIAOMUSIC_ACTIVE_CMD 环境变量,用于唤醒令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43> - XIAOMUSIC_ACTIVE_CMD 配置唤醒令,具体见 <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_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82> - XIAOMUSIC_VERBOSE为 true 时开启 debug 日志,用于排查问题
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐用于兼容不能播放的型号v0.1.86 之后的版本可以不用设置。
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_ENABLE_FUZZY_MATCH 设为 true 时开启模糊匹配(默认),设为 false 时关闭模糊匹配,支持模糊匹配歌名和歌单名。 具体见 <https://github.com/hanxi/xiaomusic/issues/52>
- XIAOMUSIC_FUZZY_MATCH_CUTOFF 设置模糊搜索匹配的最低相似度阈值默认0.6可以配0到1直接的小数越小越模糊越大越精准。具体见 <https://github.com/hanxi/xiaomusic/issues/52>
## 讨论区 ## 讨论区
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss) - [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
- [点击链接加入群聊【xiaomusic】 604526973](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=13St5PLVcTxYlWTAs_iAawazjtdD1l-a&authKey=dJWEpaT2fDBDpdUUOWj%2FLt6NS1ePBfShDfz7a6seNURi05VvVnAGQzXF%2FM%2F5HgIm&noverify=0&group_code=604526973) - [点击链接加入群聊【xiaomusic】 604526973](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=13St5PLVcTxYlWTAs_iAawazjtdD1l-a&authKey=dJWEpaT2fDBDpdUUOWj%2FLt6NS1ePBfShDfz7a6seNURi05VvVnAGQzXF%2FM%2F5HgIm&noverify=0&group_code=604526973)
- <https://github.com/hanxi/xiaomusic/issues> - https://github.com/hanxi/xiaomusic/issues
- [微信群二维码](https://github.com/hanxi/xiaomusic/issues/86)
## 感谢 ## 感谢
@@ -353,7 +219,4 @@ services:
[![Star History Chart](https://api.star-history.com/svg?repos=hanxi/xiaomusic&type=Date)](https://star-history.com/#hanxi/xiaomusic&Date) [![Star History Chart](https://api.star-history.com/svg?repos=hanxi/xiaomusic&type=Date)](https://star-history.com/#hanxi/xiaomusic&Date)
## 赞赏 ## 赞赏
谢谢就够了
- 爱发电 <https://afdian.net/a/imhanxi>
- 点个 Star ⭐
- 谢谢 ❤️

View File

@@ -1,32 +0,0 @@
{
"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,41 +4,14 @@
# https://github.com/yt-dlp/yt-dlp#dependencies # https://github.com/yt-dlp/yt-dlp#dependencies
# 判断系统架构 # 判断系统架构
arch=$(uname -m) arch=$(arch)
# 输出架构信息 pkg=ffmpeg-master-latest-linuxarm64-gpl
echo "当前系统架构是:$arch" if [[ "${arch}" == "x86_64" ]]; then
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
install_from_build "$pkg" fi
;;
arm64 | aarch64) #export ALL_PROXY=http://192.168.2.5:8080
echo "64位 ARM 架构" wget https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/$pkg.tar.xz
pkg=ffmpeg-master-latest-linuxarm64-gpl tar -xvJf $pkg.tar.xz
install_from_build "$pkg" mv $pkg ffmpeg
;;
*)
echo "未知架构 $arch"
install_from_apt
;;
esac

61
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:e7455b13bf13306ccf5ad11781191edb62991d9fbe8f8ce1e61a2f35c713cc2a" content_hash = "sha256:813253734c7d7835a76cd87fe8fe0329e02ad067f535aee6a9e11cb106569dd2"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@@ -519,7 +519,7 @@ files = [
[[package]] [[package]]
name = "miservice-fork" name = "miservice-fork"
version = "2.6.1" version = "2.5.0"
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.1-py3-none-any.whl", hash = "sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39"}, {file = "miservice_fork-2.5.0-py3-none-any.whl", hash = "sha256:97b6360ea53c34fe035ac9d94e8705f305b8fa7fc2b44a7aea182449a76cb622"},
{file = "miservice_fork-2.6.1.tar.gz", hash = "sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c"}, {file = "miservice_fork-2.5.0.tar.gz", hash = "sha256:8ca2d370d5b32f7e330add38aa1912d734aefa7880f16cef9eac110a5a3029e2"},
] ]
[[package]] [[package]]
@@ -632,8 +632,8 @@ files = [
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.31.0" version = "2.32.3"
requires_python = ">=3.7" requires_python = ">=3.8"
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.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
] ]
[[package]] [[package]]
@@ -662,28 +662,27 @@ files = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.5.0" version = "0.4.9"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust." summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [ files = [
{file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, {file = "ruff-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b262ed08d036ebe162123170b35703aaf9daffecb698cd367a8d585157732991"},
{file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, {file = "ruff-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:98ec2775fd2d856dc405635e5ee4ff177920f2141b8e2d9eb5bd6efd50e80317"},
{file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"}, {file = "ruff-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4555056049d46d8a381f746680db1c46e67ac3b00d714606304077682832998e"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"}, {file = "ruff-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e91175fbe48f8a2174c9aad70438fe9cb0a5732c4159b2a10a3565fea2d94cde"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"}, {file = "ruff-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8e7b95673f22e0efd3571fb5b0cf71a5eaaa3cc8a776584f3b2cc878e46bff"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"}, {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2d45ddc6d82e1190ea737341326ecbc9a61447ba331b0a8962869fcada758505"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"}, {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78de3fdb95c4af084087628132336772b1c5044f6e710739d440fc0bccf4d321"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"}, {file = "ruff-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06b60f91bfa5514bb689b500a25ba48e897d18fea14dce14b48a0c40d1635893"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"}, {file = "ruff-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88bffe9c6a454bf8529f9ab9091c99490578a593cc9f9822b7fc065ee0712a06"},
{file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"}, {file = "ruff-0.4.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:673bddb893f21ab47a8334c8e0ea7fd6598ecc8e698da75bcd12a7b9d0a3206e"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"}, {file = "ruff-0.4.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c1aff58c31948cc66d0b22951aa19edb5af0a3af40c936340cd32a8b1ab7438"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"}, {file = "ruff-0.4.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:784d3ec9bd6493c3b720a0b76f741e6c2d7d44f6b2be87f5eef1ae8cc1d54c84"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"}, {file = "ruff-0.4.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:732dd550bfa5d85af8c3c6cbc47ba5b67c6aed8a89e2f011b908fc88f87649db"},
{file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"}, {file = "ruff-0.4.9-py3-none-win32.whl", hash = "sha256:8064590fd1a50dcf4909c268b0e7c2498253273309ad3d97e4a752bb9df4f521"},
{file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"}, {file = "ruff-0.4.9-py3-none-win_amd64.whl", hash = "sha256:e0a22c4157e53d006530c902107c7f550b9233e9706313ab57b892d7197d8e52"},
{file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"}, {file = "ruff-0.4.9-py3-none-win_arm64.whl", hash = "sha256:5d5460f789ccf4efd43f265a58538a2c24dbce15dbf560676e430375f20a8198"},
{file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"}, {file = "ruff-0.4.9.tar.gz", hash = "sha256:f1cb0828ac9533ba0135d148d214e284711ede33640465e706772645483427e3"},
{file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"},
] ]
[[package]] [[package]]
@@ -832,7 +831,7 @@ files = [
[[package]] [[package]]
name = "yt-dlp" name = "yt-dlp"
version = "2024.5.30.232720.dev0" version = "2024.6.17.232743.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 +840,11 @@ dependencies = [
"certifi", "certifi",
"mutagen", "mutagen",
"pycryptodomex", "pycryptodomex",
"requests<3,>=2.31.0", "requests<3,>=2.32.2",
"urllib3<3,>=1.26.17", "urllib3<3,>=1.26.17",
"websockets>=12.0", "websockets>=12.0",
] ]
files = [ files = [
{file = "yt_dlp-2024.5.30.232720.dev0-py3-none-any.whl", hash = "sha256:d6e563a2923807392325722028e7792e35affb694a505617b008195d0d212d2c"}, {file = "yt_dlp-2024.6.17.232743.dev0-py3-none-any.whl", hash = "sha256:dd6e7e194b96e778691f58a0cb6b42956cf956b22f6bb1a12bdef5ab3ac0c9ad"},
{file = "yt_dlp-2024.5.30.232720.dev0.tar.gz", hash = "sha256:9e2b177c5b13ea6f54cee1c56a69dd7832d506fba73a2247c6470e7d1952f959"}, {file = "yt_dlp-2024.6.17.232743.dev0.tar.gz", hash = "sha256:2f6f44eff755a7b051cdcd3c4375771033dbeb64d6164351022efdc67cce0c52"},
] ]

View File

@@ -1,12 +1,12 @@
[project] [project]
name = "xiaomusic" name = "xiaomusic"
version = "0.1.87" version = "0.1.63"
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,12 +19,6 @@ 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.1 \ miservice-fork==2.5.0 \
--hash=sha256:1702281e1e9827958eb3e82bc3242cd013c018e9aa1de8509b4805b5ccf5e60c \ --hash=sha256:8ca2d370d5b32f7e330add38aa1912d734aefa7880f16cef9eac110a5a3029e2 \
--hash=sha256:9b2cc4208486bbbf788d1bde6e2cbc70f241ce10db4dca6f918076a2d2942a39 --hash=sha256:97b6360ea53c34fe035ac9d94e8705f305b8fa7fc2b44a7aea182449a76cb622
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.31.0 \ requests==2.32.3 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
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.5.30.232720.dev0 \ yt-dlp==2024.6.17.232743.dev0 \
--hash=sha256:9e2b177c5b13ea6f54cee1c56a69dd7832d506fba73a2247c6470e7d1952f959 \ --hash=sha256:2f6f44eff755a7b051cdcd3c4375771033dbeb64d6164351022efdc67cce0c52 \
--hash=sha256:d6e563a2923807392325722028e7792e35affb694a505617b008195d0d212d2c --hash=sha256:dd6e7e194b96e778691f58a0cb6b42956cf956b22f6bb1a12bdef5ab3ac0c9ad

View File

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

View File

@@ -1,21 +1,9 @@
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()
@@ -57,8 +45,6 @@ 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

@@ -7,15 +7,18 @@ from dataclasses import dataclass
from xiaomusic.utils import validate_proxy from xiaomusic.utils import validate_proxy
# 默认口令 LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}&timestamp={timestamp}&limit=2"
DEFAULT_KEY_WORD_DICT = { COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}"
KEY_WORD_DICT = {
"播放歌曲": "play", "播放歌曲": "play",
"播放本地歌曲": "playlocal", "歌曲": "play",
"关机": "stop",
"下一首": "play_next", "下一首": "play_next",
"单曲循环": "set_play_type_one", "单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all", "全部循环": "set_play_type_all",
"随机播放": "random_play", "随机播放": "random_play",
"关机": "stop",
"停止播放": "stop",
"分钟后关机": "stop_after_minute", "分钟后关机": "stop_after_minute",
"播放列表": "play_music_list", "播放列表": "play_music_list",
"刷新列表": "gen_music_list", "刷新列表": "gen_music_list",
@@ -28,21 +31,30 @@ KEY_WORD_ARG_BEFORE_DICT = {
"分钟后关机": True, "分钟后关机": True,
} }
# 口令匹配优先级 # 匹配优先级
DEFAULT_KEY_MATCH_ORDER = [ KEY_MATCH_ORDER = [
"set_volume#", "set_volume#",
"get_volume#", "get_volume#",
"分钟后关机", "分钟后关机",
"播放歌曲", "播放歌曲",
"放歌曲",
"下一首", "下一首",
"单曲循环", "单曲循环",
"全部循环", "全部循环",
"随机播放", "随机播放",
"关机", "关机",
"停止播放",
"刷新列表", "刷新列表",
"播放列表", "播放列表",
] ]
SUPPORT_MUSIC_TYPE = [
".mp3",
".flac",
".wav",
".ape",
]
@dataclass @dataclass
class Config: class Config:
@@ -61,9 +73,7 @@ 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( active_cmd: str = os.getenv("XIAOMUSIC_ACTIVE_CMD", "play,random_play")
"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 = (
@@ -73,46 +83,10 @@ class Config:
httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "admin") httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "admin")
music_list_url: str = os.getenv("XIAOMUSIC_MUSIC_LIST_URL", "") music_list_url: str = os.getenv("XIAOMUSIC_MUSIC_LIST_URL", "")
music_list_json: str = os.getenv("XIAOMUSIC_MUSIC_LIST_JSON", "") music_list_json: str = os.getenv("XIAOMUSIC_MUSIC_LIST_JSON", "")
disable_download: bool = (
os.getenv("XIAOMUSIC_DISABLE_DOWNLOAD", "false").lower() == "true"
)
key_word_dict = DEFAULT_KEY_WORD_DICT.copy()
key_match_order = DEFAULT_KEY_MATCH_ORDER.copy()
use_music_api: bool = (
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):
for key in keys.split(","):
self.key_word_dict[key] = action
if key not in self.key_match_order:
self.key_match_order.append(key)
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.proxy: if self.proxy:
validate_proxy(self.proxy) validate_proxy(self.proxy)
self.append_keyword(self.keywords_playlocal, "playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_stop, "stop")
# 保存配置到 config-example.json 文件
# with open("config-example.json", "w") as f:
# data = asdict(self)
# 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

@@ -1,10 +0,0 @@
SUPPORT_MUSIC_TYPE = [
".mp3",
".flac",
".wav",
".ape",
".ogg",
]
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}&timestamp={timestamp}&limit=2"
COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}"

View File

@@ -2,13 +2,16 @@
import os import os
from threading import Thread from threading import Thread
from flask import Flask, request, send_file, send_from_directory from flask import Flask, request, send_from_directory
from flask_httpauth import HTTPBasicAuth from flask_httpauth import HTTPBasicAuth
from waitress import serve from waitress import serve
from xiaomusic import ( from xiaomusic import (
__version__, __version__,
) )
from xiaomusic.config import (
KEY_WORD_DICT,
)
from xiaomusic.utils import ( from xiaomusic.utils import (
downloadfile, downloadfile,
) )
@@ -38,7 +41,7 @@ def verify_password(username, password):
@app.route("/allcmds") @app.route("/allcmds")
@auth.login_required @auth.login_required
def allcmds(): def allcmds():
return xiaomusic.config.key_word_dict return KEY_WORD_DICT
@app.route("/getversion", methods=["GET"]) @app.route("/getversion", methods=["GET"])
@@ -150,43 +153,13 @@ def delmusic():
def downloadjson(): def downloadjson():
data = request.get_json() data = request.get_json()
log.info(data) log.info(data)
url = data["url"] ret, content = downloadfile(data["url"])
try:
ret = "OK"
content = downloadfile(url)
except Exception as e:
log.warning(f"downloadjson failed. url:{url} e:{e}")
ret = "Download JSON file failed."
return { return {
"ret": ret, "ret": ret,
"content": content, "content": content,
} }
@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,13 +82,6 @@ $(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

@@ -1,40 +0,0 @@
<!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,11 +8,7 @@
<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>小爱音箱操控面板 <h2>小爱音箱操控面板<span id="version">(版本未知)</span></h2>
(<a id="version" href="https://github.com/hanxi/xiaomusic/releases">
版本未知
</a>)
</h2>
<hr> <hr>
<div id="cmds"> <div id="cmds">
</div> </div>
@@ -33,7 +29,7 @@
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)"></input> <input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)"></input>
</div> </div>
<button id="play">播放</button> <button id="play">播放</button>
<div class="container"> <div class="cawait get_web_music_duration(url)ontainer">
<div id="playering-music" class="text"></div> <div id="playering-music" class="text"></div>
</div> </div>
@@ -47,12 +43,6 @@
<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

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>M3U to JSON Converter</title>
<link rel="stylesheet" type="text/css" href="/static/style.css">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole 默认会挂载到 `window.VConsole` 上
var vConsole = new window.VConsole();
</script>
-->
<script>
function handleFileSelect(evt) {
var file = evt.target.files[0];
if (file) {
var reader = new FileReader();
reader.onload = function(e) {
document.getElementById('m3u-input').value = e.target.result;
};
reader.readAsText(file);
} else {
alert('无法加载文件');
}
}
function convertToJSON() {
var m3uContent = document.getElementById('m3u-input').value;
var lines = m3uContent.split('\n');
console.log(lines);
var musicsArray = [];
var currentName = '';
lines.forEach(function(line) {
line = line.trim();
if (line.startsWith('#EXTINF:')) {
currentName = line.replace(/.*,/g, '');
} else if (line.startsWith('http') && currentName !== '') {
musicsArray.push({"name": currentName, "type": "radio", "url": line});
currentName = ''; // Reset the name for the next entry
}
});
var output = [{
"name": "m3u电台",
"musics": musicsArray
}];
document.getElementById('json-output').value = JSON.stringify(output, null, 2);
}
</script>
</head>
<body>
<h1>M3U to JSON Converter</h1>
<input type="file" id="file-input" accept=".m3u" onchange="handleFileSelect(event)"/><br>
<textarea id="m3u-input" rows="10" cols="50" placeholder="粘贴m3u内容或上传文件..."></textarea><br>
<button onclick="convertToJSON()">转换</button><br>
<textarea id="json-output" rows="10" cols="50" placeholder="转换后的JSON..."></textarea>
</body>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>
</html>

View File

@@ -8,17 +8,13 @@
<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>小爱音箱设置面板 <h2>小爱音箱设置面板<span id="version">(版本未知)</span></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>
<select id="mi_did"></select> <select id="mi_did"></select>
<label for="mi_hardware">MI_HARDWARE(型号):</label> <label for="mi_hardware">MI_HARDWARE:</label>
<select id="mi_hardware" disabled></select> <select id="mi_hardware"></select>
<label for="xiaomusic_search">XIAOMUSIC_SEARCH:</label> <label for="xiaomusic_search">XIAOMUSIC_SEARCH:</label>
<select id="xiaomusic_search"> <select id="xiaomusic_search">
<option value="ytsearch:">ytsearch:</option> <option value="ytsearch:">ytsearch:</option>
@@ -35,12 +31,6 @@
<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>
<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,52 +2,36 @@ $(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 select = $(selectId);
select.empty();
optionsList.forEach(option => {
select.append(new Option(option, option));
});
select.val(selectedOption);
};
let isChanging = false;
// 更新下拉菜单的函数
const updateSelect = (selectId, value) => {
if (!isChanging) {
isChanging = true;
$(selectId).val(value);
isChanging = false;
}
};
// 联动逻辑
const linkSelects = (sourceSelect, sourceList, targetSelect, targetList) => {
$(sourceSelect).change(function() {
if (!isChanging) {
const selectedValue = $(this).val();
const selectedIndex = sourceList.indexOf(selectedValue);
console.log(selectedIndex, selectedValue,sourceList,targetList)
if (selectedIndex !== -1) {
updateSelect(targetSelect, targetList[selectedIndex]);
}
}
});
};
// 拉取现有配置 // 拉取现有配置
$.get("/getsetting", function(data, status) { $.get("/getsetting", function(data, status) {
console.log(data, status); console.log(data, status);
updateSelectOptions("#mi_did", data.mi_did_list, data.mi_did); var mi_did_div = $("#mi_did")
updateSelectOptions("#mi_hardware", data.mi_hardware_list, data.mi_hardware); mi_did_div.empty();
$.each(data.mi_did_list, function(index, option){
mi_did_div.append($('<option>', {
value:option,
text:option,
}));
if (data.mi_did == option) {
mi_did_div.val(option);
}
});
// 初始化联动 var mi_hardware_div = $("#mi_hardware")
linkSelects('#mi_did', data.mi_did_list, '#mi_hardware', data.mi_hardware_list); mi_hardware_div.empty();
$.each(data.mi_hardware_list, function(index, option){
mi_hardware_div.append($('<option>', {
value:option,
text:option,
}));
if (data.mi_hardware == option) {
mi_hardware_div.val(option);
}
});
if (data.xiaomusic_search != "") { if (data.xiaomusic_search != "") {
$("#xiaomusic_search").val(data.xiaomusic_search); $("#xiaomusic_search").val(data.xiaomusic_search);

View File

@@ -1,8 +1,4 @@
.button { button {
line-height: 50px;
font-size: 14px;
}
button, .button {
margin: 10px; margin: 10px;
width: 100px; width: 100px;
height: 50px; height: 50px;
@@ -14,7 +10,7 @@ button, .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

@@ -3,9 +3,7 @@ from __future__ import annotations
import difflib import difflib
import os import os
import random
import re import re
import string
import tempfile import tempfile
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
@@ -16,8 +14,6 @@ import mutagen
import requests import requests
from requests.utils import cookiejar_from_dict from requests.utils import cookiejar_from_dict
from xiaomusic.const import SUPPORT_MUSIC_TYPE
### HELP FUNCTION ### ### HELP FUNCTION ###
def parse_cookie_string(cookie_string): def parse_cookie_string(cookie_string):
@@ -72,12 +68,7 @@ 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, n=10, cutoff=0.1) return difflib.get_close_matches(user_input, collection, 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
# 歌曲排序 # 歌曲排序
@@ -160,23 +151,19 @@ def walk_to_depth(root, depth=None, *args, **kwargs):
def downloadfile(url): def downloadfile(url):
# 清理和验证URL try:
# 解析URL response = requests.get(url, timeout=5) # 增加超时以避免长时间挂起
parsed_url = urlparse(url) response.raise_for_status() # 如果响应不是200引发HTTPError异常
return ("OK", response.text)
# 基础验证仅允许HTTP和HTTPS协议 except requests.exceptions.HTTPError as errh:
if parsed_url.scheme not in ("http", "https"): return (f"HTTP Error: {errh}", "")
raise Warning( except requests.exceptions.ConnectionError as errc:
f"Invalid URL scheme: {parsed_url.scheme}. Only HTTP and HTTPS are allowed." return (f"Error Connecting: {errc}", "")
) except requests.exceptions.Timeout as errt:
return (f"Timeout Error: {errt}", "")
# 构建目标URL except requests.exceptions.RequestException as err:
cleaned_url = parsed_url.geturl() return (f"Oops: Something Else, {err}", "")
return ("Unknow Error", "")
# 发起请求
response = requests.get(cleaned_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
return response.text
async def _get_web_music_duration(session, url, start=0, end=500): async def _get_web_music_duration(session, url, start=0, end=500):
@@ -200,20 +187,6 @@ async def _get_web_music_duration(session, url, start=0, end=500):
async def get_web_music_duration(url, start=0, end=500): async def get_web_music_duration(url, start=0, end=500):
duration = 0 duration = 0
try: try:
parsed_url = urlparse(url)
file_path = parsed_url.path
_, extension = os.path.splitext(file_path)
if extension.lower() not in SUPPORT_MUSIC_TYPE:
cleaned_url = parsed_url.geturl()
async with aiohttp.ClientSession() as session:
async with session.get(
cleaned_url,
allow_redirects=True,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
},
) as response:
url = str(response.url)
# 设置总超时时间为3秒 # 设置总超时时间为3秒
timeout = aiohttp.ClientTimeout(total=3) timeout = aiohttp.ClientTimeout(total=3)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
@@ -224,7 +197,7 @@ async def get_web_music_duration(url, start=0, end=500):
) )
except Exception: except Exception:
pass pass
return duration, url return duration
# 获取文件播放时长 # 获取文件播放时长
@@ -236,7 +209,3 @@ def get_local_music_duration(filename):
except Exception: except Exception:
pass pass
return duration return duration
def get_random(length):
return "".join(random.sample(string.ascii_letters + string.digits, length))

View File

@@ -1,6 +1,5 @@
#!/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
@@ -10,7 +9,6 @@ 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
@@ -20,18 +18,17 @@ from xiaomusic import (
__version__, __version__,
) )
from xiaomusic.config import ( from xiaomusic.config import (
KEY_WORD_ARG_BEFORE_DICT,
Config,
)
from xiaomusic.const import (
COOKIE_TEMPLATE, COOKIE_TEMPLATE,
KEY_MATCH_ORDER,
KEY_WORD_ARG_BEFORE_DICT,
KEY_WORD_DICT,
LATEST_ASK_API, LATEST_ASK_API,
SUPPORT_MUSIC_TYPE, SUPPORT_MUSIC_TYPE,
Config,
) )
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_web_music_duration, get_web_music_duration,
@@ -93,8 +90,14 @@ class XiaoMusic:
# 关机定时器 # 关机定时器
self._stop_timer = None self._stop_timer = None
# 初始化日志 # setup logger
self.setup_logger() logging.basicConfig(
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()
@@ -105,30 +108,7 @@ class XiaoMusic:
# 启动时初始化获取声音 # 启动时初始化获取声音
self.set_last_record("get_volume#") self.set_last_record("get_volume#")
def setup_logger(self): self.log.info("ffmpeg_location: %s", self.ffmpeg_location)
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:
@@ -289,13 +269,10 @@ 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):
@@ -307,25 +284,8 @@ 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):
ret = await self.mina_service.player_pause(self.device_id) await self.mina_service.player_stop(self.device_id)
self.log.debug(f"force_stop_xiaoai player_pause ret:{ret}")
# 是否在下载中 # 是否在下载中
def is_downloading(self): def is_downloading(self):
@@ -400,28 +360,7 @@ class XiaoMusic:
url = self._all_music[name] url = self._all_music[name]
return url.startswith(("http://", "https://")) return url.startswith(("http://", "https://"))
# 获取歌曲播放时长,播放地址 # 获取歌曲播放地址
async def get_music_sec_url(self, name):
sec = 0
url = self.get_music_url(name)
if self.is_web_radio_music(name):
self.log.info("电台不会有播放时长")
return 0, url
if self.is_web_music(name):
origin_url = url
duration, url = await get_web_music_duration(url)
sec = int(duration)
self.log.info(f"网络歌曲 {name} : {origin_url} {url} 的时长 {sec}")
else:
filename = self.get_filename(name)
sec = int(get_local_music_duration(filename))
self.log.info(f"本地歌曲 {name} : {filename} {url} 的时长 {sec}")
if sec <= 0:
self.log.warning(f"获取歌曲时长失败 {name} {url}")
return sec, url
def get_music_url(self, name): def get_music_url(self, name):
if self.is_web_music(name): if self.is_web_music(name):
url = self._all_music[name] url = self._all_music[name]
@@ -489,7 +428,6 @@ 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:
@@ -510,7 +448,7 @@ class XiaoMusic:
# 处理电台列表 # 处理电台列表
if music_type == "radio": if music_type == "radio":
self._all_radio[name] = url self._all_radio[name] = url
self.log.debug(one_music_list) self.log.info(one_music_list)
# 歌曲名字相同会覆盖 # 歌曲名字相同会覆盖
self._music_list[list_name] = one_music_list self._music_list[list_name] = one_music_list
if self._all_radio: if self._all_radio:
@@ -559,14 +497,30 @@ class XiaoMusic:
return name return name
# 设置下一首歌曲的播放定时器 # 设置下一首歌曲的播放定时器
async def set_next_music_timeout(self, sec): async def set_next_music_timeout(self):
if sec <= 0: name = self.cur_music
if self.is_web_radio_music(name):
self.log.info("电台不会有下一首的定时器")
return return
if self.is_web_music(name):
url = self._all_music[name]
duration = await get_web_music_duration(url)
sec = int(duration)
self.log.info(f"网络歌曲 {name} : {url} 的时长 {sec}")
else:
filename = self.get_filename(name)
sec = int(get_local_music_duration(filename))
self.log.info(f"本地歌曲 {name} : {filename} 的时长 {sec}")
if self._next_timer: if self._next_timer:
self._next_timer.cancel() self._next_timer.cancel()
self.log.info("定时器已取消") self.log.info("定时器已取消")
if sec <= 0:
self.log.warning("获取歌曲时长失败,不会开启下一首歌曲的定时器")
return
self._timeout = sec self._timeout = sec
async def _do_next(): async def _do_next():
@@ -577,7 +531,7 @@ class XiaoMusic:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}") self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
self._next_timer = asyncio.ensure_future(_do_next()) self._next_timer = asyncio.ensure_future(_do_next())
self.log.info(f"{sec}秒后将会播放下一首歌曲") self.log.info(f"{sec}秒后将会播放下一首")
async def run_forever(self): async def run_forever(self):
StartHTTPServer(self.port, self.music_path, self) StartHTTPServer(self.port, self.music_path, self)
@@ -587,11 +541,10 @@ class XiaoMusic:
task = asyncio.create_task(self.poll_latest_ask()) task = asyncio.create_task(self.poll_latest_ask())
assert task is not None # to keep the reference to task, do not remove this assert task is not None # to keep the reference to task, do not remove this
filtered_keywords = [ filtered_keywords = [
keyword for keyword in self.config.key_match_order if "#" not in keyword keyword for keyword in KEY_MATCH_ORDER if "#" not in keyword
] ]
joined_keywords = "/".join(filtered_keywords) joined_keywords = "/".join(filtered_keywords)
self.log.info(f"Running xiaomusic now, 用`{joined_keywords}`开头来控制") self.log.info(f"Running xiaomusic now, 用`{joined_keywords}`开头来控制")
self.log.info(self.config.key_word_dict)
while True: while True:
self.polling_event.set() self.polling_event.set()
@@ -610,7 +563,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.info("收到消息:%s 控制面板:%s", query, ctrl_panel) self.log.debug("收到消息:%s 控制面板:%s", query, ctrl_panel)
# 匹配命令 # 匹配命令
opvalue, oparg = self.match_cmd(query, ctrl_panel) opvalue, oparg = self.match_cmd(query, ctrl_panel)
@@ -624,27 +577,9 @@ 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):
# 优先处理完全匹配 for opkey in KEY_MATCH_ORDER:
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:
patternarg = rf"(.*){opkey}(.*)" patternarg = rf"(.*){opkey}(.*)"
# 匹配参数 # 匹配参数
matcharg = re.match(patternarg, query) matcharg = re.match(patternarg, query)
@@ -661,18 +596,20 @@ class XiaoMusic:
argafter, argafter,
) )
oparg = argafter oparg = argafter
if opkey in KEY_WORD_ARG_BEFORE_DICT: opvalue = KEY_WORD_DICT[opkey]
oparg = argpre
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.ifno(f"不在激活命令中 {opvalue}") self.log.debug(f"不在激活命令中 {opvalue}")
continue continue
self.log.info(f"匹配到指令. opkey:{opkey} opvalue:{opvalue} oparg:{oparg}") if opkey in KEY_WORD_ARG_BEFORE_DICT:
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", "notts") return ("stop", {})
return (None, None) return (None, None)
# 判断是否播放下一首歌曲 # 判断是否播放下一首歌曲
@@ -686,66 +623,6 @@ class XiaoMusic:
return True return True
return False return False
async def play_url(self, **kwargs):
url = kwargs.get("arg1", "")
if self.config.use_music_api:
ret = await self.mina_service.play_by_music_url(self.device_id, url, _type=2)
self.log.debug(
f"play_url play_by_music_url {self.config.hardware}. ret:{ret} url:{url}"
)
else:
ret = await self.mina_service.play_by_url(self.device_id, url)
self.log.debug(
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):
name = kwargs.get("arg1", "")
if name == "":
if self.check_play_next():
await self.play_next()
return
else:
name = self.cur_music
self.log.info(f"playlocal. name:{name}")
# 本地歌曲不存在时下载
name = self.find_real_music_name(name)
if not self.is_music_exist(name):
await self.do_tts(f"本地不存在歌曲{name}")
return
await self._playmusic(name)
async def _playmusic(self, name):
self._playing = True
self.cur_music = name
self.log.info(f"cur_music {self.cur_music}")
sec, url = await self.get_music_sec_url(name)
self.log.info(f"播放 {url}")
await self.force_stop_xiaoai()
await self.play_url(arg1=url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
await self.set_next_music_timeout(sec)
# 播放歌曲 # 播放歌曲
async def play(self, **kwargs): async def play(self, **kwargs):
parts = kwargs.get("arg1", "").split("|") parts = kwargs.get("arg1", "").split("|")
@@ -764,17 +641,23 @@ 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:
await self.do_tts(f"本地不存在歌曲{name}")
return
await self.download(search_key, name) await self.download(search_key, name)
self.log.info(f"正在下载中 {search_key} {name}") self.log.info("正在下载中 %s", search_key + ":" + name)
await self.download_proc.wait() await self.download_proc.wait()
# 把文件插入到播放列表里 # 把文件插入到播放列表里
self.add_download_music(name) self.add_download_music(name)
await self._playmusic(name)
self._playing = True
self.cur_music = name
self.log.info("cur_music %s", self.cur_music)
url = self.get_music_url(name)
self.log.info("播放 %s", url)
await self.force_stop_xiaoai()
await self.mina_service.play_by_url(self.device_id, url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
await self.set_next_music_timeout()
# 下一首 # 下一首
async def play_next(self, **kwargs): async def play_next(self, **kwargs):
@@ -826,27 +709,10 @@ 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
@@ -864,9 +730,6 @@ 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("定时器已取消")
@@ -881,12 +744,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(arg1="notts") await self.stop()
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())
await self.do_tts(f"收到,{minute}分钟后将关机") self.log.info(f"{minute}分钟后将关机")
async def set_volume(self, **kwargs): async def set_volume(self, **kwargs):
value = kwargs.get("arg1", 0) value = kwargs.get("arg1", 0)
@@ -896,7 +759,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", 0 "volume", 5
) )
self.log.info("get_volume. volume:%s", self._volume) self.log.info("get_volume. volume:%s", self._volume)
@@ -970,7 +833,7 @@ class XiaoMusic:
self.search_prefix = self.config.search_prefix self.search_prefix = self.config.search_prefix
self.proxy = self.config.proxy self.proxy = self.config.proxy
self.log.debug("update_config_from_setting ok. data:%s", data) self.log.info("update_config_from_setting ok. data:%s", data)
# 重新初始化 # 重新初始化
async def reinit(self, **kwargs): async def reinit(self, **kwargs):
@@ -1011,15 +874,3 @@ 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,
)