Universal Booru Tag Copier

Add a copy button to copy all non-meta tags from major booru sites with settings

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Universal Booru Tag Copier
// @namespace    http://tampermonkey.net/
// @version      1.10
// @description  Add a copy button to copy all non-meta tags from major booru sites with settings
// @author       Zelest Carlyone
// @match        https://danbooru.donmai.us/*
// @match        https://safebooru.donmai.us/*
// @match        https://gelbooru.com/*
// @match        https://*.gelbooru.com/*
// @match        https://rule34.xxx/*
// @match        https://e621.net/*
// @match        https://e926.net/*
// @match        https://konachan.*/*
// @match        https://yande.re/*
// @match        https://*.zerochan.net/*
// @match        https://*.sankakucomplex.com/*
// @match        https://safebooru.org/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";

    // Settings management
    const SETTINGS_KEY = 'booru_tag_copier_settings';

    function getSettings() {
        const defaultSettings = {
            categoryOrder: ['general', 'character', 'copyright', 'artist'],
            filterCensor: false
        };

        try {
            const stored = localStorage.getItem(SETTINGS_KEY);
            return stored ? { ...defaultSettings, ...JSON.parse(stored) } : defaultSettings;
        } catch (e) {
            return defaultSettings;
        }
    }

    function saveSettings(settings) {
        try {
            localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
        } catch (e) {
            console.error('Failed to save settings:', e);
        }
    }

    function cleanTagName(tagName) {
        // List of face emoticons that should keep their underscores
        const emoticons = ["o_o", "0_0", "|_|", "._.", "^_^", ">_<", "@_@", ">_@", "+_+", "+_-", "=_=", "<o>_<o>", "<|>_<|>"];

        // If it's an emoticon, keep it as-is
        if (emoticons.includes(tagName)) {
            return tagName;
        }

        // Otherwise, replace underscores with spaces
        return tagName.replace(/_/g, " ");
    }

    // Detect which site we're on
    function detectSite() {
        const hostname = window.location.hostname.toLowerCase();

        // Danbooru family
        if (hostname.includes("danbooru") || hostname.includes("safebooru.donmai")) {
            return "danbooru";
        }
        // Gelbooru family (includes rule34.xxx, safebooru.org, etc.)
        else if (hostname.includes("gelbooru") || hostname.includes("rule34.xxx") || hostname.includes("safebooru.org")) {
            return "gelbooru";
        }
        // E621/E926 family
        else if (hostname.includes("e621") || hostname.includes("e926")) {
            return "e621";
        }
        // Moebooru family (Konachan, Yande.re, etc.)
        else if (hostname.includes("konachan") || hostname.includes("yande.re")) {
            return "moebooru";
        }
        // Sankaku family
        else if (hostname.includes("sankakucomplex")) {
            return "sankaku";
        }
        // Zerochan
        else if (hostname.includes("zerochan")) {
            return "zerochan";
        }

        return null;
    }

    // Get site-specific selectors
    function getSiteConfig(site) {
        if (site === "danbooru") {
            return {
                tagSection: "#tag-list",
                categories: [
                    { selector: "ul.general-tag-list li", name: "general" },
                    { selector: "ul.character-tag-list li", name: "character" },
                    { selector: "ul.copyright-tag-list li", name: "copyright" },
                    { selector: "ul.artist-tag-list li", name: "artist" },
                ],
                getTagName: (item) => {
                    // Try data-tag-name first
                    const dataName = item.getAttribute("data-tag-name");
                    if (dataName) return dataName;

                    // Fallback: try to get from search link
                    const searchLink = item.querySelector("a.search-tag");
                    if (searchLink) return searchLink.textContent?.trim();

                    return null;
                },
            };
        } else if (site === "gelbooru") {
            return {
                tagSection: "#tag-list, #tag-sidebar",
                categories: [
                    { selector: "li.tag-type-general", name: "general" },
                    { selector: "li.tag-type-character", name: "character" },
                    { selector: "li.tag-type-copyright", name: "copyright" },
                    { selector: "li.tag-type-artist", name: "artist" },
                ],
                getTagName: (item) => {
                    const link = item.querySelector('a[href*="tags="]');
                    return link ? link.textContent?.trim() : null;
                },
            };
        } else if (site === "e621") {
            return {
                tagSection: "#tag-list",
                categories: [
                    { selector: "ul.general-tag-list li.tag-list-item", name: "general" },
                    { selector: "ul.character-tag-list li.tag-list-item", name: "character" },
                    { selector: "ul.copyright-tag-list li.tag-list-item", name: "copyright" },
                    { selector: "ul.artist-tag-list li.tag-list-item", name: "artist" },
                ],
                getTagName: (item) => {
                    const nameAttr = item.getAttribute("data-name");
                    if (nameAttr) return nameAttr;
                    const nameSpan = item.querySelector(".tag-list-name");
                    return nameSpan ? nameSpan.textContent?.trim() : null;
                },
            };
        } else if (site === "moebooru") {
            return {
                tagSection: "#tag-sidebar",
                categories: [
                    { selector: "li.tag-type-general", name: "general" },
                    { selector: "li.tag-type-character", name: "character" },
                    { selector: "li.tag-type-copyright", name: "copyright" },
                    { selector: "li.tag-type-artist", name: "artist" },
                ],
                getTagName: (item) => {
                    const nameAttr = item.getAttribute("data-name");
                    if (nameAttr) return nameAttr;
                    const link = item.querySelector('a[href*="tags="]');
                    return link ? link.textContent?.trim() : null;
                },
            };
        } else if (site === "sankaku") {
            return {
                tagSection: "#tag-sidebar, .tag-sidebar",
                categories: [
                    { selector: 'li[class*="tag-type-general"]', name: "general" },
                    { selector: 'li[class*="tag-type-character"]', name: "character" },
                    { selector: 'li[class*="tag-type-copyright"]', name: "copyright" },
                    { selector: 'li[class*="tag-type-artist"]', name: "artist" },
                ],
                getTagName: (item) => {
                    const link = item.querySelector("a");
                    return link ? link.textContent?.trim() : null;
                },
            };
        } else if (site === "zerochan") {
            return {
                tagSection: "#tags, .tags",
                categories: [{ selector: 'a[href*="/"]', name: "general" }],
                getTagName: (item) => item.textContent?.trim(),
            };
        }

        return null;
    }

    // Category display names and icons
    const categoryInfo = {
        general: { label: 'General Tags', icon: '🏷️', color: '#0073e6' },
        character: { label: 'Character Tags', icon: '👤', color: '#00aa00' },
        copyright: { label: 'Copyright/Series', icon: '©️', color: '#dd00dd' },
        artist: { label: 'Artist Tags', icon: '🎨', color: '#ee8800' }
    };

    // Create settings panel
    function createSettingsPanel() {
        const settings = getSettings();

        const panel = document.createElement('div');
        panel.id = 'tag-copier-settings';
        panel.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            border: 2px solid #0073e6;
            border-radius: 8px;
            padding: 20px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
            z-index: 10000;
            font-family: sans-serif;
            font-size: 14px;
            color: #333;
            min-width: 350px;
            max-width: 400px;
        `;

        panel.innerHTML = `
            <h3 style="margin: 0 0 15px 0; color: #0073e6;">Tag Copier Settings</h3>

            <div style="margin-bottom: 20px;">
                <label style="display: block; margin-bottom: 8px; font-weight: bold;">
                    Tag Order (drag to reorder):
                </label>
                <div id="category-order-list" style="
                    border: 1px solid #ddd;
                    border-radius: 4px;
                    padding: 5px;
                    background: #f8f9fa;
                ">
                    <!-- Categories will be added here -->
                </div>
                <div style="margin-top: 5px; font-size: 11px; color: #666;">
                    💡 Drag categories to change the order they appear in copied tags
                </div>
            </div>

            <label style="display: block; margin-bottom: 20px; cursor: pointer;">
                <input type="checkbox" id="filterCensor" ${settings.filterCensor ? 'checked' : ''}
                       style="margin-right: 8px;">
                Filter out tags containing "censor"
            </label>

            <div style="text-align: right;">
                <button id="settings-cancel" style="margin-right: 10px; padding: 8px 16px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button>
                <button id="settings-save" style="padding: 8px 16px; background: #0073e6; color: white; border: none; border-radius: 4px; cursor: pointer;">Save</button>
            </div>
        `;

        // Create overlay
        const overlay = document.createElement('div');
        overlay.id = 'tag-copier-overlay';
        overlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 9999;
        `;

        document.body.appendChild(overlay);
        document.body.appendChild(panel);

        // Add category items
        const orderList = panel.querySelector('#category-order-list');
        settings.categoryOrder.forEach((category, index) => {
            const info = categoryInfo[category];
            const item = createCategoryItem(category, info, index);
            orderList.appendChild(item);
        });

        // Setup drag and drop
        setupDragAndDrop(orderList);

        // Event handlers
        panel.querySelector('#settings-save').addEventListener('click', () => {
            const newSettings = {
                categoryOrder: getCategoryOrder(orderList),
                filterCensor: panel.querySelector('#filterCensor').checked
            };
            saveSettings(newSettings);
            closeSettingsPanel();
            showFeedback("⚙️ Settings saved!", "#28a745");
        });

        panel.querySelector('#settings-cancel').addEventListener('click', closeSettingsPanel);
        overlay.addEventListener('click', closeSettingsPanel);
    }

    function createCategoryItem(category, info, index) {
        const item = document.createElement('div');
        item.className = 'category-item';
        item.draggable = true;
        item.dataset.category = category;
        item.style.cssText = `
            background: white;
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 10px;
            margin: 5px 0;
            cursor: move;
            display: flex;
            align-items: center;
            transition: all 0.2s;
        `;

        item.innerHTML = `
            <span style="font-size: 18px; margin-right: 10px;">${info.icon}</span>
            <span style="flex: 1; font-weight: 500;">${info.label}</span>
            <span style="color: #999; font-size: 11px;">☰</span>
        `;

        // Hover effect
        item.addEventListener('mouseenter', () => {
            item.style.background = '#f0f0f0';
            item.style.borderColor = info.color;
        });
        item.addEventListener('mouseleave', () => {
            item.style.background = 'white';
            item.style.borderColor = '#ddd';
        });

        return item;
    }

    function setupDragAndDrop(container) {
        let draggedItem = null;

        container.addEventListener('dragstart', (e) => {
            draggedItem = e.target.closest('.category-item');
            if (draggedItem) {
                draggedItem.style.opacity = '0.5';
                e.dataTransfer.effectAllowed = 'move';
            }
        });

        container.addEventListener('dragend', (e) => {
            if (draggedItem) {
                draggedItem.style.opacity = '1';
            }
        });

        container.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';

            const afterElement = getDragAfterElement(container, e.clientY);
            if (afterElement == null) {
                container.appendChild(draggedItem);
            } else {
                container.insertBefore(draggedItem, afterElement);
            }
        });
    }

    function getDragAfterElement(container, y) {
        const draggableElements = [...container.querySelectorAll('.category-item:not(.dragging)')];

        return draggableElements.reduce((closest, child) => {
            const box = child.getBoundingClientRect();
            const offset = y - box.top - box.height / 2;

            if (offset < 0 && offset > closest.offset) {
                return { offset: offset, element: child };
            } else {
                return closest;
            }
        }, { offset: Number.NEGATIVE_INFINITY }).element;
    }

    function getCategoryOrder(container) {
        const items = container.querySelectorAll('.category-item');
        return Array.from(items).map(item => item.dataset.category);
    }

    function closeSettingsPanel() {
        const panel = document.getElementById('tag-copier-settings');
        const overlay = document.getElementById('tag-copier-overlay');
        if (panel) panel.remove();
        if (overlay) overlay.remove();
    }

    // Wait for the page to load and add button
    function addCopyButton() {
        const site = detectSite();
        if (!site) {
            console.log("Tag Copier: Unsupported site");
            return;
        }

        console.log(`Tag Copier: Detected site: ${site}`);
        const config = getSiteConfig(site);

        console.log("Tag Copier: Looking for tag list...");

        // Try to find the tag list section
        const tagSection = document.querySelector(config.tagSection);
        if (!tagSection) {
            console.log("Tag Copier: No tag section found");
            return;
        }

        console.log("Tag Copier: Found tag section!");

        // Check if button already exists
        if (document.querySelector("#tag-copy-button")) {
            console.log("Tag Copier: Button already exists");
            return;
        }

        console.log("Tag Copier: Adding copy button...");

        // Create button container
        const buttonContainer = document.createElement("div");
        buttonContainer.style.cssText = `
            position: absolute;
            top: 5px;
            right: 5px;
            z-index: 1000;
            display: flex;
            gap: 5px;
        `;

        // Create the copy button
        const copyButton = document.createElement("button");
        copyButton.id = "tag-copy-button";
        copyButton.innerHTML = "📋 Copy Tags";
        copyButton.style.cssText = `
            background: #0073e6;
            color: white;
            border: none;
            padding: 5px 10px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            font-family: sans-serif;
        `;

        // Create settings button
        const settingsButton = document.createElement("button");
        settingsButton.id = "tag-settings-button";
        settingsButton.innerHTML = "⚙️";
        settingsButton.title = "Settings";
        settingsButton.style.cssText = `
            background: #6c757d;
            color: white;
            border: none;
            padding: 5px 8px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            font-family: sans-serif;
        `;

        // Add hover effects
        copyButton.addEventListener("mouseenter", () => {
            copyButton.style.background = "#005bb5";
        });
        copyButton.addEventListener("mouseleave", () => {
            copyButton.style.background = "#0073e6";
        });

        settingsButton.addEventListener("mouseenter", () => {
            settingsButton.style.background = "#5a6268";
        });
        settingsButton.addEventListener("mouseleave", () => {
            settingsButton.style.background = "#6c757d";
        });

        // Add click handlers
        copyButton.addEventListener("click", () => copyTags(site));
        settingsButton.addEventListener("click", createSettingsPanel);

        // Add buttons to container
        buttonContainer.appendChild(copyButton);
        buttonContainer.appendChild(settingsButton);

        // Make tag section container relative positioned so button positions correctly
        tagSection.style.position = "relative";

        // Add button container to the tag section
        tagSection.appendChild(buttonContainer);
        console.log("Tag Copier: Buttons added successfully!");
    }

    function copyTags(site) {
        console.log("Tag Copier: Copy button clicked!");
        const config = getSiteConfig(site);
        const settings = getSettings();
        const tagsByCategory = {
            artist: [],
            general: [],
            character: [],
            copyright: []
        };

        config.categories.forEach((category) => {
            console.log(`Tag Copier: Looking for ${category.name} tags...`);

            // Universal approach: just look for the selector and extract tags
            const tagItems = document.querySelectorAll(category.selector);
            console.log(`Tag Copier: Found ${tagItems.length} ${category.name} tags`);

            tagItems.forEach((item) => {
                const tagName = config.getTagName(item);
                if (tagName && tagName.length > 0) {
                    let cleanedTag = cleanTagName(tagName);

                    // Filter out censor tags if setting is enabled
                    if (settings.filterCensor && cleanedTag.toLowerCase().includes('censor')) {
                        console.log(`Tag Copier: Filtered out censor tag: ${cleanedTag}`);
                        return;
                    }

                    // Add "artist:" prefix for artist tags
                    if (category.name === "artist") {
                        cleanedTag = "artist:" + cleanedTag;
                    }

                    // Store in appropriate category
                    const categoryName = category.name === "general" ? "general" : category.name;
                    if (tagsByCategory[categoryName]) {
                        tagsByCategory[categoryName].push(cleanedTag);
                    } else {
                        tagsByCategory.general.push(cleanedTag);
                    }

                    console.log(`Tag Copier: Added ${category.name} tag: ${tagName} -> ${cleanedTag}`);
                }
            });
        });

        // Combine tags based on user-defined order
        const allTags = [];
        settings.categoryOrder.forEach(category => {
            if (tagsByCategory[category]) {
                allTags.push(...tagsByCategory[category]);
            }
        });

        console.log(`Tag Copier: Total tags collected: ${allTags.length}`);
        console.log("Tag Copier: Tags:", allTags);

        // Join tags with commas and copy to clipboard
        const tagString = allTags.join(", ");
        console.log("Tag Copier: Final tag string:", tagString);

        if (tagString.length === 0) {
            console.log("Tag Copier: No tags found to copy!");
            showFeedback("❌ No tags found", "#dc3545");
            return;
        }

        // Copy to clipboard
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard
                .writeText(tagString)
                .then(() => {
                console.log("Tag Copier: Successfully copied to clipboard");
                showFeedback("✅ Copied!", "#28a745");
            })
                .catch((err) => {
                console.error("Tag Copier: Failed to copy tags:", err);
                fallbackCopy(tagString);
            });
        } else {
            console.log("Tag Copier: Using fallback copy method");
            fallbackCopy(tagString);
        }
    }

    function fallbackCopy(text) {
        // Fallback for older browsers
        const textArea = document.createElement("textarea");
        textArea.value = text;
        textArea.style.position = "fixed";
        textArea.style.left = "-999999px";
        textArea.style.top = "-999999px";
        document.body.appendChild(textArea);
        textArea.focus();
        textArea.select();

        try {
            const result = document.execCommand("copy");
            if (result) {
                console.log("Tag Copier: Fallback copy successful");
                showFeedback("✅ Copied!", "#28a745");
            } else {
                console.log("Tag Copier: Fallback copy failed");
                showFeedback("❌ Copy failed", "#dc3545");
            }
        } catch (err) {
            console.error("Tag Copier: Fallback copy error:", err);
            showFeedback("❌ Copy failed", "#dc3545");
        }

        document.body.removeChild(textArea);
    }

    function showFeedback(message, color) {
        const button = document.querySelector("#tag-copy-button");
        if (!button) return;

        const originalText = button.innerHTML;
        const originalColor = button.style.background;

        button.innerHTML = message;
        button.style.background = color;

        setTimeout(() => {
            button.innerHTML = originalText;
            button.style.background = originalColor;
        }, 1500);
    }

    // Initialize when DOM is ready
    function initialize() {
        if (document.readyState === "loading") {
            document.addEventListener("DOMContentLoaded", addCopyButton);
        } else {
            addCopyButton();
        }

        // Also run when navigating (for single-page app behavior)
        let lastUrl = location.href;
        new MutationObserver(() => {
            const url = location.href;
            if (url !== lastUrl) {
                lastUrl = url;
                setTimeout(addCopyButton, 500); // Small delay for content to load
            }
        }).observe(document, { subtree: true, childList: true });
    }

    // Start the script
    initialize();
})();