COLORFUL UPDATE

Make output more and more and more colorful!

Change file structure and README.md
This commit is contained in:
1826013250 2023-05-07 01:59:03 +08:00
parent e7d19d6892
commit 94149eabea
13 changed files with 198 additions and 137 deletions

20
main.py
View File

@ -3,15 +3,18 @@
# -*- coding: utf-8 -*-
# author: David-123
from sys import exit
from colorama import init
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.functions.mainly.multi_download import mdl
from modules.functions.mainly.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
from modules.functions.settings.save_load_settings import load_settings
from modules.utils.clear_screen import cls_stay
from modules.functions.mainly.load_file_song import get_lyric_from_folder
class MainProcess(object):
@ -22,10 +25,8 @@ class MainProcess(object):
def mainloop(self):
"""程序主循环"""
while True:
clear()
print(f"[NeteaseMusicLyricDownloader] {self.version}\n"
"[程序主菜单]\n"
"[0] 退出程序\n[1] 单个歌曲的歌词下载\n[2] 多个歌曲的歌词下载\n[3] 从网易云下载的歌曲中获取歌词"
cls_stay(self, "[程序主菜单]")
print("[0] 退出程序\n[1] 单个歌曲的歌词下载\n[2] 多个歌曲的歌词下载\n[3] 从网易云下载的歌曲中获取歌词"
"\n[s] 进入设置\n[i] 程序信息")
r = rinput("请选择:")
@ -46,6 +47,7 @@ class MainProcess(object):
if __name__ == "__main__":
init(autoreset=True)
app = MainProcess()
try:
app.mainloop()

View File

View File

@ -1,48 +1,55 @@
"""集合 下载歌词 以及 获取歌曲信息 的功能"""
import os
from json import loads
from requests import post
from requests.exceptions import ConnectionError
from time import sleep
from colorama import Fore, Style
from modules.utils.bar import CompactBar, bprint
def wait_retry():
print("api提示操作频繁等待恢复...")
def wait_retry(kind, identify, bar=None):
bprint("api提示操作频繁等待恢复...", bar)
if kind == "information":
url = f"https://music.163.com/api/song/detail/?&ids=[{identify}]"
elif kind == "lyric":
url = f"https://music.163.com/api/song/media?id={identify}"
else:
return "unknown_kind"
while True:
try:
tmp = post(f"http://music.163.com/api/song/detail/?&ids=[1]")
tmp = post(url).json()
except ConnectionError:
return "dl_err_connection"
else:
if loads(tmp.text)["code"] == 200:
return "continue"
if tmp["code"] == 200:
return tmp
sleep(1)
def get_song_info_raw(types: list, id: str):
def get_song_info_raw(types: list, identify: str, bar: CompactBar = None):
"""获取歌曲信息
types 提供一个list,将会返回内部所有符合要求的信息类型\n
id 提供一个歌曲id(str),将会把歌曲的`types`信息返回"""
print("id:%s" % id)
``types`` 提供一个list,将会返回内部所有符合要求的信息类型\n
``identify`` 提供一个歌曲id(str),将会把歌曲的`types`信息返回"""
bprint(Fore.CYAN + "ID:%s" % identify, bar)
try:
response = post(f"http://music.163.com/api/song/detail/?&ids=[{id}]")
info = post(f"https://music.163.com/api/song/detail/?&ids=[{identify}]").json()
except ConnectionError:
return "dl_err_connection"
else:
info = loads(response.text)
if info["code"] == 406: # 判断当操作频繁时继续获取直到可以返回值为200为止
result = wait_retry()
if result == "continue":
pass
result = wait_retry("information", identify, bar=bar)
if type(result) == dict:
info = result
elif result == "dl_err_connection":
return "dl_err_connection"
else:
raise Exception("Unknown exception...")
if not info.get("songs"): # 判断是否存在该歌曲
print("这首歌没有找到,跳过...")
bprint(Fore.LIGHTBLACK_EX + "\t-> 这首歌没有找到,跳过...", bar)
return "song_nf"
else:
need = {}
@ -51,17 +58,19 @@ def get_song_info_raw(types: list, id: str):
return need
def get_song_lyric(id: str | int | dict, path: str, allinfo: bool = False):
def get_song_lyric(identify: str | int | dict, path: str, allinfo: bool = False, bar: CompactBar = None):
"""获取歌词
``id`` 提供一个歌曲id
``identify`` 提供一个歌曲id
``path`` 提供歌曲下载的路径
``allinfo`` 若此项为 True ,则提供的id格式必须为 {"id": int | str, "name": str, "artists": [[str, ...], ...]} (dict)"""
``allinfo`` 若此项为 True ,则提供的identify格式必须为存储在网易云下载文件中meta_data的格式
``bar`` 若获取歌词时下方有进度条, 则应当传入此参数"""
if allinfo:
sinfo = id
id = id["id"]
sinfo = identify
identify = identify["id"]
bprint(Fore.CYAN + f"ID: {identify}", bar)
else:
sinfo = get_song_info_raw(["name", "artists"], id)
sinfo = get_song_info_raw(["name", "artists"], identify, bar)
if sinfo == "dl_err_connection": # 处理各式各样的事件
return "dl_err_connection"
elif sinfo == "song_nf":
@ -79,7 +88,7 @@ def get_song_lyric(id: str | int | dict, path: str, allinfo: bool = False):
name = sinfo["name"]
if not name:
print("歌曲错误!这是网易云的问题,请不要找作者")
bprint(Fore.RED + "歌曲错误!这是网易云的问题,请不要找作者", bar)
return "song_err"
replaces = { # 处理非法字符所用的替换字典(根据网易云下载的文件分析得到)
"|": "",
@ -96,30 +105,28 @@ def get_song_lyric(id: str | int | dict, path: str, allinfo: bool = False):
name = name.replace(k, v)
artists = artists.replace(k, v)
print(f"歌曲:{name} - {artists}")
bprint(Fore.YELLOW + "\t-> 歌曲:" + Style.RESET_ALL + f"{name} - {artists}", bar)
filename = f"{name} - {artists}.lrc"
try:
response = post(f"http://music.163.com/api/song/media?id={id}")
info = post(f"https://music.163.com/api/song/media?id={identify}").json()
except ConnectionError:
return "dl_err_connection"
else:
info = loads(response.text)
if info["code"] == 406: # 此处与上方一样,防止因为请求限制而跳过下载
result = wait_retry()
if result == "continue":
pass
result = wait_retry("lyric", identify, bar=bar)
if type(result) == dict:
info = result
elif result == "dl_err_connection":
return "dl_err_connection"
else:
raise Exception("Unknown exception...")
tmp = loads(response.text)
if tmp.get("nolyric") or not tmp.get('lyric'):
print("这首歌没有歌词,跳过...")
if info.get("nolyric") or not info.get('lyric'):
bprint(Fore.LIGHTBLACK_EX + "\t--> 这首歌没有歌词,跳过...\n", bar)
return
else:
with open(os.path.join(path, filename), "w", encoding="utf-8") as f:
f.write(tmp["lyric"])
print(f"歌词下载完成!被保存在{os.path.join(path, filename)}")
f.write(info["lyric"])
bprint(Fore.GREEN + "\t--> 歌词下载完成!被保存在" + Style.RESET_ALL + f"{os.path.join(path, filename)}\n", bar)
return

View File

@ -9,13 +9,15 @@ from time import sleep
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 colorama import Fore, Style
from modules.utils.clear_screen import clear
from modules.functions.get_song import get_song_lyric
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
from modules.utils.bar import CompactBar, CompactArrowBar
def load_information_from_song(path) -> str | dict:
@ -43,20 +45,12 @@ def load_information_from_song(path) -> str | dict:
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")))
r = unpad(cryptor.decrypt(b64decode(bytes(ciphertext, "utf-8"))), 16).decode("utf-8")
except ValueError:
return "decrypt_failed"
@ -72,9 +66,6 @@ def load_information_from_song(path) -> str | dict:
def load_and_decrypt_from_ncm(file_path, target_dir) -> dict: # nondanee的源代码, 根据需求更改了某些东西
core_key = binascii.a2b_hex("687A4852416D736F356B496E62617857")
meta_key = binascii.a2b_hex("2331346C6A6B5F215C5D2630553C2728")
def unpad(s):
return 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'
@ -87,7 +78,7 @@ def load_and_decrypt_from_ncm(file_path, target_dir) -> dict: # nondanee的源
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_data = unpad(cryptor.decrypt(key_data), 16)[17:]
key_length = len(key_data)
key_data = bytearray(key_data)
key_box = bytearray(range(256))
@ -112,7 +103,7 @@ def load_and_decrypt_from_ncm(file_path, target_dir) -> dict: # nondanee的源
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 = 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]
@ -171,11 +162,8 @@ def process_work(path, filename, target, q_err: Queue, q_info: Queue):
def get_lyric_from_folder(self):
clear()
path = cinput(
f"[NeteaseMusicLyricDownloader] {self.version}\n"
"[自动获取]\n"
"请输入歌曲的保存文件夹(绝对路径):")
cls_stay(self, "[自动获取 - 加载文件]")
path = cinput("请输入歌曲的保存文件夹(绝对路径):")
if not os.path.exists(path):
input("路径不存在.\n按回车返回...")
return
@ -239,8 +227,8 @@ def get_lyric_from_folder(self):
max_process = 20 # 最大进程数
current_process = 0 # 当前正在活动的进程数
passed = 0 # 总共结束的进程数
with CompactBar(f"正在解 %(index){len(str(len(ncm_files)))}d/%(max)d",
suffix="", max=len(ncm_files), color="blue", width=9999) as bar:
with CompactArrowBar(f"正在 %(index){len(str(len(ncm_files)))}d/%(max)d",
suffix="", max=len(ncm_files), color="green", width=9999) as bar:
total = len(ncm_files)
allocated = 0 # 已经分配的任务数量
while True: # 进入循环,执行 新建进程->检测队列->检测任务完成 的循环
@ -252,7 +240,7 @@ def get_lyric_from_folder(self):
target_path,
q_err,
q_info)).start()
bar.print_onto_bar("已分配: %s" % ncm_files[allocated])
bar.print_onto_bar(Fore.CYAN + "已分配: " + Style.RESET_ALL + "%s" % ncm_files[allocated])
allocated += 1
current_process += 1
while True: # 错误队列检测
@ -270,16 +258,17 @@ def get_lyric_from_folder(self):
musics.append({"id": r['musicId'], "name": r["musicName"], "artists": r["artist"]})
passed += 1
current_process -= 1
bar.print_onto_bar(f"\"{r['musicName']} - "
bar.print_onto_bar(Fore.YELLOW +
f"\"{r['musicName']} - "
f"{''.join([x + ', ' for x in [x[0] for x in r['artist']]])[:-2]}"
"\" 已完成!")
"\"" + Fore.GREEN + " 已完成!")
bar.next()
except Empty:
break
if passed >= len(ncm_files):
break
if errors:
print("解密过程中发现了以下错误:")
print(Fore.LIGHTRED_EX+"解锁过程中发现了以下错误:")
for i in errors:
print(i)
@ -304,18 +293,20 @@ def get_lyric_from_folder(self):
else:
try:
input("无效选择, 若取消请按 ^C ,继续请按回车")
clear()
except KeyboardInterrupt:
return
clear()
cls_stay(self, "[自动获取 - 下载歌词]")
with CompactArrowBar(f"进度: %(index){len(str(len(musics)))}d/%(max)d",
suffix="", max=len(musics), color="yellow", width=9999) as bar:
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 get_song_lyric(musics[i], lyric_path, allinfo=True, bar=bar) == "dl_err_connection":
bar.print_onto_bar(Fore.RED + "下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...")
input()
bar.next()
if ncm_files:
if target_path != "NOT_DECRYPT":
agree = rinput("是否删除原ncm文件? (y/n)")
agree = rinput(Fore.RED + "是否删除原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="")

View File

@ -0,0 +1,42 @@
import re
from colorama import Fore
from modules.utils.clear_screen import cls_stay
from modules.utils.inputs import rinput
from modules.functions.mainly.get_song import get_song_lyric
from modules.utils.bar import CompactArrowBar
def mdl(self):
"""多个歌词文件的下载
``path: str`` 传入歌词文件保存的路径"""
cls_stay(self, "[手动-多个下载]")
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(int(r))
print("\t#%d id:%s - 已添加!" % (len(ids), r))
cls_stay(self, "[手动-多个下载]")
with CompactArrowBar(f"进度: %(index){len(str(len(ids)))}d/%(max)d",
suffix="", max=len(ids), color="yellow", width=9999) as bar:
for i in range(0, len(ids)):
r = get_song_lyric(ids[i], self.settings.lyric_path, bar=bar)
if r == "dl_err_connection":
bar.print_onto_bar(Fore.RED + "下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...")
input()
bar.next()
input("按回车键返回...")

View File

@ -1,6 +1,6 @@
import re
from modules.utils.inputs import rinput
from modules.functions.get_song import get_song_lyric
from modules.functions.mainly.get_song import get_song_lyric
from modules.utils.clear_screen import clear
@ -23,6 +23,6 @@ def download_one_lyric(self):
input("不合法的形式.\n按回车键返回...")
return
if get_song_lyric(song_id, self.settings.lyric_path) == "dl_err_connection":
if get_song_lyric(int(song_id), self.settings.lyric_path) == "dl_err_connection":
input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键返回...")
input("按回车键返回...")

View File

@ -1,38 +0,0 @@
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("按回车键返回...")

View File

View File

@ -5,8 +5,9 @@ import os
class Settings(object): # 设定一个基础的存储设置信息的 class ,并设置形参用于 json 导入设置
def __init__(self, l_p="./out/", lang="en"):
def __init__(self, l_p="./out/", l_f="", lang="en"):
self.lyric_path = l_p
self.lyric_format = l_f
self.language = lang
@ -24,7 +25,7 @@ def dict2class(adict): # 让 json.load 将读取到的 dict 转化为我们所
return Settings(adict["lyric_path"], adict["language"])
def load_settings(): # 加载 的函数
def load_settings() -> Settings: # 加载 的函数
"""加载设置
调用即可无需参数
返回: 设置 class"""

View File

@ -1,18 +1,16 @@
"""集合设置参数"""
import os
from modules.utils.clear_screen import clear
from modules.utils.clear_screen import cls_stay
from modules.utils.inputs import rinput, cinput
from modules.functions.save_load_settings import save_settings
from modules.functions.settings.save_load_settings import save_settings
def settings_menu(self):
"""设置菜单主循环"""
while True:
clear()
print(f"[NeteaseMusicLyricDownloader] {self.version}\n"
"[设置菜单]\n"
"[0] 返回上级\n[1] 歌曲保存路径\n[2] 清空输出文件夹内的内容\n[3] 歌词文件保存格式\n[4] 部分动态效果\n"
cls_stay(self, "[设置菜单]")
print("[0] 返回上级\n[1] 歌曲保存路径\n[2] 清空输出文件夹内的内容\n[3] 歌词文件保存格式\n[4] 部分动态效果\n"
"[s] 将设置保存到文件")
r = rinput("请选择:")
if r == "0":
@ -33,10 +31,8 @@ def settings_menu(self):
def __remove_output_files(self):
while True:
clear()
print(f"[NeteaseMusicLyricDownloader] {self.version}\n"
"[设置菜单 - 删除文件]\n"
"[0] 返回上级\n[1] 清除歌词文件\n[2] 清除歌曲文件\n[a] 清除所有文件")
cls_stay(self, "[设置菜单 - 删除文件]")
print("[0] 返回上级\n[1] 清除歌词文件\n[2] 清除歌曲文件\n[a] 清除所有文件")
r = rinput("请选择:") # 选择清除的文件格式
if r == "0":
return
@ -74,7 +70,7 @@ def __remove_output_files(self):
def __set_lyric_path(self):
clear()
cls_stay(self, "[设置菜单 - 保存路径]")
print("允许使用相对路径和绝对路径,默认为\"./out/\"\n请*不要*使用反斜杠来确保通用性\n"
"当前值:%s\n请输入新的歌词保存路径:" % self.settings.lyric_path)
r = cinput()
@ -93,6 +89,7 @@ def __set_lyric_path(self):
if not os.path.exists(path):
os.mkdir(path)
self.settings.lyric_path = r
save_settings(self.settings)
input("设置成功!\n按回车继续...")
return
@ -100,6 +97,3 @@ def __set_lyric_path(self):
def __set_lyric_format(self):
pass
def __save_settings(self):
return save_settings(self.settings)

View File

@ -67,3 +67,57 @@ class CompactBar(Bar):
line = ''.join([message, suffix])[:display_length]+"..."
shorten = len_abs(line)
self.writeln(line, shorten=shorten)
class CompactArrowBar(CompactBar):
def update(self):
"""
覆写原有的update方法自适应终端宽度
支持中文
"""
filled_length = int(self.width * self.progress)
empty_length = self.width - filled_length
message = self.message % self
s = "=" * filled_length
if s:
s = s[:-1] + ">"
bar = color(s, fg=self.color)
empty = self.empty_fill * empty_length
suffix = self.suffix % self
line = ''.join([message, self.bar_prefix, bar, empty, self.bar_suffix,
suffix])
# 以上为原本update代码
term_size = get_terminal_size().columns
shorten = False
if len_abs(line) > term_size: # 检测完整长度是否小于终端长度
if len_abs(line) - len_abs(''.join([bar, empty])) <= term_size: # 检测无进度条时是否小于终端长度
width = term_size - (len_abs(line) - len_abs("".join([bar, empty]))) - 1
filled_length = int(width * self.progress)
empty_length = width - filled_length
s = "=" * filled_length
if s:
s = s[:-1] + ">"
bar = color(s, fg=self.color)
empty = self.empty_fill * empty_length
line = ''.join([message, self.bar_prefix, bar, empty, self.bar_suffix,
suffix])
shorten = len_abs(line)
elif len_abs(''.join([message, suffix])) <= term_size: # 检测仅有前缀后缀时是否小于终端长度
line = ''.join([message, suffix])
shorten = len_abs(line)
else: # 全部不符合时,以仅有前缀后缀的模式,直接截断
display_length = term_size - get_more_length(''.join([message, suffix])[:term_size]) - 3
if display_length < 0:
display_length = 0
line = ''.join([message, suffix])[:display_length] + "..."
shorten = len_abs(line)
self.writeln(line, shorten=shorten)
def bprint(content, bar: CompactBar = None):
"""添加对进度条的支持的print, 若传入bar参数, 则使用print_onto_bar参数来打印内容"""
if bar:
bar.print_onto_bar(content)
else:
print(content)

View File

@ -1,4 +1,4 @@
"""包含一个函数,用来清空命令行的信息,自动判别系统"""
"""用来清空命令行的信息,自动判别系统"""
import os
@ -10,3 +10,10 @@ def clear():
os.system("clear")
else:
os.system("clear")
def cls_stay(self, custom=""):
"""保留版本号清除屏幕"""
clear()
print(f"[NeteaseMusicLyricDownloader] {self.version}")
print(custom)

View File

@ -2,3 +2,4 @@ requests
mutagen
pycryptodomex
progress
colorama