// ==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);
});
}
})();