From 2a1fa9f8cfb913d574fe2a75065727ddc18e6184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B6=B5=E6=9B=A6?= Date: Thu, 1 Aug 2024 00:05:19 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=A7=E7=BB=AD=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- xiaomusic/cli.py | 45 ++++++++++++--------- xiaomusic/config.py | 1 + xiaomusic/gate.py | 87 ++++++++++++++++++++++++++++++----------- xiaomusic/httpserver.py | 84 +++++++++++++++++++++++++++++++++------ xiaomusic/xiaomusic.py | 2 +- 5 files changed, 166 insertions(+), 53 deletions(-) diff --git a/xiaomusic/cli.py b/xiaomusic/cli.py index 14887a3..e3d1639 100644 --- a/xiaomusic/cli.py +++ b/xiaomusic/cli.py @@ -143,39 +143,48 @@ def main(): except Exception as e: print(f"Execption {e}") - def run_server(): + def run_server(port): xiaomusic = XiaoMusic(config) HttpInit(xiaomusic) uvicorn.run( HttpApp, - host="127.0.0.1", - port=config.port + 1, + host=["0.0.0.0", "::"], + port=port, log_config=LOGGING_CONFIG, ) - command = [ - "uvicorn", - "xiaomusic.gate:app", - "--workers", - "4", - "--host", - "0.0.0.0", - "--port", - str(config.port), - ] + process = None + + def run_gate(): + command = [ + "uvicorn", + "xiaomusic.gate:app", + "--workers", + "4", + "--host", + "0.0.0.0", + "--port", + str(config.port), + ] + global process + process = subprocess.Popen(command) - process = subprocess.Popen(command) def signal_handler(sig, frame): print("主进程收到退出信号,准备退出...") - process.terminate() # 终止子进程 - process.wait() # 等待子进程退出 - print("子进程已退出") + if process is not None: + process.terminate() # 终止子进程 + process.wait() # 等待子进程退出 + print("子进程已退出") os._exit(0) # 退出主进程 # 捕获主进程的退出信号 signal.signal(signal.SIGINT, 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__": diff --git a/xiaomusic/config.py b/xiaomusic/config.py index 126278b..b3a4d83 100644 --- a/xiaomusic/config.py +++ b/xiaomusic/config.py @@ -137,6 +137,7 @@ class Config: os.getenv("XIAOMUSIC_REMOVE_ID3TAG", "false").lower() == "true" ) 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): for key in keys.split(","): diff --git a/xiaomusic/gate.py b/xiaomusic/gate.py index bf487b0..8228def 100644 --- a/xiaomusic/gate.py +++ b/xiaomusic/gate.py @@ -1,13 +1,15 @@ import json import logging +import mimetypes import os +import re from contextlib import asynccontextmanager +import aiofiles import httpx -from fastapi import FastAPI, Request -from fastapi.responses import Response +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import Response, StreamingResponse from fastapi.staticfiles import StaticFiles -from starlette.background import BackgroundTask from xiaomusic import __version__ from xiaomusic.config import Config @@ -36,19 +38,69 @@ app = FastAPI( ) -def reset_gate(): - # 更新 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", +folder = os.path.dirname(__file__) +app.mount("/static", StaticFiles(directory=f"{folder}/static"), name="static") + + +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, ) -folder = os.path.dirname(__file__) -app.mount("/static", StaticFiles(directory=f"{folder}/static"), name="static") -reset_gate() +@app.options("/music/{file_path:path}") +async def music_options(): + headers = { + "Accept-Ranges": "bytes", + } + return Response(headers=headers) @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, 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( content=response.content, status_code=response.status_code, diff --git a/xiaomusic/httpserver.py b/xiaomusic/httpserver.py index faf92c6..0383bc6 100644 --- a/xiaomusic/httpserver.py +++ b/xiaomusic/httpserver.py @@ -1,6 +1,8 @@ import asyncio import json +import mimetypes import os +import re import secrets import shutil import tempfile @@ -8,12 +10,14 @@ from contextlib import asynccontextmanager from dataclasses import asdict from typing import Annotated +import aiofiles from fastapi import Depends, FastAPI, HTTPException, Request, status +from fastapi.responses import StreamingResponse from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from starlette.background import BackgroundTask -from starlette.responses import FileResponse +from starlette.responses import FileResponse, Response from xiaomusic import __version__ from xiaomusic.utils import ( @@ -79,14 +83,6 @@ def reset_http_server(): else: 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): global xiaomusic, config, log @@ -170,9 +166,12 @@ async def do_cmd(data: DidCmd): return {"ret": "Did not exist"} if len(cmd) > 0: - await xiaomusic.cancel_all_tasks() - task = asyncio.create_task(xiaomusic.do_check_cmd(did=did, query=cmd)) - xiaomusic.append_running_task(task) + try: + await xiaomusic.cancel_all_tasks() + 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": "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) except json.JSONDecodeError as 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) diff --git a/xiaomusic/xiaomusic.py b/xiaomusic/xiaomusic.py index ef0f8cc..94b7f39 100644 --- a/xiaomusic/xiaomusic.py +++ b/xiaomusic/xiaomusic.py @@ -932,7 +932,7 @@ class XiaoMusicDevice: self.log.info(f"播放 {name} 失败") await asyncio.sleep(1) if self.isplaying() and self._last_cmd != "stop": - await self._play_next() + await self._play_next() return self.log.info(f"【{name}】已经开始播放了")