// ==UserScript== // @name flickr easy download // @version 1.0.5 // @description download the highest resolution image on flickr with just one click! // @author Mjokfox // @updateURL https://gitinthebutt.ofafox.com/Mjokfox/flickr_photostream_dl/raw/branch/main/flickrezdl.user.js // @license GPLv3 // @match https://www.flickr.com/* // @match https://flickr.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=flickr.com // @grant none // @inject-into content // ==/UserScript== (function() { 'use strict'; // Function to fetch HTML content async function fetchHtml(url) { try { const response = await fetch(url); return await response.text(); } catch (error) { console.error(`Error fetching the HTML page for ${url}: ${error}`); return null; } } // Function to find the image URL from HTML content function findImageUrl(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const element = doc.querySelector('div#allsizes-photo img'); // child of div.allsizes-photo with type return element ? element.src : null; } // Function to find the largest resolution image URL async function findLargestResolution(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const smallElements = doc.querySelectorAll('small'); let maxResolution = 0; let largesti = 0; let maxHref = null; // instead of doing arithmatic, exploit the standard page layout if (smallElements[0].textContent == "(75 × 75)") { largesti = smallElements.length - 1; } else if (smallElements[1].parentElement.querySelector(':scope > a').innerHTML == "Original"){ largesti = 1; } else { largesti = 0; } // find the next page url to the largest image const parent = smallElements[largesti].parentElement; if (parent) { const link = parent.querySelector(':scope > a'); // single layer depth maxHref = link ? link.href : null; // if there is no element, we are already on the largest size page } if (smallElements[largesti].textContent == "(All sizes of this photo are available for download under a Creative Commons license)") { return maxHref // stupid edge case } if (maxHref) { // If it's not null, fetch the HTML for the larger image html = await fetchHtml(maxHref); return findImageUrl(html); } return findImageUrl(html); } // download the image function downloadImage(url) { fetch(url) .then(response => response.blob()) .then(blob => { const burl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = burl; a.download = url.split('/').pop();; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); }) .catch((error) => console.error(`Failed to download image for: ${url}: ${error}`)); } // remove everything after /in/ because that can sometimes be in the url function stripAfterIn(url) { var index = url.indexOf("/in/"); if (index !== -1) { return url.substring(0, index); } return url; } // main function async function downloadLargestFlickrImage(element) { let pageUrl = ""; if (element.type != "click"){pageUrl = element.parentElement.parentElement.querySelector('a').href;} else{pageUrl = window.location.href;} if (!pageUrl){console.error("url not found"); return} pageUrl = stripAfterIn(pageUrl); // add "sizes" to the url if (!pageUrl.includes("sizes")) { pageUrl += pageUrl.endsWith("/") ? "sizes/" : "/sizes/" } const html = await fetchHtml(pageUrl); if (html) { const imageUrl = await findLargestResolution(html); if (imageUrl) { downloadImage(imageUrl); } else { console.error('Failed to find the image URL.'); } } else { console.error('Failed to download the HTML page.'); } } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function downloadAll(buttonelement) { buttonelement.disabled = true const elements = document.querySelectorAll("a.overlay"); for (const element of elements) { downloadLargestFlickrImage(element) await delay(500 + Math.floor(Math.random() * 500)); } buttonelement.disabled = false } // Add a global floating button so it can be added and removed without querying function makeButton(text, clickHandler) { const button = document.createElement('button'); button.style.position = 'fixed'; button.style.bottom = '10px'; button.style.right = '10px'; button.style.height = "32px"; button.style.fontFamily = "Proxima Nova, helvetica neue, helvetica, arial, sans-serif"; button.style.fontWeight = "600"; button.style.padding = '0px 20px'; button.style.zIndex = '1000'; button.style.backgroundColor = '#1c9be9'; button.style.color = '#FFF'; button.style.border = 'none'; button.style.borderRadius = '5px'; button.style.fontSize = '16px'; button.id = "amogus"; button.innerHTML = text; button.addEventListener('click', clickHandler); return button; } const buttonSingle = makeButton('Download Image', downloadLargestFlickrImage); buttonSingle.style.cursor = "pointer" const buttonAll = makeButton('Download All Images', function() { downloadAll(buttonAll); }); function addFloatingButton(){ if (document.getElementById('amogus')){document.body.removeChild(document.getElementById('amogus'));} if (document.documentElement.classList.contains('html-photo-page-scrappy-view') || window.location.href.includes("sizes")) { document.body.appendChild(buttonSingle); } else if (document.documentElement.classList.contains('html-search-photos-unified-page-view')) { document.body.appendChild(buttonAll); } } // in photostream download button function addDownloadButton(element) { if(element.querySelector('a.engagement-item.download')){return} // skip if it already exists const a = document.createElement('a'); a.className = 'engagement-item download'; a.title = "Download this photo"; const i = document.createElement('i'); i.className = 'ui-icon-download'; // use the page built in icon a.appendChild(i); a.addEventListener('click', function() { a.style.cursor = "wait" a.style.backgroundColor = "#0a0" a.style.border = "2px solid white" downloadLargestFlickrImage(element).then(() => { a.style.cursor = "inherit"; }) }); element.appendChild(a); } // Callback function to execute when mutations are observed const observerCallback = function(mutationsList, observer) { for (const mutation of mutationsList) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.matches('div.engagement')) { addDownloadButton(node); } // check descendants const overlayElements = node.querySelectorAll && node.querySelectorAll('div.engagement'); if (overlayElements) {overlayElements.forEach(addDownloadButton);} } } if (mutation.type === 'attributes' && mutation.attributeName === 'class') { addFloatingButton() } } }; const style = document.createElement('style'); style.innerHTML = ` #amogus:disabled { cursor: wait !important; opacity: 0.6; } `; document.head.appendChild(style); // Create an observer instance linked to the callback function const observer = new MutationObserver(observerCallback); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); // maybe some already exist on load document.querySelectorAll('div.engagement').forEach(addDownloadButton); })();