Archived
为什么:上一次分包重构把 1900 行巨石按职责拆成 9 个子模块后,留了一个 runtime.py 作为「原样再导出」的兼容门面,好让调用点零改动。但迁移期结束后 没有一个调用点迁走——门面成了永久中间层:每次读代码都要先跳一层才知道 符号真正住在哪,pyright 也无法从门面追溯定义位置。 改法:保持「函数内延迟导入」的形式不变,只把导入目标换成真实子模块。 **刻意不提到模块顶部**——services/__init__.py 里有依赖自检 (缺 auth_system/podcast_api/music_api/music_downloader/qr_login 就 sys.exit(1)), 提到顶层会让「import web.*」这一步就触发该自检,改变启动语义,超出本次范围。 7 处调用点(AST 精确扫描确认全集): - web/routers/download.py ×4:download / download_batch_zip / podcast_download / podcast_batch_zip,分别直连 services.downloads / batch / podcast / tasks 外加 _shared._build_download_base 与 api_service.api_service - web/routers/tasks.py ×2:_semaphores 取 _shared.BATCH_ZIP_SEM + api_service, topen 取 _shared.ENDPOINT_NAMES - web/tasks_registry.py ×1:start_task 直连 _taskcore._start_task_thread 子模块短名统一加 `services_` 前缀别名(services_tasks 等),避免与函数内 既有局部名相撞。 顺带清理:9 个子模块 docstring 里「(自 services/runtime.py 拆出……)」的 历史说明在门面删除后已成悬空引用,一并去掉;包的模块划分说明改为 「不再有兼容门面层」。 验证: - 符号可达:11 个被引符号在真实子模块上全部 hasattr 通过; services.runtime 已 ModuleNotFoundError;api_service 仍是同一实例对象 - 三个调用方模块均可正常 import - 门禁:ruff 0 · pyright 0 errors 0 warnings · pytest 175 passed - 真实 uvicorn 冒烟 8/8(含 POST /auth/login 400),临时数据目录独立落库 9 张表,生产 data/ 指纹三项一致
62 lines
2.0 KiB
Python
62 lines
2.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""同步流式响应垫片(音频分段透传)。
|
|
"""
|
|
|
|
|
|
|
|
class _SyncStreamResponse:
|
|
"""同步流式响应包装(把 aiohttp 的异步流按需拉成同步迭代)。
|
|
|
|
供流式下载使用:worker 是同步线程,这里经 core.async_bridge 在后台事件循环
|
|
逐块取数据,避免引入 requests 依赖(Flask 已删除、requests 已移出依赖)。
|
|
"""
|
|
|
|
def __init__(self, url: str, timeout: float):
|
|
from core import async_bridge
|
|
self._bridge = async_bridge
|
|
self._url = url
|
|
self._timeout = timeout
|
|
self._agent = None
|
|
self.status = 0
|
|
self.headers: dict[str, str] = {}
|
|
self._resp = None
|
|
|
|
def __enter__(self):
|
|
self._agent = self._bridge.run_sync(self._open())
|
|
self.status = self._agent[0]
|
|
self.headers = self._agent[1]
|
|
return self
|
|
|
|
async def _open(self):
|
|
from core.http_client import client
|
|
resp = await client.open('GET', self._url, headers={
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 '
|
|
'(KHTML, like Gecko) Safari/537.36',
|
|
'Referer': 'https://music.163.com/',
|
|
}, timeout=self._timeout)
|
|
return resp.status, dict(resp.headers), resp
|
|
|
|
def iter_content(self, chunk_size: int = 256 * 1024):
|
|
assert self._agent is not None
|
|
resp = self._agent[2]
|
|
it = resp.content.iter_chunked(chunk_size)
|
|
while True:
|
|
try:
|
|
chunk = self._bridge.run_sync(it.__anext__())
|
|
except StopAsyncIteration:
|
|
break
|
|
yield chunk
|
|
|
|
def __exit__(self, *exc):
|
|
if self._agent is not None and self._agent[2] is not None:
|
|
try:
|
|
self._agent[2].close()
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
|
|
def _stream_open(url: str, timeout: float = 60) -> _SyncStreamResponse:
|
|
"""打开同步可迭代的流式响应(见 _SyncStreamResponse)。"""
|
|
return _SyncStreamResponse(url, timeout)
|