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

Compare commits

...

17 Commits

Author SHA1 Message Date
涵曦
a5f2fc195e bump: version 0.3.22 → 0.3.23 2024-08-01 16:00:06 +00:00
涵曦
393dbabf4b build: update static version 2024-08-01 16:00:05 +00:00
涵曦
444e697f9d fix: 修复部分文件获取不到播放时长问题 2024-08-01 15:52:20 +00:00
涵曦
cf01039b53 fix: 处理安全问题 2024-08-01 00:46:18 +00:00
涵曦
a8fefc6f82 bump: version 0.3.21 → 0.3.22 2024-08-01 00:23:17 +00:00
涵曦
ae0b6066d9 build: update static version 2024-08-01 00:23:17 +00:00
涵曦
53f5e7db8c feat: 网关模式支持配置,默认关闭 2024-08-01 00:22:48 +00:00
涵曦
2a1fa9f8cf fix: 继续优化延迟问题 2024-08-01 00:05:19 +00:00
涵曦
6e2d674758 bump: version 0.3.20 → 0.3.21 2024-07-30 22:55:35 +00:00
涵曦
3bb6573ec0 build: update static version 2024-07-30 22:55:34 +00:00
涵曦
4ae3774a0e fix: 使用前置网关处理静态文件来加速,尝试解决延迟的问题 2024-07-30 11:49:29 +00:00
涵曦
b34215560c feat: 尝试加个网关在前面处理静态文件来加速文件获取 2024-07-30 11:36:51 +00:00
涵曦
4426017ba8 fix: 播放前先立即暂停之前的音乐 2024-07-30 11:36:35 +00:00
涵曦
f1635f8e32 build: 限定python版本必须是3.10和3.11 2024-07-30 06:41:52 +00:00
涵曦
4de032e193 bump: version 0.3.19 → 0.3.20 2024-07-30 06:09:27 +00:00
涵曦
d655157e1d build: update static version 2024-07-30 06:09:26 +00:00
涵曦
ff06958ab3 fix: 尝试修复延迟问题,修复播放停止不了的问题 2024-07-30 06:09:18 +00:00
15 changed files with 404 additions and 71 deletions

View File

@@ -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
View File

@@ -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"},
]

View File

@@ -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"}

View 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))

View File

@@ -1 +1 @@
__version__ = "0.3.19"
__version__ = "0.3.23"

View File

@@ -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,12 +134,58 @@ def main():
},
},
}
uvicorn.run(
HttpApp,
host=["::", "0.0.0.0"],
port=config.port,
log_config=LOGGING_CONFIG,
)
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=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__":

View File

@@ -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
View 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),
)

View File

@@ -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)

View File

@@ -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();

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
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

View File

@@ -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