Civitai Text Downloader Mod

Click Download button will download the file and save the JSON and images at the same time. Also add a button to download JSON and images under details.

// ==UserScript==
// @name            Civitai Text Downloader Mod
// @name:ja         Civitai Text Downloader Mod
// @namespace       http://tampermonkey.net/
// @version         5.3.7
// @description     Click Download button will download the file and save the JSON and images at the same time. Also add a button to download JSON and images under details.
// @description:ja  Downloadボタンをクリックするとファイルのダウンロードと同時にJSONと画像が保存されます。また、Detailsの下にJSONと画像をダウンロードするボタンを追加します。
// @author          Takenoko3333
// @match           https://civitai.com/*
// @match           https://civitai.green/*
// @icon            https://civitai.com/favicon.ico
// @grant           GM.addStyle
// @grant           GM.xmlHttpRequest
// @connect         civitai.com
// @connect         civitai.green
// @license         BSD
// ==/UserScript==

(function() {
    'use strict';
    GM.addStyle(`.ctd-button:not([data-disabled]) {color: #ffff00;}`);
    GM.addStyle(`a[data-tour="model:download"] {color: #ffff00;}`);

    const CTD_SELECTOR = 'main a[data-tour="model:download"], .json-image-download-button, .mantine-Accordion-panel .mantine-Button-root';
    let file_id = null;
    let category = "";

    // Processing by Browser
    if (typeof MutationObserver !== 'undefined') {
        let lastUrl = location.href;
        let observerTriggered = false;
        const observer = new MutationObserver((mutations) => {
            if (lastUrl !== location.href) {
                lastUrl = location.href;
                observerTriggered = true;
                const interval = setInterval(function() {
                    setupDownload();
                    if (file_id || category != "models" ) {
                        clearInterval(interval);
                    }
                }, 200);
            }
        });
        const config = { subtree: true, childList: true };
        observer.observe(document, config);
        // URL change not detected (initial access)
        setTimeout(() => {
            if (!observerTriggered) {
                if (isFirefox()) {
                    setupDownloadForFirefox();
                } else {
                    setupDownload();
                }
            }
        }, 0);
    } else {
        // MutationObserver is not supported
        setInterval(function(){
            setupDownload();
        }, 500);
    }

    // Process to prevent multiple download executions of the same click
    let ctdLastClickAt = 0;
    let ctdLastClickKey = '';

    document.addEventListener('click', (ev) => {
        const btn = ev.target.closest(CTD_SELECTOR);
        if (!btn) return;

        // Skip if the same element has been processed within the last 500ms
        const now = Date.now();
        const key = btn;
        if (key === ctdLastClickKey && (now - ctdLastClickAt) < 500) return;
        ctdLastClickKey = key;
        ctdLastClickAt = now;

        // Do not preventDefault here (official download behavior is maintained)
        // Only save JSON & images Additional Execution
        try {
            jsonAndImageDownload();
        } catch (e) {
            console.error('CTD error:', e);
        }
    }, /* useCapture = */ true); // Performed only once early


    function isFirefox() {
        const isFirefoxBrowser = typeof InstallTrigger !== 'undefined';
        return isFirefoxBrowser;
    }

    function setupDownloadForFirefox() {
        let counter = 0;
        const interval = setInterval(function() {
            setupDownload();
            counter++;
            if (category != "models" || counter >= 3) {
                clearInterval(interval);
            }
        }, 500);
    }

    function createButton() {
        const newElement = document.createElement('a');
        newElement.className = 'json-image-download-button';
        newElement.type = 'button';
        newElement.setAttribute('data-button', 'true');
        newElement.style.display = 'inline-block';
        newElement.style.marginTop = '8px';
        newElement.style.cursor = 'pointer';

        const innerDiv = document.createElement('div');
        innerDiv.className = 'mantine-3xbgk5 mantine-Button-inner';

        const innerSpan = document.createElement('span');
        innerSpan.className = 'mantine-qo1k2 mantine-Button-label';
        innerSpan.textContent = 'JSON and Image Download';

        const outerDiv = document.createElement('div');
        outerDiv.className = 'json-image-download-button-outer';

        innerDiv.appendChild(innerSpan);
        newElement.appendChild(innerDiv);
        outerDiv.appendChild(newElement);

        const tableElement = document.querySelector('table');
        let currentElement = tableElement;
        while (currentElement && !currentElement.classList.contains('mantine-Accordion-item')) {
            currentElement = currentElement.parentElement;
        }
        if (currentElement) {
            currentElement.insertAdjacentElement('afterend', outerDiv);
        }
    }

    function setupDownload() {
        file_id = null
        category = location.pathname.split("/")[1];
        if (category != "models") return;

        const codeElements = document.querySelectorAll('table code');
        codeElements.forEach((element, index) => {
            if (element.textContent === '@') {
                if (codeElements[index + 1]) {
                    file_id = codeElements[index + 1].textContent;
                }
            }
        });

        const jsonImageDownloadButton = document.querySelector('.json-image-download-button');
        if (file_id && !jsonImageDownloadButton) createButton();

        document.querySelectorAll(CTD_SELECTOR).forEach(button => {
            if (file_id && !jsonImageDownloadButton) button.classList.add("ctd-button");
        });
    }

    function jsonAndImageDownload() {
        if (category !== "models" || !file_id) return;

        const modelName = document.querySelector('main h1')?.textContent || '';
        const spanElements = document.querySelectorAll('table span');
        let strength = null;
        let _id = location.pathname.split("/")[2];

        spanElements.forEach((element) => {
            if (/^Strength:/.test(element.textContent)) {
                strength = element.textContent.split(":")[1].trim();
            }
        });

        GM.xmlHttpRequest({
            method: "GET",
            url: location.origin + "/api/v1/models/" + _id,
            onload: function(response) {
                let j = JSON.parse(response.responseText);
                let file = j.modelVersions.find(x => x.id == file_id);
                let link = document.createElement('a');
                let text = {
                    "description": "",
                    "model name": modelName,
                    "model url": document.URL,
                    "base model": file.baseModel,
                    "sd version": "Unknown",
                    "activation text": "",
                    "preferred weight": 0,
                    "notes": document.URL + "\nModel name: " + modelName + "\nBase model: " + file.baseModel
                };

                if (/^SD 1/.test(file.baseModel)) {
                    text["sd version"] = "SD1";
                } else if (/^SD 2/.test(file.baseModel)) {
                    text["sd version"] = "SD2";
                } else if (/^SDXL/.test(file.baseModel) || /^Pony/.test(file.baseModel)) {
                    text["sd version"] = "SDXL";
                } else if (/^SD 3/.test(file.baseModel)) {
                    text["sd version"] = "SD3";
                }

                if (j.description && j.description.textContent) {
                    text.description = j.description.textContent;
                }
                if (file.trainedWords) {
                    text["activation text"] = file.trainedWords.join(" ");
                }
                if (strength) {
                    text["preferred weight"] = strength;
                }

                const blobText = [JSON.stringify(text)];
                link.href = window.URL.createObjectURL(new Blob(blobText));
                let filename = (file.files.find(x => x)?.name || (file_id + ".json")).replace(/\.[a-z]*$/, ".json");
                link.download = filename;
                link.click();

                let image = file.images.find(x => x.type === 'image');
                if (image) {
                    GM.xmlHttpRequest({
                        method: "GET",
                        url: image.url,
                        responseType: "blob",
                        onload: function (resp) {
                            let dlLink = document.createElement("a");
                            const dataUrl = URL.createObjectURL(resp.response);
                            dlLink.href = dataUrl;
                            let suffix = "." + image.url.slice(image.url.lastIndexOf('.') + 1).toLowerCase();
                            if (suffix === ".jpeg") suffix = ".jpg";
                            dlLink.download = filename.replace(".json", suffix);
                            document.body.insertAdjacentElement("beforeEnd", dlLink);
                            dlLink.click();
                            dlLink.remove();
                            setTimeout(() => URL.revokeObjectURL(dataUrl), 1000);
                        }
                    });
                }
            }
        });
    }
})();