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