snowy-yang
|
22b263f547
|
refactor: SMTP 发信改 aiosmtplib,最后一个阻塞网络调用从 to_thread 里取出来
为什么:smtplib 是同步栈,原先靠 asyncio.to_thread 卸载——线程池里躺着的仍是一次
完整的 TCP + TLS 握手加多轮 SMTP 交互,属于「同步网络 IO 藏在 to_thread 里」:既
占着线程,也不产生并发收益。这是本系列最后一条此类遗留(aria2p 那条已在 C2 清零)。
改动:
- auth_system/mail.py:_smtp_connect / _smtp_send 两个同步函数合成一个原生异步
_smtp_send,底层换 aiosmtplib.SMTP。TLS 口径与原实现一一对应:465 用 use_tls
(隐式 TLS),其余端口用 start_tls(显式 STARTTLS,先握手再 LOGIN);两条路都传
显式构造的 ssl.create_default_context(),不依赖运行时默认校验口径。finally 里
quit 失败只吞连接关闭错误,sendmail 的真实异常原样上抛——send_verify_code 依赖
它决定「不写库、不烧 24h 额度」。
- auth_system/mail.py:send_email 改直接 await;移除 import asyncio / import smtplib。
- auth_system/__init__.py:撤掉 _smtp_connect 的再导出——它是同步时代的辅助函数,
全仓无消费方,新实现里也不存在对应物。
- web/routers/register.py:更新过时的模块说明(原文仍写「smtplib 阻塞发送已卸载」)。
- pyproject.toml / uv.lock:新增 aiosmtplib>=5.1.3,<6.0。
- tests/test_mail_smtp.py(新增 5 条):SMTP 路径此前零覆盖,而「端口 → TLS 方式」
的映射属「授权码会不会明文过网」级别的差异,一旦被改反不会有任何地方报警。用假
SMTP 替身记录构造参数与调用序、全程不触网:587 必须 start_tls 且 LOGIN 在 connect
之后、465 必须 use_tls 且不得再叠 STARTTLS、端口缺省回落 587、quit 失败不掩盖结果、
sendmail 失败原样上抛且仍关连接。
验证:
- uv run ruff check . → All checks passed!
- uv run pyright → 0 errors, 0 warnings, 0 informations
(aiosmtplib 自带 py.typed,故对它的调用是有效类型校验;另用 inspect 逐一核对了
5.1.3 的 SMTP / connect / login / sendmail / quit 参数名)
- uv run pytest → 397 项,0 失败 / 0 error / 0 skipped(C3 后 392,新增 5)
- data/site.db sha256 前后一致(a19f5dbf…6487),无 -wal/-shm 残留
|
2026-09-29 15:28:13 +08:00 |
|
snowy-yang
|
e8cc7cb34a
|
refactor: 本地文件 IO 归位——重活留线程、微秒级直接同步,移除零使用依赖 aiofiles
为什么:文件 IO 这条线上 to_thread 用得过宽。凡是「单文件 exists / unlink / stat」
这类微秒级元数据操作,线程调度开销(几十微秒起)比操作本身还大,套 to_thread 只是
给同步调用加了层异步外壳,不产生任何并发收益;真正需要卸载的是整库复制、ZIP 整段
拷贝、SQLite 探测这类活。本批按这个口径把两端都摆正,并顺手修掉一处真实缺陷。
改动:
- web/routers/admin_site.py 备份上传:原 `content = await f.read()` 会把整库一次性
读进内存(端点对外无大小上限,等于给了上传方一个内存放大器),落盘后还要把文件
重新打开读一次头。改为 _save_upload_stream 按 1 MiB 逐块搬运——内存占用恒为一块,
文件头在写第一块时就地截获(省掉那趟回读)。仍属阻塞 IO,故保留在 to_thread 里。
- web/routers/admin_site.py backup_list:撤掉仅为一次 stat 而拆出的嵌套函数 +
to_thread,直接在事件循环里做完。
- web/routers/admin.py:save_cookie / logout / _get_cookie_info 四处 to_thread 改
直接调用——它们读写 1KB 级的 cookie.txt,全程不碰网络。
- music_api/qr_client.py:logout() 的 print 改 logger。该方法既被 CLI 也被 admin
端点调用,在 web 语境里 print 会把内容直接喷到服务端 stdout,绕过 logging 配置、
无法被收集,也污染容器日志。
- auth_system/backup.py:三处单文件 unlink(占位文件回滚、删备份、裁剪 pre_restore_)
改直接同步;把只为配合 to_thread 而拆出的 _backup_path_sync 并回 backup_path。
保留在线程里的:整库 copy2 / os.replace、glob + N 次 stat、SQLite 探测、以及持
跨线程锁的 _reserve_backup_path(内联会让事件循环等一把别的线程可能持有的锁)。
- web/routers/delivery.py:两处 os.path.exists 改直接同步(带 ASYNC240 行内豁免,
与 core/db.py、auth_system/db.py 同一写法),顺带删掉随之未使用的 asyncio 导入。
- pyproject.toml / uv.lock:移除 aiofiles。它全仓零引用,且底层同样是 to_thread——
对 1KB 的 cookie 文件不会带来任何真异步收益。
验证:
- 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
(含 test_admin_backup_upload 四条:接受真实 SQLite / 缺字段拒 / 后缀拒 /
后缀对但内容非库必须拒且不留半成品)
- data/site.db sha256 前后一致(a19f5dbf…6487),无 -wal/-shm 残留
|
2026-09-29 15:20:51 +08:00 |
|
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
|
064a89d726
|
refactor: 删除旧前端单体,React 产物成为唯一首页来源
为什么:React 迁移已验证完毕(浏览器冒烟全过、golden 刷新),
legacy 模板兜底分支指向已删除的文件只会制造幻觉路径。删除
templates/index.html(466KB)与 static/site_auth.js(80KB)及
/static 挂载;jinja2 随兜底分支失去唯一使用者,连同 markupsafe
一并移出依赖(63 包 -2)。_render_index 简化为直接读构建产物——
dist 已入库恒存在,无需分支。
|
2026-09-27 18:41:30 +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 |
|
snowy-yang
|
8446166120
|
refactor!: uv 管理环境 + 彻底删除技术债(Flask 与 requests 全部移除)
## uv 环境管理
- pyproject.toml 声明 dependencies + [dependency-groups].dev,uv.lock 精确锁定
- 删除 requirements.txt / requirements-fastapi.txt / requirements-dev.txt
- .python-version = 3.12;Dockerfile 改用 uv sync --frozen(可复现构建)
- 新增 jinja2(首页渲染)
## 删除 Flask 技术债
- 从 main.py 用 ast 精确剥离 56 个 Flask 节点(路由/中间件/错误处理/启动),
框架无关的服务层落为 services/runtime.py(MusicAPIService、下载/打包 worker、
任务监控、缓存清扫),零 Flask 依赖
- 删除 main.py(4527 行)与 auth_routes.py(1050 行)
- auth_system.py 移除会话/请求上下文层(login_session/current_user/login_required/
admin_required/_client_ip 等),保留数据与业务层
- 新增 core/net.py(IP 解析)、web/auth_state.py(登录锁定/2FA 票据/发码限流)
- FastAPI 侧改直接依赖 services.runtime,不再 import main
- web/app.py 去掉 Flask fallback;入口默认根路径模式(API_PREFIX 可选)
## 删除 requests 技术债
- 新增 core/sync_http.py:requests 风格的同步门面(底层 aiohttp 连接池,
流式读取经有界队列背压),含 Session/headers/(connect,read) 元组超时兼容
- core/utils、auth_system、podcast_api、music_downloader、services 全部切换
- 实测真实下载 5.2MB 音频完整成功
## 质量门禁归零
- ruff:0 错误(B904 全部修复,不再忽略)
- pyright:0 错误(类型收窄修复)
- pytest:140/140(含 golden 契约回归、root 模式 66 项路径覆盖)
- 真实 uvicorn 启动:/health、/、/static、受保护端鉴权全部正确
新增 tests/golden/responses.json:删除 Flask 前捕获的 32 例响应快照,
FastAPI 逐字段复现——删除巨石后行为回归仍有硬证据。
|
2026-09-26 11:10:17 +08:00 |
|