← back to Patternbank Archive
scripts/download-images.js
104 lines
#!/usr/bin/env node
/**
* Download phase: pull image bytes for any pattern_images row missing local_path.
* Stores under data/images/<pattern_id>/<position>.<ext>
*/
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { withPage, sleep, UA } = require('../src/browser');
const db = require('../src/db');
const RATE = Number(process.env.RATE_LIMIT_MS || 2500);
const ROOT = path.join(__dirname, '..', 'data', 'images');
fs.mkdirSync(ROOT, { recursive: true });
function args() {
const a = {};
for (const arg of process.argv.slice(2)) {
const m = arg.match(/^--([^=]+)=(.+)$/);
if (m) a[m[1]] = m[2];
}
return a;
}
function extFromCT(ct, url) {
if (ct?.includes('jpeg') || /\.jpe?g(\?|$)/i.test(url)) return 'jpg';
if (ct?.includes('png') || /\.png(\?|$)/i.test(url)) return 'png';
if (ct?.includes('webp') || /\.webp(\?|$)/i.test(url)) return 'webp';
if (ct?.includes('gif') || /\.gif(\?|$)/i.test(url)) return 'gif';
return 'bin';
}
async function fetchImage(url) {
// Use browserbase page.context().request to inherit any CF cookies / UA
return await require('../src/browser').withPage(async (page, session) => {
// Warm the origin so CF challenge is solved if needed
try { await page.goto('https://patternbank.com/', { waitUntil: 'domcontentloaded', timeout: 30000 }); } catch {}
const resp = await page.context().request.get(url, {
headers: { 'User-Agent': UA, 'Accept': 'image/avif,image/webp,image/png,image/jpeg,*/*;q=0.8' },
timeout: 60000,
});
const status = resp.status();
const ct = resp.headers()['content-type'] || '';
const buf = Buffer.from(await resp.body());
return { status, ct, buf };
});
}
(async () => {
const a = args();
const limit = Number(a.limit || 25);
const runId = await db.startRun('images', `limit=${limit}`);
console.log(`[img] run=${runId} limit=${limit}`);
const r = await db.q(
`SELECT image_id, pattern_id, position, remote_url
FROM pattern_images
WHERE local_path IS NULL
ORDER BY image_id ASC
LIMIT $1`, [limit]);
if (!r.rows.length) {
console.log(`[img] nothing pending`);
await db.finishRun(runId, {});
return db.pool.end();
}
let dl = 0, bytes = 0, err = 0;
for (const row of r.rows) {
try {
const { status, ct, buf } = await fetchImage(row.remote_url);
if (status >= 400 || !buf || buf.length < 64) {
err++;
console.warn(`[img] HTTP ${status} ${row.remote_url}`);
await sleep(RATE);
continue;
}
const ext = extFromCT(ct, row.remote_url);
const dir = path.join(ROOT, String(row.pattern_id));
fs.mkdirSync(dir, { recursive: true });
const out = path.join(dir, `${row.position}.${ext}`);
fs.writeFileSync(out, buf);
const sha = crypto.createHash('sha256').update(buf).digest('hex');
await db.q(
`UPDATE pattern_images
SET local_path=$1, bytes=$2, sha256=$3, content_type=$4
WHERE image_id=$5`,
[path.relative(path.join(__dirname,'..'), out), buf.length, sha, ct, row.image_id]
);
dl++; bytes += buf.length;
console.log(`[img] ✓ ${row.pattern_id}/${row.position}.${ext} ${(buf.length/1024).toFixed(1)}KB sha=${sha.slice(0,8)}`);
} catch (e) {
err++;
console.warn(`[img] ${row.remote_url}: ${e.message}`);
}
await sleep(RATE);
}
await db.finishRun(runId, {
images_downloaded: dl,
bytes_downloaded: bytes,
errors: err,
});
console.log(`[img] done dl=${dl} bytes=${bytes} err=${err}`);
await db.pool.end();
})().catch(e => { console.error(e); process.exit(1); });