diff --git a/main.py b/main.py index 19315be..6dc8e6e 100644 --- a/main.py +++ b/main.py @@ -4,14 +4,14 @@ # author: David-123 -from modules.inputs import rinput -from modules.information import print_info -from modules.multi_download import mdl -from modules.one_download import download_one_lyric -from modules.settings import settings_menu -from modules.save_load_settings import load_settings -from modules.clear_screen import clear -from modules.load_file_song import get_lyric_from_folder +from modules.utils.inputs import rinput +from modules.utils.information import print_info +from modules.functions.multi_download import mdl +from modules.functions.one_download import download_one_lyric +from modules.submenus.settings import settings_menu +from modules.functions.save_load_settings import load_settings +from modules.utils.clear_screen import clear +from modules.functions.load_file_song import get_lyric_from_folder class MainProcess(object): @@ -30,11 +30,11 @@ class MainProcess(object): r = rinput("请选择:") if r == "1": - download_one_lyric(self.settings.lyric_path) + download_one_lyric(self) elif r == "2": - mdl(self.settings.lyric_path) + mdl(self) elif r == "3": - get_lyric_from_folder(self.settings.lyric_path) + get_lyric_from_folder(self) elif r == "0": exit(0) elif r == "i": diff --git a/modules/functions/__init__.py b/modules/functions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/get_song.py b/modules/functions/get_song.py similarity index 100% rename from modules/get_song.py rename to modules/functions/get_song.py diff --git a/modules/load_file_song.py b/modules/functions/load_file_song.py similarity index 94% rename from modules/load_file_song.py rename to modules/functions/load_file_song.py index 68992f2..bd5c454 100644 --- a/modules/load_file_song.py +++ b/modules/functions/load_file_song.py @@ -1,298 +1,306 @@ -import binascii -import json -import os -import struct -from base64 import b64decode -from multiprocessing import Process, Queue -from queue import Empty - -from progress.bar import Bar -from Cryptodome.Cipher import AES -from mutagen import File, flac -from mutagen.id3 import ID3, TPE1, APIC, COMM, TIT2, TALB - -from modules.clear_screen import clear -from modules.get_song import get_song_lyric -from modules.inputs import cinput, rinput - - -def load_information_from_song(path): - """从音乐文件中的 Comment 字段获取 163 key 并解密返回歌曲信息""" - file = File(path) # 使用 mutagen 获取歌曲信息 - if os.path.splitext(path)[-1] == ".mp3": # 当文件为 mp3 时使用 ID3 格式读取 - 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" - elif os.path.splitext(path)[-1] == ".flac": # 当文件为 flac 时使用 FLAC 格式读取 - if file.tags.get("DESCRIPTION"): - if file.tags["DESCRIPTION"][0][:7] == "163 key": - ciphertext = file.tags["DESCRIPTION"][0][22:] - else: - return "not_support" - 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('检测队列->检测任务完成 的循环 - if current_process <= max_process and finished < total: # 分配进程 - Process(target=process_work, - args=(os.path.join(path, ncm_files[finished]), - ncm_files[finished], - target_path, - q_err, - q_info)).start() - finished += 1 - while True: # 错误队列检测 - try: - errors.append(q_err.get_nowait()) - passed += 1 # 总任务完成数 - current_process -= 1 # 检测到进程完毕将进程-1 - bar.next() # 推动进度条 - fails += 1 # 错误数量+1 - except Empty: - break - while True: # 信息队列检测 - try: - r = q_info.get_nowait() - musics.append({"id": r['musicId'], "name": r["musicName"], "artists": r["artist"]}) - passed += 1 - current_process -= 1 - bar.next() - except Empty: - break - if passed >= len(ncm_files): - break - if errors: - print("解密过程中发现了以下错误:") - for i in errors: - print(i) - - # 汇报索引结果 - 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 = rinput("请选择: ") - if r == "1": - lyric_path = path - break - elif r == "2": - 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], lyric_path, allinfo=True) == "dl_err_connection": - input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...") - if ncm_files: - if target_path != "NOT_DECRYPT": - agree = rinput("是否删除原ncm文件? (y/n)") - if agree == "y": - for i in range(0, len(ncm_files)): - print("删除进度: %d/%d\n -> %s\033[F" % (i + 1, len(ncm_files), ncm_files[i]), end="") - os.remove(os.path.join(path, ncm_files[i])) - else: - print("取消.", end="") - input("\n\033[K按回车返回...") - return +import binascii +import json +import os +import struct +from base64 import b64decode +from multiprocessing import Process, Queue +from queue import Empty + +from progress.bar import Bar +from Cryptodome.Cipher import AES +from mutagen import File, flac +from mutagen.id3 import ID3, TPE1, APIC, COMM, TIT2, TALB + +from modules.utils.clear_screen import clear +from modules.functions.get_song import get_song_lyric +from modules.utils.inputs import cinput, rinput + + +def load_information_from_song(path): + """从音乐文件中的 Comment 字段获取 163 key 并解密返回歌曲信息""" + file = File(path) # 使用 mutagen 获取歌曲信息 + if os.path.splitext(path)[-1] == ".mp3": # 当文件为 mp3 时使用 ID3 格式读取 + 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" + elif os.path.splitext(path)[-1] == ".flac": # 当文件为 flac 时使用 FLAC 格式读取 + if file.tags.get("DESCRIPTION"): + if file.tags["DESCRIPTION"][0][:7] == "163 key": + ciphertext = file.tags["DESCRIPTION"][0][22:] + else: + return "not_support" + 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('检测队列->检测任务完成 的循环 + if current_process <= max_process and finished < total: # 分配进程 + Process(target=process_work, + args=(os.path.join(path, ncm_files[finished]), + ncm_files[finished], + target_path, + q_err, + q_info)).start() + finished += 1 + while True: # 错误队列检测 + try: + errors.append(q_err.get_nowait()) + passed += 1 # 总任务完成数 + current_process -= 1 # 检测到进程完毕将进程-1 + bar.next() # 推动进度条 + fails += 1 # 错误数量+1 + except Empty: + break + while True: # 信息队列检测 + try: + r = q_info.get_nowait() + musics.append({"id": r['musicId'], "name": r["musicName"], "artists": r["artist"]}) + passed += 1 + current_process -= 1 + bar.next() + except Empty: + break + if passed >= len(ncm_files): + break + if errors: + print("解密过程中发现了以下错误:") + for i in errors: + print(i) + + # 汇报索引结果 + ncm_files_num = 0 + if ncm_files: + if target_path == "NOT_DECRYPT": + ncm_files_num = len(ncm_files) + print(f"\n索引完毕!共找到{fails + len(musics) + ncm_files_num}个目标文件\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 = rinput("请选择: ") + if r == "1": + lyric_path = path + break + elif r == "2": + 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], lyric_path, allinfo=True) == "dl_err_connection": + input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...") + if ncm_files: + if target_path != "NOT_DECRYPT": + agree = rinput("是否删除原ncm文件? (y/n)") + if agree == "y": + for i in range(0, len(ncm_files)): + print("删除进度: %d/%d\n -> %s\033[F" % (i + 1, len(ncm_files), ncm_files[i]), end="") + os.remove(os.path.join(path, ncm_files[i])) + else: + print("取消.", end="") + input("\n\033[K按回车返回...") + return diff --git a/modules/multi_download.py b/modules/functions/multi_download.py similarity index 71% rename from modules/multi_download.py rename to modules/functions/multi_download.py index b106d30..5cb0a23 100644 --- a/modules/multi_download.py +++ b/modules/functions/multi_download.py @@ -1,36 +1,38 @@ -import re -from modules.clear_screen import clear -from modules.inputs import rinput -from modules.get_song import get_song_lyric - - -def mdl(path: str): - """多个歌词文件的下载 - - ``path: str`` 传入歌词文件保存的路径""" - clear() - ids = [] - print("输入歌曲id,用回车分开,输入s停止") - while True: - r = rinput() - if r == 's': - break - else: - try: - int(r) - except ValueError: - tmp = re.search(r"song\?id=[0-9]*", r) - if tmp: - r = tmp.group()[8:] - else: - print("不合法的形式.\n") - continue - ids.append(r) - print("\t#%d id:%s - 已添加!" % (len(ids), r)) - clear() - for i in range(0, len(ids)): - print("进度: %d/%d" % (i+1, len(ids))) - r = get_song_lyric(ids[i], path) - if r == "dl_err_connection": - input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...") - input("按回车键返回...") +import re +from modules.utils.clear_screen import clear +from modules.utils.inputs import rinput +from modules.functions.get_song import get_song_lyric + + +def mdl(self): + """多个歌词文件的下载 + + ``path: str`` 传入歌词文件保存的路径""" + clear() + ids = [] + print(f"[NeteaseMusicLyricDownloader] {self.version}\n" + "[手动-多个下载]\n" + "输入歌曲id,用回车分开,输入s停止") + while True: + r = rinput() + if r == 's': + break + else: + try: + int(r) + except ValueError: + tmp = re.search(r"song\?id=[0-9]*", r) + if tmp: + r = tmp.group()[8:] + else: + print("不合法的形式.\n") + continue + ids.append(r) + print("\t#%d id:%s - 已添加!" % (len(ids), r)) + clear() + for i in range(0, len(ids)): + print("进度: %d/%d" % (i+1, len(ids))) + r = get_song_lyric(ids[i], self.settings.lyric_path) + if r == "dl_err_connection": + input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...") + input("按回车键返回...") diff --git a/modules/one_download.py b/modules/functions/one_download.py similarity index 55% rename from modules/one_download.py rename to modules/functions/one_download.py index 9838525..3122c86 100644 --- a/modules/one_download.py +++ b/modules/functions/one_download.py @@ -1,15 +1,18 @@ import re -from modules.inputs import rinput -from modules.get_song import get_song_lyric -from modules.clear_screen import clear +from modules.utils.inputs import rinput +from modules.functions.get_song import get_song_lyric +from modules.utils.clear_screen import clear -def download_one_lyric(path: str): +def download_one_lyric(self): """单次下载歌词 ``path: str`` 存储歌词的路径""" clear() - song_id = rinput("请输入歌曲id:") + song_id = rinput( + f"[NeteaseMusicLyricDownloader] {self.version}\n" + "[手动-单个下载]\n" + "请输入歌曲id:") try: int(song_id) except ValueError: @@ -20,6 +23,6 @@ def download_one_lyric(path: str): input("不合法的形式.\n按回车键返回...") return - if get_song_lyric(song_id, path) == "dl_err_connection": + if get_song_lyric(song_id, self.settings.lyric_path) == "dl_err_connection": input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键返回...") input("按回车键返回...") diff --git a/modules/save_load_settings.py b/modules/functions/save_load_settings.py similarity index 100% rename from modules/save_load_settings.py rename to modules/functions/save_load_settings.py diff --git a/modules/submenus/__init__.py b/modules/submenus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/settings.py b/modules/submenus/settings.py similarity index 85% rename from modules/settings.py rename to modules/submenus/settings.py index 0ec3b3d..6d1edc0 100644 --- a/modules/settings.py +++ b/modules/submenus/settings.py @@ -1,9 +1,9 @@ """集合设置参数""" import os -from modules.clear_screen import clear -from modules.inputs import rinput, cinput -from modules.save_load_settings import save_settings +from modules.utils.clear_screen import clear +from modules.utils.inputs import rinput, cinput +from modules.functions.save_load_settings import save_settings def settings_menu(self): @@ -36,7 +36,7 @@ def __remove_output_files(self): clear() print(f"[NeteaseMusicLyricDownloader] {self.version}\n" "[设置菜单 - 删除文件]\n" - "[0] 返回上级\n[1] 清除歌词文件\n[2] 清除歌曲文件") + "[0] 返回上级\n[1] 清除歌词文件\n[2] 清除歌曲文件\n[a] 清除所有文件") r = rinput("请选择:") # 选择清除的文件格式 if r == "0": return @@ -46,14 +46,20 @@ def __remove_output_files(self): elif r == "2": dellist = [".mp3", ".flac"] break + elif r == "a": + dellist = ["ALL"] + break else: input("输入无效!\n按回车键继续...") files = [] for i in os.listdir(self.settings.lyric_path): # 列出所有文件 - if os.path.splitext(i)[-1] in dellist: # 匹配文件 + if dellist[0] == "ALL": + files = os.listdir(self.settings.lyric_path) + break + elif os.path.splitext(i)[-1] in dellist: # 匹配文件 files.append(i) # 将匹配到的文件加入到列表, 等待删除 if len(files) != 0: - if len(files) > 50: + if len(files) > 30: special_text = "\033[F" else: special_text = "\n" diff --git a/modules/utils/__init__.py b/modules/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/clear_screen.py b/modules/utils/clear_screen.py similarity index 95% rename from modules/clear_screen.py rename to modules/utils/clear_screen.py index 8722697..7a9b7f5 100644 --- a/modules/clear_screen.py +++ b/modules/utils/clear_screen.py @@ -1,12 +1,12 @@ -"""包含一个函数,用来清空命令行的信息,自动判别系统""" -import os - - -def clear(): - name = os.name - if name == "nt": - os.system("cls") - elif name == "posix": - os.system("clear") - else: +"""包含一个函数,用来清空命令行的信息,自动判别系统""" +import os + + +def clear(): + name = os.name + if name == "nt": + os.system("cls") + elif name == "posix": + os.system("clear") + else: os.system("clear") \ No newline at end of file diff --git a/modules/information.py b/modules/utils/information.py similarity index 92% rename from modules/information.py rename to modules/utils/information.py index 8146368..b23177c 100644 --- a/modules/information.py +++ b/modules/utils/information.py @@ -1,24 +1,24 @@ -"""该程序的自述信息,调用即输出""" -from modules.clear_screen import clear - - -def print_info(self): - """调用即输出,无返回值""" - clear() - print(f"""[NeteaseMusicLyricDownloader] -版本: {self.version} -本软件开源,项目地址:https://github.com/1826013250/NeteaseMusicLyricDownloader -作者:David-123 -联系方式: -\tQQ:1826013250 -\tE-mail:1826013250@qq.com(mainly) -\t mc1826013250@gmail.com - -特别感谢: -\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 +"""该程序的自述信息,调用即输出""" +from modules.utils.clear_screen import clear + + +def print_info(self): + """调用即输出,无返回值""" + clear() + print(f"""[NeteaseMusicLyricDownloader] +版本: {self.version} +本软件开源,项目地址:https://github.com/1826013250/NeteaseMusicLyricDownloader +作者:David-123 +联系方式: +\tQQ:1826013250 +\tE-mail:1826013250@qq.com(mainly) +\t mc1826013250@gmail.com + +特别感谢: +\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/inputs.py b/modules/utils/inputs.py similarity index 97% rename from modules/inputs.py rename to modules/utils/inputs.py index 1d16ff3..2b15ab0 100644 --- a/modules/inputs.py +++ b/modules/utils/inputs.py @@ -1,11 +1,11 @@ -"""该模块提供几个自定义处理输入函数""" - - -def rinput(string: str = ''): - """当调用该函数时,同input()一样,但是返回一个去除首尾空格并全部小写的str""" - return input(string).strip().lower() - - -def cinput(string: str = ''): - """当调用该函数时,同input()一样,但是返回一个去除首尾空格的str""" - return input(string).strip() +"""该模块提供几个自定义处理输入函数""" + + +def rinput(string: str = ''): + """当调用该函数时,同input()一样,但是返回一个去除首尾空格并全部小写的str""" + return input(string).strip().lower() + + +def cinput(string: str = ''): + """当调用该函数时,同input()一样,但是返回一个去除首尾空格的str""" + return input(string).strip() diff --git a/modules/raw_input.py b/modules/utils/raw_input.py similarity index 97% rename from modules/raw_input.py rename to modules/utils/raw_input.py index 6597fac..cca11f3 100644 --- a/modules/raw_input.py +++ b/modules/utils/raw_input.py @@ -1,11 +1,11 @@ -"""该模块提供几个自定义处理输入函数""" - - -def rinput(string: str = ''): - """当调用该函数时,同input()一样,但是返回一个去除首位空格并全部小写的str""" - return input(string).strip().lower() - - -def cinput(string: str = ''): - """当调用该函数时,同input()一样,但是返回一个去除首尾空格的str""" +"""该模块提供几个自定义处理输入函数""" + + +def rinput(string: str = ''): + """当调用该函数时,同input()一样,但是返回一个去除首位空格并全部小写的str""" + return input(string).strip().lower() + + +def cinput(string: str = ''): + """当调用该函数时,同input()一样,但是返回一个去除首尾空格的str""" return input(string).strip() \ No newline at end of file