-
-
Save owlwang/f1d625fcfe60bfaf0906e37fbc2ee1ed to your computer and use it in GitHub Desktop.
weread download,直接生成epub。仅用于技术研究。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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><div>', | |
' <p>{{book.intro}}</p></div>', | |
' </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