ehx direct download

direct download archive from list / sort gallery (in current page) / show full title in pure text

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         ehx direct download
// @namespace    https://github.com/x94fujo6rpg/SomeTampermonkeyScripts
// @version      1.15
// @description  direct download archive from list / sort gallery (in current page) / show full title in pure text
// @author       x94fujo6
// @match        https://e-hentai.org/*
// @exclude      https://e-hentai.org/mytags
// @exclude      https://e-hentai.org/mpv/*
// @match        https://exhentai.org/*
// @exclude      https://exhentai.org/mytags
// @exclude      https://exhentai.org/mpv/*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==
/* jshint esversion: 9 */

// this script only work in Thumbnail mode
(function () {
    'use strict';
    let api;
    let domain;
    let hid = true;
    let m = "[ehx direct download] ";
    let debug_message = true;
    let debug_adv = false;
    let gdata = [];
    let gallery_count = 0;
    let timer_list = [];
    let key_list = {
        dl_list: "exhddl_list",
        exclude_tag: "exhddl_exclude_tag_list",
        exclude_uploader: "exhddl_exclude_uploader_list",
        sort_setting: "exhddl_sortsetting",
        dl_and_copy: "exhddl_dl_and_copy",
        auto_fix_title: "exhddl_auto_fix_title",
        auto_enable_puretext: "exhddl_auto_enable_puretext",
        sort_numeric: "exhddl_sort_numeric",
        sort_ignore_punctuation: "exhddl_sort_ignore_punctuation",
    };
    let default_value = {
        dl_list: [],
        exclude_tag: [],
        exclude_uploader: [],
        sort_setting: true,
        dl_and_copy: true,
        auto_fix_title: true,
        auto_enable_puretext: true,
        sort_numeric: true,
        sort_ignore_punctuation: false,
    };
    let id_list = {
        mainbox: "exhddl_activate",
        dd: "exhddl_ddbutton",
        puretext: "exhddl_puretext",
        sort_setting: "exhddl_sortsetting",
        jump_to_last: "exhddl_jump_to_last",
        dl_and_copy: "exhddl_dl_and_copy",
        auto_fix_title: "exhddl_auto_fix_title",
        auto_enable_puretext: "exhddl_auto_enable_puretext",
        sort_numeric: "exhddl_sort_numeric",
        sort_ignore_punctuation: "exhddl_sort_ignore_punctuation",
    };
    let style_list = {
        top_button: { width: "max-content" },
        gallery_button: {
            //width: "max-content",
            width: "75%",
            alignSelf: "center",
            lineHeight: "1.75rem",
            border: "transparent solid 0.125rem",
            borderRadius: "0.25rem",
            marginTop: "0.75rem",
        },
        gallery_marked: { backgroundColor: "black" },
        button_marked: {
            color: "gray",
            backgroundColor: "transparent",
            border: "gray solid 0.125rem",
        },
        ex: { backgroundColor: "goldenrod" },
        mainbox: { textAlign: "center", lineHeight: "2rem" },
        separator: { color: "transparent" },
    };
    let status_update_interval = 500;
    let fix_prefix = false;
    let ignore_prefix = [
        "(同人誌)",
        "(成年コミック)",
        "(成年コミック・雑誌)",
        "(一般コミック)",
        "(一般コミック・雑誌)",
        "(エロライトノベル)",
        "(ゲームCG)",
        "(同人ゲームCG)",
        "(18禁ゲームCG)",
        "(同人CG集)",
        "(画集)",
    ];
    let container_list = [
        "()", "[]", "{}", "()",
        "[]", "{}", "【】", "『』", "《》", "〈〉", "「」"
    ];
    let [container_start, container_end] = extracContainer();
    let container_reg = containerRegexGenerator();
    let group_reg = new RegExp(`^${container_reg}`);
    let excess_reg = new RegExp(`^\\s*${container_reg}\\s*|\\s*${container_reg}\\s*$`, "g");
    let blank_reg = /[\s ]{2,}/g;
    let enable_sim_search = false;
    let sim_search_threshold = 0.6;
    let gallery_data_max_size = 0; // kb, 0 = no limit
    let gallery_data_limit = { max_size: 1024 * (gallery_data_max_size), max_length: parseInt((1024 * gallery_data_max_size) / 7, 10), };

    window.onload = main();

    function main() {
        domain = `https://${document.domain}`;
        api = `${domain}/api.php`;

        myCss();
        timerMananger();

        let link = document.location.href;
        if (link.includes(".php")) {
            return print(`${m}see php, abort`);
        } else if (link.includes("/g/")) {
            print(`${m}gallery page`);
            return setEvent(link);
        } else {
            print(`${m}normal start`);
            if (!checkDisplayMode) return wrongDisplayMode();
            return setButton();
        }

        function myCss() {
            let newcss = Object.assign(document.createElement("style"), {
                id: "ehx_direct_download_css",
                innerHTML: `
                    .puretext {
                        overflow: hidden;
                        min-height: 32px;
                        line-height: 16px;
                        margin: 6px 4px 0;
                        font-size: 10pt;
                        text-align: center;
                    }

                    .gallery_box {
                        text-align: center;
                        /*line-height: 2rem;*/
                        /*margin: auto auto 0rem auto;*/
                        margin: auto 0.25rem 0.25rem;
                        max-height: max-content;
                    }

                    .torrent_title {
                        line-height: 1rem;
                        text-align: center;
                        margin: 0.2rem;
                        border: 0.1rem solid;
                        padding-bottom: 0.4rem;
                        display: inline-grid;
                    }

                    .prefix_from {
                        text-align: center;
                        white-space: break-spaces;
                        border: 0.1rem solid blueviolet;
                        position: relative;
                        background: rgba(138, 43, 226, 0.1);
                    }

                    .prefix_from>div {
                        display: none;
                    }

                    .prefix_from:hover>div {
                        display: block;
                        position: absolute;
                        z-index: 10;
                        margin: 0.6rem;
                        overflow: hidden;
                        bottom: 100%;
                    }
                `,
            });
            document.head.appendChild(newcss);
            setTitleStyle();
        }

        function setTitleStyle() {
            [...[...document.styleSheets].find(s => s.href).cssRules].find(s => s.selectorText == ".gl4t").style.removeProperty("max-height");
        }

        function setEvent(link) {
            let _link = link.split("/");
            if (_link[3] === "g") {
                let gid = _link[4];
                let archive_download = findEleByText("a", "Archive Download");
                if (archive_download) {
                    archive_download.addEventListener("click", () => {
                        addToDownloadedList(gid);
                    });
                    print(`${m}set trigger for updateList on gallery:${gid}`);
                } else {
                    if (typeof gotonext != "undefined") {
                        let search_site = `https://panda.chaika.moe/search/?qsearch=${encodeURIComponent(link)}`;
                        let redir = () => {
                            let _ele = document.querySelector("#continue");
                            _ele.firstChild.text = "(Go to Front Page)";
                            _ele = _ele.parentElement;
                            _ele.children[1].remove();
                            _ele.innerHTML = `${_ele.innerHTML}
                            <p></p>
                            <br>
                            <p><a href="${search_site}">[ Search on panda.chaika.moe ]</a></p>
                            `;
                        };
                        gotonext = () => { };
                        redir();
                        return;
                    }
                }
            }

            let url = document.location.href;
            let match_eh = url.match(/e-hentai.org\/g\/\d+\/\w+\//) ? url.replace("e-hentai", "exhentai") : false;
            let match_ex = url.match(/exhentai.org\/g\/\d+\/\w+\//) ? url.replace("exhentai", "e-hentai") : false;
            if (match_eh || match_ex) {
                let pos = document.querySelector(".gtb");
                let b = Object.assign(document.createElement("div"), {
                    className: "tha",
                    textContent: match_eh ? "goto exhentai" : "goto e-hentai",
                    style: `
                        margin: auto;
                        margin-bottom: 0.5rem;
                        float: none;
                        width: max-content;
                        `,
                    onclick: () => { document.location.href = match_eh ? match_eh : match_ex; },
                });
                pos.insertAdjacentElement("afterbegin", b);
            }
        }

        function checkDisplayMode() {
            let setting = document.getElementById("dms");
            if (!setting) return false;
            setting = setting.querySelector("option[value='t']");
            return setting.selected;
        }

        function wrongDisplayMode() {
            let pos = document.querySelector(".ido");
            pos.insertAdjacentElement("afterbegin", newSpan("Display mode is not Thumbnail. Script stop"));
        }

        function setButton() {
            let box = Object.assign(document.createElement("div"), { id: id_list.mainbox });
            Object.assign(box.style, style_list.mainbox);
            let nodelist = [
                newButton(id_list.dd, "Enable Archive Download / Sorting / Show torrents Title / Fix Event in Ttile / Exclude Gallery", style_list.top_button, enableDirectDownload),
                newSeparate(),
                newButton(id_list.puretext, "Show Pure Text", style_list.top_button, pureText),
                newSeparate(),
                newButton(id_list.jump_to_last, "Jump To Nearest Downloaded", style_list.top_button, jumpToLastDownload),
                newLine(),
            ];
            box = appendAllChild(box, nodelist);

            document.getElementById("toppane").insertAdjacentElement("afterend", box);
            if (hid) hlexg();
            forEachGallery(gallery => {
                addInfoToGallery(gallery);
                setLinkToNewTab(gallery);
            });
            addTimer(updateGalleryStatus, status_update_interval);

            function enableDirectDownload() {
                group();
                time(`${m}request_data`);
                let dd = document.getElementById(id_list.dd);
                dd.disabled = true;
                dd.removeAttribute("onclick");
                dd.insertAdjacentElement("afterend", newSpan("Processing... Please Wait"));
                acquireGalleryData();
                setBottomStyle();

                function setBottomStyle() {
                    [...[...document.styleSheets].find(s => s.href).cssRules].find(s => s.selectorText == ".gl5t").style.removeProperty("margin");
                }

                function acquireGalleryData() {
                    let gallery_nodelist = selectAllGallery();
                    if (gallery_nodelist) {
                        print(`${m}acquire gallery data`);
                        let data = { method: "gdata", gidlist: [], namespace: 1 };
                        let alldata = [];
                        let count = 0;
                        let glist = [];
                        for (let index = 0, length = gallery_nodelist.length; index < length; index++) {
                            let gallery = gallery_nodelist[index];
                            let gid = gallery.getAttribute("gid");
                            let gtoken = gallery.getAttribute("gtoken");
                            if (gid && gtoken) {
                                glist.push([gid, gtoken]);
                                count++;
                                gallery_count++;
                                if (count === 25 || index === length - 1) {
                                    count = 0;
                                    let newdata = Object.assign({}, data);
                                    newdata.gidlist = Object.assign([], glist);
                                    alldata.push(newdata);
                                    glist = [];
                                }
                            }
                        }
                        print(`${m}gallery queue length:[${alldata.length}], total gallery count:[${gallery_count}]`);
                        if (alldata.length != 0) {
                            requestData(alldata);
                        } else {
                            print(`${m}gallery queue is empty`);
                        }
                    }
                }

                async function requestData(datalist) {
                    print(`${m}start sending request`);
                    print(`${m}----------------------`);
                    for (let index = 0, length = datalist.length; index < length; index++) {
                        print(`${m}sending request[${index}]`);
                        await myApiCall(datalist[index])
                            .then(async reslove => {
                                print(`${m}receive request[${index}]`);
                                await directDL(reslove, index);
                            })
                            .catch(reject => {
                                print(`${m}request[${index}] failed`, reject);
                            });
                    }
                    // all done
                    groupEnd();
                    timeEnd(`${m}request_data`);
                    print(`${m}all request done`);
                    print(`${m}process data`);
                    processGdata();
                    print(`${m}setup sorting`);
                    setSortingButton();
                    print(`${m}setup copy title`);
                    forEachGallery(setCopyTitle);
                    print(`${m}setup show torrent title`);
                    forEachGallery(setShowTorrent);
                    if (getGMValue("auto_fix_title")) {
                        print(`${m}auto enable fix title`);
                        document.getElementById("exhddl_fix_title").click();
                    }
                    if (getGMValue("auto_enable_puretext")) {
                        print(`${m}auto enable pure text`);
                        document.getElementById("exhddl_puretext").click();
                    }
                }

                function myApiCall(data) {
                    return new Promise((reslove, reject) => {
                        let request = new XMLHttpRequest();
                        request.open("POST", api);
                        request.setRequestHeader("Content-Type", "application/json");
                        request.withCredentials = true;
                        request.onreadystatechange = () => {
                            if (request.readyState == 4) {
                                return (request.status == 200) ? reslove(request.responseText) : reject(request.responseText);
                            }
                        };
                        request.send(JSON.stringify(data));
                    });
                }
            }

            function pureText() {
                let button = document.getElementById(id_list.puretext);
                button.disabled = true;
                button.removeAttribute("onclick");
                forEachGallery(gallery => {
                    let puretext_div = Object.assign(document.createElement("div"), {
                        innerHTML: gallery.querySelector(".glname").innerHTML,
                        className: "puretext",
                    });
                    puretext_div.setAttribute("name", id_list.puretext);
                    let pos = gallery.querySelector(".prefix_from");
                    if (!pos) pos = gallery.querySelector(".gl3t");
                    pos.insertAdjacentElement("afterend", puretext_div);
                });
            }

            function jumpToLastDownload() {
                let last = document.querySelector("[marked='true']");
                if (last) last.scrollIntoView();
            }

            function hlexg() {
                forEachGallery(gallery => { if (gallery.querySelector("s")) Object.assign(gallery.style, style_list.ex); });
            }
        }
    }

    function directDL(data, index) {
        data = JSON.parse(data);
        print(`${m}process request[${index}] data, gallery count:[${Object.keys(data.gmetadata).length}]`);

        let downloaded_list = getGMList("dl_list");
        let gidlist = [];
        let mark_list = [];

        for (let gallery_data of data.gmetadata) {
            gdata.push(gallery_data);
            let gid = gallery_data.gid;
            let gtoken = gallery_data.token;
            let archiver_key = gallery_data.archiver_key;

            let archivelink = `${domain}/archiver.php?gid=${gid}&token=${gtoken}&or=${archiver_key}`;
            let glink = `${domain}/g/${gid}/${gtoken}/`;
            let gallery = document.querySelector(`.gl1t[gid='${gid}']`);
            if (gallery) {
                let dl_button = Object.assign(document.createElement("button"), {
                    id: `gallery_dl_${gid}`,
                    className: "gdd",
                    textContent: "Archive Download",
                    onclick: function () {
                        let self = this;
                        let ck = document.getElementById(id_list.dl_and_copy).checked;
                        if (ck) {
                            navigator.clipboard.writeText(repalceForbiddenChar(self.parentElement.parentElement.querySelector(".glname").textContent.trim()))
                                .then(() => downloadButton(self, gid, archivelink, glink));
                        } else {
                            downloadButton(self, gid, archivelink, glink);
                        }
                    },
                });

                Object.assign(dl_button.style, style_list.gallery_button);

                if (downloaded_list.indexOf(`${gid}`) != -1) {
                    Object.assign(dl_button.style, style_list.button_marked);
                    dl_button.setAttribute("marked", true);
                    mark_list.push(gid);
                }

                let box = Object.assign(document.createElement("div"), { className: "gallery_box" });
                box.insertAdjacentElement("afterbegin", dl_button);

                let pos = gallery.querySelector(".gl5t");
                pos.insertAdjacentElement("beforebegin", box);

                gidlist.push(dl_button.id);

                let set_status_button = Object.assign(document.createElement("button"), {
                    id: `gallery_status_${gid}`,
                    className: "gstatus",
                    textContent: "Mark/Unmark This",
                    onclick: function () { setGalleryStatus(gid); },
                });
                Object.assign(set_status_button.style, style_list.gallery_button);

                dl_button.insertAdjacentElement("afterend", set_status_button);
                dl_button.insertAdjacentElement("afterend", newLine());
            }
        }
        if (mark_list.length > 0) print(`${m}found in list, set as downloaded:\n`, mark_list);

        print(`${m}request[${index}] done`);
        print(`${m}----------------------`);
        return new Promise(reslove => reslove());

        function downloadButton(button, gid, archivelink, glink) {
            Object.assign(button.style, style_list.button_marked);
            button.setAttribute("marked", true);
            visitGallery(glink);
            addToDownloadedList(gid);
            my_popUp(archivelink, 480, 320);
            updateGalleryStatus();
        }

        function setGalleryStatus(gid) {
            gid = `${gid}`; // convert to string
            let downloaded_list = getGMList("dl_list");
            if (downloaded_list.length > 0) {
                let index = downloaded_list.indexOf(gid);
                if (index != -1) {
                    downloaded_list.splice(index, 1);
                    print(`${m}gallery:[${gid}] in list, remove from list`);
                    resetGalleryStatus(gid);
                } else {
                    downloaded_list.push(gid);
                    print(`${m}gallery:[${gid}] not in list, add to list`);
                    let count = 0;
                    if (downloaded_list.length > gallery_data_limit.max_size && gallery_data_limit.max_size != 0) {
                        while (downloaded_list.length > gallery_data_limit.max_size) {
                            let r = downloaded_list.shift();
                            print(`${m}%creach limit, remove [${r}]`, "color:OrangeRed;");
                            count++;
                            if (count > 100) return print(`${m}unknow error while removing old data, script stop`);
                        }
                    }
                }
            } else {
                downloaded_list = [gid];
            }
            let list_length = downloaded_list.length;
            setGMData(key_list.dl_list, downloaded_list);

            //downloaded_list = downloaded_list.join();
            //GM_setValue(key_list.dl_list, downloaded_list);

            print(`${m}save list. [list_size:${downloaded_list.join().length} (limit:${gallery_data_limit.max_size}), list_length:${list_length} (possible limit:${gallery_data_limit.max_length})]`);

            updateGalleryStatus();

            function resetGalleryStatus(gid) {
                let gallery = document.querySelector(`[gid="${gid}"]`);
                if (!gallery.querySelector("s")) gallery.style.removeProperty("background-color");
                gallery.removeAttribute("marked");

                let dl_button = gallery.querySelector(".gdd");
                dl_button.style = "";
                Object.assign(dl_button.style, style_list.gallery_button);
                dl_button.removeAttribute("marked");
            }
        }
    }

    function processGdata() {
        let tag_key_list = [
            "artist:",
            "group:",
            "female:",
            "male:",
            "parody:",
            "character:",
            "language:",
        ];
        dGroup();
        for (let data of gdata) {
            // extract tags
            let copy_tags = Object.assign([], data.tags);
            for (let tag_key of tag_key_list) {
                let data_key = tag_key.replace(":", "");
                data[data_key] = [];
                copy_tags.forEach(tag => { if (tag.includes(tag_key)) data[data_key].push(tag); });
                // remove used
                copy_tags = copy_tags.filter(tag => !data[data_key].includes(tag));
            }
            data.misc = copy_tags; // unuse list

            data.title_original = decodeHTMLString(data.title);
            data.title_jpn = data.title_jpn.length > 0 ? decodeHTMLString(data.title_jpn) : data.title;
            data.title_jpn_original = data.title_jpn;
            [data.title_prefix, data.title_no_event] = extractPrefix(data.title);

            let title_prefix_jpn;
            [title_prefix_jpn, data.title_no_event_jpn] = extractPrefix(data.title_jpn);
            // try to found prefix in title_jpn
            if (title_prefix_jpn) data.title_prefix = title_prefix_jpn;

            let from_torrent = false;
            if (data.title_prefix.length == 0) {
                // try to found prefix in torrent
                let torrent_list = getTorrentList(data.gid);
                if (torrent_list) {
                    for (let torrent of torrent_list) {
                        let [prefix,] = extractPrefix(torrent);
                        if (prefix) {
                            data.title_prefix = prefix;
                            from_torrent = true;
                            break;
                        }
                    }
                }
            }

            [data.title_group, data.title_no_group] = extractGroup(data.title_no_event);
            [data.title_group_jpn, data.title_no_group_jpn] = extractGroup(data.title_no_event_jpn);
            data.title_pure = removeExcess(data.title_no_group);
            data.title_pure_jpn = removeExcess(data.title_no_group_jpn);
            data.title_pure_for_sim = removeAllPunctuation(data.title_pure).toLowerCase();
            data.title_pure_jpn_for_sim = removeAllPunctuation(data.title_pure_jpn).toLowerCase();

            if (debug_message && debug_adv) {
                dPrint(`${String(data.gid).padStart(10)}|__________`);
                let title_list = [
                    "title_original",
                    //"title_no_event",
                    //"title_no_group",
                    "title_pure",
                    "title_prefix",
                    //"title_group",
                    "title_jpn",
                    //"title_no_event_jpn",
                    //"title_no_group_jpn",
                    "title_pure_jpn",
                    //"title_group_jpn"
                    "title_pure_for_sim",
                    "title_pure_jpn_for_sim",
                ];
                title_list.forEach(key => {
                    let add = (from_torrent && key == "title_prefix") ? " found in torrent" : "";
                    dPrint(`${String(data.gid).padStart(10)}|${key.padStart(25)}|${data[key]}%c${add}`, "color:DarkOrange;");
                });
            }
        }
        dGroupEnd();
    }

    function setSortingButton() {
        let pos = document.getElementById(id_list.mainbox);
        let input_list = [
            newSetting("(Sort) Descending", "sort_setting"), newSeparate(),
            newSetting("(Sort) Numeric", "sort_numeric"), newSeparate(),
            newSetting("(Sort) Ignore Punctuation", "sort_ignore_punctuation"), newSeparate(),
            newSetting("Copy Title When Download", "dl_and_copy"), newSeparate(),
            newSetting("Auto Enable Pure Text", "auto_enable_puretext"), newSeparate(),
            newSetting("Auto Enable Fix Title", "auto_fix_title"), newLine(),
        ].flat();
        input_list.every(node => { if (node.tagName == "INPUT") { node.addEventListener("change", updateSetting); } });
        let form = Object.assign(
            document.createElement("form"),
            {
                id: "exhddl_setting_form_prevent_send_data",
                onsubmit: (event) => {
                    event.preventDefault();
                    return false;
                },
            }
        );
        appendAllChild(form, input_list);
        let nodelist = [form];

        let sort_jp = document.createElement("div");
        sort_jp.textContent = "Sort by JP";
        sort_jp.style = "display: inline-block;";

        let sort_en = document.createElement("div");
        sort_en.textContent = "Sort by EN";
        sort_en.style = "display: inline-block;";

        nodelist.push([
            sort_jp,
            newSeparate(),
            newButton("exhddl_sort_by_title_jp", "Full Title", style_list.top_button, () => { sortGalleryByKey("title_jpn"); }),
            newSeparate(),
            newButton("exhddl_sort_by_title_pure", "ignore Prefix/Group/End", style_list.top_button, () => { sortGalleryByKey("title_pure_jpn"); }),
            newSeparate(),
            newButton("exhddl_sort_by_title_no_group", "ignore Prefix/Group", style_list.top_button, () => { sortGalleryByKey("title_no_group_jpn"); }),
            newSeparate(),
            newButton("exhddl_sort_by_title_no_event", "ignore Prefix", style_list.top_button, () => { sortGalleryByKey("title_no_event_jpn"); }),
            newLine(),

            sort_en,
            newSeparate(),
            newButton("exhddl_sort_by_title_en", "Full Title", style_list.top_button, () => { sortGalleryByKey("title"); }),
            newSeparate(),
            newButton("exhddl_sort_by_title_pure", "ignore Prefix/Group/End", style_list.top_button, () => { sortGalleryByKey("title_pure"); }),
            newSeparate(),
            newButton("exhddl_sort_by_title_no_group", "ignore Prefix/Group", style_list.top_button, () => { sortGalleryByKey("title_no_group"); }),
            newSeparate(),
            newButton("exhddl_sort_by_title_no_event", "ignore Prefix", style_list.top_button, () => { sortGalleryByKey("title_no_event"); }),
            newLine(),

            newButton("exhddl_sort_by_date", "Date (Default)", style_list.top_button, () => { sortGalleryByKey("posted"); }),
            newSeparate(),
            newButton("exhddl_sort_by_prefix", "Event", style_list.top_button, () => { sortGalleryByKey("title_prefix"); }),
            newSeparate(),
            newButton("exhddl_sort_by_artist", "Artist", style_list.top_button, () => { sortGalleryByKey("artist"); }),
            newSeparate(),
            newButton("exhddl_sort_by_group", "Group/Circle", style_list.top_button, () => { sortGalleryByKey("group"); }),
            newSeparate(),
            newButton("exhddl_sort_by_category", "Category", style_list.top_button, () => { sortGalleryByKey("category"); }),
            newSeparate(),

            newButton("exhddl_sort_by_page", "Page Count", style_list.top_button, () => { sortGalleryByKey("filecount"); }),
            newSeparate(),
            newButton("exhddl_sort_by_rating", "Rating", style_list.top_button, () => { sortGalleryByKey("rating"); }),
            newSeparate(),
            newButton("exhddl_sort_by_uploader", "Uploader", style_list.top_button, () => { sortGalleryByKey("uploader"); }),
            newLine(),
            /*
            newButton("exhddl_sort_by_ex", "???", style_list.top_button, () => { sortGalleryByKey("expunged"); }),
            newLine(),
            */

            newButton("exhddl_fix_title", "Fix/Unfix Event in Title (Search in torrents/same title gallery)", style_list.top_button, () => { fixTitlePrefix(); }),
            newSeparate(),
            newButton("exhddl_exclude_buttons", "Show Exclude List", style_list.top_button, () => { setExclude(); }),
        ]);
        nodelist = nodelist.flat();
        pos.querySelector("span").remove(); // remove loading message
        appendAllChild(pos, nodelist);

        function newSetting(lable_text, id_key) {
            return [
                Object.assign(document.createElement("input"), {
                    type: "checkbox",
                    id: id_list[id_key],
                    checked: getGMValue(id_key),
                }),
                Object.assign(document.createElement("label"), {
                    htmlFor: id_list[id_key],
                    textContent: lable_text,
                })
            ];
        }

        function updateSetting() {
            let skip_list = Object.keys(default_value).filter(key => typeof (default_value[key]) != "boolean");
            let updatelist = Object.keys(key_list).filter(key => !skip_list.includes(key));
            let [info, style] = [[], []];
            for (let key of updatelist) {
                let value = document.getElementById(id_list[key]).checked;
                GM_setValue(key_list[key], value);
                info.push(`[${key}]:%c${value}`);
                style.push("", value ? "color:DeepSkyBlue" : "color:DeepPink");
            }
            print(`${m}updateSetting | %c${info.join(" %c| ")}`, ...style);
        }

        function sortGalleryByKey(key = "") {
            if (!key) return print("no key");
            dTime(sortGalleryByKey.name);
            dGroup();
            let sortedID = getSortedID(gdata, key);
            let container = document.querySelector(".itg.gld");
            dGroupEnd();
            if (container) {
                sortedID.forEach(id => {
                    let gallery = document.querySelector(`[gid="${id}"]`);
                    if (gallery) container.appendChild(gallery);
                });
            } else {
                print(`${m}container not found`);
            }
            dTimeEnd(sortGalleryByKey.name);

            function getSortedID(gdata, sort_key) {
                let descending = document.getElementById(id_list.sort_setting).checked;
                return gdata.sort((a, b) => {
                    return descending ? naturalSort(b[sort_key], a[sort_key]) : naturalSort(a[sort_key], b[sort_key]);
                }).map(g => {
                    dPrint(`${String(g.gid).padStart(10)} | ${g[sort_key]}`);
                    return g.gid;
                });
            }

            function naturalSort(a, b) {
                let numeric = document.getElementById(id_list.sort_numeric).checked;
                let ignore_punctuation = document.getElementById(id_list.sort_ignore_punctuation).checked;
                return String(a).localeCompare(String(b), navigator.languages[0] || navigator.language, { numeric: numeric, ignorePunctuation: ignore_punctuation });
            }
        }

        function setExclude() {
            let self = document.getElementById("exhddl_exclude_buttons");
            self.disabled = true;
            self.removeAttribute("onclick");
            forEachGallery(gallery => {
                let gid = gallery.getAttribute("gid");
                let data = gdata.find(gallery_data => gallery_data.gid == gid);
                let pos = gallery.querySelector(".gallery_box");
                let tag_list = [
                    `uploader:${data.uploader.trim()}`,
                    data.language,
                    data.artist,
                    data.group,
                    data.female,
                    data.male,
                    data.parody,
                    data.character,
                ].flat();
                let select = newSelect(tag_list);
                Object.assign(select.style, style_list.gallery_button);
                let span = newSpan("Exclude");
                Object.assign(span.style, style_list.gallery_button);
                appendAllChild(pos, [
                    newLine(),
                    span, newLine(),
                    select, newLine(),
                    newButton(`exhddl_exclude_${gid}`, "Add/Remove", style_list.gallery_button, () => { updateExcludeList(gid); }),
                ]);
            });

            function newSelect(data_list) {
                let select = document.createElement("select");
                if (data_list.length == 0) return select;
                data_list.forEach(data => { if (data.length > 0) select.appendChild(newOption(data)); });
                return select;
            }

            function newOption(text) {
                return Object.assign(document.createElement("option"), { textContent: text });
            }

            function updateExcludeList(gid) {
                let gallery = document.querySelector(`[gid="${gid}"]`);
                if (!gallery) return;
                let up = "uploader:";
                let exclude = gallery.querySelector("select").selectedOptions[0].textContent;
                let update_key = exclude.includes(up) ? "exclude_uploader" : "exclude_tag";

                if (exclude.includes(up)) exclude = exclude.replace(up, "");
                updateByKey(update_key, exclude);

                function updateByKey(key, value) {
                    let list = getGMList(key);
                    let index = list.indexOf(value);
                    if (index == -1) {
                        list.push(value);
                        print(`${m}add [${value}] to list [${key}]`);
                    } else {
                        list.splice(index, 1);
                        print(`${m}remove [${value}] from list [${key}]`);
                    }
                    setGMData(key_list[key], list);
                    //GM_setValue(key_list[key], list.join());
                }
            }
        }
    }

    function setCopyTitle(gallery) {
        let gid = gallery.getAttribute("gid");
        let pos = gallery.querySelector(`#gallery_status_${gid}`);
        let button = newButton(`copy_title_${gid}`, "Copy Title", style_list.gallery_button, function () {
            navigator.clipboard.writeText(repalceForbiddenChar(document.querySelector(`[gid="${gid}"] .glname`).textContent.trim()));
        });
        pos.insertAdjacentElement("beforebegin", button);
        pos.insertAdjacentElement("beforebegin", newLine());
    }

    function setShowTorrent(gallery) {
        let gid = gallery.getAttribute("gid");
        let torrent_list = getTorrentList(gid);
        let pos = gallery.querySelector(".gallery_box");
        let button = newButton(`t_title_${gid}`, "Show torrent List", style_list.gallery_button, function () {
            let torrent_list = document.querySelector(`[gid="${gid}"] .torrent_title`);
            torrent_list.style.display = (torrent_list.style.display == "none") ? "" : "none";
        });
        pos.insertAdjacentElement("beforeend", newLine());
        pos.insertAdjacentElement("beforeend", button);

        if (torrent_list) {
            let box = Object.assign(document.createElement("div"), { className: "torrent_title", style: "display:none" });
            torrent_list.forEach(torrent => { box.appendChild(Object.assign(newSpan(torrent), { className: "puretext", })); });
            pos.insertAdjacentElement("beforebegin", box);
        } else {
            button.disabled = true;
            button.removeAttribute("onclick");
        }
    }

    function extracContainer() {
        let [start, end] = ["", ""];
        for (let c of container_list) {
            start += c[0];
            end += c[1];
        }
        return [start, end];
    }

    function containerRegexGenerator() {
        let reg = [];
        let esc_reg = /[-\/\\^$*+?.()|[\]{}]/g;
        for (let c of container_list) {
            let esc = c.replace(esc_reg, "\\$&");
            let end = esc.slice(esc.length / 2);
            let start = esc.replace(end, "");
            reg.push(`${start}[^${esc}]*${end}`);
        }
        return `(${reg.join("|")})`;
    }

    function forEachGallery(handler) {
        for (let index = 0, all = selectAllGallery(), length = all.length; index < length; index++) handler(all[index]);
    }

    function selectAllGallery() {
        return document.querySelectorAll(".gl1t");
    }

    function print(...any) {
        if (debug_message) console.log(...any);
    }

    function time(tag = "") {
        if (debug_message) return tag ? console.time(tag) : console.time();
    }

    function timeEnd(tag = "") {
        if (debug_message) return tag ? console.timeEnd(tag) : console.timeEnd();
    }

    function group() {
        if (debug_message) return console.groupCollapsed();
    }

    function groupEnd() {
        if (debug_message) return console.groupEnd();
    }

    function dPrint(...any) {
        if (debug_message && debug_adv) console.log(...any);
    }

    function dTime(tag = "") {
        if (debug_message && debug_adv) return tag ? console.time(tag) : console.time();
    }

    function dTimeEnd(tag = "") {
        if (debug_message && debug_adv) return tag ? console.timeEnd(tag) : console.timeEnd();
    }

    function dGroup() {
        if (debug_message && debug_adv) return console.groupCollapsed();
    }

    function dGroupEnd() {
        if (debug_message && debug_adv) return console.groupEnd();
    }

    function timerMananger() {
        document.addEventListener("visibilitychange", () => {
            if (timer_list.length > 0) {
                let pause = (document.visibilityState === "visible") ? false : true;
                for (let timer of timer_list) {
                    if (!pause) {
                        timer.id = setInterval(timer.handler, timer.delay);
                        //dPrint(`${m}start timer[${timer.note}] id:${timer.id}`);
                    } else {
                        clearInterval(timer.id);
                        //dPrint(`${m}stop timer[${timer.note}] id:${timer.id}`);
                    }
                }
            }
        });
    }

    function addTimer(handler, delay, note = "") {
        if (note == "") note = handler.name;
        timer_list.push({
            id: setInterval(handler, delay),
            handler: handler,
            delay: delay,
            note: note,
        });
    }

    function findEleByText(css_selector, string) {
        let es = document.querySelectorAll(css_selector);
        for (let ele of es) {
            if (ele.textContent.includes(string)) return ele;
        }
        return false;
    }

    function visitGallery(link) {
        if (link) {
            // trigger :visited
            let current = window.location.href;
            history.pushState({}, "", link); // add link to history. this will change current winodw link.
            print(`${m}add history, link:${window.location.href}`);
            history.pushState({}, "", current); // change it back.
        }
    }

    function addToDownloadedList(gid) {
        gid = `${gid}`; // convert to string
        let downloaded_list = getGMList("dl_list");
        if (downloaded_list.includes(gid)) return print(`${m}[${gid}] is already in the list, abort`);
        if (downloaded_list.length > gallery_data_limit.max_size && gallery_data_limit.max_size != 0) {
            let count = 0;
            while (downloaded_list.length > gallery_data_limit.max_size) {
                let r = downloaded_list.shift();
                print(`${m}%creach limit, remove [${r}]`, "color:OrangeRed;");
                count++;
                if (count > 100) return print(`${m}unknow error while removing old data, script stop`);
            }
        }
        downloaded_list.push(gid);
        let list_length = downloaded_list.length;
        setGMData(key_list.dl_list, downloaded_list);

        //downloaded_list = downloaded_list.join();
        //GM_setValue(key_list.dl_list, downloaded_list);
        print(`${m}add [${gid}] to list. [list_size:${downloaded_list.join().length}, list_length:${list_length}]`);
    }

    function my_popUp(URL, w, h) {
        window.open(
            URL,
            `_pu${Math.random().toString().replace(/0\./, "")}`,
            `toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0` +
            `,width=${w},height=${h},left=${(screen.width - w) / 2},top=${(screen.height - h) / 2}`
        );
        return false;
    }

    function appendAllChild(to_node, nodeList) {
        nodeList.forEach(e => to_node.appendChild(e));
        return to_node;
    }

    function newSpan(text = "") {
        return Object.assign(document.createElement("span"), { textContent: text, });
    }

    function newLine() {
        return document.createElement("br");
    }

    function newSeparate() {
        let sep = newSpan("/");
        Object.assign(sep.style, style_list.separator);
        return sep;
    }

    function newButton(button_id, button_text, button_style, button_onclick) {
        let button = Object.assign(document.createElement("button"), {
            id: button_id,
            textContent: button_text,
            onclick: button_onclick,
        });
        Object.assign(button.style, button_style);
        return button;
    }

    function extractPrefix(string = "") {
        let reg = /^\([^\(\)]*\)/;
        let prefix = reg.exec(string);
        if (prefix) {
            prefix = prefix[0];
            if (!ignore_prefix.some(text => prefix == text)) return [prefix, string.replace(prefix, "").trim()];
        }
        return ["", string];
    }

    function extractGroup(string = "") {
        let group = group_reg.exec(string);
        return group ? [group[0], (string.replace(group[0], "")).trim()] : ["", string];
    }

    function removeExcess(text = "") {
        // remove excess text
        let count = 0;
        while (count < 100 && text.match(excess_reg)) {
            text = text.replace(excess_reg, "");
            count++;
        }
        // remove container if it at start or end
        count = 0;
        while (count < 100) {
            count++;
            let index = container_start.indexOf(text[0]);
            if (index == -1) break;
            text = text.slice(1).trim();
            if (container_end[index] == text[text.length - 1]) text = text.slice(0, text.length - 1).trim();
        }
        text = text.replace(blank_reg, " ");
        return text;
    }

    function fixTitlePrefix() {
        time(`${m}fixTitlePrefix`);
        dGroup();
        fix_prefix = fix_prefix ? false : true;
        dPrint(fix_prefix ? "try fix prefix" : "restore original title");
        forEachGallery(gallery => {
            let id = gallery.getAttribute("gid");
            let tofix = gdata.find(gallery_data => gallery_data.gid == id);
            let title_ele = gallery.querySelector(".glname");
            let prefix = tofix.title_prefix;
            if (fix_prefix) {
                dPrint("==================================================");
                dPrint(`[%c${String(id).padStart(10)} ${tofix.title_jpn}%c]`, "color:DarkOrange;", "");
                if (prefix.length > 0) {
                    let checklist = [
                        title_ele.innerHTML,
                        tofix.title,
                        tofix.title_original,
                        tofix.title_jpn,
                    ];
                    ignore_prefix.forEach(ignore => checklist.push(ignore));
                    if (!checklist.some(title => title.includes(prefix))) {
                        tofix.title = `${prefix} ${tofix.title_original}`;
                        tofix.title_jpn = `${prefix} ${tofix.title_jpn}`;
                        title_ele.insertAdjacentElement("afterbegin", Object.assign(newSpan(`${prefix} `), { style: tofix.from_other_gallery ? "color:blueviolet" : "color:green;" }));
                        if (tofix.from_other_gallery) gallery.querySelector(".prefix_from").style.display = "";
                        dPrint(`add prefix "${prefix}" from self`);
                    } else {
                        dPrint(`skip`);
                    }
                } else {
                    // search in same title gallery
                    let same_title = gdata.find(gallery_data => ((gallery_data.title_pure_for_sim == tofix.title_pure_for_sim || gallery_data.title_pure_jpn_for_sim == tofix.title_pure_jpn_for_sim) && (gallery_data.title_prefix.length > 0)));
                    let by_sim = "";
                    dPrint(`same_title [%c${same_title ? (same_title.gid + " " + same_title.title_pure_jpn) : "not found"}%c]`, "color:OrangeRed;", "");
                    if (!same_title && enable_sim_search) {
                        // try similarity search
                        let search_key = ["title_pure_for_sim", "title_pure_jpn_for_sim",];
                        let search_result = [];
                        let timetag = `sim search`;
                        dTime(timetag);
                        for (let key of search_key) {
                            let best = similaritySearch(tofix, key);
                            if (best) {
                                search_result.push(best);
                                if (best.sim == 1) break;
                            }
                        }
                        dTimeEnd(timetag);
                        if (search_result.length > 0) {
                            search_result = search_result.sort((a, b) => b.sim - a.sim);
                            let sim = search_result[0].sim;
                            dPrint(`best: `, search_result[0]);
                            search_result = gdata.find(gallery_data => gallery_data.gid == search_result[0].gid);
                            if (search_result) {
                                if (checkNumberInTitle(tofix.title_pure_jpn_for_sim, search_result.title_pure_jpn_for_sim)) {
                                    [same_title, by_sim] = [search_result, ` ${sim}`];
                                } else {
                                    print(`similarity search found [%c${search_result.gid}%c] (${sim}) but failed in final test, abort\n`, "color:DarkOrange", "");
                                }
                            }
                        } else {
                            dPrint(`sim search result: ${search_result.length}`);
                        }
                    }
                    if (same_title) {
                        let new_prefix = same_title.title_prefix;
                        tofix.title = `${new_prefix} ${tofix.title_original}`;
                        tofix.title_jpn = `${new_prefix} ${tofix.title_jpn_original}`;
                        tofix.title_prefix = new_prefix;
                        tofix.from_other_gallery = same_title.from_other_gallery ? same_title.from_other_gallery : same_title.gid;
                        title_ele.insertAdjacentElement("afterbegin", Object.assign(newSpan(`${new_prefix} `), { style: "color:blueviolet;" }));

                        // add span to show where the prefix came from
                        let from = gallery.querySelector(".prefix_from");
                        if (!from) {
                            let pos = gallery.querySelector(".gl3t");
                            let box = Object.assign(document.createElement("div"), { className: "prefix_from" });
                            let img_source_div = document.querySelector(`[gid="${same_title.gid}"] .gl3t`);
                            let img_source_img = img_source_div.querySelector("img");
                            let img = Object.assign(document.createElement("img"), {
                                src: img_source_img.src,
                                style: `height:${img_source_img.style.height};width:${img_source_img.style.width};`,
                            });
                            let img_div = Object.assign(document.createElement("div"), {
                                style: `height:${img_source_div.style.height};width:${img_source_div.style.width};`,
                            });
                            img_div.appendChild(img);
                            let nodelist = [
                                img_div,
                                newSpan(`prefix from: ${same_title.gid}`),
                                newSpan(`\n${new_prefix} ${same_title.title_no_event_jpn}`),
                            ];
                            appendAllChild(box, nodelist);
                            let pt = gallery.querySelector("div.puretext");
                            if (pt) {
                                pt.insertAdjacentElement("beforebegin", box);
                            } else {
                                pos.insertAdjacentElement("afterend", box);
                            }
                        } else {
                            from.style.display = "";
                        }

                        let style = ["color:DarkOrange;", "", "color:OrangeRed;", "", "color:DeepPink;"];
                        print(`[%c${String(id).padStart(10)} ${tofix.title_pure_jpn}%c] add prefix "${new_prefix}" from\n[%c${String(same_title.gid).padStart(10)} ${same_title.title_pure_jpn}%c]%c${by_sim}`, ...style);
                    }
                }
            } else {
                dPrint(`restore [%c${String(id).padStart(10)} ${tofix.title_jpn_original}%c]`, "color:DarkOrange;", "");
                tofix.title = tofix.title_original;
                tofix.title_jpn = tofix.title_jpn_original;
                title_ele.innerHTML = tofix.title_jpn ? tofix.title_jpn : tofix.title;
                let from = gallery.querySelector(".prefix_from");
                if (from) from.style.display = "none";
            }
            let title_puretext = gallery.querySelector(`[name="${id_list.puretext}"]`);
            if (title_puretext) title_puretext.innerHTML = title_ele.innerHTML;
        });
        dGroupEnd();
        timeEnd(`${m}fixTitlePrefix`);

        function checkNumberInTitle(a, b) {
            let test = /總集篇|総集編|soushuuhen/g;
            let [na, nb] = [a.match(test), b.match(test)];
            let style = ["color:DarkOrange;", "", "color:OrangeRed;", "", "color:DarkOrange;", "", "color:OrangeRed;", ""];

            if ((na || nb) && !(na && nb)) {
                print(`only found 1 match use regexp ${test} , abort\n${a}\n${b}`);
                return false;
            }

            test = tester(a, b, getNumber);
            if (test) return true;
            if (test != null) return false;

            test = tester(a, b, utf8Number);
            if (test) return true;
            if (test != null) return false;

            return maskTest(a, b);

            function tester(a, b, test) {
                let [na, nb] = [test(a), test(b)];
                print(`${test.name} [%c${a}%c, %c${b}%c] >>> [%c${na}%c, %c${nb}%c]`, ...style);
                if (na && nb) {
                    if (na.length != nb.length) return false;
                    return (na.every((data, index) => data == nb[index])) ? true : false;
                }
                return (!na && !nb) ? null : false;
            }

            function getNumber(input) {
                let reg = /\d/g;
                return reg.test(input) ? [input.match(reg)].flat() : false;
            }

            function utf8Number(input) {
                let number_system = [
                    "²³¹⁰⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞⅟ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻ",
                    "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛",
                    "⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵",
                    "ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ",
                    "⓪⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴⓵⓶⓷⓸⓹⓺⓻⓼⓽⓾⓿❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓",
                    "㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉一七三九二五伍八六十叁参參叄四壱壹弐拾捌柒玖肆貳贰陆陸零",
                    "0123456789𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿",
                    "上下中前后後",
                ].join("");
                let reg_number_system = new RegExp(`[\d${number_system}]`, "g");
                return reg_number_system.test(input) ? [input.match(reg_number_system)].flat() : false;
            }

            function maskTest(a, b) {
                let mask_a = a.replace(new RegExp(`[${b}]`, "g"), "");
                let mask_b = b.replace(new RegExp(`[${a}]`, "g"), "");
                print(`mask test [%c${a}%c, %c${b}%c] >>> [%c${mask_a}%c, %c${mask_b}%c]`, ...style);
                return (!mask_a && !mask_b) ? true : false;
            }
        }

        function similaritySearch(target, key = "", threshold = sim_search_threshold) {
            if (!key || !target) return;
            if (gdata.length == 0) return;
            let best_match;
            findBestMatch();
            if (!best_match) return;
            return best_match;

            function findBestMatch() {
                for (let g of gdata) {
                    if (g.gid == target.gid) continue;
                    if (!target[key] || !g[key]) continue;
                    if (g.title_prefix.length == 0) continue;
                    let sim = similarity(target[key], g[key]);
                    let better = false;
                    if (sim > threshold) {
                        let style = ["color:DarkOrange;", "", "color:DeepPink;", ""];
                        dPrint(`[%c${String(target.gid).padStart(10)} ${target[key]}%c] use key [${key}] found prefix "${g.title_prefix}" in\n[%c${String(g.gid).padStart(10)} ${g[key]}%c] ${sim}`, ...style);
                        if (!best_match) {
                            better = true;
                        } else {
                            if (sim > best_match.sim) better = true;
                        }
                    }
                    if (better) best_match = { gid: g.gid, sim: sim, };
                }
            }

            //https://stackoverflow.com/a/36566052/13800616
            function similarity(s1, s2) {
                var longer = s1;
                var shorter = s2;
                if (s1.length < s2.length) {
                    longer = s2;
                    shorter = s1;
                }
                var longerLength = longer.length;
                if (longerLength == 0) return 1.0;
                return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength);

                function editDistance(s1, s2) {
                    var costs = [];
                    for (var i = 0; i <= s1.length; i++) {
                        var lastValue = i;
                        for (var j = 0; j <= s2.length; j++) {
                            if (i == 0) {
                                costs[j] = j;
                            } else {
                                if (j > 0) {
                                    var newValue = costs[j - 1];
                                    if (s1.charAt(i - 1) != s2.charAt(j - 1)) newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
                                    costs[j - 1] = lastValue;
                                    lastValue = newValue;
                                }
                            }
                        }
                        if (i > 0) costs[s2.length] = lastValue;
                    }
                    return costs[s2.length];
                }
            }
        }
    }

    function removeAllPunctuation(input = "") {
        let reg = /[\u2000-\u206F\u2E00-\u2E7F\u3000-\u303F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~\s ]/g;
        return shiftCode(input).replace(reg, "");
    }

    function shiftCode(string = "") {
        let reg_fullwidth_code = /[\uFF01-\uFF63]/g;
        let reg_muti_blank = /[\s \n\t]+/g;
        return string.replace(reg_fullwidth_code, match => String.fromCharCode(match.charCodeAt(0) - 0xFEE0)).replace(reg_muti_blank, "").trim();
    }

    function decodeHTMLString(input = "") {
        let parser = new DOMParser();
        let text = parser.parseFromString(input, "text/html").documentElement.textContent;
        parser = null;
        return text;
    }

    function getTorrentList(gid = "") {
        let torrents = gdata.find(gallery_data => gallery_data.gid == gid);
        if (!torrents) {
            print(`${m}${gdata.gid} have no torrents ???`);
            return false;
        }
        torrents = torrents.torrents;
        if (torrents.length == 0) return false;
        return torrents.map(torrent => decodeHTMLString(torrent.name)).reverse();
    }

    function addInfoToGallery(gallery) {
        let link = gallery.querySelector("a").href;
        let id = link.split("/g/")[1].split("/")[0];
        let token = link.split("/g/")[1].split("/")[1];
        let title = gallery.querySelector(".glname").textContent;
        gallery.setAttribute("gid", id);
        gallery.setAttribute("gtoken", token);
        gallery.setAttribute("gtitle", title);
    }

    function setLinkToNewTab(gallery) {
        gallery.querySelectorAll("a").forEach(a => { if (a.href.includes("/g/")) a.target = "_blank"; });
    }

    function updateGalleryStatus() {
        let dl_list = getGMList("dl_list");

        let find_button = document.querySelector(".itg.gld");
        if (!find_button) return print(`${m}gallery list not found`);

        find_button = find_button.querySelectorAll("button");
        if (find_button.length > 0) { updateButtonStatus(); updateExclude(); }
        updateGalleryColor();

        function updateExclude() {
            if (gdata.length == 0) return;
            let ex_uploader = getGMList("exclude_uploader");
            let ex_tag = getGMList("exclude_tag");
            gdata.forEach(gallery_data => {
                let match_uploader = ex_uploader.includes(gallery_data.uploader);
                let match_tag = gallery_data.tags.some(tag => ex_tag.includes(tag));
                let gallery = document.querySelector(`[gid="${gallery_data.gid}"]`);
                gallery.style.opacity = (match_uploader || match_tag) ? 0.1 : 1;
                gallery.querySelector("img").style.display = (match_uploader || match_tag) ? "none" : "";
                let options = gallery.querySelectorAll("option");
                if (options) { options.forEach(o => { o.style.color = (ex_tag.includes(o.textContent) || ex_uploader.includes(o.textContent.replace("uploader:", ""))) ? "red" : ""; }); }
            });
        }

        function updateGalleryColor() {
            let marked = [];
            forEachGallery(gallery => {
                let id = gallery.getAttribute("gid");
                let puretext = gallery.querySelector(".puretext");
                if (dl_list.includes(id)) {
                    if (!gallery.getAttribute("marked")) {
                        if (!gallery.querySelector("s")) Object.assign(gallery.style, style_list.gallery_marked);
                        gallery.setAttribute("marked", true);
                        marked.push(id);
                        if (isEH()) gallery.querySelector(".gl5t").style = "color:white;";
                    }
                    if (puretext && isEH()) puretext.setAttribute("style", "color:white;");
                } else {
                    if (isEH()) {
                        gallery.querySelector(".gl5t").removeAttribute("style");
                        if (puretext) puretext.removeAttribute("style");
                    }
                }
            });
            if (marked.length > 0) print(`${m}found in list, mark gallery:\n`, marked);

            function isEH() {
                return document.domain == "e-hentai.org" ? true : false;
            }
        }

        function updateButtonStatus() {
            let marked = [];
            gdata.forEach(gallery_data => {
                let gid = gallery_data.gid;
                let dl_button = document.querySelector(`#gallery_dl_${gid}`);
                if (dl_list.indexOf(`${gid}`) != -1 && !dl_button.getAttribute("marked")) {
                    Object.assign(dl_button.style, style_list.button_marked);
                    dl_button.setAttribute("marked", true);
                    marked.push(gid);
                }
            });
            if (marked.length > 0) print(`${m}found in list, mark dl_button: ${marked}`);
        }
    }

    const forbidden = `<>:"/|?*\\`;
    const replacer = `<>:”/|?*\`;
    const regesc = t => t.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
    function repalceForbiddenChar(text = "") {
        for (let index in forbidden) {
            text = text.replace(new RegExp(regesc(forbidden[index]), "g"), replacer[index]);
        }
        return text.trim();
    }

    function setGMData(key, data) {
        return GM_setValue(key, data);
    }

    function getGMList(key = "") {
        let value = GM_getValue(key_list[key], default_value[key]);

        if (typeof value == "string") {
            return value.length > 0 ? value.split(",") : [];
        }

        if (value instanceof Array) {
            return value;
        }
    }

    function getGMValue(key = "") {
        return GM_getValue(key_list[key], default_value[key]);
    }
})();