Greasy Fork

Greasy Fork is available in English.

HyReadEPUBV2

适配新版

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         HyReadEPUBV2
// @namespace    https://qinlili.bid
// @version      0.2.2
// @description  适配新版
// @author       琴梨梨
// @match        https://service.ebook.hyread.com.tw/ebookservice/epubreader/hyread/v3/openbook2.jsp?*
// @icon         https://webcdn2.ebook.hyread.com.tw/Template/store/favicon/favicon.ico
// @grant        none
// @require      https://lib.baomitu.com/jquery/3.6.0/jquery.min.js#sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==
// @require      https://lib.baomitu.com/forge/0.10.0/forge.all.min.js#sha512-vD/QEI4MRcUFkosDvj9l/W20cvpkM5zSLPbWUqwscPySsicOTwapvHLHCQ1k8CCufi+1nOEJlENsAwQJNIygbg==
// @require      https://lib.baomitu.com/jszip/3.10.1/jszip.min.js#sha512-XMVd28F1oH/O71fzwBnV7HucLxVwtxf26XV8P4wPk26EDxuGZ91N8bsOttmnomcCD3CS5ZMRL50H0GgOHvegtg==
// @require      https://cdn.jsdelivr.net/npm/[email protected]/base64.min.js
// @license      MPL2.0
// ==/UserScript==

(function () {
    'use strict';
    //CODENAME:SALMON
    //初始化依赖
    const SakiProgress = {
        isLoaded: false,
        progres: false,
        pgDiv: false,
        textSpan: false,
        first: false,
        alertMode: false,
        init: function (color) {
            if (!this.isLoaded) {
                this.isLoaded = true;
                console.info("SakiProgress Initializing!\nVersion:1.0.4\nQinlili Tech:Github@qinlili23333");
                this.pgDiv = document.createElement("div");
                this.pgDiv.id = "pgdiv";
                this.pgDiv.style = "z-index:9999;position:fixed;background-color:white;min-height:32px;width:auto;height:32px;left:0px;right:0px;top:0px;box-shadow:0px 2px 2px 1px rgba(0, 0, 0, 0.5);transition:opacity 0.5s;display:none;";
                this.pgDiv.style.opacity = 0;
                this.first = document.body.firstElementChild;
                document.body.insertBefore(this.pgDiv, this.first);
                this.first.style.transition = "margin-top 0.5s"
                this.progress = document.createElement("div");
                this.progress.id = "dlprogress"
                this.progress.style = "position: absolute;top: 0;bottom: 0;left: 0;background-color: #F17C67;z-index: -1;width:0%;transition: width 0.25s ease-in-out,opacity 0.25s,background-color 1s;"
                if (color) {
                    this.setColor(color);
                }
                this.pgDiv.appendChild(this.progress);
                this.textSpan = document.createElement("span");
                this.textSpan.style = "padding-left:4px;font-size:24px;";
                this.textSpan.style.display = "inline-block"
                this.pgDiv.appendChild(this.textSpan);
                var css = ".barBtn:hover{ background-color: #cccccc }.barBtn:active{ background-color: #999999 }";
                var style = document.createElement('style');
                if (style.styleSheet) {
                    style.styleSheet.cssText = css;
                } else {
                    style.appendChild(document.createTextNode(css));
                }
                document.getElementsByTagName('head')[0].appendChild(style);
                console.info("SakiProgress Initialized!");
            } else {
                console.error("Multi Instance Error-SakiProgress Already Loaded!");
            }
        },
        destroy: function () {
            if (this.pgDiv) {
                document.body.removeChild(this.pgDiv);
                this.isLoaded = false;
                this.progres = false;
                this.pgDiv = false;
                this.textSpan = false;
                this.first = false;
                console.info("SakiProgress Destroyed!You Can Reload Later!");
            }
        },
        setPercent: function (percent) {
            if (this.progress) {
                this.progress.style.width = percent + "%";
            } else {
                console.error("Not Initialized Error-Please Call `init` First!");
            }
        },
        clearProgress: function () {
            if (this.progress) {
                this.progress.style.opacity = 0;
                setTimeout(function () { SakiProgress.progress.style.width = "0%"; }, 500);
                setTimeout(function () { SakiProgress.progress.style.opacity = 1; }, 750);
            } else {
                console.error("Not Initialized Error-Please Call `init` First!")
            }
        },
        hideDiv: function () {
            if (this.pgDiv) {
                if (this.alertMode) {
                    setTimeout(function () {
                        SakiProgress.pgDiv.style.opacity = 0;
                        SakiProgress.first.style.marginTop = "";
                        setTimeout(function () {
                            SakiProgress.pgDiv.style.display = "none";
                        }, 500);
                    }, 3000);
                } else {
                    this.pgDiv.style.opacity = 0;
                    this.first.style.marginTop = "";
                    setTimeout(function () {
                        SakiProgress.pgDiv.style.display = "none";
                    }, 500);
                }
            }
            else {
                console.error("Not Initialized Error-Please Call `init` First!");
            }
        },
        showDiv: function () {
            if (this.pgDiv) {
                this.pgDiv.style.display = "";
                setTimeout(function () { SakiProgress.pgDiv.style.opacity = 1; }, 10);
                this.first.style.marginTop = (this.pgDiv.clientHeight + 8) + "px";
            }
            else {
                console.error("Not Initialized Error-Please Call `init` First!");
            }
        },
        setText: function (text) {
            if (this.textSpan) {
                if (this.alertMode) {
                    setTimeout(function () {
                        if (!SakiProgress.alertMode) {
                            SakiProgress.textSpan.innerText = text;
                        }
                    }, 3000);
                } else {
                    this.textSpan.innerText = text;
                }
            }
            else {
                console.error("Not Initialized Error-Please Call `init` First!");
            }
        },
        setTextAlert: function (text) {
            if (this.textSpan) {
                this.textSpan.innerText = text;
                this.alertMode = true;
                setTimeout(function () { this.alertMode = false; }, 3000);
            }
            else {
                console.error("Not Initialized Error-Please Call `init` First!");
            }
        },
        setColor: function (color) {
            if (this.progress) {
                this.progress.style.backgroundColor = color;
            }
            else {
                console.error("Not Initialized Error-Please Call `init` First!");
            }
        },
        addBtn: function (img) {
            if (this.pgDiv) {
                var btn = document.createElement("img");
                btn.style = "display: inline-block;right:0px;float:right;height:32px;width:32px;transition:background-color 0.2s;"
                btn.className = "barBtn"
                btn.src = img;
                this.pgDiv.appendChild(btn);
                return btn;
            }
            else {
                console.error("Not Initialized Error-Please Call `init` First!");
            }
        },
        removeBtn: function (btn) {
            if (this.pgDiv) {
                if (btn) {
                    this.pgDiv.removeChild(btn);
                }
            }
            else {
                console.error("Not Initialized Error-Please Call `init` First!");
            }
        }
    };
    const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));
    const dlFile = (link, name) => {
        let eleLink = document.createElement('a');
        eleLink.download = name;
        eleLink.style.display = 'none';
        eleLink.href = link;
        document.body.appendChild(eleLink);
        eleLink.click();
        document.body.removeChild(eleLink);
    };


    SakiProgress.init();
    SakiProgress.showDiv();
    SakiProgress.setText("正在等待密钥...");
    let key = false;
    let dlHyRead = async (key, url) => {
        //开始下载
        await sleep(100);
        let fileList = [];
        SakiProgress.showDiv();
        SakiProgress.setText("正在准备下载...");
        console.log("Igniting Salmon Core ...");
        const basePath = url;
        await sleep(100);
        SakiProgress.setPercent(12);
        SakiProgress.setText("正在读取EPUB信息...");
        let container = await (await fetch(basePath + "META-INF/container.xml")).blob();
        fileList.push({ file: container, path: "META-INF/container.xml" });
        let containerParsed = new DOMParser().parseFromString(await container.text(), "text/xml");
        console.log(containerParsed);
        await sleep(100);
        SakiProgress.setPercent(15);
        SakiProgress.setText("正在读取文件清单...");
        let rootfile = containerParsed.getElementsByTagName("rootfile")[0];
        if (rootfile.getAttribute("media-type") == "application/oebps-package+xml" || confirm("该书的信息似乎不符合标准,继续吗?")) {
            //OEBPS/content.opf
            let itemPath = rootfile.getAttribute("full-path");
            console.log(basePath + itemPath);
            let opf = await (await fetch(basePath + itemPath)).blob();
            fileList.push({ file: opf, path: itemPath });
            let opfParsed = new DOMParser().parseFromString(await opf.text(), "text/xml");
            console.log(opfParsed);
            await sleep(100);
            SakiProgress.setPercent(20);
            SakiProgress.setText("正在索引文件...");
            let itemList = opfParsed.getElementsByTagName("item");
            let baseItemPath = basePath + itemPath.substring(0, itemPath.lastIndexOf("/")) + "/";
            let zipPath = itemPath.substring(0, itemPath.lastIndexOf("/")) + "/"
            for (let i = 0; itemList[i]; i++) {
                SakiProgress.setPercent(20 + i / itemList.length * 60);
                SakiProgress.setText("正在下载文件[" + i + "/" + itemList.length + "]...");
                let item = itemList[i];
                if (item.getAttribute("href")) {
                    let itemBlob = await (await fetch(baseItemPath + item.getAttribute("href"))).blob();
                    let path = zipPath + item.getAttribute("href");
                    let type = item.getAttribute("properties") || "normal";
                    fileList.push({ file: itemBlob, path: path, type: type });
                }
            };
            console.log(fileList);
            SakiProgress.setPercent(80);
            SakiProgress.setText("正在解密文本流...");
            await sleep(50);
            let advancedDecrypt = "0";
            let removeList = []
            window.addEventListener("message", e => {
                if (e.data.action == "remove") {
                    removeList.push(e.data.filename);
                };
            })
            for (const file of fileList) {
                if (file.path.endsWith(".xhtml")) {
                    let fileBuffer = forge.util.createBuffer((await file.file.arrayBuffer()));
                    let decryptor = forge.cipher.createDecipher("AES-ECB", key);
                    let output
                    if (decryptor.start(), decryptor.update(fileBuffer), !decryptor.finish()){
                        //解密失败,推测为未加密文件
                        output=await file.file.text();
                    }else{
                        output = decryptor.output.toString();
                    };
                    if (output.indexOf("//<![CDATA[") > 0) {
                        //触发进阶解密:CANVAS解密模式
                        if (advancedDecrypt === "0") {
                            advancedDecrypt = confirm("检测到EPUB自身已被混淆或加密,是否启用进阶解密?\n该功能不稳定,可能无法正常工作\n启用该功能可以让生成的EPUB文件被更多软件支持\n但会延长十倍甚至九倍解密时间\n进阶解密必须保持浏览器处于前台,严禁最小化或切换到其他标签页\n若解密过程失去响应,请打开F12提交日志反馈")
                        }
                        if (advancedDecrypt) {
                            SakiProgress.setText("初始化进阶解密组件...");
                            let urlList = [];
                            fileList.forEach(file => {
                                urlList.push({
                                    filename: file.path.substr(file.path.lastIndexOf("/") + 1),
                                    url: URL.createObjectURL(file.file)
                                })
                            });
                            console.log(urlList);
                            let decryptFrame = document.createElement("iframe");
                            decryptFrame.frameBorder = 0;
                            decryptFrame.style = "padding:100%;z-index:9999;position:fixed;width:100%;margin-top:0px;height:100%;left:0px;right:0px;top:0px;";
                            document.body.appendChild(decryptFrame);
                            SakiProgress.setText("加载文档数据...");
                            let xhtmlParsed = new DOMParser().parseFromString(output, "text/html");
                            console.log(xhtmlParsed);
                            let scriptsBackup = [];
                            for (; xhtmlParsed.getElementsByTagName("script")[0];) {
                                scriptsBackup.push(xhtmlParsed.getElementsByTagName("script")[0].outerHTML);
                                xhtmlParsed.getElementsByTagName("script")[0].remove();
                            };
                            let origincss = "";
                            [].forEach.call(xhtmlParsed.head.children, node => {
                                if (node.rel == "stylesheet") {
                                    console.log(node);
                                    origincss = node.getAttribute("href");
                                    let cssname = node.href;
                                    cssname = cssname.substr(cssname.lastIndexOf("/") + 1);
                                    urlList.forEach(file => {
                                        if (file.filename == cssname) {
                                            node.href = file.url;
                                        }
                                    })
                                }
                            });
                            let hookScript = document.createElement("script");
                            hookScript.innerHTML = `
			console.log("Salmon Advanced Decrypt Mode:0x1");
			window.addEventListener("message", e => {
				console.log(e);
				switch (e.data.method) {
					case "file": {
                        console.log("Filelist Loaded.");
						window.list = e.data.list;
						break;
					};
                    case "restore":{
                        //还原css
                        [].forEach.call(document.head.children,node=>{
                                if(node.rel=="stylesheet"){
                                    console.log(node);
                                    node.href=e.data.css;
                                }
                            });
                        break;
                    };
					case "convert": {
						//用图片取代canvas
						[].forEach.call(document.getElementsByTagName("canvas"), obj => {
							let img = document.createElement("img");
							document.body.appendChild(img);
							img.src = obj.toDataURL("image/jpeg", 0.95);
							img.width = obj.width;
							img.height = obj.height;
							obj.remove()
						});
						//清理所有不该出现在EPUB里的元素
						for(;document.getElementsByTagName("script")[0];){
							document.getElementsByTagName("script")[0].remove();
						};
                        break;
					};
				}
			}, false);
			var valueProp = Object.getOwnPropertyDescriptor(Image.prototype, 'src');
			Object.defineProperty(Image.prototype, 'src', {
				set: function (image) {
					console.log("Hook Image Loader...");
					let filename = image.substr(image.lastIndexOf("/")+1);
					window.list.forEach(file => {
						if (file.filename == filename) {
							image = file.url;
					        console.log("Hook Image Success!");
                            window.parent.postMessage({action:"remove",filename:filename}, "*");
				     	}
					})
					valueProp.set.call(this, image);
				}
			});
			(draw => {
				CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) {
					console.log([sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight]);
					draw.call(this, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
				};
			})(CanvasRenderingContext2D.prototype.drawImage);
                            `
                            xhtmlParsed.head.appendChild(hookScript);
                            let docFrame = xhtmlParsed.documentElement.outerHTML;
                            let decryptDoc = decryptFrame.contentDocument;
                            //debugger
                            decryptDoc.open();
                            decryptDoc.write(docFrame);
                            await sleep(10);
                            decryptFrame.contentWindow.postMessage({
                                method: "file",
                                list: urlList
                            }, "*");
                            SakiProgress.setText("加载全文数据...");
                            await sleep(10);
                            scriptsBackup.forEach(script => {
                                decryptDoc.write(script);
                            });
                            SakiProgress.setText("渲染页面...");
                            await sleep(10);
                            //debugger
                            decryptFrame.contentWindow.postMessage({
                                method: "convert"
                            }, "*");
                            decryptDoc.close();
                            SakiProgress.setText("解密页面...");
                            await sleep(10);
                            decryptFrame.contentWindow.postMessage({
                                method: "restore",
                                css: origincss
                            }, "*");
                            await sleep(10);
                            output = decryptFrame.contentDocument.documentElement.outerHTML;
                            SakiProgress.setText("保存页面...");
                            await sleep(10);
                            document.body.removeChild(decryptFrame);
                        }
                    }
                    file.file = new Blob([output]);
                }
            };
            SakiProgress.setPercent(84);
            SakiProgress.setText("正在清理...");
            await sleep(100);
            let emptyfile = await (await fetch("data:image/jpeg,1")).blob();
            removeList.forEach(remove => {
                fileList.forEach(file => {
                    if (file.path.substr(file.path.lastIndexOf("/") + 1) == remove && file.type == "normal") {
                        file.file = emptyfile;
                    }
                });
            });
            SakiProgress.setPercent(85);
            SakiProgress.setText("正在生成文件结构...");
            await sleep(100);
            let zip = new JSZip();
            fileList.forEach(file => zip.file(file.path, file.file, { binary: true }));
            await sleep(100);
            SakiProgress.setPercent(90);
            SakiProgress.setText("正在打包...");
            let zipFile = await zip.generateAsync({
                type: "blob",
                compression: "DEFLATE",
                compressionOptions: {
                    level: 9
                }
            });
            SakiProgress.setPercent(97);
            SakiProgress.setText("正在导出...");
            await sleep(2000);
            dlFile(URL.createObjectURL(zipFile), document.title + ".epub");
            SakiProgress.clearProgress();
            SakiProgress.setText("下载成功!");
            await sleep(3000);
            SakiProgress.hideDiv();
            alert("恭喜下载成功!\n如果对成品满意的话,记得给脚本好评\n除了关注琴梨梨外,也别忘了关注温柔可爱铸币的塔宝@帅比笙歌超可爱OvO,本脚本开发过程全程以塔宝的直播作为BGM")
        } else {
            SakiProgress.clearProgress();
            SakiProgress.hideDiv();
            console.log("User Cancel.");
        }
    }
    let injectWorker = `self.addEventListener("message",(e)=>{console.log(e.data);})`;
    const originWorker = window.Worker
    window.Worker = function (aurl, options) {
        console.log("捉住Worker请求了喵!")
        console.log(aurl)
        var oReq = new XMLHttpRequest();
        oReq.open("GET", aurl, false);
        oReq.send();
        const mergedWorker = URL.createObjectURL(new Blob([injectWorker + oReq.response]))
        console.log("给Worker加点料!寄汤来咯!")
        let hookWorker = new originWorker(mergedWorker, options);
        let originMessage = hookWorker.postMessage;
        hookWorker.postMessage = (msg, list) => {
            console.log(msg);
            if (msg.token && !key) {
                key = Base64.atob(msg.token);
                SakiProgress.setText("已得到密钥!正在准备文档信息...");
                SakiProgress.setPercent(10);
                dlHyRead(key, msg.url.substr(0, msg.url.indexOf("_epub") + 6))
            }
            return originMessage.call(hookWorker, msg, list);
        }
        hookWorker.addEventListener('message', (event) => {
            console.log(hookWorker);
            console.log(event.data);
        }, true);
        return hookWorker;
    };
})();