判断网页端QQ音乐及网易云音乐歌曲能否播放

随国内版权意识逐渐增强,各大音乐平台已要求付费下载歌曲,但大部分歌曲可以在线播放,播放时会把歌曲的相关信息获取到本地。网上已有很多相关 API 的分析及根据 API 写好的工具,本文主要对网页端判断歌曲能否播放的代码进行了简单分析,仅作学习研究使用。

QQ音乐

点击播放按钮,弹出新标签页,查看网络请求,发现:

可以验证这就是 m4a 格式的歌曲文件。

歌词

继续查看之前的几个请求,可以发现获取歌词的请求 https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg,返回的歌词经过了 Base64 编码:

URL

获取 URL 的请求是 https://u.y.qq.com/cgi-bin/musicu.fcg

GET 请求需要 data 参数,经过搜索,在此处断点:

相关参数如下:

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
function _getGuid() {
if (_guid.length > 0)
return _guid;
var e = MUSIC.cookie.get("pgv_pvid");
if (e && e.length > 0)
return _guid = e;
var t = (new Date).getUTCMilliseconds();
return _guid = Math.round(2147483647 * Math.random()) * t % 1e10,
document.cookie = "pgv_pvid=" + _guid + "; Expires=Sun, 18 Jan 2038 00:00:00 GMT; PATH=/; DOMAIN=qq.com;",
_guid
}

var i = {
guid: "" + _getGuid(),
songmid: r,
songtype: n,
uin: g_user.getUin() + "",
loginflag: 1,
platform: "20"
};

var e = {
req: {
module: "CDN.SrfCdnDispatchServer",
method: "GetCdnDispatch",
param: {
guid: "" + _getGuid(),
calltype: 0,
userip: ""
}
},
req_0: {
module: "vkey.GetVkeyServer",
method: "CgiGetVkey",
param: i
},
comm: {
uin: g_user.getUin(),
format: "json",
ct: 24,
cv: 0
}
};

观察到 guid 为 [0, 1e10) 的随机数,不登录时 uin 为 0。最终的 URL 由 CDN 与 purl 拼接而成。

歌曲能否播放

有些歌曲由于版权原因不能播放,因此基本不能通过这些方法获取歌曲文件,需先进行过滤。无法播放的歌曲在网页端显示为灰色,点击播放,弹框提示,直接搜索字符串,未找到相关代码:

分析播放按钮的响应函数,添加断点:

调试发现在此处弹框:

继续调试,发现 play 函数:

执行流程与 s[0].action.play 有关,搜索 action.play

添加断点进行验证,代码执行到此处,于是最终判断歌曲能否播放的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
e["switch"] || (e["switch"] = 403);
var o = e["switch"].toString(2).split("");
o.pop(),
o.reverse();
var a = ["play_lq", "play_hq", "play_sq", "down_lq", "down_hq", "down_sq", "soso", "fav", "share", "bgm", "ring", "sing", "radio", "try", "give"];
e.action = {};
for (var s = 0; s < a.length; s++)
e.action[a[s]] = parseInt(o[s], 10) || 0;
e.pay = e.pay || {},
e.preview = e.preview || {},
e.playTime = makePlayTime(e.interval),
e.action.play = 0,
(e.action.play_lq || e.action.play_hq || e.action.play_sq) && (e.action.play = 1),

e 是歌曲的详细数据:

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
{
"songurl": "http://stream7.qqmusic.qq.com/16830286.wma",
"songid": 4830286,
"songmid": "0033P66R0qEtlT",
"songname": "知足",
"singer": [
{
"id": 74,
"mid": "000Sp0Bz4JXH0o",
"name": "五月天",
"title": "五月天",
"type": 2,
"uin": 0
}
],
"album": {
"id": 96397,
"mid": "003PIMo40rxcAn",
"name": "知足 最真杰作选",
"subtitle": "《后来的我们》电影插曲",
"time_public": "2005-08-26",
"title": "知足 最真杰作选"
},
"switch": 65537,
// ...
}

网易云音乐

网易云音乐的 HTTP 请求都经过了 AES 加密,具体的 POST 数据可以通过断点获得,url、搜索等 API 已有很多相关帖子。

歌曲能否播放

无法播放的歌曲显示为灰色:

点击播放,出现:

尝试搜索字符串,下断点:

相关代码如下

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
function cnp9g(d5i) {
var j5o = [pf0x.eR7K(d5i.id)]
, bm5r = j5o[0]
, sg1x = l5q.qy0x(bm5r)
, tX1x = bm5r.privilege || {};
if (sg1x == 10) {
l5q.wJ2x(tX1x.fee || bm5r.fee, bm5r.id, "song", null, tX1x)
} else if (sg1x == 100) {
l5q.iM8E(null, null, null, true, bm5r)
} else if (sg1x == 11) {
l5q.bWj5o(bm5r.id, 18)
} else {
bgW3x(j5o, d5i.ext)
}
};

l5q.iM8E = function(bG5L, D5I, u5z, czT2x, bm5r) {
bG5L = bG5L || "由于版权保护,您所在的地区暂时无法使用。";
// ...
};

l5q.wJ2x = function(oC0x, tu1x, u5z, du6o, nw0x) {
var bT5Y, pN0x = "m-popup-info", bxI8A = "单首购买", bxK8C = "马上去开通", cV6P = "唱片公司要求,当前歌曲须付费使用。", bWk5p = true;
// ...
};

l5q.bWj5o = function(hM8E, gv8n) {
l5q.hH8z({
title: "提示",
message: "版权方要求,该歌曲须下载后播放",
btnok: "下载",
btncc: "取消",
okstyle: "u-btn2-w1",
ccstyle: "u-btn2-w1",
action: function(u5z) {
if (u5z == "ok") {
l5q.Mc6W({
id: hM8E,
type: gv8n
})
}
}
})
};

猜测当 sg1x 等于 10, 100, 11 时歌曲无法播放。在 bgW3x 处断点,可以发现歌曲能播放时会进入到该分支。sg1xl5q.qy0x 返回:

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
l5q.qy0x = function(bm5r) {
if (!bm5r)
return 0;
var fC7v = bm5r.privilege;
if (bm5r.program)
return 0;
if (window.GAbroad)
return 100;
if (fC7v) {
if (fC7v.st != null && fC7v.st < 0) {
return 100
}
if (fC7v.fee > 0 && fC7v.fee != 8 && fC7v.payed == 0 && fC7v.pl <= 0)
return 10;
if (fC7v.fee == 16 || fC7v.fee == 4 && fC7v.flag & 2048)
return 11;
if ((fC7v.fee == 0 || fC7v.payed) && fC7v.pl > 0 && fC7v.dl == 0)
return 1e3;
if (fC7v.pl == 0 && fC7v.dl == 0)
return 100;
return 0
} else {
var eA7t = bm5r.status != null ? bm5r.status : bm5r.st != null ? bm5r.st : 0;
if (bm5r.status >= 0)
return 0;
if (bm5r.fee > 0)
return 10;
return 100
}
}

bm5r 是在搜索“周杰伦”时返回的相关歌曲信息:

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
{
"name": "屋顶",
"ringtone": "600902000009484210",
"rtUrl": null,
"status": 0,
"pstatus": 0,
"fee": 8,
"version": 19,
"songType": 0,
"mst": 9,
"ftype": 0,
"privilege": {
"id": 298317,
"fee": 8,
"payed": 0,
"st": 0,
"pl": 128000,
"dl": 0,
"sp": 7,
"cp": 1,
"subp": 1,
"cs": false,
"maxbr": 320000,
"fl": 128000,
"toast": false,
"flag": 0,
"preSell": false
}
// ...
}

歌单详情

歌单数据在返回的响应中能看到歌名,更详细的信息在网页中以密文的形式隐藏了:

解密的方法参考网易云音乐歌单详情列表爬虫破解

Python 实现

音乐下载工具

------ 本文结束 ------

版权声明

Memory is licensed under a Creative Commons BY-NC-SA 4.0 International License.
博客采用知识共享署署名(BY)-非商业性(NC)-相同方式共享(SA)
本文首发于Memory,转载请保留出处。