diff --git a/ehentai_better_viewer.user.js b/ehentai_better_viewer.user.js index c1a537d..dccea32 100644 --- a/ehentai_better_viewer.user.js +++ b/ehentai_better_viewer.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name E-Hentai better viewer // @namespace https://github.com/lifegpc/userscript -// @version 0.2.2 +// @version 0.3.0 // @description Add a viewer to view original picture on website. // @author lifegpc // @match https://*.e-hentai.org/s/*/* @@ -10,14 +10,82 @@ // @grant GM_getResourceText // @grant GM_addElement // @grant GM_addStyle +// @grant GM_getValue +// @grant GM_setValue +// @grant GM_registerMenuCommand // @require https://github.com/lifegpc/viewerjs/raw/main/dist/viewer.min.js +// @require https://github.com/emn178/js-sha512/raw/v0.8.0/build/sha512.min.js +// @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @resource viewercss https://github.com/lifegpc/viewerjs/raw/main/dist/viewer.min.css // @run-at document-start // ==/UserScript== +GM_config.init({ + id: 'e-hentai', + fields: { + enableRedirectAPI: { + type: 'checkbox', + label: 'Enable redirect api. This need a thirdparty server.', + default: false + }, + APIEndpoint: { + type: 'hidden' + }, + tempAPIEndpoint: { + label: 'The endpoint of the API:', + type: 'text', + save: false + }, + APISecret: { + label: 'The API secret:', + type: 'text' + }, + LoadOriginalWhenOpen: { + type: 'checkbox', + label: 'Load original pictures when opening the viewer modal.', + default: false + }, + }, + events: { + init: () => { + GM_config.set("tempAPIEndpoint", GM_config.get("APIEndpoint")) + }, + open: () => { + GM_config.set("tempAPIEndpoint", GM_config.get("APIEndpoint")) + }, + save: (values) => { + console.log(values); + if (GM_config.get('enableRedirectAPI')) { + if (values.tempAPIEndpoint !== null) { + try { + GM_config.set("APIEndpoint", new URL(values.tempAPIEndpoint).toString()); + } catch (e) { + GM_config.set("enableRedirectAPI", false); + console.log(e); + } + } + if (!GM_config.get("APISecret")) { + GM_config.set("enableRedirectAPI", false); + } + } + } + } +}) +GM_registerMenuCommand("Edit Settings", () => { GM_config.open() }, "e"); GM_addStyle(GM_getResourceText("viewercss")); function indirectEval(script) { return eval(`"use strict";${script}`); } +function parse_cookies() { + let cookies = document.cookie.split(";"); + let o = {}; + for (let c of cookies) { + c = c.trim().split("="); + let k = c[0]; + let v = c.slice(1).join("="); + o[k] = v; + } + return o; +} function find_script() { let cols = document.getElementsByTagName("script"); for (let col of cols) { @@ -26,6 +94,140 @@ function find_script() { } } } +/** + * encodeURIComponent as python's quote_plus behavoir + * @param {string} str + * @returns + */ +function py_quote(str) { + let s = encodeURIComponent(str); + while (s.includes('%20')) { + s = s.replace('%20', '+'); + } + while (s.includes('!')) { + s = s.replace('!', '%21'); + } + while (s.includes("'")) { + s = s.replace("'", '%27'); + } + while (s.includes('(')) { + s = s.replace('(', '%28'); + } + while (s.includes(')')) { + s = s.replace(')', '%29'); + } + while (s.includes('*')) { + s = s.replace('*', '%2A'); + } + return s; +} +class URLParams extends URLSearchParams { + toString() { + let r = ""; + for (let p of this.entries()) { + if (r.length) r += "&"; + r += py_quote(p[0]) + "=" + py_quote(p[1]); + } + return r; + } +} +/** + * 发送GET请求 + * @param {string} url 网站 + * @param {Object|Array|string>} data 字典 + * @param {(content: string)=>void} callback 回调函数 + * @param {()=>void} failedCallback 失败回调函数 + * @param {Object} headers HTTP头部 + */ +function get(url, data, callback, failedCallback, headers) { + var xhr = new XMLHttpRequest(); + var uri = new URL(url, window.location.href); + if (data == undefined); + else if (Array.isArray(data)) { + for (let i = 0; i < data.length; i++) { + var pair = data[i]; + if (Array.isArray(pair)) { + uri.searchParams.append(pair[0], pair.length > 1 ? pair[1] : ""); + } else if (typeof pair == "string") { + uri.searchParams.append(pair, ""); + } + } + } else { + Object.getOwnPropertyNames(data).forEach((key) => { + if (typeof data[key] == "string") + uri.searchParams.append(key, data[key]); + }) + } + xhr.open("GET", uri.href); + if (callback != undefined) xhr.onload = () => { + callback(xhr.responseText); + }; + if (failedCallback != undefined) xhr.onerror = failedCallback; + if (headers != undefined) { + Object.getOwnPropertyNames(headers).forEach((key) => { + if (typeof headers[key] == "string") + xhr.setRequestHeader(key, headers[key]) + }) + } + try { + xhr.send(); + } catch (e) { + if (failedCallback != undefined) failedCallback(); + } +} +/** + * Generate sign for data + * @param {Object>|FormData} data Data + * @param {string} secret Secret + * @param {(data: string) => string|undefined} hash The hash function + * @returns {string} + */ +function genGetSign(data, secret, hash) { + if (!hash) hash = sha512; + /**@type {Array<{k: string, v: string}>} */ + let arr = []; + if (data.constructor.name == "FormData") { + for (let pair of data.entries()) { + if (typeof pair[1] != "string") continue; + arr.push({ k: pair[0], v: pair[1] }); + } + } else { + Object.getOwnPropertyNames(data).forEach((key) => { + let v = data[key]; + if (typeof v == "string") arr.push({ k: key, v: v }); + else if (Array.isArray(v)) { + v.forEach((v) => { + if (typeof v == "string") arr.push({ k: key, v: v }); + }) + } + }) + } + arr.sort((a, b) => { + return a.k == b.k ? a.v == b.v ? 0 : a.v > b.v ? 1 : -1 : a.k > b.k ? 1 : -1; + }) + let par = new URLParams(); + arr.forEach((v) => { + par.append(v.k, v.v); + }) + return hash(secret + par.toString()); +} +function redirect_url(url) { + return new Promise((resolve, reject) => { + let api = GM_config.get("APIEndpoint"); + let screct = GM_config.get("APISecret"); + if (!api || !screct) { + reject("Endpoint or screct not found."); + return; + } + let data = {'t': url, 'ar': '0', 'rraj': '1', 'c': JSON.stringify(parse_cookies()), 'r': 'https://e-hentai.org/' }; + data['sign'] = genGetSign(data, screct); + get(api, data, (c) => { + resolve(JSON.parse(c)['location']); + }, () => { + reject("Error"); + }) + }) +} let base_url = undefined; let cur_img = null; let cur_viewer = null; @@ -34,6 +236,8 @@ let api_url = undefined; let gid = undefined; let showkey = undefined; let img_keys = {}; +let original_url = null; +let redirected_url = null; let cur_page = null; let total_page = null; function get_api_url() { @@ -104,6 +308,10 @@ async function loadPage(page) { let xhr = await api_call({ method: "showpage", gid, page, imgkey, showkey }); let a = JSON.parse(xhr.responseText); if (cur_viewer) cur_viewer.destroy(); + cur_img = null; + cur_viewer = null; + original_url = null; + redirected_url = null; document.getElementById("i1").style.width = a.x + "px"; document.getElementById("i2").innerHTML = a.n + a.i; document.getElementById("i3").innerHTML = a.i3; @@ -126,6 +334,20 @@ function replaceLink(a) { na.addEventListener("click", () => { loadPage(num); }); a.replaceWith(na); } +async function loadOriginalImage() { + let url = original_url; + if (!url) return false; + if (GM_config.get("enableRedirectAPI")) { + if (redirected_url) { + url = redirected_url; + } else { + redirected_url = await redirect_url(url); + url = redirected_url; + } + } + cur_img.src = url; + return true; +} let load = () => { let img = document.getElementById("img"); if (img == null) { @@ -150,13 +372,15 @@ let load = () => { let original = document.querySelector("#i7>a"); if (original != null) { let ourl = original.href; - console.log(ourl); - options.url = ourl; + original_url = ourl; } console.log(options); let viewer = new Viewer(img, options); let click = async () => { img.removeEventListener("click", click); + if (GM_config.get("LoadOriginalWhenOpen")) { + await loadOriginalImage(); + } await viewer.init(); console.log("Inited complete"); await viewer.show();