This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
NeteaseUrl/services/streaming.py
T
snowy-yang 7a70eef636 refactor: 删除 services.runtime 兼容门面,7 处调用点直连真实子模块
为什么:上一次分包重构把 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/ 指纹三项一致
2026-09-26 14:48:05 +08:00

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)