VNDB 跳转

VNDB跳转其他网站(自用)

// ==UserScript==
// @name         VNDB 跳转
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @license      MIT
// @description  VNDB跳转其他网站(自用)
// @author       高龄啊
// @match        https://vndb.org/v*
// @match        https://seiya-saiga.com/game/kouryaku.html?search=*
// @match        https://bbs.kfpromax.com/index.php?search=*
// @icon         https://www.vndb.org/favicon.ico
// @grant        GM_xmlhttpRequest
// @connect      erovoice.us
// @connect      anime-sharing.com
// @connect      asmrconnecting.xyz
// @connect      sunshineboy.top
// @connect      bangumi.tv
// @connect      2dfan.com
// @connect      e-hentai.org
// @connect      bbs.kfpromax.com
// ==/UserScript==

((css) => {
    if (typeof GM_addStyle == "function") {
        GM_addStyle(css);
        return;
    }

    const styleElement = document.createElement("style");
    styleElement.textContent = css;
    document.head.append(styleElement);
})(`
    .rdl-app {
        position: absolute;
        top: 250px; /* 上侧距离 */
        right: 30px; /* 右侧距离 */
        width: 110px; /* 宽度 */
        background-color: #000000; /* 背景色 */
        border-radius: 8px;
        padding: 10px; /* 内边距 */
        z-index: 1000;
    }

    .rdl-list {
        display: flex;
        flex-direction: column;
        gap: 10px;
    }

    .rdl-button {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 8px 12px;
        border-radius: 4px;
        font-size: 14px;
        font-weight: 500;
        color: #fff;
        background-color: #000000; /* 按钮背景色 */
        border: 1px solid #409eff; /* 边框颜色 */
        cursor: pointer;
        text-decoration: none;
        transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
    }

    .rdl-button:visited {
        color: #3377AA; /* 访问过的链接颜色 */
    }

    .rdl-button:hover {
        background-color: #66b1ff;
        border-color: #66b1ff;
    }

    .rdl-button_green {
        background-color: #67c23a;
        border-color: #67c23a;
    }

    .rdl-button_green:hover {
        background-color: #85ce61;
        border-color: #85ce61;
    }

    .rdl-button_red {
        background-color: #f56c6c;
        border-color: #f56c6c;
    }

    .rdl-button_red:hover {
        background-color: #f89898;
        border-color: #f89898;
    }
`);

(function () {
    'use strict';

    initView();
    unlockTitleCopy();

    function initView() {
        const infoTable = document.querySelector('.vndetails');
        if (!infoTable) return;

        const rdlApp = createElement("div", null, "rdl-app");
        const rdlList = createElement("div", null, "rdl-list");

        const siteItems = [
            { "title": "SunshineBoy", "onlyAsmr": false },
            { "title": "animeShare", "onlyAsmr": false },
            { "title": "bangumi", "onlyAsmr": false },
            { "title": "绯月", "onlyAsmr": false },
            { "title": "青桔网", "onlyAsmr": false },
            { "title": "失落小站", "onlyAsmr": false },
            { "title": "稻荷", "onlyAsmr": false },
            { "title": "2DFan", "onlyAsmr": false },
            { "title": "誠也の部屋", "onlyAsmr": true },
            { "title": "ggbases", "onlyAsmr": false },
            { "title": "nyaa", "onlyAsmr": false },
            { "title": "F95", "onlyAsmr": false },
            { "title": "eHentai", "onlyAsmr": false },
            { "title": "DMM", "onlyAsmr": false }
        ];

        siteItems.forEach(item => {
            const element = createElement("a", item.title, "rdl-button");
            element.target = "_blank";
            rdlList.appendChild(element);
        });

        rdlApp.appendChild(rdlList);
        infoTable.parentNode.insertBefore(rdlApp, infoTable.nextSibling);

        checkExits();
    }

    function createElement(tag, text = null, className = null) {
        const element = document.createElement(tag);
        if (text) element.textContent = text;
        if (className) element.className = className;
        return element;
    }

    function findItem(t) {
        const list = document.getElementsByClassName("rdl-list")[0];
        return Array.from(list.children).find(item => item.textContent === t);
    }

    function getSearchParam(paramName) {
        try {
            const url = new URL(window.location.href);
            const param = url.searchParams.get(paramName);
            return param ? decodeURIComponent(param) : null;
        } catch {
            const regex = new RegExp(`[?&]${paramName}=([^&]+)`);
            const match = window.location.href.match(regex);
            return match ? decodeURIComponent(match[1]) : null;
        }
    }

    function normalizeText(text) {
        return text.trim()
                  .replace(/\s+/g, ' ')
                  .replace(/[!-~]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))
                  .toLowerCase();
    }

    function searchAndHighlight(node, keyword) {
        if (!keyword) return;

        const normalizedKeyword = normalizeText(keyword);
        let isFirstMatch = true;

        function processNode(node) {
            if (node.nodeType === Node.TEXT_NODE) {
                const text = node.textContent;
                const normalizedText = normalizeText(text);

                if (normalizedText.includes(normalizedKeyword)) {
                    const span = document.createElement('span');
                    span.textContent = text;
                    span.className = 'keyword-highlight';
                    span.style.backgroundColor = 'yellow';

                    node.replaceWith(span);

                    if (isFirstMatch) {
                        span.scrollIntoView({ behavior: 'smooth', block: 'center' });
                        isFirstMatch = false;
                    }

                    return true;
                }
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                const style = window.getComputedStyle(node);
                if (style.display === 'none' || style.visibility === 'hidden') {
                    return false;
                }

                Array.from(node.childNodes).forEach(child => processNode(child));
            }
            return false;
        }

        processNode(node);
    }

    function init() {
        const keyword = getSearchParam('search');
        console.log('搜索关键词:', keyword);

        if (keyword) {
            if (document.body) {
                searchAndHighlight(document.body, keyword);
            } else {
                document.addEventListener('DOMContentLoaded', () =>
                    searchAndHighlight(document.body, keyword)
                );
            }

            const searchInput = document.querySelector('input[name="keyword"]');
            const searchButton = document.querySelector('input[name="submit"]');

            if (searchInput && searchButton) {
                searchInput.value = keyword;
                searchInput.dispatchEvent(new Event('input', { bubbles: true }));
                searchButton.click();

                console.log(`搜索已触发,关键词: "${keyword}",等待结果加载...`);

                const observer = new MutationObserver(() => {
                    const searchResults = document.querySelector('.search-results');
                    if (searchResults) {
                        console.log('搜索结果已加载:', searchResults);
                        observer.disconnect();
                    }
                });

                observer.observe(document.body, { childList: true, subtree: true });
            } else {
                console.error('未找到搜索框或搜索按钮元素');
            }
        }
    }

    init();

    async function checkExits() {
        const selectors = [
            "body > main > article:nth-child(1) > h2",
            "body > main > article:nth-child(1) > div.vndetails > table > tbody > tr:nth-child(1) > td:nth-child(2) > table > tbody > tr > td:nth-child(2) > span",
            "body > main > article:nth-child(1) > div.vndetails > table > tbody > tr:nth-child(1) > td > details > summary > table > tbody > tr > td:nth-child(2) > span",
            "body > main > article:nth-child(1) > h1" // 更多备选选择器
        ];

        let Titles = null;
        for (const selector of selectors) {
            Titles = document.querySelector(selector);
            if (Titles) break;
        }

        if (Titles) {
            console.log("Titles found:", Titles.innerText);
        } else {
            console.error("No matching element found!");
        }

        setItemLink("绯月", `https://bbs.kfpromax.com/index.php?search=${Titles.innerText}`);
        setItemLink("青桔网", `https://spare.qingju.org/?s=${Titles.innerText}`);
        setItemLink("失落小站", `https://www.shinnku.com/?search=${Titles.innerText}`);
        setItemLink("eHentai", `https://e-hentai.org/?f_search=${Titles.innerText}`);
        setItemLink("DMM", `https://www.dmm.co.jp/search/=/searchstr=${encodeURIComponent(Titles.innerText)}/`);
        setItemLink("animeShare", `https://www.anime-sharing.com/search/3528560/?q=${Titles.innerText}&o=relevance`);
        setItemLink("bangumi", `https://bangumi.tv/subject_search/${Titles.innerText}?cat=4`);
        setItemLink("2DFan", `https://2dfan.com/subjects/search?keyword=${Titles.innerText}`);
        setItemLink("ggbases", `https://www.ggbases.com/search.so?title=${Titles.innerText}`);
        setItemLink("nyaa", `https://nyaa.si/?f=0&c=0_0&q=${Titles.innerText}`);
        setItemLink("F95", `https://f95zone.to/search/040867454/?q=${Titles.innerText}&o=relevance`);
        setItemLink("誠也の部屋", `https://seiya-saiga.com/game/kouryaku.html?search=${Titles.innerText}`);
        setItemLink("稻荷", `https://amoebi.com`);

        const checks = [
            { site: "SunshineBoy", check: SunshineBoyCheck },
            { site: "animeShare", check: animeShareCheck },
        ];

        for (const { site, check } of checks) {
            await checkSite(site, Titles.innerText, check);
        }
    }

    async function checkSite(siteName, Titles, checkFunction) {
        const item = findItem(siteName);
        try {
            await checkFunction(Titles, item);
        } catch (error) {
            console.error(error);
            item.className = "rdl-button rdl-button_red";
        }
    }

    async function SunshineBoyCheck(Titles, item) {
        const url = "https://sunshineboy.top/api/fs/search";
        const postData = JSON.stringify({
            "parent": "/",
            "keywords": Titles,
            "page": 1,
            "scope": 0,
            "per_page": 100,
            "password": ""
        });
        const response = await fetchDataWithPost(url, postData);
        const data = JSON.parse(response.responseText).data;
        const ref = data.content.reduce((acc, contentKey) => {
            if (contentKey.is_dir && contentKey.size < acc.size) {
                const parent = contentKey.parent.replaceAll("/Guest", "");
                return { size: contentKey.size, href: `https://sunshineboy.top${parent}/${contentKey.name}` };
            }
            return acc;
        }, { size: Number.MAX_SAFE_INTEGER, href: "" }).href;
        handleCheckResult(data.total, item, ref, "https://sunshineboy.top");
    }

    async function animeShareCheck(Titles, item) {
        const url = `https://www.anime-sharing.com/search/3528560/?q=${Titles}&o=relevance`;
        const response = await fetchData(url);
        const parser = new DOMParser();
        const doc = parser.parseFromString(response.responseText, 'text/html');
        const voiceIconNodeLength = doc.getElementsByClassName("label label--primary").length;
        handleCheckResult(voiceIconNodeLength > 0, item, url, url);
    }

    function handleCheckResult(condition, item, successLink, failLink) {
        if (condition > 0) {
            item.className = "rdl-button rdl-button_green";
            item.href = successLink;
        } else {
            item.className = "rdl-button rdl-button_red";
            item.href = failLink;
        }
    }

    function fetchData(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: (response) => resolve(response),
                onerror: (error) => reject(error),
            });
        });
    }

    async function fetchDataWithPost(url, postData) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: url,
                data: postData,
                headers: {
                    "Content-Type": "application/json"
                },
                onload: resolve,
                onerror: reject
            });
        });
    }

    function unlockTitleCopy() {
        if (!window.location.href.includes("vndb.org")) return;

        const titleElement1 = document.querySelector("#top_wrapper > ul > li:last-child");
        const titleElement2 = document.querySelector("body > main > article:nth-child(1) > div.vndetails > table > tbody > tr:nth-child(1) > td > details > summary > table > tbody > tr > td:nth-child(2) > span");

        if (titleElement1) {
            titleElement1.style = "user-select:auto";
        }
        if (titleElement2) {
            titleElement2.style = "user-select:auto";
        }
    }

    function setItemLink(Titles, url) {
        const item = findItem(Titles);
        if (item) {
            item.Titles = "rdl-button";
            item.href = url;
        }
    }
})();