├── .gitignore
├── License.txt
├── MusicBoxApi
├── __init__.py
├── api.py
├── config.py
├── const.py
├── logger.py
├── singleton.py
├── storage.py
└── utils.py
├── README.md
├── README.rst
├── setup.cfg
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.pyc
3 | #*#
4 | .#*
5 | dist
6 | MusicBoxApi.egg-info/
7 | build
--------------------------------------------------------------------------------
/License.txt:
--------------------------------------------------------------------------------
1 | ### The MIT License (MIT)
2 |
3 | CopyRight (c) 2015 omi <4399.omi@gmail.com>
4 | CopyRight (c) 2015 Joseph Pan <m@hahack.com>
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/MusicBoxApi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wzpan/MusicBoxApi/e6d80c89118d80d187c9a51d2d55a33e91a9b0e1/MusicBoxApi/__init__.py
--------------------------------------------------------------------------------
/MusicBoxApi/api.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # @Author: omi
4 | # @Date: 2014-08-24 21:51:57
5 | '''
6 | 网易云音乐 Api
7 | '''
8 | from __future__ import print_function
9 | from __future__ import unicode_literals
10 | from __future__ import division
11 | from __future__ import absolute_import
12 | from builtins import chr
13 | from builtins import int
14 | from builtins import map
15 | from builtins import open
16 | from builtins import range
17 | from builtins import str
18 | from builtins import pow
19 | from future import standard_library
20 | standard_library.install_aliases()
21 |
22 | import re
23 | import os
24 | import json
25 | import time
26 | import hashlib
27 | import random
28 | import base64
29 | import binascii
30 | import sys
31 | import crypto
32 |
33 | try:
34 | from Crypto.Cipher import AES
35 | except:
36 | sys.modules['Crypto'] = crypto
37 | from Crypto.Cipher import AES
38 |
39 | from Crypto.Cipher import AES
40 | from http.cookiejar import LWPCookieJar
41 | from bs4 import BeautifulSoup
42 | import requests
43 |
44 | from .config import Config
45 | from .storage import Storage
46 | from .utils import notify
47 | from . import logger
48 |
49 |
50 | class TooManyTracksException(Exception):
51 | """The playlist contains more than 1000 tracks."""
52 | pass
53 |
54 |
55 | # 歌曲榜单地址
56 | top_list_all = {
57 | 0: ['云音乐新歌榜', '/discover/toplist?id=3779629'],
58 | 1: ['云音乐热歌榜', '/discover/toplist?id=3778678'],
59 | 2: ['网易原创歌曲榜', '/discover/toplist?id=2884035'],
60 | 3: ['云音乐飙升榜', '/discover/toplist?id=19723756'],
61 | 4: ['云音乐电音榜', '/discover/toplist?id=10520166'],
62 | 5: ['UK排行榜周榜', '/discover/toplist?id=180106'],
63 | 6: ['美国Billboard周榜', '/discover/toplist?id=60198'],
64 | 7: ['KTV嗨榜', '/discover/toplist?id=21845217'],
65 | 8: ['iTunes榜', '/discover/toplist?id=11641012'],
66 | 9: ['Hit FM Top榜', '/discover/toplist?id=120001'],
67 | 10: ['日本Oricon周榜', '/discover/toplist?id=60131'],
68 | 11: ['韩国Melon排行榜周榜', '/discover/toplist?id=3733003'],
69 | 12: ['韩国Mnet排行榜周榜', '/discover/toplist?id=60255'],
70 | 13: ['韩国Melon原声周榜', '/discover/toplist?id=46772709'],
71 | 14: ['中国TOP排行榜(港台榜)', '/discover/toplist?id=112504'],
72 | 15: ['中国TOP排行榜(内地榜)', '/discover/toplist?id=64016'],
73 | 16: ['香港电台中文歌曲龙虎榜', '/discover/toplist?id=10169002'],
74 | 17: ['华语金曲榜', '/discover/toplist?id=4395559'],
75 | 18: ['中国嘻哈榜', '/discover/toplist?id=1899724'],
76 | 19: ['法国 NRJ EuroHot 30周榜', '/discover/toplist?id=27135204'],
77 | 20: ['台湾Hito排行榜', '/discover/toplist?id=112463'],
78 | 21: ['Beatport全球电子舞曲榜', '/discover/toplist?id=3812895']
79 | }
80 |
81 | default_timeout = 10
82 |
83 | log = logger.getLogger(__name__)
84 |
85 | modulus = ('00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7'
86 | 'b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280'
87 | '104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932'
88 | '575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b'
89 | '3ece0462db0a22b8e7')
90 | nonce = '0CoJUm6Qyw8W8jud'
91 | pubKey = '010001'
92 |
93 |
94 | # 歌曲加密算法, 基于https://github.com/yanunon/NeteaseCloudMusic脚本实现
95 | def encrypted_id(id):
96 | magic = bytearray('3go8&$8*3*3h0k(2)2', 'u8')
97 | song_id = bytearray(id, 'u8')
98 | magic_len = len(magic)
99 | for i, sid in enumerate(song_id):
100 | song_id[i] = sid ^ magic[i % magic_len]
101 | m = hashlib.md5(song_id)
102 | result = m.digest()
103 | result = base64.b64encode(result)
104 | result = result.replace(b'/', b'_')
105 | result = result.replace(b'+', b'-')
106 | return result.decode('utf-8')
107 |
108 |
109 | # 登录加密算法, 基于https://github.com/stkevintan/nw_musicbox脚本实现
110 | def encrypted_request(text):
111 | text = json.dumps(text)
112 | log.debug(text)
113 | secKey = createSecretKey(16)
114 | encText = aesEncrypt(aesEncrypt(text, nonce), secKey)
115 | encSecKey = rsaEncrypt(secKey, pubKey, modulus)
116 | data = {'params': encText, 'encSecKey': encSecKey}
117 | return data
118 |
119 |
120 | def aesEncrypt(text, secKey):
121 | pad = 16 - len(text) % 16
122 | text = text + chr(pad) * pad
123 | encryptor = AES.new(secKey, 2, '0102030405060708')
124 | ciphertext = encryptor.encrypt(text)
125 | ciphertext = base64.b64encode(ciphertext).decode('utf-8')
126 | return ciphertext
127 |
128 |
129 | def rsaEncrypt(text, pubKey, modulus):
130 | text = text[::-1]
131 | rs = pow(int(binascii.hexlify(text), 16), int(pubKey, 16), int(modulus, 16))
132 | return format(rs, 'x').zfill(256)
133 |
134 |
135 | def createSecretKey(size):
136 | return binascii.hexlify(os.urandom(size))[:16]
137 |
138 |
139 | # list去重
140 | def uniq(arr):
141 | arr2 = list(set(arr))
142 | arr2.sort(key=arr.index)
143 | return arr2
144 |
145 |
146 | # 获取高音质mp3 url
147 | def geturl(song):
148 | quality = Config().get_item('music_quality')
149 | if song['hMusic'] and quality <= 0:
150 | music = song['hMusic']
151 | quality = 'HD'
152 | play_time = str(music['playTime'])
153 | elif song['mMusic'] and quality <= 1:
154 | music = song['mMusic']
155 | quality = 'MD'
156 | play_time = str(music['playTime'])
157 | elif song['lMusic'] and quality <= 2:
158 | music = song['lMusic']
159 | quality = 'LD'
160 | play_time = str(music['playTime'])
161 | else:
162 | play_time = 0
163 | return song['mp3Url'], '', play_time
164 | quality = quality + ' {0}k'.format(music['bitrate'] // 1000)
165 | song_id = str(music['dfsId'])
166 | enc_id = encrypted_id(song_id)
167 | url = 'http://m%s.music.126.net/%s/%s.mp3' % (2,
168 | enc_id, song_id)
169 | return url, quality, play_time
170 |
171 |
172 | def geturl_new_api(song):
173 | br_to_quality = {128000: 'MD 128k', 320000: 'HD 320k'}
174 | alter = NetEase().songs_detail_new_api([song['id']])[0]
175 | url = alter['url']
176 | quality = br_to_quality.get(alter['br'], '')
177 | return url, quality
178 |
179 |
180 | def geturls_new_api(song_ids):
181 | """ 批量获取音乐的地址 """
182 | br_to_quality = {128000: 'MD 128k', 320000: 'HD 320k'}
183 | alters = NetEase().songs_detail_new_api(song_ids)
184 | urls = [alter['url'] for alter in alters]
185 | return urls
186 |
187 |
188 | class NetEase(object):
189 |
190 | def __init__(self):
191 | self.header = {
192 | 'Accept': '*/*',
193 | 'Accept-Encoding': 'gzip,deflate,sdch',
194 | 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
195 | 'Connection': 'keep-alive',
196 | 'Content-Type': 'application/x-www-form-urlencoded',
197 | 'Host': 'music.163.com',
198 | 'Referer': 'http://music.163.com/search/',
199 | 'User-Agent':
200 | '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' # NOQA
201 | }
202 | self.cookies = {'appver': '1.5.2', 'os': 'osx'}
203 | self.playlist_class_dict = {}
204 | self.session = requests.Session()
205 | self.storage = Storage()
206 | self.session.cookies = requests.utils.add_dict_to_cookiejar(LWPCookieJar(self.storage.cookie_path), self.cookies)
207 | try:
208 | self.session.cookies.load()
209 | cookie = ''
210 | if os.path.isfile(self.storage.cookie_path):
211 | self.file = open(self.storage.cookie_path, 'r')
212 | cookie = self.file.read()
213 | self.file.close()
214 | expire_time = re.compile(r'\d{4}-\d{2}-\d{2}').findall(cookie)
215 | if expire_time:
216 | if expire_time[0] < time.strftime('%Y-%m-%d', time.localtime(time.time())):
217 | self.storage.database['user'] = {
218 | 'username': '',
219 | 'password': '',
220 | 'user_id': '',
221 | 'nickname': '',
222 | }
223 | self.storage.save()
224 | os.remove(self.storage.cookie_path)
225 | except IOError as e:
226 | log.error(e)
227 | self.session.cookies.save()
228 |
229 | def return_toplists(self):
230 | return [l[0] for l in top_list_all.values()]
231 |
232 | def httpRequest(self,
233 | method,
234 | action,
235 | query=None,
236 | urlencoded=None,
237 | callback=None,
238 | timeout=None):
239 | connection = json.loads(
240 | self.rawHttpRequest(method, action, query, urlencoded, callback, timeout)
241 | )
242 | return connection
243 |
244 | def rawHttpRequest(self,
245 | method,
246 | action,
247 | query=None,
248 | urlencoded=None,
249 | callback=None,
250 | timeout=None):
251 | if method == 'GET':
252 | url = action if query is None else action + '?' + query
253 | connection = self.session.get(url,
254 | headers=self.header,
255 | timeout=default_timeout)
256 |
257 | elif method == 'POST':
258 | connection = self.session.post(action,
259 | data=query,
260 | headers=self.header,
261 | timeout=default_timeout)
262 |
263 | elif method == 'Login_POST':
264 | connection = self.session.post(action,
265 | data=query,
266 | headers=self.header,
267 | timeout=default_timeout)
268 | self.session.cookies.save()
269 |
270 | connection.encoding = 'UTF-8'
271 | return connection.text
272 |
273 | # 登录
274 | def login(self, username, password):
275 | pattern = re.compile(r'^0\d{2,3}\d{7,8}$|^1[34578]\d{9}$')
276 | if pattern.match(username):
277 | return self.phone_login(username, password)
278 | action = 'https://music.163.com/weapi/login?csrf_token='
279 | text = {
280 | 'username': username,
281 | 'password': password,
282 | 'rememberLogin': 'true'
283 | }
284 | data = encrypted_request(text)
285 | try:
286 | return self.httpRequest('Login_POST', action, data)
287 | except requests.exceptions.RequestException as e:
288 | log.error(e)
289 | return {'code': 501}
290 |
291 | # 手机登录
292 | def phone_login(self, username, password, countrycode = None):
293 | action = 'https://music.163.com/weapi/login/cellphone'
294 | text = {
295 | 'phone': username,
296 | 'password': password,
297 | 'rememberLogin': 'true'
298 | }
299 | if countrycode is not None:
300 | text['countrycode'] = countrycode
301 | data = encrypted_request(text)
302 | try:
303 | return self.httpRequest('Login_POST', action, data)
304 | except requests.exceptions.RequestException as e:
305 | log.error(e)
306 | return {'code': 501}
307 |
308 | # 每日签到
309 | def daily_signin(self, type):
310 | action = 'http://music.163.com/weapi/point/dailyTask'
311 | text = {'type': type}
312 | data = encrypted_request(text)
313 | try:
314 | return self.httpRequest('POST', action, data)
315 | except requests.exceptions.RequestException as e:
316 | log.error(e)
317 | return -1
318 |
319 | # 用户歌单
320 | def user_playlist(self, uid, offset=0, limit=100):
321 | action = 'http://music.163.com/api/user/playlist/?offset={}&limit={}&uid={}'.format( # NOQA
322 | offset, limit, uid)
323 | try:
324 | data = self.httpRequest('GET', action)
325 | return data['playlist']
326 | except (requests.exceptions.RequestException, KeyError) as e:
327 | log.error(e)
328 | return -1
329 |
330 | # 每日推荐歌单
331 | def recommend_playlist(self):
332 | try:
333 | action = 'http://music.163.com/weapi/v1/discovery/recommend/songs?csrf_token=' # NOQA
334 | self.session.cookies.load()
335 | csrf = ''
336 | for cookie in self.session.cookies:
337 | if cookie.name == '__csrf':
338 | csrf = cookie.value
339 | if csrf == '':
340 | return False
341 | action += csrf
342 | req = {'offset': 0, 'total': True, 'limit': 20, 'csrf_token': csrf}
343 | page = self.session.post(action,
344 | data=encrypted_request(req),
345 | headers=self.header,
346 | timeout=default_timeout)
347 | results = json.loads(page.text)['recommend']
348 | song_ids = []
349 | for result in results:
350 | song_ids.append(result['id'])
351 | data = map(self.song_detail, song_ids)
352 | return [d[0] for d in data]
353 | except (requests.exceptions.RequestException, ValueError) as e:
354 | log.error(e)
355 | return False
356 |
357 | # 私人FM
358 | def personal_fm(self):
359 | action = 'http://music.163.com/api/radio/get'
360 | try:
361 | data = self.httpRequest('GET', action)
362 | return data['data']
363 | except requests.exceptions.RequestException as e:
364 | log.error(e)
365 | return -1
366 |
367 | # like
368 | def fm_like(self, songid, like=True, time=25, alg='itembased'):
369 | action = 'http://music.163.com/api/radio/like?alg={}&trackId={}&like={}&time={}'.format( # NOQA
370 | alg, songid, 'true' if like else 'false', time)
371 |
372 | try:
373 | data = self.httpRequest('GET', action)
374 | if data['code'] == 200:
375 | return data
376 | else:
377 | return -1
378 | except requests.exceptions.RequestException as e:
379 | log.error(e)
380 | return -1
381 |
382 | # FM trash
383 | def fm_trash(self, songid, time=25, alg='RT'):
384 | action = 'http://music.163.com/api/radio/trash/add?alg={}&songId={}&time={}'.format( # NOQA
385 | alg, songid, time)
386 | try:
387 | data = self.httpRequest('GET', action)
388 | if data['code'] == 200:
389 | return data
390 | else:
391 | return -1
392 | except requests.exceptions.RequestException as e:
393 | log.error(e)
394 | return -1
395 |
396 | # 搜索单曲(1),歌手(100),专辑(10),歌单(1000),用户(1002) *(type)*
397 | def search(self, s, stype=1, offset=0, total='true', limit=60):
398 | action = 'http://music.163.com/api/search/get'
399 | data = {
400 | 's': s,
401 | 'type': stype,
402 | 'offset': offset,
403 | 'total': total,
404 | 'limit': limit
405 | }
406 | return self.httpRequest('POST', action, data)
407 |
408 | # 新碟上架 http://music.163.com/#/discover/album/
409 | def new_albums(self, offset=0, limit=50):
410 | action = 'http://music.163.com/api/album/new?area=ALL&offset={}&total=true&limit={}'.format( # NOQA
411 | offset, limit)
412 | try:
413 | data = self.httpRequest('GET', action)
414 | return data['albums']
415 | except requests.exceptions.RequestException as e:
416 | log.error(e)
417 | return []
418 |
419 | # 歌单(网友精选碟) hot||new http://music.163.com/#/discover/playlist/
420 | def top_playlists(self, category='全部', order='hot', offset=0, limit=50):
421 | action = 'http://music.163.com/api/playlist/list?cat={}&order={}&offset={}&total={}&limit={}'.format( # NOQA
422 | category, order, offset, 'true' if offset else 'false',
423 | limit) # NOQA
424 | try:
425 | data = self.httpRequest('GET', action)
426 | return data['playlists']
427 | except requests.exceptions.RequestException as e:
428 | log.error(e)
429 | return []
430 |
431 | # 分类歌单
432 | def playlist_classes(self):
433 | action = 'http://music.163.com/discover/playlist/'
434 | try:
435 | data = self.rawHttpRequest('GET', action)
436 | return data
437 | except requests.exceptions.RequestException as e:
438 | log.error(e)
439 | return []
440 |
441 | # 分类歌单中某一个分类的详情
442 | def playlist_class_detail(self):
443 | pass
444 |
445 | # 歌单详情
446 | def playlist_detail(self, playlist_id):
447 | action = 'http://music.163.com/api/playlist/detail?id={}'.format(
448 | playlist_id)
449 | try:
450 | data = self.httpRequest('GET', action)
451 | if data['code'] == 200:
452 | result = data['result']
453 | length = result['trackCount']
454 | if length > 1000:
455 | raise TooManyTracksException("There are {} (> 1000) in the playlist.".format(length))
456 | else:
457 | return data['result']['tracks']
458 | else:
459 | log.error(data['code'])
460 | return []
461 | except requests.exceptions.RequestException as e:
462 | log.error(e)
463 | return []
464 |
465 | # 热门歌手 http://music.163.com/#/discover/artist/
466 | def top_artists(self, offset=0, limit=100):
467 | action = 'http://music.163.com/api/artist/top?offset={}&total=false&limit={}'.format( # NOQA
468 | offset, limit)
469 | try:
470 | data = self.httpRequest('GET', action)
471 | return data['artists']
472 | except requests.exceptions.RequestException as e:
473 | log.error(e)
474 | return []
475 |
476 | # 热门单曲 http://music.163.com/discover/toplist?id=
477 | def top_songlist(self, idx=0, offset=0, limit=100):
478 | action = 'http://music.163.com' + top_list_all[idx][1]
479 | try:
480 | connection = requests.get(action,
481 | headers=self.header,
482 | timeout=default_timeout)
483 | connection.encoding = 'UTF-8'
484 | songids = re.findall(r'/song\?id=(\d+)', connection.text)
485 | if songids == []:
486 | return []
487 | # 去重
488 | songids = uniq(songids)
489 | return self.songs_detail(songids)
490 | except requests.exceptions.RequestException as e:
491 | log.error(e)
492 | return []
493 |
494 | # 歌手单曲
495 | def artists(self, artist_id):
496 | action = 'http://music.163.com/api/artist/{}'.format(artist_id)
497 | try:
498 | data = self.httpRequest('GET', action)
499 | return data['hotSongs']
500 | except requests.exceptions.RequestException as e:
501 | log.error(e)
502 | return []
503 |
504 | def get_artist_album(self, artist_id, offset=0, limit=50):
505 | action = 'http://music.163.com/api/artist/albums/{}?offset={}&limit={}'.format(
506 | artist_id, offset, limit)
507 | try:
508 | data = self.httpRequest('GET', action)
509 | return data['hotAlbums']
510 | except requests.exceptions.RequestException as e:
511 | log.error(e)
512 | return []
513 |
514 | # album id --> song id set
515 | def album(self, album_id):
516 | action = 'http://music.163.com/api/album/{}'.format(album_id)
517 | try:
518 | data = self.httpRequest('GET', action)
519 | return data['album']['songs']
520 | except requests.exceptions.RequestException as e:
521 | log.error(e)
522 | return []
523 |
524 | def song_comments(self, music_id, offset=0, total='false', limit=100):
525 | action = 'http://music.163.com/api/v1/resource/comments/R_SO_4_{}/?rid=R_SO_4_{}&\
526 | offset={}&total={}&limit={}'.format(music_id, music_id, offset, total, limit)
527 | try:
528 | comments = self.httpRequest('GET', action)
529 | return comments
530 | except requests.exceptions.RequestException as e:
531 | log.error(e)
532 | return []
533 |
534 | # song ids --> song urls ( details )
535 | def songs_detail(self, ids, offset=0):
536 | tmpids = ids[offset:]
537 | tmpids = tmpids[0:100]
538 | tmpids = list(map(str, tmpids))
539 | action = 'http://music.163.com/api/song/detail?ids=[{}]'.format( # NOQA
540 | ','.join(tmpids))
541 | try:
542 | data = self.httpRequest('GET', action)
543 |
544 | # the order of data['songs'] is no longer the same as tmpids,
545 | # so just make the order back
546 | data['songs'].sort(key=lambda song: tmpids.index(str(song['id'])))
547 |
548 | return data['songs']
549 | except requests.exceptions.RequestException as e:
550 | log.error(e)
551 | return []
552 |
553 | def songs_detail_new_api(self, music_ids, bit_rate=320000):
554 | action = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token=' # NOQA
555 | self.session.cookies.load()
556 | csrf = ''
557 | for cookie in self.session.cookies:
558 | if cookie.name == '__csrf':
559 | csrf = cookie.value
560 | if csrf == '':
561 | notify('You Need Login', 1)
562 | action += csrf
563 | data = {'ids': music_ids, 'br': bit_rate, 'csrf_token': csrf}
564 | connection = self.session.post(action,
565 | data=encrypted_request(data),
566 | headers=self.header, )
567 | result = json.loads(connection.text)
568 | return result['data']
569 |
570 | # song id --> song url ( details )
571 | def song_detail(self, music_id):
572 | action = 'http://music.163.com/api/song/detail/?id={}&ids=[{}]'.format(
573 | music_id, music_id) # NOQA
574 | try:
575 | data = self.httpRequest('GET', action)
576 | return data['songs']
577 | except requests.exceptions.RequestException as e:
578 | log.error(e)
579 | return []
580 |
581 | # lyric http://music.163.com/api/song/lyric?os=osx&id= &lv=-1&kv=-1&tv=-1
582 | def song_lyric(self, music_id):
583 | action = 'http://music.163.com/api/song/lyric?os=osx&id={}&lv=-1&kv=-1&tv=-1'.format( # NOQA
584 | music_id)
585 | try:
586 | data = self.httpRequest('GET', action)
587 | if 'lrc' in data and data['lrc']['lyric'] is not None:
588 | lyric_info = data['lrc']['lyric']
589 | else:
590 | lyric_info = '未找到歌词'
591 | return lyric_info
592 | except requests.exceptions.RequestException as e:
593 | log.error(e)
594 | return []
595 |
596 | def song_tlyric(self, music_id):
597 | action = 'http://music.163.com/api/song/lyric?os=osx&id={}&lv=-1&kv=-1&tv=-1'.format( # NOQA
598 | music_id)
599 | try:
600 | data = self.httpRequest('GET', action)
601 | if 'tlyric' in data and data['tlyric'].get('lyric') is not None:
602 | lyric_info = data['tlyric']['lyric'][1:]
603 | else:
604 | lyric_info = '未找到歌词翻译'
605 | return lyric_info
606 | except requests.exceptions.RequestException as e:
607 | log.error(e)
608 | return []
609 |
610 | # 今日最热(0), 本周最热(10),历史最热(20),最新节目(30)
611 | def djchannels(self, stype=0, offset=0, limit=50):
612 | action = 'http://music.163.com/discover/djradio?type={}&offset={}&limit={}'.format( # NOQA
613 | stype, offset, limit)
614 | try:
615 | connection = requests.get(action,
616 | headers=self.header,
617 | timeout=default_timeout)
618 | connection.encoding = 'UTF-8'
619 | channelids = re.findall(r'/program\?id=(\d+)', connection.text)
620 | channelids = uniq(channelids)
621 | return self.channel_detail(channelids)
622 | except requests.exceptions.RequestException as e:
623 | log.error(e)
624 | return []
625 |
626 | # DJchannel ( id, channel_name ) ids --> song urls ( details )
627 | # 将 channels 整理为 songs 类型
628 | def channel_detail(self, channelids, offset=0):
629 | channels = []
630 | for i in range(0, len(channelids)):
631 | action = 'http://music.163.com/api/dj/program/detail?id={}'.format(
632 | channelids[i])
633 | try:
634 | data = self.httpRequest('GET', action)
635 | channel = self.dig_info(data['program']['mainSong'], 'channels')
636 | channels.append(channel)
637 | except requests.exceptions.RequestException as e:
638 | log.error(e)
639 | continue
640 |
641 | return channels
642 |
643 | # 获取版本
644 | def get_version(self):
645 | action = 'https://pypi.python.org/pypi?:action=doap&name=NetEase-MusicBox' # NOQA
646 | try:
647 | data = requests.get(action)
648 | return data.content
649 | except requests.exceptions.RequestException as e:
650 | log.error(e)
651 | return ""
652 |
653 | def dig_info(self, data, dig_type):
654 | temp = []
655 | if dig_type == 'songs' or dig_type == 'fmsongs':
656 | for i in range(0, len(data)):
657 | url, quality, play_time = geturl(data[i])
658 | if data[i]['album'] is not None:
659 | album_name = data[i]['album']['name']
660 | album_id = data[i]['album']['id']
661 | else:
662 | album_name = '未知专辑'
663 | album_id = ''
664 |
665 | song_info = {
666 | 'song_id': data[i]['id'],
667 | 'artist': [],
668 | 'song_name': data[i]['name'],
669 | 'album_name': album_name,
670 | 'album_id': album_id,
671 | 'mp3_url': url,
672 | 'quality': quality,
673 | 'playTime': play_time
674 | }
675 | if 'artist' in data[i]:
676 | song_info['artist'] = data[i]['artist']
677 | elif 'artists' in data[i]:
678 | for j in range(0, len(data[i]['artists'])):
679 | song_info['artist'].append(data[i]['artists'][j][
680 | 'name'])
681 | song_info['artist'] = ', '.join(song_info['artist'])
682 | else:
683 | song_info['artist'] = '未知艺术家'
684 |
685 | temp.append(song_info)
686 |
687 |
688 |
689 | elif dig_type == 'artists':
690 | artists = []
691 | for i in range(0, len(data)):
692 | artists_info = {
693 | 'artist_id': data[i]['id'],
694 | 'artists_name': data[i]['name'],
695 | 'alias': ''.join(data[i]['alias'])
696 | }
697 | artists.append(artists_info)
698 |
699 | return artists
700 |
701 | elif dig_type == 'albums':
702 | for i in range(0, len(data)):
703 | albums_info = {
704 | 'album_id': data[i]['id'],
705 | 'albums_name': data[i]['name'],
706 | 'artists_name': data[i]['artist']['name']
707 | }
708 | temp.append(albums_info)
709 |
710 | elif dig_type == 'top_playlists':
711 | for i in range(0, len(data)):
712 | playlists_info = {
713 | 'playlist_id': data[i]['id'],
714 | 'playlists_name': data[i]['name'],
715 | 'creator_name': data[i]['creator']['nickname']
716 | }
717 | temp.append(playlists_info)
718 |
719 | elif dig_type == 'channels':
720 | url, quality = geturl(data)
721 | channel_info = {
722 | 'song_id': data['id'],
723 | 'song_name': data['name'],
724 | 'artist': data['artists'][0]['name'],
725 | 'album_name': '主播电台',
726 | 'mp3_url': url,
727 | 'quality': quality
728 | }
729 | temp = channel_info
730 |
731 | elif dig_type == 'playlist_classes':
732 | soup = BeautifulSoup(data, 'lxml')
733 | dls = soup.select('dl.f-cb')
734 | for dl in dls:
735 | title = dl.dt.text
736 | sub = [item.text for item in dl.select('a')]
737 | temp.append(title)
738 | self.playlist_class_dict[title] = sub
739 |
740 | elif dig_type == 'playlist_class_detail':
741 | log.debug(data)
742 | temp = self.playlist_class_dict[data]
743 |
744 |
745 | elif dig_type == 'user_songs':
746 | for i in range(0, len(data)):
747 | url, quality = geturl_new_api(data[i])
748 | song_info = {
749 | 'song_id': data[i]['id'],
750 | 'artist': [],
751 | 'song_name': data[i]['name'],
752 | 'mp3_url': url,
753 | 'quality': quality,
754 | }
755 | temp.append(song_info)
756 |
757 |
758 | return temp
759 |
760 |
761 | if __name__ == '__main__':
762 | ne = NetEase()
763 | print(geturl_new_api(ne.songs_detail([27902910])[0])) # MD 128k, fallback
764 | print(ne.songs_detail_new_api([27902910])[0]['url'])
765 | print(ne.songs_detail([405079776])[0]['mp3Url']) # old api
766 | print(requests.get(ne.songs_detail([405079776])[0]['mp3Url']).status_code) # 404
767 |
--------------------------------------------------------------------------------
/MusicBoxApi/config.py:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 |
3 | from __future__ import unicode_literals
4 | from __future__ import print_function
5 | from __future__ import division
6 | from __future__ import absolute_import
7 | from builtins import open
8 | from future import standard_library
9 | standard_library.install_aliases()
10 | import json
11 | import os
12 |
13 | from . import logger
14 | from .singleton import Singleton
15 | from .const import Constant
16 | from .utils import utf8_data_to_file
17 |
18 | log = logger.getLogger(__name__)
19 |
20 |
21 | class Config(Singleton):
22 |
23 | def __init__(self):
24 | if hasattr(self, '_init'):
25 | return
26 | self._init = True
27 | self.config_file_path = Constant.config_path
28 | self.default_config = {
29 | 'version': 7,
30 | 'cache': {
31 | 'value': False,
32 | 'default': False,
33 | 'describe': ('A toggle to enable cache function or not. '
34 | 'Set value to true to enable it.')
35 | },
36 | 'mpg123_parameters': {
37 | 'value': [],
38 | 'default': [],
39 | 'describe': 'The additional parameters when mpg123 start.'
40 | },
41 | 'aria2c_parameters': {
42 | 'value': [],
43 | 'default': [],
44 | 'describe': ('The additional parameters when '
45 | 'aria2c start to download something.')
46 | },
47 | 'music_quality': {
48 | 'value': 0,
49 | 'default': 0,
50 | 'describe': ('Select the quality of the music. '
51 | 'May be useful when network is terrible. '
52 | '0 for high quality, 1 for medium and 2 for low.')
53 | },
54 | 'global_play_pause': {
55 | 'value': 'p',
56 | 'default': 'p',
57 | 'describe': 'Global keybind for play/pause.'
58 | 'Uses gtk notation for keybinds.'
59 | },
60 | 'global_next': {
61 | 'value': 'j',
62 | 'default': 'j',
63 | 'describe': 'Global keybind for next song.'
64 | 'Uses gtk notation for keybinds.'
65 | },
66 | 'global_previous': {
67 | 'value': 'k',
68 | 'default': 'k',
69 | 'describe': 'Global keybind for previous song.'
70 | 'Uses gtk notation for keybinds.'
71 | },
72 | 'notifier': {
73 | 'value': True,
74 | 'default': True,
75 | 'describe': 'Notifier when switching songs.'
76 | },
77 | 'translation': {
78 | 'value': True,
79 | 'default': True,
80 | 'describe': 'Foreign language lyrics translation.'
81 | },
82 | 'osdlyrics': {
83 | 'value': False,
84 | 'default': False,
85 | 'describe': 'Desktop lyrics for musicbox.'
86 | },
87 | 'osdlyrics_transparent': {
88 | 'value': False,
89 | 'default': False,
90 | 'describe': 'Desktop lyrics transparent bg.'
91 | },
92 | 'osdlyrics_color': {
93 | 'value': [225, 248, 113],
94 | 'default': [225, 248, 113],
95 | 'describe': 'Desktop lyrics RGB Color.'
96 | },
97 | 'osdlyrics_font': {
98 | 'value': ['Decorative', 16],
99 | 'default': ['Decorative', 16],
100 | 'describe': 'Desktop lyrics font-family and font-size.'
101 | },
102 | 'osdlyrics_background': {
103 | 'value': 'rgba(100, 100, 100, 120)',
104 | 'default': 'rgba(100, 100, 100, 120)',
105 | 'describe': 'Desktop lyrics background color.'
106 | },
107 | 'osdlyrics_on_top': {
108 | 'value': True,
109 | 'default': True,
110 | 'describe': 'Desktop lyrics OnTopHint.'
111 | },
112 | 'curses_transparency': {
113 | 'value': False,
114 | 'default': False,
115 | 'describe': 'Set true to make curses transparency.'
116 | }
117 | }
118 | self.config = {}
119 | if not os.path.isfile(self.config_file_path):
120 | self.generate_config_file()
121 | try:
122 | f = open(self.config_file_path, 'r')
123 | except IOError:
124 | log.debug('Read config file error.')
125 | return
126 | try:
127 | self.config = json.loads(f.read())
128 | except ValueError:
129 | log.debug('Load config json data failed.')
130 | return
131 | f.close()
132 | if not self.check_version():
133 | self.save_config_file()
134 |
135 | def generate_config_file(self):
136 | f = open(self.config_file_path, 'w')
137 | utf8_data_to_file(f, json.dumps(self.default_config, indent=2))
138 | f.close()
139 |
140 | def save_config_file(self):
141 | f = open(self.config_file_path, 'w')
142 | utf8_data_to_file(f, json.dumps(self.config, indent=2))
143 | f.close()
144 |
145 | def check_version(self):
146 | if self.config['version'] == self.default_config['version']:
147 | return True
148 | else:
149 | # Should do some update. Like
150 | # if self.database['version'] == 2 : self.database.['version'] = 3
151 | # update database form version 1 to version 2
152 | if self.config['version'] == 1:
153 | self.config['version'] = 2
154 | self.config['global_play_pause'] = {
155 | 'value': 'p',
156 | 'default': 'p',
157 | 'describe': 'Global keybind for play/pause.'
158 | 'Uses gtk notation for keybinds.'
159 | }
160 | self.config['global_next'] = {
161 | 'value': 'j',
162 | 'default': 'j',
163 | 'describe': 'Global keybind for next song.'
164 | 'Uses gtk notation for keybinds.'
165 | }
166 | self.config['global_previous'] = {
167 | 'value': 'k',
168 | 'default': 'k',
169 | 'describe': 'Global keybind for previous song.'
170 | 'Uses gtk notation for keybinds.'
171 | }
172 | elif self.config['version'] == 2:
173 | self.config['version'] = 3
174 | self.config['notifier'] = {
175 | 'value': True,
176 | 'default': True,
177 | 'describe': 'Notifier when switching songs.'
178 | }
179 | elif self.config['version'] == 3:
180 | self.config['version'] = 4
181 | self.config['translation'] = {
182 | 'value': True,
183 | 'default': True,
184 | 'describe': 'Foreign language lyrics translation.'
185 | }
186 | elif self.config['version'] == 4:
187 | self.config['version'] = 5
188 | self.config['osdlyrics'] = {
189 | 'value': False,
190 | 'default': False,
191 | 'describe': 'Desktop lyrics for musicbox.'
192 | }
193 | self.config['osdlyrics_color'] = {
194 | 'value': [225, 248, 113],
195 | 'default': [225, 248, 113],
196 | 'describe': 'Desktop lyrics RGB Color.'
197 | }
198 | self.config['osdlyrics_font'] = {
199 | 'value': ['Decorative', 16],
200 | 'default': ['Decorative', 16],
201 | 'describe': 'Desktop lyrics font-family and font-size.'
202 | }
203 | self.config['osdlyrics_background'] = {
204 | 'value': 'rgba(100, 100, 100, 120)',
205 | 'default': 'rgba(100, 100, 100, 120)',
206 | 'describe': 'Desktop lyrics background color.'
207 | }
208 | self.config['osdlyrics_transparent'] = {
209 | 'value': False,
210 | 'default': False,
211 | 'describe': 'Desktop lyrics transparent bg.'
212 | }
213 | elif self.config['version'] == 5:
214 | self.config['version'] = 6
215 | self.config['osdlyrics_on_top'] = {
216 | 'value': True,
217 | 'default': True,
218 | 'describe': 'Desktop lyrics OnTopHint.'
219 | }
220 | elif self.config['version'] == 6:
221 | self.config['version'] = 7
222 | self.config['curses_transparency'] = {
223 | 'value': False,
224 | 'default': False,
225 | 'describe': 'Set true to make curses transparency.'
226 | }
227 | #self.check_version()
228 | return False
229 |
230 | def get_item(self, name):
231 | if name not in self.config.keys():
232 | if name not in self.default_config.keys():
233 | return None
234 | return self.default_config[name].get('value')
235 | return self.config[name].get('value')
236 |
--------------------------------------------------------------------------------
/MusicBoxApi/const.py:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 |
3 | from __future__ import unicode_literals
4 | from __future__ import print_function
5 | from __future__ import division
6 | from __future__ import absolute_import
7 | from future import standard_library
8 | standard_library.install_aliases()
9 | import os
10 |
11 |
12 | class Constant(object):
13 | conf_dir = os.path.join(os.path.expanduser('~'), '.netease-musicbox')
14 | download_dir = os.path.join(conf_dir, 'cached')
15 | config_path = os.path.join(conf_dir, 'config.json')
16 | storage_path = os.path.join(conf_dir, 'database.json')
17 | cookie_path = os.path.join(conf_dir, 'cookie')
18 | log_path = os.path.join(conf_dir, 'musicbox.log')
19 |
--------------------------------------------------------------------------------
/MusicBoxApi/logger.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # @Author: omi
4 | # @Date: 2014-08-24 21:51:57
5 |
6 | from __future__ import unicode_literals
7 | from __future__ import print_function
8 | from __future__ import division
9 | from __future__ import absolute_import
10 | from builtins import open
11 | from future import standard_library
12 | standard_library.install_aliases()
13 | import os
14 | import logging
15 |
16 | from . import const
17 |
18 | FILE_NAME = const.Constant.log_path
19 | if os.path.isdir(const.Constant.conf_dir) is False:
20 | os.mkdir(const.Constant.conf_dir)
21 |
22 | with open(FILE_NAME, 'a+') as f:
23 | f.write('#' * 80)
24 | f.write('\n')
25 |
26 |
27 | def getLogger(name):
28 | log = logging.getLogger(name)
29 | log.setLevel(logging.DEBUG)
30 |
31 | # File output handler
32 | fh = logging.FileHandler(FILE_NAME)
33 | fh.setLevel(logging.DEBUG)
34 | fh.setFormatter(logging.Formatter(
35 | '%(asctime)s - %(levelname)s - %(name)s:%(lineno)s: %(message)s')) # NOQA
36 | log.addHandler(fh)
37 |
38 | return log
39 |
--------------------------------------------------------------------------------
/MusicBoxApi/singleton.py:
--------------------------------------------------------------------------------
1 | class Singleton(object):
2 | """Singleton Class
3 | This is a class to make some class being a Singleton class.
4 | Such as database class or config class.
5 |
6 | usage:
7 | class xxx(Singleton):
8 | def __init__(self):
9 | if hasattr(self, '_init'):
10 | return
11 | self._init = True
12 | other init method
13 | """
14 |
15 | def __new__(cls, *args, **kwargs):
16 | if not hasattr(cls, '_instance'):
17 | orig = super(Singleton, cls)
18 | cls._instance = orig.__new__(cls, *args, **kwargs)
19 | return cls._instance
20 |
--------------------------------------------------------------------------------
/MusicBoxApi/storage.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # @Author: Catofes
3 | # @Date: 2015-08-15
4 | '''
5 | Class to stores everything into a json file.
6 | '''
7 | from __future__ import unicode_literals
8 | from __future__ import print_function
9 | from __future__ import division
10 | from __future__ import absolute_import
11 | from builtins import open
12 | from future import standard_library
13 | standard_library.install_aliases()
14 | import json
15 |
16 | from .const import Constant
17 | from .singleton import Singleton
18 | from .utils import utf8_data_to_file
19 |
20 |
21 | class Storage(Singleton):
22 |
23 | def __init__(self):
24 | '''
25 | Database stores every info.
26 |
27 | version int
28 | #if value in file is unequal to value defined in this class.
29 | #An database update will be applied.
30 | user dict:
31 | username str
32 | key str
33 | collections list:
34 | collection_info(dict):
35 | collection_name str
36 | collection_type str
37 | collection_describe str
38 | collection_songs list:
39 | song_id(int)
40 | songs dict:
41 | song_id(int) dict:
42 | song_id int
43 | artist str
44 | song_name str
45 | mp3_url str
46 | album_name str
47 | album_id str
48 | quality str
49 | lyric str
50 | tlyric str
51 | player_info dict:
52 | player_list list:
53 | songs_id(int)
54 | playing_list list:
55 | songs_id(int)
56 | playing_mode int
57 | playing_offset int
58 |
59 |
60 | :return:
61 | '''
62 | if hasattr(self, '_init'):
63 | return
64 | self._init = True
65 | self.version = 4
66 | self.database = {
67 | 'version': 4,
68 | 'user': {
69 | 'username': '',
70 | 'password': '',
71 | 'user_id': '',
72 | 'nickname': '',
73 | },
74 | 'collections': [[]],
75 | 'songs': {},
76 | 'player_info': {
77 | 'player_list': [],
78 | 'player_list_type': '',
79 | 'player_list_title': '',
80 | 'playing_list': [],
81 | 'playing_mode': 0,
82 | 'idx': 0,
83 | 'ridx': 0,
84 | 'playing_volume': 60,
85 | }
86 | }
87 | self.storage_path = Constant.storage_path
88 | self.cookie_path = Constant.cookie_path
89 | self.file = None
90 |
91 | def load(self):
92 | try:
93 | self.file = open(self.storage_path, 'r')
94 | self.database = json.loads(self.file.read())
95 | self.file.close()
96 | except (ValueError, OSError, IOError):
97 | self.__init__()
98 | if not self.check_version():
99 | self.save()
100 |
101 | def check_version(self):
102 | if self.database['version'] == self.version:
103 | return True
104 | else:
105 | # Should do some update.
106 | if self.database['version'] == 1:
107 | self.database['version'] = 2
108 | self.database['cache'] = False
109 | elif self.database['version'] == 2:
110 | self.database['version'] = 3
111 | self.database.pop('cache')
112 | elif self.database['version'] == 3:
113 | self.database['version'] = 4
114 | self.database['user'] = {'username': '',
115 | 'password': '',
116 | 'user_id': '',
117 | 'nickname': ''}
118 | self.check_version()
119 | return False
120 |
121 | def save(self):
122 | self.file = open(self.storage_path, 'w')
123 | db_str = json.dumps(self.database)
124 | utf8_data_to_file(self.file, db_str)
125 | self.file.close()
126 |
--------------------------------------------------------------------------------
/MusicBoxApi/utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # utils.py --- utils for musicbox
4 | # Copyright (c) 2015-2016 omi & Contributors
5 |
6 | from __future__ import unicode_literals
7 | from __future__ import print_function
8 | from __future__ import division
9 | from __future__ import absolute_import
10 | from builtins import str
11 | from future import standard_library
12 | standard_library.install_aliases()
13 | import platform
14 | import os
15 |
16 |
17 | def utf8_data_to_file(f, data):
18 | if hasattr(data, 'decode'):
19 | f.write(data.decode('utf-8'))
20 | else:
21 | f.write(data)
22 |
23 |
24 | def notify_command_osx(msg, msg_type, t=None):
25 | command = '/usr/bin/osascript -e "display notification \\\"{}\\\" {} with title \\\"musicbox\\\""'
26 | sound = 'sound name \\\"/System/Library/Sounds/Ping.aiff\\\"' if msg_type else ''
27 | return command.format(msg, sound)
28 |
29 |
30 | def notify_command_linux(msg, t=None):
31 | command = '/usr/bin/notify-send "' + msg + '"'
32 | if t:
33 | command += ' -t ' + str(t)
34 | command += ' -h int:transient:1'
35 | return command
36 |
37 |
38 | def notify(msg, msg_type=0, t=None):
39 | "Show system notification with duration t (ms)"
40 | if platform.system() == 'Darwin':
41 | command = notify_command_osx(msg, msg_type, t)
42 | else:
43 | command = notify_command_linux(msg, t)
44 | os.system(command.encode('utf-8'))
45 |
46 |
47 | if __name__ == "__main__":
48 | notify("I'm test 1", msg_type=1, t=1000)
49 | notify("I'm test 2", msg_type=0, t=1000)
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MusicBoxApi
2 |
3 | [](LICENSE.txt)
4 | [](https://pypi.python.org/pypi/MusicBoxApi/)
5 | []()
6 | []()
7 |
8 | 从 musicbox 网易云音乐 CLI 播放器抽离出来的 API ,去掉了界面相关的逻辑,方便在其他程序中复用。
9 |
10 | ## 安装
11 |
12 | ``` sh
13 | pip install MusicBoxApi
14 | ```
15 |
16 | ## 使用示例
17 |
18 | ``` py
19 | from MusicBoxApi import api as NetEaseApi
20 |
21 | def get_top_songlist():
22 | netease = NetEaseApi.NetEase()
23 | music_list = netease.top_songlist()
24 | datalist = netease.dig_info(music_list, 'songs')
25 | playlist = []
26 | for data in datalist:
27 | music_info = {}
28 | music_info.setdefault("song_name", data.get("song_name"))
29 | music_info.setdefault("artist", data.get("artist"))
30 | music_info.setdefault("album_name", data.get("album_name"))
31 | music_info.setdefault("mp3_url", data.get("mp3_url"))
32 | music_info.setdefault("playTime", data.get("playTime"))
33 | music_info.setdefault("quality", data.get("quality"))
34 | playlist.append(music_info)
35 | return playlist
36 |
37 | print(get_top_songlist())
38 | ```
39 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | NetEaseMusicApi
2 | ===============
3 |
4 | NetEase Music Api that comes from musicbox project. Ui-related parts are elimated, so as to be used at other places.
5 |
6 | **Install**
7 |
8 | .. code:: shell
9 |
10 | pip install MusicBoxApi
11 |
12 |
13 | **Example**
14 |
15 | .. code:: python
16 |
17 | from MusicBoxApi import api as NetEaseApi
18 |
19 | def get_top_songlist():
20 | netease = NetEaseApi.NetEase()
21 | music_list = netease.top_songlist()
22 | datalist = netease.dig_info(music_list, 'songs')
23 | playlist = []
24 | for data in datalist:
25 | music_info = {}
26 | music_info.setdefault("song_name", data.get("song_name"))
27 | music_info.setdefault("artist", data.get("artist"))
28 | music_info.setdefault("album_name", data.get("album_name"))
29 | music_info.setdefault("mp3_url", data.get("mp3_url"))
30 | music_info.setdefault("playTime", data.get("playTime"))
31 | music_info.setdefault("quality", data.get("quality"))
32 | playlist.append(music_info)
33 | return playlist
34 |
35 | print(get_top_songlist())
36 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal=1
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """ MusicBoxApi
2 |
3 | NetEase Music Api
4 |
5 | https://github.com/wzpan/MusicBoxApi
6 | """
7 |
8 | from setuptools import setup, find_packages
9 | from codecs import open
10 | from os import path
11 |
12 | here = path.abspath(path.dirname(__file__))
13 |
14 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
15 | long_description = f.read()
16 |
17 | setup(
18 | name='MusicBoxApi',
19 |
20 | version='1.0.9',
21 |
22 | description='NetEase Music Api',
23 | long_description=long_description,
24 |
25 | url='https://github.com/wzpan/MusicBoxApi',
26 |
27 | author='wzpan',
28 | author_email='m@hahack.com',
29 |
30 | license='MIT',
31 |
32 | classifiers=[
33 | 'Development Status :: 3 - Alpha',
34 |
35 | 'Intended Audience :: Developers',
36 | 'Topic :: Software Development :: Libraries :: Python Modules',
37 |
38 | 'License :: OSI Approved :: MIT License',
39 |
40 | 'Programming Language :: Python :: 2',
41 | 'Programming Language :: Python :: 2.6',
42 | 'Programming Language :: Python :: 2.7',
43 | 'Programming Language :: Python :: 3',
44 | 'Programming Language :: Python :: 3.3',
45 | 'Programming Language :: Python :: 3.4',
46 | 'Programming Language :: Python :: 3.5',
47 | ],
48 |
49 | keywords='netEase music api python',
50 |
51 | # You can just specify the packages manually here if your project is
52 | # simple. Or you can use find_packages().
53 | packages=['MusicBoxApi',],
54 |
55 | install_requires=['requests', 'BeautifulSoup4', 'pycrypto', 'future', 'crypto'],
56 |
57 | # List additional groups of dependencies here
58 | extras_require={},
59 | )
60 |
--------------------------------------------------------------------------------