Skip to content

Instantly share code, notes, and snippets.

@bryant988
Last active September 29, 2025 19:32
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);
@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!

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