Skip to content

Commit 9d9584c

Browse files
authored
更新 NetEase_Cloud_Music_Download (#18)
* Update dev_package_and_upload.yml (#17) * 更新 NetEase_Cloud_Music_Download
1 parent cf04873 commit 9d9584c

4 files changed

Lines changed: 273 additions & 6 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99

1010
## [网易云音乐歌单批量下载歌曲][1]
1111

12-
最新版:[v.24-10-06][2]
12+
使用 metting api 批量下载网易云音乐歌曲
13+
14+
注意,这程序严重依赖第三方的 metting api
1315

1416

1517
## [键盘监听][4]
1618

17-
最新版:[v.24.07.16][5]
19+
键盘监听
1820

1921

2022
## [psql_terminal][7]

package/nuitka_config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
output-name-template: '{{name}}_{{version}}_nuitka_{{os}}_{{arch}}'
3434

3535
- name: 'NetEase_Cloud_Music_Download'
36-
version: 'v.24-10-06'
37-
python-file: '网易云音乐歌单批量下载歌曲\v.24-10-06.py'
36+
version: 'v.25-07-08'
37+
python-file: '网易云音乐歌单批量下载歌曲\v.25-07-08.py'
3838
install-requirements: [
3939
'quote',
4040
'requests',

网易云音乐歌单批量下载歌曲/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
注意,这程序严重依赖第三方的 metting api
66

7-
最新版:[v.24-10-06][7]
7+
最新版:[v.25-07-08][8]
88

99
## 说明
1010

@@ -24,6 +24,9 @@ python ***.py
2424

2525
## 日志
2626

27+
- [v.25-07-08][8]
28+
- 删去 `#!/usr/bin/python` 指令
29+
2730
- [v.24-10-06][7]
2831
- 小优化
2932
- 添加了是否下载小于60秒音乐的选项
@@ -60,4 +63,5 @@ python ***.py
6063
[4]: https://github.com/God-2077/python-code/blob/main/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%E6%AD%8C%E5%8D%95%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E6%AD%8C%E6%9B%B2/v.24-04-05.%E6%9C%80%E7%BB%88%E7%89%88.py
6164
[5]: https://github.com/God-2077/python-code/blob/main/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%E6%AD%8C%E5%8D%95%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E6%AD%8C%E6%9B%B2/v.24-07-18.py
6265
[6]: https://github.com/God-2077/python-code/blob/main/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%E6%AD%8C%E5%8D%95%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E6%AD%8C%E6%9B%B2/v.24-07-19.py
63-
[7]: https://github.com/God-2077/python-code/blob/main/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%E6%AD%8C%E5%8D%95%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E6%AD%8C%E6%9B%B2/v.24-10-06.py
66+
[7]: https://github.com/God-2077/python-code/blob/main/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%E6%AD%8C%E5%8D%95%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E6%AD%8C%E6%9B%B2/v.24-10-06.py
67+
[8]: v.24-10-06.py
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# -*- coding: UTF-8 -*-
2+
3+
import os
4+
import requests
5+
from mutagen.mp3 import MP3
6+
import time
7+
import signal
8+
import sys
9+
import re
10+
from tabulate import tabulate
11+
12+
13+
def download_file(url, file_path, file_type, index, total_files, timeout=10):
14+
try:
15+
response = requests.get(url, stream=True, timeout=timeout)
16+
response.raise_for_status()
17+
total_size = int(response.headers.get('content-length', 0))
18+
downloaded_size = 0
19+
20+
with open(file_path, "wb") as file:
21+
for data in response.iter_content(chunk_size=4096):
22+
downloaded_size += len(data)
23+
file.write(data)
24+
progress = downloaded_size / total_size * 100 if total_size > 0 else 0
25+
print(f"正在下载 [{index}/{total_files}][{file_type}] {file_path},进度:{progress:.2f}%\r", end="")
26+
27+
print(f"下载完成 [{index}/{total_files}][{file_type}] {file_path}")
28+
return True
29+
except requests.exceptions.RequestException as e:
30+
print(f"下载 [{index}/{total_files}][{file_type}] {file_path} 失败:{e}")
31+
return False
32+
33+
34+
def download_lyrics(url, lrc_path, song_index, total_songs):
35+
try:
36+
response = requests.get(url, timeout=10)
37+
response.raise_for_status()
38+
with open(lrc_path, "w", encoding="utf-8") as lrc_file:
39+
lrc_file.write(response.content.decode('utf-8'))
40+
print(f"下载完成 [{song_index}/{total_songs}][LRC] {lrc_path}")
41+
return True
42+
except requests.exceptions.RequestException as e:
43+
print(f"下载歌词失败:{e}")
44+
return False
45+
46+
47+
def safe_filename(filename):
48+
invalid_chars = '\\/:*?"<>|'
49+
for char in invalid_chars:
50+
filename = filename.replace(char, '_')
51+
return filename
52+
53+
54+
def delete_file(file_path):
55+
if os.path.exists(file_path):
56+
os.remove(file_path)
57+
print(f"删除文件:{file_path}")
58+
59+
60+
def song_table(data):
61+
table_data = []
62+
for idx, item in enumerate(data, start=1):
63+
name = item['name']
64+
artist = item['artist']
65+
url_id = re.search(r'\d+', item['url']).group()
66+
table_data.append([idx, name, artist, url_id])
67+
table_headers = ["序号", "标题", "艺术家", "ID"]
68+
table = tabulate(table_data, table_headers, tablefmt="pipe")
69+
print(table)
70+
71+
72+
def exit_ctrl_c(sig, frame):
73+
print("\n退出程序...")
74+
sys.exit(0)
75+
76+
77+
def welcome():
78+
print("welcome")
79+
print("欢迎使用我开发的小程序")
80+
print("Github Rope: https://github.com/God-2077/python-code/")
81+
print("-------------------------")
82+
print("网易云音乐歌单批量下载歌曲")
83+
84+
85+
def download_playlist(playlist_id, download_path):
86+
if not playlist_id.isdigit():
87+
print("歌单ID必须为数字")
88+
return
89+
90+
error_song_name = []
91+
error_song_id = []
92+
93+
api_urls = [
94+
"https://meting.qjqq.cn/?type=playlist&id=",
95+
"https://api.injahow.cn/meting/?type=playlist&id=",
96+
"https://meting-api.mnchen.cn/?type=playlist&id=",
97+
"https://meting-api.mlj-dragon.cn/meting/?type=playlist&id="
98+
]
99+
100+
selected_api = None
101+
data = None
102+
103+
for api_url in api_urls:
104+
try:
105+
response = requests.get(f"{api_url}{playlist_id}", timeout=10)
106+
# if requests.status_codes != 200:
107+
# print("出错了...")
108+
# print(f"状态码:{requests.status_codes}")
109+
# return
110+
response.raise_for_status()
111+
data = response.json()
112+
selected_api = api_url
113+
if 'error' in data:
114+
error = data.get("error", "")
115+
print("出错了...")
116+
print(f"错误详细:{error}")
117+
return
118+
break
119+
except requests.exceptions.RequestException as e:
120+
print(f"API {api_url} 请求失败:{e}")
121+
continue
122+
123+
if not data:
124+
print("所有API都无法获取数据")
125+
return
126+
127+
os.makedirs(download_path, exist_ok=True)
128+
129+
print(f"Meting API: {selected_api}")
130+
131+
total_songs = len(data)
132+
print(f"歌单共有 {total_songs} 首歌曲")
133+
song_table(data)
134+
envisage_size = total_songs * 7.7
135+
print(f"歌单共有 {total_songs} 首歌曲,预计歌曲文件总大小为 {envisage_size} MB")
136+
chose = str(input("是否继续下载?(yes): "))
137+
if chose not in ["y", "", "yes"]:
138+
print("退出程序...")
139+
sys.exit(0)
140+
chose = str(input("是否下载小于 60 秒的歌曲(可能为试听音乐)?(not): "))
141+
if chose not in ["y", "", "yes"]:
142+
downtrymusic = 1
143+
else:
144+
downtrymusic = 0
145+
successful_downloads = 0
146+
failed_downloads = []
147+
148+
def signal_handler(sig, frame):
149+
print("\n检测到Ctrl+C, exiting gracefully...")
150+
print(f"程序运行完成,共 {total_songs} 首歌曲,成功下载 {successful_downloads} 首歌曲")
151+
if failed_downloads:
152+
print(f"共有 {len(failed_downloads)} 首歌曲下载失败")
153+
print("失败列表如下")
154+
table_data = [[i + 1, error_song_name[i], error_song_id[i]] for i in range(len(error_song_name))]
155+
print(tabulate(table_data, headers=["序号", "标题 - 艺术家", "ID"], tablefmt="grid"))
156+
sys.exit(0)
157+
158+
signal.signal(signal.SIGINT, signal_handler)
159+
160+
for index, song in enumerate(data, start=1):
161+
name = song.get("name", "")
162+
artist = song.get("artist", "")
163+
url = song.get("url", "")
164+
lrc = song.get("lrc", "")
165+
pic = song.get("pic", "")
166+
167+
if not name or not artist or not url or not lrc:
168+
print(f"歌曲信息不完整:{song}")
169+
continue
170+
171+
song_filename = f"{safe_filename(name)} - {safe_filename(artist)}.mp3"
172+
song_name = f"{safe_filename(name)} - {safe_filename(artist)}"
173+
song_path = os.path.join(download_path, song_filename)
174+
175+
retry_count = 0
176+
while retry_count < 5:
177+
if download_file(url, song_path, "MP3", index, total_songs):
178+
successful_downloads += 1
179+
state = True
180+
break
181+
else:
182+
retry_count += 1
183+
print(f"重试下载 [{index}/{total_songs}][MP3] {song_path},次数:{retry_count}")
184+
state = False
185+
time.sleep(1)
186+
if state == False:
187+
print("")
188+
match = re.search(r'\d+', url)
189+
error_song_id.append(int(match.group()))
190+
error_song_name.append(song_name)
191+
failed_downloads.append(match)
192+
193+
if state == True:
194+
try:
195+
audio = MP3(song_path)
196+
audio_duration = audio.info.length
197+
if downtrymusic == 1:
198+
if audio_duration < 60:
199+
print(f"歌曲时长小于一分钟,删除歌曲和取消下载歌词和图片:{song_path}")
200+
delete_file(song_path)
201+
audio_duration_TF = False
202+
successful_downloads -= 1
203+
match = re.search(r'\d+', url)
204+
error_song_id.append(int(match.group()))
205+
error_song_name.append(song_name)
206+
else:
207+
print(f"歌曲时长为 {audio_duration} 秒")
208+
audio_duration_TF = True
209+
else:
210+
print(f"歌曲时长为 {audio_duration} 秒")
211+
audio_duration_TF = True
212+
except Exception as e:
213+
print(f"无法获取歌曲时长:{e}")
214+
print("应该为 VIP 单曲,删除歌曲文件和取消下载歌词和图片")
215+
delete_file(song_path)
216+
audio_duration_TF = False
217+
successful_downloads -= 1
218+
match = re.search(r'\d+', url)
219+
error_song_id.append(int(match.group()))
220+
error_song_name.append(song_name)
221+
failed_downloads.append(match)
222+
223+
if audio_duration_TF:
224+
lrc_filename = f"{safe_filename(name)} - {safe_filename(artist)}.lrc"
225+
lrc_path = os.path.join(download_path, lrc_filename)
226+
227+
retry_count = 0
228+
while retry_count < 5:
229+
if download_lyrics(lrc, lrc_path, index, total_songs):
230+
break
231+
else:
232+
retry_count += 1
233+
print(f"重试下载 [{index}/{total_songs}][LRC] {lrc_path},次数:{retry_count}")
234+
time.sleep(1)
235+
236+
pic_filename = f"{safe_filename(name)} - {safe_filename(artist)}.jpg"
237+
pic_path = os.path.join(download_path, pic_filename)
238+
239+
retry_count = 0
240+
while retry_count < 5:
241+
if download_file(pic, pic_path, "PIC", index, total_songs):
242+
break
243+
else:
244+
retry_count += 1
245+
print(f"重试下载 [{index}/{total_songs}][PIC] {pic_path},次数:{retry_count}")
246+
time.sleep(1)
247+
248+
print(f"程序运行完成,共 {total_songs} 首歌曲,成功下载 {successful_downloads} 首歌曲")
249+
if failed_downloads or error_song_name:
250+
print(f"共有 {len(failed_downloads)} 首歌曲下载失败")
251+
print("失败列表如下")
252+
table_data = [[i + 1, error_song_name[i], error_song_id[i]] for i in range(len(error_song_name))]
253+
print(tabulate(table_data, headers=["序号", "标题 - 艺术家", "ID"], tablefmt="grid"))
254+
255+
256+
if __name__ == "__main__":
257+
signal.signal(signal.SIGINT, exit_ctrl_c)
258+
welcome()
259+
playlist_id = input("歌单ID:")
260+
download_path = input(r"下载路径(默认为 ./down):") or r"./down"
261+
download_playlist(playlist_id, download_path)

0 commit comments

Comments
 (0)