fix: 继续优化延迟问题

This commit is contained in:
涵曦 2024-08-01 00:05:19 +00:00
parent 6e2d674758
commit 2a1fa9f8cf
5 changed files with 166 additions and 53 deletions

View File

@ -143,39 +143,48 @@ def main():
except Exception as e: except Exception as e:
print(f"Execption {e}") print(f"Execption {e}")
def run_server(): def run_server(port):
xiaomusic = XiaoMusic(config) xiaomusic = XiaoMusic(config)
HttpInit(xiaomusic) HttpInit(xiaomusic)
uvicorn.run( uvicorn.run(
HttpApp, HttpApp,
host="127.0.0.1", host=["0.0.0.0", "::"],
port=config.port + 1, port=port,
log_config=LOGGING_CONFIG, log_config=LOGGING_CONFIG,
) )
command = [ process = None
"uvicorn",
"xiaomusic.gate:app", def run_gate():
"--workers", command = [
"4", "uvicorn",
"--host", "xiaomusic.gate:app",
"0.0.0.0", "--workers",
"--port", "4",
str(config.port), "--host",
] "0.0.0.0",
"--port",
str(config.port),
]
global process
process = subprocess.Popen(command)
process = subprocess.Popen(command)
def signal_handler(sig, frame): def signal_handler(sig, frame):
print("主进程收到退出信号,准备退出...") print("主进程收到退出信号,准备退出...")
process.terminate() # 终止子进程 if process is not None:
process.wait() # 等待子进程退出 process.terminate() # 终止子进程
print("子进程已退出") process.wait() # 等待子进程退出
print("子进程已退出")
os._exit(0) # 退出主进程 os._exit(0) # 退出主进程
# 捕获主进程的退出信号 # 捕获主进程的退出信号
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
run_server() if config.enable_gate:
run_gate()
run_server(config.port + 1)
else:
run_server(config.port)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -137,6 +137,7 @@ class Config:
os.getenv("XIAOMUSIC_REMOVE_ID3TAG", "false").lower() == "true" os.getenv("XIAOMUSIC_REMOVE_ID3TAG", "false").lower() == "true"
) )
delay_sec: int = int(os.getenv("XIAOMUSIC_DELAY_SEC", 3)) # 下一首歌延迟播放秒数 delay_sec: int = int(os.getenv("XIAOMUSIC_DELAY_SEC", 3)) # 下一首歌延迟播放秒数
enable_gate: bool = os.getenv("XIAOMUSIC_ENABLE_GATE", "true").lower() == "true"
def append_keyword(self, keys, action): def append_keyword(self, keys, action):
for key in keys.split(","): for key in keys.split(","):

View File

@ -1,13 +1,15 @@
import json import json
import logging import logging
import mimetypes
import os import os
import re
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import aiofiles
import httpx import httpx
from fastapi import FastAPI, Request from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import Response from fastapi.responses import Response, StreamingResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.background import BackgroundTask
from xiaomusic import __version__ from xiaomusic import __version__
from xiaomusic.config import Config from xiaomusic.config import Config
@ -36,19 +38,69 @@ app = FastAPI(
) )
def reset_gate(): folder = os.path.dirname(__file__)
# 更新 music 链接 app.mount("/static", StaticFiles(directory=f"{folder}/static"), name="static")
app.router.routes = [route for route in app.router.routes if route.path != "/music"]
app.mount(
"/music", async def file_iterator(file_path, start, end):
StaticFiles(directory=config.music_path, follow_symlink=True), async with aiofiles.open(file_path, mode="rb") as file:
name="music", await file.seek(start)
chunk_size = 1024
while start <= end:
read_size = min(chunk_size, end - start + 1)
data = await file.read(read_size)
if not data:
break
start += len(data)
yield data
range_pattern = re.compile(r"bytes=(\d+)-(\d*)")
@app.get("/music/{file_path:path}")
async def music_file(request: Request, file_path: str):
absolute_path = os.path.abspath(config.music_path)
absolute_file_path = os.path.join(absolute_path, file_path)
if not os.path.exists(absolute_file_path):
raise HTTPException(status_code=404, detail="File not found")
file_size = os.path.getsize(absolute_file_path)
range_start, range_end = 0, file_size - 1
range_header = request.headers.get("Range")
log.info(f"music_file range_header {range_header}")
if range_header:
range_match = range_pattern.match(range_header)
if range_match:
range_start = int(range_match.group(1))
if range_match.group(2):
range_end = int(range_match.group(2))
log.info(f"music_file in range {absolute_file_path}")
log.info(f"music_file {range_start} {range_end} {absolute_file_path}")
headers = {
"Content-Range": f"bytes {range_start}-{range_end}/{file_size}",
"Accept-Ranges": "bytes",
}
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
mime_type = "application/octet-stream"
return StreamingResponse(
file_iterator(absolute_file_path, range_start, range_end),
headers=headers,
status_code=206 if range_header else 200,
media_type=mime_type,
) )
folder = os.path.dirname(__file__) @app.options("/music/{file_path:path}")
app.mount("/static", StaticFiles(directory=f"{folder}/static"), name="static") async def music_options():
reset_gate() headers = {
"Accept-Ranges": "bytes",
}
return Response(headers=headers)
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
@ -63,15 +115,6 @@ async def proxy(path: str, request: Request):
params=request.query_params, params=request.query_params,
content=await request.body() if request.method in ["POST", "PUT"] else None, content=await request.body() if request.method in ["POST", "PUT"] else None,
) )
if path == "savesetting":
# 使用BackgroundTask在响应发送完毕后执行逻辑
background_task = BackgroundTask(reset_gate)
return Response(
content=response.content,
status_code=response.status_code,
headers=dict(response.headers),
background=background_task,
)
return Response( return Response(
content=response.content, content=response.content,
status_code=response.status_code, status_code=response.status_code,

View File

@ -1,6 +1,8 @@
import asyncio import asyncio
import json import json
import mimetypes
import os import os
import re
import secrets import secrets
import shutil import shutil
import tempfile import tempfile
@ -8,12 +10,14 @@ from contextlib import asynccontextmanager
from dataclasses import asdict from dataclasses import asdict
from typing import Annotated from typing import Annotated
import aiofiles
from fastapi import Depends, FastAPI, HTTPException, Request, status from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.responses import StreamingResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
from starlette.background import BackgroundTask from starlette.background import BackgroundTask
from starlette.responses import FileResponse from starlette.responses import FileResponse, Response
from xiaomusic import __version__ from xiaomusic import __version__
from xiaomusic.utils import ( from xiaomusic.utils import (
@ -79,14 +83,6 @@ def reset_http_server():
else: else:
app.dependency_overrides = {} app.dependency_overrides = {}
# 更新 music 链接
app.router.routes = [route for route in app.router.routes if route.path != "/music"]
app.mount(
"/music",
StaticFiles(directory=config.music_path, follow_symlink=True),
name="music",
)
def HttpInit(_xiaomusic): def HttpInit(_xiaomusic):
global xiaomusic, config, log global xiaomusic, config, log
@ -170,9 +166,12 @@ async def do_cmd(data: DidCmd):
return {"ret": "Did not exist"} return {"ret": "Did not exist"}
if len(cmd) > 0: if len(cmd) > 0:
await xiaomusic.cancel_all_tasks() try:
task = asyncio.create_task(xiaomusic.do_check_cmd(did=did, query=cmd)) await xiaomusic.cancel_all_tasks()
xiaomusic.append_running_task(task) task = asyncio.create_task(xiaomusic.do_check_cmd(did=did, query=cmd))
xiaomusic.append_running_task(task)
except Exception as e:
log.warning(f"Execption {e}")
return {"ret": "OK"} return {"ret": "OK"}
return {"ret": "Unknow cmd"} return {"ret": "Unknow cmd"}
@ -295,3 +294,64 @@ async def debug_play_by_music_url(request: Request):
return await xiaomusic.debug_play_by_music_url(arg1=data_dict) return await xiaomusic.debug_play_by_music_url(arg1=data_dict)
except json.JSONDecodeError as err: except json.JSONDecodeError as err:
raise HTTPException(status_code=400, detail="Invalid JSON") from err raise HTTPException(status_code=400, detail="Invalid JSON") from err
async def file_iterator(file_path, start, end):
async with aiofiles.open(file_path, mode="rb") as file:
await file.seek(start)
chunk_size = 1024
while start <= end:
read_size = min(chunk_size, end - start + 1)
data = await file.read(read_size)
if not data:
break
start += len(data)
yield data
range_pattern = re.compile(r"bytes=(\d+)-(\d*)")
@app.get("/music/{file_path:path}")
async def music_file(request: Request, file_path: str):
absolute_path = os.path.abspath(config.music_path)
absolute_file_path = os.path.join(absolute_path, file_path)
if not os.path.exists(absolute_file_path):
raise HTTPException(status_code=404, detail="File not found")
file_size = os.path.getsize(absolute_file_path)
range_start, range_end = 0, file_size - 1
range_header = request.headers.get("Range")
log.info(f"music_file range_header {range_header}")
if range_header:
range_match = range_pattern.match(range_header)
if range_match:
range_start = int(range_match.group(1))
if range_match.group(2):
range_end = int(range_match.group(2))
log.info(f"music_file in range {absolute_file_path}")
log.info(f"music_file {range_start} {range_end} {absolute_file_path}")
headers = {
"Content-Range": f"bytes {range_start}-{range_end}/{file_size}",
"Accept-Ranges": "bytes",
}
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
mime_type = "application/octet-stream"
return StreamingResponse(
file_iterator(absolute_file_path, range_start, range_end),
headers=headers,
status_code=206 if range_header else 200,
media_type=mime_type,
)
@app.options("/music/{file_path:path}")
async def music_options():
headers = {
"Accept-Ranges": "bytes",
}
return Response(headers=headers)

View File

@ -932,7 +932,7 @@ class XiaoMusicDevice:
self.log.info(f"播放 {name} 失败") self.log.info(f"播放 {name} 失败")
await asyncio.sleep(1) await asyncio.sleep(1)
if self.isplaying() and self._last_cmd != "stop": if self.isplaying() and self._last_cmd != "stop":
await self._play_next() await self._play_next()
return return
self.log.info(f"{name}】已经开始播放了") self.log.info(f"{name}】已经开始播放了")