Files
lyroc/backend/modules/routes.py
2025-05-27 14:16:48 +08:00

206 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
路由模块
包含所有API端点的定义和处理逻辑
"""
import asyncio
import time
import logging
from fastapi import WebSocket, WebSocketDisconnect
from pydantic import BaseModel
from .apple_music import get_music_status, control_music
from .lyrics import get_lyrics_data, delete_lyrics, get_lyrics_from_netease, get_lyrics_from_id, save_lyrics, clear_lyrics_cache
from .websocket_manager import ConnectionManager
logger = logging.getLogger(__name__)
# 音乐控制请求模型
class ControlRequest(BaseModel):
action: str # 'playpause', 'previous', 'next', 'seek'
position: float = None # 仅当 action=='seek' 时有效
# 手动搜索歌词请求模型
class SearchLyricsRequest(BaseModel):
track_name: str
artist: str
# 获取指定ID的歌词请求模型
class GetLyricsFromIdRequest(BaseModel):
id: int
track_name: str
artist: str
def register_routes(app, manager: ConnectionManager):
"""注册所有API路由到FastAPI应用"""
# 保留向下兼容的HTTP接口
@app.get("/status")
def status_endpoint():
"""获取 Apple Music 播放状态"""
return get_music_status()
@app.get("/lyrics")
def lyrics_endpoint():
"""获取当前歌词"""
status = get_music_status()
return get_lyrics_data(status)
@app.delete("/lyrics")
def delete_lyrics_endpoint(track_name: str, artist: str):
"""删除指定歌曲的歌词"""
success = delete_lyrics(track_name, artist)
if success:
return {"status": "success", "message": "歌词已删除"}
else:
return {"status": "error", "message": "未找到要删除的歌词"}
@app.post("/lyrics/search")
def search_lyrics_endpoint(request: SearchLyricsRequest):
"""手动搜索歌词"""
try:
# 搜索歌词
result = get_lyrics_from_netease(request.track_name, request.artist)
if result['code'] == 200 and result['result']['songCount'] > 0:
songs = result['result']['songs']
return {
"status": "success",
"songs": songs
}
else:
return {
"status": "error",
"message": "未找到歌词"
}
except Exception as e:
logger.error(f"搜索歌词时出错: {e}", exc_info=True)
return {
"status": "error",
"message": f"搜索歌词时出错: {str(e)}"
}
@app.post("/lyrics/getLyricsFromId")
def get_lyrics_from_id_endpoint(request: GetLyricsFromIdRequest):
"""获取指定ID的歌词"""
result = get_lyrics_from_id(request.id)
if result['code'] == 200 and 'lrc' in result:
logger.debug(f"网易云音乐歌词: {result['lrc']['lyric']}")
lyrics_text = result['lrc']['lyric']
# 保存到数据库
save_lyrics(request.track_name, request.artist, lyrics_text)
clear_lyrics_cache()
return {
"status": "success",
}
# 添加播放控制接口
@app.post("/control")
async def control_endpoint(request: ControlRequest):
"""控制Apple Music播放"""
result = control_music(request.action, request.position)
# 如果控制成功广播更新的状态给所有WebSocket客户端
if result["status"] == "success":
# 延迟一下让Apple Music有时间执行命令
await asyncio.sleep(0.1)
status = get_music_status()
lyrics = get_lyrics_data(status)
combined_data = {
"type": "position_update", # 添加消息类型
"timestamp": time.time(),
"status": status,
"lyrics": lyrics
}
# 异步广播更新
asyncio.create_task(manager.broadcast(combined_data))
return result
# WebSocket端点
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket连接处理合并推送歌词和状态"""
await manager.connect(websocket)
try:
# 连接建立后立即发送一次数据
status = get_music_status()
lyrics = get_lyrics_data(status)
logger.info(f"发送初始状态数据: 当前歌曲={status.get('track_name', '')}, 歌词可用={lyrics is not None}")
combined_data = {
"type": "track_change", # 确保添加消息类型
"timestamp": time.time(),
"status": status,
"lyrics": lyrics
}
await websocket.send_json(combined_data)
# 发送欢迎消息
await websocket.send_json({"type": "info", "message": "WebSocket连接已建立"})
# 连接状态标志,用于安全终止循环
is_connected = True
# 持续接收并处理客户端消息
while is_connected:
try:
# 接收客户端消息,设置超时避免无限阻塞
data = await asyncio.wait_for(websocket.receive_json(), timeout=1.0)
logger.info(f"收到WebSocket消息: {data}")
# 处理不同类型的消息
if isinstance(data, dict):
# 处理请求状态消息
if data.get("type") == "request_status":
logger.info("客户端请求状态更新")
status = get_music_status()
lyrics = get_lyrics_data(status)
update_data = {
"type": "track_change", # 确保添加消息类型
"timestamp": time.time(),
"status": status,
"lyrics": lyrics
}
await websocket.send_json(update_data)
# 处理ping消息
elif data.get("type") == "ping":
logger.debug("收到客户端ping")
await websocket.send_json({
"type": "pong",
"timestamp": time.time()
})
except WebSocketDisconnect:
# 连接已断开,退出循环
logger.info("WebSocket连接断开")
is_connected = False
break
except asyncio.TimeoutError:
# 接收超时,但连接可能仍然有效,继续循环
continue
except Exception as e:
# 其他错误,记录后继续(除非是连接断开)
if "disconnect" in str(e).lower() or "not connected" in str(e).lower():
logger.info(f"WebSocket连接可能已断开: {e}")
is_connected = False
break
else:
logger.error(f"处理WebSocket消息时出错: {e}")
# 继续监听下一条消息,不断开连接
continue
except WebSocketDisconnect:
logger.info("WebSocket连接断开 (外层异常)")
except Exception as e:
logger.error(f"WebSocket错误: {e}")
finally:
# 确保连接从管理器中移除
await manager.disconnect(websocket)
logger.info("WebSocket连接已清理")