Archived
为什么:原先三处都在手写轮子——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
88 lines
2.2 KiB
Python
88 lines
2.2 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""网易云音乐下载器(标签写入基于 mutagen,纯 Python)
|
||
|
||
提供功能:
|
||
- 同步 / 异步下载(本地落盘 / 内存返回)
|
||
- 批量并发下载
|
||
- 进度查询
|
||
- 基于 mutagen 的标签 + 封面写入(MP3/FLAC/M4A/MP4/OGG/Opus)
|
||
|
||
设计要点:
|
||
1. 内容嗅探交给 filetype(按魔数识别容器),不再自维护魔数表
|
||
2. 传输通道二选一:aria2c 常驻 RPC(多连接)优先,不可用时回落内置 aiohttp 流式
|
||
3. 标签写入按容器分派(ID3v2.3 / Vorbis comment / iTunes ilst),全部原子替换
|
||
4. 扩展名优先使用 API 返回的 type 字段,URL/Content-Type 兜底
|
||
"""
|
||
|
||
# 兼容门面:实现已按职责拆分到同包各子模块,此处原样再导出。
|
||
# 用 `__all__` 声明再导出(ruff F401 与 pyright 均据此认定「有意再导出」),
|
||
# 使既有调用点(music_downloader.xxx / from music_downloader import Y)零改动。
|
||
|
||
from ._concurrency import (
|
||
_FILE_LOCKS,
|
||
_FILE_LOCKS_GUARD,
|
||
CountingSemaphore,
|
||
RefCountedFileLock,
|
||
_FileLockHandle,
|
||
get_file_lock,
|
||
)
|
||
from ._http import (
|
||
_SESSION_LOCAL,
|
||
DEFAULT_UA,
|
||
DL_CHUNK_SIZE,
|
||
_sniff_audio_header,
|
||
get_http_session,
|
||
)
|
||
from ._log import (
|
||
logger,
|
||
)
|
||
from .cover import (
|
||
_COVER_CACHE,
|
||
_COVER_CACHE_LOCK,
|
||
_COVER_CACHE_MAX,
|
||
_COVER_CACHE_ORDER,
|
||
fetch_cover_bytes,
|
||
)
|
||
from .downloader import (
|
||
MusicDownloader,
|
||
)
|
||
from .models import (
|
||
API_TYPE_TO_EXT,
|
||
METADATA_FORMATS,
|
||
DownloadCanceled,
|
||
DownloadException,
|
||
DownloadResult,
|
||
MusicInfo,
|
||
QualityLevel,
|
||
_RetryFromScratch,
|
||
)
|
||
|
||
__all__ = [
|
||
'API_TYPE_TO_EXT',
|
||
'DEFAULT_UA',
|
||
'DL_CHUNK_SIZE',
|
||
'METADATA_FORMATS',
|
||
'_COVER_CACHE',
|
||
'_COVER_CACHE_LOCK',
|
||
'_COVER_CACHE_MAX',
|
||
'_COVER_CACHE_ORDER',
|
||
'_FILE_LOCKS',
|
||
'_FILE_LOCKS_GUARD',
|
||
'_SESSION_LOCAL',
|
||
'CountingSemaphore',
|
||
'DownloadCanceled',
|
||
'DownloadException',
|
||
'DownloadResult',
|
||
'MusicDownloader',
|
||
'MusicInfo',
|
||
'QualityLevel',
|
||
'RefCountedFileLock',
|
||
'_FileLockHandle',
|
||
'_RetryFromScratch',
|
||
'_sniff_audio_header',
|
||
'fetch_cover_bytes',
|
||
'get_file_lock',
|
||
'get_http_session',
|
||
'logger',
|
||
]
|