fuo-qqmusic 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fuo-qqmusic might be problematic. Click here for more details.

fuo_qqmusic/api.py ADDED
@@ -0,0 +1,694 @@
1
+ #!/usr/bin/env python
2
+ # encoding: UTF-8
3
+
4
+ import base64
5
+ import hashlib
6
+ import logging
7
+ import math
8
+ import json
9
+ import random
10
+ import time
11
+
12
+ import requests
13
+ from .excs import QQIOError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ api_base_url = 'http://c.y.qq.com'
18
+
19
+
20
+ def djb2(string):
21
+ ''' Hash a word using the djb2 algorithm with the specified base. '''
22
+ h = 5381
23
+ for c in string:
24
+ h = ((h << 5) + h + ord(c)) & 0xffffffff
25
+ return str(2147483647 & h)
26
+
27
+
28
+ # reference from: https://blog.csdn.net/zq1391345114/article/details/113815906
29
+ def _get_sign(data):
30
+ # zza+一段随机的小写字符串,由小写字母和数字组成,长度为10-16位+CJBPACrRuNy7和data取md5。
31
+ st = 'abcdefghijklmnopqrstuvwxyz0123456789'
32
+ count = (math.floor(random.randint(10, 16)))
33
+ sign = 'zza'
34
+ for i in range(count):
35
+ sign += st[math.floor(random.randint(0, 35))]
36
+ s = 'CJBPACrRuNy7' + data
37
+ s_md5 = hashlib.md5(s.encode('utf-8')).hexdigest()
38
+ sign += s_md5
39
+ return sign
40
+
41
+
42
+ class CodeShouldBe0(QQIOError):
43
+ def __init__(self, data):
44
+ self._code = data['code']
45
+
46
+ def __str__(self):
47
+ return f'json code field should be 0, got {self._code}'
48
+
49
+ @classmethod
50
+ def check(cls, data):
51
+ if data['code'] != 0:
52
+ raise cls(data)
53
+
54
+
55
+ class API(object):
56
+ """qq music api
57
+
58
+ Please http capture request from (mobile) qqmusic mobile web page
59
+ """
60
+
61
+ def __init__(self, timeout=2):
62
+ # TODO: 暂时无脑统一一个 timeout
63
+ # 正确的应该是允许不同接口有不同的超时时间
64
+ self._timeout = timeout
65
+ self._headers = {
66
+ 'Accept': '*/*',
67
+ 'Accept-Encoding': 'gzip,deflate,sdch',
68
+ 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
69
+ 'Referer': 'http://y.qq.com/',
70
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0;'
71
+ ' Nexus 5 Build/MRA58N)'
72
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
73
+ 'Chrome/66.0.3359.181 Mobile Safari/537.36',
74
+ }
75
+ self.set_cookies(None)
76
+
77
+ def set_cookies(self, cookies):
78
+ """
79
+
80
+ :type cookies: dict
81
+ """
82
+ if cookies:
83
+ self._cookies = cookies
84
+ self._uin = self.get_uin_from_cookies(cookies)
85
+ self._guid = cookies.get('guid', str(int(random.random() * 1000000000)))
86
+ else:
87
+ self._cookies = None
88
+ self._uin = '0'
89
+ self._guid = str(int(random.random() * 1000000000)) # 暂时不知道 guid 有什么用
90
+
91
+ def get_uin_from_cookies(self, cookies):
92
+ if 'wxuin' in cookies:
93
+ # a sample wxuin: o1152921504803324670
94
+ # remove the 'o' prefix
95
+ wxuin = cookies['wxuin']
96
+ if wxuin.startswith('o'):
97
+ uin = wxuin[1:]
98
+ else:
99
+ uin = wxuin
100
+ else:
101
+ uin = cookies.get('uin')
102
+ return uin
103
+
104
+ def get_token_from_cookies(self):
105
+ cookies = self._cookies
106
+ if not cookies:
107
+ return 5381 # 不知道这个数字有木有特殊含义
108
+
109
+ # 不同客户端cookies返回的字段类型各有不同, 这里做一个折衷
110
+ string = cookies.get('qqmusic_key') or cookies.get('p_skey') or \
111
+ cookies.get('skey') or cookies.get('p_lskey') or \
112
+ cookies.get('lskey') or ''
113
+ return djb2(string)
114
+
115
+ def get_cover(self, mid, type_):
116
+ """获取专辑、歌手封面
117
+
118
+ :param type_: 专辑: 2,歌手:1
119
+ """
120
+ return 'http://y.gtimg.cn/music/photo_new/T00{}R800x800M000{}.jpg' \
121
+ .format(type_, mid)
122
+
123
+ def search(self, keyword, type_=0, limit=20, page=1):
124
+ # Other supported types: songlist, user, mv, qc, gedantip, zhida.
125
+ if type_ == 0:
126
+ key_ = 'song'
127
+ elif type_ == 1:
128
+ key_ = 'singer'
129
+ elif type_ == 2:
130
+ key_ = 'album'
131
+ elif type_ == 3:
132
+ key_ = 'songlist' # playlist
133
+ elif type_ == 4:
134
+ key_ = 'mv' # video
135
+ else:
136
+ raise QQIOError('invalid search type_:%d', type_)
137
+ payload = {
138
+ "search": {
139
+ "method": "DoSearchForQQMusicDesktop",
140
+ "module": "music.search.SearchCgiService",
141
+ "param": {
142
+ # People said that the max num_per_page is 30.
143
+ "num_per_page": max(limit, 30),
144
+ "page_num": page,
145
+ "search_type": type_,
146
+ "query": keyword,
147
+ }
148
+ }
149
+ }
150
+ js = self.rpc(payload)
151
+ result = js['search']['data']['body'][key_]['list']
152
+ return result
153
+
154
+ def search_playlists(self, query, limit=20, page=1):
155
+ raise QQIOError('search api is not available')
156
+
157
+ def song_detail(self, song_id):
158
+ uin = self._uin
159
+ song_id = int(song_id)
160
+ # 往 payload 添加字段,有可能还可以获取相似歌曲、歌单等
161
+ payload = {
162
+ 'comm': {
163
+ 'g_tk': 5381,
164
+ 'uin': uin,
165
+ 'format': 'json',
166
+ 'inCharset': 'utf-8',
167
+ 'outCharset': 'utf-8',
168
+ 'notice': 0,
169
+ 'platform': 'h5',
170
+ 'needNewCode': 1
171
+ },
172
+ 'detail': {
173
+ 'module': 'music.pf_song_detail_svr',
174
+ 'method': 'get_song_detail',
175
+ 'param': {'song_id': song_id}
176
+ }
177
+ }
178
+ js = self.rpc(payload)
179
+ data_song = js['detail']['data']['track_info']
180
+ if data_song['id'] <= 0:
181
+ return None
182
+ return data_song
183
+
184
+ def batch_song_details(self, song_ids):
185
+ """
186
+ song_ids should be a list of int
187
+ """
188
+ payload = {
189
+ 'comm': self.get_wkv17_common_params(),
190
+ 'req_0': {
191
+ 'module': 'music.trackInfo.UniformRuleCtrl',
192
+ 'method': 'CgiGetTrackInfo',
193
+ 'param': {
194
+ 'ids': song_ids,
195
+ 'types': [200]*len(song_ids),
196
+ 'source': 'AiNoFree',
197
+ }
198
+ }
199
+ }
200
+ js = self.rpc(payload)
201
+ return js['req_0']['data']['tracks']
202
+
203
+ def song_similar(self, song_id):
204
+ payload = {
205
+ "simsongs": {
206
+ "module": "rcmusic.similarSongRadioServer",
207
+ "method": "get_simsongs",
208
+ "param": {
209
+ "songid": song_id,
210
+ }
211
+ }
212
+ }
213
+ js = self.rpc(payload)
214
+ data_songs = js['simsongs']['data']['songInfoList']
215
+ return data_songs
216
+
217
+ def artist_detail(self, artist_mid):
218
+ payload = {
219
+ 'req_0': {
220
+ 'module': 'music.musichallSinger.SingerInfoInter',
221
+ 'method': 'GetSingerDetail',
222
+ 'param': {
223
+ 'singer_mids': [artist_mid],
224
+ 'pic': 1,
225
+ 'group_singer': 1,
226
+ 'wiki_singer': 1,
227
+ 'ex_singer': 1
228
+ }},
229
+ 'comm': {
230
+ 'g_tk': self.get_token_from_cookies(),
231
+ 'uin': self._uin,
232
+ 'format': 'json',
233
+ }
234
+ }
235
+ js = self.rpc(payload)
236
+ data = js['req_0']['data']['singer_list'][0]
237
+ data = {
238
+ 'singer_id': data['basic_info']['singer_id'],
239
+ 'singer_mid': data['basic_info']['singer_mid'],
240
+ 'singer_name': data['basic_info']['name'],
241
+ 'SingerDesc': data['ex_info']['desc'],
242
+ }
243
+ return data
244
+
245
+ def artist_songs(self, artist_id, page=1, page_size=50):
246
+ payload = {
247
+ 'req_0': {
248
+ 'module': 'music.musichallSong.SongListInter',
249
+ 'method': 'GetSingerSongList',
250
+ 'param': {
251
+ 'singerid': artist_id,
252
+ 'begin': (page - 1) * page_size,
253
+ 'num': page_size,
254
+ 'order': 1, # 热门/新,不带这个字段就是按歌曲新旧排序
255
+ # 有 newsong 字段时,服务端会返回含有 file 字段的字典
256
+ 'newsong': 1
257
+ }},
258
+ 'comm': {
259
+ 'g_tk': self.get_token_from_cookies(),
260
+ 'uin': self._uin,
261
+ 'format': 'json',
262
+ }
263
+ }
264
+ js = self.rpc(payload)
265
+ return js['req_0']['data']
266
+
267
+ def artist_albums(self, artist_id, page=1, page_size=20):
268
+ url = api_base_url + '/v8/fcg-bin/fcg_v8_singer_album.fcg'
269
+ params = {
270
+ 'singerid': artist_id,
271
+ 'order': 'time',
272
+ 'begin': (page - 1) * page_size, # TODO: 这里应该代表偏移量
273
+ 'num': page_size
274
+ }
275
+ response = requests.get(url, params=params)
276
+ js = response.json()
277
+ return js['data']
278
+
279
+ def album_detail(self, album_id):
280
+ url = api_base_url + '/v8/fcg-bin/fcg_v8_album_detail_cp.fcg'
281
+ params = {
282
+ 'albumid': album_id,
283
+ 'format': 'json',
284
+ 'newsong': 1
285
+ }
286
+ resp = requests.get(url, params=params)
287
+ return resp.json()['data']
288
+
289
+ def playlist_detail(self, pid, offset=0, limit=50):
290
+ url = api_base_url + '/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg'
291
+ params = {
292
+ 'type': '1',
293
+ 'utf8': '1',
294
+ 'disstid': pid,
295
+ 'format': 'json',
296
+ 'new_format': '1', # 需要这个字段来获取file等信息
297
+ 'song_begin': offset,
298
+ 'song_num': limit,
299
+ }
300
+ resp = requests.get(url, params=params, headers=self._headers,
301
+ cookies=self._cookies, timeout=self._timeout)
302
+ js = resp.json()
303
+ if js['code'] != 0:
304
+ raise CodeShouldBe0(js)
305
+ return js['cdlist'][0]
306
+
307
+ def user_detail(self, uid):
308
+ """
309
+ this API can be called only when user has logged in
310
+ """
311
+ url = api_base_url + '/rsc/fcgi-bin/fcg_get_profile_homepage.fcg'
312
+ params = {
313
+ # 这两个字段意义不明,不过至少固定为此值时可正常使用
314
+ 'cid': 205360838,
315
+ 'reqfrom': 1,
316
+ 'userid': uid
317
+ }
318
+ resp = requests.get(url, params=params, headers=self._headers,
319
+ cookies=self._cookies, timeout=self._timeout)
320
+ js = resp.json()
321
+ if js['code'] != 0:
322
+ raise CodeShouldBe0(js)
323
+ return js['data']
324
+
325
+ def user_favorite_artists(self, uid, mid, page=1, page_size=30):
326
+ # FIXME: page/page_size is just a guess
327
+ payload = {
328
+ 'req_0': {
329
+ 'module': 'music.concern.RelationList',
330
+ 'method': 'GetFollowSingerList',
331
+ 'param': {
332
+ 'From': page - 1,
333
+ 'Size': page_size,
334
+ 'HostUin': mid
335
+ }},
336
+ 'comm': {
337
+ "cv": 4747474,
338
+ "ct": 24,
339
+ 'g_tk': int(self.get_token_from_cookies()),
340
+ 'g_tk_new_20200303': int(self.get_token_from_cookies()),
341
+ 'uin': uid,
342
+ 'format': 'json',
343
+ 'notice': 0,
344
+ 'platform': 'yqq.json',
345
+ 'needNewCode': 1,
346
+ }
347
+ }
348
+ js = self.rpc(payload)
349
+ return js['req_1']['data']['List']
350
+
351
+ def user_favorite_albums(self, uid, start=0, end=100):
352
+ url = api_base_url + '/fav/fcgi-bin/fcg_get_profile_order_asset.fcg'
353
+ params = {
354
+ 'ct': 20, # 不知道此字段什么含义
355
+ 'reqtype': 2,
356
+ 'sin': start, # 每一页的开始
357
+ 'ein': end, # 每一页的结尾,目前假设最多收藏 30 个专辑
358
+ 'cid': 205360956,
359
+ 'reqfrom': 1,
360
+ 'userid': uid
361
+ }
362
+ resp = requests.get(url, params=params, headers=self._headers,
363
+ cookies=self._cookies, timeout=self._timeout)
364
+ js = resp.json()
365
+ if js['code'] != 0:
366
+ raise CodeShouldBe0(js)
367
+ return js['data']['albumlist']
368
+
369
+ def user_favorite_playlists(self, uid, mid, start=0, end=100):
370
+ url = api_base_url + '/fav/fcgi-bin/fcg_get_profile_order_asset.fcg'
371
+
372
+ params = {
373
+ 'loginUin': uid,
374
+ 'userid': mid,
375
+ 'cid': 205360956,
376
+ 'sin': start,
377
+ 'ein': end,
378
+ 'reqtype': 3,
379
+ 'ct': 20, # 没有该字段 返回中文字符是乱码
380
+ }
381
+
382
+ resp = requests.get(url, params=params, headers=self._headers,
383
+ cookies=self._cookies, timeout=self._timeout)
384
+ js = resp.json()
385
+ if js['code'] != 0:
386
+ raise CodeShouldBe0(js)
387
+ return js['data']['cdlist']
388
+
389
+ def recommend_playlists(self):
390
+ data = {
391
+ 'recomPlaylist': {
392
+ 'module': "playlist.HotRecommendServer",
393
+ 'method': "get_hot_recommend",
394
+ 'param': {
395
+ 'cmd': 2,
396
+ 'async': 1
397
+ }
398
+ },
399
+ }
400
+ js = self.rpc(data)
401
+ playlist = js['recomPlaylist']
402
+ return playlist['data']['v_hot']
403
+
404
+ def get_recommend_feed(self, page=1):
405
+ # APIs are found in https://y.qq.com/wk_v17/#/recommend
406
+ data = {
407
+ 'req_0': {
408
+ 'module': 'recommend.RecommendFeedServer',
409
+ 'method': 'get_recommend_feed',
410
+ 'param': {
411
+ 'direction': 0,
412
+ 'page': page,
413
+ 'v_cache': [],
414
+ 'v_uniq': [],
415
+ 's_num': 0
416
+ }
417
+ },
418
+ 'comm': self.get_wkv17_common_params(),
419
+ }
420
+ js = self.rpc(data)
421
+ return js['req_0']['data']
422
+
423
+ def get_lyric_by_songmid(self, songmid):
424
+ url = api_base_url + '/lyric/fcgi-bin/fcg_query_lyric_new.fcg'
425
+ params = {
426
+ 'songmid': songmid,
427
+ 'pcachetime': int(round(time.time() * 1000)),
428
+ 'format': 'json',
429
+ }
430
+ response = requests.get(url, params=params, headers=self._headers,
431
+ timeout=self._timeout)
432
+ js = response.json()
433
+ CodeShouldBe0.check(js)
434
+ lyric = js['lyric'] or ''
435
+ return base64.b64decode(lyric).decode()
436
+
437
+ def rpc(self, payload):
438
+ if 'comm' not in payload:
439
+ payload['comm'] = self.get_common_params()
440
+ data_str = json.dumps(payload, ensure_ascii=False)
441
+ params = {
442
+ '_': int(round(time.time() * 1000)),
443
+ 'sign': _get_sign(data_str),
444
+ 'data': data_str,
445
+ }
446
+ url = 'https://u.y.qq.com/cgi-bin/musicu.fcg'
447
+ resp = requests.get(url, params=params, headers=self._headers,
448
+ cookies=self._cookies, timeout=self._timeout)
449
+ js = resp.json()
450
+ CodeShouldBe0.check(js)
451
+ return js
452
+
453
+ def get_wkv17_common_params(self):
454
+ return {
455
+ # ct field is important, without this field,
456
+ # the req_0 result is completely different.
457
+ "ct": 20,
458
+ "cv": 1770,
459
+ 'g_tk': 5381,
460
+ 'uin': self._uin,
461
+ 'format': 'json',
462
+ 'inCharset': 'utf-8',
463
+ 'outCharset': 'utf-8',
464
+ 'platform': 'wk_v17',
465
+ 'uid': '',
466
+ 'guid': '',
467
+ }
468
+
469
+ def get_common_params(self):
470
+ return {
471
+ 'loginUin': self._uin,
472
+ 'hostUin': 0,
473
+ 'g_tk': self.get_token_from_cookies(),
474
+ 'inCharset': 'utf8',
475
+ 'outCharset': 'utf-8',
476
+ 'notice': 0,
477
+ 'platform': 'yqq',
478
+ 'needNewCode': 0,
479
+ }
480
+
481
+ def get_mv(self, vid):
482
+ payload = {
483
+ 'getMvUrl': {
484
+ 'module': "gosrf.Stream.MvUrlProxy",
485
+ 'method': "GetMvUrls",
486
+ 'param': {
487
+ "vids": [vid],
488
+ 'request_typet': 10001
489
+ }
490
+ }
491
+ }
492
+ js = self.rpc(payload)
493
+ return js['getMvUrl']['data'][vid]
494
+
495
+ def get_radio_music(self):
496
+ payload = {
497
+ 'songlist': {
498
+ 'module': "mb_track_radio_svr",
499
+ 'method': "get_radio_track",
500
+ 'param': {
501
+ 'id': 99,
502
+ 'firstplay': 1,
503
+ 'num': 15
504
+ },
505
+ },
506
+ 'radiolist': {
507
+ 'module': "pf.radiosvr",
508
+ 'method': "GetRadiolist",
509
+ 'param': {
510
+ 'ct': "24"
511
+ },
512
+ },
513
+ 'comm': {
514
+ 'ct': 24,
515
+ 'cv': 0
516
+ },
517
+ }
518
+ js = self.rpc(payload)
519
+ return js['songlist']['data']['tracks']
520
+
521
+ def get_song_url(self, song_mid):
522
+ uin = self._uin
523
+ songvkey = str(random.random()).replace("0.", "")
524
+ guid = self._guid
525
+ # filename = f'C400{song_mid}.m4a'
526
+ data = {
527
+ "req": {
528
+ "module": "CDN.SrfCdnDispatchServer",
529
+ "method": "GetCdnDispatch",
530
+ "param": {
531
+ "guid": guid,
532
+ "calltype": 0,
533
+ "userip": ""
534
+ }
535
+ },
536
+ "req_0": {
537
+ "module": "vkey.GetVkeyServer",
538
+ "method": "CgiGetVkey",
539
+ "param": {
540
+ "cid": 205361747,
541
+ "guid": guid,
542
+ "songmid": [song_mid],
543
+ # "filename": [filename],
544
+ "songtype": [1],
545
+ "uin": str(uin), # NOTE: must be a string
546
+ # "loginflag": 1,
547
+ # "platform": "20"
548
+ }
549
+ },
550
+ "comm": {
551
+ "uin": uin,
552
+ "format": "json",
553
+ "ct": 24,
554
+ "cv": 0
555
+ }
556
+ }
557
+ data_str = json.dumps(data, ensure_ascii=False)
558
+ params = {
559
+ '-': 'getplaysongvkey' + str(songvkey),
560
+ 'g_tk': 5381,
561
+ 'loginUin': uin,
562
+ 'hostUin': 0,
563
+ 'format': 'json',
564
+ 'inCharset': 'utf8',
565
+ 'outCharset': 'utf8',
566
+ 'notice': 0,
567
+ 'platform': 'yqq.json',
568
+ 'needNewCode': 0,
569
+ }
570
+ # 这里没有把 data=data_str 放在 params 中,因为 QQ 服务端不识别这种写法
571
+ # 另外测试发现:python(flask) 是可以识别这两种写法的
572
+ url = 'http://u.y.qq.com/cgi-bin/musicu.fcg?data=' + data_str
573
+ # 如果是绿钻会员,这里带上 cookies,就能请求到收费歌曲的 url
574
+ resp = requests.get(url, params=params,
575
+ headers=self._headers, cookies=self._cookies)
576
+ js = resp.json()
577
+ midurlinfo = js['req_0'].get('data', {}).get('midurlinfo')
578
+ if midurlinfo:
579
+ purl = midurlinfo[0]['purl']
580
+ prefix = 'http://dl.stream.qqmusic.qq.com/'
581
+ prefix = 'http://mobileoc.music.tc.qq.com/'
582
+
583
+ # 经过个人(cosven)测试,无论是普通还是绿钻用户,下面几个都会失败
584
+ quality_suffix = [
585
+ # ('sq', 'M500', 'mp3'),
586
+ # ('shq', 'F000', 'flac'),
587
+ # ('hq', 'M800', 'mp3'),
588
+ # ('shq', 'A000', 'ape'),
589
+ ]
590
+ C400_filename = midurlinfo[0]['filename']
591
+ pure_filename = C400_filename[4:-3]
592
+
593
+ req_data = js['req']['data']
594
+ testfilewifi = req_data.get('testfilewifi', '')
595
+ vkey = req_data['vkey']
596
+
597
+ # 抓客户端的包,发现 guid/uin/vkey 三个参数配上对非常重要。
598
+ # 尝试了客户端的 cookie 拷贝过来,还是请求不到无损音乐。
599
+ if testfilewifi:
600
+ params_str = testfilewifi.split('?')[1]
601
+ else:
602
+ params_str = f'vkey={vkey}&guid=MS&uin=0&fromtag=8'
603
+
604
+ # 如果前两个音质都不行,我们认为后面的音乐也都不可以,
605
+ # 目前通过这样简单的策略来节省请求次数
606
+ max_try_count = 2
607
+ failed_try_count = 0
608
+ valid_urls = {}
609
+ for quality, q, s in quality_suffix:
610
+ if quality in valid_urls:
611
+ continue
612
+ q_filename = q + pure_filename + s
613
+ # 通过抓客户端接口可以发现,这个 uin 和用户 uin 不是一个东西
614
+ # 这个 uin 似乎只有三位数
615
+ url = f'{prefix}{q_filename}?{params_str}'
616
+ print(url)
617
+ _resp = requests.head(url, headers=self._headers, cookies=self._cookies)
618
+ if _resp.status_code == 200:
619
+ valid_urls[quality] = url
620
+ logger.info(f'song:{song_mid} quality:{q} url is valid')
621
+ continue
622
+ logger.info(f'song:{song_mid} quality:{q} url is invalid')
623
+ failed_try_count += 1
624
+ if failed_try_count >= max_try_count:
625
+ break
626
+ # 尝试拿到网页版接口的 url
627
+ if not valid_urls and purl:
628
+ song_path = purl
629
+ url = prefix + song_path
630
+ valid_urls['lq'] = url
631
+ logger.info(f'song:{song_mid} quality:web url is valid')
632
+ return valid_urls
633
+ return {}
634
+
635
+ def get_song_url_v2(self, song_mid, media_id, quality):
636
+ switcher = {
637
+ 'F000': 'flac',
638
+ 'A000': 'ape',
639
+ 'M800': 'mp3',
640
+ 'C600': 'm4a',
641
+ 'M500': 'mp3'
642
+ }
643
+
644
+ uin = self._uin
645
+ guid = self._guid
646
+ filename = '{}{}.{}'.format(quality, media_id, switcher.get(quality))
647
+ data = {
648
+ "req_0": {
649
+ "module": "vkey.GetVkeyServer",
650
+ "method": "CgiGetVkey",
651
+ "param": {
652
+ "filename": [filename],
653
+ "guid": guid,
654
+ "songmid": [song_mid],
655
+ "songtype": [0],
656
+ "uin": str(uin), # NOTE: must be a string
657
+ "loginflag": 1,
658
+ "platform": "20"
659
+ }
660
+ },
661
+ "comm": {
662
+ "uin": str(uin),
663
+ "format": "json",
664
+ "ct": 19,
665
+ "cv": 0
666
+ }
667
+ }
668
+ data_str = json.dumps(data, ensure_ascii=False)
669
+
670
+ sign = _get_sign(data_str)
671
+ params = {
672
+ 'sign': sign,
673
+ 'g_tk': 5381,
674
+ 'loginUin': '',
675
+ 'hostUin': 0,
676
+ 'format': 'json',
677
+ 'inCharset': 'utf8',
678
+ 'outCharset': 'utf-8¬ice=0',
679
+ 'platform': 'yqq.json',
680
+ 'needNewCode': 0,
681
+ 'data': data_str
682
+ }
683
+ url = 'https://u.y.qq.com/cgi-bin/musicu.fcg'
684
+ # TODO: 似乎存在一种有效时间更长的cookies, https://github.com/PeterDing/chord
685
+ resp = requests.get(url, params=params,
686
+ headers=self._headers, cookies=self._cookies)
687
+ js = resp.json()
688
+ midurlinfo = js['req_0'].get('data', {}).get('midurlinfo')
689
+ if midurlinfo and midurlinfo[0]['purl']:
690
+ return 'http://isure.stream.qqmusic.qq.com/{}'.format(midurlinfo[0]['purl'])
691
+ return ''
692
+
693
+
694
+ api = API()