# -*- coding: utf-8 -*- """HTTP 客户端与响应体收窄工具。 (自 music_api.py 拆出,行为与原实现一致。) """ import asyncio from typing import Any from core.http_client import ( HTTPResult, UpstreamError, UpstreamHTTPError, UpstreamTooLarge, ) from core.http_client import client as _http 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 风控)不重试——立即重试会加剧风控。 走 core.http_client.request_once(单次、不重试):重试策略由本函数自管, 非 2xx 由 request_once 抛 UpstreamHTTPError,按 .status 分流—— 与原先「垫片只在 raise_for_status() 时才抛 HTTPError」的分流等价。 """ 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 _http.request_once( 'POST', url, headers=headers, cookies=request_cookies, data={"params": params}, timeout=30) return response.text except UpstreamHTTPError as e: if e.status >= 500 and attempt == 0: await asyncio.sleep(1.0) continue # 4xx(429/460 风控)不重试——原实现 HTTPError 落入 # 下方异常分支被重试一次,与意图相反 raise APIException(f"HTTP请求失败: {e}") from e except UpstreamTooLarge as e: # 响应体超限是确定性失败,重试无意义 raise APIException("上游响应体异常(超过大小上限)") from e except UpstreamError 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]) -> HTTPResult: """发送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 _http.request_once( 'POST', url, headers=headers, cookies=request_cookies, data={"params": params}, timeout=30) return response except UpstreamHTTPError as e: if e.status >= 500 and attempt == 0: await asyncio.sleep(1.0) continue # 4xx 不重试(同上) raise APIException(f"HTTP请求失败: {e}") from e except UpstreamTooLarge as e: raise APIException("上游响应体异常(超过大小上限)") from e except UpstreamError as e: last_err = e if attempt == 0: await asyncio.sleep(1.0) continue raise APIException(f"HTTP请求失败: {last_err}")