#!/usr/bin/python3
# Modifier: Liang Lixin
# Folder dump version by LiangLixin
import binascii
import struct
import base64
import json
import os
import logging
import urllib 
import requests
import time

from Crypto.Cipher import AES

music_suffix_list = ['mp3', 'wav', 'ape', 'flac', 'MP3', 'WAV', 'APE', 'FLAC']
def dump(file_path, file_name_no_suffix):
    core_key = binascii.a2b_hex("687A4852416D736F356B496E62617857")
    meta_key = binascii.a2b_hex("2331346C6A6B5F215C5D2630553C2728")
    unpad = lambda s : s[0:-(s[-1] if type(s[-1]) == int else ord(s[-1]))]
    f = open(file_path,'rb')
    header = f.read(8)
    assert binascii.b2a_hex(header) == b'4354454e4644414d'
    f.seek(2, 1)
    key_length = f.read(4)
    key_length = struct.unpack('<I', bytes(key_length))[0]
    key_data = f.read(key_length)
    key_data_array = bytearray(key_data)
    for i in range (0,len(key_data_array)): key_data_array[i] ^= 0x64
    key_data = bytes(key_data_array)
    cryptor = AES.new(core_key, AES.MODE_ECB)
    key_data = unpad(cryptor.decrypt(key_data))[17:]
    key_length = len(key_data)
    key_data = bytearray(key_data)
    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
    meta_length = f.read(4)
    meta_length = struct.unpack('<I', bytes(meta_length))[0]
    meta_data = f.read(meta_length)
    meta_data_array = bytearray(meta_data)
    for i in range(0,len(meta_data_array)): meta_data_array[i] ^= 0x63
    meta_data = bytes(meta_data_array)
    meta_data = base64.b64decode(meta_data[22:])
    cryptor = AES.new(meta_key, AES.MODE_ECB)
    meta_data = unpad(cryptor.decrypt(meta_data)).decode('utf-8')[6:]
    meta_data = json.loads(meta_data)
    crc32 = f.read(4)
    crc32 = struct.unpack('<I', bytes(crc32))[0]
    f.seek(5, 1)
    image_size = f.read(4)
    image_size = struct.unpack('<I', bytes(image_size))[0]
    image_data = f.read(image_size)
    file_name = file_name_no_suffix + '.' + meta_data['format']

    ## delete
    # try:
    #     os.remove(os.path.join(os.path.split(file_path)[0], meta_data['musicName'] + '.' + meta_data['format']))
    #     print('删除文件 :' + os.path.join(os.path.split(file_path)[0], meta_data['musicName'] + '.' + meta_data['format']))
    #     ## delete 
    # except Exception as e:
    #     print('删除失败', e)
    

    m = open(os.path.join(os.path.split(file_path)[0],file_name),'wb')
    chunk = bytearray()
    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()
    try:
        urllib.request.urlretrieve(meta_data['albumPic'], os.path.join(os.path.split(file_path)[0], file_name_no_suffix) + '.jpg');
    except Exception as e:
        print('下载专辑图片出错', e)

def file_extension(path):
    return os.path.splitext(path)[1]

def file_no_extension(path):
    return os.path.splitext(path)[0]

def file_exist(file_name, file_list, file_list_path):
    for file in file_list:
        if (os.path.isdir(os.path.join(file_list_path, file))):
            # print('######### 文件夹, 跳过')
            continue
        for suffix in music_suffix_list:
            if (file_no_extension(file_name) + "." + suffix) == file:
                return True;
    return False;

def recursion(file_name, root_dir, file_list):
    # print('root_dir: ' + root_dir)
    # print('file_name: ' + file_name)
    full_file = os.path.join(root_dir, file_name)
    if os.path.isfile(full_file):
        print('>>>>>>>>>>>>>>>> 当前文件: ' + full_file)
        if file_extension(full_file) == ".ncm":
            # 校验文件存在同名
            if file_exist(file_name, file_list, root_dir):
                print('>>>>>>>>>>>>>>> 同名文件跳过: ' + full_file)
                return
            try:
                print('>>>>>>>>>>>>>>> 开始转码文件: ' + full_file)
                dump(full_file, file_no_extension(file_name));
                print('>>>>>>>>>>>>>>> 转码文件成功: ' + full_file)
            except Exception as err:
                print('转码文件失败: ' + full_file + ' error: ' + err)
        else:
            print('>>>>>>>>>>>>>>> 非ncm文件, 跳过')
    elif os.path.isdir(full_file):
        print('>>>>>>>>>>>>>>> 处理当前文件夹内容: ' + full_file)
        list = os.listdir(full_file)
        for i in range(0, len(list)):
            recursion(list[i], full_file, list)
if __name__ == '__main__':
    import sys
    if len(sys.argv) > 1:
        rootdir = sys.argv[1]; #如果有输入路径参数,取当前路径参数
    else:
        # 没写路径默认走当前文件夹
        rootdir = os.path.split(os.path.realpath(__file__))[0];
    print('>>>>>>>>>>>>>>> 初始路径层级: ' + rootdir)
    list = os.listdir(rootdir) # Get all files in folder.is
    for i in range(0,len(list)):
        try:
            recursion(list[i], rootdir, list)
        except Exception as e:
            print('递归处理出现错误');
            logging.exception(e)
        finally:
            pass

    print('全部文件处理完成 ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()));


https://stackoverflow.com/questions/19623267/importerror-no-module-named-crypto-cipher

pip3 uninstall crypto

pip3 uninstall pycrypto

pip3 install pycryptodome

然后就可以了

python3 tr.py

一个目录下的文件会被自动转换掉

fork from:https://github.com/waterkokoro/ncmTranslatorScript_python


 https://github.com/lemonhall/ncmTran_mac 

里面有ZIP包可以直接用

pip install pyinstaller
pyinstaller --onefile .\tr.py

打包方式为以上

运行方式是:

.\tr.exe ./