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/music_downloader/__init__.py
T
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

88 lines
2.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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',
]