您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Cleverly download all images on a webpage
// ==UserScript== // @name Ripper // @namespace http://tampermonkey.net/ // @version 0.3 // @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 = (selector = '') => { 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: 750px; 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'; if (selector && typeof selector === 'string') textbox.value = 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'], ['Preserve Original Filename'], ['(WIP) Support Video Elements'], '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 ResolveMediaElementUrl = (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? const 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'); const 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"]'); if (!(matchCb?.checked ?? true)) { console.warn('Skipping match ', match, ' cause it was unchecked in the match list'); return ''; } const opts = Array.from(document.querySelector('.checkbox-group').querySelectorAll('input[type="checkbox"]')); const optSupportVideoElements = opts.find(_ => _.parentElement.textContent.includes("Support Video Elements"))?.checked ?? false; const supportedTypes = optSupportVideoElements ? [[HTMLImageElement,"IMG"],[HTMLVideoElement,"VIDEO"]] : [[HTMLImageElement,"IMG"]]; let actualMatch = supportedTypes.some(supportedType => { const typeName = supportedType[0]; return match instanceof typeName; }) ? match : supportedTypes.map(supportedType => { const nodeName = supportedType[1]; return match.querySelector(nodeName); }).filter(res => res)[0]; if (!actualMatch) { console.warn('Failed to find supported element type for parent match element: ', match); return ''; } const src = ResolveMediaElementUrl(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) => { // TODO: Handle blob urls? // const isBlobUrl = url.startsWith('blob:'); // console.warn('Encountered a blob url but implementation is missing'); // if (isBlobUrl) { // try { // const _res = await GM.xmlHttpRequest({method:'GET',url:url}); // debugger; // } catch (err) { debugger; return reject(err); } // } 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','webm']; 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 (urls.length === 0) return; if (typeof urls === 'string') urls = [urls]; cancelSignal.cancelled = false; 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="text" style="color:black;padding-right:5px;"></span> <button class="cancel">Stop</button `; document.body.appendChild(progressbar); const text = progressbar.querySelector('.text'); const btn = progressbar.querySelector('.cancel'); btn.onclick = () => { cancelSignal.cancelled = true; text.textContent = 'Aborting download, please wait ...'; }; const opts = Array.from(document.querySelector('.checkbox-group').querySelectorAll('input[type="checkbox"]')); const optHttpOnlyCookies = opts.find(_ => _.parentElement.textContent.includes("Inherit HTTP Only Cookies"))?.checked ?? true; const optHumanize = opts.find(_ => _.parentElement.textContent.includes('Humanize'))?.checked ?? true; const optPreserveOriginalFilename = opts.find(_ => _.parentElement.textContent.includes('Preserve Original Filename'))?.checked ?? false; 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, optHttpOnlyCookies); const filename = optPreserveOriginalFilename ? url.split('/').pop() : `${i}.${filetype}`; await SaveBlob(blob, filename); } catch (err) { console.error('Something went wrong downloading from url ', url); console.error(err); } if (optHumanize) await SleepRange(650, 850); } progressbar.remove(); } const defaultSelector = GM_getValue(document.location.host, undefined); if (typeof defaultSelector === 'undefined') { GM_registerMenuCommand('Show GUI', () => { RenderGui(); }); GM_registerMenuCommand(`Always show GUI for ${location.host}`, () => { GM_setValue(location.host, true); RenderGui(); }); // GM_registerMenuCommand(`Always show GUI for ${location.host} and save current selector`, () => { // const selector = document.querySelector('.input-text')?.value ?? true; // GM_setValue(selector); // RenderGui(); // }); } else { RenderGui(typeof defaultSelector === 'string' ? defaultSelector : ''); GM_registerMenuCommand(`Dont show GUI for ${location.host}`, () => { GM_deleteValue(location.host); // TODO: Remove the GUI }); }