diff --git a/main.py b/main.py new file mode 100644 index 0000000..c1ede07 --- /dev/null +++ b/main.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# author David-123 + + +import os + +from modules.raw_input 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 + + +class MainProcess(object): + def __init__(self): + self.settings = load_settings() + if not os.path.exists(self.settings.lyric_path): + os.mkdir(self.settings.lyric_path) + self.version = "0.0.0" + + def mainloop(self): + """程序主循环""" + while True: + clear() + print(f"[NeteaseMusicLyricDownloader Reloaded] {self.version}\n" + "[程序主菜单]\n" + "[0] 退出程序\n[i] 程序信息\n[1] 单个歌曲的歌词下载\n[2] 多个歌曲的歌词下载\n[s] 进入设置") + r = rinput("请选择:") + + if r == "1": + download_one_lyric(self.settings.lyric_path) + elif r == "2": + mdl(self.settings.lyric_path) + elif r == "0": + break + elif r == "i": + print_info(self) + elif r == "s": + settings_menu(self) + else: + input("请输入正确的选项\n按回车键继续...") + + +if __name__ == "__main__": + app = MainProcess() + app.mainloop() diff --git a/modules/clear_screen.py b/modules/clear_screen.py new file mode 100644 index 0000000..8722697 --- /dev/null +++ b/modules/clear_screen.py @@ -0,0 +1,12 @@ +"""包含一个函数,用来清空命令行的信息,自动判别系统""" +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/get_song.py b/modules/get_song.py new file mode 100644 index 0000000..fbe8648 --- /dev/null +++ b/modules/get_song.py @@ -0,0 +1,108 @@ +"""集合 下载歌词 以及 获取歌曲信息 的功能""" +from json import loads +from requests import post +from requests.exceptions import ConnectionError +from time import sleep + + +def wait_retry(): + print("api提示操作频繁,等待恢复...") + while True: + try: + tmp = post(f"http://music.163.com/api/song/detail/?&ids=[1]") + except ConnectionError: + return "dl_err_connection" + else: + if loads(tmp.text)["code"] == 200: + return "continue" + sleep(1) + + +def get_song_info_raw(types: list, id: str): + """获取歌曲信息 + + types 提供一个list,将会返回内部所有符合要求的信息类型\n + id 提供一个歌曲id(str),将会把歌曲的`types`信息返回""" + print("id:%s" % id) + + try: + response = post(f"http://music.163.com/api/song/detail/?&ids=[{id}]") + except ConnectionError: + return "dl_err_connection" + else: + info = loads(response.text) + if info["code"] == 406: # 判断当操作频繁时,继续获取,直到可以返回值为200为止 + result = wait_retry() + if result == "continue": + pass + elif result == "dl_err_connection": + return "dl_err_connection" + else: + raise Exception("Unknown exception...") + + if not info.get("songs"): # 判断是否存在该歌曲 + print("这首歌没有找到,跳过...") + return "song_nf" + else: + need = {} + for i in types: # 通过传入的变量 types 获取信息(根据返回的信息分析得到的下面这句语句) + need.setdefault(i, info["songs"][0][i]) + return need + + +def get_song_lyric(id: str | int, path: str): + """获取歌词 + + ``id`` 提供一个歌曲id + ``path`` 提供歌曲下载的路径""" + sinfo = get_song_info_raw(["name", "artists"], id) + if sinfo == "dl_err_connection": # 处理各式各样的事件 + return "dl_err_connection" + elif sinfo == "song_nf": + return "song_nf" + else: # 整理歌曲数据,获取歌词 + artists = "" + for i in sinfo["artists"]: + artists += f"{i['name']}," + artists = artists[:-1] + + name = sinfo["name"] + replaces = { # 处理非法字符所用的替换字典(根据网易云下载的文件分析得到) + "|": "|", + ":": ":", + "<": "<", + ">": ">", + "?": "?", + "/": "/", + "\\": "\", + "*": "*", + '"': """ + } + for k, v in replaces.items(): + name = name.replace(k, v) + + print(f"歌曲:{name} - {artists}") + filename = f"{name} - {artists}.lrc" + + try: + response = post(f"http://music.163.com/api/song/media?id={id}") + except ConnectionError: + return "dl_err_connection" + else: + info = loads(response.text) + if info["code"] == 406: # 此处与上方一样,防止因为请求限制而跳过下载 + result = wait_retry() + if result == "continue": + pass + 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("这首歌没有歌词,跳过...") + else: + with open(f"{path}{filename}", "w", encoding="utf-8") as f: + f.write(tmp["lyric"]) + print(f"歌词下载完成!被保存在{path}{filename}") diff --git a/modules/information.py b/modules/information.py new file mode 100644 index 0000000..ed92d6a --- /dev/null +++ b/modules/information.py @@ -0,0 +1,16 @@ +"""该程序的自述信息,调用即输出""" +from modules.clear_screen import clear + + +def print_info(self): + """调用即输出,无返回值""" + clear() + print(f"""[NeteaseMusicLyricDownloader Reloaded] +版本: {self.version} +本软件开源,项目地址: +作者:David-123 +联系方式: + QQ:1826013250 + E-mail:1826013250@qq.com(mainly) + mc1826013250@gmail.com""") + input("按回车键返回...") \ No newline at end of file diff --git a/modules/multi_download.py b/modules/multi_download.py new file mode 100644 index 0000000..e5f58cb --- /dev/null +++ b/modules/multi_download.py @@ -0,0 +1,28 @@ +"""多文件下载""" + +from modules.clear_screen import clear +from modules.raw_input import rinput +from modules.get_song import get_song_lyric + + +def mdl(path: str): + clear() + ids = [] + print("输入歌曲id,用回车分开,输入s停止") + while True: + r = rinput() + if r == 's': + break + else: + try: + int(r) + except ValueError: + print("该输入不合法") + else: + ids.append(r) + clear() + for i in range(0, len(ids)): + print("进度: %d/%d" % (i+1, len(ids))) + if get_song_lyric(ids[i], path) == "dl_err_connection": + input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...") + input("按回车键返回...") diff --git a/modules/one_download.py b/modules/one_download.py new file mode 100644 index 0000000..b345421 --- /dev/null +++ b/modules/one_download.py @@ -0,0 +1,22 @@ +# 导入自定义模块使用try,原因是如果在外部单独运行文件,无法通过modules索引到依赖的模块 +# 直接单独运行会出现 ModuleNotFoundError 报错 + +from modules.raw_input import rinput +from modules.get_song import get_song_lyric +from modules.clear_screen import clear + + +def download_one_lyric(path: str): + """单次下载歌词 + + ``path: str`` 存储歌词的路径""" + clear() + song_id = rinput("请输入歌曲id:") + try: + int(song_id) + except ValueError: + input("不合法的id形式.\n按回车键返回...") + else: + if get_song_lyric(song_id, path) == "dl_err_connection": + input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键返回...") + input("按回车键返回...") diff --git a/modules/raw_input.py b/modules/raw_input.py new file mode 100644 index 0000000..6597fac --- /dev/null +++ b/modules/raw_input.py @@ -0,0 +1,11 @@ +"""该模块提供几个自定义处理输入函数""" + + +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 diff --git a/modules/save_load_settings.py b/modules/save_load_settings.py new file mode 100644 index 0000000..411e5ba --- /dev/null +++ b/modules/save_load_settings.py @@ -0,0 +1,51 @@ +"""加载 or 保存设置文件""" + +import json +import os + + +class Settings(object): # 设定一个基础的存储设置信息的 class + def __init__(self, l_p="./out/", lang="en"): + self.lyric_path = l_p + self.language = lang + + +def class2dict(aclass): # 让 json.dumps 将 class 转化为一个 dict ,用于保存 + return { + "lyric_path": aclass.lyric_path, + "language": aclass.language, + } + + +def dict2class(adict): # 让 json.load 将读取到的 dict 转化为我们所需要的 class + if len(adict) != 2: # 若检测到多余的设定将抛出异常 + raise json.decoder.JSONDecodeError("Too many keys", "none", 0) + else: + return Settings(adict["lyric_path"], adict["language"]) + + +def load_settings(): # 加载 的函数 + """加载设置 + 调用即可,无需参数 + 返回: 设置 class""" + if os.path.exists("settings.json"): # 判断目录下是否存在 settings.json ,若没有则创建,若有则读取 + with open("settings.json", 'r', encoding="utf-8") as f: + try: + return json.load(f, object_hook=dict2class) + except json.decoder.JSONDecodeError: # 如果检测到文件无法读取,将会删除设置文件并重新创建 + print("设置文件损坏,重新创建...") + os.remove("settings.json") + return load_settings() + else: + with open("settings.json", 'w', encoding="utf-8") as f: + f.write(json.dumps(Settings(), default=class2dict)) + return Settings() + + +def save_settings(settings): # 保存 的函数 + """保存设置 + ``settings`` 传入一个 设置 class 将其序列化为json + 返回 done 即为完成,理论上不存在报错所以暂时没有做其他处理""" + with open("settings.json", 'w', encoding="utf-8") as f: + f.write(json.dumps(settings, default=class2dict)) + input("保存完成!按回车继续...") diff --git a/modules/settings.py b/modules/settings.py new file mode 100644 index 0000000..2c6b71e --- /dev/null +++ b/modules/settings.py @@ -0,0 +1,63 @@ +"""集合设置参数""" + +import os +import re +from modules.clear_screen import clear +from modules.raw_input import rinput, cinput +from modules.save_load_settings import save_settings + + +def settings_menu(self): + """设置菜单主循环""" + while True: + clear() + print(f"[NeteaseMusicLyricDownloader Reloaded] {self.version}\n" + "[设置菜单]\n" + "[0] 返回上级\n[1] 设置歌曲保存路径\n[2] 清空输出文件夹内的所有歌词\n[s] 将设置保存到文件") + r = rinput("请选择:") + if r == "0": + return + elif r == "1": + __set_lyric_path(self) + elif r == "2": + __save_settings(self) + elif r == "s": + __remove_lyric_files(self.settings.lyric_path) + else: + input("输入") + + +def __remove_lyric_files(path): + clear() + files = [] + for i in os.listdir(path): + if re.match(r".*(\.lrc)$", i): + files.append(i) + if len(files) != 0: + for i in range(0, len(files)): + print("正在删除(%d/%d): %s" % (i+1, len(files), files[i])) + os.remove(path+files[i]) + print("删除完毕!\n按回车继续...") + else: + print("文件夹内没有要删除的东西\n按回车继续...") + + +def __set_lyric_path(self): + print("允许使用相对路径和绝对路径,默认为\"./out/\"\n请避免使用反斜杠来确保通用性\n" + "当前值:%s\n请输入新的歌词保存路径:" % self.settings.lyric_path) + r = cinput() + if not r: + input("输入为空!\n按回车继续...") + if r[-1] != "/": + r += "/" + path = "" + for i in r.split("/"): + path += i+"/" + if not os.path.exists(path): + os.mkdir(path) + self.settings.lyric_path = r + input("设置成功!\n按回车继续...") + + +def __save_settings(self): + return save_settings(self.settings)