Add new functions!!!
Change Special Thanks ADD FUNCTION: use the source code from ncmdump and modify it to satisfy my needs
This commit is contained in:
parent
ffdd69000c
commit
35cdec2c37
0
modules/__init__.py
Normal file
0
modules/__init__.py
Normal file
@ -1,4 +1,5 @@
|
|||||||
"""集合 下载歌词 以及 获取歌曲信息 的功能"""
|
"""集合 下载歌词 以及 获取歌曲信息 的功能"""
|
||||||
|
import os
|
||||||
from json import loads
|
from json import loads
|
||||||
from requests import post
|
from requests import post
|
||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
@ -90,6 +91,7 @@ def get_song_lyric(id: str | int | dict, path: str, allinfo: bool = False):
|
|||||||
}
|
}
|
||||||
for k, v in replaces.items():
|
for k, v in replaces.items():
|
||||||
name = name.replace(k, v)
|
name = name.replace(k, v)
|
||||||
|
artists = artists.replace(k, v)
|
||||||
|
|
||||||
print(f"歌曲:{name} - {artists}")
|
print(f"歌曲:{name} - {artists}")
|
||||||
filename = f"{name} - {artists}.lrc"
|
filename = f"{name} - {artists}.lrc"
|
||||||
@ -114,7 +116,7 @@ def get_song_lyric(id: str | int | dict, path: str, allinfo: bool = False):
|
|||||||
print("这首歌没有歌词,跳过...")
|
print("这首歌没有歌词,跳过...")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
with open(f"{path}{filename}", "w", encoding="utf-8") as f:
|
with open(os.path.join(path, filename), "w", encoding="utf-8") as f:
|
||||||
f.write(tmp["lyric"])
|
f.write(tmp["lyric"])
|
||||||
print(f"歌词下载完成!被保存在{path}{filename}")
|
print(f"歌词下载完成!被保存在{os.path.join(path, filename)}")
|
||||||
return
|
return
|
||||||
|
@ -5,7 +5,7 @@ from modules.clear_screen import clear
|
|||||||
def print_info(self):
|
def print_info(self):
|
||||||
"""调用即输出,无返回值"""
|
"""调用即输出,无返回值"""
|
||||||
clear()
|
clear()
|
||||||
print(f"""[NeteaseMusicLyricDownloader Reloaded]
|
print(f"""[NeteaseMusicLyricDownloader]
|
||||||
版本: {self.version}
|
版本: {self.version}
|
||||||
本软件开源,项目地址:https://github.com/1826013250/NeteaseMusicLyricDownloader
|
本软件开源,项目地址:https://github.com/1826013250/NeteaseMusicLyricDownloader
|
||||||
作者:David-123
|
作者:David-123
|
||||||
@ -14,7 +14,11 @@ def print_info(self):
|
|||||||
\tE-mail:1826013250@qq.com(mainly)
|
\tE-mail:1826013250@qq.com(mainly)
|
||||||
\t mc1826013250@gmail.com
|
\t mc1826013250@gmail.com
|
||||||
|
|
||||||
Special Thanks:
|
特别感谢:
|
||||||
\t- chenjunyu19, provided the 163key's cipher
|
\t- nondanee - ncmdump https://github.com/nondanee/ncmdump
|
||||||
\t- website 'MKLAB', provided the AES decryption service""")
|
\t- QCloudHao - 保存了完整的ncmdump源代码 https://github.com/QCloudHao/ncmdump
|
||||||
input("按回车键返回...")
|
\t- chuyaoxin - 提供了对ncmdump以及ncm文件的详细解说 https://www.cnblogs.com/cyx-b/p/13443003.html
|
||||||
|
|
||||||
|
若程序遇到bug请提交至github上的issue""")
|
||||||
|
input("按回车键返回...")
|
||||||
|
return
|
||||||
|
230
modules/load_file_song.py
Normal file
230
modules/load_file_song.py
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import binascii
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
from base64 import b64decode
|
||||||
|
|
||||||
|
from Cryptodome.Cipher import AES
|
||||||
|
from mutagen import File
|
||||||
|
from mutagen.id3 import ID3, TPE1, APIC, COMM, TIT2, TALB
|
||||||
|
|
||||||
|
from modules.clear_screen import clear
|
||||||
|
from modules.get_song import get_song_lyric
|
||||||
|
|
||||||
|
|
||||||
|
def load_information_from_song(path):
|
||||||
|
"""从音乐文件中的 Comment 字段获取 163 key 并解密返回歌曲信息"""
|
||||||
|
file = File(path) # 使用 TinyTag 获取歌曲信息
|
||||||
|
if file.tags.get("COMM::XXX"):
|
||||||
|
if file.tags["COMM::XXX"].text[0][:7] == "163 key":
|
||||||
|
ciphertext = file.tags["COMM::XXX"].text[0][22:]
|
||||||
|
else:
|
||||||
|
return "not_support"
|
||||||
|
else:
|
||||||
|
return "not_support"
|
||||||
|
|
||||||
|
def unpad(s): # 创建清理针对于网易云的 AES-128-ECB 解密后末尾占位符的函数
|
||||||
|
if type(s[-1]) == int:
|
||||||
|
end = s[-1]
|
||||||
|
else:
|
||||||
|
end = ord(s[-1])
|
||||||
|
return s[0:-end]
|
||||||
|
# return s[0:-(s[-1] if type(s[-1]) == int else ord(s[-1]))] 更加清晰的理解 ↑
|
||||||
|
cryptor = AES.new(b"#14ljk_!\\]&0U<'(", AES.MODE_ECB) # 使用密钥创建解密器
|
||||||
|
|
||||||
|
# 下方这一行将密文 ciphertext 转换为 bytes 后进行 base64 解码, 得到加密过的 AES 密文
|
||||||
|
# 再通过上方创建的 AES 128-ECB 的解密器进行解密, 然后使用 unpad 清除末尾无用的占位符后得到结果
|
||||||
|
try:
|
||||||
|
r = unpad((cryptor.decrypt(b64decode(bytes(ciphertext, "utf-8"))).decode("utf-8")))
|
||||||
|
except ValueError:
|
||||||
|
return "decrypt_failed"
|
||||||
|
|
||||||
|
if r:
|
||||||
|
if r[:5] == "music":
|
||||||
|
return json.loads(r[6:])
|
||||||
|
else:
|
||||||
|
return "not_a_normal_music"
|
||||||
|
else:
|
||||||
|
return "decrypt_failed"
|
||||||
|
|
||||||
|
|
||||||
|
def load_and_decrypt_from_ncm(file_path, targetdir): # nondanee的源代码, 根据需求更改了某些东西
|
||||||
|
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)
|
||||||
|
comment = meta_data
|
||||||
|
meta_data = 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 = f.name.split("/")[-1].split(".ncm")[0] + '.' + meta_data['format']
|
||||||
|
m = open(os.path.join(targetdir, 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()
|
||||||
|
|
||||||
|
# 对解密后的文件进行信息补全
|
||||||
|
audio = ID3(os.path.splitext(file_path)[0]+"."+meta_data["format"])
|
||||||
|
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()
|
||||||
|
|
||||||
|
return meta_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_lyric_from_folder(lyric_path: str):
|
||||||
|
clear()
|
||||||
|
path = input("请输入歌曲的保存文件夹(绝对路径):").strip()
|
||||||
|
if not os.path.exists(path):
|
||||||
|
input("路径不存在.\n按回车返回...")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("正在遍历目录,请稍后...")
|
||||||
|
musics = []
|
||||||
|
ncm_files = []
|
||||||
|
fails = 0
|
||||||
|
for i in os.listdir(path): # 遍历目录,查找目标文件
|
||||||
|
ext = os.path.splitext(i)[-1]
|
||||||
|
if ext in ['.mp3', '.flac']: # 对于 mp3 和 flac 文件, 使用对应读取解密方式
|
||||||
|
result = load_information_from_song(os.path.join(path, i))
|
||||||
|
if result == "not_support":
|
||||||
|
fails += 1
|
||||||
|
print(f"文件 \"{i}\" 未包含 163 key ,跳过")
|
||||||
|
elif result == "decrypt_failed":
|
||||||
|
fails += 1
|
||||||
|
print(f"文件 \"{i}\" 内 163 key 解密失败,跳过")
|
||||||
|
elif result == "not_a_normal_music":
|
||||||
|
fails += 1
|
||||||
|
print(f"文件 \"{i}\" 内 163 key 不是一个普通音乐文件,这可能是一个电台曲目")
|
||||||
|
else:
|
||||||
|
musics.append({"id": result['musicId'], "name": result["musicName"], "artists": result["artist"]})
|
||||||
|
elif ext == ".ncm": # 对于 ncm 先加入到列表,等待解密
|
||||||
|
ncm_files.append(i)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if ncm_files:
|
||||||
|
while True:
|
||||||
|
print(f"\n发现{len(ncm_files)}个ncm加密文件!")
|
||||||
|
print("请问解密后的文件保存在哪里?\n"
|
||||||
|
"[1] 保存在相同文件夹内\n[2] 保存在程序设定的下载文件夹中\n[3] 保存在自定义文件夹内\n[q] 取消解密,下载歌词时将忽略这些文件")
|
||||||
|
select = input("请选择: ").strip().lower()
|
||||||
|
if select == 'q':
|
||||||
|
target_path = "NOT_DECRYPT"
|
||||||
|
break
|
||||||
|
elif select == '1':
|
||||||
|
target_path = path
|
||||||
|
break
|
||||||
|
elif select == '2':
|
||||||
|
target_path = lyric_path
|
||||||
|
break
|
||||||
|
elif select == '3':
|
||||||
|
target_path = input("请输入: ").strip()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("输入无效!按回车继续...")
|
||||||
|
|
||||||
|
if target_path != "NOT_DECRYPT":
|
||||||
|
for i in range(0, len(ncm_files)):
|
||||||
|
print("破解进度: %d/%d ~ %s" % (i+1, len(ncm_files), ncm_files[i]))
|
||||||
|
try:
|
||||||
|
result = load_and_decrypt_from_ncm(os.path.join(path, ncm_files[i]), target_path)
|
||||||
|
except AssertionError:
|
||||||
|
print(f"\t- 文件 \"{ncm_files[i]}\" 破解失败!\n\t 可能是文件不完整或者重命名了别的文件?跳过...")
|
||||||
|
fails += 1
|
||||||
|
continue
|
||||||
|
print("\t--> %s" % os.path.splitext(ncm_files[i])[0]+"."+result["format"])
|
||||||
|
musics.append({"id": result['musicId'], "name": result["musicName"], "artists": result["artist"]})
|
||||||
|
|
||||||
|
# 汇报索引结果
|
||||||
|
print(f"\n索引完毕!共找到{fails + len(musics) + len(ncm_files)}个目标文件\n{len(musics)}个文件已载入\n{fails}个文件失败")
|
||||||
|
if ncm_files:
|
||||||
|
if target_path == "NOT_DECRYPT":
|
||||||
|
print(f"{len(ncm_files)}个文件放弃加载")
|
||||||
|
while True:
|
||||||
|
print("\n你希望如何保存这些歌曲的歌词?\n[1]保存到刚刚输入的绝对路径中\n[2]保存到程序设定的下载路径中")
|
||||||
|
r = input("请选择: ").strip().lower()
|
||||||
|
if r == "1":
|
||||||
|
break
|
||||||
|
elif r == "2":
|
||||||
|
path = lyric_path
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
input("无效选择, 若取消请按 ^C ,继续请按回车")
|
||||||
|
clear()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return
|
||||||
|
|
||||||
|
clear()
|
||||||
|
for i in range(0, len(musics)): # 根据索引结果获取歌词
|
||||||
|
print("\n进度: %d/%d" % (i + 1, len(musics)))
|
||||||
|
if get_song_lyric(musics[i], path, allinfo=True) == "dl_err_connection":
|
||||||
|
input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...")
|
||||||
|
if ncm_files:
|
||||||
|
if target_path != "NOT_DECRYPT":
|
||||||
|
agree = input("是否删除原ncm文件? (y/n)").strip().lower()
|
||||||
|
if agree == "y":
|
||||||
|
for i in range(0, len(ncm_files)):
|
||||||
|
print("删除进度: %d/%d ~ %s" % (i+1, len(ncm_files), ncm_files[i]))
|
||||||
|
os.remove(os.path.join(path, ncm_files[i]))
|
||||||
|
else:
|
||||||
|
print("取消.")
|
||||||
|
input("按回车返回...")
|
||||||
|
return
|
@ -4,7 +4,7 @@ from modules.get_song import get_song_lyric
|
|||||||
from modules.clear_screen import clear
|
from modules.clear_screen import clear
|
||||||
|
|
||||||
|
|
||||||
def download_one_lyric(song_id, path: str):
|
def download_one_lyric(path: str):
|
||||||
"""单次下载歌词
|
"""单次下载歌词
|
||||||
|
|
||||||
``path: str`` 存储歌词的路径"""
|
``path: str`` 存储歌词的路径"""
|
||||||
|
@ -11,7 +11,7 @@ def settings_menu(self):
|
|||||||
"""设置菜单主循环"""
|
"""设置菜单主循环"""
|
||||||
while True:
|
while True:
|
||||||
clear()
|
clear()
|
||||||
print(f"[NeteaseMusicLyricDownloader Reloaded] {self.version}\n"
|
print(f"[NeteaseMusicLyricDownloader] {self.version}\n"
|
||||||
"[设置菜单]\n"
|
"[设置菜单]\n"
|
||||||
"[0] 返回上级\n[1] 设置歌曲保存路径\n[2] 清空输出文件夹内的所有歌词\n[s] 将设置保存到文件")
|
"[0] 返回上级\n[1] 设置歌曲保存路径\n[2] 清空输出文件夹内的所有歌词\n[s] 将设置保存到文件")
|
||||||
r = rinput("请选择:")
|
r = rinput("请选择:")
|
||||||
|
Loading…
Reference in New Issue
Block a user