First Update

This commit is contained in:
1826013250 2022-04-02 22:50:52 +08:00 committed by GitHub
parent 2d3782cbe2
commit 097ef5ed5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 360 additions and 0 deletions

49
main.py Normal file
View File

@ -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()

12
modules/clear_screen.py Normal file
View File

@ -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")

108
modules/get_song.py Normal file
View File

@ -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}")

16
modules/information.py Normal file
View File

@ -0,0 +1,16 @@
"""该程序的自述信息,调用即输出"""
from modules.clear_screen import clear
def print_info(self):
"""调用即输出,无返回值"""
clear()
print(f"""[NeteaseMusicLyricDownloader Reloaded]
版本: {self.version}
本软件开源项目地址:<url>
作者:David-123
联系方式:
QQ:1826013250
E-mail:1826013250@qq.com(mainly)
mc1826013250@gmail.com""")
input("按回车键返回...")

28
modules/multi_download.py Normal file
View File

@ -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("按回车键返回...")

22
modules/one_download.py Normal file
View File

@ -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("按回车键返回...")

11
modules/raw_input.py Normal file
View File

@ -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()

View File

@ -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("保存完成!按回车继续...")

63
modules/settings.py Normal file
View File

@ -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)