[E/Ex-Hentai] Downloader

在 E 和 Ex 的漫畫頁面, 創建下載按鈕, 可使用[壓縮下載/單圖下載], 自動獲取圖片下載

As of 2023-08-17. See the latest version.

// ==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.3
// @author       HentiSaru
// @description         在 E 和 Ex 的漫畫頁面, 創建下載按鈕, 可使用[壓縮下載/單圖下載], 自動獲取圖片下載
// @description:zh-TW   在 E 和 Ex 的漫畫頁面, 創建下載按鈕, 可使用[壓縮下載/單圖下載], 自動獲取圖片下載
// @description:zh-CN   在 E 和 Ex 的漫画页面, 创建下载按钮, 可使用[压缩下载/单图下载], 自动获取图片下载
// @description:ja      EとExの漫画ページで、ダウンロードボタンを作成し、[圧縮ダウンロード/単一画像ダウンロード]を使用して、自動的に画像をダウンロードします。
// @description:ko      E 및 Ex의 만화 페이지에서 다운로드 버튼을 만들고, [압축 다운로드/단일 이미지 다운로드]를 사용하여 이미지를 자동으로 다운로드합니다.
// @description:en      On the comic pages of E and Ex, create a download button that can use [compressed download/single image download] to automatically download images.

// @match        https://e-hentai.org/*
// @match        https://exhentai.org/*
// @icon         https://e-hentai.org/favicon.ico

// @license      MIT
// @namespace    https://greasyfork.org/users/989635

// @run-at       document-end
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_download
// @grant        GM_addElement
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand

// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// ==/UserScript==

var count = 0,
ModeDisplay,
parser = new DOMParser(),
OriginalTitle = document.title,
url = window.location.href.split("?p=")[0],
CompressMode = GM_getValue("CompressedMode", []),
language = display_language(navigator.language);

(function() {
    const Ex_HManga = /https:\/\/exhentai\.org\/g\/\d+\/[a-zA-Z0-9]+/;
    const E_HManga = /https:\/\/e-hentai\.org\/g\/\d+\/[a-zA-Z0-9]+/;
    if (Ex_HManga.test(url) || E_HManga.test(url)) {
        ButtonCreation();
    }
    GM_registerMenuCommand(language[0], function() {DownloadModeSwitch()}, "C");
})();

/* 按鈕創建 */
async function ButtonCreation() {
    GM_addStyle(`
        .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;
        }
    `);
    AdaptiveCSS(`
        .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;
        }
    `);
    let download_button;
    try {
        download_button = GM_addElement(document.querySelector("div#gd2"), "button", {
            class: "Download_Button"
        });
        if (CompressMode) {
            ModeDisplay = language[1];
        } else {
            ModeDisplay = language[2];
        }
        download_button.textContent = ModeDisplay;
        download_button.addEventListener("click", function() {
            download_button.textContent = language[3];
            download_button.disabled = true;
            HomeDataProcessing(download_button);
        });
    } catch {}
}

/* 非法字元排除 */
function IllegalFilter(Name) {
    return Name.replace(/[\/\?<>\\:\*\|":]/g, '');
}
/* 取得總頁數 */
function GetTotal(page) {
    return parseInt(page[page.length - 2].textContent.replace(/\D/g, ''));
}
/* 取得漫畫擴展名 */
function GetExtension(link) {
    try {
        const match = link.match(/\.([^.]+)$/);
        if (match) {return match[1].toLowerCase()}
        return "png";
    } catch {return "png"}
}

/* 主頁數據處理 */
async function HomeDataProcessing(button) {
    let title,
    homebox = [],
    pages = GetTotal(document.querySelector("div#gdd").querySelectorAll("td.gdt2"));
    try {
        title = document.getElementById("gj").textContent.trim();
        if (title === "") {throw new Error()}
    } catch {
        title = document.getElementById("gn").textContent.trim();
        if (title === "") {title = language[4]}
    }
    title = IllegalFilter(title);
    pages = Math.ceil(pages / 20);

    async function GetLink(data) { // 獲取頁面所有連結
        data.querySelector("#gdt").querySelectorAll("a").forEach(link => {
            homebox.push(link.href);
        });
    }

    async function FetchRequest(url) { // 數據請求
        const response = await fetch(url);
        const html = await response.text();
        GetLink(parser.parseFromString(html, "text/html"));
    }

    const promises = [FetchRequest(url)];
    for (let i = 1; i < pages; i++) {
        promises.push(FetchRequest(`${url}?p=${i}`));
        button.textContent = `${language[5]}: [${i+1}/${pages}]`;
        await new Promise(resolve => setTimeout(resolve, 150));
    }

    await Promise.allSettled(promises);
    ImageLinkProcessing(button, title, homebox);
}

/* 漫畫連結處理 */
async function ImageLinkProcessing(button, title, link) {
    let imgbox = new Map(), pages = link.length;
    async function GetLink(index, data) {
        try {
            imgbox.set(index, data.src);
            button.textContent = `${language[6]}: [${index + 1}/${pages}]`;
        } catch {
            try {
                imgbox.set(index, data.href);
                button.textContent = `${language[6]}: [${index + 1}/${pages}]`;
            } catch {}
        }
    }

    async function FetchRequest(index, url) {
        try {
            const response = await fetch(url);
            const html = await response.text();
            GetLink(index, parser.parseFromString(html, "text/html").querySelector("img#img"));
        } catch (error) {
            await FetchRequest(index, url);
        }
    }

    const promises = [];
    for (let index = 0; index < pages; index++) {
        promises.push(FetchRequest(index, link[index]));
        await new Promise(resolve => setTimeout(resolve, 50));
    }

    await Promise.allSettled(promises);
    DownloadTrigger(button, title, imgbox);
}

/* 下載觸發器 */
async function DownloadTrigger(button, title, link) {
    if (CompressMode) {ZipDownload(button, title, link)}
    else {ImageDownload(button, title, link)}
}

/* 壓縮下載 */
async function ZipDownload(Button, Folder, ImgData) {
    const zip = new JSZip(), Total = ImgData.size, promises = [];
    let progress = 1, link, mantissa, extension, BackgroundWork, retry=0;
    async function Request(index) {
        link = ImgData.get(index);
        extension = GetExtension(link);
        return new Promise((resolve) => {
            if (link !== undefined) {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: link,
                    responseType: "blob",
                    headers : {"user-agent": navigator.userAgent},
                    onload: response => {
                        if (response.status === 200 && response.response instanceof Blob && response.response.size > 0) {
                            mantissa = (index + 1).toString().padStart(4, '0');
                            zip.file(`${Folder}/${mantissa}.${extension}`, response.response);
                            document.title = `[${progress}/${Total}]`;
                            Button.textContent = `${language[7]}: [${progress}/${Total}]`;
                            progress++;
                            resolve();
                        } else {
                            retry++;
                            if (retry < 10) {
                                Request(index)
                            } else {resolve()}
                        }
                    },
                    onerror: error => {
                        resolve();
                    }
                });
            } else {
                document.title = `[${progress}/${Total}]`;
                Button.textContent = `${language[7]}: [${progress}/${Total}]`;
                progress++;
                resolve();
            }
        });
    }
    for (let i = 0; i < Total; i++) {
        promises.push(Request(i));
        count++;
        if (count === 20) {
            count = 0;
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
    }
    await Promise.allSettled(promises);
    Compression();
    async function Compression() {
        if (typeof(Worker) !== "undefined" && typeof(BackgroundWork) === "undefined") {
            BackgroundWork = new Worker(BackgroundCreation());
            BackgroundWork.postMessage([
                await zip.generateAsync({
                    type: "blob",
                    compression: "DEFLATE",
                    compressionOptions: {
                        level: 5
                    }
                }, (progress) => {
                    document.title = `${progress.percent.toFixed(1)} %`;
                    Button.textContent = `${language[8]}: ${progress.percent.toFixed(1)} %`;
                }).then(zip => {
                    saveAs(zip, `${Folder}.zip`);
                    Button.textContent = language[9];
                    document.title = OriginalTitle;
                    setTimeout(() => {
                        Button.textContent = ModeDisplay;
                        Button.disabled = false;
                    }, 3000);
                }).catch(result => {
                    Button.textContent = language[10];
                    document.title = OriginalTitle;
                    setTimeout(() => {
                        Button.textContent = ModeDisplay;
                        Button.disabled = false;
                    }, 6000);
                })
            ])
        }
    }
}

/* 單圖下載 */
async function ImageDownload(Button, Folder, ImgData) {
    const Total = ImgData.size, promises = [];
    let progress = 1, link, extension, retry=0;
    async function Request(index) {
        link = ImgData.get(index);
        extension = GetExtension(link);
        return new Promise((resolve) => {
            if (link !== undefined) {
                GM_download({
                    url: link,
                    name: `${Folder}_${(index + 1).toString().padStart(4, '0')}.${extension}`,
                    headers : {"user-agent": navigator.userAgent},
                    onload: () => {
                        document.title = `[${progress}/${Total}]`;
                        Button.textContent = `${language[7]}: [${progress}/${Total}]`;
                        progress++;
                        resolve();
                    },
                    onerror: () => {
                        retry++;
                        if (retry < 10) {
                            Request(index);
                        } else {resolve()}
                    }
                });
            } else {
                document.title = `[${progress}/${Total}]`;
                Button.textContent = `${language[7]} [${progress}/${Total}]`;
                progress++;
                resolve();
            }
        });
    }
    for (let i = 0; i < Total; i++) {
        promises.push(Request(i));
    }
    await Promise.allSettled(promises);
    Button.textContent = language[11];
    setTimeout(() => {
        Button.textContent = ModeDisplay;
        Button.disabled = false;
    }, 3000);
}

/* 下載模式切換 */
async function DownloadModeSwitch() {
    if (CompressMode){
        GM_setValue("CompressedMode", false);
    } else {
        GM_setValue("CompressedMode", true);
    }
    location.reload();
}

/* 自適應css */
function AdaptiveCSS(e, ex) {
    const Domain = window.location.hostname;
    if (Domain === "e-hentai.org") {
        GM_addStyle(`${e}`);
    } else if (Domain === "exhentai.org") {
        GM_addStyle(`${ex}`);
    }
}

/* work創建 */
function BackgroundCreation() {
    let blob = new Blob([""], {type: "application/javascript"});
    return URL.createObjectURL(blob);
}

/* 顯示語言 */
function display_language(language) {
    let display = {
        "zh-TW": [
            "🔁 切換下載模式",
            "壓縮下載",
            "單圖下載",
            "開始下載",
            "未找到標題",
            "獲取頁面",
            "獲取連結",
            "下載進度",
            "壓縮封裝",
            "壓縮完成",
            "壓縮失敗",
            "下載完成"
        ],
        "zh-CN": [
            "🔁 切换下载模式",
            "压缩下载",
            "单图下载",
            "开始下载",
            "未找到标题",
            "获取页面",
            "获取链接",
            "下载进度",
            "压缩封装",
            "压缩完成",
            "压缩失败",
            "下载完成"
        ],
        "ja": [
            "🔁 ダウンロードモードの切り替え",
            "圧縮ダウンロード",
            "単一画像ダウンロード",
            "ダウンロード開始",
            "タイトルが見つかりませんでした",
            "ページを取得する",
            "リンクを取得する",
            "ダウンロードの進捗状況",
            "圧縮パッケージング",
            "圧縮完了",
            "圧縮に失敗しました",
            "ダウンロードが完了しました"
        ],
        "en": [
            "🔁 Switch download mode",
            "Compressed download",
            "Single image download",
            "Start download",
            "Title not found",
            "Get page",
            "Get link",
            "Download progress",
            "Compressed packaging",
            "Compression complete",
            "Compression failed",
            "Download complete"
        ],
        "ko": [
            "🔁 다운로드 모드 전환",
            "압축 다운로드",
            "단일 이미지 다운로드",
            "다운로드 시작",
            "제목을 찾을 수 없습니다",
            "페이지 가져오기",
            "링크 가져오기",
            "다운로드 진행 상황",
            "압축 포장",
            "압축 완료",
            "압축 실패",
            "다운로드 완료"
        ]
    };
    return display[language] || display["en"];
}