背景
本来我在A平台听书,到了后面的章节,便只能去B平台。
操作繁琐,体验极差,还不支持缓存,流量超贵的(上个月套餐外流量消费60多)
于是乎,我只能把音频手动下载起来,但操作同样繁琐。
需求
下载一个音频的操作如下
- 进入列表页,可能需要滚动加载
- 进入播放页面,打开了一个新的标签,点击"播放按钮"
- 打开发者工具,找到audio标签,不点"播放按钮"audio标签不出来
- 复制链接到新的标签页打开
- 通过浏览器自带的功能下载,需要点两下
- 等待下载完成
- 去另一个标签页复制音频的名字,
- 进入下载内容的文件夹,然后修改名字,不然都是一个名字,没法区分
这个过程还是很繁琐的,下载五六个之后已经很痛苦了,还有两百多个章节....
实现
我首先想到了无头浏览器,一番折腾后,断断续续后,终于解决,具体的音频地址便不透露了,大同小异。
const puppeteer = require("puppeteer");
const axios = require("axios");
const querystring = require("querystring");
const fs = require("fs");
puppeteer
可以理解为一个"无头浏览器",通过代码的形式完成人机交互。
中文文档: http://www.puppeteerjs.com/#?...
(async () => {
const audioInfoList = [];
const [
reqUrl,
search,
] = "分页获取列表信息的接口地址".split(
"?"
);
"分页获取列表信息的接口地址" 是借助开发者工具拿到的,尽量减少对人机交互的模拟,不然这里还要鼠标滚动来触发上拉加载。
let params = Object.assign({}, querystring.parse(search), {
begin: 20,
count: 10,
});
参数的替换
const audioRsp = await axios.post(
reqUrl + "?" + querystring.stringify(params)
);
const audioInfoList = audioRsp.data.appmsg_list.map((item) => ({
name: item.title,
url: item.link,
}));
发起ajax请求,记录音频的名字以及音频播放页地址
for (let i = 0; i < audioInfoList.length; i++) {
const audioItem = audioInfoList[i];
try {
const browser = await puppeteer.launch();
const page = await browser.newPage({});
await page.goto(audioItem.url, {
waitUntil: "networkidle0",
});
- 创建一个浏览器
- 创建一个标签页
跳转到音频播放页
page.waitForSelector(".audio_card_switch"); await page.click(".audio_card_switch");
等待
.audio_card_switch
代表的"播放按钮"出现再点击,因为"播放按钮"需要某个网络请求才会出现。
const downloadSrc = await page.evaluate(() => {
return document.querySelector("audio").src;
});
const audioContent = await axios.get(downloadSrc, {
responseType: "arraybuffer",
});
fs.writeFileSync(`download/${audioItem.name}.mp3`, audioContent.data);
console.log(
`${i}/${audioInfoList.length - 1} ${audioItem.name} : ${downloadSrc}`
);
await browser.close();
} catch (error) {
console.log(
"下载失败:" + i + " " + audioItem.name + " " + audioItem.url
);
}
}
- 获取音频地址
- 通过ajax请求获取音频内容,注意
responseType: "arraybuffer",
是必须的,否则音频无法播放,你可能得到"此文件无法播放。这可能是因为文件类型不受支持、文件扩展名不正确或文件已损坏。0xc00d36c4“的错误 - 将音频文件存储到本地。
下载方式
for (let i = 0; i < audioInfoList.length; i++) {
,下载完一个再去下载下一个,这样一次性可以把当前页的音频下载完,经实验证明,这样要好于一次下载多个 audioInfoList.forEach(async (audioItem, audioIndex) => {
,后者很吃"资源",下载顺序也是乱的,出现下载失败的几率高,一旦出现也不好排查是哪个出了问题,目录中有的就不是音频。
0xc00d36fa
"找不到音频设备。请确保耳机或扬声器已连接。有关更多信息,请在设备中搜索“管理音频设备"。
不要把这个错误当成下载的音频文件有问题。