Skip to content

Instantly share code, notes, and snippets.

@bryant988
Last active March 28, 2024 14:21
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);
@imageEngineer
Copy link

imageEngineer commented Jan 5, 2022 via email

@constellabella
Copy link

Just used @beppodb 's code on my home that closed a while back, but pix were still up - perfect! Thanks a ton!

@imderek
Copy link

imderek commented Mar 27, 2022

@ziyiwu9494's code worked great!

@bxs1260
Copy link

bxs1260 commented Apr 1, 2022

It worked fine for me today:

How to execute it :

  1. Open the listing photos you need to download on website (I used google chrome browser) and it should look like this

image

  • Then right click and select inspect it would look like this :

image

  • then select console and paste the code provided by @beppodb or @ziyiwu9494 (tested both and worked just fine) and hit enter.

image

it will then ask you to save on ur computer by using a pop up for every file. Keep hitting enter and it will stop producing more pop ups once all phots have been downloaded.

In last if you are still having issues, please let me know I'll be happy to help to use this utility

Thanks to @bryant988 and all other contributors. Good job!

@joncl
Copy link

joncl commented Apr 4, 2022

Only Firefox worked for me running code last posted by ziyiwu9494 (https://gist.github.com/bryant988/9510cff838d86dcefa3b9ea3835b8552?permalink_comment_id=4016913#gistcomment-4016913). However, I got only 12/99 photos every time. Increasing the delay to 2 seconds worked like a charm. Updated code with the 2 second delay below.

Also, I flipped through all the images from the gallery first, and then ran the code in the console (righ-click > Inspect) from the first image.

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].replace(/cc_ft_.*.jpg/, 'uncropped_scaled_within_1536_1152.jpg')
  }).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('2 sec delay...');
          await delay(2000);
        }
        
        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);

@methodin
Copy link

I kept getting 6 photos only so I had to change

$('ul.media-stream li picture source[type="image/jpeg"]').map(function () {

to

$('picture source[type="image/jpeg"]').map(function () {

@BradFD
Copy link

BradFD commented May 11, 2022

Same issue, 6 photos only but your mod gives me an error,
Uncaught TypeError: Cannot read properties of undefined (reading 'replace')
at HTMLSourceElement. (:9:38)
at VM1211 jquery.min.js:2:935
at Function.map (VM1211 jquery.min.js:2:3485)
at n.fn.init.map (VM1211 jquery.min.js:2:903)
at HTMLScriptElement.script.onload (:7:64)

I kept getting 6 photos only so I had to change

$('ul.media-stream li picture source[type="image/jpeg"]').map(function () {

to

$('picture source[type="image/jpeg"]').map(function () {

@MilesWells
Copy link

@joncl Worked perfectly for me, thank you! Scrolled so all the thumbnails loaded into the DOM and pasted this in. Firefox v100.0.

@ZackWhiteIT
Copy link

@joncl's version worked as expected for me. Thanks!

@teriross
Copy link

teriross commented Aug 5, 2022

Hi. Can I pay someone to help me with this? I have one listing on Zillow. TIA

@jaredpavan
Copy link

jaredpavan commented Aug 5, 2022

@teriross - are you having any issues? Here are the steps:

  1. Open the listing in Firefox or Google Chrome
  2. Click on the first image, then click through each image so they are loaded to your browser
  3. Right click anywhere on the page and select "Inspect"
  4. Click on the "console" tab of the window that just opened
  5. If on firefox, first type allow pasting and press enter
  6. Paste this code and click enter (this is @joncl's code with @methodin's fix added):
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 = $('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].replace(/cc_ft_.*.jpg/, 'uncropped_scaled_within_1536_1152.jpg')
  }).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('2 sec delay...');
          await delay(2000);
        }
        
        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);

You should get a download prompt for each image if using Chrome, and if using Firefox the download center will open and show each image being downloaded.

If this works for you, then I'll take payment in the form of a donation of any amount to https://www.climatefoundation.org/ :)

@teriross
Copy link

teriross commented Aug 5, 2022 via email

@T150
Copy link

T150 commented Oct 1, 2022

TY! Still working as of today, using bxs1260 instructions. Great for cataloging favorite ZIllow listings/photos every now and then.
TY again, to all contributors!

@teriross
Copy link

teriross commented Oct 11, 2022 via email

@raghucbz
Copy link

Thanks for the code and all the comments so far. Here are some additional insights:

  • Code runs based on Zillow picture tiles when you click on the property. Scroll all the way down to load all the squares in the tiles, especially listings with over 10 pictures. Otherwise only 8 or so pics would be downloaded.
  • Zillow has changed the download function so only low resolution JFIF files would be downloaded by the browser.
  • I made a change to the code to download WEBP files (uncropped_scaled_within_1536_1152.webp)
  • You can convert WEBP files to JPG online using https://anywebp.com/ (20 pics at a time). Scale your JPG to 2x for converting and download HiRes pics
  • Here's the update code that I used:
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].replace(/cc_ft_.*.jpg/, 'uncropped_scaled_within_1536_1152.webp')
  }).toArray();
  const delay = ms => new Promise(res => setTimeout(res, ms)); // promise delay
  
  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);

@mhardin
Copy link

mhardin commented Apr 10, 2023

Thanks @raghucbz I can confirm your version works as of April 2023.

@samuelzamvil
Copy link

Working in May as well. Thank you @raghucbz !

@Camz-art
Copy link

Hi everyone!!
Thanks for the great information!! I was able to get the code to work on Zillow. Does anyone know of a similar code to work on Redfin?

@sp-hapsync
Copy link

I only see that 6 images are getting loaded.

@raghucbz
Copy link

raghucbz commented Jul 19, 2023 via email

@QuinnGT
Copy link

QuinnGT commented Aug 7, 2023

Confirmed it worked for me with a 2 sec delay. Thanks!

@tentzian
Copy link

Worked for me - thanks so much! Saved me a ton of time.

@jax075
Copy link

jax075 commented Sep 27, 2023

Does anyone else have a problem with the code after zillow changed the way listings are shown? I had no problem until this week, code worked flawlessly. I tried in Firefox and Chrome, doesn't initiate download now

@sumgthub
Copy link

I am having issues downloading pictures now

@frederickjansen
Copy link

@jax075 @sumgthub Here's an updated version. I tested it in Firefox. The code is based on an older version that automatically navigates through the images, rather than having to load them manually ahead of time. I also reverted back to JPEG instead of WEBP so people don't have to convert the images manually.

Make sure you open up the lightbox with the first image loaded before you run the code. Let me know if you run into any issues.

const script = document.createElement("script");
script.src = "https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js";

script.onload = async () => {
  $ = jQuery.noConflict();

  const delay = (ms) => new Promise((res) => setTimeout(res, ms)); // promise delay

  // can't map since there isn't a list, so just push as we find more.
  const imageList = [];

  // while there is a next button
  while (
    $(".photo-carousel-icon-wrapper .icon-arrow-right").length ||
    $(".photo-carousel-icon-wrapper .icon-reload").length
  ) {
    // Wait a little to make sure the next image source is loaded. If you get an error, increasing the timeout might help
    await delay(200);
    // Last image, break out of loop
    if ($(".photo-carousel-icon-wrapper .icon-reload").length) {
      break;
    }
    const srcs = $('.hdp-gallery-image-content .image:visible source[type="image/jpeg"]').attr("srcset").split(" ");
    const src = srcs[srcs.length - 2];
    // just in case... let make sure the src is not already in the list.
    if (imageList.indexOf(src) === -1) {
      imageList.push(src);
    }

    // go to the next slide
    $(".photo-carousel-icon-wrapper .icon-arrow-right").parent().click();
  }

  // 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);
        }

        let a = document.createElement("a");
        a.style = "display: none";
        console.log(i);

        let 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);

@jax075
Copy link

jax075 commented Sep 28, 2023

A lot of times it works on first try (especially on listings with less than 50 pics), other times gives the following error: Uncaught (in promise) TypeError: $(...).attr(...) is undefined
When I refresh the page and manually scroll through the pics with arrow keys on the keyboard and then run the code it usually works, but if the listing is over 100 pics it is a coin toss if even that works

@frederickjansen
Copy link

// Wait a little to make sure the next image source is loaded. If you get an error, increasing the timeout might help
 await delay(200);

There is a 200ms timeout between clicking the next picture arrow and trying to obtain its source property. This might have to be increased on slower connections, or perhaps Zillow artificially slows down loading of pages if it detects you're making many sequential requests. I can try to come up with a more robust solution that works for large image galleries.

@FredyLegacy
Copy link

@jax075 @sumgthub Here's an updated version. I tested it in Firefox. The code is based on an older version that automatically navigates through the images, rather than having to load them manually ahead of time. I also reverted back to JPEG instead of WEBP so people don't have to convert the images manually.

Make sure you open up the lightbox with the first image loaded before you run the code. Let me know if you run into any issues.

const script = document.createElement("script");
script.src = "https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js";

script.onload = async () => {
  $ = jQuery.noConflict();

  const delay = (ms) => new Promise((res) => setTimeout(res, ms)); // promise delay

  // can't map since there isn't a list, so just push as we find more.
  const imageList = [];

  // while there is a next button
  while (
    $(".photo-carousel-icon-wrapper .icon-arrow-right").length ||
    $(".photo-carousel-icon-wrapper .icon-reload").length
  ) {
    // Wait a little to make sure the next image source is loaded. If you get an error, increasing the timeout might help
    await delay(200);
    // Last image, break out of loop
    if ($(".photo-carousel-icon-wrapper .icon-reload").length) {
      break;
    }
    const srcs = $('.hdp-gallery-image-content .image:visible source[type="image/jpeg"]').attr("srcset").split(" ");
    const src = srcs[srcs.length - 2];
    // just in case... let make sure the src is not already in the list.
    if (imageList.indexOf(src) === -1) {
      imageList.push(src);
    }

    // go to the next slide
    $(".photo-carousel-icon-wrapper .icon-arrow-right").parent().click();
  }

  // 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);
        }

        let a = document.createElement("a");
        a.style = "display: none";
        console.log(i);

        let 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);

I think Zillow updated their page so this doesn't work anymore. Has anyone found a solution they could share with me?

Thanks in advance

@frederickjansen
Copy link

@FredyLegacy I just tested the code on Firefox and it's still working for me. Are you opening up the images in the slideshow view? Open up the property, click on the images, then click on the image again so you only see a single image on the page.

@FredyLegacy
Copy link

Hi Frederick, thanks for answering. I did the steps you mentioned and it doesn't work. I could do it before using Google Chrome, I downloaded Firefox and had no luck.

Google Chrome
-Version 119.0.6045.123 (Official Build) (arm64) (MAC)
-Version 119.0.6045.123 (Official Build) (arm64) (PC)
Firefox version
-119.0.1 (64-bit) (PC)

@timbitzen
Copy link

@FredyLegacy

Worked for me today using Arc browser (based on Chromium). When you open picture from slideshow view, right-click and choose inspect, then select the Console tab, and run the code at bottom at the > and tap enter key

@T150
Copy link

T150 commented Dec 28, 2023

EDIT = Only worked for a closed listing.I too am now unable to use the script for an open listing as of 12/28/23.

TY! Still working as of today, using timbitzen comments. I'm using Chromium Version 120.0.6099.109 (Official Build) for Linux Mint (64-bit).
Downloaded 58 webp images from a listing, no problem. TY again, to all contributors!

@T150
Copy link

T150 commented Dec 28, 2023

Also, props to raghucbz November 18 2022 wisdom in this GitHub thread, for how to batch convert webp to jpg. That website works great too. TY!

@frederickjansen
Copy link

@T150 Can you share a listing it doesn't work on? I just tested it on an open listing and no problems downloading the images.

@knowyourrivals
Copy link

This worked for me, but only after clicking to load the first image in the lightbox, and then advancing through each photo individually using the arrow key. Once I was on the last image, I then ran the script through the console, and was successful. Using: Mac Chrome Version 120.0.6099.199 (Official Build) (arm64)

const script = document.createElement("script");
script.src = "https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js";

script.onload = async () => {
  $ = jQuery.noConflict();

  const delay = (ms) => new Promise((res) => setTimeout(res, ms)); // promise delay

  // can't map since there isn't a list, so just push as we find more.
  const imageList = [];

  // while there is a next button
  while (
    $(".photo-carousel-icon-wrapper .icon-arrow-right").length ||
    $(".photo-carousel-icon-wrapper .icon-reload").length
  ) {
    // Wait a little to make sure the next image source is loaded. If you get an error, increasing the timeout might help
    await delay(200);
    // Last image, break out of loop
    if ($(".photo-carousel-icon-wrapper .icon-reload").length) {
      break;
    }
    const srcs = $('.hdp-gallery-image-content .image:visible source[type="image/jpeg"]').attr("srcset").split(" ");
    const src = srcs[srcs.length - 2];
    // just in case... let make sure the src is not already in the list.
    if (imageList.indexOf(src) === -1) {
      imageList.push(src);
    }

    // go to the next slide
    $(".photo-carousel-icon-wrapper .icon-arrow-right").parent().click();
  }

  // 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);
        }

        let a = document.createElement("a");
        a.style = "display: none";
        console.log(i);

        let 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);

@T150
Copy link

T150 commented Jan 9, 2024

@frederickjansen @knowyourrivals Okay, working as advertised. Perhaps I was not drilling down enough into the Lightbox (pictures only) feature of the Zillow webpages (so I followed the process @knowyourrivals described). And all looks good now. Thanks for responding and motivating me to keep trying. Cheers!

@medosf
Copy link

medosf commented Mar 19, 2024

here is the new script to download the highest resolution images, the issue with opening the list view it will show a highest resolution of 380 x 280. in the code below i replaced the the image url with uncropped_scaled_within_1344_1008.jpg which is the highest resolution available

const highRes = srcset[0].replace("-cc_ft_192.jpg","-uncropped_scaled_within_1344_1008.jpg")

use the same script and just replace imageList with this below code

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
const highRes = srcset[0].replace("-cc_ft_192.jpg","-uncropped_scaled_within_1344_1008.jpg")
return highRes
}).toArray();

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