Civitai Direct Link Helper

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

// ==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);
    }

})();