mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-07 15:02:55 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5f2fc195e | ||
|
|
393dbabf4b | ||
|
|
444e697f9d | ||
|
|
cf01039b53 | ||
|
|
a8fefc6f82 | ||
|
|
ae0b6066d9 | ||
|
|
53f5e7db8c | ||
|
|
2a1fa9f8cf | ||
|
|
6e2d674758 | ||
|
|
3bb6573ec0 | ||
|
|
4ae3774a0e | ||
|
|
b34215560c | ||
|
|
4426017ba8 | ||
|
|
f1635f8e32 | ||
|
|
4de032e193 | ||
|
|
d655157e1d | ||
|
|
ff06958ab3 |
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,3 +1,37 @@
|
||||
## v0.3.23 (2024-08-01)
|
||||
|
||||
### Fix
|
||||
|
||||
- 修复部分文件获取不到播放时长问题
|
||||
- 处理安全问题
|
||||
|
||||
## v0.3.22 (2024-08-01)
|
||||
|
||||
### Feat
|
||||
|
||||
- 网关模式支持配置,默认关闭
|
||||
|
||||
### Fix
|
||||
|
||||
- 继续优化延迟问题
|
||||
|
||||
## v0.3.21 (2024-07-30)
|
||||
|
||||
### Feat
|
||||
|
||||
- 尝试加个网关在前面处理静态文件来加速文件获取
|
||||
|
||||
### Fix
|
||||
|
||||
- 使用前置网关处理静态文件来加速,尝试解决延迟的问题
|
||||
- 播放前先立即暂停之前的音乐
|
||||
|
||||
## v0.3.20 (2024-07-30)
|
||||
|
||||
### Fix
|
||||
|
||||
- 尝试修复延迟问题,修复播放停止不了的问题
|
||||
|
||||
## v0.3.19 (2024-07-30)
|
||||
|
||||
### Fix
|
||||
|
||||
47
pdm.lock
generated
47
pdm.lock
generated
@@ -5,7 +5,7 @@
|
||||
groups = ["default", "dev", "lint"]
|
||||
strategy = ["inherit_metadata"]
|
||||
lock_version = "4.5.0"
|
||||
content_hash = "sha256:662037dc1982f7f3592bd691dffc2ffd7b7d7f7b464fa74f716403a03e3a7725"
|
||||
content_hash = "sha256:0a0b1f63fdd9dd2c4ca2a777f12d294126a880631c1b3d48108d1df283ba14a8"
|
||||
|
||||
[[metadata.targets]]
|
||||
requires_python = "==3.10.12"
|
||||
@@ -25,14 +25,27 @@ files = [
|
||||
{file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.3.4"
|
||||
requires_python = "<4.0,>=3.8"
|
||||
summary = "Happy Eyeballs for asyncio"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
files = [
|
||||
{file = "aiohappyeyeballs-2.3.4-py3-none-any.whl", hash = "sha256:40a16ceffcf1fc9e142fd488123b2e218abc4188cf12ac20c67200e1579baa42"},
|
||||
{file = "aiohappyeyeballs-2.3.4.tar.gz", hash = "sha256:7e1ae8399c320a8adec76f6c919ed5ceae6edd4c3672f4d9eae2b27e37c80ff6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.9.5"
|
||||
version = "3.10.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Async http client/server framework (asyncio)"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
dependencies = [
|
||||
"aiohappyeyeballs>=2.3.0",
|
||||
"aiosignal>=1.1.2",
|
||||
"async-timeout<5.0,>=4.0; python_version < \"3.11\"",
|
||||
"attrs>=17.3.0",
|
||||
@@ -41,8 +54,8 @@ dependencies = [
|
||||
"yarl<2.0,>=1.0",
|
||||
]
|
||||
files = [
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
|
||||
{file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
|
||||
{file = "aiohttp-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06ef0135d7ab7fb0284342fbbf8e8ddf73b7fee8ecc55f5c3a3d0a6b765e6d8b"},
|
||||
{file = "aiohttp-3.10.0.tar.gz", hash = "sha256:e8dd7da2609303e3574c95b0ec9f1fd49647ef29b94701a2862cceae76382e1d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -976,14 +989,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
requires_python = ">=3.7"
|
||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
groups = ["lint"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
files = [
|
||||
{file = "ruff-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"},
|
||||
{file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"},
|
||||
{file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1094,7 +1107,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.30.3"
|
||||
version = "0.30.4"
|
||||
requires_python = ">=3.8"
|
||||
summary = "The lightning-fast ASGI server."
|
||||
groups = ["default"]
|
||||
@@ -1105,13 +1118,13 @@ dependencies = [
|
||||
"typing-extensions>=4.0; python_version < \"3.11\"",
|
||||
]
|
||||
files = [
|
||||
{file = "uvicorn-0.30.3-py3-none-any.whl", hash = "sha256:94a3608da0e530cea8f69683aa4126364ac18e3826b6630d1a65f4638aade503"},
|
||||
{file = "uvicorn-0.30.3.tar.gz", hash = "sha256:0d114d0831ff1adbf231d358cbf42f17333413042552a624ea6a9b4c33dcfd81"},
|
||||
{file = "uvicorn-0.30.4-py3-none-any.whl", hash = "sha256:06b00e3087e58c6865c284143c0c42f810b32ff4f265ab19d08c566f74a08728"},
|
||||
{file = "uvicorn-0.30.4.tar.gz", hash = "sha256:00db9a9e3711a5fa59866e2b02fac69d8dc70ce0814aaec9a66d1d9e5c832a30"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.30.3"
|
||||
version = "0.30.4"
|
||||
extras = ["standard"]
|
||||
requires_python = ">=3.8"
|
||||
summary = "The lightning-fast ASGI server."
|
||||
@@ -1122,14 +1135,14 @@ dependencies = [
|
||||
"httptools>=0.5.0",
|
||||
"python-dotenv>=0.13",
|
||||
"pyyaml>=5.1",
|
||||
"uvicorn==0.30.3",
|
||||
"uvicorn==0.30.4",
|
||||
"uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"",
|
||||
"watchfiles>=0.13",
|
||||
"websockets>=10.4",
|
||||
]
|
||||
files = [
|
||||
{file = "uvicorn-0.30.3-py3-none-any.whl", hash = "sha256:94a3608da0e530cea8f69683aa4126364ac18e3826b6630d1a65f4638aade503"},
|
||||
{file = "uvicorn-0.30.3.tar.gz", hash = "sha256:0d114d0831ff1adbf231d358cbf42f17333413042552a624ea6a9b4c33dcfd81"},
|
||||
{file = "uvicorn-0.30.4-py3-none-any.whl", hash = "sha256:06b00e3087e58c6865c284143c0c42f810b32ff4f265ab19d08c566f74a08728"},
|
||||
{file = "uvicorn-0.30.4.tar.gz", hash = "sha256:00db9a9e3711a5fa59866e2b02fac69d8dc70ce0814aaec9a66d1d9e5c832a30"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1345,7 +1358,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "yt-dlp"
|
||||
version = "2024.7.25"
|
||||
version = "2024.8.1"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A feature-rich command-line audio/video downloader"
|
||||
groups = ["default"]
|
||||
@@ -1361,6 +1374,6 @@ dependencies = [
|
||||
"websockets>=12.0",
|
||||
]
|
||||
files = [
|
||||
{file = "yt_dlp-2024.7.25-py3-none-any.whl", hash = "sha256:f44b5f33776b4f718900c670fe6e4698fb6fcd426455cd837cf25a1d6d4d9560"},
|
||||
{file = "yt_dlp-2024.7.25.tar.gz", hash = "sha256:7587aa25e236cf7b14bdb9378bbffff51202d901b04202be0cf62cbb56d3b52c"},
|
||||
{file = "yt_dlp-2024.8.1-py3-none-any.whl", hash = "sha256:d0d927038e30a05f6eab26ff6189628456ea21bb159a3d9dc2e855eef2810eac"},
|
||||
{file = "yt_dlp-2024.8.1.tar.gz", hash = "sha256:4318aa523694611562f01419c8d526b662a72df34ef8ba454016b34c8366c158"},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.3.19"
|
||||
version = "0.3.23"
|
||||
description = "Play Music with xiaomi AI speaker"
|
||||
authors = [
|
||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||
@@ -15,7 +15,7 @@ dependencies = [
|
||||
"starlette>=0.37.2",
|
||||
"aiofiles>=24.1.0",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.10,<3.12"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
|
||||
32
test/test_music_duration.py
Normal file
32
test/test_music_duration.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import math
|
||||
|
||||
from xiaomusic.const import (
|
||||
SUPPORT_MUSIC_TYPE,
|
||||
)
|
||||
from xiaomusic.utils import (
|
||||
get_local_music_duration,
|
||||
traverse_music_directory,
|
||||
)
|
||||
|
||||
|
||||
async def test_one_music(filename):
|
||||
# 获取播放时长
|
||||
duration = await get_local_music_duration(filename)
|
||||
sec = math.ceil(duration)
|
||||
print(f"本地歌曲 : {filename} 的时长 {duration} {sec} 秒")
|
||||
|
||||
|
||||
async def main(directory):
|
||||
# 获取所有歌曲文件
|
||||
local_musics = traverse_music_directory(directory, 10, [], SUPPORT_MUSIC_TYPE)
|
||||
print(local_musics)
|
||||
for _, files in local_musics.items():
|
||||
for file in files:
|
||||
await test_one_music(file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
directory = "./music" # 替换为你的音乐目录路径
|
||||
asyncio.run(main(directory))
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.3.19"
|
||||
__version__ = "0.3.23"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
import uvicorn
|
||||
|
||||
@@ -75,9 +79,6 @@ def main():
|
||||
options = parser.parse_args()
|
||||
config = Config.from_options(options)
|
||||
|
||||
xiaomusic = XiaoMusic(config)
|
||||
HttpInit(xiaomusic)
|
||||
|
||||
LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
@@ -133,13 +134,59 @@ def main():
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
filename = config.getsettingfile()
|
||||
with open(filename) as f:
|
||||
data = json.loads(f.read())
|
||||
config.update_config(data)
|
||||
except Exception as e:
|
||||
print(f"Execption {e}")
|
||||
|
||||
def run_server(port):
|
||||
xiaomusic = XiaoMusic(config)
|
||||
HttpInit(xiaomusic)
|
||||
uvicorn.run(
|
||||
HttpApp,
|
||||
host=["::", "0.0.0.0"],
|
||||
port=config.port,
|
||||
host=["0.0.0.0", "::"],
|
||||
port=port,
|
||||
log_config=LOGGING_CONFIG,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
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)
|
||||
port = int(config.port)
|
||||
if config.enable_gate:
|
||||
run_gate()
|
||||
run_server(port + 1)
|
||||
else:
|
||||
run_server(port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -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", "false").lower() == "true"
|
||||
|
||||
def append_keyword(self, keys, action):
|
||||
for key in keys.split(","):
|
||||
|
||||
124
xiaomusic/gate.py
Normal file
124
xiaomusic/gate.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import aiofiles
|
||||
import httpx
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.responses import Response, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from xiaomusic import __version__
|
||||
from xiaomusic.config import Config
|
||||
|
||||
config = Config()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_lifespan(app):
|
||||
global config
|
||||
try:
|
||||
filename = config.getsettingfile()
|
||||
with open(filename) as f:
|
||||
data = json.loads(f.read())
|
||||
config.update_config(data)
|
||||
except Exception as e:
|
||||
log.exception(f"Execption {e}")
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
lifespan=app_lifespan,
|
||||
version=__version__,
|
||||
)
|
||||
|
||||
|
||||
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.normpath(os.path.join(absolute_path, file_path))
|
||||
if not absolute_file_path.startswith(absolute_path):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
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)
|
||||
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def proxy(path: str, request: Request):
|
||||
async with httpx.AsyncClient() as client:
|
||||
port = config.port + 1
|
||||
url = f"http://127.0.0.1:{port}/{path}"
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=url,
|
||||
headers=request.headers,
|
||||
params=request.query_params,
|
||||
content=await request.body() if request.method in ["POST", "PUT"] else None,
|
||||
)
|
||||
return Response(
|
||||
content=response.content,
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers),
|
||||
)
|
||||
@@ -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,7 +166,12 @@ async def do_cmd(data: DidCmd):
|
||||
return {"ret": "Did not exist"}
|
||||
|
||||
if len(cmd) > 0:
|
||||
asyncio.create_task(xiaomusic.do_check_cmd(did=did, query=cmd))
|
||||
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"}
|
||||
|
||||
@@ -293,3 +294,66 @@ 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.normpath(os.path.join(absolute_path, file_path))
|
||||
if not absolute_file_path.startswith(absolute_path):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
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)
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Debug For XiaoMusic</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722304771">
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722528005">
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1722304771"></script>
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1722528005"></script>
|
||||
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1722304771"></script>
|
||||
<script src="/static/app.js?version=1722304771"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722304771">
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1722528005"></script>
|
||||
<script src="/static/app.js?version=1722528005"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722528005">
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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?version=1722304771">
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722528005">
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1722304771"></script>
|
||||
<script src="/static/setting.js?version=1722304771"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722304771">
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1722528005"></script>
|
||||
<script src="/static/setting.js?version=1722528005"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722528005">
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
@@ -127,6 +127,12 @@ var vConsole = new window.VConsole();
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
|
||||
<label for="enable_gate">开启网关(重启生效):</label>
|
||||
<select id="enable_gate">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="public_port">外网访问端口(0表示跟监听端口一致):</label>
|
||||
<input id="public_port" type="number" value="0"></input>
|
||||
|
||||
|
||||
@@ -141,9 +141,7 @@ def _append_files_result(result, root, joinpath, files, support_extension):
|
||||
result[dir_name].append(os.path.join(joinpath, file))
|
||||
|
||||
|
||||
def traverse_music_directory(
|
||||
directory, depth=10, exclude_dirs=None, support_extension=None
|
||||
):
|
||||
def traverse_music_directory(directory, depth, exclude_dirs, support_extension):
|
||||
result = {}
|
||||
for root, dirs, files in os.walk(directory, followlinks=True):
|
||||
# 忽略排除的目录
|
||||
@@ -247,10 +245,9 @@ async def get_local_music_duration(filename):
|
||||
m = await loop.run_in_executor(None, mutagen.mp3.MP3, filename)
|
||||
else:
|
||||
m = await loop.run_in_executor(None, mutagen.File, filename)
|
||||
if m and m.info:
|
||||
duration = m.info.length
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting local music duration: {e}")
|
||||
logging.error(f"Error getting local music {filename} duration: {e}")
|
||||
return duration
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ class XiaoMusic:
|
||||
self._all_radio = {} # 电台列表
|
||||
self.music_list = {} # 播放列表 key 为目录名, value 为 play_list
|
||||
self.devices = {} # key 为 did
|
||||
self.running_task = []
|
||||
|
||||
# 初始化配置
|
||||
self.init_config()
|
||||
@@ -309,10 +310,10 @@ class XiaoMusic:
|
||||
|
||||
def get_filename(self, name):
|
||||
if name not in self.all_music:
|
||||
self.log.debug(f"get_filename not in. name:{name}")
|
||||
self.log.info(f"get_filename not in. name:{name}")
|
||||
return ""
|
||||
filename = self.all_music[name]
|
||||
self.log.debug(f"try get_filename. filename:{filename}")
|
||||
self.log.info(f"try get_filename. filename:{filename}")
|
||||
if os.path.exists(filename):
|
||||
return filename
|
||||
return ""
|
||||
@@ -343,6 +344,7 @@ class XiaoMusic:
|
||||
async def get_music_sec_url(self, name):
|
||||
sec = 0
|
||||
url = self.get_music_url(name)
|
||||
self.log.info(f"get_music_sec_url. name:{name} url:{url}")
|
||||
if self.is_web_radio_music(name):
|
||||
self.log.info("电台不会有播放时长")
|
||||
return 0, url
|
||||
@@ -354,6 +356,7 @@ class XiaoMusic:
|
||||
self.log.info(f"网络歌曲 {name} : {origin_url} {url} 的时长 {sec} 秒")
|
||||
else:
|
||||
filename = self.get_filename(name)
|
||||
self.log.info(f"get_music_sec_url. name:{name} filename:{filename}")
|
||||
duration = await get_local_music_duration(filename)
|
||||
sec = math.ceil(duration)
|
||||
self.log.info(f"本地歌曲 {name} : {filename} {url} 的时长 {sec} 秒")
|
||||
@@ -365,7 +368,7 @@ class XiaoMusic:
|
||||
def get_music_url(self, name):
|
||||
if self.is_web_music(name):
|
||||
url = self.all_music[name]
|
||||
self.log.debug(f"get_music_url web music. name:{name}, url:{url}")
|
||||
self.log.info(f"get_music_url web music. name:{name}, url:{url}")
|
||||
return url
|
||||
|
||||
filename = self.get_filename(name)
|
||||
@@ -383,7 +386,7 @@ class XiaoMusic:
|
||||
filename = filename[len(self.config.music_path) :]
|
||||
if filename.startswith("/"):
|
||||
filename = filename[1:]
|
||||
self.log.debug(f"get_music_url local music. name:{name}, filename:{filename}")
|
||||
self.log.info(f"get_music_url local music. name:{name}, filename:{filename}")
|
||||
encoded_name = urllib.parse.quote(filename)
|
||||
return f"http://{self.hostname}:{self.public_port}/music/{encoded_name}"
|
||||
|
||||
@@ -505,6 +508,19 @@ class XiaoMusic:
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
|
||||
def append_running_task(self, task):
|
||||
self.running_task.append(task)
|
||||
|
||||
async def cancel_all_tasks(self):
|
||||
if len(self.running_task) == 0:
|
||||
self.log.info("cancel_all_tasks no task")
|
||||
return
|
||||
for task in self.running_task:
|
||||
self.log.info(f"cancel_all_tasks {task}")
|
||||
task.cancel()
|
||||
await asyncio.gather(*self.running_task, return_exceptions=True)
|
||||
self.running_task = []
|
||||
|
||||
async def check_replay(self, did):
|
||||
return await self.devices[did].check_replay()
|
||||
|
||||
@@ -909,7 +925,7 @@ class XiaoMusicDevice:
|
||||
self.cur_music = name
|
||||
self.log.info(f"cur_music {self.cur_music}")
|
||||
sec, url = await self.xiaomusic.get_music_sec_url(name)
|
||||
# await self.group_force_stop_xiaoai()
|
||||
await self.group_force_stop_xiaoai()
|
||||
self.log.info(f"播放 {url}")
|
||||
results = await self.group_player_play(url)
|
||||
if all(ele is None for ele in results):
|
||||
@@ -1125,21 +1141,19 @@ class XiaoMusicDevice:
|
||||
|
||||
# 设置下一首歌曲的播放定时器
|
||||
async def set_next_music_timeout(self, sec):
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info("旧定时器已取消")
|
||||
|
||||
self.cancel_next_timer()
|
||||
self._timeout = sec
|
||||
|
||||
async def _do_next():
|
||||
await asyncio.sleep(self._timeout)
|
||||
try:
|
||||
self.log.info("定时器时间到了")
|
||||
await self.play_next()
|
||||
self._next_timer = None
|
||||
await self._play_next()
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
self._next_timer = asyncio.ensure_future(_do_next())
|
||||
self._next_timer = asyncio.create_task(_do_next())
|
||||
self.log.info(f"{sec} 秒后将会播放下一首歌曲")
|
||||
|
||||
async def set_volume(self, volume: int):
|
||||
@@ -1179,10 +1193,8 @@ class XiaoMusicDevice:
|
||||
self._playing = False
|
||||
if arg1 != "notts":
|
||||
await self.do_tts(self.config.stop_tts_msg)
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self._next_timer = None
|
||||
self.log.info("定时器已取消")
|
||||
# 取消组内所有的下一首歌曲的定时器
|
||||
self.cancel_group_next_timer()
|
||||
await self.group_force_stop_xiaoai()
|
||||
self.log.info("stop now")
|
||||
|
||||
@@ -1207,13 +1219,14 @@ class XiaoMusicDevice:
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
|
||||
self._stop_timer = asyncio.ensure_future(_do_stop())
|
||||
self._stop_timer = asyncio.create_task(_do_stop())
|
||||
await self.do_tts(f"收到,{minute}分钟后将关机")
|
||||
|
||||
def cancel_next_timer(self):
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info("下一曲定时器已取消")
|
||||
self.log.info(f"下一曲定时器已取消 {self.device_id}")
|
||||
self._next_timer = None
|
||||
|
||||
def cancel_group_next_timer(self):
|
||||
devices = self.xiaomusic.get_group_devices(self.group_name)
|
||||
@@ -1228,10 +1241,12 @@ class XiaoMusicDevice:
|
||||
self.log.info("in cancel_all_timer")
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self._next_timer = None
|
||||
self.log.info("cancel_all_timer _next_timer.cancel")
|
||||
|
||||
if self._stop_timer:
|
||||
self._stop_timer.cancel()
|
||||
self._stop_timer = None
|
||||
self.log.info("cancel_all_timer _stop_timer.cancel")
|
||||
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user