1
+ # -*- coding:utf-8 -*-
2
+ import requests , hashlib , sys , click , re , base64 , binascii , json , os
3
+ from Crypto .Cipher import AES
4
+ from http import cookiejar
5
+
6
+ """
7
+ Website:http://cuijiahua.com
8
+ Author:Jack Cui
9
+ Refer:https://github.com/darknessomi/musicbox
10
+ """
11
+
12
+ class Encrypyed ():
13
+ """
14
+ 解密算法
15
+ """
16
+ def __init__ (self ):
17
+ self .modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
18
+ self .nonce = '0CoJUm6Qyw8W8jud'
19
+ self .pub_key = '010001'
20
+
21
+ # 登录加密算法, 基于https://github.com/stkevintan/nw_musicbox脚本实现
22
+ def encrypted_request (self , text ):
23
+ text = json .dumps (text )
24
+ sec_key = self .create_secret_key (16 )
25
+ enc_text = self .aes_encrypt (self .aes_encrypt (text , self .nonce ), sec_key .decode ('utf-8' ))
26
+ enc_sec_key = self .rsa_encrpt (sec_key , self .pub_key , self .modulus )
27
+ data = {'params' : enc_text , 'encSecKey' : enc_sec_key }
28
+ return data
29
+
30
+ def aes_encrypt (self , text , secKey ):
31
+ pad = 16 - len (text ) % 16
32
+ text = text + chr (pad ) * pad
33
+ encryptor = AES .new (secKey .encode ('utf-8' ), AES .MODE_CBC , b'0102030405060708' )
34
+ ciphertext = encryptor .encrypt (text .encode ('utf-8' ))
35
+ ciphertext = base64 .b64encode (ciphertext ).decode ('utf-8' )
36
+ return ciphertext
37
+
38
+ def rsa_encrpt (self , text , pubKey , modulus ):
39
+ text = text [::- 1 ]
40
+ rs = pow (int (binascii .hexlify (text ), 16 ), int (pubKey , 16 ), int (modulus , 16 ))
41
+ return format (rs , 'x' ).zfill (256 )
42
+
43
+ def create_secret_key (self , size ):
44
+ return binascii .hexlify (os .urandom (size ))[:16 ]
45
+
46
+
47
+ class Song ():
48
+ """
49
+ 歌曲对象,用于存储歌曲的信息
50
+ """
51
+ def __init__ (self , song_id , song_name , song_num , song_url = None ):
52
+ self .song_id = song_id
53
+ self .song_name = song_name
54
+ self .song_num = song_num
55
+ self .song_url = '' if song_url is None else song_url
56
+
57
+ class Crawler ():
58
+ """
59
+ 网易云爬取API
60
+ """
61
+ def __init__ (self , timeout = 60 , cookie_path = '.' ):
62
+ self .headers = {
63
+ 'Accept' : '*/*' ,
64
+ 'Accept-Encoding' : 'gzip,deflate,sdch' ,
65
+ 'Accept-Language' : 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4' ,
66
+ 'Connection' : 'keep-alive' ,
67
+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
68
+ 'Host' : 'music.163.com' ,
69
+ 'Referer' : 'http://music.163.com/search/' ,
70
+ 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'
71
+ }
72
+ self .session = requests .Session ()
73
+ self .session .headers .update (self .headers )
74
+ self .session .cookies = cookiejar .LWPCookieJar (cookie_path )
75
+ self .download_session = requests .Session ()
76
+ self .timeout = timeout
77
+ self .ep = Encrypyed ()
78
+
79
+ def post_request (self , url , params ):
80
+ """
81
+ Post请求
82
+ :return: 字典
83
+ """
84
+
85
+ data = self .ep .encrypted_request (params )
86
+ resp = self .session .post (url , data = data , timeout = self .timeout )
87
+ result = resp .json ()
88
+ if result ['code' ] != 200 :
89
+ click .echo ('post_request error' )
90
+ else :
91
+ return result
92
+
93
+ def search (self , search_content , search_type , limit = 9 ):
94
+ """
95
+ 搜索API
96
+ :params search_content: 搜索内容
97
+ :params search_type: 搜索类型
98
+ :params limit: 返回结果数量
99
+ :return: 字典.
100
+ """
101
+
102
+ url = 'http://music.163.com/weapi/cloudsearch/get/web?csrf_token='
103
+ params = {'s' : search_content , 'type' : search_type , 'offset' : 0 , 'sub' : 'false' , 'limit' : limit }
104
+ result = self .post_request (url , params )
105
+ return result
106
+
107
+ def search_song (self , song_name , song_num , quiet = True , limit = 9 ):
108
+ """
109
+ 根据音乐名搜索
110
+ :params song_name: 音乐名
111
+ :params song_num: 下载的歌曲数
112
+ :params quiet: 自动选择匹配最优结果
113
+ :params limit: 返回结果数量
114
+ :return: Song独享
115
+ """
116
+
117
+ result = self .search (song_name , search_type = 1 , limit = limit )
118
+
119
+ if result ['result' ]['songCount' ] <= 0 :
120
+ click .echo ('Song {} not existed.' .format (song_name ))
121
+ else :
122
+ songs = result ['result' ]['songs' ]
123
+ if quiet :
124
+ song_id , song_name = songs [0 ]['id' ], songs [0 ]['name' ]
125
+ song = Song (song_id = song_id , song_name = song_name , song_num = song_num )
126
+ return song
127
+
128
+ def get_song_url (self , song_id , bit_rate = 320000 ):
129
+ """
130
+ 获得歌曲的下载地址
131
+ :params song_id: 音乐ID<int>.
132
+ :params bit_rate: {'MD 128k': 128000, 'HD 320k': 320000}
133
+ :return: 歌曲下载地址
134
+ """
135
+
136
+ url = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token='
137
+ csrf = ''
138
+ params = {'ids' : [song_id ], 'br' : bit_rate , 'csrf_token' : csrf }
139
+ result = self .post_request (url , params )
140
+ # 歌曲下载地址
141
+ song_url = result ['data' ][0 ]['url' ]
142
+
143
+ # 歌曲不存在
144
+ if song_url is None :
145
+ click .echo ('Song {} is not available due to copyright issue.' .format (song_id ))
146
+ else :
147
+ return song_url
148
+
149
+ def get_song_by_url (self , song_url , song_name , song_num , folder ):
150
+ """
151
+ 下载歌曲到本地
152
+ :params song_url: 歌曲下载地址
153
+ :params song_name: 歌曲名字
154
+ :params song_num: 下载的歌曲数
155
+ :params folder: 保存路径
156
+ """
157
+ if not os .path .exists (folder ):
158
+ os .makedirs (folder )
159
+ fpath = os .path .join (folder , str (song_num ) + '_' + song_name + '.mp3' )
160
+ if sys .platform == 'win32' or sys .platform == 'cygwin' :
161
+ valid_name = re .sub (r'[<>:"/\\|?*]' , '' , song_name )
162
+ if valid_name != song_name :
163
+ click .echo ('{} will be saved as: {}.mp3' .format (song_name , valid_name ))
164
+ fpath = os .path .join (folder , str (song_num ) + '_' + valid_name + '.mp3' )
165
+
166
+ if not os .path .exists (fpath ):
167
+ resp = self .download_session .get (song_url , timeout = self .timeout , stream = True )
168
+ length = int (resp .headers .get ('content-length' ))
169
+ label = 'Downloading {} {}kb' .format (song_name , int (length / 1024 ))
170
+
171
+ with click .progressbar (length = length , label = label ) as progressbar :
172
+ with open (fpath , 'wb' ) as song_file :
173
+ for chunk in resp .iter_content (chunk_size = 1024 ):
174
+ if chunk :
175
+ song_file .write (chunk )
176
+ progressbar .update (1024 )
177
+
178
+
179
+ class NetEase ():
180
+ """
181
+ 网易云音乐下载
182
+ """
183
+ def __init__ (self , timeout , folder , quiet , cookie_path ):
184
+ self .crawler = Crawler (timeout , cookie_path )
185
+ self .folder = '.' if folder is None else folder
186
+ self .quiet = quiet
187
+
188
+ def download_song_by_search (self , song_name , song_num ):
189
+ """
190
+ 根据歌曲名进行搜索
191
+ :params song_name: 歌曲名字
192
+ :params song_num: 下载的歌曲数
193
+ """
194
+
195
+ try :
196
+ song = self .crawler .search_song (song_name , song_num , self .quiet )
197
+ except :
198
+ click .echo ('download_song_by_serach error' )
199
+ # 如果找到了音乐, 则下载
200
+ if song != None :
201
+ self .download_song_by_id (song .song_id , song .song_name , song .song_num , self .folder )
202
+
203
+ def download_song_by_id (self , song_id , song_name , song_num , folder = '.' ):
204
+ """
205
+ 通过歌曲的ID下载
206
+ :params song_id: 歌曲ID
207
+ :params song_name: 歌曲名
208
+ :params song_num: 下载的歌曲数
209
+ :params folder: 保存地址
210
+ """
211
+ try :
212
+ url = self .crawler .get_song_url (song_id )
213
+ # 去掉非法字符
214
+ song_name = song_name .replace ('/' , '' )
215
+ song_name = song_name .replace ('.' , '' )
216
+ self .crawler .get_song_by_url (url , song_name , song_num , folder )
217
+
218
+ except :
219
+ click .echo ('download_song_by_id error' )
220
+
221
+
222
+ if __name__ == '__main__' :
223
+ timeout = 60
224
+ output = 'Musics'
225
+ quiet = True
226
+ cookie_path = 'Cookie'
227
+ netease = NetEase (timeout , output , quiet , cookie_path )
228
+ music_list_name = 'music_list.txt'
229
+ # 如果music列表存在, 那么开始下载
230
+ if os .path .exists (music_list_name ):
231
+ with open (music_list_name , 'r' ) as f :
232
+ music_list = list (map (lambda x : x .strip (), f .readlines ()))
233
+ for song_num , song_name in enumerate (music_list ):
234
+ netease .download_song_by_search (song_name ,song_num + 1 )
235
+ else :
236
+ click .echo ('music_list.txt not exist.' )
0 commit comments