怠惰小説下載器 ZIP 擴充

下載時分章節儲存 TXT 並打包為 ZIP

// ==UserScript==
// @name         DownloadAllContent ZIP addon
// @name:zh-CN   怠惰小说下载器 ZIP 扩展
// @name:zh-TW   怠惰小説下載器 ZIP 擴充
// @namespace    hoothin
// @version      0.6
// @description  Save content as ZIP for DownloadAllContent
// @description:zh-CN  下载时分章节保存 TXT 并打包为 ZIP
// @description:zh-TW  下載時分章節儲存 TXT 並打包為 ZIP
// @author       hoothin
// @match        *://*/*
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @require      https://unpkg.com/jszip@3.7.1/dist/jszip.js
// ==/UserScript==

(function() {
    'use strict';
    const threadNum = 20;//圖片下載綫程數
    const ocrApi = "";
    //const ocrApi = "http://127.0.0.1:416/?img=%t";//ocr 識別 api
    var _unsafeWindow = (typeof unsafeWindow == 'undefined') ? window : unsafeWindow;
    var zipTips, loadingImgs = [], loadingIndex = 0, loadedNum = 0, ocrNum = 0, zip, blobDict = {}, ocrDict = {};

    async function dataURLToBlob(dataurl, ext = "jpeg") {
        return await new Promise((resolve) => {
            if (!dataurl) return resolve(null);
            var canvas = document.createElement('CANVAS');
            var ctx = canvas.getContext('2d');
            var img = new Image();
            img.setAttribute("crossOrigin", "anonymous");
            img.onload = function() {
                canvas.width = img.width;
                canvas.height = img.height;
                ctx.drawImage(img, 0, 0);
                var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                for(var i = 0; i < imageData.data.length; i += 4) {
                    if(imageData.data[i + 3] == 0) {
                        imageData.data[i] = 255;
                        imageData.data[i + 1] = 255;
                        imageData.data[i + 2] = 255;
                        imageData.data[i + 3] = 255;
                    }
                }
                ctx.putImageData(imageData, 0, 0);
                canvas.toBlob(blob => {
                    resolve(blob);
                }, `image/${ext}`);
            };
            img.onerror = function() {
                resolve(null);
            };
            img.src = dataurl;
        });
    }

    async function blobToDataURL(blob) {
        return await new Promise((resolve) => {
            var a = new FileReader();
            a.readAsDataURL(blob);
            a.onload = function (e) {
                resolve(e.target.result);
            };
            a.onerror = function (e) {
                resolve(null);
            };
        });
    }

    async function urlToBlob(url) {
        return await new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType:'blob',
                timeout:20000,
                headers: {
                    origin: location.origin,
                    referer: location.href,
                    accept: "*/*"
                },
                onload: async function(d) {
                    resolve(d.response);
                },
                onerror: function() {
                    resolve(null);
                },
                ontimeout: function() {
                    resolve(null);
                }
            });
        })
    }

    async function downloadImage(cb) {
        if (loadedNum >= loadingImgs.length || loadingIndex >= loadingImgs.length) return;
        let i = loadingIndex;
        loadingIndex++;
        let src = loadingImgs[i];
        let blob = await urlToBlob(src);
        if (blob) {
            zip.file("imgs/" + i + ".jpg", blob);
            zipTips.innerText = `Download images ${loadedNum + "/" + loadingImgs.length}...`;
            blobDict[src] = blob;
        }
        loadedNum++;
        if (loadedNum >= loadingImgs.length) {
            if (ocrApi) {
                loadingIndex = 0;
                let length = Math.min(loadingImgs.length, threadNum);
                for (let i = 0; i < length; i++) {
                    recognizeImage(cb);
                }
            } else {
                cb();
            }
        } else downloadImage(cb);
    }

    async function recognizeImage(cb) {
        if (ocrNum >= loadingImgs.length || loadingIndex >= loadingImgs.length) return;
        let i = loadingIndex;
        loadingIndex++;
        let src = loadingImgs[i];
        let dataURL = await blobToDataURL(blobDict[src]);
        let blob = await dataURLToBlob(dataURL, "jpeg");
        dataURL = await blobToDataURL(blob);
        let result = await new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: ocrApi.replace("%t", encodeURIComponent(dataURL.replace("data:image/jpeg;base64,", ""))),
                timeout:20000,
                onload: function(d) {
                    resolve(d.response);
                },
                onerror: function() {
                    resolve(null);
                },
                ontimeout: function() {
                    resolve(null);
                }
            });
        });
        ocrNum++;
        zipTips.innerText = `Recognize images ${ocrNum + "/" + loadingImgs.length}...`;
        if (result) {
            ocrDict[i] = result;
        }
        if (ocrNum >= loadingImgs.length) {
            cb();
        } else recognizeImage(cb);
    }

    function downloadImages(cb) {
        let length = Math.min(loadingImgs.length, threadNum);
        if (length == 0) {
            return cb();
        }
        for (let i = 0; i < length; i++) {
            downloadImage(cb);
        }
    }

    const mdImgReg = /!\[img\]\((http.+?)\)/;
    _unsafeWindow.downloadAllContentSaveAsZip = async (rCats, info, callback) => {
        loadingIndex = 0;
        loadedNum = 0;
        ocrNum = 0;
        if (!zipTips) {
            zipTips = document.createElement("div");
        }
        if (!zipTips.parentNode) {
            let txtDownWords = _unsafeWindow.txtDownWords;
            if (txtDownWords) {
                txtDownWords.appendChild(zipTips);
            }
        }
        console.log("Begin compress to ZIP...");
        zipTips.innerText = "Begin compress to ZIP...";
        zip = new JSZip();
        zip.file("readme.txt", info);
        let zipTemp = [];
        for (let i = 0; i < rCats.length; i++) {
            let cat = rCats[i];
            if (!cat) continue;
            let catTitle = cat.match(/.*?\r\n/);
            if (!catTitle) continue;
            catTitle = catTitle[0].trim();
            cat = cat.replace(catTitle, "").replace(/^[\n\r]+/, "");
            let imgMatch = cat.match(mdImgReg), hasImg = false;
            while (imgMatch) {
                let index = loadingImgs.indexOf(imgMatch[1]);
                if (index == -1) {
                    index = loadingImgs.length;
                    loadingImgs.push(imgMatch[1]);
                }
                cat = cat.replace(imgMatch[0], `![img](imgs/${index}.jpg)`);
                imgMatch = cat.match(mdImgReg);
                hasImg = true;
            }
            zipTemp.push({title: (i + 1) + " - " + catTitle.replace(/[\*\/:<>\?\\\|\r\n]/g, "_").slice(0, 50), hasImg: hasImg, content: cat});
        }
        downloadImages(() => {
            zipTemp.forEach(d => {
                if (ocrApi) {
                    d.hasImg = false;
                    Object.keys(ocrDict).forEach(key => {
                        d.content = d.content.replace(new RegExp(`!\\[img\\]\\(imgs/${key}\\.jpg\\)`, 'g'), ocrDict[key]);
                    });
                }
                zip.file(d.title + (d.hasImg ? ".md" : ".txt"), d.content);
            });
            zip.generateAsync({type: "blob", compression: "DEFLATE"}, meta => {zipTips.innerText = "percent: " + ((meta && meta.percent && meta.percent.toFixed(2)) || "100") + "%"}).then(function(content){
                callback(content);
                zipTips.innerText = "";
            })
        });
    }
})();