Archived
refactor: 拆分 4 个巨石模块为包,清理过时注释并删除作废文档
为什么:单文件 1100~1900 行的巨石模块让「改一处要通读全文」;同时注释里残留 大量已删台账的编号,以及「Flask/FastAPI 双栈迁移期」这类已失实的架构描述, 会误导后来者按不复存在的架构去改代码。 拆分(对外导入路径不变,行为零变更): - music_downloader.py -> music_downloader/(models/downloader/cover/_http/_log/_concurrency) - auth_system.py -> auth_system/(db/security/users/credits/ledger/mail/cards/backup...) - music_api.py -> music_api/(api/client/crypto/qr/cookies/compat/constants/errors) - services/runtime.py -> services/(api_service/_shared/_taskcore/downloads/batch/podcast/ streaming/tasks);services/runtime.py 保留为兼容门面 兼容性做法:各包用 __init__.py 以 __all__ 原样再导出全部符号,既有调用点 (auth_system.xxx / services.xxx / from music_api import Y)零改动;路径常量改为经包属性 动态读取(auth_system.DB_PATH),否则测试的 monkeypatch 会静默失效并写到生产库。 修复 9 处代码损伤(上一轮注释清理把 `()` 当「空括号残留」删掉所致;用「与拆分前逐字 等价」校验逐一定位,静态检查只会报出其中 4 处): - core/config.py ).strip() or '*' - core/state.py threading.Lock() x2 - qr_login.py datetime.now() / .strip() - music_downloader/downloader.py .lower() - services/podcast.py set() / time.time() x2 清理与删除: - 删除 部署文档.md / 功能介绍.md / README_私有包警告.txt - 清理指向已删台账与文档的过时编号注释;把「迁移期 / 两栈 / 双框架 / 已迁移」等 失实描述改写为现状描述(保留 `<-> Flask /path` 映射表等仍有价值的契约来源说明) - _mkpkg.py:公开包剔除清单去掉已不存在的文档名;私有包警告改为自包含 - pyproject.toml:补 pythonpath = ["."],使裸跑 pytest 不再 import core 失败 验证:ruff 全通过;pyright 0 errors / 0 warnings(102 文件);pytest 144 passed; 真实 uvicorn 冒烟 6 条路由符合预期且生产 data/ 指纹未变;忽略注释的逐字等价性校验通过。
This commit is contained in:
1 parent
838d635bc5
commit
6288a98ecc
89 files changed
+5806
-5112
No files matched your search
@@ -0,0 +1,112 @@
|
||||
# -*- 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
|
||||
Reference in new issue
Block a user