Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
f84d8ccf05 | |||
6fe6a1c759 | |||
e371ae1052 | |||
43cf038ec0 | |||
71e967808b | |||
dcd275e3b1 | |||
afd3e84a8e | |||
2f4db4e11c | |||
4c5bd467cb | |||
![]() |
07318a2ef8 | ||
![]() |
474efd1321 | ||
![]() |
27a3eda9fe | ||
![]() |
1db3555ea8 | ||
![]() |
5f025cdfbf |
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
out/
|
||||||
|
settings.json
|
||||||
|
.idea/
|
||||||
|
venv/
|
||||||
|
venv_win/
|
||||||
|
test/
|
11
README.md
11
README.md
@@ -10,10 +10,13 @@ It uses the module `requests` to fetch the lyric on music.163.com
|
|||||||
Netease Music supply us an api that we can get the lyric of the current song, so I make this program...
|
Netease Music supply us an api that we can get the lyric of the current song, so I make this program...
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
First, you need a python environment, and use `pip` to install these packages: `requests` `mutagen` `pycryptodome`
|
First, you need a python(>=3.10) environment
|
||||||
>Feel complex? just copy this command to your terminal: `python3 -m pip install requests mutagen pycryptodome`
|
|
||||||
|
|
||||||
Second, clone the entire project and run the command `python3 main.py` in the project folder.
|
Second, clone the entire project and install packages with the command below:
|
||||||
|
```commandline
|
||||||
|
python3 -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
Last, run the command `python3 main.py` in the project folder.
|
||||||
|
|
||||||
## What can it do?
|
## What can it do?
|
||||||
Just download lyrics.
|
Just download lyrics.
|
||||||
@@ -22,7 +25,7 @@ You need to provide the id or the share link of the song, and the program will d
|
|||||||
|
|
||||||
Now it can recognize 163 key in music files that download from Netease Cloudmusic client.
|
Now it can recognize 163 key in music files that download from Netease Cloudmusic client.
|
||||||
|
|
||||||
- 2022/8/13 Now it has the function from `ncmdump`, it can decrypt the ncm files and fetch the specific lyric.
|
- 2022/8/13 Now it has the function from `ncmdump`, and it can decrypt the ncm files and fetch the specific lyric.
|
||||||
|
|
||||||
## Todo
|
## Todo
|
||||||
|
|
||||||
|
12
README_cn.md
12
README_cn.md
@@ -9,10 +9,16 @@
|
|||||||
网易云音乐提供给我们了一个获取歌词的API ~~然后我就做了这个程序(~~
|
网易云音乐提供给我们了一个获取歌词的API ~~然后我就做了这个程序(~~
|
||||||
|
|
||||||
## 爷要安装
|
## 爷要安装
|
||||||
首先,你需要一个Python环境(要求版本>=3.10,因为使用了3.10python的新特性),然后使用pip来安装`requests` `mutagen` `pycryptodome`这几个包
|
首先,你需要一个Python环境(要求版本>=3.10,因为使用了3.10python的新特性)
|
||||||
>您觉得很麻烦?_~~事真多~~_ 直接复制这段命令到终端罢!(你得先有Python和pip): `python3 -m pip install requests mutagen pycryptodome`
|
|
||||||
|
|
||||||
然后,把整个项目薅下来,在当前目录下运行`python3 main.py`就行了
|
然后,把整个项目薅下来,安装依赖项目:
|
||||||
|
```commandline
|
||||||
|
python3 -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
最后运行下面命令:
|
||||||
|
```commandline
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
## 这玩意到底能干啥?
|
## 这玩意到底能干啥?
|
||||||
~~nmd~~这玩意就像它的名字一样,下载歌词用的
|
~~nmd~~这玩意就像它的名字一样,下载歌词用的
|
||||||
|
24
main.py
24
main.py
@@ -1,17 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# ↑ For Linux & MacOS to run this program directly if the user currently installed python and third-party packages.
|
# ↑ For Linux & macOS to run this program directly if the user currently installed python and third-party packages.
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# author: David-123
|
# author: David-123
|
||||||
|
|
||||||
|
|
||||||
from modules.raw_input import rinput
|
from modules.utils.inputs import rinput
|
||||||
from modules.information import print_info
|
from modules.utils.information import print_info
|
||||||
from modules.multi_download import mdl
|
from modules.functions.multi_download import mdl
|
||||||
from modules.one_download import download_one_lyric
|
from modules.functions.one_download import download_one_lyric
|
||||||
from modules.settings import settings_menu
|
from modules.submenus.settings import settings_menu
|
||||||
from modules.save_load_settings import load_settings
|
from modules.functions.save_load_settings import load_settings
|
||||||
from modules.clear_screen import clear
|
from modules.utils.clear_screen import clear
|
||||||
from modules.load_file_song import get_lyric_from_folder
|
from modules.functions.load_file_song import get_lyric_from_folder
|
||||||
|
|
||||||
|
|
||||||
class MainProcess(object):
|
class MainProcess(object):
|
||||||
@@ -30,11 +30,11 @@ class MainProcess(object):
|
|||||||
r = rinput("请选择:")
|
r = rinput("请选择:")
|
||||||
|
|
||||||
if r == "1":
|
if r == "1":
|
||||||
download_one_lyric(self.settings.lyric_path)
|
download_one_lyric(self)
|
||||||
elif r == "2":
|
elif r == "2":
|
||||||
mdl(self.settings.lyric_path)
|
mdl(self)
|
||||||
elif r == "3":
|
elif r == "3":
|
||||||
get_lyric_from_folder(self.settings.lyric_path)
|
get_lyric_from_folder(self)
|
||||||
elif r == "0":
|
elif r == "0":
|
||||||
exit(0)
|
exit(0)
|
||||||
elif r == "i":
|
elif r == "i":
|
||||||
|
0
modules/functions/__init__.py
Normal file
0
modules/functions/__init__.py
Normal file
315
modules/functions/load_file_song.py
Normal file
315
modules/functions/load_file_song.py
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import binascii
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
from base64 import b64decode
|
||||||
|
from multiprocessing import Process, Queue
|
||||||
|
from queue import Empty
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
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) -> str | dict:
|
||||||
|
"""从音乐文件中的 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, 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'
|
||||||
|
f.seek(2, 1)
|
||||||
|
key_length = f.read(4)
|
||||||
|
key_length = struct.unpack('<I', bytes(key_length))[0]
|
||||||
|
key_data = f.read(key_length)
|
||||||
|
key_data_array = bytearray(key_data)
|
||||||
|
for i in range(0, len(key_data_array)):
|
||||||
|
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_length = len(key_data)
|
||||||
|
key_data = bytearray(key_data)
|
||||||
|
key_box = bytearray(range(256))
|
||||||
|
last_byte = 0
|
||||||
|
key_offset = 0
|
||||||
|
for i in range(256):
|
||||||
|
swap = key_box[i]
|
||||||
|
c = (swap + last_byte + key_data[key_offset]) & 0xff
|
||||||
|
key_offset += 1
|
||||||
|
if key_offset >= 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('<I', bytes(meta_length))[0]
|
||||||
|
meta_data = f.read(meta_length)
|
||||||
|
meta_data_array = bytearray(meta_data)
|
||||||
|
for i in range(0, len(meta_data_array)):
|
||||||
|
meta_data_array[i] ^= 0x63
|
||||||
|
meta_data = bytes(meta_data_array)
|
||||||
|
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 = json.loads(meta_data)
|
||||||
|
crc32 = f.read(4)
|
||||||
|
crc32 = struct.unpack('<I', bytes(crc32))[0]
|
||||||
|
..., crc32
|
||||||
|
f.seek(5, 1)
|
||||||
|
image_size = f.read(4)
|
||||||
|
image_size = struct.unpack('<I', bytes(image_size))[0]
|
||||||
|
image_data = f.read(image_size)
|
||||||
|
file_name = f.name.split("/")[-1].split(".ncm")[0] + '.' + meta_data['format']
|
||||||
|
m = open(os.path.join(target_dir, file_name), 'wb')
|
||||||
|
while True:
|
||||||
|
chunk = bytearray(f.read(0x8000))
|
||||||
|
chunk_length = len(chunk)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
for i in range(1, chunk_length + 1):
|
||||||
|
j = i & 0xff
|
||||||
|
chunk[i - 1] ^= key_box[(key_box[j] + key_box[(key_box[j] + j) & 0xff]) & 0xff]
|
||||||
|
m.write(chunk)
|
||||||
|
m.close()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# 对解密后的文件进行信息补全
|
||||||
|
if meta_data["format"] == "mp3": # 针对 mp3 使用 ID3 进行信息补全
|
||||||
|
audio = ID3(os.path.join(target_dir, os.path.splitext(file_path.split("/")[-1])[0] + ".mp3"))
|
||||||
|
artists = []
|
||||||
|
for i in meta_data["artist"]:
|
||||||
|
artists.append(i[0])
|
||||||
|
audio["TPE1"] = TPE1(encoding=3, text=artists) # 插入歌手
|
||||||
|
audio["APIC"] = APIC(encoding=3, mime='image/jpg', type=3, desc='', data=image_data) # 插入封面
|
||||||
|
audio["COMM::XXX"] = COMM(encoding=3, lang='XXX', desc='', text=[comment.decode("utf-8")]) # 插入 163 key 注释
|
||||||
|
audio["TIT2"] = TIT2(encoding=3, text=[meta_data["musicName"]]) # 插入歌曲名
|
||||||
|
audio["TALB"] = TALB(encoding=3, text=[meta_data["album"]]) # 插入专辑名
|
||||||
|
audio.save()
|
||||||
|
elif meta_data["format"] == "flac": # 针对 flac 使用 FLAC 进行信息补全
|
||||||
|
audio = flac.FLAC(os.path.join(target_dir, os.path.splitext(file_path.split("/")[-1])[0] + ".flac"))
|
||||||
|
artists = []
|
||||||
|
for i in meta_data["artist"]:
|
||||||
|
artists.append(i[0])
|
||||||
|
audio["artist"] = artists[:] # 插入歌手
|
||||||
|
audio["title"] = [meta_data["musicName"]] # 插入歌曲名
|
||||||
|
audio["album"] = [meta_data["album"]] # 插入专辑名
|
||||||
|
audio["description"] = comment.decode("utf-8") # 插入 163 key 注释
|
||||||
|
audio.save()
|
||||||
|
|
||||||
|
return meta_data
|
||||||
|
|
||||||
|
|
||||||
|
def process_work(path, filename, target, q_err: Queue, q_info: Queue):
|
||||||
|
try:
|
||||||
|
result = load_and_decrypt_from_ncm(path, target)
|
||||||
|
except AssertionError:
|
||||||
|
q_err.put(f"\t- 文件 \"{filename}\" 破解失败!")
|
||||||
|
else:
|
||||||
|
q_info.put(result)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lyric_from_folder(self):
|
||||||
|
clear()
|
||||||
|
path = cinput(
|
||||||
|
f"[NeteaseMusicLyricDownloader] {self.version}\n"
|
||||||
|
"[自动获取]\n"
|
||||||
|
"请输入歌曲的保存文件夹(绝对路径):")
|
||||||
|
if not os.path.exists(path):
|
||||||
|
input("路径不存在.\n按回车返回...")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("正在遍历目录,请稍后...")
|
||||||
|
musics = []
|
||||||
|
ncm_files = []
|
||||||
|
fails = 0
|
||||||
|
for i in os.listdir(path): # 遍历目录,查找目标文件
|
||||||
|
ext = os.path.splitext(i)[-1]
|
||||||
|
if ext in ['.mp3', '.flac']: # 对于 mp3 和 flac 文件, 使用对应读取解密方式
|
||||||
|
result = load_information_from_song(os.path.join(path, i))
|
||||||
|
if result == "not_support":
|
||||||
|
fails += 1
|
||||||
|
print(f"文件 \"{i}\" 未包含 163 key ,跳过")
|
||||||
|
elif result == "decrypt_failed":
|
||||||
|
fails += 1
|
||||||
|
print(f"文件 \"{i}\" 内 163 key 解密失败,跳过")
|
||||||
|
elif result == "not_a_normal_music":
|
||||||
|
fails += 1
|
||||||
|
print(f"文件 \"{i}\" 内 163 key 不是一个普通音乐文件,这可能是一个电台曲目")
|
||||||
|
else:
|
||||||
|
musics.append({"id": result['musicId'], "name": result["musicName"], "artists": result["artist"]})
|
||||||
|
elif ext == ".ncm": # 对于 ncm 先加入到列表,等待解密
|
||||||
|
ncm_files.append(i)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
target_path = ""
|
||||||
|
if ncm_files:
|
||||||
|
while True:
|
||||||
|
print(f"\n发现{len(ncm_files)}个ncm加密文件!")
|
||||||
|
print("请问解密后的文件保存在哪里?\n"
|
||||||
|
"[1] 保存在相同文件夹内\n[2] 保存在程序设定的下载文件夹中\n[3] 保存在自定义文件夹内\n[q] 取消解密,下载歌词时将忽略这些文件")
|
||||||
|
select = rinput("请选择: ")
|
||||||
|
if select == 'q':
|
||||||
|
target_path = "NOT_DECRYPT"
|
||||||
|
break
|
||||||
|
elif select == '1':
|
||||||
|
target_path = path
|
||||||
|
break
|
||||||
|
elif select == '2':
|
||||||
|
target_path = self.settings.lyric_path
|
||||||
|
break
|
||||||
|
elif select == '3':
|
||||||
|
target_path = cinput("请输入: ")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("输入无效!按回车继续...")
|
||||||
|
|
||||||
|
if target_path != "NOT_DECRYPT": # 开始进行逐个文件解密
|
||||||
|
errors = [] # 初始化变量
|
||||||
|
q_err = Queue() # 错误信息队列
|
||||||
|
q_info = Queue() # 返回信息队列
|
||||||
|
max_process = 20 # 最大进程数
|
||||||
|
current_process = 0 # 当前正在活动的进程数
|
||||||
|
passed = 0 # 总共结束的进程数
|
||||||
|
with Bar(f"正在破解 %(index){len(str(len(ncm_files)))}d/%(max)d",
|
||||||
|
suffix="", max=len(ncm_files), color="blue") as bar:
|
||||||
|
total = len(ncm_files)
|
||||||
|
allocated = 0 # 已经分配的任务数量
|
||||||
|
while True: # 进入循环,执行 新建进程->检测队列->检测任务完成 的循环
|
||||||
|
sleep(0)
|
||||||
|
if current_process <= max_process and allocated < total: # 分配进程
|
||||||
|
Process(target=process_work,
|
||||||
|
args=(os.path.join(path, ncm_files[allocated]),
|
||||||
|
ncm_files[allocated],
|
||||||
|
target_path,
|
||||||
|
q_err,
|
||||||
|
q_info)).start()
|
||||||
|
allocated += 1
|
||||||
|
bar.suffix = f"已分配: {ncm_files[allocated-1]}"
|
||||||
|
bar.update()
|
||||||
|
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.suffix = f"已完成: {r['musicName']} - "\
|
||||||
|
f"{''.join([x + ', ' for x in [x[0] for x in r['artist']]])[:-2]}"
|
||||||
|
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":
|
||||||
|
lyric_path = self.settings.lyric_path
|
||||||
|
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
|
@@ -1,37 +1,38 @@
|
|||||||
import re
|
import re
|
||||||
from modules.clear_screen import clear
|
from modules.utils.clear_screen import clear
|
||||||
from modules.raw_input import rinput
|
from modules.utils.inputs import rinput
|
||||||
from modules.get_song import get_song_lyric
|
from modules.functions.get_song import get_song_lyric
|
||||||
|
|
||||||
|
|
||||||
def mdl(path: str):
|
def mdl(self):
|
||||||
"""多个歌词文件的下载
|
"""多个歌词文件的下载
|
||||||
|
|
||||||
``path: str`` 传入歌词文件保存的路径"""
|
``path: str`` 传入歌词文件保存的路径"""
|
||||||
clear()
|
clear()
|
||||||
ids = []
|
ids = []
|
||||||
print("输入歌曲id,用回车分开,输入s停止")
|
print(f"[NeteaseMusicLyricDownloader] {self.version}\n"
|
||||||
while True:
|
"[手动-多个下载]\n"
|
||||||
r = rinput()
|
"输入歌曲id,用回车分开,输入s停止")
|
||||||
if r == 's':
|
while True:
|
||||||
break
|
r = rinput()
|
||||||
else:
|
if r == 's':
|
||||||
try:
|
break
|
||||||
int(r)
|
else:
|
||||||
|
try:
|
||||||
except ValueError:
|
int(r)
|
||||||
|
except ValueError:
|
||||||
tmp = re.search(r"song\?id=[0-9]*", r)
|
tmp = re.search(r"song\?id=[0-9]*", r)
|
||||||
if tmp:
|
if tmp:
|
||||||
r = tmp.group()[8:]
|
r = tmp.group()[8:]
|
||||||
else:
|
else:
|
||||||
print("不合法的形式.\n")
|
print("不合法的形式.\n")
|
||||||
continue
|
continue
|
||||||
ids.append(r)
|
ids.append(r)
|
||||||
print("\t#%d id:%s - 已添加!" % (len(ids), r))
|
print("\t#%d id:%s - 已添加!" % (len(ids), r))
|
||||||
clear()
|
clear()
|
||||||
for i in range(0, len(ids)):
|
for i in range(0, len(ids)):
|
||||||
print("\n进度: %d/%d" % (i+1, len(ids)))
|
print("进度: %d/%d" % (i+1, len(ids)))
|
||||||
if get_song_lyric(ids[i], path) == "dl_err_connection":
|
r = get_song_lyric(ids[i], self.settings.lyric_path)
|
||||||
input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...")
|
if r == "dl_err_connection":
|
||||||
input("按回车键返回...")
|
input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...")
|
||||||
|
input("按回车键返回...")
|
@@ -1,15 +1,18 @@
|
|||||||
import re
|
import re
|
||||||
from modules.raw_input import rinput
|
from modules.utils.inputs import rinput
|
||||||
from modules.get_song import get_song_lyric
|
from modules.functions.get_song import get_song_lyric
|
||||||
from modules.clear_screen import clear
|
from modules.utils.clear_screen import clear
|
||||||
|
|
||||||
|
|
||||||
def download_one_lyric(path: str):
|
def download_one_lyric(self):
|
||||||
"""单次下载歌词
|
"""单次下载歌词
|
||||||
|
|
||||||
``path: str`` 存储歌词的路径"""
|
``path: str`` 存储歌词的路径"""
|
||||||
clear()
|
clear()
|
||||||
song_id = rinput("请输入歌曲id:")
|
song_id = rinput(
|
||||||
|
f"[NeteaseMusicLyricDownloader] {self.version}\n"
|
||||||
|
"[手动-单个下载]\n"
|
||||||
|
"请输入歌曲id:")
|
||||||
try:
|
try:
|
||||||
int(song_id)
|
int(song_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -20,6 +23,6 @@ def download_one_lyric(path: str):
|
|||||||
input("不合法的形式.\n按回车键返回...")
|
input("不合法的形式.\n按回车键返回...")
|
||||||
return
|
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("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键返回...")
|
||||||
input("按回车键返回...")
|
input("按回车键返回...")
|
@@ -1,230 +0,0 @@
|
|||||||
import binascii
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import struct
|
|
||||||
from base64 import b64decode
|
|
||||||
|
|
||||||
from Cryptodome.Cipher import AES
|
|
||||||
from mutagen import File
|
|
||||||
from mutagen.id3 import ID3, TPE1, APIC, COMM, TIT2, TALB
|
|
||||||
|
|
||||||
from modules.clear_screen import clear
|
|
||||||
from modules.get_song import get_song_lyric
|
|
||||||
|
|
||||||
|
|
||||||
def load_information_from_song(path):
|
|
||||||
"""从音乐文件中的 Comment 字段获取 163 key 并解密返回歌曲信息"""
|
|
||||||
file = File(path) # 使用 TinyTag 获取歌曲信息
|
|
||||||
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"
|
|
||||||
|
|
||||||
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('<I', bytes(key_length))[0]
|
|
||||||
key_data = f.read(key_length)
|
|
||||||
key_data_array = bytearray(key_data)
|
|
||||||
for i in range(0, len(key_data_array)):
|
|
||||||
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_length = len(key_data)
|
|
||||||
key_data = bytearray(key_data)
|
|
||||||
key_box = bytearray(range(256))
|
|
||||||
c = 0
|
|
||||||
last_byte = 0
|
|
||||||
key_offset = 0
|
|
||||||
for i in range(256):
|
|
||||||
swap = key_box[i]
|
|
||||||
c = (swap + last_byte + key_data[key_offset]) & 0xff
|
|
||||||
key_offset += 1
|
|
||||||
if key_offset >= 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('<I', bytes(meta_length))[0]
|
|
||||||
meta_data = f.read(meta_length)
|
|
||||||
meta_data_array = bytearray(meta_data)
|
|
||||||
for i in range(0, len(meta_data_array)):
|
|
||||||
meta_data_array[i] ^= 0x63
|
|
||||||
meta_data = bytes(meta_data_array)
|
|
||||||
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 = json.loads(meta_data)
|
|
||||||
crc32 = f.read(4)
|
|
||||||
crc32 = struct.unpack('<I', bytes(crc32))[0]
|
|
||||||
f.seek(5, 1)
|
|
||||||
image_size = f.read(4)
|
|
||||||
image_size = struct.unpack('<I', bytes(image_size))[0]
|
|
||||||
image_data = f.read(image_size)
|
|
||||||
file_name = f.name.split("/")[-1].split(".ncm")[0] + '.' + meta_data['format']
|
|
||||||
m = open(os.path.join(targetdir, file_name), 'wb')
|
|
||||||
chunk = bytearray()
|
|
||||||
while True:
|
|
||||||
chunk = bytearray(f.read(0x8000))
|
|
||||||
chunk_length = len(chunk)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
for i in range(1, chunk_length + 1):
|
|
||||||
j = i & 0xff
|
|
||||||
chunk[i - 1] ^= key_box[(key_box[j] + key_box[(key_box[j] + j) & 0xff]) & 0xff]
|
|
||||||
m.write(chunk)
|
|
||||||
m.close()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
# 对解密后的文件进行信息补全
|
|
||||||
audio = ID3(os.path.splitext(file_path)[0]+"."+meta_data["format"])
|
|
||||||
artists = []
|
|
||||||
for i in meta_data["artist"]:
|
|
||||||
artists.append(i[0])
|
|
||||||
audio["TPE1"] = TPE1(encoding=3, text=artists) # 插入歌手
|
|
||||||
audio["APIC"] = APIC(encoding=3, mime='image/jpg', type=3, desc='', data=image_data) # 插入封面
|
|
||||||
audio["COMM::XXX"] = COMM(encoding=3, lang='XXX', desc='', text=[comment.decode("utf-8")]) # 插入 163 key 注释
|
|
||||||
audio["TIT2"] = TIT2(encoding=3, text=[meta_data["musicName"]]) # 插入歌曲名
|
|
||||||
audio["TALB"] = TALB(encoding=3, text=[meta_data["album"]]) # 插入专辑名
|
|
||||||
audio.save()
|
|
||||||
|
|
||||||
return meta_data
|
|
||||||
|
|
||||||
|
|
||||||
def get_lyric_from_folder(lyric_path: str):
|
|
||||||
clear()
|
|
||||||
path = input("请输入歌曲的保存文件夹(绝对路径):").strip()
|
|
||||||
if not os.path.exists(path):
|
|
||||||
input("路径不存在.\n按回车返回...")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("正在遍历目录,请稍后...")
|
|
||||||
musics = []
|
|
||||||
ncm_files = []
|
|
||||||
fails = 0
|
|
||||||
for i in os.listdir(path): # 遍历目录,查找目标文件
|
|
||||||
ext = os.path.splitext(i)[-1]
|
|
||||||
if ext in ['.mp3', '.flac']: # 对于 mp3 和 flac 文件, 使用对应读取解密方式
|
|
||||||
result = load_information_from_song(os.path.join(path, i))
|
|
||||||
if result == "not_support":
|
|
||||||
fails += 1
|
|
||||||
print(f"文件 \"{i}\" 未包含 163 key ,跳过")
|
|
||||||
elif result == "decrypt_failed":
|
|
||||||
fails += 1
|
|
||||||
print(f"文件 \"{i}\" 内 163 key 解密失败,跳过")
|
|
||||||
elif result == "not_a_normal_music":
|
|
||||||
fails += 1
|
|
||||||
print(f"文件 \"{i}\" 内 163 key 不是一个普通音乐文件,这可能是一个电台曲目")
|
|
||||||
else:
|
|
||||||
musics.append({"id": result['musicId'], "name": result["musicName"], "artists": result["artist"]})
|
|
||||||
elif ext == ".ncm": # 对于 ncm 先加入到列表,等待解密
|
|
||||||
ncm_files.append(i)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if ncm_files:
|
|
||||||
while True:
|
|
||||||
print(f"\n发现{len(ncm_files)}个ncm加密文件!")
|
|
||||||
print("请问解密后的文件保存在哪里?\n"
|
|
||||||
"[1] 保存在相同文件夹内\n[2] 保存在程序设定的下载文件夹中\n[3] 保存在自定义文件夹内\n[q] 取消解密,下载歌词时将忽略这些文件")
|
|
||||||
select = input("请选择: ").strip().lower()
|
|
||||||
if select == 'q':
|
|
||||||
target_path = "NOT_DECRYPT"
|
|
||||||
break
|
|
||||||
elif select == '1':
|
|
||||||
target_path = path
|
|
||||||
break
|
|
||||||
elif select == '2':
|
|
||||||
target_path = lyric_path
|
|
||||||
break
|
|
||||||
elif select == '3':
|
|
||||||
target_path = input("请输入: ").strip()
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("输入无效!按回车继续...")
|
|
||||||
|
|
||||||
if target_path != "NOT_DECRYPT":
|
|
||||||
for i in range(0, len(ncm_files)):
|
|
||||||
print("破解进度: %d/%d ~ %s" % (i+1, len(ncm_files), ncm_files[i]))
|
|
||||||
try:
|
|
||||||
result = load_and_decrypt_from_ncm(os.path.join(path, ncm_files[i]), target_path)
|
|
||||||
except AssertionError:
|
|
||||||
print(f"\t- 文件 \"{ncm_files[i]}\" 破解失败!\n\t 可能是文件不完整或者重命名了别的文件?跳过...")
|
|
||||||
fails += 1
|
|
||||||
continue
|
|
||||||
print("\t--> %s" % os.path.splitext(ncm_files[i])[0]+"."+result["format"])
|
|
||||||
musics.append({"id": result['musicId'], "name": result["musicName"], "artists": result["artist"]})
|
|
||||||
|
|
||||||
# 汇报索引结果
|
|
||||||
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 = input("请选择: ").strip().lower()
|
|
||||||
if r == "1":
|
|
||||||
break
|
|
||||||
elif r == "2":
|
|
||||||
path = lyric_path
|
|
||||||
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], path, allinfo=True) == "dl_err_connection":
|
|
||||||
input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...")
|
|
||||||
if ncm_files:
|
|
||||||
if target_path != "NOT_DECRYPT":
|
|
||||||
agree = input("是否删除原ncm文件? (y/n)").strip().lower()
|
|
||||||
if agree == "y":
|
|
||||||
for i in range(0, len(ncm_files)):
|
|
||||||
print("删除进度: %d/%d ~ %s" % (i+1, len(ncm_files), ncm_files[i]))
|
|
||||||
os.remove(os.path.join(path, ncm_files[i]))
|
|
||||||
else:
|
|
||||||
print("取消.")
|
|
||||||
input("按回车返回...")
|
|
||||||
return
|
|
@@ -1,89 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from tinytag import TinyTag
|
|
||||||
from requests import post
|
|
||||||
from modules.get_song import get_song_lyric
|
|
||||||
from modules.clear_screen import clear
|
|
||||||
|
|
||||||
|
|
||||||
def load_information_from_mp3(path):
|
|
||||||
"""从音乐文件中的 Comment 字段获取 163 key 并解密返回歌曲信息"""
|
|
||||||
file = TinyTag.get(path) # 使用 TinyTag 获取歌曲信息
|
|
||||||
if file.comment:
|
|
||||||
if file.comment[:7] == "163 key":
|
|
||||||
ciphertext = file.comment[22:]
|
|
||||||
else:
|
|
||||||
return "not_support"
|
|
||||||
else:
|
|
||||||
return "not_support"
|
|
||||||
data = {
|
|
||||||
"data": ciphertext,
|
|
||||||
"type": "aes_decrypt",
|
|
||||||
"encode": "base64",
|
|
||||||
"key": "#14ljk_!\\]&0U<'(", # 感谢大佬 chenjunyu19 在 MorFans Dev 上提供的密文密钥
|
|
||||||
"digit": 128,
|
|
||||||
"mode": "ECB",
|
|
||||||
"pad": "Pkcs5Padding"
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
r = post("https://www.mklab.cn/utils/handle", data=data).text[17:-2].replace("\\\"", "\"") # 十分感谢 MKLAB 网站提供的解密接口!!
|
|
||||||
except ConnectionError:
|
|
||||||
return "dl_err_connection"
|
|
||||||
if r:
|
|
||||||
return json.loads(r)
|
|
||||||
else:
|
|
||||||
return "decrypt_failed"
|
|
||||||
|
|
||||||
|
|
||||||
def get_lyric_from_folder(lyric_path: str):
|
|
||||||
clear()
|
|
||||||
path = input("请输入歌曲的保存文件夹(绝对路径):").strip().replace("\\", "/")
|
|
||||||
if not os.path.exists(path):
|
|
||||||
input("路径不存在,请检查输入...")
|
|
||||||
return
|
|
||||||
if path[-1] != "/":
|
|
||||||
path += "/"
|
|
||||||
|
|
||||||
print("正在遍历目录,请稍后...")
|
|
||||||
musics = []
|
|
||||||
fails = 0
|
|
||||||
for i in os.listdir(path): # 遍历目录,查找目标文件
|
|
||||||
match = re.match(r".*\.((mp3)|(flac))$", i)
|
|
||||||
if match:
|
|
||||||
result = load_information_from_mp3(path+match.group())
|
|
||||||
if result == "not_support":
|
|
||||||
fails += 1
|
|
||||||
print(f"文件 \"{i}\" 未包含 163 key ,跳过")
|
|
||||||
elif result == "decrypt_failed":
|
|
||||||
fails += 1
|
|
||||||
print(f"文件 \"{i}\" 内 163 key 解密失败,跳过")
|
|
||||||
elif result == "dl_err_connection":
|
|
||||||
input("获取解密结果失败!请检查网络连接是否正常,若确信自身没有问题请向作者反馈是否解密接口出现问题!")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
musics.append({"id": result['musicId'], "name": result["musicName"], "artists": result["artist"]})
|
|
||||||
|
|
||||||
# 汇报索引结果
|
|
||||||
print(f"\n索引完毕!共找到{fails+len(musics)}个目标文件\n{len(musics)}个文件已载入\n{fails}个文件失败\n")
|
|
||||||
while True:
|
|
||||||
r = input("你希望如何保存这些歌曲的歌词?\n[1]保存到刚刚输入的绝对路径中\n[2]保存到程序设定的下载路径中\n请选择: ").strip().lower()
|
|
||||||
if r == "1":
|
|
||||||
break
|
|
||||||
elif r == "2":
|
|
||||||
path = lyric_path
|
|
||||||
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], path, allinfo=True) == "dl_err_connection":
|
|
||||||
input("下载发生错误!可能是连接被拒绝!请检查网络后再试\n按回车键继续任务(该任务会被跳过)...")
|
|
||||||
input("按回车键返回...")
|
|
||||||
return
|
|
@@ -1,65 +0,0 @@
|
|||||||
"""集合设置参数"""
|
|
||||||
|
|
||||||
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] {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":
|
|
||||||
__remove_lyric_files(self.settings.lyric_path)
|
|
||||||
elif r == "s":
|
|
||||||
__save_settings(self)
|
|
||||||
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])
|
|
||||||
input("删除完毕!\n按回车继续...")
|
|
||||||
else:
|
|
||||||
input("文件夹内没有要删除的东西\n按回车继续...")
|
|
||||||
|
|
||||||
|
|
||||||
def __set_lyric_path(self):
|
|
||||||
clear()
|
|
||||||
print("允许使用相对路径和绝对路径,默认为\"./out/\"\n请*不要*使用反斜杠来确保通用性\n"
|
|
||||||
"当前值:%s\n请输入新的歌词保存路径:" % self.settings.lyric_path)
|
|
||||||
r = cinput()
|
|
||||||
if not r:
|
|
||||||
input("输入为空!\n按回车继续...")
|
|
||||||
return
|
|
||||||
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)
|
|
0
modules/submenus/__init__.py
Normal file
0
modules/submenus/__init__.py
Normal file
101
modules/submenus/settings.py
Normal file
101
modules/submenus/settings.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""集合设置参数"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
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):
|
||||||
|
"""设置菜单主循环"""
|
||||||
|
while True:
|
||||||
|
clear()
|
||||||
|
print(f"[NeteaseMusicLyricDownloader] {self.version}\n"
|
||||||
|
"[设置菜单]\n"
|
||||||
|
"[0] 返回上级\n[1] 歌曲保存路径\n[2] 清空输出文件夹内的内容\n[3] 歌词文件保存格式\n[4] 部分动态效果\n"
|
||||||
|
"[s] 将设置保存到文件")
|
||||||
|
r = rinput("请选择:")
|
||||||
|
if r == "0":
|
||||||
|
return
|
||||||
|
elif r == "1":
|
||||||
|
__set_lyric_path(self)
|
||||||
|
elif r == "2":
|
||||||
|
__remove_output_files(self)
|
||||||
|
elif r == "3":
|
||||||
|
pass
|
||||||
|
elif r == "4":
|
||||||
|
pass
|
||||||
|
elif r == "s":
|
||||||
|
__save_settings(self)
|
||||||
|
else:
|
||||||
|
input("输入无效!按回车键继续...")
|
||||||
|
|
||||||
|
|
||||||
|
def __remove_output_files(self):
|
||||||
|
while True:
|
||||||
|
clear()
|
||||||
|
print(f"[NeteaseMusicLyricDownloader] {self.version}\n"
|
||||||
|
"[设置菜单 - 删除文件]\n"
|
||||||
|
"[0] 返回上级\n[1] 清除歌词文件\n[2] 清除歌曲文件\n[a] 清除所有文件")
|
||||||
|
r = rinput("请选择:") # 选择清除的文件格式
|
||||||
|
if r == "0":
|
||||||
|
return
|
||||||
|
elif r == "1":
|
||||||
|
dellist = [".lrc"]
|
||||||
|
break
|
||||||
|
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 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) > 30:
|
||||||
|
special_text = "\033[F"
|
||||||
|
else:
|
||||||
|
special_text = "\n"
|
||||||
|
for i in range(0, len(files)):
|
||||||
|
print("删除进度: %d/%d\n -> %s%s" % (i+1, len(files), files[i], special_text), end="") # 删除进度提示
|
||||||
|
os.remove(self.settings.lyric_path+files[i])
|
||||||
|
input("\n\033[K删除完毕!\n按回车继续...")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
input("文件夹内没有要删除的东西\n按回车继续...")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def __set_lyric_path(self):
|
||||||
|
clear()
|
||||||
|
print("允许使用相对路径和绝对路径,默认为\"./out/\"\n请*不要*使用反斜杠来确保通用性\n"
|
||||||
|
"当前值:%s\n请输入新的歌词保存路径:" % self.settings.lyric_path)
|
||||||
|
r = cinput()
|
||||||
|
if not r:
|
||||||
|
input("输入为空!\n按回车继续...")
|
||||||
|
return
|
||||||
|
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按回车继续...")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def __set_lyric_format(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def __save_settings(self):
|
||||||
|
return save_settings(self.settings)
|
0
modules/utils/__init__.py
Normal file
0
modules/utils/__init__.py
Normal file
@@ -1,12 +1,12 @@
|
|||||||
"""包含一个函数,用来清空命令行的信息,自动判别系统"""
|
"""包含一个函数,用来清空命令行的信息,自动判别系统"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def clear():
|
def clear():
|
||||||
name = os.name
|
name = os.name
|
||||||
if name == "nt":
|
if name == "nt":
|
||||||
os.system("cls")
|
os.system("cls")
|
||||||
elif name == "posix":
|
elif name == "posix":
|
||||||
os.system("clear")
|
os.system("clear")
|
||||||
else:
|
else:
|
||||||
os.system("clear")
|
os.system("clear")
|
@@ -1,24 +1,24 @@
|
|||||||
"""该程序的自述信息,调用即输出"""
|
"""该程序的自述信息,调用即输出"""
|
||||||
from modules.clear_screen import clear
|
from modules.utils.clear_screen import clear
|
||||||
|
|
||||||
|
|
||||||
def print_info(self):
|
def print_info(self):
|
||||||
"""调用即输出,无返回值"""
|
"""调用即输出,无返回值"""
|
||||||
clear()
|
clear()
|
||||||
print(f"""[NeteaseMusicLyricDownloader]
|
print(f"""[NeteaseMusicLyricDownloader]
|
||||||
版本: {self.version}
|
版本: {self.version}
|
||||||
本软件开源,项目地址:https://github.com/1826013250/NeteaseMusicLyricDownloader
|
本软件开源,项目地址:https://github.com/1826013250/NeteaseMusicLyricDownloader
|
||||||
作者:David-123
|
作者:David-123
|
||||||
联系方式:
|
联系方式:
|
||||||
\tQQ:1826013250
|
\tQQ:1826013250
|
||||||
\tE-mail:1826013250@qq.com(mainly)
|
\tE-mail:1826013250@qq.com(mainly)
|
||||||
\t mc1826013250@gmail.com
|
\t mc1826013250@gmail.com
|
||||||
|
|
||||||
特别感谢:
|
特别感谢:
|
||||||
\t- nondanee - ncmdump https://github.com/nondanee/ncmdump
|
\t- nondanee - ncmdump https://github.com/nondanee/ncmdump
|
||||||
\t- QCloudHao - 保存了完整的ncmdump源代码 https://github.com/QCloudHao/ncmdump
|
\t- QCloudHao - 保存了完整的ncmdump源代码 https://github.com/QCloudHao/ncmdump
|
||||||
\t- chuyaoxin - 提供了对ncmdump以及ncm文件的详细解说 https://www.cnblogs.com/cyx-b/p/13443003.html
|
\t- chuyaoxin - 提供了对ncmdump以及ncm文件的详细解说 https://www.cnblogs.com/cyx-b/p/13443003.html
|
||||||
|
|
||||||
若程序遇到bug请提交至github上的issue""")
|
若程序遇到bug请提交至github上的issue""")
|
||||||
input("按回车键返回...")
|
input("按回车键返回...")
|
||||||
return
|
return
|
11
modules/utils/inputs.py
Normal file
11
modules/utils/inputs.py
Normal 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()
|
@@ -1,11 +1,11 @@
|
|||||||
"""该模块提供几个自定义处理输入函数"""
|
"""该模块提供几个自定义处理输入函数"""
|
||||||
|
|
||||||
|
|
||||||
def rinput(string: str = ''):
|
def rinput(string: str = ''):
|
||||||
"""当调用该函数时,同input()一样,但是返回一个去除首位空格并全部小写的str"""
|
"""当调用该函数时,同input()一样,但是返回一个去除首位空格并全部小写的str"""
|
||||||
return input(string).strip().lower()
|
return input(string).strip().lower()
|
||||||
|
|
||||||
|
|
||||||
def cinput(string: str = ''):
|
def cinput(string: str = ''):
|
||||||
"""当调用该函数时,同input()一样,但是返回一个去除首尾空格的str"""
|
"""当调用该函数时,同input()一样,但是返回一个去除首尾空格的str"""
|
||||||
return input(string).strip()
|
return input(string).strip()
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
requests
|
||||||
|
mutagen
|
||||||
|
pycryptodomex
|
||||||
|
progress
|
Reference in New Issue
Block a user