Ripper

Cleverly download all images on a webpage

目前為 2025-04-13 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Ripper
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Cleverly download all images on a webpage
// @author       TetteDev
// @match        *://*/*
// @icon         https://icons.duckduckgo.com/ip2/tampermonkey.net.ico
// @license      MIT
// @grant        GM_cookie
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_setValue
// @run-at       document-idle
// @noframes
// ==/UserScript==

const RenderGui = () => {
    const guiId = "downloader_gui";
 
    let container = document.getElementById(guiId);
    if (container) {
        const _input = container.querySelector('input[type="text"]');
        _input.value = "";
        _input.dispatchEvent(new Event('input', { 'bubbles': true }));
        container.remove();
        container = document.createElement('div');
    } else {
        container = document.createElement('div');
    }

    container.id = guiId;
    container.style.cssText = 'position: fixed; top: 10px; right: 10px; background: white; padding: 10px; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.2); z-index: 9999;';

    const input = document.createElement('input');
    input.type = 'text';
    input.placeholder = 'Enter CSS Selector';
    input.style.cssText = 'margin-bottom: 10px; padding: 5px; width: 200px; color: black;';

    const button = document.createElement('button');
    button.textContent = 'Download 0 Image(s)';
    button.style.cssText = 'display: block; margin: 10px 0; padding: 5px 10px; cursor: pointer;';
    button.onclick = () => {
        if (previousElements.length < 1) return; 

        const ResolveImageUrl = (img) => {
            const lazyAttributes = [
                "data-src", "data-pagespeed-lazy-src", "srcset", "src", "zoomfile", "file", "original", "load-src", "_src", "imgsrc", "real_src", "src2", "origin-src",
                "data-lazyload", "data-lazyload-src", "data-lazy-load-src",
                "data-ks-lazyload", "data-ks-lazyload-custom", "loading",
                "data-defer-src", "data-actualsrc",
                "data-cover", "data-original", "data-thumb", "data-imageurl", "data-placeholder",
            ];
            const IsUrl = (url) => {
                // TODO: needs support for relative file paths also?
                var pattern = new RegExp(
                    '^(https?:\\/\\/)?'+ // protocol
                    '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
                    '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
                    '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
                    '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
                    '(\\#[-a-z\\d_]*)?$','i');
                let isUrl = !!pattern.test(url);
                if (!isUrl) {
                    try {
                        new URL(url);
                        return true;
                    } catch(err) {
                        return false;
                    }
                }
                return true;
            };

            let possibleImageUrls = lazyAttributes.filter(attr => {
                let attributeValue = img.getAttribute(attr);
                if (!attributeValue) return false;
                attributeValue = attributeValue.replaceAll('\t', '').replaceAll('\n','');
                let ok = IsUrl(attributeValue.trim());
                if (!ok && attr === "srcset") { 
                    // srcset usually contains a comma delimited string that is formatted like
                    // <URL1>, <WIDTH>w, <URL2>, <WIDTH>w, <URL3>, <WIDTH>w,
                    // TODO: handle this case
                    const srcsetItems = attributeValue.split(',').map(attr => attr.trim()).map(item => item.split(' '));
                    if (srcsetItems.length > 0) {
                        img.setAttribute("srcset", srcsetItems[srcsetItems.length - 1][0]);
                        ok = IsUrl(img.getAttribute("srcset"));
                    }
                }
                return ok;
            }).map(validAttr => img.getAttribute(validAttr).trim());

            if (!possibleImageUrls || possibleImageUrls.length < 1) {
                if (img.hasAttribute("src")) return img.src.trim();
                console.error(`Could not resolve the image source URL from the image object`, img);
                return "";
            }
            return possibleImageUrls.length > 1 ? [...new Set(possibleImageUrls)][0] : possibleImageUrls[0];
        };

        const urls = previousElements.map(el => {
            const cb = Array.from(document.querySelectorAll('input[type="checkbox"][id="download_selected"]')).find(cb => typeof cb.for !== "undefined" && cb.for.isEqualNode(el));
            if (!cb) debugger;
            if (!cb.checked) return "";

            return ResolveImageUrl(el);
        }).filter(url => url.length > 0);

        if (urls.length < 1) return;

        Download(urls, button);
    };

    const checkbox = document.createElement('input');
    checkbox.id = "inherit_cookies";
    checkbox.type = 'checkbox';
    checkbox.checked = true;
    const label = document.createElement('label');
    label.style.cssText = "color: black;";
    label.appendChild(checkbox);
    label.appendChild(document.createTextNode(' Inherit Session Cookies'));

    const checkbox_sleep = document.createElement('input');
    checkbox_sleep.id = "use_sleep";
    checkbox_sleep.type = 'checkbox';
    checkbox_sleep.checked = true;
    const label_sleep = document.createElement('label');
    label_sleep.style.cssText = "color: black;";
    label_sleep.appendChild(checkbox_sleep);
    label_sleep.appendChild(document.createTextNode(' Delay between downloads'));

    const highlightCss = '2px dashed green';
    const excludedHighlightCss = '2px dashed gray';

    const restoreElements = () => {
        previousElements.forEach(el => {
            el.style.border = '';
            const cb = Array.from(document.querySelectorAll('input[type="checkbox"][id="download_selected"]')).find(cb => typeof cb.for !== "undefined" && cb.for.isEqualNode(el));
            if (cb) cb.remove();
        });
        previousElements = [];
    };

    let previousElements = [];
    input.addEventListener('input', () => {
        restoreElements();
        try { const elements = document.querySelectorAll(input.value); button.textContent = `Download ${elements.length} Image(s)`;} catch (e) {}
    });

    input.addEventListener("keyup", (evt) => {
        if (evt.key !== "Enter") return;

        restoreElements();

        try {
            const elements = document.querySelectorAll(input.value);

            elements.forEach(el => {
                el.style.border = highlightCss;
                
                const cb = document.createElement("input");
                cb.type = "checkbox";
                cb.checked = true;
                cb.id = "download_selected";
                cb.for = el;
                cb.addEventListener("change", (evt) => {
                    const checked = evt.currentTarget.checked;
                    cb.for.style.border = checked ? highlightCss : excludedHighlightCss;
                    
                    const tmp = Array.from(document.querySelectorAll(input.value)).filter(el => {
                        const cb = Array.from(document.querySelectorAll('input[type="checkbox"][id="download_selected"]')).find(cb => typeof cb.for !== "undefined" && cb.for.isEqualNode(el));
                        return cb && cb.checked;
                    });
                    button.textContent = `Download ${tmp.length} Image(s)`;
                });
                el.insertAdjacentElement("beforebegin", cb);
                previousElements.push(el);
            });

            button.textContent = `Download ${elements.length} Image(s)`;
        } catch (e) {
            button.textContent = 'Download 0 Image(s)';
        }
    });

    container.appendChild(input);
    container.appendChild(button);
    container.appendChild(label);
    container.appendChild(label_sleep);
    document.body.appendChild(container);
};

const GetBlob = (url, inheritHttpOnlyCookies = true) => {
    return new Promise(async (resolve, reject) => {
        const res = await GM.xmlHttpRequest({
            method: "GET",
            url: url,
            headers: {
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
                "Accept-Language": "en-US,en;q=0.9",
                "Accept-Encoding": "gzip, deflate, br, zstd",
                "DNT": `${window.navigator.doNotTrack || "1"}`,

                "Referer":  document.location.href || url,
                "Origin": document.location.origin || url,

                "Host": window.location.host || window.location.hostname,
                "User-Agent": window.navigator.userAgent,
                "Priority": "u=0, i",
                "Upgrade-Insecure-Requests": "1",
                "Connection": "keep-alive",
                //"Cache-Control": "no-cache",
                "Cache-Control": "max-age=0",

                "Sec-Fetch-Dest": "document",
                "Sec-Fetch-Mode": "navigate",
                "Sec-Fetch-User": "?1",
                "Sec-GPC": "1",
            },
            responseType: "blob",
            cookiePartition: {
                topLevelSite: inheritHttpOnlyCookies ? location.origin : null
            }
        })
        .catch((error) => { debugger; return reject(error); });

        const allowedImageTypes = ["webp","png","jpg","jpeg","gif","bmp"];
        const HTTP_OK_CODE = 200;
        const ok =
                res.readyState == res["DONE"] &&
                res.status === HTTP_OK_CODE &&
                //res.response && ["webp","image"].some(t => res.response.type.includes(t))
                res.response && (res.response.type.startsWith('image/') && allowedImageTypes.includes(res.response.type.split('/')[1].toLowerCase()));

        if (!ok) {
            debugger;
            return reject(error);
        }

        return resolve(res.response);
    });
};
const SaveBlob = async (blob, fileName) => {
    const MakeAndClickATagAsync = async (blobUrl, fileName) => {
        try {
            let link;
            
            // Reuse existing element for sequential downloads
            if (!window._downloadLink) {
                window._downloadLink = document.createElement("a");
                window._downloadLink.style.cssText = "display: none !important;";
                try {
                    document.body.appendChild(window._downloadLink);
                } catch (err) {
                    // Handle Trusted Types policy
                    if (window.trustedTypes && window.trustedTypes.createPolicy) {
                        const policy = window.trustedTypes.createPolicy('default', {
                            createHTML: (string) => string
                        });
                    }
                    document.body.appendChild(window._downloadLink);
                }
            }
            link = window._downloadLink;
    
            // Set attributes and trigger download
            link.href = blobUrl;
            link.download = fileName;
            await Promise.resolve(link.click());
    
            return true;
        } catch (error) {
            console.error('Download failed:', error);
            await Promise.reject([false, error]);
        }
    };

    const blobUrl = window.URL.createObjectURL(blob)

    await MakeAndClickATagAsync(blobUrl, fileName)
    .catch(([state, errorMessage]) => { window.URL.revokeObjectURL(blobUrl); console.error(errorMessage); debugger; return reject([false, errorMessage, res]); });
    window.URL.revokeObjectURL(blobUrl);
};

const Download = async (LinksArray, downloadButton) => {
    if (LinksArray.length < 1) return;

    const shouldInheritCookies = document.getElementById("inherit_cookies")?.checked ?? true;
    const shouldSleep = document.getElementById("use_sleep")?.checked ?? true;

    const SleepRange = (min, max) => {
        const ms = Math.floor(Math.random() * (max - min + 1) + min);
        return new Promise(r => setTimeout(r, ms));
    };

    const originalButtonText = downloadButton.textContent;
    let errored = false;
    for (let i = 0; i < LinksArray.length; i++) {
        const url = LinksArray[i];
        const fileName = url.split('/').pop().trim();

        const blob = await GetBlob(url, shouldInheritCookies).catch(err => {
            console.error(err);
            errored = true;
        });
        if (errored) {
            debugger;
            break;
        }

        await SaveBlob(blob, fileName).catch(err => { debugger; });
        if (shouldSleep) await SleepRange(550, 750);
        downloadButton.textContent = `Downloaded ${i + 1} Image(s) ...`;
    }

    if (errored) {
        alert("Something went wrong with the download, check the developer console!");
    }
    downloadButton.textContent = originalButtonText;
};


if (GM_getValue(document.location.host, false)) {
    RenderGui();
    GM_registerMenuCommand("Dont always show GUI for current Host", (evt) => {
        GM_deleteValue(document.location.host);
    });
} else {
    GM_registerMenuCommand("Show GUI", (evt) => {
        RenderGui();
    });
    GM_registerMenuCommand("Always show GUI for current Host", (evt) => {
        GM_setValue(document.location.host, true);
        RenderGui();
    });
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址