// ==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();
});
}