├── NetEaseApi.py ├── NetEaseMusicSync.py ├── TiantianJingtingMusicDownloader.py ├── GeneralMusicDownloader.py └── README.md /NetEaseApi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author : Huangcc 3 | 4 | import json 5 | import base64 6 | from Crypto.Cipher import AES 7 | import requests 8 | import hashlib 9 | import os 10 | 11 | 12 | class NetEaseApi: 13 | # 基于 gaoyuan427 的github项目进行修改 https://github.com/gaoyuan427/NetEaseMusicSync 14 | # 感谢 gaoyuan427 提供的网易云音乐api 15 | 16 | def __init__(self): 17 | self.session = requests.session() 18 | self.cookies = None 19 | 20 | def _aes_encrypt(self, text, sec_key): 21 | pad = 16 - len(text) % 16 22 | text = text + pad * chr(pad) 23 | encryptor = AES.new(sec_key, 2, '0102030405060708') 24 | cipher_text = encryptor.encrypt(text) 25 | cipher_text = base64.b64encode(cipher_text) 26 | return cipher_text 27 | 28 | def _rsa_encrypt(self, text, pub_key, modulus): 29 | text = text[::-1] 30 | rs = int(text.encode('hex'), 16) ** int(pub_key, 16) % int(modulus, 16) 31 | return format(rs, 'x').zfill(256) 32 | 33 | def _create_secret_key(self, size): 34 | return (''.join(map(lambda xx: (hex(ord(xx))[2:]), os.urandom(size))))[0:16] 35 | 36 | def get_info_from_nem(self, url, params): 37 | headers = { 38 | 'Accept': '*/*', 39 | 'Accept-Encoding': 'gzip,deflate,sdch', 40 | 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 41 | 'Connection': 'keep-alive', 42 | 'Content-Type': 'application/x-www-form-urlencoded', 43 | 'Host': 'music.163.com', 44 | 'Referer': 'http://music.163.com/', 45 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36' 46 | } 47 | modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 48 | nonce = '0CoJUm6Qyw8W8jud' 49 | pub_key = '010001' 50 | params = json.dumps(params) 51 | sec_key = self._create_secret_key(16) 52 | enc_text = self._aes_encrypt(self._aes_encrypt(params, nonce), sec_key) 53 | enc_sec_key = self._rsa_encrypt(sec_key, pub_key, modulus) 54 | data = { 55 | 'params': enc_text, 56 | 'encSecKey': enc_sec_key 57 | } 58 | req = self.session.post(url, headers=headers, data=data) 59 | return req.json() 60 | 61 | def get_play_list(self, uid): 62 | url = 'http://music.163.com/weapi/user/playlist?csrf_token=' 63 | params = { 64 | 'offset': '0', 65 | 'limit': '9999', 66 | 'uid': str(uid) 67 | } 68 | return self.get_info_from_nem(url, params) 69 | 70 | def get_play_list_info(self, music_list_id): 71 | url = 'http://music.163.com/weapi/playlist/detail?csrf_token=' 72 | params = {'id': str(music_list_id)} 73 | return self.get_info_from_nem(url, params) 74 | 75 | def get_music_url(self, music_id): 76 | url = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token=' 77 | if type([]) != type(music_id): 78 | music_id = [music_id] 79 | params = { 80 | "ids": str(music_id), 81 | "br": '320000' 82 | } 83 | return self.get_info_from_nem(url, params) 84 | 85 | def cellphone_login(self, username, password): 86 | url = 'http://music.163.com/weapi/login/cellphone?csrf_token=' 87 | data = {'phone': username, 'password': hashlib.md5(password).hexdigest(), 88 | 'rememberLogin': "true"} 89 | return self.get_info_from_nem(url, data) 90 | 91 | def get_daily_recommend(self): 92 | url = 'http://music.163.com/weapi/v2/discovery/recommend/songs?csrf_token=' 93 | data = { 94 | 'csrf_token': '', 95 | 'limit': '999', 96 | 'offset': '0', 97 | 'total': 'true' 98 | } 99 | return self.get_info_from_nem(url, data) 100 | -------------------------------------------------------------------------------- /NetEaseMusicSync.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author : Huangcc 3 | 4 | import os 5 | import re 6 | import datetime 7 | import traceback 8 | from NetEaseApi import NetEaseApi 9 | from TiantianJingtingMusicDownloader import TiantianJingtiingMusicDownloader 10 | 11 | 12 | def to_win_safe(s): 13 | return re.sub('[\/:*?"<>|]', '-', s) 14 | 15 | 16 | class NetEaseMusicSync: 17 | def __init__(self, cellphone, password): 18 | self.cellphone = cellphone 19 | self.password = password 20 | self.netease_api = NetEaseApi() 21 | self.downloader = TiantianJingtiingMusicDownloader() 22 | 23 | def login(self): 24 | user_data = self.netease_api.cellphone_login(self.cellphone, self.password) 25 | if user_data["code"] != 200: 26 | raise ValueError("login fail! phone: %s, password: %s, response: %s" % 27 | (self.cellphone, self.password, user_data)) 28 | return user_data['account']['id'] 29 | 30 | def sync_song_lists(self, uid): 31 | play_list = self.netease_api.get_play_list(uid) 32 | for music_list in play_list['playlist']: 33 | list_name = music_list['name'] 34 | # 去除特殊字符 35 | list_name = to_win_safe(list_name) 36 | 37 | music_dir = os.path.join('music', list_name) 38 | if not os.path.exists(music_dir): 39 | os.mkdir(music_dir) 40 | 41 | music_list_info = self.netease_api.get_play_list_info(music_list['id']) 42 | if music_list_info['code'] == 200: 43 | music_name_artist = [] 44 | exist_filename_list = os.listdir(music_dir) 45 | # 获取歌单中的音乐名字和演唱者 46 | for music in music_list_info['result']['tracks']: 47 | music_name_artist.append((to_win_safe(music['name']), to_win_safe(music['artists'][0]['name']) if music['artists'] else "")) 48 | # 过滤已下载的音乐 49 | music_name_artist = filter(lambda x: ('%s - %s.mp3' % (x[1], x[0])) not in exist_filename_list, 50 | music_name_artist) 51 | # 搜索下载对应的音乐 52 | num_of_music = len(music_name_artist) 53 | curr = 1 54 | for name, artist in music_name_artist: 55 | try: 56 | print '-*- %d/%d downloading the song %s from list %s -*-' % (curr, num_of_music, name, list_name) 57 | self.downloader.search_and_download_song('%s %s' % (name, artist), music_dir, None) 58 | except Exception: 59 | traceback.print_exc() 60 | curr += 1 61 | 62 | def sync_daily_recommend(self): 63 | current_date = (datetime.datetime.now() - datetime.timedelta(hours=6)).strftime('%Y%m%d') # 每天6点更新 64 | daily_dir = os.path.join('music', current_date) 65 | if not os.path.exists(daily_dir): 66 | os.mkdir(daily_dir) 67 | 68 | daily_data = self.netease_api.get_daily_recommend() 69 | if daily_data['code'] == 200: 70 | music_list = daily_data['recommend'] 71 | music_name_artist = [] 72 | exist_filename_list = os.listdir(daily_dir) 73 | # 获取歌单中的音乐名字和演唱者 74 | for music in music_list: 75 | music_name_artist.append((music['name'], music['artists'][0]['name'] if music['artists'] else "")) 76 | # 过滤已下载的音乐 77 | music_name_artist = filter(lambda x: ('%s - %s.mp3' % (x[1], x[0])) not in exist_filename_list, 78 | music_name_artist) 79 | # 搜索下载对应的音乐 80 | num_of_music = len(music_name_artist) 81 | curr = 1 82 | for name, artist in music_name_artist: 83 | print '-*- %d/%d downloading the song %s from list %s -*-' % (curr, num_of_music, name, current_date) 84 | self.downloader.search_and_download_song('%s %s' % (name, artist), daily_dir, None) 85 | curr += 1 86 | else: 87 | print 'Can not get daily recommend of %s' % current_date 88 | 89 | 90 | if __name__ == '__main__': 91 | my_netease = NetEaseMusicSync('your_phone_number', 'your_password') 92 | uid = my_netease.login() 93 | my_netease.sync_song_lists(uid) # 同步歌单 94 | my_netease.sync_daily_recommend() # 同步每日推荐 95 | -------------------------------------------------------------------------------- /TiantianJingtingMusicDownloader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author : Huangcc 3 | 4 | import requests 5 | import time 6 | import random 7 | import json 8 | import os 9 | import urllib 10 | import traceback 11 | import codecs 12 | import eyed3 13 | 14 | 15 | def timestamp_ms_string(): 16 | return '%d' % (time.time() * 1000) 17 | 18 | 19 | def random_int_string(length): 20 | start = int('1' + '0' * (length - 1)) 21 | end = int('9' * length) 22 | return '%d' % random.randint(start, end) 23 | 24 | 25 | class TiantianJingtiingMusicDownloader: 26 | default_headers = { 27 | 'Host': '47.112.23.238', 28 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0', 29 | 'Accept': 'application/json,*/*', 30 | 'Accept-Language': 'en-US,en;q=0.5', 31 | 'Accept-Encoding': 'gzip, deflate', 32 | 'Referer': 'http://47.112.23.238/', 33 | 'Connection': 'keep-alive', 34 | 'Pragma': 'no-cache', 35 | 'Cache-Control': 'no-cache', 36 | 'Origin': 'http://47.112.23.238', 37 | } 38 | 39 | query_song_data = { 40 | 'number': '20', 41 | 'type': 'netease', # 按需更改 42 | 'musicName': '幼稚完 林峰', # 按需更改 43 | } 44 | get_lrc_params = { 45 | 'lrc': '513360721', # 按需更改 46 | 'type': 'netease', # 按需更改 47 | } 48 | 49 | source_list = ['netease', 'qq', 'kugou',] # 下载源 50 | 51 | api_url = 'http://47.112.23.238/Music/getMusicList' 52 | lrc_api_url = 'http://47.112.23.238/MusicAPI/getLrc/' 53 | 54 | default_download_folder = 'music' 55 | default_pic_folder = 'pic' 56 | 57 | def __init__(self): 58 | self.session = requests.Session() 59 | if not os.path.exists(self.default_download_folder): 60 | os.mkdir(self.default_download_folder) 61 | if not os.path.exists(self.default_pic_folder): 62 | os.mkdir(self.default_pic_folder) 63 | 64 | def get_song_info_from_source(self, search_keyword, source='netease'): 65 | if source not in self.source_list: 66 | print 'Unsupported source expected the one of that: %s' % self.source_list 67 | return False 68 | query_song_data = self.query_song_data.copy() 69 | query_song_data['musicName'] = search_keyword 70 | query_song_data['type'] = source 71 | 72 | resp = self.session.post(url=self.api_url, data=query_song_data, headers=self.default_headers) 73 | songs = [] 74 | try: 75 | data = resp.json() 76 | status, songs = data['status'], data['data'] 77 | if status != 1: 78 | print "query music error:", resp.content 79 | return songs 80 | return songs 81 | except: 82 | traceback.print_exc() 83 | print resp.content 84 | # 响应示例 85 | # { 86 | # "status": 1, 87 | # "message": "\u83b7\u53d6\u6210\u529f", 88 | # "data": [ 89 | # { 90 | # "lrc": "513360721", 91 | # "author": "\u623f\u4e1c\u7684\u732b", 92 | # "url": "https:\/\/music.163.com\/song\/media\/outer\/url?id=513360721.mp3", 93 | # "pic": "http:\/\/p2.music.126.net\/DSTg1dR7yKsyGq4IK3NL8A==\/109951163046050093.jpg?param=320y320", 94 | # "title": "\u4e91\u70df\u6210\u96e8", 95 | # "type": "netease", 96 | # "id": "513360721" 97 | # } 98 | # ] 99 | # } 100 | return songs 101 | 102 | def get_song_download_url(self, search_keyword): 103 | for source in self.source_list: 104 | songs = self.get_song_info_from_source(search_keyword, source) 105 | # song example: 106 | # { 107 | # "lrc": "513360721", 108 | # "author": "\u623f\u4e1c\u7684\u732b", 109 | # "url": "https:\/\/music.163.com\/song\/media\/outer\/url?id=513360721.mp3", 110 | # "pic": "http:\/\/p2.music.126.net\/DSTg1dR7yKsyGq4IK3NL8A==\/109951163046050093.jpg?param=320y320", 111 | # "title": "\u4e91\u70df\u6210\u96e8", 112 | # "type": "netease", 113 | # "id": "513360721" 114 | # } 115 | if songs: 116 | song_id = songs[0]['id'] 117 | song_name = songs[0]['title'] 118 | song_artist = songs[0]['author'] 119 | pic_id = songs[0]['pic'] 120 | lyric_id = songs[0]['lrc'] 121 | album = song_name # 天天静听没有提供专辑信息,使用歌曲名代替 122 | 123 | download_url = songs[0]['url'] 124 | if download_url: 125 | print 'find the download url from source %s' % source 126 | return download_url, song_name, song_artist, pic_id, lyric_id, source, album 127 | else: 128 | print 'Can not download from source %s' % source 129 | continue # 无法下载则切换源 130 | else: 131 | print 'Can not find any songs from source %s' % source 132 | continue # 找不到歌则切换源 133 | return None 134 | 135 | def download_song_from_url(self, song_url, song_name, song_artist): 136 | filename = '%s - %s.mp3' % (song_artist, song_name) 137 | if os.path.exists(os.path.join(self.default_download_folder, filename)): 138 | print 'already download the song %s of %s successfully' % (song_name, song_artist) 139 | return True 140 | try: 141 | urllib.urlretrieve(url=song_url, filename=os.path.join(self.default_download_folder, filename)) 142 | print 'download the song %s of %s successfully' % (song_name, song_artist) 143 | return True 144 | except: 145 | traceback.print_exc() 146 | print 'Can not download the song %s of %s from %s' % (song_name, song_artist, song_url) 147 | return False 148 | 149 | def get_and_download_pic(self, pic_id, song_name, song_artist, source): 150 | filename = '%s - %s.jpg' % (song_artist, song_name) 151 | if os.path.exists(os.path.join(self.default_pic_folder, filename)): 152 | print 'already download cover pic %s successfully' % filename 153 | return True 154 | try: 155 | urllib.urlretrieve(url=pic_id, filename=os.path.join(self.default_pic_folder, filename)) 156 | print 'download cover pic %s successfully' % filename 157 | return True 158 | except: 159 | traceback.print_exc() 160 | print 'Can not download cover pic %s' % filename 161 | return False 162 | 163 | def get_and_download_lyric(self, lyric_id, song_name, song_artist, source): 164 | filename = '%s - %s.lrc' % (song_artist, song_name) 165 | if os.path.exists(os.path.join(self.default_download_folder, filename)): 166 | print 'already download the lyric %s successfully' % filename 167 | return True 168 | # 修改必要参数 169 | lrc_params = self.get_lrc_params.copy() 170 | lrc_params["lrc"] = lyric_id 171 | lrc_params["type"] = source 172 | # 获取下载信息 173 | resp = self.session.get(url=self.lrc_api_url, params=lrc_params, headers=self.default_headers) 174 | with codecs.open(os.path.join(self.default_download_folder, filename), 'wb', 'utf-8') as f: 175 | f.write(resp.text) 176 | print 'download the lyric %s successfully' % filename 177 | return True 178 | 179 | def write_pic_to_song(self, song_name, song_artist, album): 180 | song_filename = os.path.join(self.default_download_folder, '%s - %s.mp3' % (song_artist, song_name)) 181 | pic_filename = os.path.join(self.default_pic_folder, '%s - %s.jpg' % (song_artist, song_name)) 182 | 183 | try: 184 | audiofile = eyed3.load(song_filename) 185 | if not audiofile: 186 | return 187 | if not audiofile.tag: 188 | audiofile.initTag() 189 | audiofile.tag.images.set(3, open(pic_filename, 'rb').read(), 'image/jpeg') 190 | audiofile.tag.artist = song_artist if not audiofile.tag.artist else audiofile.tag.artist 191 | audiofile.tag.title = song_name if not audiofile.tag.title else audiofile.tag.title 192 | audiofile.tag.album = album if not audiofile.tag.album else audiofile.tag.album 193 | audiofile.tag.save() 194 | except: 195 | traceback.print_exc() 196 | print song_name, song_artist, album, repr(song_name), repr(song_artist), repr(album) 197 | 198 | def set_default_download_folder(self, path): 199 | self.default_download_folder = path 200 | 201 | def set_default_pic_folder(self, path): 202 | self.default_pic_folder = path 203 | 204 | def search_and_download_song(self, keyword, download_dir=None, pic_dir=None): 205 | if download_dir: 206 | self.set_default_download_folder(download_dir) 207 | if pic_dir: 208 | self.set_default_pic_folder(pic_dir) 209 | 210 | try: 211 | download_url, song_name, song_artist, pic_id, lyric_id, source, album = self.get_song_download_url(keyword) 212 | if self.download_song_from_url(download_url, song_name, song_artist): 213 | self.get_and_download_lyric(lyric_id=lyric_id, song_name=song_name, song_artist=song_artist, source=source) 214 | if self.get_and_download_pic(pic_id=pic_id, song_name=song_name, song_artist=song_artist, source=source): 215 | self.write_pic_to_song(song_name, song_artist, album) 216 | except Exception: 217 | traceback.print_exc() 218 | print "download fail", keyword 219 | 220 | 221 | if __name__ == '__main__': 222 | api = TiantianJingtiingMusicDownloader() 223 | download_url, song_name, song_artist, pic_id, lyric_id, source, album = api.get_song_download_url('沙漠骆驼 展展与罗罗') 224 | if api.download_song_from_url(download_url, song_name, song_artist): 225 | api.get_and_download_lyric(lyric_id=lyric_id, song_name=song_name, song_artist=song_artist, source=source) 226 | if api.get_and_download_pic(pic_id=pic_id, song_name=song_name, song_artist=song_artist, source=source): 227 | api.write_pic_to_song(song_name, song_artist, album) 228 | -------------------------------------------------------------------------------- /GeneralMusicDownloader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author : Huangcc 3 | 4 | import requests 5 | import time 6 | import random 7 | import json 8 | import os 9 | import urllib 10 | import traceback 11 | import codecs 12 | import eyed3 13 | 14 | 15 | def timestamp_ms_string(): 16 | return '%d' % (time.time() * 1000) 17 | 18 | 19 | def random_int_string(length): 20 | start = int('1' + '0' * (length - 1)) 21 | end = int('9' * length) 22 | return '%d' % random.randint(start, end) 23 | 24 | 25 | class GeneralMusicDownloader: 26 | default_headers = { 27 | 'Host': 'lab.mkblog.cn', 28 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0', 29 | 'Accept': '*/*', 30 | 'Accept-Language': 'en-US,en;q=0.5', 31 | 'Accept-Encoding': 'gzip, deflate', 32 | 'Referer': 'http://lab.mkblog.cn/music/', 33 | 'Connection': 'keep-alive', 34 | 'Pragma': 'no-cache', 35 | 'Cache-Control': 'no-cache' 36 | } 37 | 38 | query_song_data = { 39 | 'callback': 'jQuery111308194906156652133_1511145723854', 40 | 'types': 'search', 41 | 'count': '20', 42 | 'source': 'netease', # 按需更改 43 | 'pages': '1', 44 | 'name': '幼稚完 林峰', # 按需更改 45 | '_': '1511145723857', 46 | } 47 | 48 | general_query_data = { 49 | 'callback': 'jQuery111308194906156652132_1511145723853', 50 | 'types': 'url', # 按需修改 51 | 'id': '25638257', 52 | 'source': 'netease', # 按需修改 53 | '_': '1511145723868', 54 | } 55 | 56 | source_list = ['netease', 'tencent', 'xiami', 'kugou', 'baidu'] # 下载源 57 | 58 | api_url = 'http://lab.mkblog.cn/music/api.php' 59 | 60 | default_download_folder = 'music' 61 | default_pic_folder = 'pic' 62 | 63 | def __init__(self): 64 | self.session = requests.Session() 65 | if not os.path.exists(self.default_download_folder): 66 | os.mkdir(self.default_download_folder) 67 | if not os.path.exists(self.default_pic_folder): 68 | os.mkdir(self.default_pic_folder) 69 | 70 | def get_song_info_from_source(self, search_keyword, source='netease'): 71 | if source not in self.source_list: 72 | print 'Unsupported source expected the one of that: %s' % self.source_list 73 | return False 74 | query_song_data = self.query_song_data.copy() 75 | callback_item = 'jQuery%s_%s' % (random_int_string(21), timestamp_ms_string()) 76 | query_song_data['callback'] = callback_item 77 | query_song_data['_'] = timestamp_ms_string() 78 | query_song_data['name'] = search_keyword 79 | query_song_data['source'] = source 80 | 81 | resp = self.session.get(url=self.api_url, params=query_song_data, headers=self.default_headers) 82 | songs = [] 83 | try: 84 | songs = json.loads(resp.content[len(callback_item) + 1: -1]) 85 | except: 86 | traceback.print_exc() 87 | print resp.content 88 | # 响应示例 89 | # [ 90 | # "id": "001ZptHS0QElqW", 91 | # "name": "\u5e7c\u7a1a\u5b8c", 92 | # "artist": ["\u6797\u5cef"], 93 | # "album": "A Time 4 You \u65b0\u66f2+\u7cbe\u9009", 94 | # "pic_id": "003lA98A4Jbn9I", 95 | # "url_id": "001ZptHS0QElqW", 96 | # "lyric_id": "001ZptHS0QElqW", 97 | # "source": "tencent", 98 | # }] 99 | return songs 100 | 101 | def get_song_download_info_from_source(self, song_id, source): 102 | # 修改必要参数 103 | general_query_data = self.general_query_data.copy() 104 | callback_item = 'jQuery%s_%s' % (random_int_string(21), timestamp_ms_string()) 105 | general_query_data['callback'] = callback_item 106 | general_query_data['_'] = timestamp_ms_string() 107 | general_query_data['id'] = song_id 108 | general_query_data['source'] = source 109 | general_query_data['types'] = 'url' 110 | # 获取下载信息 111 | resp = self.session.get(url=self.api_url, params=general_query_data, headers=self.default_headers) 112 | json_data = json.loads(resp.content[len(callback_item) + 1: -1]) 113 | # 响应示例 114 | # {"url":"","br":-1} 115 | # {"url":"https:\/\/dl.stream.qqmusic.qq.com\/M800001ZptHS0QElqW.mp3?vkey=483BB7027FD2626BFAE54FFD77A7C31F58F44BAB9F5E2A1DBFA9EAA58C2A39D1627A7D4F2C3E8478D690B085BF43BE47B99BF0DECA512B29&guid=513525444&uid=0&fromtag=30","br":320} 116 | return json_data 117 | 118 | def get_song_download_url(self, search_keyword): 119 | for source in self.source_list: 120 | songs = self.get_song_info_from_source(search_keyword, source) 121 | if songs: 122 | song_id = songs[0]['id'] 123 | song_name = songs[0]['name'] 124 | song_artist = songs[0]['artist'][0] # 取第一位艺术家 125 | pic_id = songs[0]['pic_id'] 126 | lyric_id = songs[0]['lyric_id'] 127 | album = songs[0]['album'] 128 | 129 | download_url = self.get_song_download_info_from_source(song_id=song_id, source=source)['url'] 130 | if download_url: 131 | print 'find the download url from source %s' % source 132 | return download_url, song_name, song_artist, pic_id, lyric_id, source, album 133 | else: 134 | print 'Can not download from source %s' % source 135 | continue # 无法下载则切换源 136 | else: 137 | print 'Can not find any songs from source %s' % source 138 | continue # 找不到歌则切换源 139 | return None 140 | 141 | def download_song_from_url(self, song_url, song_name, song_artist): 142 | filename = '%s - %s.mp3' % (song_artist, song_name) 143 | if os.path.exists(os.path.join(self.default_download_folder, filename)): 144 | print 'already download the song %s of %s successfully' % (song_name, song_artist) 145 | return True 146 | try: 147 | urllib.urlretrieve(url=song_url, filename=os.path.join(self.default_download_folder, filename)) 148 | print 'download the song %s of %s successfully' % (song_name, song_artist) 149 | return True 150 | except: 151 | traceback.print_exc() 152 | print 'Can not download the song %s of %s from %s' % (song_name, song_artist, song_url) 153 | return False 154 | 155 | def get_and_download_pic(self, pic_id, song_name, song_artist, source): 156 | filename = '%s - %s.jpg' % (song_artist, song_name) 157 | if os.path.exists(os.path.join(self.default_pic_folder, filename)): 158 | print 'already download cover pic %s successfully' % filename 159 | return True 160 | # 修改必要参数 161 | general_query_data = self.general_query_data.copy() 162 | callback_item = 'jQuery%s_%s' % (random_int_string(21), timestamp_ms_string()) 163 | general_query_data['callback'] = callback_item 164 | general_query_data['_'] = timestamp_ms_string() 165 | general_query_data['id'] = pic_id 166 | general_query_data['source'] = source 167 | general_query_data['types'] = 'pic' 168 | # 获取下载信息 169 | resp = self.session.get(url=self.api_url, params=general_query_data, headers=self.default_headers) 170 | json_data = json.loads(resp.content[len(callback_item) + 1: -1]) 171 | if json_data['url']: 172 | urllib.urlretrieve(url=json_data['url'], filename=os.path.join(self.default_pic_folder, filename)) 173 | print 'download cover pic %s successfully' % filename 174 | return True 175 | else: 176 | print 'Can not download cover pic %s' % filename 177 | return False 178 | 179 | def get_and_download_lyric(self, lyric_id, song_name, song_artist, source): 180 | filename = '%s - %s.lrc' % (song_artist, song_name) 181 | if os.path.exists(os.path.join(self.default_download_folder, filename)): 182 | print 'already download the lyric %s successfully' % filename 183 | return True 184 | # 修改必要参数 185 | general_query_data = self.general_query_data.copy() 186 | callback_item = 'jQuery%s_%s' % (random_int_string(21), timestamp_ms_string()) 187 | general_query_data['callback'] = callback_item 188 | general_query_data['_'] = timestamp_ms_string() 189 | general_query_data['id'] = lyric_id 190 | general_query_data['source'] = source 191 | general_query_data['types'] = 'lyric' 192 | # 获取下载信息 193 | resp = self.session.get(url=self.api_url, params=general_query_data, headers=self.default_headers) 194 | json_data = json.loads(resp.content[len(callback_item) + 1: -1]) 195 | if json_data['lyric']: 196 | with codecs.open(os.path.join(self.default_download_folder, filename), 'wb', 'utf-8') as f: 197 | f.write(json_data['lyric']) 198 | print 'download the lyric %s successfully' % filename 199 | return True 200 | else: 201 | print 'Can not download the lyric %s' % filename 202 | return False 203 | 204 | def write_pic_to_song(self, song_name, song_artist, album): 205 | song_filename = os.path.join(self.default_download_folder, '%s - %s.mp3' % (song_artist, song_name)) 206 | pic_filename = os.path.join(self.default_pic_folder, '%s - %s.jpg' % (song_artist, song_name)) 207 | 208 | try: 209 | audiofile = eyed3.load(song_filename) 210 | if not audiofile: 211 | return 212 | if not audiofile.tag: 213 | audiofile.initTag() 214 | audiofile.tag.images.set(3, open(pic_filename, 'rb').read(), 'image/jpeg') 215 | audiofile.tag.artist = song_artist if not audiofile.tag.artist else audiofile.tag.artist 216 | audiofile.tag.title = song_name if not audiofile.tag.title else audiofile.tag.title 217 | audiofile.tag.album = album if not audiofile.tag.album else audiofile.tag.album 218 | audiofile.tag.save() 219 | except: 220 | traceback.print_exc() 221 | print song_name, song_artist, album, repr(song_name), repr(song_artist), repr(album) 222 | 223 | def set_default_download_folder(self, path): 224 | self.default_download_folder = path 225 | 226 | def set_default_pic_folder(self, path): 227 | self.default_pic_folder = path 228 | 229 | def search_and_download_song(self, keyword, download_dir=None, pic_dir=None): 230 | if download_dir: 231 | self.set_default_download_folder(download_dir) 232 | if pic_dir: 233 | self.set_default_pic_folder(pic_dir) 234 | download_url, song_name, song_artist, pic_id, lyric_id, source, album = self.get_song_download_url(keyword) 235 | if self.download_song_from_url(download_url, song_name, song_artist): 236 | self.get_and_download_lyric(lyric_id=lyric_id, song_name=song_name, song_artist=song_artist, source=source) 237 | if self.get_and_download_pic(pic_id=pic_id, song_name=song_name, song_artist=song_artist, source=source): 238 | self.write_pic_to_song(song_name, song_artist, album) 239 | 240 | 241 | if __name__ == '__main__': 242 | api = GeneralMusicDownloader() 243 | download_url, song_name, song_artist, pic_id, lyric_id, source, album = api.get_song_download_url('幼稚完 林峰') 244 | if api.download_song_from_url(download_url, song_name, song_artist): 245 | api.get_and_download_lyric(lyric_id=lyric_id, song_name=song_name, song_artist=song_artist, source=source) 246 | if api.get_and_download_pic(pic_id=pic_id, song_name=song_name, song_artist=song_artist, source=source): 247 | api.write_pic_to_song(song_name, song_artist, album) 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 网易云音乐同步下载 2 | 3 | ## 文件功能描述 4 | ### GeneralMusicDownloader 5 | 使用第三方下载api [MkOnlinePlayer](http://lab.mkblog.cn/music/) 6 | 7 | 支持`网易`, `QQ`, `虾米`, `酷狗`, `百度`搜索和下载 8 | 9 | 支持歌词自动下载和封面添加到mp3文件的功能 10 | 11 | ### NetEaseApi 12 | 提供网易云音乐登录/获取每日推荐/歌单信息的api 13 | 14 | ### NetEaseMusicSync 15 | 调用`NetEaseApi`和`GeneralMusicDownloader`实现登录/获取歌单/下载歌单到本地文件夹的功能 16 | 17 | ## Api接口说明 18 | 更详细的接口信息请看对应的`.py`文件 19 | 20 | ### 网易云音乐统一api请求方式 21 | 使用同一种加密方式和请求体格式,响应均为`json`格式 22 | #### 请求方式 23 | `POST` 24 | #### 请求加密方式 25 | ```python 26 | def get_info_from_nem(self, url, params): 27 | headers = { 28 | 'Accept': '*/*', 29 | 'Accept-Encoding': 'gzip,deflate,sdch', 30 | 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 31 | 'Connection': 'keep-alive', 32 | 'Content-Type': 'application/x-www-form-urlencoded', 33 | 'Host': 'music.163.com', 34 | 'Referer': 'http://music.163.com/', 35 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36' 36 | } 37 | modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 38 | nonce = '0CoJUm6Qyw8W8jud' 39 | pub_key = '010001' 40 | params = json.dumps(params) 41 | sec_key = self._create_secret_key(16) 42 | enc_text = self._aes_encrypt(self._aes_encrypt(params, nonce), sec_key) 43 | enc_sec_key = self._rsa_encrypt(sec_key, pub_key, modulus) 44 | data = { 45 | 'params': enc_text, 46 | 'encSecKey': enc_sec_key 47 | } 48 | req = self.session.post(url, headers=headers, data=data) 49 | return req.json() 50 | ``` 51 | 52 | ### NetEaseApi::cellphone_login 53 | #### 请求url 54 | ``` 55 | http://music.163.com/weapi/login/cellphone?csrf_token= 56 | ``` 57 | #### 请求参数 58 | ``` 59 | { 60 | 'phone': username, 61 | 'password': hashlib.md5(password).hexdigest(), 62 | 'rememberLogin': "true" 63 | } 64 | ``` 65 | #### 响应示例 66 | ```python 67 | { 68 | u'profile': { 69 | u'followed': False, 70 | u'remarkName': None, 71 | u'expertTags': None, 72 | u'userId': xxxxxx, 73 | u'authority': 0, 74 | u'userType': 0, 75 | u'experts': { 76 | 77 | }, 78 | u'backgroundImgId': xxxxxx, 79 | u'city': xxxxxx, 80 | u'mutual': False, 81 | u'avatarUrl': xxxxxx, 82 | u'detailDescription': u'', 83 | u'avatarImgIdStr': u'xxxxxx', 84 | u'province': xxxxxx, 85 | u'description': u'', 86 | u'signature': u'', 87 | u'birthday': -xxxxxx, 88 | u'nickname': u'xxxxxx', 89 | u'vipType': 0, 90 | u'avatarImgId': xxxxxx, 91 | u'gender': 1, 92 | u'djStatus': 0, 93 | u'accountStatus': 0, 94 | u'backgroundImgIdStr': u'xxxxxx', 95 | u'backgroundUrl': u'xxxxxx', 96 | u'defaultAvatar': False, 97 | u'authStatus': 0 98 | }, 99 | u'account': { 100 | u'userName': u'xxxxxx', 101 | u'status': 0, 102 | u'anonimousUser': False, 103 | u'whitelistAuthority': 0, 104 | u'baoyueVersion': 0, 105 | u'salt': u'[B@xxxxxx', 106 | u'createTime': 0, 107 | u'tokenVersion': 0, 108 | u'vipType': 0, 109 | u'ban': 0, 110 | u'viptypeVersion': 0, 111 | u'type': 1, 112 | u'id': xxxxxx, 113 | u'donateVersion': 0 114 | }, 115 | u'code': 200, 116 | u'bindings': [{ 117 | u'expiresIn': xxxxxx, 118 | u'tokenJsonStr': u'{ 119 | "countrycode": "", 120 | "cellphone": "xxxxxx", 121 | "hasPassword": true 122 | }', 123 | u'url': u'', 124 | u'expired': False, 125 | u'userId': xxxxxx, 126 | u'refreshTime': xxxxxx, 127 | u'type': 1, 128 | u'id': xxxxxx 129 | },] 130 | ...... 131 | } 132 | ``` 133 | 134 | ### NetEaseApi::get_play_list 135 | 根据uid获取列表信息 136 | #### 请求url 137 | ``` 138 | http://music.163.com/weapi/user/playlist?csrf_token= 139 | ``` 140 | #### 请求参数 141 | ```python 142 | params = { 143 | 'offset': '0', 144 | 'limit': '9999', 145 | 'uid': str(uid) 146 | } 147 | ``` 148 | #### 响应示例 149 | ```python 150 | { 151 | u'playlist': [{ 152 | u'updateTime': xxxxxx, 153 | u'ordered': False, 154 | u'anonimous': False, 155 | u'creator': { 156 | ...... 157 | }, 158 | u'trackUpdateTime': xxxxxx, 159 | u'userId': xxxxxx, 160 | u'coverImgUrl': xxxxxx, 161 | u'artists': None, 162 | u'newImported': False, 163 | u'commentThreadId': u'xxxxxx', 164 | u'subscribed': False, 165 | u'privacy': 0, 166 | u'id': xxxxxx, 167 | u'trackCount': 1138, 168 | u'specialType': 5, 169 | u'status': 0, 170 | u'description': None, 171 | u'subscribedCount': 0, 172 | u'tags': [], 173 | u'trackNumberUpdateTime': xxxxxx, 174 | u'tracks': None, 175 | u'highQuality': False, 176 | u'subscribers': [], 177 | u'playCount': 112, 178 | u'coverImgId': xxxxxx, 179 | u'createTime': xxxxxx, 180 | u'name': u'xxxxxx', 181 | u'cloudTrackCount': 0, 182 | u'adType': 0, 183 | u'totalDuration': 0 184 | },] 185 | } 186 | ``` 187 | 188 | ### NetEaseApi::get_play_list_info 189 | 根据列表id来获取歌名列表 190 | #### 请求url 191 | ``` 192 | url = 'http://music.163.com/weapi/playlist/detail?csrf_token=' 193 | ``` 194 | #### 请求参数 195 | ```python 196 | params = {'id': str(music_list_id)} 197 | ``` 198 | #### 响应示例 199 | ```python 200 | { 201 | u'code': 200, 202 | u'result': { 203 | u'updateTime': xxxxx, 204 | u'ordered': False, 205 | u'anonimous': False, 206 | u'creator': { 207 | u'followed': False, 208 | u'remarkName': None, 209 | u'expertTags': None, 210 | u'userId': xxxx, 211 | u'authority': 0, 212 | u'userType': 0, 213 | u'experts': None, 214 | u'gender': 1, 215 | u'backgroundImgId': xxxxx, 216 | u'city': xxxxx, 217 | u'mutual': False, 218 | u'avatarUrl': u'xxxxx', 219 | u'avatarImgIdStr': u'xxxxx', 220 | u'detailDescription': u'', 221 | u'province': xxxx, 222 | u'description': u'', 223 | u'birthday': -xxxx, 224 | u'nickname': u'xxxx', 225 | u'vipType': 0, 226 | u'avatarImgId': xxxx, 227 | u'defaultAvatar': False, 228 | u'djStatus': 0, 229 | u'accountStatus': 0, 230 | u'backgroundImgIdStr': u'xxxxx', 231 | u'backgroundUrl': u'xxxx', 232 | u'signature': u'', 233 | u'authStatus': 0 234 | }, 235 | u'trackUpdateTime': xxxxx, 236 | u'userId': xxxxx, 237 | u'coverImgUrl': u'xxxxx', 238 | u'commentCount': 0, 239 | u'artists': None, 240 | u'newImported': False, 241 | u'commentThreadId': u'xxxxx', 242 | u'subscribed': False, 243 | u'privacy': 0, 244 | u'id': xxxxxx, 245 | u'trackCount': 20, 246 | u'specialType': 0, 247 | u'status': 0, 248 | u'description': None, 249 | u'subscribedCount': 0, 250 | u'tags': [], 251 | u'coverImgId': xxxxxx, 252 | u'tracks': [{ 253 | u'bMusic': { 254 | u'name': u'\u8fd9\u6837\u7231\u4e86', 255 | u'extension': u'mp3', 256 | u'volumeDelta': -1.48, 257 | u'sr': 44100, 258 | u'dfsId': 0, 259 | u'playTime': 263105, 260 | u'bitrate': 96000, 261 | u'id': 29417131, 262 | u'size': 3194123 263 | }, 264 | u'hearTime': 0, 265 | u'mvid': 1115, 266 | u'hMusic': { 267 | u'name': u'\u8fd9\u6837\u7231\u4e86', 268 | u'extension': u'mp3', 269 | u'volumeDelta': -1.84, 270 | u'sr': 44100, 271 | u'dfsId': 0, 272 | u'playTime': 263105, 273 | u'bitrate': 320000, 274 | u'id': 29417129, 275 | u'size': 10562534 276 | }, 277 | u'disc': u'1', 278 | u'artists': [{ 279 | u'img1v1Url': u'http: //p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg', 280 | u'name': u'\u5f20\u5a67', 281 | u'briefDesc': u'', 282 | u'albumSize': 0, 283 | u'img1v1Id': 0, 284 | u'musicSize': 0, 285 | u'alias': [], 286 | u'picId': 0, 287 | u'picUrl': u'http: //p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg', 288 | u'trans': u'', 289 | u'id': 10753 290 | }], 291 | u'duration': 263105, 292 | u'id': 391564, 293 | u'album': { 294 | u'pic': 72567767449563L, 295 | u'subType': u'\u5f55\u97f3\u5ba4\u7248', 296 | u'artists': [{ 297 | u'img1v1Url': u'http: //p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg', 298 | u'name': u'\u7fa4\u661f', 299 | u'briefDesc': u'', 300 | u'albumSize': 0, 301 | u'img1v1Id': 0, 302 | u'musicSize': 0, 303 | u'alias': [], 304 | u'picId': 0, 305 | u'picUrl': u'http: //p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg', 306 | u'trans': u'', 307 | u'id': 122455 308 | }], 309 | u'id': 38789, 310 | u'size': 10, 311 | u'commentThreadId': u'R_AL_3_38789', 312 | u'companyId': 0, 313 | u'briefDesc': u'', 314 | u'type': u'\u4e13\u8f91', 315 | u'status': 1, 316 | u'description': u'', 317 | u'tags': u'', 318 | u'company': u'\u6d77\u8776\u97f3\u4e50', 319 | u'picId': 72567767449563L, 320 | u'picUrl': u'http: //p1.music.126.net/Rk334sCFsT_HeO3ZJ-IfeA==/72567767449563.jpg', 321 | u'blurPicUrl': u'http: //p1.music.126.net/Rk334sCFsT_HeO3ZJ-IfeA==/72567767449563.jpg', 322 | u'copyrightId': 14026, 323 | u'name': u'\u8f69\u8f95\u5251\u7535\u89c6\u539f\u58f0\u5927\u789f', 324 | u'artist': { 325 | u'img1v1Url': u'http: //p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg', 326 | u'name': u'', 327 | u'briefDesc': u'', 328 | u'albumSize': 0, 329 | u'img1v1Id': 0, 330 | u'musicSize': 0, 331 | u'alias': [], 332 | u'picId': 0, 333 | u'picUrl': u'http: //p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg', 334 | u'trans': u'', 335 | u'id': 0 336 | }, 337 | u'publishTime': 1342368000000L, 338 | u'alias': [], 339 | u'songs': [] 340 | }, 341 | u'fee': 0, 342 | u'copyright': 1, 343 | u'no': 1, 344 | u'rtUrl': None, 345 | u'ringtone': u'600902000009339522', 346 | u'rtUrls': [], 347 | u'score': 100, 348 | u'rurl': None, 349 | u'status': 0, 350 | u'ftype': 0, 351 | u'mp3Url': None, 352 | u'audition': None, 353 | u'playedNum': 0, 354 | u'commentThreadId': u'R_SO_4_391564', 355 | u'mMusic': { 356 | u'name': u'\u8fd9\u6837\u7231\u4e86', 357 | u'extension': u'mp3', 358 | u'volumeDelta': -1.44, 359 | u'sr': 44100, 360 | u'dfsId': 0, 361 | u'playTime': 263105, 362 | u'bitrate': 160000, 363 | u'id': 29417130, 364 | u'size': 5299384 365 | }, 366 | u'lMusic': { 367 | u'name': u'\u8fd9\u6837\u7231\u4e86', 368 | u'extension': u'mp3', 369 | u'volumeDelta': -1.48, 370 | u'sr': 44100, 371 | u'dfsId': 0, 372 | u'playTime': 263105, 373 | u'bitrate': 96000, 374 | u'id': 29417131, 375 | u'size': 3194123 376 | }, 377 | u'copyrightId': 14026, 378 | u'name': u'\u8fd9\u6837\u7231\u4e86', 379 | u'rtype': 0, 380 | u'crbt': u'0a25e5667006a3b49b79ec8e7ffaf8f2', 381 | u'popularity': 100.0, 382 | u'dayPlays': 0, 383 | u'alias': [u'\u7535\u89c6\u5267\u300a\u8f69\u8f95\u5251\u4e4b\u5929\u4e4b\u75d5\u300b\u7247\u5c3e\u66f2'], 384 | u'copyFrom': u'', 385 | u'position': 1, 386 | u'starred': False, 387 | u'starredNum': 0 388 | }, 389 | ], 390 | u'highQuality': False, 391 | u'subscribers': [], 392 | u'playCount': 0, 393 | u'trackNumberUpdateTime': xxxxx, 394 | u'createTime': xxxxx, 395 | u'name': u'\u4e0b\u8f7d', 396 | u'cloudTrackCount': 0, 397 | u'shareCount': 0, 398 | u'adType': 0, 399 | u'totalDuration': 0 400 | } 401 | } 402 | ``` 403 | 404 | ### MKOnlinePlayer 音乐接口 405 | 使用同一个api接口和请求方式,响应均为`JSONP`格式 406 | #### 请求url 407 | ``` 408 | api_url = 'http://122.112.253.137/music/api.php' 409 | ``` 410 | #### 请求方式 411 | `GET` 412 | 413 | ### GeneralMusicDownloader::get_song_info_from_source 414 | 根据关键词获取歌曲信息 415 | #### 请求参数 416 | ```python 417 | query_song_data = { 418 | 'callback': 'jQuery111308194906156652133_1511145723854', 419 | 'types': 'search', 420 | 'count': '20', 421 | 'source': 'netease', # 按需更改 422 | 'pages': '1', 423 | 'name': '幼稚完 林峰', # 按需更改 424 | '_': '1511145723857', 425 | } 426 | ``` 427 | #### 响应示例 428 | ```python 429 | jQuery111307832949596001351_1511159299186([{ 430 | "id": "001ZptHS0QElqW", 431 | "name": "\u5e7c\u7a1a\u5b8c", 432 | "artist": ["\u6797\u5cef"], 433 | "album": "A Time 4 You \u65b0\u66f2+\u7cbe\u9009", 434 | "pic_id": "003lA98A4Jbn9I", 435 | "url_id": "001ZptHS0QElqW", 436 | "lyric_id": "001ZptHS0QElqW", 437 | "source": "tencent" 438 | }... 439 | ]) 440 | ``` 441 | 442 | ### GeneralMusicDownloader::get_song_download_info_from_source 443 | 根据`url_id`获取下载`url` 444 | #### 请求参数 445 | ```python 446 | general_query_data = { 447 | 'callback': 'jQuery111308194906156652132_1511145723853', 448 | 'types': 'url', # 按需修改 449 | 'id': '25638257', # url_id 450 | 'source': 'netease', # 按需修改 451 | '_': '1511145723868', 452 | } 453 | ``` 454 | #### 响应示例 455 | ```python 456 | {"url":"https:\/\/dl.stream.qqmusic.qq.com\/M800001ZptHS0QElqW.mp3?vkey=483BB7027FD2626BFAE54FFD77A7C31F58F44BAB9F5E2A1DBFA9EAA58C2A39D1627A7D4F2C3E8478D690B085BF43BE47B99BF0DECA512B29&guid=513525444&uid=0&fromtag=30","br":320} 457 | ``` 458 | 459 | --------------------------------------------------------------------------------