当然,我们是以课程(产品为单位的),无论如何,也要获取课程列表。
所以,要先写一个获取json数据的方法,全部是post接口,那么容我偷懒。
加一点常量
let BASEURL = "https://time.geekbang.org/serv"; let API_URL = { chapters: "/serv/v1/chapters", articles: "/serv/v1/column/articles", article: "/serv/v1/article", comments: "/serv/v1/comments", product: "/serv/v3/learn/product" };获取之后,往界面里面注入一个下载面板,列出标题,和加一个下载按钮就好。
; (async function init() { try { // 获取已购买的产品 const resProducts = await request(API_URL.product, { "desc": true, "expire": 1, "last_learn": 0, "learn_status": 0, "prev": 0, "size": 50, "sort": 1, "type": "", "with_learn_count": 1 }); if (resProducts.code != 0 || resProducts.data.list.length < 0) { return console.log("穷光蛋,没有购买任何产品"); } injectControlPanel(resProducts.data.products); } catch (err) { console.log("脚本执行异常", err); } })(); let CONTROL_PANEL_ID = "xxx_yyy_zzz"; function injectControlPanel(products = []) { if(document.getElementById(CONTROL_PANEL_ID)){ return; } const container = document.createElement("div"); container.id = CONTROL_PANEL_ID; container.style.cssText = "position:fixed; top:20px; right:20px; z-index:99999; background-color:#DDD;padding: 10px;"; document.body.appendChild(container); function onDownload(ev) { const id = ev.target.dataset.id; const product = products.find(p => p.id == id); if (!product) { return console.log("未找到 id为", id, "的产品"); } console.log("开始下载"); // TODO:: 执行下载 } products.forEach(p => { const pEl = document.createElement("div"); const textEl = document.createTextNode(p.title); const btnEl = document.createElement("button"); btnEl.type = "button"; btnEl.textContent = "下载"; btnEl.dataset.id = p.id; btnEl.style.marginLeft = "10px"; btnEl.onclick = onDownload; pEl.appendChild(textEl); pEl.appendChild(btnEl); container.appendChild(pEl) }) }这个试行,你执行一次,就能在界面看到
下载 - 2. 注入JSZipcheckScript 检查某脚本是否已经注入
function checkScript({ names = [], objectName } = {}) { if (typeof objectName !== "string") { return false } if (window[objectName] != null) { return true; } const ns = Array.isArray(names) ? names : [names]; const scripts = Array.from(document.scripts); return ns.some(n => { return scripts.find(s => s.src && s.src.toLocaleLowerCase().endsWith(n.toLocaleLowerCase())) }) }injectScript注入脚本, 用来注入JSZip
function injectScript(src) { return new Promise((resolve, reject) => { const d = delay(reject, 10000); const script = document.createElement("script"); script.crossorigin = "anonymous"; script.src = src; script.onerror = reject; script.onload = () => { d.cancel(); resolve(); } document.body.appendChild(script); }); }执行检查和注入
console.log("准备检查和注入JSZip"); if (!checkScript({ names: "jszip.min.js", objectName: "JSzip", })) { await injectScript("https://cdn.bootcdn.net/ajax/libs/jszip/3.5.0/jszip.min.js"); console.log("注入JSZip成功"); } 下载 - 3.下载某个课程这里其实也是蛮简单的思路。
获取课程信息
获取课程的章节信息
依次获取课程的内容
打包下载
这里,浏览器里面请求太快,会返回451的状态码。可以关闭再来。这个我怀疑是浏览器的行为,还不一定是极客时间的行为。如果有同志告诉我原因。最好了。
所以我这里需要几个工具方法。
delay 避免请求太快
function delay(delay = 5000, fn = () => { }, context = null) { let ticket = null; return { run(...args) { return new Promise((resolve, reject) => { ticket = setTimeout(async () => { try { const res = await fn.apply(context, args); resolve(res); } catch (err) { reject(err) } }, delay) }) }, cancel: () => { clearTimeout(ticket); } } }download 下载
function downloadBlob(name, blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a') a.href = url; a.download = name; a.click(); }