NCMDUMP UPDATE
Make menu more colorful! Changed NCMDUMP code, 1000% faster!! Settings update and small fix
This commit is contained in:
parent
94149eabea
commit
bee6f61c38
12
main.py
12
main.py
@ -8,7 +8,7 @@ from sys import exit
|
||||
from colorama import init
|
||||
|
||||
from modules.utils.inputs import rinput
|
||||
from modules.utils.information import print_info
|
||||
from modules.utils.prints import print_info, print_menu
|
||||
from modules.functions.mainly.multi_download import mdl
|
||||
from modules.functions.mainly.one_download import download_one_lyric
|
||||
from modules.submenus.settings import settings_menu
|
||||
@ -26,8 +26,14 @@ class MainProcess(object):
|
||||
"""程序主循环"""
|
||||
while True:
|
||||
cls_stay(self, "[程序主菜单]")
|
||||
print("[0] 退出程序\n[1] 单个歌曲的歌词下载\n[2] 多个歌曲的歌词下载\n[3] 从网易云下载的歌曲中获取歌词"
|
||||
"\n[s] 进入设置\n[i] 程序信息")
|
||||
print_menu({
|
||||
"0": "退出程序",
|
||||
"1": "单个歌曲的歌词下载",
|
||||
"2": "多个歌曲的歌词下载",
|
||||
"3": "从网易云下载的歌曲中获取歌词",
|
||||
"s": "进入设置",
|
||||
"i": "程序信息",
|
||||
})
|
||||
r = rinput("请选择:")
|
||||
|
||||
if r == "1":
|
||||
|
@ -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])
|
||||
|
@ -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"
|
||||
|
@ -1,17 +1,29 @@
|
||||
"""集合设置参数"""
|
||||
|
||||
import os
|
||||
from colorama import Fore
|
||||
from modules.utils.clear_screen import cls_stay
|
||||
from modules.utils.inputs import rinput, cinput
|
||||
from modules.functions.settings.save_load_settings import save_settings
|
||||
from modules.utils.prints import print_menu
|
||||
|
||||
|
||||
def settings_menu(self):
|
||||
"""设置菜单主循环"""
|
||||
while True:
|
||||
cls_stay(self, "[设置菜单]")
|
||||
print("[0] 返回上级\n[1] 歌曲保存路径\n[2] 清空输出文件夹内的内容\n[3] 歌词文件保存格式\n[4] 部分动态效果\n"
|
||||
"[s] 将设置保存到文件")
|
||||
if self.settings.auto_save:
|
||||
save_settings(self.settings)
|
||||
cls_stay(self, f"[设置菜单] "
|
||||
f"{Fore.LIGHTCYAN_EX}自动保存: "
|
||||
f"{({True: f'{Fore.GREEN}开', False: f'{Fore.RED}关'}[self.settings.auto_save])}")
|
||||
print_menu({
|
||||
"0": "返回上级菜单",
|
||||
"1": "歌曲保存路径",
|
||||
"2": "清空输出文件夹内的内容",
|
||||
"3": "歌词文件保存格式",
|
||||
"4": "部分动态效果",
|
||||
"s": "切换设置自动保存"
|
||||
})
|
||||
r = rinput("请选择:")
|
||||
if r == "0":
|
||||
return
|
||||
@ -24,7 +36,7 @@ def settings_menu(self):
|
||||
elif r == "4":
|
||||
pass
|
||||
elif r == "s":
|
||||
__save_settings(self)
|
||||
self.settings.auto_save = not self.settings.auto_save
|
||||
else:
|
||||
input("输入无效!按回车键继续...")
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""用来清空命令行的信息,自动判别系统"""
|
||||
import os
|
||||
from colorama import Fore
|
||||
|
||||
|
||||
def clear():
|
||||
@ -15,5 +16,5 @@ def clear():
|
||||
def cls_stay(self, custom=""):
|
||||
"""保留版本号清除屏幕"""
|
||||
clear()
|
||||
print(f"[NeteaseMusicLyricDownloader] {self.version}")
|
||||
print(custom)
|
||||
print(f"{Fore.YELLOW}[{Fore.GREEN}NeteaseMusicLyricDownloader{Fore.YELLOW}] {Fore.LIGHTBLACK_EX}{self.version}")
|
||||
print(Fore.LIGHTMAGENTA_EX+custom)
|
||||
|
131
modules/utils/dump.py
Normal file
131
modules/utils/dump.py
Normal file
@ -0,0 +1,131 @@
|
||||
import binascii
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import base64
|
||||
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Util.Padding import unpad
|
||||
from Cryptodome.Util.strxor import strxor as xor
|
||||
from mutagen import mp3, flac, id3
|
||||
|
||||
|
||||
def load_and_decrypt_from_ncm(file_path, target_dir, out_format) -> dict | str: # author: Nzix Repo: nondanee
|
||||
|
||||
core_key = binascii.a2b_hex('687A4852416D736F356B496E62617857')
|
||||
meta_key = binascii.a2b_hex('2331346C6A6B5F215C5D2630553C2728')
|
||||
|
||||
f = open(file_path, 'rb')
|
||||
|
||||
# magic header
|
||||
header = f.read(8)
|
||||
assert binascii.b2a_hex(header) == b'4354454e4644414d'
|
||||
|
||||
f.seek(2, 1)
|
||||
|
||||
# key data
|
||||
key_length = f.read(4)
|
||||
key_length = struct.unpack('<I', bytes(key_length))[0]
|
||||
|
||||
key_data = bytearray(f.read(key_length))
|
||||
key_data = bytes(bytearray([byte ^ 0x64 for byte in key_data]))
|
||||
|
||||
cryptor = AES.new(core_key, AES.MODE_ECB)
|
||||
key_data = unpad(cryptor.decrypt(key_data), 16)[17:]
|
||||
key_length = len(key_data)
|
||||
|
||||
# S-box (standard RC4 Key-scheduling algorithm)
|
||||
key = bytearray(key_data)
|
||||
S = bytearray(range(256))
|
||||
j = 0
|
||||
|
||||
for i in range(256):
|
||||
j = (j + S[i] + key[i % key_length]) & 0xFF
|
||||
S[i], S[j] = S[j], S[i]
|
||||
|
||||
# meta data
|
||||
meta_length = f.read(4)
|
||||
meta_length = struct.unpack('<I', bytes(meta_length))[0]
|
||||
|
||||
if meta_length:
|
||||
meta_data = bytearray(f.read(meta_length))
|
||||
meta_data = bytes(bytearray([byte ^ 0x63 for byte in meta_data]))
|
||||
identifier = meta_data.decode('utf-8')
|
||||
meta_data = base64.b64decode(meta_data[22:])
|
||||
|
||||
cryptor = AES.new(meta_key, AES.MODE_ECB)
|
||||
meta_data = unpad(cryptor.decrypt(meta_data), 16).decode('utf-8')
|
||||
meta_data = json.loads(meta_data[6:])
|
||||
else:
|
||||
meta_data = {'format': 'flac' if os.fstat(f.fileno()).st_size > 1024 ** 2 * 16 else 'mp3'}
|
||||
|
||||
f.seek(5, 1)
|
||||
|
||||
# album cover
|
||||
image_space = f.read(4)
|
||||
image_space = struct.unpack('<I', bytes(image_space))[0]
|
||||
image_size = f.read(4)
|
||||
image_size = struct.unpack('<I', bytes(image_size))[0]
|
||||
image_data = f.read(image_size) if image_size else None
|
||||
|
||||
f.seek(image_space - image_size, 1)
|
||||
|
||||
# media data
|
||||
if meta_length:
|
||||
output_path = os.path.join(target_dir, out_format % {"name": meta_data["musicName"], "artists": "".join(
|
||||
[x[0]+"," for x in meta_data["artist"]]
|
||||
)[:-1]} + "." + meta_data["format"])
|
||||
else:
|
||||
output_path = os.path.join(target_dir, "Unnamed." + meta_data["format"])
|
||||
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
# stream cipher (modified RC4 Pseudo-random generation algorithm)
|
||||
stream = [S[(S[i] + S[(i + S[i]) & 0xFF]) & 0xFF] for i in range(256)]
|
||||
stream = bytes(bytearray(stream * (len(data) // 256 + 1))[1:1 + len(data)])
|
||||
data = xor(data, stream)
|
||||
|
||||
m = open(output_path, 'wb')
|
||||
m.write(data)
|
||||
m.close()
|
||||
|
||||
# media tag
|
||||
if meta_length:
|
||||
def embed(item, content, t):
|
||||
item.encoding = 0
|
||||
item.type = t
|
||||
item.mime = 'image/png' if content[0:4] == binascii.a2b_hex('89504E47') else 'image/jpeg'
|
||||
item.data = content
|
||||
|
||||
if image_data:
|
||||
if meta_data['format'] == 'flac':
|
||||
audio = flac.FLAC(output_path)
|
||||
image = flac.Picture()
|
||||
embed(image, image_data, 3)
|
||||
audio.clear_pictures()
|
||||
audio.add_picture(image)
|
||||
audio.save()
|
||||
elif meta_data['format'] == 'mp3':
|
||||
audio = mp3.MP3(output_path)
|
||||
image = id3.APIC()
|
||||
embed(image, image_data, 6)
|
||||
audio.tags.add(image)
|
||||
audio.save()
|
||||
|
||||
if meta_length:
|
||||
if meta_data['format'] == 'flac':
|
||||
audio = flac.FLAC(output_path)
|
||||
audio['description'] = identifier
|
||||
else:
|
||||
audio = mp3.EasyMP3(output_path)
|
||||
audio['title'] = 'placeholder'
|
||||
audio.tags.RegisterTextKey('comment', 'COMM')
|
||||
audio['comment'] = identifier
|
||||
audio['title'] = meta_data['musicName']
|
||||
audio['album'] = meta_data['album']
|
||||
audio['artist'] = '/'.join([artist[0] for artist in meta_data['artist']])
|
||||
audio.save()
|
||||
return meta_data
|
||||
else:
|
||||
return "no_meta_data"
|
@ -1,5 +1,6 @@
|
||||
"""该程序的自述信息,调用即输出"""
|
||||
from modules.utils.clear_screen import clear
|
||||
from colorama import Fore
|
||||
|
||||
|
||||
def print_info(self):
|
||||
@ -16,9 +17,14 @@ def print_info(self):
|
||||
|
||||
特别感谢:
|
||||
\t- nondanee - ncmdump https://github.com/nondanee/ncmdump
|
||||
\t- QCloudHao - 保存了完整的ncmdump源代码 https://github.com/QCloudHao/ncmdump
|
||||
\t- chuyaoxin - 提供了对ncmdump以及ncm文件的详细解说 https://www.cnblogs.com/cyx-b/p/13443003.html
|
||||
|
||||
若程序遇到bug请提交至github上的issue""")
|
||||
input("按回车键返回...")
|
||||
return
|
||||
|
||||
|
||||
def print_menu(menu: dict):
|
||||
"""传入一个字典, 格式为 {"需要输入的字符": "功能描述", ...}"""
|
||||
for k, v in menu.items():
|
||||
print(f"{Fore.LIGHTBLUE_EX}[{k}] {Fore.LIGHTWHITE_EX}{v}")
|
Loading…
Reference in New Issue
Block a user