├── .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 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.txt) 4 | [![versions](https://img.shields.io/pypi/v/MusicBoxApi.svg)](https://pypi.python.org/pypi/MusicBoxApi/) 5 | [![platform](https://img.shields.io/badge/python-2.7-green.svg)]() 6 | [![platform](https://img.shields.io/badge/python-3.5-green.svg)]() 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 | --------------------------------------------------------------------------------