Civitai Direct Link Helper

Adds a convenient copy button next to download buttons on Civitai to easily get direct download links for models

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
    }

})();