免责声明

  1. 使用目的该文件夹及其子目录下的所有资源仅供学习和研究使用。其旨在为学术和研究人员提供参考和资料,任何其他目的均不适用。

  2. 非商业与非法用途严禁将此文件夹及其内容用于任何商业或非法用途。对于因违反此规定而产生的任何法律后果,用户需自行承担全部责任。

  3. 来源与版权该文件夹目录下的所有资源信息均来源于网络。如有关于版权的争议或问题,请联系原作者或权利人。本声明者与版权问题无关且不承担任何相关责任。

  4. 下载后的处理请注意,您在下载任何资源后,必须在24小时内从您的电脑或存储设备中彻底删除上述资源,无论这些资源是软件、文档还是其他形式的数据。

  5. 支持正版如果您发现某个程序或资源对您有帮助或您喜欢它,请积极支持正版。购买和注册正版软件不仅可以获取官方的支持和更新,而且可以享受更多的功能和服务,如正版的人工智能服务。

  6. 法律参考根据《计算机软件保护条例》(2002年1月1日实施)的第十七条规定:为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件的方式使用软件,可以不经软件著作权人许可,不向其支付报酬。鉴于此,我们强烈建议用户在使用该文件夹及其内容时,遵循上述法规,并确保其行为目的仅限于学习和研究软件内部的设计思想和原理。

  7. 最终解释权本免责声明的最终解释权归声明者所有。

  8. 解释权说明本声明中的“最终解释权”条款并非意在单方面施加权利或霸王条款,而是为了在可能出现的模糊或争议情况下提供清晰的指导和解释。其目的是确保本声明的内容和意图得到恰当和公正的实施,同时为用户提供更明确的方向和帮助。

  9. 开源协议请自觉遵守 CC BY-NC-SA 4.0 开源协议的相关要求
    署名 — 您必须给出 适当的署名 ,提供指向本许可协议的链接,同时 标明是否(对原始作品)作了修改 。您可以用任何合理的方式来署名,但是不得以任何方式暗示许可人为您或您的使用背书。
    非商业性使用 — 您不得将本作品用于 商业目的 。
    相同方式共享 — 如果您再混合、转换或者基于本作品进行创作,您必须基于 与原先许可协议相同的许可协议 分发您贡献的作品。
    没有附加限制 — 您不得适用法律术语或者 技术措施 从而限制其他人做许可协议允许的事情。

在使用该文件夹及其内容前,请确保已仔细阅读并完全理解上述声明和法律参考。您的使用行为将被视为对上述内容的完全接受。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import os

import requests
import json
from concurrent.futures import ThreadPoolExecutor, as_completed

from pathvalidate import sanitize_filename
from tqdm import tqdm
from hashlib import md5
import re
import datetime


class log:
@staticmethod
def time():
current_time = datetime.datetime.now()
formatted_time = current_time.strftime('%Y/%m/%d %H:%M:%S')
return formatted_time

@staticmethod
def info(msg):
print(f'\033[0;34m{log.time()} [INFO]\033[0m ' + msg)

@staticmethod
def error(msg):
print(f'\033[1;31m{log.time()} [INFO]\033[0m ', msg)


class NetMusic:
def __init__(self):
self.NETMUSIC_URL_V6 = "https://music.163.com/api/v6/playlist/detail"
self.NETMUSIC_URL_V3 = "https://music.163.com/api/v3/song/detail"
self.CHUNK_SIZE = 500

def get_songs_info(self, link):
"""
从歌单链接获取歌单信息,包括歌单名、歌曲 ID 列表和歌曲总数
"""
song_list_id = re.findall(r'[\?\&]id=(\d+)', link)[0]
try:
resp = requests.post(self.NETMUSIC_URL_V6, data={"id": song_list_id})
resp.raise_for_status()
except requests.RequestException as e:
print(f"Failed to fetch song info: {e}")
return None

try:
song_ids_resp = resp.json()
except json.JSONDecodeError as e:
print(f"Failed to parse response: {e}")
return None

if song_ids_resp.get("code") == 401:
print(f"No access to playlist: {song_list_id}")
return None

return song_ids_resp

def batch_get_songs(self, missing_keys):
"""
批量从网易云音乐 API 获取歌曲详情
"""
chunks = [missing_keys[i:i + self.CHUNK_SIZE] for i in range(0, len(missing_keys), self.CHUNK_SIZE)]
result_map = {}

with ThreadPoolExecutor() as executor:
futures = []
for chunk in chunks:
payload = {"c": json.dumps([{"id": key} for key in chunk])}
futures.append(executor.submit(requests.post, self.NETMUSIC_URL_V3, data=payload))

for future in as_completed(futures):
try:
resp = future.result()
resp.raise_for_status()
songs = resp.json().get("songs", [])
for song in songs:
authors = " / ".join(artist["name"] for artist in song.get("ar", []))
song_info = f"{song.get('name', '')} - {authors}"
result_map[song["id"]] = song_info
except Exception as e:
print(f"Failed to process chunk: {e}")

return result_map

def net_music_discover(self, link):
"""
获取歌单信息并返回歌单详情,包括歌单名、歌曲详情和歌曲总数
"""
song_ids_resp = self.get_songs_info(link)
if not song_ids_resp:
return None

song_list_name = song_ids_resp["playlist"]["name"]
track_ids = song_ids_resp["playlist"]["trackIds"]
track_count = song_ids_resp["playlist"]["trackCount"]

missing_keys = [track["id"] for track in track_ids]
result_map = self.batch_get_songs(missing_keys)

return {"name": song_list_name, "songs": result_map, "songs_count": track_count}

def download(self, Id, path=''):
headers = {
'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36 EdgA/131.0.0.0",
'Accept': "application/json, text/plain, */*",
'Accept-Encoding': "gzip, deflate, br, zstd",
'sec-ch-ua-full-version-list': "\"Microsoft Edge\";v=\"131.0.2903.145\", \"Chromium\";v=\"131.0.6778.265\", \"Not_A Brand\";v=\"24.0.0.0\"",
'sec-ch-ua-platform': "\"Android\"",
'sec-ch-ua': "\"Microsoft Edge\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
'sec-ch-ua-model': "\"XT2175-2\"",
'sec-ch-ua-mobile': "?1",
'sec-ch-ua-platform-version': "\"12.0.0\"",
'origin': "https://api.toubiec.cn",
'sec-fetch-site': "same-origin",
'sec-fetch-mode': "cors",
'sec-fetch-dest': "empty",
'referer': "https://api.toubiec.cn/wyapi.html",
'accept-language': "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
'priority': "u=1, i"
}
token = requests.post("https://api.toubiec.cn/api/get-token.php", headers=headers).json()['token']
# print(token)
headers = {
'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36 EdgA/131.0.0.0",
'Accept': "application/json",
'Accept-Encoding': "gzip",
'Content-Type': "application/json",
'sec-ch-ua-full-version-list': "\"Microsoft Edge\";v=\"131.0.2903.145\", \"Chromium\";v=\"131.0.6778.265\", \"Not_A Brand\";v=\"24.0.0.0\"",
'sec-ch-ua-platform': "\"Android\"",
'authorization': "Bearer " + token,
'sec-ch-ua': "\"Microsoft Edge\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
'sec-ch-ua-model': "\"XT2175-2\"",
'sec-ch-ua-mobile': "?1",
'sec-ch-ua-platform-version': "\"12.0.0\"",
'origin': "https://api.toubiec.cn",
'sec-fetch-site': "same-origin",
'sec-fetch-mode': "cors",
'sec-fetch-dest': "empty",
'referer': "https://api.toubiec.cn/wyapi.html",
'accept-language': "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
'priority': "u=1, i"
}
try:
response = requests.post('https://api.toubiec.cn/api/music_v1.php',
data=json.dumps({
"url": "https://y.music.163.com/m/song?id=" + str(Id),
"level": "exhigh",
"type": "song",
"token": md5(token.encode('UTF-8')).hexdigest()
}),
headers=headers).json()
# print(response)
if response['status'] == 200:
name = response['song_info']['name']
log.info('获得歌曲名称:' + name)
log.info('下载链接:' + response['url_info']['url'])
music = requests.get(response['url_info']['url'], stream=True)
# print(music.text)
total_size = int(music.headers.get('content-length', 0))
progress_bar = tqdm(
total=total_size,
unit='B',
unit_scale=True,
desc=f'\033[1;33m{name}\033[0m',
unit_divisor=1024,
colour='#388E3C'
)
with open(os.path.join(path.replace(' ', ''), sanitize_filename(name, '_')
+ '.' + response['url_info']['type']), 'wb') as file:
for data in music.iter_content(chunk_size=1024):
if data:
progress_bar.update(len(data))
file.write(data)
progress_bar.close()
except Exception as e:
log.error(e)


# 使用示例
def main():
net_music = NetMusic()
while True:
pattern = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+') # 匹配模式

string = input('请输入链接:')
if 'exit' in string:
exit()
url = re.findall(pattern, string)[0]
log.info(f'\033[1;33m获得链接:\033[0m' + url)
if '163cn.tv' in url:
response = requests.get(url)
Id = re.findall(r'song[\?\&]id=(\d+)', response.text)[0]
if Id:
net_music.download(Id)
else:
log.error('歌曲 ID 获取失败')
Id = input('请输入歌曲ID([N]退出):')
if 'N' not in Id:
net_music.download(Id)
if 'playlist' in url:
results = net_music.net_music_discover(url)
print('\033[1;33m歌单名称:\033[0m\033[1;32m' + results['name'] + '\033[0m')
results['name'] = sanitize_filename(results['name'])
if not os.path.exists(results['name'].replace(' ', '')):
os.makedirs(results['name'].replace(' ', ''))
print('\033[1;33m歌单歌曲数量:\033[0m' + str(results['songs_count']))
for i in range(0, results['songs_count']):
print(f'\033[1;38m{str(i + 1)}.\033[0m \033[0;32m{list(results['songs'].values())[i]}\033[0m')
par = input('请选择下载歌曲(A:全部;序号:单独下载(注意:‘,’隔开),-序号:不下载):')
if 'A' in par:
for i in list(results['songs'].keys()):
net_music.download(i, results['name'])
elif '-' not in par:
par = par.replace(',', ',').split(',')
for i in par:
net_music.download(list(results['songs'].keys())[int(i) - 1], results['name'])
else:
par = par.replace(',', ',').replace('-', '').split(',')
par = list(map(lambda x: int(x) - 1, par))
for i in range(0, len(results['songs'].keys())):
if i not in par:
net_music.download(list(results['songs'].keys())[i], results['name'])

main()