Skip to content

Instantly share code, notes, and snippets.

@bryant988
Last active May 22, 2026 15:46
Show Gist options
  • Select an option

  • Save bryant988/9510cff838d86dcefa3b9ea3835b8552 to your computer and use it in GitHub Desktop.

Select an option

Save bryant988/9510cff838d86dcefa3b9ea3835b8552 to your computer and use it in GitHub Desktop.
Zillow Image Downloader
/**
* NOTE: this specifically works if the house is for sale since it renders differently.
* This will download the highest resolution available per image.
*/
/**
* STEP 1: Make sure to *SCROLL* through all images so they appear on DOM.
* No need to click any images.
*/
/**
* STEP 2: Open Dev Tools Console.
* Copy and paste code below
*/
const script = document.createElement('script');
script.src = "https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js";
script.onload = () => {
$ = jQuery.noConflict();
const imageList = $('ul.media-stream li picture source[type="image/jpeg"]').map(function () {
const srcset = $(this).attr('srcset').split(' '); // get highest res urls for each image
return srcset[srcset.length - 2]
}).toArray();
const delay = ms => new Promise(res => setTimeout(res, ms)); // promise delay
// get all image blobs in parallel first before downloading for proper batching
Promise.all(imageList.map(i => fetch(i))
).then(responses =>
Promise.all(responses.map(res => res.blob()))
).then(async (blobs) => {
for (let i = 0; i < blobs.length; i++) {
if (i % 10 === 0) {
console.log('1 sec delay...');
await delay(1000);
}
var a = document.createElement('a');
a.style = "display: none";
console.log(i);
var url = window.URL.createObjectURL(blobs[i]);
a.href = url;
a.download = i + '';
document.body.appendChild(a);
a.click();
setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 100);
}
});
};
document.getElementsByTagName('head')[0].appendChild(script);
@mkatychev
Copy link
Copy Markdown

@legendaryroots thank you so much for the updated script, this saved me a lot of time!

@rachelnd87
Copy link
Copy Markdown

Yeah I was getting mediaWall errors. So in summary, as of February 2025 this script worked for me after scrolling down to the bottom to load all the photos and pasting it in console. Combination of @jessereitz and @aarrtteemm's line update in one easy spot for copy paste:

const TARGET_FORMAT = "jpeg";  // Options: `jpeg` or `webp`
const TARGET_SIZE = "1536";  // Options: `1536`, `1344`, `1152`, `960`, `768`, `576`, `384`, `192`

// Load JSZip library
const script = document.createElement('script');
script.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js";
document.head.appendChild(script);

script.onload = function() {
    // Function to download the zip file
    function downloadZip(zip) {
        zip.generateAsync({type: 'blob'}).then(function(content) {
            const link = document.createElement('a');
            link.href = URL.createObjectURL(content);
            link.download = 'images.zip';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        });
    }

    // Function to gather and zip image URLs from "media wall"
    function gatherAndZipImages() {
        // Gather the image URLs
        const mediaWall = document.querySelector('div[data-testid="hollywood-vertical-media-wall"]');
        const sources = Array.from(mediaWall.querySelectorAll(`source[type="image/${TARGET_FORMAT}"]`));

        // Try to pull the largest src URL from a source's srcset
        // srcset is in the format "<url> <size>, <url> <size>" so we split it and try to grab the last (hopefully largest) URL
        // It shouldn't really matter, though, since the regex will replace the target size with the largest possible anyway
        const imageUrls = sources.map(source => {return source.srcset.split(",").at(-1).split(" ")[1].replaceAll(/_\d+.(jpg|webp)/g, `_${TARGET_SIZE}.${TARGET_FORMAT}`)});

        const zip = new JSZip();
        const imgFolder = zip.folder("images");

        if (imageUrls.length > 0) {
            console.log('Image URLs:', imageUrls);
            const downloadPromises = imageUrls.map((url, index) => {
                return fetch(url).then(response => response.blob()).then(blob => {
                    imgFolder.file(`image_${index + 1}.${TARGET_FORMAT}`, blob);
                });
            });

            Promise.all(downloadPromises).then(() => {
                downloadZip(zip);
            });
        } else {
            console.log(`No .${TARGET_FORMAT} images found.`);
        }
    }

    // Execute the function to gather and zip images
    gatherAndZipImages();
}

Thanks!

Excellent! Worked perfectly May 2026!

@McMilez
Copy link
Copy Markdown

McMilez commented May 17, 2026

Updated script here:
(() => {
const SCRIPT_NAME = "download-zillow-images.sh";
const FOLDER_NAME = "zillow-images";
const ZIP_NAME = "zillow-images.zip";

const found = new Map();

function cleanUrl(raw) {
if (!raw) return null;

try {
  const url = new URL(raw, location.href);
  url.search = "";

  if (!url.hostname.endsWith("zillowstatic.com")) return null;
  if (!url.pathname.includes("/fp/")) return null;
  if (!/\.(jpe?g|webp)$/i.test(url.pathname)) return null;

  return url.href;
} catch {
  return null;
}

}

function photoKey(url) {
const path = new URL(url).pathname;
return path.match(//fp/([^-./]+)/)?.[1] || path;
}

function qualityScore(url) {
const path = new URL(url).pathname.toLowerCase();

const numbers = [...path.matchAll(/[_-](\d{3,4})(?=[_.-]|\.(?:jpe?g|webp)$)/g)]
  .map(match => Number(match[1]));

const maxSize = numbers.length ? Math.max(...numbers) : 0;
const jpgBonus = /\.(jpg|jpeg)$/i.test(path) ? 25 : 0;

return maxSize * 100 + jpgBonus;

}

function add(raw) {
const url = cleanUrl(raw);
if (!url) return;

const key = photoKey(url);
const existing = found.get(key);

if (!existing || qualityScore(url) > qualityScore(existing)) {
  found.set(key, url);
}

}

function addSrcset(srcset) {
if (!srcset) return;

srcset.split(",").forEach(part => {
  const url = part.trim().split(/\s+/)[0];
  add(url);
});

}

document.querySelectorAll("img, source").forEach(el => {
add(el.currentSrc);
add(el.src);
addSrcset(el.srcset);
});

performance.getEntriesByType("resource").forEach(entry => {
add(entry.name);
});

const html = document.documentElement.innerHTML
.replaceAll("\u002F", "/")
.replaceAll("\/", "/")
.replaceAll("&", "&");

const urlRegex = /https?://photos.zillowstatic.com/fp/[^\s"'<>\\]+?.(?:jpe?g|webp)/gi;
for (const match of html.matchAll(urlRegex)) {
add(match[0]);
}

const urls = [...found.values()];

if (!urls.length) {
console.log("No Zillow photo URLs found. Open the full photo gallery, scroll through the photos, then run this again.");
return;
}

const script = `#!/usr/bin/env bash
set -euo pipefail

mkdir -p "${FOLDER_NAME}"

cat > zillow-image-urls.txt <<'URLS'
${urls.join("\n")}
URLS

i=1

while IFS= read -r url; do
clean="${url%%\?}"
ext="${clean##
.}"

case "$ext" in
jpg|jpeg|webp) ;;
*) ext="jpg" ;;
esac

out=$(printf "${FOLDER_NAME}/image_%03d.%s" "$i" "$ext")
echo "Downloading $out"

curl -L --fail --retry 3 --connect-timeout 20 \
-A "Mozilla/5.0" \
-e "${location.href}" \
"$url" \
-o "$out"

i=$((i + 1))
done < zillow-image-urls.txt

zip -qr "${ZIP_NAME}" "${FOLDER_NAME}"
echo "Done: ${ZIP_NAME}"
`;

const blobUrl = URL.createObjectURL(
new Blob([script], { type: "text/x-shellscript" })
);

const link = document.createElement("a");
link.href = blobUrl;
link.download = SCRIPT_NAME;
document.body.appendChild(link);
link.click();
link.remove();

setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);

console.log(Found ${urls.length} photos.);
console.log(Downloaded ${SCRIPT_NAME}.);
console.log("Then run this in Terminal:");
console.log(cd ~/Downloads && bash ${SCRIPT_NAME});
console.log("Photo URLs:", urls);
})();

@Divine-Overlord-Jawn
Copy link
Copy Markdown

@McMilez :(

Uncaught SyntaxError: expected expression, got '}'
debugger eval code:1
eval-with-debugger.js:304:22
getEvalResult resource://devtools/server/actors/webconsole/eval-with-debugger.js:304
evalWithDebugger resource://devtools/server/actors/webconsole/eval-with-debugger.js:218
evaluateJS resource://devtools/server/actors/webconsole.js:896
evaluateJSAsync resource://devtools/server/actors/webconsole.js:789
makeInfallible resource://devtools/shared/ThreadSafeDevToolsUtils.js:103
enter resource://devtools/server/actors/utils/event-loop.js:82
_pauseAndRespond resource://devtools/server/actors/thread.js:984
onDebuggerStatement resource://devtools/server/actors/thread.js:1970
debugger eval code:1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment