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

Compare commits

..

19 Commits

Author SHA1 Message Date
涵曦
af25300917 new version v0.1.77 2024-06-29 13:24:37 +00:00
涵曦
f18b2f49bf fix: #52 支持配置模糊匹配本地歌曲 2024-06-29 13:21:37 +00:00
涵曦
db1e4e6fc4 整理代码 2024-06-29 12:42:15 +00:00
涵曦
dc49f63a37 新增直接播放链接的接口 2024-06-29 12:41:29 +00:00
涵曦
6837841872 Update README.md 2024-06-29 09:32:41 +08:00
涵曦
637347ae0c Update README.md 2024-06-29 09:18:26 +08:00
涵曦
ff968c4db4 Update README.md 2024-06-29 00:14:52 +08:00
涵曦
ca547e0d81 new version v0.1.76 2024-06-28 16:10:50 +00:00
涵曦
f0931c447b 下载日志链接改为按钮样式 2024-06-28 16:10:34 +00:00
涵曦
d098b5eb60 Update README.md 2024-06-28 23:45:18 +08:00
涵曦
09111e849d Update README.md 2024-06-28 23:43:48 +08:00
涵曦
49a76dee60 Update README.md 2024-06-28 23:20:45 +08:00
涵曦
637672473e Update README.md 2024-06-28 23:14:16 +08:00
涵曦
e4e1d13b69 Update README.md 2024-06-28 23:00:01 +08:00
涵曦
7736f8c5b4 Update README.md 2024-06-28 22:55:31 +08:00
涵曦
29db69b52f Update README.md 2024-06-28 22:52:04 +08:00
涵曦
37fe51771b Update README.md 2024-06-28 22:48:42 +08:00
涵曦
77ece713dc new version v0.1.75 2024-06-28 14:46:51 +00:00
涵曦
4436cc3a15 新增ogg格式文件 2024-06-28 14:46:46 +00:00
12 changed files with 151 additions and 36 deletions

View File

@@ -11,6 +11,8 @@
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
<https://github.com/hanxi/xiaomusic>
## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下:
@@ -28,8 +30,22 @@ services:
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
XIAOMUSIC_VERBOSE: 'true'
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 参数。
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678
@@ -46,10 +62,13 @@ services:
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
XIAOMUSIC_VERBOSE: 'true'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
XIAOMUSIC_PORT: 5678
```
其中 XIAOMUSIC_VERBOSE 设置为 'true' 时表示开启 debug 日志,遇到问题可以去 web 设置页面底部【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
## 开发环境运行
- 使用 install_dependencies.sh 下载依赖
@@ -87,28 +106,34 @@ pdm run xiaomusic.py
## 已测试支持的设备
```txt
- L06A
- L07A
- S12
- S12A
- LX5A
- LX05
- L16A
- L17A
- LX06
- LX01
- L05B
- L05C
````
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
## 支持音乐格式
- mp3
- flac
- wav
- ape
- ogg
> 本地音乐会搜索 mp3 和 flac 格式的文件,下载的歌曲是 mp3 格式的。
## 其他参数
- XIAOMUSIC_ACTIVE_CMD 环境变量,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C 不支持 flac 格式。
## 在 Docker 里使用
@@ -199,7 +224,7 @@ services:
XIAOMUSIC_HOSTNAME: '192.168.2.5'
```
setting.json 文件不存到 music 可以这样写,会把 setting.json 文件放到容器的 /app/conf 目录且映射到本地的 ./conf 目录:
如果想让 setting.json 文件不存到 music 目录,可以这样配,下面的示例会把 setting.json 文件放到容器的 /app/conf 目录且映射到本地的 ./conf 目录:
```yaml
services:
@@ -226,11 +251,14 @@ services:
- ip 是 XIAOMUSIC_HOSTNAME 设置的
- 8090 是默认端口
- 功能
- 支持功能
- 显示正在播放的歌曲
- 模糊搜索本地歌曲
- 播放列表
- 删除歌曲
- 设置页面
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER
@@ -245,18 +273,19 @@ services:
## 网络歌单功能
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
> 欢迎有想法的朋友们制作更多的歌单转换工具。
## 更多其他可选配置
- XIAOMUSIC_ACTIVE_CMD 配置唤醒令,具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_ACTIVE_CMD 环境变量,用于唤醒令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,具体见 <https://github.com/hanxi/xiaomusic/issues/76>
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台具体见 <https://github.com/hanxi/xiaomusic/issues/47>
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录记得把目录映射到主机默认情况会把配置存放在music目录具体见 <https://github.com/hanxi/xiaomusic/issues/74>
- XIAOMUSIC_VERBOSE 设置为 true 时开启 debug 日志,用于排查问题
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,见 <https://github.com/hanxi/xiaomusic/issues/82>
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,用于兼容不能播放的型号
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.1.74"
version = "0.1.77"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},

View File

@@ -1 +1 @@
__version__ = "0.1.74"
__version__ = "0.1.77"

View File

@@ -61,7 +61,9 @@ class Config:
"XIAOMUSIC_SEARCH", "ytsearch:"
) # "bilisearch:" or "ytsearch:"
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
active_cmd: str = os.getenv("XIAOMUSIC_ACTIVE_CMD", "play,random_play,playlocal,play_music_list")
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")
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
disable_httpauth: bool = (
@@ -80,6 +82,12 @@ class Config:
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"
)
def append_keyword(self, keys, action):
for key in keys.split(","):

View File

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

View File

@@ -2,7 +2,7 @@
import os
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 waitress import serve
@@ -162,11 +162,21 @@ def downloadjson():
"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)
def static_path_handler(filename):
log.debug(filename)
log.debug(static_path)

View File

@@ -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) {
append_op_button(name, name);
}

View File

@@ -43,6 +43,12 @@
<button id="play_music_list">播放列表歌曲</button>
<button id="del_music">删除选中歌曲</button>
<hr>
<div>
<input id="music-url" type="text" placeholder="链接(http://ngcdn001.cnr.cn/live/zgzs/index.m3u8)"></input>
</div>
<button id="playurl">播放链接</button>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>

View File

@@ -31,10 +31,10 @@
<button onclick="location.href='/';">返回首页</button>
<button id="get_music_list">获取歌单</button>
<button id="save">保存</button>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<hr>
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>
<a href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import asyncio
import copy
import json
import logging
import os
@@ -9,10 +10,9 @@ import re
import time
import traceback
import urllib.parse
from pathlib import Path
import copy
from logging.handlers import RotatingFileHandler
from pathlib import Path
from aiohttp import ClientSession, ClientTimeout
from miservice import MiAccount, MiIOService, MiNAService
@@ -31,6 +31,7 @@ from xiaomusic.const import (
from xiaomusic.httpserver import StartHTTPServer
from xiaomusic.utils import (
custom_sort_key,
find_best_match,
fuzzyfinder,
get_local_music_duration,
get_random,
@@ -117,15 +118,17 @@ class XiaoMusic:
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)
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 = '******'
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):
@@ -287,10 +290,13 @@ class XiaoMusic:
await self.mina_service.text_to_speech(self.device_id, value)
except Exception as e:
self.log.error(f"Execption {e}")
# 最大等8秒
sec = min(8, int(len(value) / 3.3))
await asyncio.sleep(sec)
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
if self._playing and not self.is_downloading():
# 继续播放歌曲
self.log.info("继续播放歌曲")
await self.play()
async def do_set_volume(self, value):
@@ -303,7 +309,10 @@ class XiaoMusic:
self.log.error(f"Execption {e}")
async def force_stop_xiaoai(self):
await self.mina_service.player_stop(self.device_id)
ret = await self.mina_service.player_pause(self.device_id)
self.log.debug(f"force_stop_xiaoai player_pause ret:{ret}")
ret = await self.mina_service.player_stop(self.device_id)
self.log.debug(f"force_stop_xiaoai player_stop ret:{ret}")
# 是否在下载中
def is_downloading(self):
@@ -587,7 +596,7 @@ class XiaoMusic:
self.polling_event.clear() # stop polling when processing the question
query = new_record.get("query", "").strip()
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)
@@ -633,7 +642,7 @@ class XiaoMusic:
return (opvalue, oparg)
if self._playing:
self.log.info("未匹配到指令,自动停止")
return ("stop", {})
return ("stop", "notts")
return (None, None)
# 判断是否播放下一首歌曲
@@ -667,7 +676,8 @@ class XiaoMusic:
{"startaudioid": audio_id, "music": json.dumps(music)},
)
async def play_url(self, url):
async def play_url(self, **kwargs):
url = kwargs.get("arg1", "")
if self.config.use_music_api:
ret = await self._play_by_music_url(self.device_id, url)
self.log.debug(
@@ -678,6 +688,21 @@ class XiaoMusic:
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}")
# 播放本地歌曲
async def playlocal(self, **kwargs):
@@ -692,6 +717,7 @@ class XiaoMusic:
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
@@ -704,7 +730,7 @@ class XiaoMusic:
sec, url = await self.get_music_sec_url(name)
self.log.info(f"播放 {url}")
await self.force_stop_xiaoai()
await self.play_url(url)
await self.play_url(arg1=url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
await self.set_next_music_timeout(sec)
@@ -727,6 +753,7 @@ class XiaoMusic:
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 self.config.disable_download:
await self.do_tts(f"本地不存在歌曲{name}")
@@ -788,10 +815,26 @@ class XiaoMusic:
self.log.error(f"del ${filename} failed")
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}")
# 播放一个播放列表
async def play_music_list(self, **kwargs):
parts = kwargs.get("arg1").split("|")
list_name = parts[0]
list_name = self.find_real_music_list_name(list_name)
if list_name not in self._music_list:
await self.do_tts(f"播放列表{list_name}不存在")
return
@@ -809,6 +852,8 @@ class XiaoMusic:
async def stop(self, **kwargs):
self._playing = False
if kwargs.get("arg1", "") != "notts":
await self.do_tts("收到关机口令,再见")
if self._next_timer:
self._next_timer.cancel()
self.log.info("定时器已取消")
@@ -823,12 +868,12 @@ class XiaoMusic:
async def _do_stop():
await asyncio.sleep(minute * 60)
try:
await self.stop()
await self.stop(arg1="notts")
except Exception as e:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
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):
value = kwargs.get("arg1", 0)
@@ -838,7 +883,7 @@ class XiaoMusic:
playing_info = await self.mina_service.player_get_status(self.device_id)
self.log.debug("get_volume. playing_info:%s", playing_info)
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
"volume", 5
"volume", 0
)
self.log.info("get_volume. volume:%s", self._volume)