QQ Music: Search and Direct Link Export

This article will explain the search mechanism of QQ Music and the mechanism for parsing direct links to lossless audio.

目前解析功能已经不可用,搜索功能正常

项目地址:🚀GitHub

1.获取数据

1
2
3
keyword = '李健'
search_api = 'http://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.center&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=1&n=50&w=' + keyword + '&jsonpCallback=searchCallbacksong2020&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0'
response = requests.get(search_api).text

搜索数据

即歌曲就在json.loads(response)['data']['song']['list']

2.分析数据

首先我们来看一个解析后的url:

1
'http://streamoc.music.tc.qq.com/M800000PLHrM2luXiz.mp3?vkey=CA23BC73FA35144644BAD2C8E3026425F724AD8AA117AD079FB5812177575B59044EF642A3B422F3A9E7FEFF185839FC163444AD015BB875&guid=DreamWalkerXZ&uin=123456&fromtag=8'

然后把它的参数给分离出来

1
'http://streamoc.music.tc.qq.com/' + prefix + mid + extension + '?vkey=' + vkey + '&guid=' + guid + '&uin=' + uin + '&fromtag=8'

prefix与extension

prefix是音频格式的前缀, 与extension相对应

qualityprefixextension
dtsD00A.flac
apeA000.ape
flacF000.flac
320M800.mp3
aacC600.m4a
oggO600.ogg
128M500.mp3

至于如何判断是否有此品质,只需要在第一步的搜索结果中的json.loads(response)['data']['song']['list'][歌曲位置]['file']中判断size_128, size_320, size_aac, size_ape, size_dts, size_flac, size_ogg这几个项的值是否为0即可(为0则代表无此品质)

mid

mid是QQ音乐中每一首歌曲的唯一标识符

mid即json.loads(response)['data']['song']['list'][歌曲位置]['mid']的值

uin与guid

guid和uin随意填充即可, 但是要与后面生成vkey时所填的一致

vkey

vkey是一个通过算法生成的具有时效性的字符串

生成算法如下:

1
2
3
vkey = json.loads(requests.get('http://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg?g_tk=0&loginUin=' + uin + '&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0&cid=205361747&uin=' + uin + '&songmid=003a1tne1nSz1Y&filename=C400003a1tne1nSz1Y.m4a&guid=' + guid).text)['data']['items'][0]['vkey']
# 1.注意uin与guid在生成vkey和拼接直链时要一致
# 2.如果服务器在国外,请使用国内的HTTP代理来访问此接口,否则会因为版权问题无法解析

成品

QQ.py (类)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import json
import requests


class QQ():
    def search(self, song_name):
        search_url = 'http://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.center&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=1&n=50&w=' + song_name + '&jsonpCallback=searchCallbacksong2020&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0'
        search_result = []
        for item in json.loads(requests.get(search_url).text)['data']['song']['list']:
            name = item['name']
            singer = ''.join(singer['name'] + '+' for singer in item['singer']).rstrip('+')
            album = item['album']['name']
            media_mid = item['file']['media_mid']
            types = []
            if item['file']['size_128'] != 0:
                types.append('128')
            if item['file']['size_320'] != 0:
                types.append('320')
            if item['file']['size_aac'] != 0:
                types.append('aac')
            if item['file']['size_ape'] != 0:
                types.append('ape')
            if item['file']['size_dts'] != 0:
                types.append('dts')
            if item['file']['size_flac'] != 0:
                types.append('flac')
            if item['file']['size_ogg'] != 0:
                types.append('ogg')
            search_result.append({'name': name, 'singer': singer, 'album': album, 'media_mid': media_mid, 'types': types})
        return search_result

    def get_audio_url(self, media_mid, type):
        if type == 'dts':
            type = {'prefix': 'D00A', 'extension': '.flac'}
        elif type == 'ape':
            type = {'prefix': 'A000', 'extension': '.ape'}
        elif type == 'flac':
            type = {'prefix': 'F000', 'extension': '.flac'}
        elif type == '320':
            type = {'prefix': 'M800', 'extension': '.mp3'}
        elif type == 'aac':
            type = {'prefix': 'C600', 'extension': '.m4a'}
        elif type == 'ogg':
            type = {'prefix': 'O600', 'extension': '.ogg'}
        elif type == '128':
            type = {'prefix': 'M500', 'extension': '.mp3'}
        else:
            raise Exception("Invalid type", type)
        uin = '123456'
        guid = 'DreamWalkerXZ'
        vkey = json.loads(requests.get('http://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg?g_tk=0&loginUin=' + uin + '&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0&cid=205361747&uin=' + uin + '&songmid=003a1tne1nSz1Y&filename=C400003a1tne1nSz1Y.m4a&guid=' + guid).text)['data']['items'][0]['vkey']
        return 'http://streamoc.music.tc.qq.com/' + type['prefix'] + media_mid + type['extension'] + '?vkey=' + vkey + '&guid=' + guid + '&uin=' + uin + '&fromtag=8'

调用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import QQ


qq = QQ.QQ()


# 搜索
search_result = qq.search('李健')
print(search_result)
"""
[{'name': '贝加尔湖畔',
  'singer': '李健',
  'album': '依然',
  'media_mid': '000PLHrM2luXiz',
  'types': ['128', '320', 'aac', 'ape', 'flac', 'ogg']},
 {'name': '假如爱有天意',
  'singer': '李健',
  'album': '李健',
  'media_mid': '003u0BdF1aocDJ',
  'types': ['128', '320', 'aac', 'flac', 'ogg']},
 ...中间省略...
 {'name': '日落之前',
  'singer': '李健',
  'album': '李健',
  'media_mid': '0047DwLA1u9FEx',
  'types': ['128', '320', 'aac', 'ape', 'dts', 'flac', 'ogg']}]
"""

# 解析
audio_url = qq.get_audio_url('001Liwq92gKerW', '320')
print(audio_url)
"""
http://streamoc.music.tc.qq.com/M800001Liwq92gKerW.mp3?vkey=17E517E90215EB25F6DD717CE479C6E9573EB9505EA633F4909A32725B490ACDF5A934BB8674363A90C8C076104C2412E6FCADCF58FB5DC0&guid=DreamWalkerXZ&uin=123456&fromtag=8
"""
Built with Hugo
Theme Stack designed by Jimmy