melonbooks图源获取

melonbooks阅读器图源自动下载

// ==UserScript==
// @name         melonbooks图源获取
// @namespace    summer-script
// @version      0.5
// @description  melonbooks阅读器图源自动下载
// @author       summer
// @match        https://www.melonbooks.co.jp/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];
            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) {
            // 因为画布被污染后无法下载, 改用可跨域的XHR获取图片
            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) {
                        img.src = URL.createObjectURL(resp.response);
                    }
                });
            });
        }
    };

    // 绘制并下载正确的图像
    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';
            GM_download(this.canvas.toDataURL(), fullname);
        },
        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();
            if (bookData.version >= 4) {
                return await this.getImageUrlV4(page);
            } else {
                return await this.getImageUrlV3(page);
            }
        },
        getImageUrlV3: async function (page) {
            var url = unsafeWindow.GUARDIAN_SERVER + '/';
            var bookData = pageUtil.getBookData();
            url += bookData.s3_key;
            url += page + '.jpg';

            return url;
        },
        getImageUrlV4: async function (page) {
            var imgExt = 'jpg';
            var bookData = pageUtil.getBookData();
            if (bookData.image_extension) {
                imgExt = bookData.image_extension;
            }
            var salt = bookData.page_salt;
            var pageSign = await this.fileNameSign(salt, page);
            var url = unsafeWindow.GUARDIAN_SERVER + '/';
            url += bookData.s3_key;
            url += pageSign + '.' + imgExt;

            return url;
        },
        fileNameSign: 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) {
                var script = this.getScript();
                var reg = /book_data\s*=\s*(\{.+?\});/;
                var match = reg.exec(script);
                if (! match) {
                    return unsafeWindow.book_data;
                }
                this.bookData = JSON.parse(match[1]);
            }

            return this.bookData;
        }
    };

    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) {
            randomizer.init(imgKey);
            var imgCoor = randomizer.getImgCoorArr(img);
            render.drawImage(img, imgCoor);
            render.downloadImage(fileName);
            pageNow++;
            // 防止请求过快延迟1秒
            setTimeout(function () {
                taskRun(pageNow);
            }, 1000);
        });
    }
})();