Gallery scraper
Bulk download images from login-protected gallery websites using an attached browser session.
How to use it
Claude Code
- Run the line below. It pulls the whole folder into
~/.claude/skills/gallery-scraper, including the files SKILL.md points to. - Describe your job in plain words. Claude Code follows the skill from there.
npx degit jdrhyne/agent-skills/clawdbot/gallery-scraper#main ~/.claude/skills/gallery-scraperFor one project only, change the path to .claude/skills/gallery-scraper. This skill also uses urls.txt — copying SKILL.md alone won't be enough. See the folder on GitHub.
Claude (web or desktop app)
- On this page open ⋯ → Download .md.
- Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
- Pick the file and Save. Claude shows the name and description and runs a security scan.
- Check the skill is switched on.
- Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
- ChatGPT: make a Project and paste it into Instructions.
- Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
- Check which app you pasted it into — the steps above name the right one.
- Some skills need the paid tier of Claude or ChatGPT.
Paste into Claude, ChatGPT or Cursor.
Source of Gallery scraper
Show the full text225 lines
| name | description | permissions |
|---|---|---|
| gallery-scraper | Bulk download images from login-protected gallery websites using an attached browser session. Use when asked to scrape, download, or save images from authenticated gallery pages, extract full-size images from thumbnails, or batch download from multi-page galleries. | - exec: "Runs local download commands for the URL list gathered from the attached browser session. - file_write: "Creates URL lists and downloaded image files in the user-approved output directory. - network: "Uses the attached browser session and direct image downloads against the user-approved gallery domain. |
Gallery Scraper
Bulk download images from authenticated gallery websites via browser relay.
Safety Boundaries
- Do not access gallery sites or user accounts that the user has not explicitly attached and authorized.
- Do not download beyond the selected gallery, profile, or page range without confirmation.
- Do not store cookies, tokens, or hidden form values in local output files.
- Do not keep retrying blocked downloads indefinitely; surface rate limits or auth failures instead.
Prerequisites
- User must have Chrome with OpenClaw Browser Relay extension
- User must be logged into the target site
- User must attach the browser tab (click relay toolbar button, badge ON)
Workflow
1. Attach Browser Tab
Ask user to:
- Log into the gallery site in Chrome
- Navigate to the target gallery/profile page
- Click the OpenClaw Browser Relay toolbar button (badge shows ON)
2. Discover Image URL Pattern
Most gallery sites store full-size URLs in data attributes. Common patterns:
// Extract via browser evaluate
() => {
// Try common patterns
const patterns = [
'img[data-max]', // data-max attribute
'img[data-src]', // lazy-load pattern
'img[data-full]', // full-size pattern
'a[data-lightbox] img', // lightbox galleries
'.gallery-item img' // generic gallery
];
for (const sel of patterns) {
const imgs = document.querySelectorAll(sel);
if (imgs.length > 0) {
return {
selector: sel,
count: imgs.length,
sample: imgs[0].outerHTML.substring(0, 200)
};
}
}
return null;
}
3. Extract Full-Size URLs
Once pattern identified, extract all URLs:
// For data-max pattern (common)
() => Array.from(document.querySelectorAll('img[data-max]'))
.map(img => img.dataset.max)
// For thumbnail→full conversion (replace path segment)
() => Array.from(document.querySelectorAll('.gallery img'))
.map(img => img.src.replace('/thumb/', '/full/'))
4. Handle Pagination
Check for multiple pages:
() => {
const pagination = document.querySelectorAll('.pagination a, [class*="page"] a');
return Array.from(pagination).map(a => ({text: a.textContent, href: a.href}));
}
Navigate to each page and collect URLs.
4b. Batch scrape multiple galleries (iframe trick)
When you need multiple galleries quickly and can’t automate CDP, you can load each gallery in a hidden iframe and extract data-max URLs:
async () => {
const urls = [
'https://site.example/galleries/view/123',
'https://site.example/galleries/view/456'
];
const results = [];
for (const url of urls) {
const iframe = document.createElement('iframe');
iframe.style.position = 'fixed';
iframe.style.left = '-9999px';
iframe.style.width = '800px';
iframe.style.height = '600px';
iframe.src = url;
document.body.appendChild(iframe);
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('timeout load')), 20000);
iframe.onload = () => { clearTimeout(t); resolve(); };
});
const doc = iframe.contentDocument;
const start = Date.now();
let imgs = [];
while (Date.now() - start < 20000) {
imgs = Array.from(doc.querySelectorAll('img[data-max]')).map(i => i.dataset.max);
if (imgs.length) break;
await new Promise(r => setTimeout(r, 500));
}
results.push({ id: url.split('/').pop(), urls: imgs });
iframe.remove();
}
return results;
}
5. Check CDN Access
Test if CDN requires authentication or just Referer:
# Test direct access
curl -I "CDN_URL" 2>/dev/null | head -3
# Test with Referer
curl -I -H "Referer: https://SITE_DOMAIN/" "CDN_URL" 2>/dev/null | head -3
6. Bulk Download
Collect the URLs into a text file, then parallel download:
# Create output directory
mkdir -p ~/Downloads/gallery_name
# Download with Referer header (parallel)
cd ~/Downloads/gallery_name
while IFS= read -r url; do
filename=$(basename "$url")
curl -s -H "Referer: https://SITE_DOMAIN/" -o "$filename" "$url" &
[ $(jobs -r | wc -l) -ge 8 ] && wait -n
done < urls.txt
wait
Python ThreadPool fallback (avoids shell quoting + wait -n issues):
import os
import requests
from concurrent.futures import ThreadPoolExecutor
outdir = os.path.expanduser('~/Downloads/gallery_name')
os.makedirs(outdir, exist_ok=True)
headers = {'Referer': 'https://SITE_DOMAIN/', 'User-Agent': 'Mozilla/5.0'}
with open('urls.txt') as f:
urls = [line.strip() for line in f if line.strip()]
def download(url):
filename = os.path.join(outdir, os.path.basename(url))
if os.path.exists(filename) and os.path.getsize(filename) > 0:
return
r = requests.get(url, headers=headers, timeout=60)
r.raise_for_status()
with open(filename, 'wb') as f:
f.write(r.content)
with ThreadPoolExecutor(max_workers=8) as ex:
for url in urls:
ex.submit(download, url)
Handling Lock Buttons
Some galleries have "lock" buttons to reveal hidden content. Look for:
// Find lock/unlock buttons
() => {
const locks = document.querySelectorAll(
'[class*="lock"], [class*="unlock"], ' +
'button[title*="lock"], .premium-unlock'
);
return Array.from(locks).map(el => ({
tag: el.tagName,
class: el.className,
text: el.innerText?.substring(0, 30)
}));
}
Click each lock button before extracting URLs.
Output Organization
Optionally organize by gallery:
# Derive a gallery-specific folder name from the selected URL
mkdir -p "gallery_<id>"
Troubleshooting
- 403 Forbidden: Add Referer header or extract cookies from browser
- Rate limited: Reduce parallel downloads, add delays
- Missing images: Check for JavaScript-loaded content, may need scroll injection
- Login required for CDN: Extract session cookies via
document.cookie
| 1 | |
| 2 | name gallery-scraper |
| 3 | description Bulk download images from login-protected gallery websites using an attached browser session. Use when asked to scrape, download, or save images from authenticated gallery pages, extract full-size images from thumbnails, or batch download from multi-page galleries. |
| 4 | permissions |
| 5 | - exec: "Runs local download commands for the URL list gathered from the attached browser session." |
| 6 | - file_write: "Creates URL lists and downloaded image files in the user-approved output directory." |
| 7 | - network: "Uses the attached browser session and direct image downloads against the user-approved gallery domain." |
| 8 | |
| 9 | |
| 10 | # Gallery Scraper |
| 11 | |
| 12 | Bulk download images from authenticated gallery websites via browser relay. |
| 13 | |
| 14 | ## Safety Boundaries |
| 15 | |
| 16 | Do not access gallery sites or user accounts that the user has not explicitly attached and authorized. |
| 17 | Do not download beyond the selected gallery, profile, or page range without confirmation. |
| 18 | Do not store cookies, tokens, or hidden form values in local output files. |
| 19 | Do not keep retrying blocked downloads indefinitely; surface rate limits or auth failures instead. |
| 20 | |
| 21 | ## Prerequisites |
| 22 | |
| 23 | User must have Chrome with OpenClaw Browser Relay extension |
| 24 | User must be logged into the target site |
| 25 | User must attach the browser tab (click relay toolbar button, badge ON) |
| 26 | |
| 27 | ## Workflow |
| 28 | |
| 29 | ### 1. Attach Browser Tab |
| 30 | |
| 31 | Ask user to: |
| 32 | Log into the gallery site in Chrome |
| 33 | Navigate to the target gallery/profile page |
| 34 | Click the OpenClaw Browser Relay toolbar button (badge shows ON) |
| 35 | |
| 36 | ### 2. Discover Image URL Pattern |
| 37 | |
| 38 | Most gallery sites store full-size URLs in data attributes. Common patterns: |
| 39 | |
| 40 | |
| 41 | // Extract via browser evaluate |
| 42 | () => { |
| 43 | // Try common patterns |
| 44 | const patterns = [ |
| 45 | 'img[data-max]', // data-max attribute |
| 46 | 'img[data-src]', // lazy-load pattern |
| 47 | 'img[data-full]', // full-size pattern |
| 48 | 'a[data-lightbox] img', // lightbox galleries |
| 49 | '.gallery-item img' // generic gallery |
| 50 | ]; |
| 51 | |
| 52 | for (const sel of patterns) { |
| 53 | const imgs = document.querySelectorAll(sel); |
| 54 | if (imgs.length > 0) { |
| 55 | return { |
| 56 | selector: sel, |
| 57 | count: imgs.length, |
| 58 | sample: imgs[0].outerHTML.substring(0, 200) |
| 59 | }; |
| 60 | } |
| 61 | } |
| 62 | return null; |
| 63 | } |
| 64 | |
| 65 | |
| 66 | ### 3. Extract Full-Size URLs |
| 67 | |
| 68 | Once pattern identified, extract all URLs: |
| 69 | |
| 70 | |
| 71 | // For data-max pattern (common) |
| 72 | () => Array.from(document.querySelectorAll('img[data-max]')) |
| 73 | .map(img => img.dataset.max) |
| 74 | |
| 75 | // For thumbnail→full conversion (replace path segment) |
| 76 | () => Array.from(document.querySelectorAll('.gallery img')) |
| 77 | .map(img => img.src.replace('/thumb/', '/full/')) |
| 78 | |
| 79 | |
| 80 | ### 4. Handle Pagination |
| 81 | |
| 82 | Check for multiple pages: |
| 83 | |
| 84 | |
| 85 | () => { |
| 86 | const pagination = document.querySelectorAll('.pagination a, [class*="page"] a'); |
| 87 | return Array.from(pagination).map(a => ({text: a.textContent, href: a.href})); |
| 88 | } |
| 89 | |
| 90 | |
| 91 | Navigate to each page and collect URLs. |
| 92 | |
| 93 | ### 4b. Batch scrape multiple galleries (iframe trick) |
| 94 | |
| 95 | When you need multiple galleries quickly and can’t automate CDP, you can load each gallery in a hidden iframe and extract `data-max` URLs: |
| 96 | |
| 97 | |
| 98 | async () => { |
| 99 | const urls = [ |
| 100 | 'https://site.example/galleries/view/123', |
| 101 | 'https://site.example/galleries/view/456' |
| 102 | ]; |
| 103 | const results = []; |
| 104 | for (const url of urls) { |
| 105 | const iframe = document.createElement('iframe'); |
| 106 | iframe.style.position = 'fixed'; |
| 107 | iframe.style.left = '-9999px'; |
| 108 | iframe.style.width = '800px'; |
| 109 | iframe.style.height = '600px'; |
| 110 | iframe.src = url; |
| 111 | document.body.appendChild(iframe); |
| 112 | await new Promise((resolve, reject) => { |
| 113 | const t = setTimeout(() => reject(new Error('timeout load')), 20000); |
| 114 | iframe.onload = () => { clearTimeout(t); resolve(); }; |
| 115 | }); |
| 116 | const doc = iframe.contentDocument; |
| 117 | const start = Date.now(); |
| 118 | let imgs = []; |
| 119 | while (Date.now() - start < 20000) { |
| 120 | imgs = Array.from(doc.querySelectorAll('img[data-max]')).map(i => i.dataset.max); |
| 121 | if (imgs.length) break; |
| 122 | await new Promise(r => setTimeout(r, 500)); |
| 123 | } |
| 124 | results.push({ id: url.split('/').pop(), urls: imgs }); |
| 125 | iframe.remove(); |
| 126 | } |
| 127 | return results; |
| 128 | } |
| 129 | |
| 130 | |
| 131 | ### 5. Check CDN Access |
| 132 | |
| 133 | Test if CDN requires authentication or just Referer: |
| 134 | |
| 135 | |
| 136 | # Test direct access |
| 137 | curl -I "CDN_URL" 2>/dev/null | head -3 |
| 138 | |
| 139 | # Test with Referer |
| 140 | curl -I -H "Referer: https://SITE_DOMAIN/" "CDN_URL" 2>/dev/null | head -3 |
| 141 | |
| 142 | |
| 143 | ### 6. Bulk Download |
| 144 | |
| 145 | Collect the URLs into a text file, then parallel download: |
| 146 | |
| 147 | |
| 148 | # Create output directory |
| 149 | mkdir -p ~/Downloads/gallery_name |
| 150 | |
| 151 | # Download with Referer header (parallel) |
| 152 | cd ~/Downloads/gallery_name |
| 153 | while IFS= read -r url; do |
| 154 | filename=$(basename "$url") |
| 155 | curl -s -H "Referer: https://SITE_DOMAIN/" -o "$filename" "$url" & |
| 156 | [ $(jobs -r | wc -l) -ge 8 ] && wait -n |
| 157 | done < urls.txt |
| 158 | wait |
| 159 | |
| 160 | |
| 161 | **Python ThreadPool fallback (avoids shell quoting + wait -n issues):** |
| 162 | |
| 163 | |
| 164 | import os |
| 165 | import requests |
| 166 | from concurrent.futures import ThreadPoolExecutor |
| 167 | |
| 168 | outdir = os.path.expanduser('~/Downloads/gallery_name') |
| 169 | os.makedirs(outdir, exist_ok=True) |
| 170 | headers = {'Referer': 'https://SITE_DOMAIN/', 'User-Agent': 'Mozilla/5.0'} |
| 171 | |
| 172 | with open('urls.txt') as f: |
| 173 | urls = [line.strip() for line in f if line.strip()] |
| 174 | |
| 175 | def download(url): |
| 176 | filename = os.path.join(outdir, os.path.basename(url)) |
| 177 | if os.path.exists(filename) and os.path.getsize(filename) > 0: |
| 178 | return |
| 179 | r = requests.get(url, headers=headers, timeout=60) |
| 180 | r.raise_for_status() |
| 181 | with open(filename, 'wb') as f: |
| 182 | f.write(r.content) |
| 183 | |
| 184 | with ThreadPoolExecutor(max_workers=8) as ex: |
| 185 | for url in urls: |
| 186 | ex.submit(download, url) |
| 187 | |
| 188 | |
| 189 | ## Handling Lock Buttons |
| 190 | |
| 191 | Some galleries have "lock" buttons to reveal hidden content. Look for: |
| 192 | |
| 193 | |
| 194 | // Find lock/unlock buttons |
| 195 | () => { |
| 196 | const locks = document.querySelectorAll( |
| 197 | '[class*="lock"], [class*="unlock"], ' + |
| 198 | 'button[title*="lock"], .premium-unlock' |
| 199 | ); |
| 200 | return Array.from(locks).map(el => ({ |
| 201 | tag: el.tagName, |
| 202 | class: el.className, |
| 203 | text: el.innerText?.substring(0, 30) |
| 204 | })); |
| 205 | } |
| 206 | |
| 207 | |
| 208 | Click each lock button before extracting URLs. |
| 209 | |
| 210 | ## Output Organization |
| 211 | |
| 212 | Optionally organize by gallery: |
| 213 | |
| 214 | |
| 215 | # Derive a gallery-specific folder name from the selected URL |
| 216 | mkdir -p "gallery_<id>" |
| 217 | |
| 218 | |
| 219 | ## Troubleshooting |
| 220 | |
| 221 | **403 Forbidden**: Add Referer header or extract cookies from browser |
| 222 | **Rate limited**: Reduce parallel downloads, add delays |
| 223 | **Missing images**: Check for JavaScript-loaded content, may need scroll injection |
| 224 | **Login required for CDN**: Extract session cookies via `document.cookie` |
| 225 |
Discussion
Browse more free Claude skills or everything in Sales.