xtc-backend / app /api /music.py
a3216's picture
sync from GitHub 3bd068e: fix(music): 修复指纹污染+试听HTTPS+IP防护+音频增强
5fb8c4d verified
Raw
History Blame Contribute Delete
4.27 kB
"""听歌识曲相关路由 /v1/music/*。
所有接口复用 require_access_key 鉴权,与现有 /v1/xtc/* 接口安全级别一致。
响应格式统一 { ok: bool, ... },错误走全局 HttpError handler。
"""
from __future__ import annotations
import logging
from typing import Optional
from fastapi import APIRouter, Depends, File, Query, Request, UploadFile
from fastapi.responses import JSONResponse
from ..auth import require_access_key
from ..errors import HttpError
from ..services import music_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/v1/music", tags=["music"])
@router.post("/recognize")
async def recognize(
request: Request,
file: UploadFile = File(..., description="AMR 录音文件"),
duration: int = Query(8, ge=3, le=15, description="录音时长(秒)"),
_key: str = Depends(require_access_key),
):
"""听歌识曲:上传 AMR 录音,返回识别结果列表。
链路:AMR → ffmpeg 转 PCM → node wasm 生成指纹 → 网易云 audio_match。
每次请求都会归档录音与诊断信息到 music_record 表(admin 面板可回放)。
"""
amr_bytes = await file.read()
if not amr_bytes:
raise HttpError("empty file", status=400, code="bad_request", hint="录音文件为空")
# 限制上传大小 2MB(8 秒 AMR 约 50KB,留充足余量)
if len(amr_bytes) > 2 * 1024 * 1024:
raise HttpError("file too large", status=413, code="bad_request", hint="录音文件超过 2MB 限制")
# 提取设备 ID:优先 header x-xtc-device-id,其次 query 参数 device_id
device_id = (
request.headers.get("x-xtc-device-id")
or request.headers.get("x-device-id")
or request.query_params.get("device_id")
or ""
)
# 实验性音频增强:客户端通过 x-xtc-enhance: 1 开启
enhance = request.headers.get("x-xtc-enhance") == "1"
try:
result = await music_service.recognize(
amr_bytes, duration=duration,
enhance=enhance,
client_ip=request.client.host if request.client else "",
device_id=device_id,
access_key=_key,
user_agent=request.headers.get("user-agent", ""),
)
return JSONResponse(result)
except HttpError:
raise
except Exception as e:
logger.exception("[music/recognize] unexpected error")
raise HttpError(
f"recognize failed: {e}", status=500, code="internal_error",
hint="识曲失败,请稍后重试",
)
@router.get("/song/url")
async def song_url(
id: int = Query(..., description="歌曲 ID"),
level: str = Query("standard", description="音质: standard/exhigh/lossless"),
unblock: bool = Query(False, description="无 URL 时尝试换源(酷我)"),
name: str = Query("", description="歌曲名(换源搜索用)"),
artist: str = Query("", description="歌手名(换源搜索用)"),
_key: str = Depends(require_access_key),
):
"""获取歌曲播放 URL。"""
try:
result = await music_service.get_song_url(
id, level=level, unblock=unblock, song_name=name, artist=artist,
)
return JSONResponse(result)
except HttpError:
raise
except Exception as e:
logger.exception("[music/song/url] unexpected error")
raise HttpError(
f"get song url failed: {e}", status=500, code="internal_error",
)
@router.get("/song/detail")
async def song_detail(
ids: str = Query(..., description="歌曲 ID 列表,逗号分隔"),
_key: str = Depends(require_access_key),
):
"""批量获取歌曲详情。"""
try:
id_list = [int(x.strip()) for x in ids.split(",") if x.strip()][:50]
except ValueError:
raise HttpError("invalid ids format", status=400, code="bad_request", hint="ids 应为逗号分隔的数字")
try:
result = await music_service.get_song_details(id_list)
return JSONResponse(result)
except HttpError:
raise
except Exception as e:
logger.exception("[music/song/detail] unexpected error")
raise HttpError(
f"get song detail failed: {e}", status=500, code="internal_error",
)