Greasy Fork is available in English.

melonbooks图源获取

melonbooks阅读器图源自动下载

// ==UserScript==
// @name         melonbooks图源获取
// @namespace    summer-script
// @version      0.6
// @description  melonbooks阅读器图源自动下载
// @author       summer
// @match        https://www.melonbooks.co.jp/viewer/*
// @match        https://www.melonbooks.co.jp/fromagee/viewer/*
// @icon         https://www.melonbooks.co.jp/favicon.ico
// @license      GPL-3.0
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @connect      cloudfront.net
// ==/UserScript==

(function () {
    'use strict';

    // 界面按钮
    var ui = {
        btn: null,
        appendStartBtn: function () {
            var btn = document.createElement('button');
            btn.innerText = '下载图源';
            btn.style.position = 'fixed';
            btn.style.top = '20px';
            btn.style.right = '20px';
            btn.style.zIndex = '10001';
            btn.style.padding = '8px';
            btn.style.background = '#fff';
            btn.style.border = '1px solid #aaa';
            btn.style.borderRadius = '4px';
            btn.style.minWidth = '112px';
            btn.style.color = '#000';
            document.body.appendChild(btn);
            this.btn = btn;
        },
        btnClick: function (callback) {
            var btn = this.btn;
            btn.onclick = function () {
                callback(btn);
            };
        },
        updateBtnText: function (text) {
            this.btn.innerText = text;
        }
    };

    // 加载图像, 获取书籍相关参数
    var loader = {
        checkValidity: function () {
            if (!unsafeWindow.GUARDIAN_SERVER) {
                return false;
            }
            if (!unsafeWindow.book_data) {
                return false;
            }
            if (!unsafeWindow.pages_data) {
                return false;
            }
            if (!unsafeWindow.pages_data.keys) {
                return false;
            }

            return true;
        },
        checkVersion: function () {
            var verSupport = [0, 3, 4, 5];
            var bookData = pageUtil.getBookData();
            var verCurrent = bookData.version;
            if (!verCurrent) {
                verCurrent = 0;
            }
            return verSupport.includes(verCurrent);
        },
        getPageKey: function (page) {
            return unsafeWindow.pages_data.keys[page - 1];
        },
        getMaxPage: function () {
            return pageUtil.getBookData().page_count;
        },
        getBookName: function () {
            var bookName = pageUtil.getBookData().title;
            if (!bookName) {
                bookName = document.title;
            }

            return bookName;
        },
        loadPage: function (page, callback) {
            var img = new Image();
            var imgKey = this.getPageKey(page);
            if (callback) {
                img.onload = function () {
                    callback(img, imgKey);
                };
            }
            imgUrlUtil.getImageUrl(page).then(function (imgSrc) {
                // console.log(imgSrc);
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: imgSrc,
                    responseType: 'blob',
                    onload: function (resp) {
                        fileDecryptor.decrypt(resp.response, imgKey).then(function (fileBlob) {
                            img.src = URL.createObjectURL(fileBlob);
                        });
                    }
                });
            });
        }
    };

    // 绘制并下载正确的图像
    var render = {
        canvas: null,
        ctx: null,
        drawImage: function (img, coorArr) {
            var canvas = document.createElement('canvas');
            canvas.width = img.naturalWidth;
            canvas.height = img.naturalHeight;
            var ctx = canvas.getContext('2d');
            this.drawOrigin(ctx, img, coorArr);
            URL.revokeObjectURL(img.src);
            this.canvas = canvas;
            this.ctx = ctx;
        },
        downloadImage: function (filename) {
            if (!this.canvas) {
                return;
            }
            var fullname = filename + '.png';
            this.canvas.toBlob(function (blob) {
                if (!blob) {
                    console.log("blob null");
                    return;
                }
                var blobURL = URL.createObjectURL(blob);
                GM_download({
                    name: fullname,
                    url: blobURL,
                    onload: function () {
                        URL.revokeObjectURL(blobURL);
                    },
                });
            });
        },
        drawOrigin: function (ctx, img, coorArr) {
            var blockEachLine = Math.floor(img.naturalWidth / 96);
            ctx.drawImage(img, 0, 0);
            for (var i = 0; i < coorArr.length; i++) {
                var dstX = Math.floor(coorArr[i] % blockEachLine) * 96;
                var dstY = Math.floor(coorArr[i] / blockEachLine) * 128;
                var srcX = Math.floor(i % blockEachLine) * 96;
                var srcY = Math.floor(i / blockEachLine) * 128;
                ctx.drawImage(img, srcX, srcY, 96, 128, dstX, dstY, 96, 128);
            }
        }
    };

    // 原网页复制过来的洗牌方法
    // 每页对应一个key, key作为随机种子还原真实顺序
    var randomizer = {
        PARAM_A: 1103515245,
        PARAM_B: 12345,
        RAND_MAX: 32767,

        init: function (e) {
            this.next = this._str_to_int(e);
        },
        rand: function (t) {
            var n;
            return null != t ? (n = t + 1,
                Math.floor(this._next_int() / (Math.floor(this.RAND_MAX / n) + 1))) : this._next_int();
        },
        shuffle: function (e) {
            var t, n, i, r, o, s;
            o = [].concat(e);
            r = o.length;
            for (t = i = 0; 0 <= r ? i < r : i > r; t = 0 <= r ? ++i : --i)
                n = this.rand(o.length - 1),
                s = o[n],
                o[n] = o[t],
                o[t] = s;
            return o;
        },
        _next_int: function () {
            this.next = (this.next * this.PARAM_A + this.PARAM_B) % (this.RAND_MAX + 1);
            return this.next;
        },
        _str_to_int: function (e) {
            var t, n, i, r, o, s;
            if (s = 0,
                null != e)
                for (t = e.split(""); t.length > 0;)
                    n = t.shift(),
                    i = t.shift(),
                    r = n.charCodeAt(0),
                    o = 0,
                    i && (o = i.charCodeAt(0)),
                    s += r << 8 | o;
            return s;
        },
        getImgCoorArr: function (img) {
            var o = Math.floor(img.naturalWidth / 96);
            var s = Math.floor(img.naturalHeight / 128);
            var r = [];

            for (var l = 0; l < o * s; ++l) {
                r.push(l);
            }

            return this.shuffle(r);
        }
    };

    var imgUrlUtil = {
        getImageUrl: async function (page) {
            var bookData = pageUtil.getBookData();
            var imgExt = this.getFileExt();
            var urlPath = await this.getImageUrlPath(page);
            var url = unsafeWindow.GUARDIAN_SERVER + '/';
            url += bookData.s3_key;
            url += urlPath + '.' + imgExt;

            return url;
        },
        getFileExt: function () {
            var bookData = pageUtil.getBookData();
            var imgExt = 'jpg';
            if (bookData.image_extension) {
                imgExt = bookData.image_extension;
            }

            return imgExt;
        },
        getImageUrlPath: async function (page) {
            var bookData = pageUtil.getBookData();
            var path = page;
            if (bookData.page_salt) {
                path = await this.fileHashName(bookData.page_salt, page);
            }
            return path;
        },
        fileHashName: async function (e, n) {
            // e => salt
            // n => page index
            const t = new TextEncoder;
            n = t.encode(n),
            e = t.encode(e),
            e = await crypto.subtle.importKey("raw", e, {
                name: "HMAC",
                hash: {
                    name: "SHA-256"
                }
            }, !1, ["sign"]),
            e = await crypto.subtle.sign("HMAC", e, n);
            {
                n = e;
                let t = "";
                var i = new Uint8Array(n)
                  , o = i.byteLength;
                for (let e = 0; e < o; e++)
                    t += String.fromCharCode(i[e]);
                return btoa(t).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_")
            }
        }
    };

    var pageUtil = {
        bookData: null,
        getScript: function () {
            var script = null;
            document.body.querySelectorAll("script").forEach(function (dom) {
                if (!script && dom.innerHTML.includes('GUARDIAN_SERVER')) {
                    script = dom.innerHTML;
                }
            });

            return script;
        },
        getBookData: function () {
            if (this.bookData) {
                return this.bookData;
            }
            var script = this.getScript();
            var reg = /book_data\s*=\s*(\{.+?\});/;
            var match = reg.exec(script);
            if (!match) {
                return unsafeWindow.book_data;
            }
            var bookData = JSON.parse(match[1]);
            if (
                bookData.s3_key &&
                bookData.s3_key[bookData.s3_key.length - 1] !== "/"
            ) {
                bookData.s3_key += "/";
            }
            this.bookData = bookData;

            return this.bookData;
        }
    };

    var fileDecryptor = {
        aesKey: null,
        decrypt: async function (fileBlob, pageKey) {
            var bookData = pageUtil.getBookData();
            if (this.isEncrypted()) {
                var aesKeyEncrypt = bookData.crypto.data.web;
                fileBlob = await this.aesDecrypt(fileBlob, aesKeyEncrypt, pageKey);
            }
            return fileBlob;
        },
        aesDecrypt: async function (fileBlob, aesKeyEncrypt, pageKey) {
            var pageKeyArr = (new TextEncoder()).encode(pageKey);
            var aesKey = await this.decryptAesKey(aesKeyEncrypt);
            var aesKeyHash = await this.hmacSha256(aesKey, pageKeyArr);
            var aesKeyC = await window.crypto.subtle.importKey("raw", aesKeyHash, {
                name: "AES-CTR",
                length: 256
            }, false, ["decrypt"]);
            var counter = await fileBlob.slice(16, 32).arrayBuffer();
            var fileDataBlob = await fileBlob.slice(32).arrayBuffer();
            var fileDataDecrypt = await window.crypto.subtle.decrypt({
                name: "AES-CTR",
                counter: counter,
                length: 128
            }, aesKeyC, fileDataBlob);
            return new Blob([fileDataDecrypt], {
                type: this.getFileMimeType()
            });
        },
        decryptAesKey: async function (cipherText64) {
            if (this.aesKey) {
                return this.aesKey;
            }
            var rsaKey = 'MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICA'
                       + 'QCy66NYR5lCYGB/bztf+l6V+lZWABTY0sHpWkAway66OKeBm8'
                       + '+yubPSMDtyvu0xCM+WsZz8a35EEOsm1BvQquIWm26pUtQofNX'
                       + 'cwOQg9yIhZDiii51dzrVKjBMOQwu6ANpwJT/7y241fEfYpzwg'
                       + 'HycYPvVjp52PIFsBP68SjABuht+jnfVUhYWb8XhmnznJc8tG/'
                       + 'PArhj0UHOX8TiA/4KkAZ2icnAkO5nSBqTSxBzgJfvdZuKtOdH'
                       + '5LWXmrhkfKljmk0SbzjWloZXr1c2UmUxA0Tl/PkVrfCu5MbN1'
                       + 'u+TWLyJysVj7YtcytnOONg3BYI/icW0pfk/u+kH7Olkp5UaQI'
                       + 'duWQuy7DY/Wa2j54PE/AwWmpigDcT2lEOBj6HJv7tWhh0NLTe'
                       + 'vOt/VFoAtfmdVRkDiKtQXGvLQ2WSGfydOrBzEghAO2iPJA2C+'
                       + 'u8LkEsee2A4OOVoSvW+YujpkjflEM1NY0OI0SbtvQEjUCoqKN'
                       + 'a6n6ynyx0i1+n3fWMYk86wGFUEBaCgNsY8tZaCMPrKZdvBFgG'
                       + 'gFLZSRBwBWTs9UUtaO/jTskqgQboAJoT7EucuyEePrYc5t+66'
                       + 'NUEajFLqsyMlVedG8nT/wxtAsfDShNDeFXfJ8uaRulMRZCK6E'
                       + 'GVuYY2lts6aQ7AQ5Y6bdb36uN4YjR3qzfHnk1oI8iSCzqjGwI'
                       + 'DAQABAoICAB6qA/S9UKbvnZo5lhN89Xj+zo0utmPyuwkjTpz9'
                       + 'waRo0UyFR7N54DfFzGp+Dmi4+dr51c3tHlbMD4E4GQxKPTVir'
                       + 'jSW3YWS35RV+sMrl83hP1OcWfwZ0ThViW6ITxoxyz4tJCojU2'
                       + 'AXTLj08HkQ3cJqB+RsdbRx9ybfo0GqfOh0dR+1krZJq/xuBh'
                       + 'SQdXbxQYWJFCBhgVZRHZmASkaoWk6XanZsx6CmHpGaTk2Izz'
                       + 'HRcNMPs0xe6sY/L3sWKHewB3EX53UWZ1pdChXHES7gTYapaA'
                       + 'JLByfy16SBg/HhBCxQ1YkzvtGlVs7qdJVsUXTJtXIWs0huNF'
                       + 'cUrKb7Wwgv1czTWrdekxx/0oOi91pH37nhfcOgYxYFZSoGtv'
                       + 'QpylhVBhcgKy7xvhHq4zDl6vOjvuDaeSiLEtha4wIwavjBTi'
                       + 'rSUosGUtplvuILZt+EiP9ynEyoZa+1C6LrJ2X9bO+K8cpxYh'
                       + 'WWGHtwm7scICovNwvO3lciOE60LW/wgL9ZnGuCuq06QuCh2S'
                       + 'kIZpTIlJuxVsgAP9vZjgLgUAh+fFZ/sTV6K/34baTsGKavPC'
                       + 'Qo4GWADxqLlGROdFXPbApDsiSbUfQZlkjquh1Cjzuzm0cno/'
                       + '5WY15kTX4Vj+u2/UMSwx8Bc4in8nwa750csFt7+uihrbxhcD'
                       + 'ZwO1AeW9vQt7aSgl0BAoIBAQD5dv4JXmpUXvjTkx+h26YdHZ'
                       + 'I9mFKd9s+rX4KvaQQrCkcqgChmFleiM81LlM+Wn5vm39pI7W'
                       + '/JZ08blA6F5eFRvYQvBu1kHU+ubZLRqna4DzcXaPWo3T8B+9'
                       + '7J1knH7axief0YoksD6eoTMqvqq4CVsWok7KG0Hs/7/Q8oID'
                       + 'lt4IiMYZtZmuEgPeEGSVpsDmWRp9VshljiQigOvAnu3TNrSf'
                       + 'OpFzPFIUzJ3Mpo0kiFJsljfSJolozBb7zHQGUcjBlyDVkQOZ'
                       + 'kcxhLOm+Nvh3imDGdKhRjm22szGjg6+g0LOfH3qxKcaJ1dl7'
                       + 'fBsvXY8AsEUCQCCSiVOjl2hB293tjVAoIBAQC3m4xIjKGQVx'
                       + 'ng24us0uD+7SjYSh++r15mC//MDENoH9U4I6VRGoTUb2MT+8'
                       + 'KpCr1dXnhvgUCI86e8NWnacIsiWc6T9Iwrv02yn3Zphdgi99'
                       + 'T8HXLe4tVxIISRn4JKevkSPfBmNfj4h0iccQjKVsf2fDBlec'
                       + 'kTJcOCC9H1uLwNzb3ot+/MOWfsfwKCoR+Ob6lhATzloBfHYK'
                       + '29qSNMP2FMYklBgp3lhl0KL1osk/yt2aJJvhYkvoHxYcbpxP'
                       + 'NghKgd8q/3EpnGRmRZktTY1VHg1cIVKgGrljzP4yFyD7Lg7f'
                       + 'PEftUT249KVjXPXkAkZ2/wNAKxh5fi4ELP1LLK94QvAoIBAE'
                       + 'DlhLa5Oh4SytZ3ip4XvwIKBFZDvxJa97FUWnH5dt0fgl37Ew'
                       + 'djvo5yvXBxGQPNJ8iK6YVZR2B0oK7C+Hg60j/qdm2pdq45td'
                       + 'XhqXUjzFiblLBhXK7+R3rjpBSLy4vYN6UyqPX0mmE9Q+iUoQ'
                       + 'aecQgALGXIrVRnQ6IBNiUxJN+BruQeLETGNtSlZFm3UW+U2z'
                       + 'VmHO5rkMnjffo/TrI2Fz9M8LdHUu9wd0J4TquwMK965J8eGY'
                       + 'ptx9Y2lDydcvBXPfNep5HB+iPzH0diZGtKKcfAqEpJj63W3O'
                       + '9hXclx7VzDSUAt39ySloWXh3U7chtqbuNDWeqxqT4Q9IvxWK'
                       + '9hPrUCggEBAKYoyYp6YlgKyyuX485yRRXPMFCUvCfH8ujs4Q'
                       + 'Aa9QGNFVupvpkoI59QclyKUT7Drl2J+foHAY0u29RSjkoV4Y'
                       + 'Qju/Rfsl6A0OLetr2GV/RFTmUejW8x3rFzGSXkMXgP08nzbd'
                       + 'RB8d+QJmEVVjwuzuW8u9uJnDOM0GKnKcpy9RSU5dFubD/oj6'
                       + 'kRxAbNo442dRWJlj/EYuCXGIR0RbJiBT6oD92ORDCMKTTnZ3'
                       + 'bCMkBunRSZRtbX5Sa6MtYp24q0YqQ/lYlGNw2ddIEvhRn56x'
                       + 'BKwkp+6mYLH1uPFBxyIpK2JQ3lLhW7c/B1Fltk0y1ewomht/'
                       + 'JLYGP8Sdplhaxy1RcCggEADPWAt8alVeV1aUYYD2xMfjwO3V'
                       + 'D6fDp5s//tXtJtnfXOiE9eQ70XYks4MlrOgR5vKqmPWREdVu'
                       + '+2wIT9VNUzBixPZecSVuk4JhH11saTxql+4Q6pb7rii4tocS'
                       + 'pzQg5p+bqXgiCjICLZsYXb0A4f5LswWB+APVQOTyN3k7NEfP'
                       + 'MXZq2VDuNWuP9+df097625iAAY9B05X06emTySzKPr7oA4Fd'
                       + 'ui6GbzdZmuKoXDgBCo4mDtJ5zMqqa22pmMTHb1kTNpIGlILM'
                       + 'Pg3/7Q42gvtqsXFWeJQmbRoiMakrTURt1y+n+br4vjUSubFc'
                       + 'Qi5Wa3DymTfNsvcXVG+/n3OOEIoQ==';
            var pk = await window.crypto.subtle.importKey(
                "pkcs8",
                this.base64ToUint8(rsaKey),
                { name: "RSA-OAEP", hash: { name: "SHA-256" } },
                false,
                ["decrypt"]
            );
            this.aesKey = await window.crypto.subtle.decrypt(
                { name: "RSA-OAEP" },
                pk,
                this.base64ToUint8(cipherText64)
            );

            return this.aesKey;
        },
        base64ToUint8: function (n) {
            const t = window.atob(n);
            var r = t.length;
            const e = new Uint8Array(r);
            for (let n = 0; n < r; n++) {
                e[n] = t.charCodeAt(n);
            }
            return e.buffer;
        },
        hmacSha256: async function (key, data) {
            var keyC = await crypto.subtle.importKey("raw", key, {
                name: "HMAC",
                hash: { name: "SHA-256" }
            }, false, ["sign"]);
            return crypto.subtle.sign("HMAC", keyC, data);
        },
        getFileMimeType: function () {
            var mimeType = 'image/jpeg';
            var bookData = pageUtil.getBookData();
            if (bookData.image_mime_type) {
                mimeType = bookData.image_mime_type
            }
            return mimeType;
        },
        isEncrypted: function () {
            var bookData = pageUtil.getBookData();
            return bookData.crypto && bookData.crypto.type === 1;
        }
    };

    var downloader = {
        downloadImg: function (img, imgKey, fileName) {
            if (fileDecryptor.isEncrypted()) {
                var blobURL = img.src;
                var fullname = fileName + '.' + this.getFileExtByMimeType();
                // v5版本文件加密但是不打乱
                GM_download({
                    name: fullname,
                    url: blobURL,
                    onload: function () {
                        URL.revokeObjectURL(blobURL);
                    },
                });
            } else {
                randomizer.init(imgKey);
                var imgCoor = randomizer.getImgCoorArr(img);
                render.drawImage(img, imgCoor);
                render.downloadImage(fileName);
            }
        },
        getFileExtByMimeType: function () {
            var bookData = pageUtil.getBookData();
            var ext = 'png';
            var map = {
                'image/png': 'png',
                'image/jpeg': 'jpg',
                'image/gif': 'gif',
            };
            var mimeType = bookData.image_mime_type;
            if (mimeType && map[mimeType]) {
                ext = map[mimeType]
            }
            return ext;
        }
    };

    ui.appendStartBtn();

    if (!loader.checkVersion()) {
        ui.updateBtnText('暂不支持此作品, 点击尝试运行');
    }
    ui.btnClick(function (btn) {
        btn.disabled = true;
        if (!loader.checkValidity()) {
            alert('脚本已失效');
            return;
        }
        taskRun();
    });

    // 顺序处理
    function taskRun(pageNow) {
        if (!pageNow) {
            pageNow = 1;
        }
        var pageMax = loader.getMaxPage();
        var fileNo = pageNow.toString().padStart(pageMax.toString().length, '0');
        var fileName = loader.getBookName() + '_' + fileNo;

        if (pageNow > pageMax) {
            ui.updateBtnText('下载完毕');
            return;
        }
        ui.updateBtnText('下载中: ' + pageNow + '/' + pageMax);
        loader.loadPage(pageNow, function (img, imgKey) {
            downloader.downloadImg(img, imgKey, fileName);
            pageNow++;
            // 防止请求过快延迟1秒
            setTimeout(function () {
                taskRun(pageNow);
            }, 1000);
        });
    }
})();