Safebooru Auto Downloader

Adds a styled download button below thumbnails on safebooru.org and downloads in background.

// ==UserScript==
// @name         Safebooru Auto Downloader
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Adds a styled download button below thumbnails on safebooru.org and downloads in background.
// @match        https://safebooru.org/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @connect      safebooru.org
// @author       Kyura
// ==/UserScript==

(function () {
    "use strict";

    function addButtons() {
        document.querySelectorAll(".image-list span.thumb").forEach(span => {
            let link = span.querySelector("a");
            if (!link || link.querySelector(".tm-download-btn")) return;

            // Flex layout
            link.style.display = "flex";
            link.style.flexDirection = "column";
            link.style.alignItems = "stretch";
            span.style.marginBottom = "28px";

            let btn = document.createElement("button");
            btn.innerText = "Download";
            btn.className = "tm-download-btn";
            btn.style.width = "100%";
            btn.style.height = "22px";
            btn.style.marginTop = "6px";
            btn.style.background = "#e0e0e0";
            btn.style.border = "1px solid #aaa";
            btn.style.borderRadius = "4px";
            btn.style.cursor = "pointer";
            btn.style.fontSize = "12px";
            btn.style.transition = "background 0.2s";
            btn.style.position = "relative";
            btn.style.zIndex = "10";

            btn.addEventListener("mouseenter", () => btn.style.background = "#d0d0d0");
            btn.addEventListener("mouseleave", () => btn.style.background = "#e0e0e0");

            btn.addEventListener("click", (e) => {
                e.preventDefault();
                e.stopPropagation();

                // Fetch post page in background
                GM_xmlhttpRequest({
                    method: "GET",
                    url: link.href,
                    onload: function (res) {
                        if (res.status === 200) {
                            // Parse HTML
                            let parser = new DOMParser();
                            let doc = parser.parseFromString(res.responseText, "text/html");
                            let img = doc.querySelector("#image");
                            if (img && img.src) {
                                // Clean filename
                                let filename = img.src.split("/").pop().split("?")[0];
                                console.log("[TM] Downloading:", img.src, "→", filename);
                                GM_download(img.src, filename);
                            } else {
                                console.log("[TM] No image found on post page.");
                            }
                        } else {
                            console.error("[TM] Failed to fetch post page:", res.status);
                        }
                    },
                    onerror: function (err) {
                        console.error("[TM] GM_xmlhttpRequest error:", err);
                    }
                });
            });

            link.appendChild(btn);
        });
    }

    // Observe page for thumbnails
    const rootObserver = new MutationObserver(() => {
        if (document.querySelector(".image-list")) {
            addButtons();
            const list = document.querySelector(".image-list");
            if (list) {
                const innerObserver = new MutationObserver(addButtons);
                innerObserver.observe(list, { childList: true, subtree: true });
            }
            rootObserver.disconnect();
        }
    });
    rootObserver.observe(document.documentElement || document, { childList: true, subtree: true });

    // Try adding immediately if DOM is already ready
    if (document.readyState !== "loading") addButtons();
})();