Civitai 下载助手

在 Civitai 的下载按钮旁添加复制按钮,轻松复制直链地址 还有去广告

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Civitai Direct Link Helper
// @name:zh-CN   Civitai 下载助手
// @namespace    http://tampermonkey.net/
// @version      1.21
// @description  Adds a convenient copy button next to download buttons on Civitai to easily get direct download links for models
// @description:zh-CN  在 Civitai 的下载按钮旁添加复制按钮,轻松复制直链地址 还有去广告
// @author       hua
// @match        https://civitai.com/*
// @match        https://civitai.green/*
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// @license      MIT
// ==/UserScript==




(function () {
    'use strict';
    unsafeWindow.setInterval = function (fn, time) {
    };
    const origin_setTimeout = unsafeWindow.setTimeout;
    unsafeWindow.setTimeout = function (fn, time) {
        const tags = ['schedule', 'coreAdServerStart', 'exited', 'maybeFetchNotificationAndTrackCurrentUrl', 'googletagservices', '/api/internal/activity', 'iframe_api'];
        if (tags.some(tag => fn.toString().includes(tag))) {
            return;
        }
        function fn_() {
            fn();
        }
        origin_setTimeout(fn_, time);
    };

    hookCreateElement();
    changeInfo();
    modifywebpack();
    function modifywebpack() {
        let webpackChunk_N_E;
        const hookPush = () => {
            const originPush = webpackChunk_N_E.push;
            webpackChunk_N_E.push = function (chunk) {
                const funs = chunk?.[1];
                if (funs?.['68714'] && !funs['68714'].inject) {
                    let funStr = funs['68714'].toString();
                    // funStr = funStr.replace('function(e,i,t){', 'function(e,i,t){debugger;');
                    // funStr = funStr.replace('function(e,t,n){"use strict";', 'function(e,t,n){"use strict";debugger;');
                    let match_tag = funStr.match(/return (.{1,5})\.length\?\(0,/);
                    if (match_tag) {
                        const tag = match_tag[1];
                        funStr = funStr.replace(`return ${tag}.length?(0,`, `${tag}=${tag}.filter(item => item.type !== "ad");return ${tag}.length?(0,`);
                    }
                    funs['68714'] = new Function('return ' + funStr)();
                    funs['68714'].inject = true;
                }
                if (funs?.['56053'] && !funs['56053'].inject) {
                    let funStr = funs['56053'].toString();
                    // funStr = funStr.replace('function(e,t,i){"use strict";', 'function(e,t,i){"use strict";debugger;');
                    let match_tag = funStr.match(/children\:(.{1,5})\.map\(\(/);
                    if (match_tag) {
                        const tag = match_tag[1];
                        const re_match = funStr.match(/return\(0,(.{1,5}).jsx\)\("div"/);
                        if (re_match) {
                            const re_str = re_match[0];
                            funStr = funStr.replace(re_str, `${tag}.forEach((item,i)=>{ ${tag}[i] = item.filter(ite => ite.data.type !== "ad")});${re_str}`);
                        }
                    }
                    funs['56053'] = new Function('return ' + funStr)();
                    funs['56053'].inject = true;
                }
                originPush.call(this, chunk);
            };
        };
        Object.defineProperty(unsafeWindow, 'webpackChunk_N_E', {
            get: function () {
                return webpackChunk_N_E;
            },
            set: function (value) {
                webpackChunk_N_E = value;
                hookPush();
            }
        });
    }

    function changeInfo() {
        const originFetch = unsafeWindow.fetch;
        unsafeWindow.fetch = function (url, options) {
            async function fetch_request(response) {
                if (url.includes('/announcement.getAnnouncements')) {
                    try {
                        const data = await response.json();
                        if (data.result?.data?.json) data.result.data.json = [];
                        console.log('modify announcement.getAnnouncements');
                        response = new Response(JSON.stringify(data), response);
                    } catch (e) {
                        console.log('fetch_request error', e);
                    }
                }
                if (url.includes('auth/session')) {
                    try {
                        const data = await response.json();
                        if (data.user) {
                            const user = data.user;
                            user.allowAds = false;
                            console.log('modify auth/session');
                        }
                        response = new Response(JSON.stringify(data), response);
                    } catch (e) {
                        console.log('fetch_request error', e);
                    }
                }
                return response;
            }
            return originFetch(url, options).then(fetch_request);
        };

        let monitorcount = 1;
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.tagName === 'SCRIPT' && node.id === '__NEXT_DATA__') {
                        monitorcount--;
                        console.log('modify __NEXT_DATA__');
                        if (monitorcount <= 0) {
                            observer.disconnect();
                        }
                        modify(node);
                    }
                    
                }
            }
        });

        observer.observe(document.documentElement, {
            childList: true,
            subtree: true
        });
        function modify(node) {
            const initalData = JSON.parse(node.textContent);
            const trpcData = initalData.props?.pageProps?.trpcState?.json;
            if (trpcData) {
                const queries = trpcData.queries || [];
                if (queries.length > 0) {
                    const query = queries[0];
                    const data = query.state?.data || [];
                    const remveIndex = [];
                    data.forEach((item, index) => {
                        const ignoreFlags = ['Announcement', 'Event', 'CosmeticShop'];
                        if (ignoreFlags.includes(item.type)) {
                            remveIndex.push(index);
                        }
                    });
                    remveIndex.reverse();
                    remveIndex.forEach(index => {
                        data.splice(index, 1);
                    });
                }
            }

            const flags = initalData.props?.pageProps?.flags;
            if (flags) {
                flags.adsEnabled = false;
            }
            const session = initalData.props?.pageProps?.session;
            if (session?.user) {
                const user = session.user;
                user.allowAds = false;
            }
            node.textContent = JSON.stringify(initalData);
        }

    }

    function paraseDownloadUrl(button) {
        let originalColor = window.getComputedStyle(button).color;
        const restoreTimeout = 10000;
        let interval = null;
        const restore = () => {
            button.style.color = originalColor;
        };

        const onError = () => {
            clearInterval(interval);
            interval = null;
            button.style.color = '#FF0000';
            setTimeout(() => {
                restore();
            }, restoreTimeout);
        };

        const onSuccess = () => {
            clearInterval(interval);
            interval = null;
            navigator.clipboard.writeText(button.downloadUrl).then(() => {
            }).catch((e) => {
                alert('copy error:' + e.message);
            });
            button.style.color = '#00FF00';
            setTimeout(() => {
                restore();
            }, restoreTimeout);
        };
        if (button.downloadUrl) {
            onSuccess();
            return;
        }
        const uri = button.getAttribute('href');
        interval = setInterval(() => {
            button.style.color = `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)})`;
        }, 100);

        GM_xmlhttpRequest({
            method: "GET",
            url: `https://civitai.com${uri}`,
            timeout: 10000,
            anonymous: false,
            redirect: 'manual',
            maxRedirects: 0,
            onload: function (response) {
                const downloadUrl = response.responseHeaders.match(/location:(.*?)(?:\r?\n)/i)?.[1];
                button.downloadUrl = downloadUrl;
                downloadUrl ? onSuccess() : onError();
            },
            onerror: function (error) {
                console.log('onerror', error);
                onError();
            },
            ontimeout: function () {
                console.log('ontimeout');
                onError();
            }
        });
    }

    function hookDownloadButton(node) {
        let isClick = false;
        let timers = [];
        node.addEventListener('click', function (e) {
            if (isClick) {
                isClick = false;
                return;
            }
            e.preventDefault();
            const timer = setTimeout(() => {
                isClick = true;
                timers.forEach(timer => clearTimeout(timer));
                timers.length = 0;
                node.click();
            }, 300);
            timers.push(timer);
        });
        node.addEventListener('dblclick', function (e) {
            e.preventDefault();
            timers.forEach(timer => clearTimeout(timer));
            timers.length = 0;
            paraseDownloadUrl(node);
        });
    }

    function hookCreateElement() {
        const origin_createElement = unsafeWindow.document.createElement;
        unsafeWindow.document.createElement = function () {
            const node = origin_createElement.apply(this, arguments);
            if (arguments[0].toUpperCase() === 'A') {
                const originSetAttribute = node.setAttribute;
                node.setAttribute = function (name, value) {
                    if (name === 'href' ) {
                        if (value?.startsWith('/api/download/models/')){
                            console.log('hookButton');
                            hookDownloadButton(node);
                        }
                        if (value?.includes('/pricing?utm_campaign=holiday_promo')) {
                            node.style.display = 'none';
                            console.log('hookPricing');
                        }
                    }
                    return originSetAttribute.call(this, name, value);
                };
            }
            if (arguments[0].toUpperCase() === 'IFRAME') {
                return null;
            }
            return node;
        };
        unsafeWindow.document.createElement.toString = origin_createElement.toString.bind(origin_createElement);
    }

})();