diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/get_song.py b/modules/get_song.py index 2285cd4..82c039c 100644 --- a/modules/get_song.py +++ b/modules/get_song.py @@ -1,4 +1,5 @@ """集合 下载歌词 以及 获取歌曲信息 的功能""" +import os from json import loads from requests import post 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(): name = name.replace(k, v) + artists = artists.replace(k, v) print(f"歌曲:{name} - {artists}") filename = f"{name} - {artists}.lrc" @@ -114,7 +116,7 @@ def get_song_lyric(id: str | int | dict, path: str, allinfo: bool = False): print("这首歌没有歌词,跳过...") return 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"]) - print(f"歌词下载完成!被保存在{path}{filename}") + print(f"歌词下载完成!被保存在{os.path.join(path, filename)}") return diff --git a/modules/information.py b/modules/information.py index dd97191..8146368 100644 --- a/modules/information.py +++ b/modules/information.py @@ -5,7 +5,7 @@ from modules.clear_screen import clear def print_info(self): """调用即输出,无返回值""" clear() - print(f"""[NeteaseMusicLyricDownloader Reloaded] + print(f"""[NeteaseMusicLyricDownloader] 版本: {self.version} 本软件开源,项目地址:https://github.com/1826013250/NeteaseMusicLyricDownloader 作者:David-123 @@ -14,7 +14,11 @@ def print_info(self): \tE-mail:1826013250@qq.com(mainly) \t mc1826013250@gmail.com -Special Thanks: -\t- chenjunyu19, provided the 163key's cipher -\t- website 'MKLAB', provided the AES decryption service""") - input("按回车键返回...") \ No newline at end of file +特别感谢: +\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 diff --git a/modules/load_file_song.py b/modules/load_file_song.py new file mode 100644 index 0000000..b8350e1 --- /dev/null +++ b/modules/load_file_song.py @@ -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('= 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(' %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 diff --git a/modules/one_download.py b/modules/one_download.py index 1adb591..6b35a41 100644 --- a/modules/one_download.py +++ b/modules/one_download.py @@ -4,7 +4,7 @@ from modules.get_song import get_song_lyric from modules.clear_screen import clear -def download_one_lyric(song_id, path: str): +def download_one_lyric(path: str): """单次下载歌词 ``path: str`` 存储歌词的路径""" diff --git a/modules/settings.py b/modules/settings.py index 5245c02..16063bb 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -11,7 +11,7 @@ def settings_menu(self): """设置菜单主循环""" while True: clear() - print(f"[NeteaseMusicLyricDownloader Reloaded] {self.version}\n" + print(f"[NeteaseMusicLyricDownloader] {self.version}\n" "[设置菜单]\n" "[0] 返回上级\n[1] 设置歌曲保存路径\n[2] 清空输出文件夹内的所有歌词\n[s] 将设置保存到文件") r = rinput("请选择:")