snowy-yang
|
046225e38d
|
refactor: aria2 改手写原生异步 JSON-RPC,aria2p 与同步 HTTP 栈从依赖树清零
为什么:aria2p 内部是 requests + websocket-client 的同步实现,每个 RPC 都是一次
「同步网络 IO 藏在 to_thread 里」——线程池跑得再满也不产生真正的并发,每次轮询
还要额外付一次线程跳转。这是本项目最后一处此类残留,删掉后整个进程的网络 IO
都走同一个 aiohttp 连接池。
改动:
- music_downloader/_aria2.py 整体重写:新增 _Aria2Rpc,直接向 aria2 的
JSON-RPC 2.0 端点发 getVersion / addUri / tellStatus / forceRemove /
removeDownloadResult,一律走 core.http_client.request_once(不重试,与 aria2p
口径一致);新增 Aria2RpcError 承载 HTTP 200 + error 对象的协议错误。
- _abort 逐条复刻 aria2p.remove(force=True, files=?, clean=True) 语义:终态走
removeDownloadResult;在途先 forceRemove 再清结果表;控制文件 .aria2 无条件
删除(clean=True 默认);仅当 files 且主移除成功才删数据文件。
- get_backend / _try_start 改原生异步:asyncio.Lock 单飞 + await rpc.get_version()
探活,不再用 to_thread 包 subprocess.Popen;超时路径改 to_thread(_reap) 回收。
- 新增 Protocol _Rpc / _ProcessHandle(补 returncode 声明)——只声明本模块真正
调用的方法,测试据此用轻量替身驱动状态机,不必依赖真实 aria2 进程。
- pyproject.toml 删 aria2p 依赖并同步更新过时注释;uv lock 连带移除 8 个传递
依赖:requests / urllib3 / websocket-client / charset-normalizer / loguru /
platformdirs / win32-setctime。至此全仓无任何 requests 栈。
- tests/test_aria2.py 重写为 21 项用例(_FakeRpc / _FakeProc / _LiveProc /
_FakeHTTP 替身),新增终态体积守卫、_try_start 三分支(就绪重试 / 超时不就绪
杀进程 / 进程提前退出)与 JSON-RPC 信封四例。
- tests/test_downloader.py 的 get_backend 替身由同步 lambda 改为协程——调用点
已直接 await,同步替身会被 await 成 None 而报 TypeError。
验证(在 aria2p 已物理移除的环境上复核,故能真正证明无残留依赖):
- uv run ruff check . → All checks passed!
- uv run pyright → 0 errors, 0 warnings, 0 informations
- uv run pytest → 392 项,0 失败 / 0 error / 0 skipped(基线 382,净增 10)
- data/site.db sha256 前后一致(a19f5dbf…6487),无 -wal/-shm 残留
|
2026-09-29 15:14:56 +08:00 |
|
snowy-yang
|
ad8c30d932
|
refactor: 下载/标签/格式探测改用成熟库(aria2 + mutagen + filetype)
为什么:原先三处都在手写轮子——ffmpeg CLI 拼参数写标签、aiohttp 单连接手搓
下载、自己比对魔数判格式。手写部分既难测又易错;改走成熟库后测试从 144 涨到
175,并且彻底去掉了 ffmpeg 这个外部二进制依赖。
下载 → aria2c 常驻 JSON-RPC(aria2p 客户端),新增 _aria2.py
- 单例后端:首次调用时拉起 aria2c,只监听 127.0.0.1 且强制随机 secret
(无鉴权的 aria2 RPC 允许本机任意进程操纵队列、读写任意文件)
- split=5 / max-connection-per-server=5 多连接 + 原生断点续传
- 异常语义与内置通道逐一对齐:取消/超限清残片抛 DownloadCanceled /
DownloadException,任务失败/超时保留残片抛 RequestException 供续传
- 取不到 aria2c 时 get_backend() 返回 None,自动降级到内置 aiohttp 通道
(本机无 aria2c 的降级路径已实测)
标签 → mutagen 纯 Python 写入,新增 tagging.py
- 取代 _write_metadata_with_ffmpeg + _build_format_args(226 行 shell 参数拼接)
- MP3(ID3v2.3)/FLAC/M4A/OGG/Opus 分容器实现,无子进程、不重编码
- 就地改帧:TXXX 等未触及的标签原样保留(ffmpeg 整帧重写会丢)
- 临时文件 + os.replace 原子替换,写坏不会留下半个文件
- 为什么不是 ffmpeg-python(原方案):实测它用 **kwargs 建模选项,无法表达
重复键(-metadata ×N),list 值还会被字符串化成 "['0', '1:v']";-map 尚有
内置的 stream 位置参数入口,但 -metadata 无解,且 overwrite_output=True
生成的 -y 被排到输出文件之后、可能失效
格式探测 → filetype,改 _http.py
- 删除手写的 _AUDIO_MAGIC 魔数表,改用 filetype.guess() 判 MIME
(audio/* 放行,video/mp4 作为容器显式放行)
配套
- Dockerfile:镜像不再装 ffmpeg,改装 aria2
- pyproject:新增 filetype / aria2p / mutagen;dev 加 httpx2(消除测试里的
StarletteDeprecationWarning,现 0 warning)
- __main__.py / __init__.py / models.py:清理 ffmpeg 相关导出与文案
测试:新增 31 个用例(test_aria2.py 13、test_tagging.py 21,含最小合法 FLAC
样本构造);全套 175 passed / 0 warning
|
2026-09-26 13:54:44 +08:00 |
|