通过获取本地安装的wy云音乐播放列表,自动下载播放列表的歌曲,增加了重复歌曲只保留一首
通过播放列表可以批量多线程下载
[Python] 纯文本查看 复制代码import requests
import re
import hashlib
import json
import os
import csv
import platform
from concurrent.futures import ThreadPoolExecutor
def md5(token):
"""
计算输入字符串的MD5哈希值。
"""
return hashlib.md5(token.encode()).hexdigest()
def download_normal_quality(song_id, song_name, folder):
"""
下载普通音质的歌曲。
"""
try:
url = f"https://music.163.com/song?id={song_id}"
response = requests.get(url)
html = response.text
title_match = re.search(r'(.*?)', html)
if title_match:
title = title_match.group(1).split('-')[0].strip()
title = title.replace("/", "、")
else:
title = song_name
download_url = f"http://music.163.com/song/media/outer/url?id={song_id}"
response = requests.get(download_url, allow_redirects=True)
file_path = os.path.join(folder, f"{title}.mp3")
with open(file_path, "wb") as f:
f.write(response.content)
return True, f"普通音质下载完成:{file_path}"
except Exception as e:
return False, f"普通音质下载失败:{song_id} - {song_name},错误:{e}"
def download_high_quality(song_id, song_name, folder):
"""
下载高质量(exhigh)的歌曲。
"""
try:
token_url = "https://api.toubiec.cn/api/get-token.php"
response = requests.post(token_url)
token_data = response.json()
token = token_data["token"]
token_md5 = md5(token)
payload = {
"url": f"https://music.163.com/#/song?id={song_id}",
"level": "exhigh",
"type": "song",
"token": token_md5
}
music_url = "https://api.toubiec.cn/api/music_v1.php"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.post(music_url, json=payload, headers=headers)
data = response.json()
download_url = data["url_info"]["url"]
song_name = song_name.replace(":", ":")
song_artist = data["song_info"]["artist"].replace("/", "、")
file_suffix = data["url_info"]["type"]
file_path = os.path.join(folder, f"{song_name} - {song_artist}.{file_suffix}")
response = requests.get(download_url)
with open(file_path, "wb") as f:
f.write(response.content)
return True, f"高质量下载完成:{file_path}"
except Exception as e:
return False, f"高质量下载失败:{song_id} - {song_name},错误:{e}"
def download_song(song, folder):
"""
下载单首歌曲,优先高质量,失败后尝试普通音质。
返回:(歌曲信息, 是否成功, 消息)
"""
song_id = song['id']
song_name = song['name']
# 优先尝试高质量下载
success, message = download_high_quality(song_id, song_name, folder)
if not success:
# 高质量失败,尝试普通音质
success, message = download_normal_quality(song_id, song_name, folder)
return song, success, message
def get_song_list():
"""
从网易云音乐的playingList文件中提取歌曲ID和名称,并返回排序后的列表。
"""
# 动态获取当前Windows用户名
username = os.getlogin()
# 构建playingList文件路径
playing_list_path = rf"C:\Users\{username}\AppData\Local\Netease\CloudMusic\webdata\file\playingList"
# 检查文件是否存在
if not os.path.exists(playing_list_path):
print(f"错误:文件 {playing_list_path} 不存在。")
print("请确保网易云音乐客户端已安装、登录并打开歌单。")
exit(1)
try:
# 读取playingList文件
with open(playing_list_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 提取歌曲ID和名称
songs = data['list']
song_list = []
for song in songs:
song_id = song['id']
song_name = song['track']['name']
song_list.append({'id': song_id, 'name': song_name})
# 按displayOrder排序
song_list.sort(key=lambda x: songs[[s['id'] for s in songs].index(x['id'])]['displayOrder'])
return song_list
except json.JSONDecodeError as e:
print(f"JSON解析错误: {e}")
print("请确保playingList文件为有效的JSON格式。")
exit(1)
except Exception as e:
print(f"发生错误: {e}")
print("请检查文件路径、权限和网易云音乐客户端状态。")
exit(1)
def normalize_song_name(song_name):
"""
规范化歌曲名称,移除括号及内容,转换为小写并去除首尾空格。
"""
# 移除括号及内容(如 (LIVE)、[Remix] 等)
song_name = re.sub(r'\s*[\(\[\{].*?[\)\]\}]', '', song_name)
# 移除常见修饰词(大小写不敏感)
song_name = re.sub(r'\s*(live|remix|version|edit|mix)\s*', '', song_name, flags=re.IGNORECASE)
# 转换为小写并去除首尾空格
return song_name.lower().strip()
def save_failed_downloads(failed_songs):
"""
将下载失败的歌曲信息保存到CSV文件。
"""
if failed_songs:
with open('failed_downloads.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['song_id', 'song_name'])
writer.writeheader()
for song in failed_songs:
writer.writerow({'song_id': song['id'], 'song_name': song['name']})
print(f"下载失败的歌曲已保存到 failed_downloads.csv,共 {len(failed_songs)} 首")
else:
print("所有歌曲下载成功,无失败记录")
if __name__ == "__main__":
# 提示用户确认
print("本软件只获取本机播放列表歌曲,请确保电脑已经安装网易云音乐,播放列表有歌曲")
while True:
user_input = input("是否继续运行?(输入 Y/N):").strip().lower()
if user_input == "y":
break
elif user_input == "n":
print("用户选择退出,程序关闭。")
exit(0)
else:
print("输入无效,请输入“Y”或“N”。")
# 设置下载文件夹
download_folder = "downloaded_songs"
if not os.path.exists(download_folder):
os.makedirs(download_folder)
# 获取歌曲列表
songs = get_song_list()
# 初始化计数器
success_count = 0
failed_count = 0
# 记录下载失败的歌曲
failed_songs = []
# 记录已处理的歌曲名称(规范化后)
processed_names = set()
# 过滤重复歌曲
filtered_songs = []
for song in songs:
# 规范化歌曲名称
normalized_name = normalize_song_name(song['name'])
# 检查是否已处理过同名歌曲
if normalized_name in processed_names:
print(f"跳过重复歌曲:{song['id']} - {song['name']}")
continue
# 添加到已处理集合
processed_names.add(normalized_name)
filtered_songs.append(song)
print(f"\n即将下载 {len(filtered_songs)} 首歌曲(已移除重复歌曲)...")
# 使用多线程下载
with ThreadPoolExecutor(max_workers=5) as executor:
# 创建下载任务
futures = [executor.submit(download_song, song, download_folder) for song in filtered_songs]
# 等待所有任务完成并打印结果
for future in futures:
song, success, message = future.result()
print(message)
if success:
success_count += 1
else:
failed_count += 1
failed_songs.append(song)
# 显示最终统计信息
print(f"\n下载完成!")
print(f"总共尝试 {len(filtered_songs)} 首歌曲")
print(f"成功下载:{success_count} 首")
print(f"失败下载:{failed_count} 首")
# 保存失败的歌曲到CSV
save_failed_downloads(failed_songs)
# 等待用户按任意键关闭
input("按任意键关闭窗口...")