# -*- coding: utf-8 -*- """HTTP 客户端与响应体收窄工具。 (自 music_api.py 拆出,行为与原实现一致。) """ import asyncio from typing import Any from core import aiohttp_compat as requests # 异步 HTTP 垫片(requests 同形 API) from .constants import ( APIConstants, ) from .errors import ( APIException, ) def _ensure_dict(result: Any, msg: str) -> dict[str, Any]: """上游 200 但返回 null/数组等非 dict 体(风控/抖动)时统一前置校验""" if isinstance(result, dict): return result raise APIException(msg) def _as_dict(v: Any) -> dict[str, Any]: """上游嵌套字段类型不稳定(dict / None / 列表都真实出现过):非 dict 一律回落空 dict。 收窄成显式函数,避免满屏 `x if isinstance(x, dict) else {}` 三元式—— 那种写法读起来累,且把字段来自 `user.get(...)`(类型 Any|None)时的收窄责任 散落在各处,容易被后来的改动改漏。 """ return v if isinstance(v, dict) else {} class HTTPClient: """HTTP客户端类""" @staticmethod async def post_request(url: str, params: str, cookies: dict[str, str]) -> str: """发送POST请求并返回文本响应。 网络异常/5xx 做一次 1 秒退避重试(全站 API 调用此前零重试, 上游抖动即整首失败);4xx(429/460 风控)不重试——立即重试会加剧风控。""" headers = { 'User-Agent': APIConstants.USER_AGENT, 'Referer': APIConstants.REFERER, } request_cookies = APIConstants.DEFAULT_COOKIES.copy() request_cookies.update(cookies) last_err = None for attempt in range(2): try: response = await requests.post(url, headers=headers, cookies=request_cookies, data={"params": params}, timeout=30) if response.status_code >= 500 and attempt == 0: await asyncio.sleep(1.0) continue response.raise_for_status() if len(response.content) > _MAX_UPSTREAM_JSON_BYTES: raise APIException("上游响应体异常(超过大小上限)") return response.text except requests.HTTPError as e: # 4xx(429/460 风控)不重试——原实现 HTTPError 落入 # 下方 RequestException 分支被重试一次,与 意图相反 raise APIException(f"HTTP请求失败: {e}") from e except requests.RequestException as e: last_err = e if attempt == 0: await asyncio.sleep(1.0) continue raise APIException(f"HTTP请求失败: {last_err}") @staticmethod async def post_request_full(url: str, params: str, cookies: dict[str, str]) -> requests.Response: """发送POST请求并返回完整响应对象(同 post_request 的有界重试)""" headers = { 'User-Agent': APIConstants.USER_AGENT, 'Referer': APIConstants.REFERER, } request_cookies = APIConstants.DEFAULT_COOKIES.copy() request_cookies.update(cookies) last_err = None for attempt in range(2): try: response = await requests.post(url, headers=headers, cookies=request_cookies, data={"params": params}, timeout=30) if response.status_code >= 500 and attempt == 0: await asyncio.sleep(1.0) continue response.raise_for_status() if len(response.content) > _MAX_UPSTREAM_JSON_BYTES: raise APIException("上游响应体异常(超过大小上限)") return response except requests.HTTPError as e: # 4xx 不重试(同上) raise APIException(f"HTTP请求失败: {e}") from e except requests.RequestException as e: last_err = e if attempt == 0: await asyncio.sleep(1.0) continue raise APIException(f"HTTP请求失败: {last_err}") # 上游 JSON 响应体上限——requests 已全量入内存,此处拦截的是 # 异常/被劫持上游的超大 body 进入后续 .json() 解析与传播(音频/封面已有字节预算) _MAX_UPSTREAM_JSON_BYTES = 8 * 1024 * 1024