# -*- coding: utf-8 -*- """音频流代理(FastAPI 栈)——与 Flask /audio_stream 行为逐项对齐。 要点(对齐 Flask 实现): - 签名校验:token 由 core.security 校验(密钥同源,两栈 token 互认);失效 403。 - HEAD 无正文:直接 200 + Accept-Ranges,不请求上游。 - Range 透传:客户端 Range 原样转发上游,206/Content-Range 原样回传(拖动进度可用)。 - 编码透明:上游若带 Content-Encoding(aiohttp 会解压),不得沿用压缩前的 Content-Length/Content-Range(否则头体不一致,同 Flask F92)。 - 流式转发:16KB 分块,不整段入内存;下游中断时 finally 关闭上游响应。 - Cache-Control: no-store。 """ from fastapi import APIRouter, Request from fastapi.responses import Response, StreamingResponse from core import security from core.http_client import client as http_client from web.response import error router = APIRouter() _UPSTREAM_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://music.163.com/', } _CHUNK = 16 * 1024 @router.api_route('/audio_stream', methods=['GET', 'HEAD']) async def audio_stream(request: Request): """签名音频代理流(支持 Range 拖动进度)。""" if request.method == 'HEAD': return Response(status_code=200, headers={'Accept-Ranges': 'bytes'}) token = request.query_params.get('token', '') url = security.verify_audio_token(token) if not url: return error('音频链接已过期或无效,请重新解析', 403) upstream_headers = dict(_UPSTREAM_HEADERS) range_header = request.headers.get('range') if range_header: upstream_headers['Range'] = range_header try: upstream = await http_client.open('GET', url, headers=upstream_headers, timeout=20) except Exception: return error('音频获取失败', 502) if upstream.status >= 400: upstream.close() return error('音频获取失败', 502) upstream_encoded = bool(upstream.headers.get('Content-Encoding')) pass_headers: dict[str, str] = {} if not upstream_encoded: for h in ('Content-Length', 'Content-Range', 'Accept-Ranges'): if h in upstream.headers: pass_headers[h] = upstream.headers[h] elif 'Accept-Ranges' in upstream.headers: pass_headers['Accept-Ranges'] = upstream.headers['Accept-Ranges'] pass_headers['Cache-Control'] = 'no-store' async def _body(): try: async for chunk in upstream.content.iter_chunked(_CHUNK): yield chunk finally: # 下游断开/提前关闭时也释放上游连接 upstream.close() return StreamingResponse( _body(), status_code=upstream.status, media_type=upstream.headers.get('Content-Type', 'audio/mpeg'), headers=pass_headers, )