mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-15 16:18:14 +08:00
新增定时关机命令,新增控制面板界面
This commit is contained in:
@@ -42,8 +42,27 @@ KEY_WORD_DICT = {
|
||||
"随机播放": "random_play",
|
||||
"关机": "stop",
|
||||
"停止播放": "stop",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
}
|
||||
|
||||
# 命令参数在前面
|
||||
KEY_WORD_ARG_BEFORE_DICT = {
|
||||
"分钟后关机": True,
|
||||
}
|
||||
|
||||
# 匹配优先级
|
||||
KEY_MATCH_ORDER = [
|
||||
"分钟后关机",
|
||||
"播放歌曲",
|
||||
"放歌曲",
|
||||
"下一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"停止播放",
|
||||
]
|
||||
|
||||
SUPPORT_MUSIC_TYPE = [
|
||||
"mp3",
|
||||
"flac",
|
||||
|
||||
68
xiaomusic/httpserver.py
Normal file
68
xiaomusic/httpserver.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from flask import Flask, request, send_from_directory
|
||||
from threading import Thread
|
||||
|
||||
from xiaomusic.config import (
|
||||
KEY_WORD_DICT,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
host = "0.0.0.0"
|
||||
port = 8090
|
||||
static_path = "music"
|
||||
xiaomusic = None
|
||||
log = None
|
||||
|
||||
|
||||
@app.route("/allcmds")
|
||||
def allcmds():
|
||||
return KEY_WORD_DICT
|
||||
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
def redirect_to_index():
|
||||
return send_from_directory("static", "index.html")
|
||||
|
||||
|
||||
@app.route("/cmd", methods=["POST"])
|
||||
async def do_cmd():
|
||||
data = request.get_json()
|
||||
cmd = data.get("cmd")
|
||||
if len(cmd) > 0:
|
||||
log.debug("docmd. cmd:%s", cmd)
|
||||
xiaomusic.set_last_record(cmd)
|
||||
return {"ret": "OK"}
|
||||
return {"ret": "Unknow cmd"}
|
||||
|
||||
|
||||
def static_path_handler(filename):
|
||||
log.debug(filename)
|
||||
log.debug(static_path)
|
||||
absolute_path = os.path.abspath(static_path)
|
||||
log.debug(absolute_path)
|
||||
return send_from_directory(absolute_path, filename)
|
||||
|
||||
|
||||
def run_app():
|
||||
app.run(host=host, port=port)
|
||||
|
||||
|
||||
def StartHTTPServer(_host, _port, _static_path, _xiaomusic):
|
||||
global host, port, static_path, xiaomusic, log
|
||||
host = _host
|
||||
port = _port
|
||||
static_path = _static_path
|
||||
xiaomusic = _xiaomusic
|
||||
log = xiaomusic.log
|
||||
|
||||
app.add_url_rule(
|
||||
f"/{static_path}/<path:filename>", "static_path_handler", static_path_handler
|
||||
)
|
||||
|
||||
server_thread = Thread(target=run_app)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
xiaomusic.log.info(f"Serving on {host}:{port}")
|
||||
44
xiaomusic/static/app.js
Normal file
44
xiaomusic/static/app.js
Normal file
@@ -0,0 +1,44 @@
|
||||
$(function(){
|
||||
// 拉取所有可操作的命令
|
||||
$.get("/allcmds", function(data, status) {
|
||||
console.log(data, status);
|
||||
|
||||
$container=$("#cmds");
|
||||
// 遍历数据
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
append_op_button(key);
|
||||
}
|
||||
|
||||
append_op_button("5分钟后关机");
|
||||
append_op_button("10分钟后关机");
|
||||
append_op_button("30分钟后关机");
|
||||
append_op_button("60分钟后关机");
|
||||
});
|
||||
|
||||
function append_op_button(name) {
|
||||
// 创建按钮
|
||||
const $button = $("<button>");
|
||||
$button.text(name);
|
||||
$button.attr("type", "button");
|
||||
|
||||
// 设置按钮点击事件
|
||||
$button.on("click", () => {
|
||||
// 发起post请求
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/cmd",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({cmd: name}),
|
||||
success: () => {
|
||||
// 请求成功时执行的操作
|
||||
},
|
||||
error: () => {
|
||||
// 请求失败时执行的操作
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 添加按钮到容器
|
||||
$container.append($button);
|
||||
}
|
||||
});
|
||||
22
xiaomusic/static/index.html
Normal file
22
xiaomusic/static/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="/static/jquery-3.7.1.min.js"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
<style>
|
||||
button {
|
||||
margin: 10px;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>小爱音箱操控面板</h2>
|
||||
<hr>
|
||||
<div id="cmds">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2
xiaomusic/static/jquery-3.7.1.min.js
vendored
Normal file
2
xiaomusic/static/jquery-3.7.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,22 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import functools
|
||||
import http.server
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import socketserver
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import traceback
|
||||
import mutagen.mp3
|
||||
import mutagen
|
||||
from xiaomusic.httpserver import StartHTTPServer
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -29,6 +22,8 @@ from xiaomusic.config import (
|
||||
COOKIE_TEMPLATE,
|
||||
LATEST_ASK_API,
|
||||
KEY_WORD_DICT,
|
||||
KEY_WORD_ARG_BEFORE_DICT,
|
||||
KEY_MATCH_ORDER,
|
||||
SUPPORT_MUSIC_TYPE,
|
||||
Config,
|
||||
)
|
||||
@@ -43,27 +38,6 @@ PLAY_TYPE_ONE = 0 # 单曲循环
|
||||
PLAY_TYPE_ALL = 1 # 全部循环
|
||||
|
||||
|
||||
class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
logger = logging.getLogger("xiaomusic")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
self.logger.debug(f"{self.address_string()} - {format}", *args)
|
||||
|
||||
def log_error(self, format, *args):
|
||||
self.logger.error(f"{self.address_string()} - {format}", *args)
|
||||
|
||||
def copyfile(self, source, outputfile):
|
||||
try:
|
||||
super().copyfile(source, outputfile)
|
||||
except (socket.error, ConnectionResetError, BrokenPipeError):
|
||||
# ignore this or TODO find out why the error later
|
||||
pass
|
||||
|
||||
|
||||
class XiaoMusic:
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
@@ -91,6 +65,9 @@ class XiaoMusic:
|
||||
self._next_timer = None
|
||||
self._timeout = 0
|
||||
|
||||
# 关机定时器
|
||||
self._stop_timer = None
|
||||
|
||||
# setup logger
|
||||
self.log = logging.getLogger("xiaomusic")
|
||||
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
|
||||
@@ -118,7 +95,7 @@ class XiaoMusic:
|
||||
await self._init_data_hardware()
|
||||
session.cookie_jar.update_cookies(self.get_cookie())
|
||||
self.cookie_jar = session.cookie_jar
|
||||
self.start_http_server()
|
||||
StartHTTPServer(self.hostname, self.port, self.music_path, self)
|
||||
|
||||
async def login_miboy(self, session):
|
||||
account = MiAccount(
|
||||
@@ -228,6 +205,13 @@ class XiaoMusic:
|
||||
self.last_record = last_record
|
||||
self.new_record_event.set()
|
||||
|
||||
# 手动发消息
|
||||
def set_last_record(self, query):
|
||||
self.last_record = {
|
||||
"query": query,
|
||||
}
|
||||
self.new_record_event.set()
|
||||
|
||||
async def do_tts(self, value, wait_for_finish=False):
|
||||
self.log.info("do_tts: %s", value)
|
||||
if not self.config.use_command:
|
||||
@@ -252,17 +236,6 @@ class XiaoMusic:
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def start_http_server(self):
|
||||
# create the server
|
||||
handler = functools.partial(HTTPRequestHandler, directory=self.music_path)
|
||||
httpd = ThreadedHTTPServer(("", self.port), handler)
|
||||
# start the server in a new thread
|
||||
server_thread = threading.Thread(target=httpd.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
self.log.info(f"Serving on {self.hostname}:{self.port}")
|
||||
|
||||
async def get_if_xiaoai_is_playing(self):
|
||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
||||
# WTF xiaomi api
|
||||
@@ -336,7 +309,8 @@ class XiaoMusic:
|
||||
|
||||
# 获取歌曲播放地址
|
||||
def get_file_url(self, filename):
|
||||
encoded_name = urllib.parse.quote(os.path.basename(filename))
|
||||
self.log.debug("get_file_url. filename:%s", filename)
|
||||
encoded_name = urllib.parse.quote(filename)
|
||||
return f"http://{self.hostname}:{self.port}/{encoded_name}"
|
||||
|
||||
# 随机获取一首音乐
|
||||
@@ -394,6 +368,7 @@ class XiaoMusic:
|
||||
self.log.info(
|
||||
f"Running xiaomusic now, 用`{'/'.join(KEY_WORD_DICT.keys())}`开头来控制"
|
||||
)
|
||||
|
||||
while True:
|
||||
self.polling_event.set()
|
||||
await self.new_record_event.wait()
|
||||
@@ -404,8 +379,8 @@ class XiaoMusic:
|
||||
self.log.debug("收到消息:%s", query)
|
||||
|
||||
# 匹配命令
|
||||
match = re.match(rf"^({'|'.join(KEY_WORD_DICT.keys())})", query)
|
||||
if not match:
|
||||
opvalue, oparg = self.match_cmd(query)
|
||||
if not opvalue:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
@@ -415,25 +390,44 @@ class XiaoMusic:
|
||||
# waiting for xiaoai speaker done
|
||||
await asyncio.sleep(8)
|
||||
|
||||
opkey = match.groups()[0]
|
||||
opvalue = KEY_WORD_DICT[opkey]
|
||||
oparg = query[len(opkey) :]
|
||||
self.log.info("收到指令:%s %s", opkey, oparg)
|
||||
|
||||
try:
|
||||
func = getattr(self, opvalue)
|
||||
await func(name=oparg)
|
||||
await func(arg1=oparg)
|
||||
except Exception as e:
|
||||
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
|
||||
|
||||
# 匹配命令
|
||||
def match_cmd(self, query):
|
||||
for opkey in KEY_MATCH_ORDER:
|
||||
patternarg = rf"(.*){opkey}(.*)"
|
||||
# 匹配参数
|
||||
matcharg = re.match(patternarg, query)
|
||||
if not matcharg:
|
||||
continue
|
||||
|
||||
argpre = matcharg.groups()[0]
|
||||
argafter = matcharg.groups()[1]
|
||||
self.log.debug(
|
||||
"matcharg. opkey:%s, argpre:%s, argafter:%s",
|
||||
opkey,
|
||||
argpre,
|
||||
argafter,
|
||||
)
|
||||
oparg = argafter
|
||||
opvalue = KEY_WORD_DICT[opkey]
|
||||
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 (None, None)
|
||||
|
||||
# 播放歌曲
|
||||
async def play(self, **kwargs):
|
||||
name = kwargs["name"]
|
||||
name = kwargs["arg1"]
|
||||
if name == "":
|
||||
await self.play_next()
|
||||
return
|
||||
|
||||
await self.do_tts(f"即将播放{name}")
|
||||
filename = self.local_exist(name)
|
||||
if len(filename) <= 0:
|
||||
await self.download(name)
|
||||
@@ -483,3 +477,19 @@ class XiaoMusic:
|
||||
self._next_timer.cancel()
|
||||
self.log.info(f"定时器已取消")
|
||||
await self.stop_if_xiaoai_is_playing()
|
||||
|
||||
async def stop_after_minute(self, **kwargs):
|
||||
if self._stop_timer:
|
||||
self._stop_timer.cancel()
|
||||
self.log.info(f"关机定时器已取消")
|
||||
minute = int(kwargs["arg1"])
|
||||
|
||||
async def _do_stop():
|
||||
await asyncio.sleep(minute * 60)
|
||||
try:
|
||||
await self.stop()
|
||||
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}分钟后将关机")
|
||||
|
||||
Reference in New Issue
Block a user