// ==UserScript==
// @name Ripper
// @namespace http://tampermonkey.net/
// @version 0.2
// @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 highlightSelector = "4px dashed purple";
const highlightElement = (element) => {
element.style.border = highlightSelector;
};
const unhighlightElement = (element) => {
element.style.border = "";
}
let container = null;
const guiClassName = 'gui-container';
if ((container = document.querySelector(`.${guiClassName}`))) {
container.remove();
}
else {
const style = document.createElement('style');
style.textContent = `
.gui-container {
font-family: 'Segoe UI', Arial, sans-serif;
max-width: 600px;
margin: 20px auto;
padding: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
position: fixed;
z-index: 9999;
width: auto;
height: auto;
top: 15px;
right: 15px;
border: 1px solid black;
}
.input-group {
display: flex;
gap: 5px;
margin-bottom: 10px;
}
.input-text {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
color: black !important;
}
.btn {
padding: 8px 16px;
background:rgb(250, 0, 0);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.item-list {
list-style: none;
padding: 0;
margin: 0 0 20px 0;
max-height: 450px;
overflow-y: auto;
overflow-x: hidden;
}
.item-list li {
display: flex;
align-items: center;
padding: 3px;
border-bottom: 1px solid #eee;
-webkit-user-select: none !important;
-khtml-user-select: none !important;
-moz-user-select: -moz-none !important;
-o-user-select: none !important;
user-select: none !important;
}
.item-list li:hover {
background-color: yellow;
}
.checkbox-group {
margin-bottom: 10px;
}
.checkbox-label {
display: inline-flex;
align-items: center;
margin-right: 20px;
cursor: pointer;
color: black !important;
}
.download-btn {
width: 100%;
padding: 12px;
background: rgb(250, 0, 0);
color: white;
font-weight: bold;
}
`;
document.body.appendChild(style);
}
// Create GUI elements
container = document.createElement('div');
container.className = guiClassName;
// Add dragging functionality
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
const dragStart = (e) => {
if (e.target !== container) return; // Only drag from container itself
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === container) {
isDragging = true;
container.style.cursor = "move";
}
};
const dragEnd = () => {
initialX = currentX;
initialY = currentY;
isDragging = false;
container.style.cursor = "";
};
const drag = (e) => {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
container.style.transform = `translate(${currentX}px, ${currentY}px)`;
};
container.removeEventListener('mousedown', dragStart);
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', dragEnd);
container.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
// Input group
const inputGroup = document.createElement('div');
inputGroup.className = 'input-group';
const textbox = document.createElement('input');
textbox.type = 'text';
textbox.className = 'input-text';
textbox.placeholder = 'Enter a valid CSS selector';
const getMatchesButton = document.createElement('button');
getMatchesButton.className = 'btn';
getMatchesButton.textContent = '⟳';
getMatchesButton.style.fontWeight = "bold";
getMatchesButton.title = "Execute the CSS Selector (or just press enter)";
let matchedElements = [];
textbox.addEventListener("keyup", (e) => {
if (e.key !== "Enter") return;
getMatchesButton.dispatchEvent(new Event('click', { 'bubbles': true }));
});
getMatchesButton.onclick = () => {
matchedElements.forEach(match => { unhighlightElement(match); });
matchedElements = [];
Array.from(document.querySelectorAll('.item-list > li')).forEach(li => { li.remove(); });
const selector = textbox.value;
if (!selector) return;
try {
const matches = Array.from(document.querySelectorAll(selector));
matches.forEach((match, index) => {
addListItem(`Match ${index + 1}`, match,
() => {
matchedElements.forEach(match => { unhighlightElement(match); });
highlightElement(match);
match.scrollIntoView();
setTimeout(() => {
unhighlightElement(match);
}, 4000);
});
matchedElements.push(match);
});
const lis = Array.from(document.querySelectorAll('.item-list > li'));
const selected = matches.filter(match => {
const cb = lis.find(li => li.ref.isEqualNode(match)).querySelector('input[type="checkbox"]');
cb.onchange = () => {
const dlbtn = document.querySelector(".download-btn");
const lis = Array.from(document.querySelectorAll('.item-list > li'));
const selected = matches.filter(match => {
const cb = lis.find(li => li.ref.isEqualNode(match)).querySelector('input[type="checkbox"]');
return cb.checked;
});
dlbtn.textContent = `Download ${selected.length} Item(s)`;
};
return cb.checked;
});
document.querySelector(".download-btn").textContent = `Download ${selected.length} Item(s)`;
} catch (err) { }
};
// List
const itemList = document.createElement('ul');
itemList.className = 'item-list';
// Checkbox group
const checkboxGroup = document.createElement('div');
checkboxGroup.className = 'checkbox-group';
const options = [['Humanize', 'checked'], ['Inherit HTTP Only Cookies', 'checked'], ['Placeholder Disabled', 'disabled'], 'Placeholder Normal'];
options.forEach(opt => {
const label = document.createElement('label');
label.className = 'checkbox-label';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
label.style.display = "block";
label.appendChild(checkbox);
if (typeof opt === "object") {
const text = opt[0];
label.appendChild(document.createTextNode(` ${text}`));
opt.slice(1).forEach(o => {
switch (o) {
case "checked":
checkbox.checked = true;
break;
case "disabled":
checkbox.disabled = true;
break;
default:
console.warn(`Unrecognized checkbox opt: '${o}'`);
break;
}
})
} else {
label.appendChild(document.createTextNode(` ${opt}`));
}
checkboxGroup.appendChild(label);
});
// Download button
const downloadBtn = document.createElement('button');
downloadBtn.className = 'btn download-btn';
downloadBtn.textContent = 'Download 0 Item(s)';
downloadBtn.onclick = async () => {
if (matchedElements.length === 0) 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 lis = Array.from(document.querySelectorAll('.item-list > li'));
let urls = matchedElements.map(match => {
const matchCb = lis.find(li => li.ref.isEqualNode(match)).querySelector('input[type="checkbox"]');
let actualMatch = match instanceof HTMLImageElement ? match : match.querySelector('img');
if (!actualMatch) return '';
const src = matchCb.checked ? ResolveImageUrl(actualMatch) : '';
return src;
}).filter(url => {
return url.length > 0;
});
// TODO: filter out duplicates?
await Download(urls);
};
// Add elements to container
inputGroup.appendChild(textbox);
inputGroup.appendChild(getMatchesButton);
container.appendChild(inputGroup);
//container.appendChild(itemListHeader);
container.appendChild(itemList);
container.appendChild(checkboxGroup);
container.appendChild(downloadBtn);
// Add to document
document.body.appendChild(container);
// Function to add new item to list
function addListItem(text, elemRef, itemClickCallback = null) {
const li = document.createElement('li');
li.style.cssText = "cursor: pointer; padding: 0px; color: black !important;"
if (itemClickCallback && typeof itemClickCallback === "function") {
li.ondblclick = itemClickCallback;
}
if (elemRef) {
li.ref = elemRef;
}
li.title = 'Double click an entry to scroll to it and highlight it';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = true;
if (!elemRef && !itemClickCallback) checkbox.disabled = true;
checkbox.style.marginRight = '10px';
const textNode = document.createTextNode(text);
li.appendChild(checkbox);
li.appendChild(textNode);
itemList.appendChild(li);
}
addListItem("No matches", null, null);
};
const SleepRange = (min, max) => {
const _min = Math.min(min, max);
const _max = Math.max(min, max);
const ms = Math.floor(Math.random() * (_max - _min + 1) + _min);
if (ms <= 0) return;
return new Promise(r => setTimeout(r, ms));
};
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({
blob: res.response,
filetype: res.response.type.split('/')[1],
});
});
};
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 cancelSignal = {cancelled:false};
async function Download(urls) {
if (typeof urls === "string") urls = [urls];
const progressbar = document.createElement("div");
progressbar.style.cssText = `position:fixed;z-index:9999;bottom:0px;right:0px;width:100%;max-height:30px;background-color:white;`;
progressbar.innerHTML = `
<span class="dl_text" style="color:black;padding-right:5px;"></span>
<button class="dl_cancel">Cancel</button
`;
document.body.appendChild(progressbar);
const text = progressbar.querySelector(".dl_text");
const btn = progressbar.querySelector(".dl_cancel");
btn.onclick = () => { cancelSignal.cancelled = true; };
for (let i = 0; i < urls.length; i++) {
if (cancelSignal.cancelled) break;
const url = urls[i];
text.textContent = `Downloading ${url} ... (${i+1}/${urls.length})`;
try {
const {blob, filetype} = await GetBlob(url, true);
await SaveBlob(blob, `${i}.${filetype}`)
} catch (err) {
console.error("Something went wrong downloading from url ", url);
console.error(err);
}
await SleepRange(650, 850);
}
progressbar.remove();
}
RenderGui();