feat(routes): 迁移音频流代理与结果交付端点(流式)

- web/routers/stream.py:/api/v1/audio_stream 音频范围流代理
  · 签名校验(core.security,密钥同源两栈互认);HEAD 直接 200 不请求上游
  · Range 原样透传上游(206 + Content-Range,拖动进度可用)
  · 编码透明:上游带 Content-Encoding 时不沿用压缩前长度(同 Flask F92)
  · StreamingResponse 16KB 分块,finally 关闭上游(下游中断亦释放)
- core/http_client.py 增 open():返回未关闭流式响应,便于先读状态码再转发
- web/routers/delivery.py:/download/result、/download/batch_zip/result
  · 归属 403 / 进行中 202 / 暂停 200 / 取消 200 / 失败 500 / 缺失 404 分支逐一对齐
  · FileResponse 流式交付 + no-store + X-Download-Filename;清理由监控线程统一回收

验证:pytest 51/51(新增 5 项流代理:无效 token/HEAD/全量/Range 透传/上游关闭);
ruff All checks passed;pyright 0 errors;奇偶校验 26/26;实测 5.2MB 流与
Range 206 正确。
This commit is contained in:
snowy-yang committed 2026-09-26 09:49:22 +08:00
1 parent f588d313f0
commit 0104ae55e5
5 files changed
+326

No files matched your search

+29
View File
@@ -254,6 +254,35 @@ class AsyncHTTPClient:
except aiohttp.ClientError as e:
raise UpstreamNetworkError(str(e)) from e
async def open(
self,
method: str,
url: str,
*,
headers: Mapping[str, str] | None = None,
cookies: Mapping[str, str] | None = None,
timeout: float | None = None,
allow_redirects: bool = True,
) -> aiohttp.ClientResponse:
"""打开流式响应并返回**未关闭**的 ClientResponse(调用方负责 close)。
与 stream() 的区别:不把响应交给 `async with`,便于「先读状态码/头决定是否
转发,再流式读取」的代理场景(如音频 Range 代理)。
"""
session = await self._session_for_loop()
req_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else None
try:
return await session.request(
method,
url,
headers=dict(headers) if headers else None,
cookies=dict(cookies) if cookies else None,
timeout=req_timeout,
allow_redirects=allow_redirects,
)
except aiohttp.ClientError as e:
raise UpstreamNetworkError(str(e)) from e
# 进程级共享客户端(UA/Referer 由 music_api 在初始化后注入)
client = AsyncHTTPClient()