NCMDUMP UPDATE

Make menu more colorful!

Changed NCMDUMP code, 1000% faster!!

Settings update and small fix
This commit is contained in:
2023-06-22 01:36:23 +08:00
parent 94149eabea
commit bee6f61c38
7 changed files with 193 additions and 112 deletions

View File

@@ -1,23 +1,23 @@
import binascii
import json
import os
import struct
from base64 import b64decode
from multiprocessing import Process, Queue
from queue import Empty
from time import sleep
from sys import exit
import mutagen.mp3
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import unpad
from mutagen import File, flac
from mutagen.id3 import ID3, TPE1, APIC, COMM, TIT2, TALB
from mutagen import File
from colorama import Fore, Style
from modules.utils.clear_screen import cls_stay
from modules.functions.mainly.get_song import get_song_lyric
from modules.utils.inputs import cinput, rinput
from modules.utils.bar import CompactBar, CompactArrowBar
from modules.utils.dump import load_and_decrypt_from_ncm
def load_information_from_song(path) -> str | dict:
@@ -27,7 +27,7 @@ def load_information_from_song(path) -> str | dict:
except mutagen.mp3.HeaderNotFoundError:
return "not_a_music"
if os.path.splitext(path)[-1] == ".mp3": # 当文件为 mp3 时使用 ID3 格式读取
if file.tags.get("COMM::XXX"):
if file.tags and file.tags.get("COMM::XXX"):
if file.tags["COMM::XXX"].text[0][:7] == "163 key":
ciphertext = file.tags["COMM::XXX"].text[0][22:]
else:
@@ -63,101 +63,17 @@ def load_information_from_song(path) -> str | dict:
return "decrypt_failed"
def load_and_decrypt_from_ncm(file_path, target_dir) -> dict: # nondanee的源代码, 根据需求更改了某些东西
core_key = binascii.a2b_hex("687A4852416D736F356B496E62617857")
meta_key = binascii.a2b_hex("2331346C6A6B5F215C5D2630553C2728")
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), 16)[17:]
key_length = len(key_data)
key_data = bytearray(key_data)
key_box = bytearray(range(256))
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)
comment = meta_data
meta_data = b64decode(meta_data[22:])
cryptor = AES.new(meta_key, AES.MODE_ECB)
meta_data = unpad(cryptor.decrypt(meta_data), 16).decode('utf-8')[6:]
meta_data = json.loads(meta_data)
crc32 = f.read(4)
crc32 = struct.unpack('<I', bytes(crc32))[0]
..., crc32
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 = f.name.split("/")[-1].split(".ncm")[0] + '.' + meta_data['format']
m = open(os.path.join(target_dir, file_name), 'wb')
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 meta_data["format"] == "mp3": # 针对 mp3 使用 ID3 进行信息补全
audio = ID3(os.path.join(target_dir, os.path.splitext(file_path.split("/")[-1])[0] + ".mp3"))
artists = []
for i in meta_data["artist"]:
artists.append(i[0])
audio["TPE1"] = TPE1(encoding=3, text=artists) # 插入歌手
audio["APIC"] = APIC(encoding=3, mime='image/jpg', type=3, desc='', data=image_data) # 插入封面
audio["COMM::XXX"] = COMM(encoding=3, lang='XXX', desc='', text=[comment.decode("utf-8")]) # 插入 163 key 注释
audio["TIT2"] = TIT2(encoding=3, text=[meta_data["musicName"]]) # 插入歌曲名
audio["TALB"] = TALB(encoding=3, text=[meta_data["album"]]) # 插入专辑名
audio.save()
elif meta_data["format"] == "flac": # 针对 flac 使用 FLAC 进行信息补全
audio = flac.FLAC(os.path.join(target_dir, os.path.splitext(file_path.split("/")[-1])[0] + ".flac"))
artists = []
for i in meta_data["artist"]:
artists.append(i[0])
audio["artist"] = artists[:] # 插入歌手
audio["title"] = [meta_data["musicName"]] # 插入歌曲名
audio["album"] = [meta_data["album"]] # 插入专辑名
audio["description"] = comment.decode("utf-8") # 插入 163 key 注释
audio.save()
return meta_data
def process_work(path, filename, target, q_err: Queue, q_info: Queue):
def process_work(path, filename, target, lyric_format, q_err: Queue, q_info: Queue):
try:
result = load_and_decrypt_from_ncm(path, target)
result = load_and_decrypt_from_ncm(path, target, lyric_format)
except AssertionError:
q_err.put(f"\t- 文件 \"{filename}\" 破解失败!")
except KeyboardInterrupt:
os.remove(target)
exit(-1)
else:
if result == "no_meta_data":
q_err.put(f"\t- 文件 \"{filename}\"破译成功, 但是未发现有效的歌曲信息, 将不会下载该歌词")
q_info.put(result)
@@ -238,6 +154,7 @@ def get_lyric_from_folder(self):
args=(os.path.join(path, ncm_files[allocated]),
ncm_files[allocated],
target_path,
self.settings.lyric_format,
q_err,
q_info)).start()
bar.print_onto_bar(Fore.CYAN + "已分配: " + Style.RESET_ALL + "%s" % ncm_files[allocated])

View File

@@ -5,24 +5,32 @@ import os
class Settings(object): # 设定一个基础的存储设置信息的 class ,并设置形参用于 json 导入设置
def __init__(self, l_p="./out/", l_f="", lang="en"):
def __init__(self, l_p="./out/", l_f="%(name)s - %(artists)s", lang="en", a_s=True):
self.lyric_path = l_p
self.lyric_format = l_f
self.language = lang
self.auto_save = a_s
def class2dict(aclass): # 让 json.dumps 将 class 转化为一个 dict ,用于保存
def class2dict(aclass: Settings): # 让 json.dumps 将 class 转化为一个 dict ,用于保存
return {
"lyric_path": aclass.lyric_path,
"lyric_format": aclass.lyric_format,
"language": aclass.language,
"auto_save": aclass.auto_save
}
def dict2class(adict): # 让 json.load 将读取到的 dict 转化为我们所需要的 class
if len(adict) != 2: # 若检测到多余的设定将抛出异常
if len(adict) != 4: # 若检测到多余的设定将抛出异常
raise json.decoder.JSONDecodeError("Too many keys", "none", 0)
else:
return Settings(adict["lyric_path"], adict["language"])
return Settings(
l_p=adict["lyric_path"],
l_f=adict["lyric_format"],
lang=adict["language"],
a_s=adict["auto_save"]
)
def load_settings() -> Settings: # 加载 的函数
@@ -36,7 +44,7 @@ def load_settings() -> Settings: # 加载 的函数
if not os.path.exists(settings.lyric_path): # 检测输出文件夹,若文件夹不存在则在启动时创建
os.mkdir(settings.lyric_path)
return settings
except json.decoder.JSONDecodeError: # 如果检测到文件无法读取,将会删除设置文件并重新创建
except json.decoder.JSONDecodeError or KeyError: # 如果检测到文件无法读取,将会删除设置文件并重新创建
print("设置文件损坏,重新创建...")
os.remove("settings.json")
return load_settings()
@@ -52,4 +60,4 @@ def save_settings(settings): # 保存 的函数
返回 done 即为完成,理论上不存在报错所以暂时没有做其他处理"""
with open("settings.json", 'w', encoding="utf-8") as f:
f.write(json.dumps(settings, default=class2dict))
input("保存完成!按回车继续...")
return "done"