├── README.md └── music163.py /README.md: -------------------------------------------------------------------------------- 1 | # 163-Cloud-Music-Unlock 2 | XX网易云音乐版权以及地域限制 3 | 4 | ## 公共测试服务器 5 | 202.168.153.244 (月妹子的大棒子国服务器 6 | 7 | ## For OpenWRT or 其他借助DNS实现的Unblocker用户 8 | 当然你也可以尝试把整个server部署到路由器里面,需要python环境... 9 | ``` 10 | 配置添加 address=/music.163.com/[serverip] 11 | ``` 12 | ## For Android & IOS 越狱用户 13 | ``` 14 | hosts添加 [serverip] music.163.com 15 | ``` 16 | ## For Android 未越狱越狱用户 17 | ``` 18 | 走 shadowsocks-android 19 | 服务器hosts添加 [serverip] music.163.com 20 | ``` 21 | ## For IOS 未越狱越狱用户 22 | ``` 23 | *需要Surge 24 | 规则中增加Local DNS Map [serverip] music.163.com 25 | ``` 26 | 27 | ## 以server方式部署 28 | 安装Python环境并运行 29 | ``` 30 | sudo apt-get install python-dev python-pip 31 | sudo pip install tornado 32 | python music163.py -m server -p 16163 -a 127.0.0.1 33 | ``` 34 | Nginx代理服务器参考配置 35 | ``` 36 | server { 37 | listen 0.0.0.0:80; 38 | server_name music.163.com; 39 | access_log /var/log/nginx/music.163.log; 40 | 41 | location / { 42 | proxy_set_header X-Real-IP $remote_addr; 43 | proxy_set_header HOST $http_host; 44 | proxy_set_header X-NginX-Proxy true; 45 | 46 | proxy_pass http://127.0.0.1:16163; 47 | proxy_redirect off; 48 | } 49 | } 50 | 51 | ``` 52 | 53 | ## 以proxy方式部署 54 | 在Winodows客户端中可以设置代理服务器,指向脚本基本,脚本可在本地运行 55 | -------------------------------------------------------------------------------- /music163.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import socket 3 | import base64 4 | import tornado.httpserver 5 | import tornado.ioloop 6 | import tornado.iostream 7 | import tornado.web 8 | import tornado.httpclient 9 | import hashlib 10 | import json 11 | import re 12 | import logging 13 | import traceback 14 | from optparse import OptionParser 15 | 16 | 17 | HOST_MODE = True 18 | 19 | logger = logging.getLogger('') 20 | __all__ = ['ProxyHandler', 'run_proxy'] 21 | counter = 0 22 | 23 | class StatHandler(tornado.web.RequestHandler): 24 | 25 | def get(self): 26 | self.write('Until startup process:%s' % counter) 27 | self.finish() 28 | 29 | class ProxyHandler(tornado.web.RequestHandler): 30 | SUPPORTED_METHODS = ['GET', 'POST', 'CONNECT'] 31 | 32 | re_url = re.compile('/eapi/(v3/song/detail/|v1/album/|v3/playlist/detail|batch|cloudsearch/pc|v1/artist|v1/search/get)') 33 | 34 | @tornado.web.asynchronous 35 | @tornado.gen.coroutine 36 | def get(self): 37 | 38 | global counter 39 | counter += 1 40 | 41 | def encrypted_id(id): 42 | magic = bytearray('3go8&$8*3*3h0k(2)2') 43 | song_id = bytearray(str(id)) 44 | magic_len = len(magic) 45 | for i in xrange(len(song_id)): 46 | song_id[i] = song_id[i] ^ magic[i % magic_len] 47 | m = hashlib.md5(song_id) 48 | result = m.digest().encode('base64')[:-1] 49 | result = result.replace('/', '_') 50 | result = result.replace('+', '-') 51 | return result 52 | 53 | def gen_mp3url(dfsId): 54 | e = encrypted_id(dfsId) 55 | return 'http://m2.music.126.net/%s/%s.mp3' % (e, dfsId) 56 | 57 | def choose_br(song, USE_H_BITRATE=False): 58 | if 'mp3Url' in song and not USE_H_BITRATE: 59 | mp3url = song['mp3Url'] 60 | if 'lMusic' in song: 61 | br = song['lMusic']['bitrate'] 62 | else: 63 | br = 96000 64 | else: 65 | if 'hMusic' in song: 66 | dfsId = song['hMusic']['dfsId'] 67 | br = song['hMusic']['bitrate'] 68 | elif 'mMusic' in song: 69 | dfsId = song['mMusic']['dfsId'] 70 | br = song['mMusic']['bitrate'] 71 | elif 'lMusic' in song: 72 | dfsId = song['lMusic']['dfsId'] 73 | br = song['lMusic']['bitrate'] 74 | elif 'bMusic' in song: 75 | dfsId = song['bMusic']['dfsId'] 76 | br = song['bMusic']['bitrate'] 77 | mp3url = gen_mp3url(dfsId) 78 | return br, mp3url 79 | 80 | def eapi_batch(response_body): 81 | rereobj = re.compile('"st":-?\d+') 82 | response_body, _ = rereobj.subn('"st":0', response_body) 83 | rereobj = re.compile('"subp":\d+') 84 | response_body, _ = rereobj.subn('"subp":1', response_body) 85 | rereobj = re.compile('"dl":0') 86 | response_body, _ = rereobj.subn('"dl":320000', response_body) 87 | rereobj = re.compile('"pl":0') 88 | response_body, _ = rereobj.subn('"pl":320000', response_body) 89 | return response_body 90 | 91 | def eapi_song_download_limit(): 92 | return "{\"overflow\":false,\"code\":200}" 93 | 94 | url = self.request.uri 95 | if HOST_MODE: 96 | url = 'http://music.163.com' + url 97 | 98 | req = tornado.httpclient.HTTPRequest(url=url, 99 | method=self.request.method, body=self.request.body, 100 | headers=self.request.headers, follow_redirects=False, 101 | allow_nonstandard_methods=True) 102 | 103 | client = tornado.httpclient.AsyncHTTPClient() 104 | try: 105 | response = yield client.fetch(req) 106 | print self.request.uri 107 | response_body = response.body 108 | if ProxyHandler.re_url.search(self.request.uri): 109 | response_body = eapi_batch(response_body) 110 | elif '/eapi/song/enhance/download/url' in self.request.uri: 111 | j = json.loads(response_body) 112 | if 'br' in j['data'] and j['data']['br'] <= 128000: 113 | try: 114 | sid = j['data']['id'] 115 | client = tornado.httpclient.AsyncHTTPClient() 116 | url = 'http://music.163.com/api/song/detail?id=%s&ids=[%s]' % (sid, sid) 117 | response_ = yield client.fetch(url) 118 | j = json.loads(response_.body) 119 | br, mp3url = choose_br(j['songs'][0], USE_H_BITRATE=True) 120 | client = tornado.httpclient.AsyncHTTPClient() 121 | response_ = yield client.fetch(mp3url, method='HEAD') 122 | size = int(response_.headers['Content-Length']) 123 | j = json.loads(response_body) 124 | j['data']['br'] = br 125 | j['data']['url'] = mp3url 126 | del j['data']['md5'] 127 | j['data']['size'] = size 128 | response_body = json.dumps(j) 129 | except: 130 | traceback.print_exc() 131 | elif '/eapi/song/enhance/player/url' in self.request.uri and '"url":null' in response_body: 132 | # get mp3 url 133 | re_id = re.compile('"id":(\d+)') 134 | sid = re_id.search(response_body).group(1) 135 | client = tornado.httpclient.AsyncHTTPClient() 136 | url = 'http://music.163.com/api/song/detail?id=%s&ids=[%s]' % (sid, sid) 137 | response_ = yield client.fetch(url) 138 | j = json.loads(response_.body) 139 | br, mp3url = choose_br(j['songs'][0], USE_H_BITRATE=False) 140 | j = json.loads(response_body) 141 | j['data'][0]['url'] = mp3url 142 | j['data'][0]['br'] = br 143 | j['data'][0]['code'] = 200 144 | #del j['data']['md5'] 145 | response_body = json.dumps(j) 146 | 147 | elif '/eapi/song/download/limit' in response.body: 148 | response_body = eapi_song_download_limit() 149 | #print response_body[:5000] 150 | 151 | if response.error and not isinstance(response.error, 152 | tornado.httpclient.HTTPError): 153 | self.set_status(500) 154 | self.write('Internal server error:\n' + str(response.error)) 155 | self.finish() 156 | else: 157 | self.set_status(response.code) 158 | for header in ('Date', 'Cache-Control', 'Server', 159 | 'Content-Type', 'Location'): 160 | v = response.headers.get(header) 161 | if v: 162 | self.set_header(header, v) 163 | if response.body: 164 | self.write(response_body) 165 | self.finish() 166 | except tornado.httpclient.HTTPError as e: 167 | if hasattr(e, 'response') and e.response: 168 | #self.handle_response(e.response) 169 | if 300 <= e.response.code <= 399: 170 | self.set_status(e.response.code) 171 | if e.response.body: 172 | self.write(e.response.body) 173 | self.finish() 174 | else: 175 | traceback.print_exc() 176 | else: 177 | self.set_status(500) 178 | self.write('Internal server error:\n' + str(e)) 179 | self.finish() 180 | 181 | @tornado.web.asynchronous 182 | def post(self): 183 | return self.get() 184 | 185 | @tornado.web.asynchronous 186 | def connect(self): 187 | host, port = self.request.uri.split(':') 188 | client = self.request.connection.stream 189 | 190 | def read_from_client(data): 191 | upstream.write(data) 192 | 193 | def read_from_upstream(data): 194 | client.write(data) 195 | 196 | def client_close(data=None): 197 | if upstream.closed(): 198 | return 199 | if data: 200 | upstream.write(data) 201 | upstream.close() 202 | 203 | def upstream_close(data=None): 204 | if client.closed(): 205 | return 206 | if data: 207 | client.write(data) 208 | client.close() 209 | 210 | def start_tunnel(): 211 | client.read_until_close(client_close, read_from_client) 212 | upstream.read_until_close(upstream_close, read_from_upstream) 213 | client.write(b'HTTP/1.0 200 Connection established\r\n\r\n') 214 | 215 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 216 | upstream = tornado.iostream.IOStream(s) 217 | upstream.connect((host, int(port)), start_tunnel) 218 | 219 | 220 | def run_proxy(eth, start_ioloop=True): 221 | """ 222 | Run proxy on the specified port. If start_ioloop is True (default), 223 | the tornado IOLoop will be started immediately. 224 | """ 225 | app = tornado.web.Application([ 226 | ('/stat', StatHandler), 227 | (r'\S+', ProxyHandler), 228 | ]) 229 | app.listen(port=eth[1], address=eth[0]) 230 | ioloop = tornado.ioloop.IOLoop.instance() 231 | if start_ioloop: 232 | ioloop.start() 233 | 234 | if __name__ == '__main__': 235 | parser = OptionParser(usage=u'usage: %prog [options]') 236 | parser.add_option('-p', '--port', dest='port', action='store', type='int', default=16163, 237 | help='Listening port') 238 | parser.add_option('-a', '--addr', dest='addr', action='store', 239 | metavar='addr',default='127.0.0.1', 240 | help='Bind address') 241 | parser.add_option('-m', '--mode', dest='mode', action='store', type='string', default='proxy', 242 | help='Work mode [server] or [proxy]') 243 | (options,args) = parser.parse_args() 244 | 245 | if options.mode == 'proxy': 246 | HOST_MODE = False 247 | elif options.mode == 'server': 248 | HOST_MODE = True 249 | else: 250 | logger.error('error mode') 251 | exit(1) 252 | eth = (options.addr, options.port) 253 | print ("Starting HTTP %s on port %s" % (options.mode, str(eth))) 254 | run_proxy(eth) 255 | --------------------------------------------------------------------------------