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