每次在网易云音乐下载的歌曲只能在特定播放器里听?收藏的数百首.ncm格式音乐无法导入车载系统或运动手环?这种困扰终于有了一劳永逸的解决方案。本文将带你用Python打造一个自动化工具链,不仅能批量转换加密格式,还能完美保留歌曲元数据和专辑封面。
网易云音乐的.ncm格式本质上是一种DRM(数字版权管理)保护方案。它通过AES加密算法对原始音频流进行混淆处理,并在文件头部嵌入解密所需的密钥信息。这种设计既保护了版权,也限制了用户在其他设备上的使用自由。
加密流程主要包含三个关键环节:
687A4852416D736F356B496E62617857进行首层AES-ECB模式加密2331346C6A6B5F215C5D2630553C2728加密存储提示:AES-ECB模式虽然存在安全性争议,但对于音乐文件保护已经足够,这也是我们能逆向解密的理论基础
在开始编写转换脚本前,需要配置合适的Python环境。推荐使用Python 3.8+版本,以获得最佳的兼容性和性能表现。
必备依赖库及其作用:
| 库名称 | 用途描述 | 安装命令 |
|---|---|---|
| pycryptodome | 提供AES解密功能 | pip install pycryptodome |
| mutagen | 处理音频元数据 | pip install mutagen |
| tqdm | 显示进度条 | pip install tqdm |
验证安装是否成功:
python复制import Crypto
from mutagen.id3 import ID3
from tqdm import tqdm
print("所有依赖检查通过!")
对于国内用户,如果遇到安装速度慢的问题,可以使用清华镜像源加速:
bash复制pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pycryptodome mutagen tqdm
下面是我们改进后的完整解密脚本,增加了错误处理和元数据保留功能:
python复制import os
import json
import base64
import binascii
import struct
from Crypto.Cipher import AES
from tqdm import tqdm
from mutagen.mp3 import MP3
from mutagen.id3 import ID3, APIC, TIT2, TPE1, TALB
def unpad(data):
padding = data[-1] if isinstance(data[-1], int) else ord(data[-1])
return data[:-padding]
def decrypt_ncm(file_path, output_dir=None):
"""解密单个.ncm文件"""
try:
# 初始化密钥
core_key = binascii.a2b_hex("687A4852416D736F356B496E62617857")
meta_key = binascii.a2b_hex("2331346C6A6B5F215C5D2630553C2728")
with open(file_path, 'rb') as f:
# 验证文件头
header = f.read(8)
if binascii.b2a_hex(header) != b'4354454e4644414d':
raise ValueError("无效的.ncm文件格式")
# 跳过2字节,读取密钥长度
f.seek(2, 1)
key_length = struct.unpack('<I', f.read(4))[0]
# 读取并解密密钥数据
key_data = bytearray([b ^ 0x64 for b in f.read(key_length)])
key_data = unpad(AES.new(core_key, AES.MODE_ECB).decrypt(key_data))[17:]
# 初始化密钥箱
key_box = bytearray(range(256))
c, last_byte, key_offset = 0, 0, 0
for i in range(256):
swap = key_box[i]
c = (swap + last_byte + key_data[key_offset]) & 0xff
key_offset = (key_offset + 1) % len(key_data)
key_box[i], key_box[c] = key_box[c], swap
last_byte = c
# 读取并解密元数据
meta_length = struct.unpack('<I', f.read(4))[0]
meta_data = bytearray([b ^ 0x63 for b in f.read(meta_length)])
meta_data = base64.b64decode(meta_data[22:])
meta_data = unpad(AES.new(meta_key, AES.MODE_ECB).decrypt(meta_data)).decode('utf-8')[6:]
meta_data = json.loads(meta_data)
# 跳过CRC校验和5字节
f.seek(9, 1)
# 读取专辑封面
image_size = struct.unpack('<I', f.read(4))[0]
image_data = f.read(image_size)
# 准备输出路径
output_dir = output_dir or os.path.dirname(file_path)
output_name = f"{meta_data['musicName']}.{meta_data['format']}"
output_path = os.path.join(output_dir, output_name)
# 解密并写入音频数据
with open(output_path, 'wb') as m:
while True:
chunk = bytearray(f.read(0x8000))
if not chunk:
break
for i in range(len(chunk)):
j = i & 0xff
chunk[i] ^= key_box[(key_box[j] + key_box[(key_box[j] + j) & 0xff]) & 0xff]
m.write(chunk)
# 添加ID3标签和封面
if meta_data['format'].lower() == 'mp3':
audio = MP3(output_path, ID3=ID3)
try:
audio.add_tags()
except:
pass
audio.tags.add(TIT2(encoding=3, text=meta_data['musicName']))
audio.tags.add(TPE1(encoding=3, text=meta_data['artist'][0][0]))
audio.tags.add(TALB(encoding=3, text=meta_data['album']))
audio.tags.add(APIC(
encoding=3,
mime='image/jpeg',
type=3,
desc='Cover',
data=image_data
))
audio.save()
return True, output_path
except Exception as e:
return False, str(e)
单个文件转换只是开始,真正的价值在于批量处理能力。我们开发了以下增强功能:
文件夹监控模式:
python复制def batch_convert(input_dir, output_dir=None, skip_existing=True):
"""批量转换目录中的所有.ncm文件"""
success, fail = 0, 0
output_dir = output_dir or input_dir
# 确保输出目录存在
os.makedirs(output_dir, exist_ok=True)
# 遍历目录
for root, _, files in os.walk(input_dir):
for file in tqdm([f for f in files if f.lower().endswith('.ncm')],
desc="转换进度"):
input_path = os.path.join(root, file)
rel_path = os.path.relpath(root, input_dir)
current_out_dir = os.path.join(output_dir, rel_path)
os.makedirs(current_out_dir, exist_ok=True)
# 检查是否已存在输出文件
if skip_existing:
with open(input_path, 'rb') as f:
f.seek(8)
key_length = struct.unpack('<I', f.read(4))[0]
f.seek(2 + 4 + key_length + 4)
meta_length = struct.unpack('<I', f.read(4))[0]
meta_data = bytearray([b ^ 0x63 for b in f.read(meta_length)])
meta_data = base64.b64decode(meta_data[22:])
meta_data = unpad(AES.new(meta_key, AES.MODE_ECB).decrypt(meta_data)).decode('utf-8')[6:]
meta_data = json.loads(meta_data)
output_name = f"{meta_data['musicName']}.{meta_data['format']}"
output_path = os.path.join(current_out_dir, output_name)
if os.path.exists(output_path):
continue
# 执行转换
result, msg = decrypt_ncm(input_path, current_out_dir)
if result:
success += 1
else:
fail += 1
print(f"转换失败: {file} - {msg}")
print(f"转换完成: 成功 {success} 个, 失败 {fail} 个")
使用示例:
python复制if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='网易云音乐.ncm文件批量转换工具')
parser.add_argument('input', help='输入文件或目录路径')
parser.add_argument('-o', '--output', help='输出目录路径')
parser.add_argument('--no-skip', action='store_false',
dest='skip_existing', help='覆盖已存在的文件')
args = parser.parse_args()
if os.path.isfile(args.input) and args.input.lower().endswith('.ncm'):
result, msg = decrypt_ncm(args.input, args.output)
print("转换成功!" if result else f"转换失败: {msg}")
elif os.path.isdir(args.input):
batch_convert(args.input, args.output, args.skip_existing)
else:
print("错误: 无效的输入路径")
实际使用中可能会遇到各种边缘情况,我们的脚本已经内置了以下增强功能:
1. 元数据修复工具:
python复制def repair_metadata(file_path, meta_data, image_data=None):
"""修复或添加音频文件的元数据"""
if file_path.lower().endswith('.mp3'):
audio = MP3(file_path, ID3=ID3)
try:
audio.add_tags()
except:
pass
# 清除现有标签
for tag in audio.tags.keys():
if tag.startswith('T') or tag == 'APIC':
del audio.tags[tag]
# 添加新标签
audio.tags.add(TIT2(encoding=3, text=meta_data['musicName']))
audio.tags.add(TPE1(encoding=3, text=meta_data['artist'][0][0]))
audio.tags.add(TALB(encoding=3, text=meta_data['album']))
if image_data:
audio.tags.add(APIC(
encoding=3,
mime='image/jpeg',
type=3,
desc='Cover',
data=image_data
))
audio.save()
return True
return False
2. 常见错误代码表:
| 错误代码 | 原因分析 | 解决方案 |
|---|---|---|
| ERR_HEADER | 文件头不匹配 | 确认是有效的.ncm文件 |
| ERR_KEY | 密钥解密失败 | 检查pycryptodome库版本 |
| ERR_META | 元数据解析错误 | 尝试手动提取元数据 |
| ERR_WRITE | 输出文件写入失败 | 检查磁盘空间和权限 |
3. 性能优化技巧:
--no-skip参数强制重新生成所有文件python复制from multiprocessing import Pool
def parallel_convert(file_list, output_dir):
with Pool(processes=4) as pool:
results = pool.starmap(decrypt_ncm,
[(f, output_dir) for f in file_list])
return sum(r[0] for r in results), len(results)
为了让脚本在不同操作系统上都能稳定运行,我们需要注意以下细节:
Windows系统:
os.path模块而非硬编码反斜杠bash复制pyinstaller --onefile --icon=music.ico ncm_converter.py
macOS/Linux系统:
bash复制cat > ~/Desktop/NCMConverter.desktop <<EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=NCM Converter
Exec=python3 /path/to/ncm_converter.py %F
Icon=audio-x-generic
Terminal=true
EOF
chmod +x ~/Desktop/NCMConverter.desktop
Docker化部署:
dockerfile复制FROM python:3.8-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY ncm_converter.py .
ENTRYPOINT ["python", "ncm_converter.py"]
构建和运行:
bash复制docker build -t ncm-converter .
docker run -v /path/to/music:/data ncm-converter /data
对于不熟悉命令行的用户,我们使用PySimpleGUI开发了图形界面:
python复制import PySimpleGUI as sg
def create_gui():
layout = [
[sg.Text("输入.ncm文件或目录:")],
[sg.Input(key='-INPUT-'), sg.FolderBrowse()],
[sg.Text("输出目录 (可选):")],
[sg.Input(key='-OUTPUT-'), sg.FolderBrowse()],
[sg.Checkbox('跳过已转换文件', default=True, key='-SKIP-')],
[sg.ProgressBar(100, size=(40,20), key='-PROGRESS-')],
[sg.Multiline(size=(60,10), key='-LOG-', autoscroll=True)],
[sg.Button('开始转换'), sg.Button('退出')]
]
window = sg.Window('网易云音乐NCM转换器', layout)
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, '退出'):
break
if event == '开始转换':
input_path = values['-INPUT-']
output_path = values['-OUTPUT-'] or None
skip_existing = values['-SKIP-']
if not input_path:
sg.popup_error("请选择输入路径!")
continue
window['-LOG-'].update("")
window['-PROGRESS-'].update(0)
if os.path.isfile(input_path):
result, msg = decrypt_ncm(input_path, output_path)
log = "转换成功!" if result else f"错误: {msg}"
window['-LOG-'].print(log)
else:
total = sum(1 for _,_,f in os.walk(input_path)
for name in f if name.lower().endswith('.ncm'))
if not total:
window['-LOG-'].print("未找到.ncm文件!")
continue
converted = 0
for root, _, files in os.walk(input_path):
for file in (f for f in files if f.lower().endswith('.ncm')):
input_file = os.path.join(root, file)
rel_path = os.path.relpath(root, input_path)
current_out = os.path.join(output_path, rel_path) if output_path else root
os.makedirs(current_out, exist_ok=True)
result, msg = decrypt_ncm(input_file, current_out)
if result:
converted += 1
window['-LOG-'].print(f"成功: {file}")
else:
window['-LOG-'].print(f"失败: {file} - {msg}")
window['-PROGRESS-'].update(converted * 100 // total)
window['-LOG-'].print(f"\n转换完成: {converted}/{total}")
window.close()
这个GUI版本保留了所有核心功能,同时提供了进度显示和日志输出,大大提升了用户体验。