Archived
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
This commit is contained in:
1 parent
183f5744d8
commit
ad8c30d932
12 files changed
+1367
-369
No files matched your search
+17
-13
@@ -8,6 +8,8 @@ from __future__ import annotations
|
||||
|
||||
import threading
|
||||
|
||||
import filetype
|
||||
|
||||
from core import sync_http as requests # 同步门面(底层 aiohttp,替代 requests)
|
||||
|
||||
# ============ HTTP 连接复用(提速核心) ============
|
||||
@@ -27,26 +29,28 @@ DL_CHUNK_SIZE = 256 * 1024
|
||||
_SESSION_LOCAL = threading.local()
|
||||
|
||||
|
||||
# 下载内容嗅探的音频魔数(未知期望尺寸时唯一的内容防线)
|
||||
_AUDIO_MAGIC = (b'ID3', b'fLaC', b'OggS', b'RIFF')
|
||||
# M4A/MP4 音频在 filetype 里被判为 video/mp4——mp4 是复合容器,仅凭魔数无法
|
||||
# 区分音频轨与视频轨,故显式放行该 mime。
|
||||
_CONTAINER_MIMES = frozenset({'video/mp4'})
|
||||
|
||||
|
||||
def _sniff_audio_header(path) -> bool:
|
||||
"""读取文件头做基础音频魔数校验:MP3(ID3/裸帧)、FLAC、OGG/Opus、WAV(RIFF)、
|
||||
M4A/MP4(ftyp)。未知期望尺寸的下载走"非空即通过"保留口径,本函数补上内容
|
||||
防线——CDN 错误页/风控 JSON 不再能被存成音频交付。"""
|
||||
"""按魔数判定文件是否为音频容器(MP3/FLAC/OGG/Opus/WAV/M4A/MP4)。
|
||||
|
||||
未知期望尺寸的下载走"非空即通过"保留口径,本函数补上内容防线——
|
||||
CDN 错误页/风控 JSON 不再能被存成音频交付。
|
||||
|
||||
识别交给 filetype(内置完整类型表,覆盖各格式的多种帧头变体),
|
||||
不再自行维护魔数前缀与 ftyp 偏移;对空文件/未知格式返回 False。
|
||||
"""
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
head = f.read(16)
|
||||
kind = filetype.guess(str(path))
|
||||
except OSError:
|
||||
return False
|
||||
if not head:
|
||||
if kind is None:
|
||||
return False
|
||||
if any(head.startswith(m) for m in _AUDIO_MAGIC):
|
||||
return True
|
||||
if len(head) >= 8 and head[:4] == b'\x00\x00\x00' and head[4:8] == b'ftyp':
|
||||
return True
|
||||
return len(head) >= 2 and head[0] == 0xFF and (head[1] & 0xE0) == 0xE0
|
||||
mime = (kind.mime or '').lower()
|
||||
return mime.startswith('audio/') or mime in _CONTAINER_MIMES
|
||||
|
||||
|
||||
def get_http_session() -> requests.Session:
|
||||
|
||||
Reference in new issue
Block a user