kemono.su links for ppixiv

Add kemono.su buttons on ppixiv user dropdown

目前為 2023-12-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name         kemono.su links for ppixiv
// @namespace    https://www.pixiv.net/
// @version      1.3.7
// @description  Add kemono.su buttons on ppixiv user dropdown
// @author       EnergoStalin
// @match        https://*.pixiv.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pixiv.net
// @license      AGPL-3.0-only
// @grant        GM_xmlhttpRequest
// @connect      www.patreon.com
// @connect      kemono.su
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const cachedPatreonUsers = {};
    const cachedRedirects = {};
    const labelMatchingMap = {
        "patreon": "patreon.com",
        "fanbox": "Fanbox",
        "fantia": "fantia.jp",
    };
    const labelList = Object.values(labelMatchingMap);

    const patreonIdRegex = /"id":\s*"(\d+)",[\n\s]*"type":\s*"user"/sm;
    const linkRegex = /[\W\s]((?:https?:\/\/)?(?:\w+[\.\/].+){2,})/g;

    function filterInPlace(a, condition) {
        let i = 0, j = 0;

        while (i < a.length) {
            const val = a[i];
            if (condition(val, i, a)) a[j++] = val;
            i++;
        }

        a.length = j;
        return a;
    }

    function notifyUserUpdated(userId) {
        unsafeWindow.ppixiv.userCache.callUserModifiedCallbacks(userId);
    }

    function normalizeUrl(url) {
        url = url.trim();
        if(!url.startsWith("http")) url = `https://${url}`;

        return url;
    }

    function getLinksFromDescription(extraLinks) {
        return removeDuplicates(
            preprocessMatches(
                Array.from(document.body.querySelector(".description").innerText.matchAll(linkRegex)).map(e => e[1])
            ),
            extraLinks
        );
    }

    function removeDuplicates(links, extraLinks) {
        const labels = extraLinks.map(e => e.label);
        return links.filter(e => !labels.includes(e.label));
    }

    function preprocessMatches(matches) {
        return matches.map(e => {
            try {
                const url = new URL(normalizeUrl(e));
                return {
                    label: labelMatchingMap[Object.keys(labelMatchingMap).filter(e => url.host.includes(e))[0]],
                    url
                };
            } catch {
                return {label: null, url: null};
            }
        });
    }

    function normalizePatreonLink(link) {
        if(typeof(link.url) === "string") link.url = new URL(normalizeUrl(link.url));

        link.url.protocol = "https";
        if(!link.url.host.startsWith("www.")) link.url.host = `www.${link.url.host}`;
    }

    async function ripPatreonId(link) {
        const response = await GM.xmlHttpRequest({
            method: "GET",
            url: link,
        });
        return response.responseText.match(patreonIdRegex)[1];
    }

    function patreon(link, extraLinks, userInfo) {
        normalizePatreonLink(link);
        const url = link.url.toString();
        const cachedId = cachedPatreonUsers[url];
        if(!cachedId) {
            ripPatreonId(url).then(id => {
                cachedPatreonUsers[url] = id;
                notifyUserUpdated(userInfo.userId);
            }).catch(console.error)
        } else {
            extraLinks.push({
                url: new URL(`https://kemono.su/patreon/user/${cachedId}`),
                icon: "mat:money_off",
                type: "kemono_patreon",
                label: "Kemono patreon"
            });
        }
    }

    function fanbox(extraLinks, userInfo) {
        extraLinks.push({
            url: new URL(`https://kemono.su/fanbox/user/${userInfo.userId}`),
            icon: "mat:money_off",
            type: "kemono_fanbox",
            label: "Kemono fanbox"
        });
    }

    function fantia(link, extraLinks) {
        const id = link.url.toString().split("/").pop();
        extraLinks.push({
            url: new URL(`https://kemono.su/fantia/user/${id}`),
            icon: "mat:money_off",
            type: "kemono_fantia",
            label: "Kemono fantia"
        });
    }

    async function checkRedirect(cache, link) {
        const url = link.toString()
        const value = cache[url]
        const cacheHit = value !== undefined
        if(!cacheHit) {
            const response = await GM.xmlHttpRequest({
                method: "GET",
                redirect: "manual",
                url: link,
            });
            cache[url] = response.finalUrl !== link.toString();
        }

        return {cacheHit, value}
    }

    function removeDeadLinks(extraLinks, userInfo) {
        Promise.all(
            extraLinks
            .filter(e => e.label?.includes("Kemono"))
            .map(e => checkRedirect(cachedRedirects, e.url))
        ).then((e) =>
            // Fire user update if at least one cache miss occured
            e.reduce((p, n) => p + n.cacheHit, 0) === e.length ? undefined : notifyUserUpdated(userInfo.userId)
        ).catch(console.error)

        filterInPlace(extraLinks, e => !cachedRedirects[e?.url?.toString()])
    }

    function addUserLinks({ extraLinks, userInfo }) {
        for(const link of [...extraLinks, ...getLinksFromDescription(extraLinks)]) {
            switch(link.label) {
                case "Fanbox": fanbox(extraLinks, userInfo); break;
                case "patreon.com": patreon(link, extraLinks, userInfo); break;
                case "fantia.jp": fantia(link, extraLinks); break;
            }
        }

        removeDeadLinks(extraLinks, userInfo)
    }

    unsafeWindow.vviewHooks = {
        addUserLinks
    };
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址