FetLife: Download Image

Adds a download button that always downloads the current high‑res center image using a filename based on the username and picture id.

// ==UserScript==
// @name         FetLife: Download Image
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Adds a download button that always downloads the current high‑res center image using a filename based on the username and picture id.
// @match        https://fetlife.com/*/pictures/*
// @icon         https://fetlife.com/favicons/favicon.ico
// @license      GPL-3.0
// @grant        GM_download
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Locate the high-res main image inside the article.
    function getMainImage() {
        return document.querySelector("main article img.object-contain");
    }

    // Parse the srcset to return the URL with the highest multiplier (e.g. 2x).
    function getHighResUrl(img) {
        if (img && img.srcset) {
            const candidates = img.srcset.split(",");
            let maxUrl = "";
            let maxMultiplier = 0;
            for (const candidate of candidates) {
                // Example candidate: "https://picv2-u2000.cdn.fetlife.com/.../u2000.jpg?... 2x"
                const parts = candidate.trim().split(" ");
                const url = parts[0];
                const descriptor = parts[1];
                if (descriptor && descriptor.endsWith("x")) {
                    const multiplier = parseFloat(descriptor);
                    if (multiplier > maxMultiplier) {
                        maxMultiplier = multiplier;
                        maxUrl = url;
                    }
                }
            }
            return maxUrl || img.src;
        }
        return img ? img.src : "";
    }

    // Generate a custom filename based on the current page URL.
    // Expected format: /<username>/pictures/<pictureId>
    // and using the extension extracted from the high-res URL.
    function getCustomFilename(highResUrl) {
        try {
            // Extract the extension from the high-res URL.
            const urlObj = new URL(highResUrl);
            const segments = urlObj.pathname.split("/");
            let lastSegment = segments[segments.length - 1];
            const dotIndex = lastSegment.lastIndexOf(".");
            let ext = dotIndex !== -1 ? lastSegment.substring(dotIndex) : ".jpg";

            // Get the username and picture id from window.location.pathname.
            const locParts = window.location.pathname.split("/");
            if (locParts.length >= 4) {
                const username = locParts[1];
                const pictureId = locParts[3];
                return username + "_" + pictureId + ext;
            }
        } catch (e) {
            console.error("Error generating custom filename:", e);
        }
        // Fallback filename.
        return "image.jpg";
    }

    // Process the main image and update (or add) the download button.
    function processImage() {
        const img = getMainImage();
        if (img) {
            // Ensure pointer events are enabled on the image.
            img.style.setProperty("pointer-events", "auto", "important");

            // Check if a download button exists.
            let downloadButton = document.getElementById("downloadImageButton");
            // If a button exists but is associated with a different image, remove it.
            if (downloadButton) {
                if (downloadButton.dataset.currentImageSrc !== img.src) {
                    downloadButton.remove();
                    downloadButton = null;
                }
            }
            // Create and insert a new download button if needed.
            if (!downloadButton) {
                downloadButton = document.createElement("button");
                downloadButton.id = "downloadImageButton";
                downloadButton.textContent = "Download high‑res image";
                downloadButton.style.display = "block";
                downloadButton.style.margin = "20px auto 0";
                downloadButton.style.padding = "8px 12px";
                downloadButton.style.fontSize = "14px";
                downloadButton.style.cursor = "pointer";
                downloadButton.style.zIndex = "9999";
                // Save the current image src in a data attribute.
                downloadButton.dataset.currentImageSrc = img.src;

                // Find the container wrapping the image and insert the button after it.
                const container = img.closest("div.relative.overflow-hidden");
                if (container) {
                    container.insertAdjacentElement("afterend", downloadButton);
                } else {
                    img.insertAdjacentElement("afterend", downloadButton);
                }

                downloadButton.addEventListener("click", () => {
                    const url = getHighResUrl(img);
                    const filename = getCustomFilename(url);
                    GM_download({
                        url: url,
                        name: filename,
                        headers: {
                            "Referer": location.origin
                        },
                        onerror: (err) => {
                            alert("Download failed: " + err);
                        }
                    });
                });
            }
            return true;
        }
        return false;
    }

    // Initially process the image.
    processImage();

    // Use a MutationObserver to monitor for page changes (e.g. SPA navigation) and update the button if needed.
    const observer = new MutationObserver(() => {
        processImage();
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();