学会NCM文件转MP3——充网抑云会员之后该做些什么大事?

不多说,就因为VIP的歌曲是ncm格式,狠狠地割了我韭菜!

我在使用网抑云听音乐,然后充了VIP,别问,问就是因为我喜欢听冷门歌曲,不然早就用QQ音乐了不是? (无拉踩)

搜来搜去,发现有爱心人士开发了多种格式转换音乐的网站:

这里就是

懒狗看到这里已经可以划走了,剩下的是给能折腾的。

但是奈何网站打包成zip真的巨慢巨慢,一个一个下载有一种在打工的感觉,虽然可以白嫖(对不起我真的没打赏),但是还是想自己用的方便些,所以又跑到github上搜刮别人剩下来的代码,有各种各样的,C#、PHP、python、java的,思来想去,我安装了那么多虚拟环境,而且好像也不用图形界面(难得不用卷图形界面),所以打算采用python脚本批量处理,最后直接用bat文件实现自动化处理,岂不美哉?

最近玩星穹铁道玩得头昏脑花,也不是没有学习,就是很难再遇上一个有阴阳师感觉的游戏,一下子带我回到初一刚刚玩手游的时候了。后天就要去地狱实训厂实习了,也不知道能快活几天?

我参考的主要有两个佬的代码,一个是java佬的,一套成型的图形化附带一篇解释说明,一个是python佬的主代码,我自己按照自己的需求稍微修改了下。

java佬 python佬

我一开始的时候没有感觉有什么不对,安装了ffmpeg之后就直接上,然后它直接抛出编码错误给我,我才明白过来,那么简单就给你解码了,那还玩什么VIP?? 后来知道了ncm是分段加密的,包含几种主流密码的加密,密钥应该是被佬破解出来的,知道密钥和算法之后,就可以愉快的解密了!

我原来学过密码学,所以看得差不多,不知道的小伙伴也不用太担心,因为你不写论文设计加密的话,也不需要知道太详细,知道原理然后会用就行。

这里附上java佬的解析,写得很好,我也不改了。因为我懒。(我就烂)

信息大小备注
Magic Header10 bytes跳过
KEY Length4 bytes用AES128加密RC4密钥后的长度(小端字节排序,无符号整型)
KEY From AES128 DecodeKEY Length(其实就是128 bytes)用AES128加密的RC4密钥(注意:1.按字节对0x64异或2.AES解密(其中PKCS5Padding填充模式会去除末尾填充部分;)3.去除前面neteasecloudmusic17个字节;
Mata Length4 bytesMata的信息的长度(小端字节排序,无符号整型)
Mata Data(JSON)Mata LengthJSON的格式的Mata的信息(注意:1.按字节对0x63异或;2.去除前面163 key(Don't modify):22个字节;3.Base64进行decode;4.AES解密;5.去除前面music:6个字节后获得JSON)
CRC校验码4 bytes跳过
Gap5 bytes跳过
Image Size4 bytes图片大小
ImageImage Size图片数据
Music Data-RC4-KSA生成s盒,RC4-PRGA解密

它应该是设计了一套专门解密的系统在软件里面,然后匹配这个来解密播放,但是我想知道为什么密钥是固定的,这样不就被别人破解了吗???真懒啊,因为这样就不用存那么多密钥了,大部分人也不知道怎么破解吧.....一个是懒得知道,一个是因为觉得没有必要,呃,但是我购买了服务竟然还是不干净的,好歹给我源数据吧,密钥我自己存着不就好了?

进入正题

没安装这个密码库的需要安装在你的环境/虚拟环境里面:

pip install pycryptodome

方案1:手动运行脚本

固定文件夹是这样的:你的主文件下放两个文件夹,一个放ncm后缀文件,一个mp3后缀文件。

运行代码之后,就会把ncm文件夹里所有歌转成mp3格式放到mp3文件夹里。

命名为convert_ncm_to_mp3.py放置在convert_ncm_to_mp3文件夹下。

出问题的话请照这里做。

# 注意ncm是加密过的 所以使用ffmpeg只能对没有加密的处理
import binascii
import struct
import base64
import json
import os
from Crypto.Cipher import AES


def run(input_file, out_file):
    ncm_files = [file for file in os.listdir(input_file) if file.lower().endswith('.ncm')]
    for ncm_file in ncm_files:
        file_path = os.path.join(input_file, ncm_file).replace('\\', '/')
        dump(file_path, out_file)
        song_name = file_path.split('.')[1]
        song_name = song_name.split('/')[2]
        print('{}.ncm 文件转换成 {}.mp3 文件完成!'.format(song_name,song_name))
    print("批量转换完成!")



def dump(file_path,out_path):
    # 687A4852416D736F356B496E62617857 (Hex) -> hzHRAmso5kInbaxW    (Text)
    core_key = binascii.a2b_hex("687A4852416D736F356B496E62617857")
    # 2331346C6A6B5F215C5D2630553C2728 (Hex) -> #14ljk_!\]&0U<'(    (Text)
    meta_key = binascii.a2b_hex("2331346C6A6B5F215C5D2630553C2728")
    # 定义 lambda 表达式
    unpad = lambda s: s[0:-(s[-1] if type(s[-1]) == int else ord(s[-1]))]
    # 以二进制读模式打开传入的 ncm 文件
    f = open(file_path, 'rb')
    song_name = file_path.split('.')[1]
    song_name = song_name.split('/')[2]
    # 读八字节
    header = f.read(8)
    # 确认其为 ncm 格式标记
    assert binascii.b2a_hex(header) == b'4354454e4644414d'
    # 后移 2 字节(多余字节)
    f.seek(2, 1)
    # 读四字节
    key_length = f.read(4)
    # 以小端方式转换 key_length 为 integer
    # 80 00 00 00 (Hex) -> 128 (int)
    key_length = struct.unpack('<I', bytes(key_length))[0]
    # 向后读文件的 128 字节
    key_data = f.read(key_length)
    # 将 key_data 转化为字符数组
    key_data_array = bytearray(key_data)
    # 将 key_data_array 中的每个字节与 0x64 做异或运算
    for i in range(0, len(key_data_array)): key_data_array[i] ^= 0x64
    # 将 bytearray key_data_array 转型为 bytes
    key_data = bytes(key_data_array)
    # 使用之前定义的 core_key 创建了 AES_ECB 解密器 cryptor
    cryptor = AES.new(core_key, AES.MODE_ECB)
    # 首先看 cryptor.decrypt(key_data):解析 key_data,解析出来的数据开头是 neteasecloudmusic,即 ncm 的全称
    # 通过开头定义的 lambda 函数 unpad 去掉末尾的 \r 和开头的 neteasecloudmusic
    # 17 为 len("neteasecloudmusic")
    key_data = unpad(cryptor.decrypt(key_data))[17:]
    # 更新 key_length 的值(即 data 的长度)
    key_length = len(key_data)

    # 将 key_data 转型为 bytearray 类型
    key_data = bytearray(key_data)

    # 以下是 RC4-KSA 算法
    key_box = bytearray(range(256))
    c = 0
    last_byte = 0
    key_offset = 0
    for i in range(256):
        swap = key_box[i]
        c = (swap + last_byte + key_data[key_offset]) & 0xff
        key_offset += 1
        if key_offset >= key_length: key_offset = 0
        key_box[i] = key_box[c]
        key_box[c] = swap
        last_byte = c

    # 读取四字节长度,和前面的 key_length 相似
    meta_length = f.read(4)
    # 以小端方式将 meta_length 转化为 int
    meta_length = struct.unpack('<I', bytes(meta_length))[0]
    # 读取 meta_kength 字节长度数据赋给 meta_data
    meta_data = f.read(meta_length)
    # 类型转换
    meta_data_array = bytearray(meta_data)
    # 与 0x63 做异或
    for i in range(0, len(meta_data_array)): meta_data_array[i] ^= 0x63
    # 转型
    meta_data = bytes(meta_data_array)
    # 这里可以打断点看下 meta_data 的值,开头是 "163 key(Don't modify):",共 22 位
    # 这里去掉无关的前 22 位然后使用 base64 解码
    meta_data = base64.b64decode(meta_data[22:])
    # 再和上面类似,构造 ECB 进行解密
    cryptor = AES.new(meta_key, AES.MODE_ECB)
    # 此处 meta_data 的一个参考数据:
    # b'music:{"musicId":441491828,"musicName":"\xe6\xb0\xb4\xe6\x98\x9f\xe8\xae\xb0","artist":[["\xe9\x83\xad\xe9\xa1\xb6",2843]],"albumId":35005583,"album":"\xe9\xa3\x9e\xe8\xa1\x8c\xe5\x99\xa8\xe7\x9a\x84\xe6\x89\xa7\xe8\xa1\x8c\xe5\x91\xa8\xe6\x9c\x9f","albumPicDocId":2946691248081599,"albumPic":"https://p4.music.126.net/wSMfGvFzOAYRU_yVIfquAA==/2946691248081599.jpg","bitrate":320000,"mp3DocId":"668809cf9ba99c3b7cc51ae17a66027f","duration":325266,"mvId":5404031,"alias":[],"transNames":[],"format":"mp3"}\r\r\r\r\r\r\r\r\r\r\r\r\r'
    # 去掉前六位 "music:"
    meta_data = unpad(cryptor.decrypt(meta_data)).decode('utf-8')[6:]
    # 转换成 json
    meta_data = json.loads(meta_data)

    # CRC32 校验码
    crc32 = f.read(4)
    crc32 = struct.unpack('<I', bytes(crc32))[0]
    # 后移五字节
    f.seek(5, 1)
    # 获取歌曲封面大小
    image_size = f.read(4)
    # 以小端方式将读取到的 Hex 数据转换成 int
    image_size = struct.unpack('<I', bytes(image_size))[0]
    # 读封面大小长度的数据,赋值给 image_data
    image_data = f.read(image_size)
    # 从之前构造的 json 中取歌曲名和文件拓展名,赋给 file_name
    # file_name = meta_data['musicName'] + '.' + meta_data['format']
    file_name = song_name + '.' + meta_data['format']
    # 以二进制写方式打开要生成的文件(若文件不存在会自动创建)
    m = open(os.path.join(out_path, file_name).replace('\\', '/'), 'wb')
    chunk = bytearray()

    # 以下是 RC4-PRGA 算法,进行还原并输出文件
    while True:
        chunk = bytearray(f.read(0x8000))
        chunk_length = len(chunk)
        if not chunk:
            break
        for i in range(1, chunk_length + 1):
            j = i & 0xff;
            chunk[i - 1] ^= key_box[(key_box[j] + key_box[(key_box[j] + j) & 0xff]) & 0xff]
        m.write(chunk)
    m.close()
    f.close()


if __name__ == '__main__':
    import sys
    input_file = './ncm'
    out_file = './mp3'
    # 固定文件夹版本
    run(input_file, out_file)

方案2:变成bat自动运行

不想手动打开pycharm,真的懒!这样,写一个脚本吧,但是由于我安装的是一个虚拟环境,我需要先启动我的虚拟环境class38,并把它添加到脚本里,然后重新写一个新的脚本文件,固定格式调用python文件。

命名为convert_ncm_to_mp3_script.py放置在convert_ncm_to_mp3文件夹下

import argparse
import os
import convert_ncm_to_mp3 as convert

if __name__ == '__main__':
    # 创建参数解析器
    parser = argparse.ArgumentParser(description='Process input and output files.')

    # 添加输入文件路径参数
    parser.add_argument('input', help='Input file path.')

    # 添加输出文件路径参数
    parser.add_argument('output', help='Output file path.')

    # 解析命令行参数
    args = parser.parse_args()

    # 获取输入文件路径和输出文件路径
    input_path = args.input
    output_path = args.output

    # 确保输出文件所在的目录存在
    os.makedirs(os.path.dirname(output_path), exist_ok=True)

    convert.run(input_path, output_path)

新建convert_run.txt在里面写上以下内容:

@echo off
set INPUT_FILE=./ncm
set OUTPUT_FILE=./mp3
set VENV_NAME=class38

call conda activate %VENV_NAME% >nul
python convert_ncm_to_mp3_script.py "%INPUT_FILE%" "%OUTPUT_FILE%"

call conda deactivate >nul

修改后缀为.bat然后保存,运行。

上面只显示了一首但是可以尝试多首如下:

到此,差不多结束,QQ音乐的话,还没有开始用,所以也没充会员,以后等充了再说。

要使用的时候,把需要转换的ncm文件放在ncm文件夹里面,运行批处理文件,然后剪切所有转换好的mp3文件转移到U盘,再清空ncm和mp3文件夹即可,主打的就是一个自力更生。

嗯,不多写了,狮子将军等我上号呢!嘻嘻!0-0!

等我有钱一定要买一只会笑的猫猫!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值