// ==UserScript==
// @name [E/Ex-Hentai] Downloader
// @name:zh-TW [E/Ex-Hentai] 下載器
// @name:zh-CN [E/Ex-Hentai] 下载器
// @name:ja [E/Ex-Hentai] ダウンローダー
// @name:ko [E/Ex-Hentai] 다운로더
// @name:en [E/Ex-Hentai] Downloader
// @version 0.0.15
// @author HentaiSaru
// @description 漫畫頁面創建下載按鈕, 可切換 (壓縮下載 | 單圖下載), 無須複雜設置一鍵點擊下載, 自動獲取(非原圖)進行下載
// @description:zh-TW 漫畫頁面創建下載按鈕, 可切換 (壓縮下載 | 單圖下載), 無須複雜設置一鍵點擊下載, 自動獲取(非原圖)進行下載
// @description:zh-CN 漫画页面创建下载按钮, 可切换 (压缩下载 | 单图下载), 无须复杂设置一键点击下载, 自动获取(非原图)进行下载
// @description:ja マンガページにダウンロードボタンを作成し、(圧缩ダウンロード | シングルイメージダウンロード)を切り替えることができ、复雑な设定は必要なく、ワンクリックでダウンロードできます。自动的に(オリジナルではない)画像を取得してダウンロードします
// @description:ko 만화 페이지에 다운로드 버튼을 만들어 (압축 다운로드 | 단일 이미지 다운로드)를 전환할 수 있으며, 복잡한 설정이 필요하지 않고, 원클릭 다운로드 기능으로 (원본이 아닌) 이미지를 자동으로 가져와 다운로드합니다
// @description:en Create download buttons on manga pages, switchable between (compressed download | single image download), without the need for complex settings, one-click download capability, automatically fetches (non-original) images for downloading
// @connect *
// @match *://e-hentai.org/g/*
// @match *://exhentai.org/g/*
// @icon https://e-hentai.org/favicon.ico
// @license MIT
// @namespace https://greasyfork.org/users/989635
// @run-at document-end
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_download
// @grant GM_addElement
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @require https://update.greasyfork.org/scripts/473358/1237031/JSZip.js
// @require https://update.greasyfork.org/scripts/487608/1330066/GrammarSimplified.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// ==/UserScript==
(function() {
var Language, OriginalTitle, CompressMode, ModeDisplay,
lock = false, api = new API(), url = document.URL.split("?p=")[0];
const Config = {
ReTry: 15, // 下載錯誤重試次數, 超過這個次數該圖片會被跳過
DeBug: false,
}
class Main {
constructor() {
this.E = /https:\/\/e-hentai\.org\/g\/\d+\/[a-zA-Z0-9]+/;
this.Ex = /https:\/\/exhentai\.org\/g\/\d+\/[a-zA-Z0-9]+/;
this.Ran = (u) => {return this.E.test(u) || this.Ex.test(u)}
this.Css = (a, e, ex) => {
let css = location.hostname != "exhentai.org" ? e : ex;
api.AddStyle(`${a}${css}`, "button-style");
}
}
static async Match() {
const self = new Main();
if (self.Ran(url)) {
Language = display_language(navigator.language);
OriginalTitle = document.title;
self.ButtonCreation();
api.Menu({[Language.MN_01]: ()=> self.DownloadModeSwitch()})
}
}
async ButtonCreation() {
CompressMode = api.store("get", "CompressedMode", []);
ModeDisplay = CompressMode ? Language.DM_01 : Language.DM_02;
this.Css(`
.Download_Button {
float: right;
width: 9rem;
cursor: pointer;
font-weight: bold;
line-height: 20px;
border-radius: 5px;
position: relative;
padding: 1px 5px 2px;
font-family: arial,helvetica,sans-serif;
}
`,`
.Download_Button {
color: #5C0D12;
border: 2px solid #9a7c7e;
background-color: #EDEADA;
}
.Download_Button:hover {
color: #8f4701;
border: 2px dashed #B5A4A4;
}
.Download_Button:disabled {
color: #B5A4A4;
border: 2px dashed #B5A4A4;
cursor: default;
}
`,`
.Download_Button {
color: #b3b3b3;
border: 2px solid #34353b;
background-color: #2c2b2b;
}
.Download_Button:hover {
color: #f1f1f1;
border: 2px dashed #4f535b;
}
.Download_Button:disabled {
color: #4f535b;
border: 2px dashed #4f535b;
cursor: default;
}
`);
try {
let download_button = GM_addElement(api.$$("#gd2"), "button", {
id: "ExDB", class: "Download_Button"
});
download_button.textContent = lock ? Language.DM_03 : ModeDisplay;
download_button.disabled = lock ? true : false;
api.AddListener(download_button, "click", () => {
lock = true;
download_button.disabled = true;
download_button.textContent = Language.DS_01;
download.HomeData(download_button);
}, {capture: true, passive: true});
} catch {}
}
async DownloadModeSwitch() {
CompressMode?
api.store("set", "CompressedMode", false):
api.store("set", "CompressedMode", true);
api.$$("#ExDB").remove();
this.ButtonCreation();
}
}
class Settings {
constructor() {
this.MAX_CONCURRENCY = 12;
this.MIN_CONCURRENCY = 3;
this.TIME_THRESHOLD = 350;
this.MAX_Delay = 3500;
this.Home_ID = 100;
this.Home_ND = 80;
this.Image_ID = 30;
this.Image_ND = 24;
this.Download_IT = 5;
this.Download_ID = 300;
this.Download_ND = 240;
this.Compr_Level = 5;
this.Enforce = false;
this.DownloadMode;
this.Show = "";
}
Dynamic(Time, Delay, Thread=null, MIN_Delay) {
let ResponseTime = (Date.now() - Time), delay, thread;
if (ResponseTime > this.TIME_THRESHOLD) {
delay = Math.floor(Math.min(Delay * 1.1, this.MAX_Delay));
if (Thread != null) {
thread = Math.floor(Math.max(Thread * (this.TIME_THRESHOLD / ResponseTime), this.MIN_CONCURRENCY));
return [delay, thread];
} else {return delay}
} else {
delay = Math.ceil(Math.max(Delay * 0.9, MIN_Delay));
if (Thread != null) {
thread = Math.ceil(Math.min(Thread * 1.2, this.MAX_CONCURRENCY));
return [delay, thread];
} else {return delay}
}
}
}
class Download extends Settings {
constructor() {
super();
this.Total = (page) => {return Math.ceil(+page[page.length - 2].textContent.replace(/\D/g, '') / 20)}
this.FillValue = (page) => {
return Math.max(2, `${page}`.length);
}
this.Mantissa = (str, fill) => {
return `${++str}`.padStart(fill, "0");
}
this.Storage = (key, value=null) => {
let data, result;
if (value) {
sessionStorage.setItem(key, JSON.stringify(Array.from(value.entries())));
result = true;
} else {
data = sessionStorage.getItem(key);
result = data ? new Map(JSON.parse(data)) : false;
}
return result;
}
this.sleep = (delay) => {
return new Promise(resolve => setTimeout(resolve, delay));
}
this.worker = api.WorkerCreation(`
let queue = [], processing = false;
onmessage = function(e) {
queue.push(e.data);
!processing ? (processing = true, processQueue()) : null;
}
async function processQueue() {
if (queue.length > 0) {
const {index, url, time, delay} = queue.shift();
FetchRequest(index, url, time, delay);
setTimeout(processQueue, delay);
} else {processing = false}
}
async function FetchRequest(index, url, time, delay) {
try {
const response = await fetch(url);
const html = await response.text();
postMessage({index, url, html, time, delay, error: false});
} catch {
postMessage({index, url, html, time, delay, error: true});
}
}
`)
}
async HomeData(button) {
const self = this, homepage = new Map();
let task = 0, DC = 0, HomeD = self.Home_ID, pages = self.Total(api.$$("#gdd td.gdt2", true)),
title = api.IllegalCharacters(api.$$("#gj").textContent.trim() || api.$$("#gn").textContent.trim());
self.DownloadMode = CompressMode;
const olddata = self.Storage(`[${title} - Download Cache]`);
if (olddata) {
self.DownloadTrigger(button, title, olddata);
return;
}
async function GetLink(index, data) {
const homebox = [];
try {
api.$$("#gdt a", true, data).forEach(link => {homebox.push(link.href)});
homepage.set(index, homebox);
self.Show = `[${++DC}/${pages}]`;
document.title = self.Show;
button.textContent = `${Language.DS_02}: ${self.Show}`;
task++;
} catch (error) {
alert(Language.DE_01);
location.reload();
}
}
self.worker.postMessage({index: 0, url: url, time: Date.now(), delay: HomeD});
for (let index = 1; index < pages; index++) {
self.worker.postMessage({index, url: `${url}?p=${index}`, time: Date.now(), delay: HomeD});
}
self.worker.onmessage = (e) => {
const {index, url, html, time, delay, error} = e.data;
HomeD = self.Dynamic(time, delay, null, self.Home_ND);
error ? self.worker.postMessage({index: index, url: url, time: time, delay: delay}) : GetLink(index, api.DomParse(html));
}
const interval = setInterval(() => {
if (task === pages) {
clearInterval(interval);
const homebox = [];
for (let i = 0; i < homepage.size; i++) {homebox.push(...homepage.get(i))}
Config.DeBug ? api.log(Language.Dev_01, `[Title] : ${title}\n${homebox}`) : null;
self.ImageData(button, title, homebox);
}
}, 500);
}
async ImageData(button, title, link) {
const self = this, imgbox = new Map();
let pages = link.length, ImageD = self.Image_ID, DC = 0, task = 0;
async function GetLink(index, img) {
try {
if (img) {
imgbox.set(index, img.src || img.href);
self.Show = `[${++DC}/${pages}]`;
document.title = self.Show;
button.textContent = `${Language.DS_03}: ${self.Show}`;
task++;
} else {
imgbox.set(index, undefined);
throw Language.DE_02;
}
} catch (error) {
api.log(null, error, "error");
task++;
}
}
for (let index = 0; index < pages; index++) {
self.worker.postMessage({index, url: link[index], time: Date.now(), delay: ImageD});
}
self.worker.onmessage = (e) => {
const {index, url, html, time, delay, error} = e.data;
ImageD = self.Dynamic(time, delay, null, self.Image_ND);
error ? self.worker.postMessage({index: index, url: url, time: time, delay: delay}) : GetLink(index, api.$$("#img", false, api.DomParse(html)));
}
let interval = setInterval(() => {
if (task === pages) {
clearInterval(interval);
Config.DeBug ? api.log(Language.Dev_02, imgbox) : null;
self.DownloadTrigger(button, title, imgbox);
self.Storage(`[${title} - Download Cache]`, imgbox);
}
}, 500);
}
async DownloadTrigger(button, title, link) {
this.DownloadMode?
this.ZipDownload(button, title, link):
this.ImageDownload(button, title, link);
}
async ZipDownload(Button, Folder, ImgData) {
const self=this, Data=new JSZip(), force = GM_registerMenuCommand(Language.MN_02, ()=> ForceDownload());
let time, blob, count=0, progress=0, clean=false,
ReTry=Config.ReTry, Total=ImgData.size, delay=self.Download_ID,
thread=self.Download_IT, Fill=self.FillValue(Total);
async function ForceDownload() {
self.Compression(Data, Folder, Button, force);
}
async function Retry_on_error() {
if (self.Enforce) {return}
else if (ReTry-- > 0) {
progress = 0;
self.Show = Language.DS_09;
document.title = self.Show;
Button.textContent = self.Show;
api.log(Language.DE_03, ReTry);
await self.sleep(2500);
setTimeout(() => {StartDownload(true)}, 2000);
} else {
self.Compression(Data, Folder, Button, force);
api.log(Language.DE_04, ImgData);
}
}
async function Request_Analysis(index, link, blob, retry=false) {
if (self.Enforce) {return}
ImgData.delete(index);
self.Show = `[${++progress}/${Total}]`;
[ delay, thread ] = self.Dynamic(time, delay, thread, self.Download_ND);
retry ? ImgData.set(index, link) : Data.file(`${Folder}/${self.Mantissa(index, Fill)}.${api.ExtensionName(link)}`, blob);
document.title = self.Show;
Button.textContent = `${Language.DS_04}: ${self.Show}`;
if (progress == Total) {
Total = ImgData.size;
if (Total == 0) {self.Compression(Data, Folder, Button, force)}
else {Retry_on_error()}
}
}
async function Request(index, link, analysis) {
time = Date.now();
if (self.Enforce) {return}
else if (typeof link !== "undefined") {
GM_xmlhttpRequest({
url: link,
method: "GET",
responseType: "blob",
onload: response => {
blob = response.response;
if (blob instanceof Blob && blob.size > 0) {analysis(index, link, blob)}
else {
Config.DeBug ? api.log(`[Delay:${delay}|Thread:${thread}]`, link, "error") : null;
analysis(index, link, null, true);
}
},
onerror: error => {
Config.DeBug ? api.log(`[Delay:${delay}|Thread:${thread}]`, link, "error") : null;
analysis(index, link, null, true);
}
})
} else {
if (!clean) {
clean = true;
sessionStorage.clear();
api.log(Language.DW_01, Language.DW_02, "warn");
}
progress++;
}
}
StartDownload();
async function StartDownload(restart=false) {
for (const [index, link] of ImgData.entries()) {
if (self.Enforce) {break}
else if (restart) {
await Request(index, link, Request_Analysis);
await self.sleep(500);
}
else {
Request(index, link, Request_Analysis);
if (++count === thread) {
count = 0;
await self.sleep(delay);
}
}
}
}
}
async ImageDownload(Button, Folder, ImgData) {
const Total=ImgData.size, self=this;
let time, link, progress=1, clean=false,
thread=self.Download_IT, delay=self.Download_ID, Fill=self.FillValue(Total);
async function Request(index, retry) {
time = Date.now();
link = ImgData.get(index);
return new Promise((resolve, reject) => {
if (typeof link !== "undefined") {
GM_download({
url: link,
name: `${Folder}-${self.Mantissa(index, Fill)}.${api.ExtensionName(link)}`,
onload: () => {
[ delay, thread ] = self.Dynamic(time, delay, thread, self.Download_ND);
self.Show = `[${progress}/${Total}]`
document.title = self.Show;
Button.textContent = `${Language.DS_04}: ${self.Show}`;
progress++;
resolve();
},
onerror: () => {
if (retry > 0) {
[ delay, thread ] = self.Dynamic(time, delay, thread, self.Download_ND);
Config.DeBug ? api.log(null, `[Delay:${delay}|Thread:${thread}|Retry:${retry}] : [${link}]`, "error") : null;
setTimeout(() => {
Request(index, retry-1);
resolve();
}, delay * 2);
} else {
reject(new Error("Request error"));
}
}
})
} else {
if (!clean) {
clean = true;
sessionStorage.clear();
api.log(Language.DW_01, Language.DW_02, "warn");
}
reject(new Error("undefined url"))
}
});
}
let count = 0, promises = [];
for (let i = 0; i < Total; i++) {
promises.push(Request(i, Config.ReTry));
if (++count === thread) {
count = 0;
await self.sleep(delay);
}
}
await Promise.allSettled(promises);
Button.textContent = Language.DS_08;
setTimeout(() => {
document.title = `✓ ${OriginalTitle}`;
ResetButton();
}, 3000);
}
async Compression(Data, Folder, Button, Menu) {
this.Enforce = true;
GM_unregisterMenuCommand(Menu);
Data.generateAsync({
type: "blob",
compression: "DEFLATE",
compressionOptions: { level: this.Compr_Level }
}, (progress) => {
document.title = `${progress.percent.toFixed(1)} %`;
Button.textContent = `${Language.DS_05}: ${progress.percent.toFixed(1)} %`;
}).then(zip => {
saveAs(zip, `${Folder}.zip`);
this.Enforce = false;
Button.textContent = Language.DS_06;
document.title = `✓ ${OriginalTitle}`;
setTimeout(() => {
ResetButton();
}, 3000);
}).catch(result => {
Button.textContent = Language.DS_07;
document.title = OriginalTitle;
setTimeout(() => {
ResetButton();
}, 6000);
})
}
}
const download = new Download();
Main.Match();
async function ResetButton() {
lock = false;
let Button = api.$$("#ExDB");
Button.disabled = false;
Button.textContent = `✓ ${ModeDisplay}`;
}
function display_language(language) {
let display = {
"zh-TW": [{
"MN_01" : "🔁 切換下載模式", "MN_02" : "📥 強制壓縮下載",
"DM_01" : "壓縮下載", "DM_02" : "單圖下載", "DM_03" : "下載中鎖定",
"DS_01" : "開始下載", "DS_02" : "獲取頁面", "DS_03" : "獲取連結", "DS_04" : "下載進度",
"DS_05" : "壓縮封裝", "DS_06" : "壓縮完成", "DS_07" : "壓縮失敗", "DS_08" : "下載完成",
"DS_09" : "等待失敗重試...", "DE_01" : "請求錯誤重新加載頁面", "DE_02" : "找不到圖片元素, 你的 IP 可能被禁止了, 請刷新頁面重試",
"DE_03" : "剩餘重載次數", "DE_04" : "下載失敗數據", "Dev_01" : "內頁跳轉數據", "Dev_02" : "圖片連結數據", "DW_01" : "清理警告", "DW_02" : "下載數據不完整將清除緩存, 建議刷新頁面後重載"
}],
"zh-CN": [{
"MN_01" : "🔁 切换下载模式", "MN_02" : "📥 强制压缩下载",
"DM_01" : "压缩下载", "DM_02" : "单图下载", "DM_03" : "下载中锁定",
"DS_01" : "开始下载", "DS_02" : "获取页面", "DS_03" : "获取链接", "DS_04" : "下载进度",
"DS_05" : "压缩封装", "DS_06" : "压缩完成", "DS_07" : "压缩失败", "DS_08" : "下载完成",
"DS_09" : "等待失败重试...", "DE_01" : "请求错误重新加载页面", "DE_02" : "找不到图片元素,你的 IP 可能被禁止了,请刷新页面重试",
"DE_03" : "剩余重载次数", "DE_04" : "下载失败数据", "Dev_01" : "内页跳转数据", "Dev_02" : "图片链接数据", "DW_01" : "清理警告", "DW_02" : "下载数据不完整将清除缓存,建议刷新页面后重载"
}],
"ja": [{
"MN_01" : "🔁 ダウンロードモードの切り替え", "MN_02" : "📥 强制圧缩ダウンロード",
"DM_01" : "圧缩ダウンロード", "DM_02" : "単一画像のダウンロード", "DM_03" : "ダウンロード中ロック",
"DS_01" : "ダウンロードを开始", "DS_02" : "ページを取得中", "DS_03" : "リンクを取得中", "DS_04" : "ダウンロードの进捗",
"DS_05" : "圧缩パッケージング", "DS_06" : "圧缩完了", "DS_07" : "圧缩失败", "DS_08" : "ダウンロード完了",
"DS_09" : "再试行を待机中...", "DE_01" : "要求エラー、ページを再読み込みしてください", "DE_02" : "画像要素が见つかりません、お使いの IP がブロックされている可能性があります。ページを更新して再试行してください",
"DE_03" : "残りの再読み込み回数", "DE_04" : "ダウンロード失败データ", "Dev_01" : "内部ページリダイレクトデータ", "Dev_02" : "画像リンクデータ", "DW_01" : "警告をクリア", "DW_02" : "ダウンロードデータが不完全な场合はキャッシュがクリアされます。ページをリフレッシュしてから再読み込みしてください"
}],
"en-US": [{
"MN_01" : "🔁 Switch Download Mode", "MN_02" : "📥 Force Compression Download",
"DM_01" : "Compression Download", "DM_02" : "Single Image Download", "DM_03" : "Downloading Lock",
"DS_01" : "Start Downloading", "DS_02" : "Getting Page", "DS_03" : "Getting Links", "DS_04" : "Download Progress",
"DS_05" : "Compression Packaging", "DS_06" : "Compression Completed", "DS_07" : "Compression Failed", "DS_08" : "Download Completed",
"DS_09" : "Waiting for Retry...", "DE_01" : "Request Error, Reload Page", "DE_02" : "Cannot find image elements, your IP may be blocked, please refresh the page and try again",
"DE_03" : "Remaining Reload Times", "DE_04" : "Download Failed Data", "Dev_01" : "Inner Page Redirect Data", "Dev_02" : "Image Link Data", "DW_01" : "Clearing Warning", "DW_02" : "Incomplete download data will clear cache, suggest refresh page and reload"
}],
"ko": [{
"MN_01" : "🔁 다운로드 모드 전환", "MN_02" : "📥 강제 압축 다운로드",
"DM_01" : "압축 다운로드", "DM_02" : "단일 이미지 다운로드", "DM_03" : "다운로드 중 잠금",
"DS_01" : "다운로드 시작", "DS_02" : "페이지 가져 오기", "DS_03" : "링크 가져 오기", "DS_04" : "다운로드 진행률",
"DS_05" : "압축 포장", "DS_06" : "압축 완료", "DS_07" : "압축 실패", "DS_08" : "다운로드 완료",
"DS_09" : "재시도 대기 중...", "DE_01" : "요청 오류, 페이지 다시로드", "DE_02" : "이미지 요소를 찾을 수 없습니다. IP가 차단 될 수 있습니다. 페이지를 새로 고쳐 다시 시도하십시오",
"DE_03" : "남은 다시로드 횟수", "DE_04" : "다운로드 실패 데이터", "Dev_01" : "내부 페이지 리디렉션 데이터", "Dev_02" : "이미지 링크 데이터", "DW_01" : "경고 지우기", "DW_02" : "다운로드 데이터가 완전하지 않으면 캐시가 지워집니다. 페이지 새로 고침 및 다시로드 권장"
}]
};
return display.hasOwnProperty(language) ? display[language][0] : display["en-US"][0];
}
})();