极客时间离线课堂 (4)

大致效果如下:

极客时间离线课堂

搭建预览网站 - 文章详情

上面传了id参数到这个页面,那么先得解queryString。就不写什么正则了,用原生API就好。

const urlSP = new URLSearchParams(window.location.href.split("?")[1]); function getQueryString(key) { return urlSP.get(key) }

这里需要额外处理的就是章节信息。

articles.json返回的数据chapter_id就是chapters.json里面的id。
有一种特殊情况,章节信息为空数据。
这里需要组合一下。

async function getNavData(id) { const articles = await getJSONData(`./products/${id}/articles.json`); const chapters = await getJSONData(`./products/${id}/chapters.json`); if (!chapters || chapters.length <= 0) { chapters[0] = { id: "0", title: "全部章节" } } const articlesMap = articles.reduce((obj, cur) => { obj[cur.chapter_id] = obj[cur.chapter_id] || []; obj[cur.chapter_id].push(cur); return obj; }, {}) chapters.forEach(c => { c.children = articlesMap[c.id] || []; }) return chapters; }

有了组合的章节信息,那么我们就来创建菜单

async function createNav(id) { try { const chapters = await getNavData(id); chapters.forEach(c => { const chapterEl = document.createElement("h3"); chapterEl.innerHTML = c.title; chapterEl.style = "padding-left:10px"; leftNavEl.appendChild(chapterEl); const ulEl = document.createElement("ul"); c.children.forEach(cc => { const liEl = document.createElement("li"); liEl.className = "menu-item"; liEl.dataset.id = cc.id; liEl.innerText = cc.article_title; ulEl.onclick = onViewArticle; ulEl.appendChild(liEl); }) leftNavEl.appendChild(ulEl); }); } catch (err) { console.log("createNav failed", err); } }

最后,我们需要默认选中第一个文章。

; (async function init() { await createNav(id); const fa = leftNavEl.querySelector(".menu-item"); if (fa) { fa.click(); } })()

最后就剩下,获取artile的内容了。

async function onViewArticle(ev) { try { acticleEl.innerHTML = ""; document.querySelectorAll(".menu-item.active").forEach(el=>{ el.classList.remove("active"); }) ev.target.classList.add("active"); const artileId = ev.target.dataset.id; const article = await getJSONData(`./products/${id}/articles/${artileId}.json`); acticleEl.innerHTML = article.article_content; if (article.is_video_preview && Object.keys(article.video_preview).length > 0) { // createVideoPlayer(article); } } catch (err) { console.log("获取文章内容失败", err); } }

目前为止,你的预览网站就搭建完毕了,是不是很简单。

我们来预览一下

极客时间离线课堂

图片和MP4下载

到这里为了,我们回头再看看需求

无限看

免费看

断网看

无限看和免费看已经实现了,那么断网看还没有实现。
这里不得不说一下,极客文章里面的图片和文章的MP4视频,是直接外挂到CDN的。直接下载就好。


我们暂且处理两种格式的资源 img, video。

基本思路:

[articleId].json文件正则匹配actile_conten

下载资源

替换actile_conten里面的 img, video地址

这里需要额外注意一下,如果是把某个mp4下载到内容,再保存到磁盘,可能会挂,所以才去pip直接传递

这里也不多说了,直接放代码。
可以改进的地方很多,比如多线程。
建议保留一份原始下载文件,毕竟下载下来还是很占空间的。保留原始的文件,可以随时切换离线和在线的 图片和视频

import * as fs from "fs"; import * as path from "path"; import axios from "axios"; const WHITELIST = ["xxxxxxx"]; function toPath(...paths: string[]) { return path.join(__dirname, ...paths) } const srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/img; const PRODUCTS_PATH = toPath("../../server/products"); const IMAGES_PATH = toPath("../../server/images"); async function downloadFiles(productId: string, articles: string[]) { const productPath = path.join(IMAGES_PATH, productId); if (!fs.existsSync(productPath)) { fs.mkdirSync(productPath); } for (let i = 0; i < articles.length; i++) { const article = articles[i]; const articleJSONPath = path.join(PRODUCTS_PATH, productId, "articles", article) const articleJSON = JSON.parse(fs.readFileSync(articleJSONPath, "utf-8")); const articleId = article.split(".")[0]; const articlePath = path.join(productPath, articleId); if (!fs.existsSync(articlePath)) { fs.mkdirSync(articlePath); } const content: string = articleJSON.article_content; let contentNew = content; while (srcReg.exec(content)) { const rerouceSrc = RegExp.$1; // 已经替换 if (!rerouceSrc.startsWith("http") || !rerouceSrc.startsWith("https")) { continue; } const fileName = rerouceSrc.split("http://www.likecs.com/").pop(); const filePath = path.join(IMAGES_PATH, productId, articleId, rerouceSrc.split("http://www.likecs.com/").pop()); if (fs.existsSync(filePath) || fileName.endsWith("m3u8")) { continue; } console.log("获取资源", rerouceSrc); await axios({ method: 'get', url: rerouceSrc, responseType: 'stream' }).then(function (response) { response.data.pipe(fs.createWriteStream(filePath, { encoding: "utf-8" })) // const buffer = response.data; // fs.writeFileSync(filePath, buffer, { encoding: "utf-8" }) }).then(() => { const from = rerouceSrc; const to = `./images/${productId}/${articleId}/${fileName}`; console.log("repalce", from, to); contentNew = contentNew.replace(from, to); }) } articleJSON.article_content = contentNew; fs.writeFileSync(articleJSONPath, JSON.stringify(articleJSON), { encoding: "utf-8" }) } } ; (async () => { // products 下面的所有文件 const files = fs.readdirSync(PRODUCTS_PATH); for (let i = 0; i < files.length; i++) { const dirName = files[i]; if (WHITELIST.length > 0 && !WHITELIST.includes(dirName)) { continue; } // 文件跳过 if (fs.statSync(path.join(PRODUCTS_PATH, dirName)).isFile()) { continue; } const dirFullPath = path.join(PRODUCTS_PATH, dirName, "articles"); const articles = fs.readdirSync(dirFullPath); // 下载 downloadFiles(dirName, articles); break; } })(); 直播格式的m3u8

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpjjgy.html