免责声明

  • 此文章具有非常强的时效性。
  • 本文只是技术思路分享,未实现完整程序,更不存在传播相关程序。
  • 测试时下载的歌曲,也在测试完成后立即删除,并未进行传播保留。

前言

这几天失业了,QQ 音乐也正好提醒会员即将到期,显然是不会续费了。那么,有没有可能将已有的歌曲保存到本地,以便在失业潮里也能…。打住,版权,版权,还是版权,我们只做技术分析学习。

从哪里开始?

QQ 音乐有多种版本:PC/Mac 版、网页版、移动端版,显然,网页版是最容易进行技术分析的。

获取歌曲列表

在浏览器打开 https://y.qq.com/ 后登录,发现我的音乐音乐列表有限制,仅显示前 10 首。

查看 Network 接口,可以看到相关请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// POST
// https://*.*.qq.com/cgi-bin/musics.fcg?_=**&sign=**

// Body
{
"comm": {
// 省略其他参数
},
"req_1": {
"module": "**",
"method": "**",
"param": {
"song_begin": 0,
"song_num": 10
// 省略其他参数
}
}
}

很容易看出,req_1.param.song_num 是控制返回歌曲数量的参数。

尝试通过 Postman 修改 song_num 参数值为 1000 后返回接口会报错。

通过多次测试确定是 sign=** 参数的原因,看样子页面在发送请求前会对请求参数进行一个签名计算,服务端进行验证。

尝试在 DevTools 中操作,在相关代码修改了song_num: 10 的值,后面代码再进行签名、发出请求,就可以成功获取到歌曲列表了。

下载歌曲

歌曲列表 API 并不包含歌曲文件地址,因此需要进入歌曲播放页面,通过 DevTools 查看网络请求,发现一个 GET 请求:

1
https://**.stream.qqmusic.qq.com/**.m4a?guid=**&vkey=**&uin=**&fromtag=**

返回了歌曲文件内容,保存为 m4a 文件后可以播放。

观察这个请求参数 发现关键难点参数是 vkey,他也是类似上面的 sign 参数,也是通过一些参数加算法处理得到的,服务端会进行验证。

实现思路

如果通过 httpClient 去实现这个程序,虽然执行效率会更高,但需要对诸多参数进行分析,还需要从压缩的 js 代码中扒取到相关算法代码,还可能遭遇验证码问题。考虑到这类需求功能通常只会用一两次,也不在乎这点执行效率,所以这个方案性价比太低。

那么选择使用 Selenium 或者 Puppeteer 来实现,性价比是最高的,只需要少量代码,少量调试即可快速搞定需求。

下面实例代码是 Puppeteer 实现的。

步骤

  1. 登录授权

Puppeteer 打开 QQ 音乐,用户再手动登录。

  1. 获取歌曲列表

可以通过 request 事件,对指定 js 文件进行拦截,修改为本地 js 文件,本地文件对 song_num: 10 参数进行了修改。下面是示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const likePage = await browser.newPage();
likePage.setRequestInterception(true);
likePage.on("request", (request) => {
const url = request.url();
if (url.includes("xxxxxx.js")) {
request.respond({
status: 200,
contentType: "application/javascript; charset=utf-8",
body: fs.readFileSync("xxxxxx.js", { encoding: "utf-8" }),
});
} else {
request.continue();
}
});
await likePage.goto("https://**.qq.com/n/**/profile/like/song");
  1. 下载歌曲

页面渲染出歌曲列表后,我们通过以下代码,点击一首歌曲的播放按钮,打开播放页面。

1
2
3
await likePage.evaluate(
`document.querySelector('.songlist__item:nth-child(${index}) .list_menu__play').click()`
);

我们在播放页面的 request 事件中对请求的 url 进行判断,如果是歌曲文件,再通过 httpClient 下载保存到本地即可。保存成功再切换到下一首,直到歌曲列表全部下载完成。

以上就整个程序的实现思路了。

可能会遇到的坑

  • 播放过的歌曲可能会缓存在浏览器的 IndexedDB 中,缓存过的歌曲不再进行网络请求。
  • 歌曲列表中可能会遇到一些无版权的歌曲,需要判断进行跳过或先从列表中移除。
  • 歌曲名、歌手名等可能存在特殊字符,用作文件名时需要进行处理。