diff --git a/README.md b/README.md
index 1e54c74..4552cab 100644
--- a/README.md
+++ b/README.md
@@ -207,3 +207,30 @@ Follow these instructions to set up and run the project on your local machine.
# Advanced usage: search for "Benchy", limit to 3 results, and enable debug output
python printables_api.py "Benchy" --limit 3 --debug
```
+
+---
+
+## What's new
+
+- Gallery image scraping: the script now scrapes all images from a model's page (cover + gallery) and includes them in the JSON output under the image_urls key. The API-provided cover image remains available as main_image_url; if the API cover is missing, the first scraped image is used as a fallback.
+
+- Example JSON keys added:
+
+```json
+"main_image_url": "https://media.printables.com/..",
+"image_urls": [
+ "https://media.printables.com/..",
+ "https://media.printables.com/.."
+]
+```
+
+These changes improve the richness of output and make it easier to download or preview all images associated with a model.
+
+---
+
+## Contributors
+
+- Jacob Robertson (https://github.com/JacRob32)
+
+If you'd like to add additional contributors, fork the repo and open a PR.
+
diff --git a/printables_api.py b/printables_api.py
index 17a8f4e..dc79692 100644
--- a/printables_api.py
+++ b/printables_api.py
@@ -235,6 +235,87 @@ def get_model_description(model_url: str, debug: bool = False):
return f"Error: Could not fetch model page after {max_retries} attempts due to network issues."
+
+def get_model_images(model_url: str, debug: bool = False):
+ """
+ Scrape the model page and return a list of image URLs (cover + gallery).
+ Attempts multiple strategies:
tags, inline background-image styles, and anchor hrefs.
+ """
+ if debug:
+ print(f" -> Fetching images from: {model_url}")
+
+ max_retries = 3
+ for attempt in range(max_retries):
+ try:
+ scraper = cloudscraper.create_scraper(browser='chrome', delay=1,)
+ response = scraper.get(model_url, timeout=20)
+ response.raise_for_status()
+
+ soup = BeautifulSoup(response.text, 'html.parser')
+ imgs = set()
+
+ # 1)
tags (src, data-src, srcset)
+ for img in soup.find_all('img'):
+ src = img.get('data-src') or img.get('src') or ''
+ src = (src or '').strip()
+ if not src:
+ # try srcset first item if present
+ srcset = img.get('srcset')
+ if srcset:
+ src = srcset.split(',')[0].split(' ')[0].strip()
+ if not src:
+ continue
+ if src.startswith('//'):
+ src = 'https:' + src
+ elif src.startswith('/'):
+ src = 'https://www.printables.com' + src
+ if src:
+ imgs.add(src)
+
+ # 2) inline styles with background-image: url(...)
+ import re
+ for tag in soup.find_all(style=True):
+ style = tag.get('style', '')
+ urls = re.findall(r'url\(([^)]+)\)', style)
+ for u in urls:
+ u = u.strip().strip('"\'')
+ if u.startswith('//'):
+ u = 'https:' + u
+ elif u.startswith('/'):
+ u = 'https://www.printables.com' + u
+ if u:
+ imgs.add(u)
+
+ # 3) anchor hrefs pointing to image files
+ for a in soup.find_all('a', href=True):
+ href = a['href'].strip()
+ if href.startswith('//'):
+ href = 'https:' + href
+ elif href.startswith('/'):
+ href = 'https://www.printables.com' + href
+ if re.search(r'\.(png|jpe?g|gif|webp)(\?|$)', href, flags=re.I):
+ imgs.add(href)
+
+ # Normalize and filter obvious non-http values
+ final_imgs = [u for u in imgs if isinstance(u, str) and u.startswith('http')]
+ final_imgs = sorted(set(final_imgs))
+ if debug:
+ print(f" -> Found {len(final_imgs)} image(s)")
+ return final_imgs
+
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
+ if debug:
+ print(f" -> Attempt {attempt + 1}/{max_retries} failed fetching images: {e}")
+ if attempt < max_retries - 1:
+ time.sleep(2 * (attempt + 1))
+ continue
+ except Exception as e:
+ if debug:
+ print(f" -> Unexpected error when scraping images: {e}")
+ return []
+
+ return []
+
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Search Printables.com and fetch model data.")
parser.add_argument("search_term", type=str, help="The term to search for.")
@@ -265,17 +346,24 @@ def get_model_description(model_url: str, debug: bool = False):
if args.debug:
print(f"({i+1}/{len(search_results)}) Processing: {model.get('name')} ({model_url})")
+ # gather images (cover from API + gallery scraped from model page)
+ images = get_model_images(model_url, args.debug)
+
main_image_url = None
if (image_info := model.get('image')) and image_info.get('filePath'):
main_image_url = "https://media.printables.com/" + image_info['filePath']
-
+ # fall back to first scraped image if API cover not provided
+ if not main_image_url and images:
+ main_image_url = images[0]
+
description = get_model_description(model_url, args.debug)
files = get_model_files(model_id_str, args.debug)
-
+
all_models_data[model_id_str] = {
"name": model.get('name'),
"url": model_url,
"main_image_url": main_image_url,
+ "image_urls": images,
"author": model.get('user', {}).get('publicUsername'),
"stats": {
"likes": model.get('likesCount'),