Skip to content

Instantly share code, notes, and snippets.

@bryant988
Last active October 22, 2025 03:18
Show Gist options
  • Save bryant988/9510cff838d86dcefa3b9ea3835b8552 to your computer and use it in GitHub Desktop.
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);
@JellyProctor
Copy link

Thank you so much for this. Unfortunately it seems to stop after the first 7 photos in Chrome.

@raghucbz
Copy link

raghucbz commented Jul 1, 2025 via email

@JellyProctor
Copy link

Thanks again. It was my mistake, I loaded all the pictures, but was in the horizontal slideshow and not the vertical scroll lightbox. Works great! In chrome, on Zillow, and converted all 54 webp to jpg. 6-30-25

@JellyProctor
Copy link

...and if I open an individual downloaded jpg in File Explorer, it even re-constitutes the slideshow to where I can just tab through the whole set. Thanks for you efforts!

@VeniceNerd
Copy link

Thanks again. It was my mistake, I loaded all the pictures, but was in the horizontal slideshow and not the vertical scroll lightbox. Works great! In chrome, on Zillow, and converted all 54 webp to jpg. 6-30-25

I am getting the same error:

Uncaught TypeError: Cannot read properties of null (reading 'querySelectorAll')
at gatherAndZipImages (:26:46)
at script.onload (:53:5)

I can't figure out how to the vertical scroll Lightbox. All I get is this one:
Screenshot 2025-07-15 at 11 18 53 PM

@JellyProctor
Copy link

@VeniceNerd I see this issue as well. That particular listing looks to be using Zillow Showcase ShowingTime+, the ST+ in the lower right hand corner, some kind of feature they're promoting to realtors. Just for comparison, looking at nearby 27556 Antelope Dr, Santa Clarita, the listing looks normal, non-ShowingTime+.
27556 Antelope Dr Santa Clarita, CA. If I try again for 27555 Antelope Drive, but this time on Redfin, I notice the 'lightbox' structure may be accessible. I also see that its not Santa Clarita, but Canyon Country, but that's probably not important. You might try the script on Redfin, I have no clue if its particular to Zillow. If it doesn't work then you'd need to appeal to the programmers in this thread; I don't know anything about it. YMMV, Regards,

@JellyProctor
Copy link

I'm not sure why my attached Redfin image didn't load, is there a limit? I'll re-post it here in a new comment.
27555 Antelop Dr Santa Clarita - Canyon Country CA on Redfin

@SabinVI
Copy link

SabinVI commented Sep 20, 2025

I just wanted to provide this code to help future people. I took and modified the code so it would work on a Zillow listing that was off market. This code worked perfectly for me and downloaded the 36 images from the lightbox. They were in "jpg" for this one, not "jpeg" or "webp". I'm not sure if this same code will work if the listing is "For Sale" or another status.

/**********************
 * Zillow VIW downloader
 * - Picks the largest candidate from srcset
 * - Works for JPG or WEBP
 * - Rewrites the size to TARGET_SIZE
 * - Dedupe + zips with JSZip
 **********************/

const TARGET_FORMAT = "jpg";     // "jpg" or "webp"  (use "jpg", not "jpeg")
const TARGET_SIZE   = "1536";    // "1536","1344","1152","960","768","576","384","192"

// --- Load JSZip ---
(function loadJSZip() {
  if (window.JSZip) {
    start();
    return;
  }
  const script = document.createElement("script");
  script.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js";
  script.onload = start;
  script.onerror = () => console.error("Failed to load JSZip.");
  document.head.appendChild(script);
})();

function start() {
  const { ext, mime } = normalizeFormat(TARGET_FORMAT);

  // Prefer the modal’s wall; fall back to any visible media wall
  const container =
    document.querySelector('#viw-modal ul.hollywood-vertical-media-wall-container') ||
    document.querySelector('ul.hollywood-vertical-media-wall-container') ||
    document.querySelector('[data-testid="viw-modal"]') ||
    document.querySelector('[data-testid="hollywood-vertical-media-wall"]');

  if (!container) {
    console.error("Could not find the media wall container.");
    return;
  }

  // Collect pictures within this area
  const pictures = Array.from(container.querySelectorAll("picture"));
  if (!pictures.length) {
    console.warn("No <picture> elements found inside the media wall.");
  }

  // Extract URLs (largest candidate) with preferred mime, else fallback
  const urls = new Set();

  for (const pic of pictures) {
    const preferred = pic.querySelector(`source[type="${mime}"]`);
    const allSources = Array.from(pic.querySelectorAll("source"));
    const img = pic.querySelector("img");

    let chosenUrl = null;

    // 1) Try preferred mime via srcset
    if (preferred && preferred.srcset) {
      const cand = pickLargest(parseSrcset(preferred.srcset));
      chosenUrl = cand?.url || null;
    }

    // 2) Else try any other source’s srcset (pick the largest)
    if (!chosenUrl) {
      let best = null;
      for (const s of allSources) {
        if (!s.srcset) continue;
        const cand = pickLargest(parseSrcset(s.srcset));
        if (!best || (cand && cand.w > best.w)) best = cand;
      }
      if (best) chosenUrl = best.url;
    }

    // 3) Else fallback to <img src>
    if (!chosenUrl && img?.src) {
      chosenUrl = img.src;
    }

    if (!chosenUrl) continue;

    // Rewrite -cc_ft_SIZE.ext to requested size/format
    const rewritten = rewriteZillowSizeAndFormat(chosenUrl, TARGET_SIZE, ext);

    // If the rewrite failed to match, still use the chosen URL
    urls.add(rewritten || chosenUrl);
  }

  const list = Array.from(urls);
  if (!list.length) {
    console.warn("No image URLs found.");
    return;
  }

  console.log("Found image URLs:", list);

  // Zip them up
  const zip = new JSZip();
  const folder = zip.folder("images");

  // Name files by the hash part (before -cc_ft_)
  const names = list.map((u, i) => {
    const m = u.match(/\/fp\/([^/]+?)-cc_ft_\d+\.(?:jpg|jpeg|webp)/i);
    const base = m?.[1] || `image_${String(i + 1).padStart(3, "0")}`;
    return `${base}.${ext}`;
  });

  const downloads = list.map((url, i) =>
    fetch(url)
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status} for ${url}`);
        return r.blob();
      })
      .then(b => folder.file(names[i], b))
      .catch(err => console.warn("Failed to fetch:", url, err))
  );

  Promise.all(downloads)
    .then(() => zip.generateAsync({ type: "blob" }))
    .then(blob => {
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = "images.zip";
      document.body.appendChild(a);
      a.click();
      a.remove();
    })
    .catch(err => console.error("Zipping error:", err));

  // --- helpers ---

  function normalizeFormat(fmt) {
    const f = String(fmt || "").toLowerCase();
    if (f === "webp") return { ext: "webp", mime: "image/webp" };
    // default to jpeg MIME but .jpg extension (Zillow uses .jpg)
    return { ext: "jpg", mime: "image/jpeg" };
  }

  function parseSrcset(srcset) {
    // "url1 384w, url2 768w, ..."
    return srcset
      .split(",")
      .map(s => s.trim())
      .map(entry => {
        const [url, wpart] = entry.split(/\s+/);
        const w = parseInt((wpart || "").replace(/[^0-9]/g, ""), 10);
        return { url, w: isNaN(w) ? 0 : w };
      })
      .filter(x => !!x.url);
  }

  function pickLargest(cands) {
    return cands.reduce((best, cur) => (!best || cur.w > best.w ? cur : best), null);
  }

  function rewriteZillowSizeAndFormat(url, size, extOut) {
    // Zillow pattern: .../fp/<hash>-cc_ft_960.jpg | .webp
    // Replace ONLY the -cc_ft_###.<ext> tail
    const re = /-cc_ft_\d+\.(jpg|jpeg|webp)(\?|$)/i;
    if (!re.test(url)) return null;
    return url.replace(re, `-cc_ft_${size}.${extOut}$2`);
  }
}

@smklancher
Copy link

I just wanted to provide this code to help future people. I took and modified the code so it would work on a Zillow listing that was off market. This code worked perfectly for me and downloaded the 36 images from the lightbox. They were in "jpg" for this one, not "jpeg" or "webp". I'm not sure if this same code will work if the listing is "For Sale" or another status.

Just used it and it worked perfectly, thanks!

@ak47us
Copy link

ak47us commented Sep 21, 2025

@SabinVI this worked on Edge. Thank you!

@tjsoco
Copy link

tjsoco commented Sep 29, 2025

@SabinVI this worked on Chrome for a "for Sale" house. Thank you!

@reworkednyc
Copy link

reworkednyc commented Oct 14, 2025

It is mid-October 2025 and I'm running Firefox current version (143.0.4 64-bit for ARM64). I was happily surprised to find that the script works perfectly. I used the version @SabinVI posted three weeks ago. Below is the exact procedure I followed:

  1. Navigated to the listing page for the house, whose status is 'under contract' (note I was on the main listing page, not the lightbox)
  2. Scrolled to the very bottom of the page without jumping, so as to load every picture that is available
  3. In Firefox, select the 'Tools' menu => 'Browser Tools' => 'Web Developer Tools'
  4. Within the Dev Tools popup that opens there are several tabs across the top. Select the 'Console' tab. Hit the Trash icon to clear the workspace (this won't harm anything)
  5. Type 'allow pasting' into the dialog
  6. Paste the @SabinVI code into the dialog and hit 'return'
  7. Wait a second while the magic happens. You may see some error lines and 'Found Image URLs'.
  8. After about 5 seconds, Firefox presented me with the 'save file' option. The resulting download was a .zip containing all of the pictures from the listing in high-resolution (~225kb avg in my case).

I was happy to find the script made it easy to archive the images - thanks to everyone who took the time to advance and refine this helpful bit of code. I hope my pedantic write-up is of use to fellow Firefox users who may be equally unfamiliar with using .js in console.

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