Universal Booru Tag Copier

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

// ==UserScript==
// @name         Universal Booru Tag Copier
// @namespace    http://tampermonkey.net/
// @version      1.01
// @description  Add a copy button to copy all non-meta tags from major booru sites
// @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";

    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;
    }

    // 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 the copy button
        const copyButton = document.createElement("button");
        copyButton.id = "tag-copy-button";
        copyButton.innerHTML = "📋 Copy Tags";
        copyButton.style.cssText = `
            position: absolute;
            top: 5px;
            right: 5px;
            background: #0073e6;
            color: white;
            border: none;
            padding: 5px 10px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            z-index: 1000;
            font-family: sans-serif;
        `;

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

      // Add click handler
      copyButton.addEventListener("click", () => copyTags(site));

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

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

    function copyTags(site) {
        console.log("Tag Copier: Copy button clicked!");
        const config = getSiteConfig(site);
        const tags = [];

        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);

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

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

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

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

        if (tagString.length === 0) {
            console.log("Tag Copier: No tags found to copy!");
            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();
})();