# coding=utf-8 ''' netease music provider. ''' import base64 import hashlib import json import logging import os.path import urllib import urllib2 import pyaes from replay import h logger = logging.getLogger('listenone.' + __name__) def filetype(): return '.mp3' # 歌曲加密算法, 基于https://github.com/yanunon/NeteaseCloudMusic脚本实现 def _encrypted_id(id): magic = bytearray('3go8&$8*3*3h0k(2)2') song_id = bytearray(id) magic_len = len(magic) for i in xrange(len(song_id)): song_id[i] = song_id[i] ^ magic[i % magic_len] m = hashlib.md5(song_id) result = m.digest().encode('base64')[:-1] result = result.replace('/', '_') result = result.replace('+', '-') return result modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b72' + \ '5152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbd' + \ 'a92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe48' + \ '75d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' nonce = '0CoJUm6Qyw8W8jud' pubKey = '010001' def _create_secret_key(size): randlist = map(lambda xx: (hex(ord(xx))[2:]), os.urandom(size)) return (''.join(randlist))[0:16] def _aes_encrypt(text, sec_key): pad = 16 - len(text) % 16 text = text + pad * chr(pad) aes = pyaes.AESModeOfOperationCBC(sec_key, iv='0102030405060708') ciphertext = '' while text != '': ciphertext += aes.encrypt(text[:16]) text = text[16:] ciphertext = base64.b64encode(ciphertext) return ciphertext def _rsa_encrypt(text, pub_key, modulus): text = text[::-1] rs = int(text.encode('hex'), 16) ** int(pubKey, 16) % int(modulus, 16) return format(rs, 'x').zfill(256) def _encrypted_request(text): text = json.dumps(text) sec_key = _create_secret_key(16) enc_text = _aes_encrypt(_aes_encrypt(text, nonce), sec_key) enc_sec_key = _rsa_encrypt(sec_key, pubKey, modulus) data = { 'params': enc_text, 'encSecKey': enc_sec_key } return data # 参考 https://github.com/darknessomi/musicbox # 歌单(网友精选碟) hot||new http://music.163.com/#/discover/playlist/ def _top_playlists(category=u'全部', order='hot', offset=0, limit=60): category = urllib2.quote(category.encode("utf8")) action = 'http://music.163.com/api/playlist/list?cat=' + category + \ '&order=' + order + '&offset=' + str(offset) + \ '&total=' + ('true' if offset else 'false') + '&limit=' + str(limit) data = json.loads(_ne_h(action)) return data['playlists'] def _ne_h(url, v=None): # http request extra_headers = { 'Accept': '*/*', 'Accept-Encoding': 'gzip,deflate,sdch', 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded', 'Host': 'music.163.com', 'Referer': 'http://music.163.com/search/', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2)' + ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome' + '/33.0.1750.152 Safari/537.36' } return h(url, v=v, extra_headers=extra_headers) def _gen_url_params(d): for k, v in d.iteritems(): d[k] = unicode(v).encode('utf-8') return urllib.urlencode(d) def _convert_song(song): d = { 'id': 'netrack_' + str(song['id']), 'title': song['name'], 'artist': song['artists'][0]['name'], 'artist_id': 'neartist_' + str(song['artists'][0]['id']), 'album': song['album']['name'], 'album_id': 'nealbum_' + str(song['album']['id']), 'source': 'netease', 'source_url': 'http://music.163.com/#/song?id=' + str(song['id']), } if 'picUrl' in song['album']: d['img_url'] = song['album']['picUrl'] else: d['img_url'] = '' params = _gen_url_params(d) d['url'] = '/track_file?' + params return d # -------------standard interface part------------------ # https://github.com/darknessomi/musicbox/wiki/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%E6%96%B0%E7%89%88WebAPI%E5%88%86%E6%9E%90%E3%80%82 def get_url_by_id(song_id): csrf = '' d = { "ids": [song_id], "br": 12800, "csrf_token": csrf } url = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token=' request = _encrypted_request(d) response = json.loads(_ne_h(url, request)) return response['data'][0]['url'] def list_playlist(): result = [] for l in _top_playlists(): d = dict( cover_img_url=l['coverImgUrl'], title=l['name'], play_count=l['playCount'], list_id='neplaylist_' + str(l['id']),) result.append(d) return result def get_playlist(playlist_id): url = 'http://music.163.com/api/playlist/detail?id=' + playlist_id data = json.loads(_ne_h(url)) info = dict( cover_img_url=data['result']['coverImgUrl'], title=data['result']['name'], id='neplaylist_' + playlist_id) result = [] for song in data['result']['tracks']: if song['status'] == -1: continue result.append(_convert_song(song)) return dict(tracks=result, info=info) def get_artist(artist_id): url = 'http://music.163.com/api/artist/' + str(artist_id) data = json.loads(_ne_h(url)) info = dict( cover_img_url=data['artist']['picUrl'], title=data['artist']['name'], id='neartist_' + artist_id) result = [] for song in data['hotSongs']: if song['status'] == -1: continue result.append(_convert_song(song)) return dict(tracks=result, info=info) def get_artist_albums(artist_id): url = 'http://music.163.com/api/artist/albums/' + str(artist_id) + \ '?offset=0&limit=50' try: data = json.loads(_ne_h(url)) return data['hotAlbums'] except: return [] def get_album(album_id): url = 'http://music.163.com/api/album/%s/' % album_id data = json.loads(_ne_h(url)) info = dict( cover_img_url=data['album']['picUrl'], title=data['album']['name'], id='nealbum_' + str(album_id)) result = [] for song in data['album']['songs']: if song['status'] == -1: continue result.append(_convert_song(song)) return dict(tracks=result, info=info) def search_track(keyword): # return matched qq music songs keyword = keyword.encode("utf8") search_url = 'http://music.163.com/api/search/get' stype = 1 offset = 0 total = 'true' data = { 's': keyword, 'type': stype, 'offset': offset, 'total': total, 'limit': 60 } jc = _ne_h(search_url, data) result = [] for song in json.loads(jc)["result"]["songs"]: if song['status'] == -1: continue result.append(_convert_song(song)) return result