Skip to content

Instantly share code, notes, and snippets.

@owlwang
Forked from jianyun8023/weread.user.js
Created June 21, 2022 11:35
Show Gist options
  • Save owlwang/f1d625fcfe60bfaf0906e37fbc2ee1ed to your computer and use it in GitHub Desktop.
Save owlwang/f1d625fcfe60bfaf0906e37fbc2ee1ed to your computer and use it in GitHub Desktop.
weread download,直接生成epub。仅用于技术研究。
// ==UserScript==
// @name 微信读书下载
// @namespace http://tampermonkey.net/
// @version 0.3
// @description 下载微信读书的书籍资源
// @author tang
// @match https://weread.qq.com/web/reader/*
// @grant unsafeWindow
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @run-at document-idle
// @connect res.weread.qq.com
// @connect tencent-cloud.com
// @connect myqcloud.com
// @require https://cdn.bootcss.com/jszip/3.2.2/jszip.js
// @require https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js
// @require https://unpkg.com/art-template/lib/template-web.js
// ==/UserScript==
(function () {
'use strict';
class Ebook {
constructor(id, title, author, intro, publisher, publishTime, maxLevel) {
this.id = id;
this.title = title;
this.author = author;
this.intro = intro;
this.publisher = publisher;
this.publishTime = publishTime;
this.maxLevel = maxLevel;
this.images = [];
};
setCpid(cpid) {
this.cpid = cpid;
}
setIsbn(isbn) {
this.isbn = isbn;
}
setChapterList(chapterList) {
this.chapterList = chapterList;
}
setImages(images){
this.images = images;
}
}
class Chapter {
constructor(uid, path, title, level, playOrder) {
this.uid = uid;
this.path = path;
this.title = title;
this.level = level;
this.playOrder = playOrder;
this.subChapter = [];
};
addSubChapter(chapter) {
this.subChapter.push(chapter)
}
getLastSubChapter() {
return this.subChapter[this.subChapter.length - 1]
}
}
const buildEbook = book => {
var maxLevel = 1
var chapterList = []
var prveFirstLevelChapter
book.chapterInfos.forEach((element, i) => {
var chapter = new Chapter(element.chapterIdx, element.chapterIdx + ".html", element.title, element.level, i + 1)
if (chapter.level > maxLevel) {
maxLevel = chapter.level
}
if (chapter.level == 1) {
chapterList.push(chapter)
prveFirstLevelChapter = chapter
} else if (chapter.level == 2) {
prveFirstLevelChapter.addSubChapter(chapter)
} else if (chapter.level == 3) {
if (prveFirstLevelChapter.getLastSubChapter() == undefined) {
prveFirstLevelChapter.addSubChapter(chapter)
} else {
prveFirstLevelChapter.getLastSubChapter().addSubChapter(chapter)
}
} else if (chapter.level == 4) {
if (prveFirstLevelChapter.getLastSubChapter().getLastSubChapter() == undefined) {
prveFirstLevelChapter.getLastSubChapter().addSubChapter(chapter)
} else {
prveFirstLevelChapter.getLastSubChapter().getLastSubChapter().addSubChapter(chapter)
}
} else {
alert("暂不支持五级目录深度 " + chapter.level)
return
}
});
var ebook = new Ebook(
book.bookInfo.bookId,
book.bookInfo.title,
book.bookInfo.author,
book.bookInfo.intro,
book.bookInfo.publisher,
book.bookInfo.publishTime,
maxLevel
)
ebook.setChapterList(chapterList)
ebook.setIsbn(book.bookInfo.isbn)
ebook.setCpid(book.bookInfo.cpid)
ebook.setImages(bookImages)
return ebook
}
const sleep = ms => {
return new Promise(resolve =>
setTimeout(resolve, ms)
)
}
function get(url, headers, type) {
return new Promise((resolve, reject) => {
let requestObj = GM_xmlhttpRequest({
method: "GET", url, headers,
responseType: type || 'json',
onload: (res) => {
if (res.status === 204) {
requestObj.abort();
}
if (type === 'blob') {
resolve(res.response);
} else {
resolve(res.response || res.responseText);
}
},
onerror: (err) => {
reject(err);
},
});
});
}
function createAndDownloadFile(fileName, content) {
var aTag = document.createElement('a');
aTag.download = fileName;
aTag.href = URL.createObjectURL(content);
aTag.click();
URL.revokeObjectURL(content);
}
const imageUrlToBlob = url => get(url, {}, 'blob')
//var $ = unsafeWindow.$
var vue = $("div.readerContent.routerView")[0]
// Your code here...
//book = unsafeWindow.$("div.readerContent.routerView").__vue__
const parseCss = cssText => cssText.replace(/\.readerChapterContent/g, "")
const fixBody = failBody => failBody.replace("</body>", "</div>")
const buildHead = book => `<title>${book.currentChapter.title}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>`
const buildHtml = (head, body, css) => {
var html = `<?xml version='1.0' encoding='utf-8'?><html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN"><head>${head}</head><body><style>${css}</style>${body}</body></html>`
return html
}
function cleanAttr(element) {
element.removeAttr("data-wr-co")
element.removeAttr("data-wr-bd")
element.removeAttr("data-wr-id")
element.removeAttr("data-ratio")
element.removeAttr("data-w")
element.removeAttr("data-w-new")
element.removeData("wr-co")
element.removeData("wr-bd")
element.removeData("wr-id")
element.removeData("ratio")
element.removeData("w")
element.removeData("w-new")
}
function cleanTag(element) {
cleanAttr(element)
element.html(element.text())
}
const bookImages = []
const log = str => {
$('.readerMemberCardTips').attr("style", "")
$('.readerMemberCardTips > .text').html(str)
}
const replaceImages = (doc, zip) => {
doc.find("img")
.each(function () {
var img = $(this)
var url = img.attr("data-src");
console.log("处理图片 " + url)
if (url.indexOf("http") == -1) return
var imageName = url.substr(url.lastIndexOf("/") + 1)
if (imageName.indexOf(".") == -1) imageName += ".jpg"
zip.file("img/" + imageName, imageUrlToBlob(url))
img.attr("src", "../img/" + imageName)
img.removeAttr("data-src")
bookImages.push("img/" + imageName)
})
return doc.html()
}
const cleanHtml = doc => {
doc.find("div").each(function () {
cleanAttr($(this))
})
doc.find("img").each(function () {
cleanAttr($(this))
})
doc.find("h1").each(function () {
cleanTag($(this))
})
doc.find("h2").each(function () {
cleanTag($(this))
})
doc.find("h3").each(function () {
cleanTag($(this))
})
doc.find("p").each(function () {
if ($(this).find("img").length > 0) {
cleanAttr($(this))
} else {
cleanTag($(this))
}
})
}
var tocncx = ['<?xml version="1.0" encoding="UTF-8"?>',
'<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">',
'<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="zh-CN">',
' <head>',
' <meta name="dtb:uid" content="{{book.id}}"/>',
' <meta name="dtb:depth" content="{{book.maxLevel}}"/>',
' <meta name="dtb:totalPageCount" content="0"/>',
' <meta name="dtb:maxPageNumber" content="0"/>',
' </head>',
' <docTitle>',
' <text>{{ book.title }}</text>',
' </docTitle>',
' <docAuthor>',
' <text>{{ book.author }}</text>',
' </docAuthor>',
' <navMap>',
' <navPoint class="toc" id="toc" playOrder="1">',
' <navLabel>',
' <text>Table of Contents</text>',
' </navLabel>',
' <content src="text/toc.html"/>',
' </navPoint>',
' {{each book.chapterList}}',
' <navPoint class="chapter" id="chapter_{{$value.uid}}" playOrder="{{$value.playOrder}}">',
' <navLabel>',
' <text>{{$value.title}}</text>',
' </navLabel>',
' <content src="text/{{$value.path}}"/>',
' {{each $value.subChapter}}',
' <navPoint class="chapter" id="chapter_{{$value.uid}}" playOrder="{{$value.playOrder}}">',
' <navLabel>',
' <text>{{$value.title}}</text>',
' </navLabel>',
' <content src="text/{{$value.path}}"/>',
' {{each $value.subChapter}}',
' <navPoint class="chapter" id="chapter_{{$value.uid}}" playOrder="{{$value.playOrder}}">',
' <navLabel>',
' <text>{{$value.title}}</text>',
' </navLabel>',
' <content src="text/{{$value.path}}"/>',
' {{each $value.subChapter}}',
' <navPoint class="chapter" id="chapter_{{$value.uid}}" playOrder="{{$value.playOrder}}">',
' <navLabel>',
' <text>{{$value.title}}</text>',
' </navLabel>',
' <content src="text/{{$value.path}}"/>',
' </navPoint>',
' {{/each}}',
' </navPoint>',
' {{/each}}',
' </navPoint>',
' {{/each}}',
' </navPoint>',
' {{/each}}',
' </navMap>',
'</ncx>'].join("\n");
var tochtml = ['<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
'<html xmlns="http://www.w3.org/1999/xhtml">',
'<head>',
' <title>Table of Contents</title>',
' <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>',
'</head>',
'<body>',
'<h1><b>TABLE OF CONTENTS</b></h1>',
'<br/>',
'{{each book.chapterList}}',
'<h3><b><a href="{{$value.path}}">{{$value.title}}</a></b></h3>',
'<ul>',
' {{each $value.subChapter}}',
' <li><a href="{{$value.path}}">{{$value.title}}</a></li>',
' {{if $value.subChapter}}',
' <ul>',
' {{each $value.subChapter}}',
' <li><a href="{{$value.path}}">{{$value.title}}</a></li>',
' {{if $value.subChapter}}',
' <ul>',
' {{each $value.subChapter}}',
' <li><a href="{{$value.path}}">{{$value.title}}</a></li>',
' {{/each}}',
' </ul>',
' {{/if}}',
' {{/each}}',
' </ul>',
' {{/if}}',
' {{/each}}',
'</ul>',
'{{/each}}',
'</body>',
'</html>',
].join("\n");
var opf_tmp = ['<?xml version="1.0" encoding="utf-8"?>',
'<package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="BookId">',
' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:opf="http://www.idpf.org/2007/opf" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">',
' <dc:title>{{ book.title }}</dc:title>',
' <dc:language>zh-cn</dc:language>',
' <dc:creator>{{ book.author }}</dc:creator>',
' {{if book.intro}}',
' <dc:description>&lt;div&gt;',
' &lt;p&gt;{{book.intro}}&lt;/p&gt;&lt;/div&gt;',
' </dc:description>',
' {{/if}}',
' {{if book.publisher}}',
' <dc:publisher>{{book.publisher}}</dc:publisher>',
' {{/if}}',
' {{if book.publishTime}}',
' <dc:date>{{book.publishTime}}</dc:date>',
' {{/if}}',
' {{if book.isbn}}',
' <dc:identifier opf:scheme="ISBN">{{book.isbn}}</dc:identifier>',
' {{/if}}',
' {{if book.cpid}}',
' <dc:identifier opf:scheme="CPID">{{book.cpid}}</dc:identifier>',
' {{/if}}',
' <meta name="cover" content="cover_image"/>',
' </metadata>',
' <manifest>',
' <item id="cover_image" href="cover.jpg" media-type="image/jpeg"/>',
' <item id="toc" media-type="application/x-dtbncx+xml" href="toc.ncx"/>',
' <item id="toc_html" media-type="application/xhtml+xml" href="toc.html"/>',
' {{each book.chapterList}}',
' <item id="chapter_{{$value.uid}}" media-type="application/xhtml+xml" href="text/{{$value.path}}"/>',
' {{each $value.subChapter}}',
' <item id="chapter_{{$value.uid}}" media-type="application/xhtml+xml" href="text/{{$value.path}}"/>',
' {{each $value.subChapter}}',
' <item id="chapter_{{$value.uid}}" media-type="application/xhtml+xml" href="text/{{$value.path}}"/>',
' {{each $value.subChapter}}',
' <item id="chapter_{{$value.uid}}" media-type="application/xhtml+xml" href="text/{{$value.path}}"/>',
' {{/each}}',
' {{/each}}',
' {{/each}}',
' {{/each}}',
' {{each book.images}}',
' <item id="image_{{$index}}" media-type="image/jpeg" href="{{$value}}"/>',
' {{/each}}',
' </manifest>',
' <spine toc="toc">',
' <itemref idref="toc_html"/>',
' {{each book.chapterList}}',
' <itemref idref="chapter_{{$value.uid}}"/>',
' {{each $value.subChapter}}',
' <itemref idref="chapter_{{$value.uid}}"/>',
' {{each $value.subChapter}}',
' <itemref idref="chapter_{{$value.uid}}"/>',
' {{each $value.subChapter}}',
' <itemref idref="chapter_{{$value.uid}}"/>',
' {{/each}}',
' {{/each}}',
' {{/each}}',
' {{/each}}',
' </spine>',
' <guide>',
' </guide>',
'</package>',
].join("\n");
var addToc = (book, zip) => {
var toc = book.bookInfo.title
book.chapterInfos.forEach(element => {
var levelStr = "#".repeat(element.level)
toc += "\n" + levelStr + " " + element.title
});
log("addToc")
console.log(toc)
//zip.file("toc.md", toc);
var ebook = buildEbook(book)
zip.file("toc.ncx", template.render(tocncx, { "book": ebook }));
zip.file("text/toc.html", template.render(tochtml, { "book": ebook }));
zip.file("content.opf", template.render(opf_tmp, { "book": ebook }));
zip.file("mimetype", "application/epub+zip");
var containerStr = ['<?xml version="1.0" encoding="UTF-8"?>',
'<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">',
' <rootfiles>',
' <rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>',
' </rootfiles>',
'</container>'].join("\n");
zip.file("META-INF/container.xml",containerStr)
}
var addInfo = (book, zip) => {
log("addInfo")
book.bookInfo.cover = $('img.wr_bookCover_img').attr("src")
zip.file("bookInfo.json", JSON.stringify(book.bookInfo));
zip.file("chapterInfos.json", JSON.stringify(book.chapterInfos));
//zip.file("readme.txt", "使用kindlegen生成电子书,执行命令:\nkindlegen -dont_append_source " + book.bookInfo.title + ".opf");
}
var addCover = (book, zip) => {
log("addCover")
book.bookInfo.cover = $('img.wr_bookCover_img').attr("src")
zip.file("cover.jpg", imageUrlToBlob(book.bookInfo.cover));
}
var count = 0
var addChapter = (book, zip) => {
log("正在下载数据 " + (count + 1) + "/" + book.chapterInfos.length + " : " + book.currentChapter.title)
var head = buildHead(book)
var rawBody = $(fixBody(book.chapterContentForEPub.join('')))
cleanHtml(rawBody)
var body = replaceImages(rawBody, zip)
var newHtml = buildHtml(head, body, parseCss(book.chapterContentStyles));
zip.file("text/" + book.currentChapter.chapterIdx + ".html", newHtml);
count++
}
var download = (book, zip) => {
if (count >= book.chapterInfos.length) {
addToc(book, zip)
console.log("生成epub文件")
// if (count >= 4) {
zip.generateAsync({ type: "blob" })
.then(function (content) {
unsafeWindow.rawBook = content
log('已获取全部数据,点击<a href="javascript:" title="下载" class="click_download">下载</a>')
$(".click_download").click(function () {
if (unsafeWindow.rawBook) {
createAndDownloadFile(book.bookInfo.title + ".epub", unsafeWindow.rawBook);
} else {
log("缺失文件,请重新下载")
}
})
$(".click_download").click()
});
return
}
sleep(3000).then(() => {
book.handleNextChapter().then(() => {
addChapter(book, zip)
download(book, zip)
});
})
}
sleep(5000).then(() => {
var book = vue.__vue__
unsafeWindow.book = book
var downloadBtn = '<button title="下载" class="readerControls_item download1"><span class="icon" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAABLUlEQVR4nO2aTQ6CMBSER08kt3GJrDwGSw+t4oYXY8PPa0WHtvMlXZAAnflKAgEAIYQQQtTKgTj3EGxTshwZk+4JCWAHYCMB7ABsJIAdgI0EsAOwkQB2ADYSwA7ARgLYAdhIADsAGwlgB2AjAewAbLYWcNr4fKw5kugBPAFcnfsPwfBwAfAAcItO92N6vIt4JcQKsPK2/24kNPgMNozb3cpxMQK6mTma5NQbE66O50rwCkg5N4XYoB4B2ZQ3YgKvCciuvOENviQg2/KGp8CcgOzLG2tFpgQUU95YKhQKKK68MXcPDwWkPEtkw9TqLo0iVj7EK6HI8saahKLLG3MSqihvhBKqKm+0AO7jaMlZaJzHIYTg8M3/uZ73eP8kqYu+C7ADsKlegBBCCCHq5QW/t89vbCrH2wAAAABJRU5ErkJggg==);"></span></button>';
$('button.catalog').after(downloadBtn);
$(".download1").click(function () {
if (!book.isEPub) {
alert("该书源非EPUB,暂不支持下载!")
}
var zip = new JSZip();
unsafeWindow.$zip = zip
// addInfo(book, zip)
addCover(book, zip)
book.changeChapter({ 'chapterUid': book.chapterInfos[0]['chapterUid'] }).then(() => {
addChapter(book, zip)
download(book, zip)
})
})
console.log("微信读书下载插件已加载!")
console.log(buildEbook(book))
})
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment