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'] 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() 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) 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()
|