← back to Dw Cowtan Recrawl
reference/scrape-cowtan.js
72 lines
#!/usr/bin/env node
// Cowtan & Tout Catalog Scraper - designs.cowtan.com AJAX API
// Target table: cowtan_tout_catalog
const https = require('https');
const { pool, upsertProduct, wait, closePool, getCatalogCount } = require('./scraper-utils');
const TABLE = 'cowtan_tout_catalog';
const API_BASE = 'https://designs.cowtan.com';
const IMG_BASE = 'https://d2mq91o692rj7w.cloudfront.net/images/Products';
const BRAND_MAP = { 1: 'Cowtan & Tout', 2: 'Colefax and Fowler', 3: 'Jane Churchill', 5: 'Larsen', 6: 'Manuel Canovas' };
const TYPE_MAP = { W: 'Wallcovering', F: 'Fabric', T: 'Trimming' };
let totalInserted = 0, totalErrors = 0;
function httpPost(url, body) {
return new Promise(function(resolve, reject) {
var parsed = new URL(url);
var opts = { hostname: parsed.hostname, port: 443, path: parsed.pathname,
method: 'POST', timeout: 30000,
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body),
'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } };
var req = https.request(opts, function(res) {
if (res.statusCode === 301 || res.statusCode === 302) {
return httpPost(res.headers.location, body).then(resolve).catch(reject);
}
var data = '';
res.on('data', function(c) { data += c; });
res.on('end', function() { try { resolve(JSON.parse(data)); } catch(e) { reject(new Error('JSON: ' + e.message + ' (len=' + data.length + ')')); } });
res.on('error', reject);
});
req.on('error', reject); req.on('timeout', function() { req.destroy(); reject(new Error('timeout')); });
req.write(body); req.end();
});
}
function buildBody(pi, tv) { var p = ['Keywords=','NewOnly=false']; ['F','W','T'].forEach(function(t,i) {
p.push('Types%5B'+i+'%5D.Value='+t); p.push('Types%5B'+i+'%5D.Selected='+(t===tv?'true':'false')); });
p.push('PageIndex='+pi); return p.join('&'); }
async function scrapeType(tv) { console.log('\n Fetching: '+TYPE_MAP[tv]+'...'); var all = [];
var f = await httpPost(API_BASE+'/Search', buildBody(0, tv)); var tot = f.TotalResults||0;
console.log(' Total: '+tot); if (!f.Products||!f.Products.length) return all;
var pp = f.Products[0]||[]; all.push.apply(all, pp); console.log(' Page 0: '+pp.length);
var mx = Math.min(Math.ceil(tot/100), 200);
for (var pi=1; pi<mx; pi++) { try { await wait(200,150);
var pg = await httpPost(API_BASE+'/Search', buildBody(pi, tv));
if (!pg.Products||!pg.Products.length) break; var it = pg.Products[0]||[]; if (!it.length) break;
all.push.apply(all, it); if ((pi+1)%10===0) console.log(' Page '+pi+': '+all.length);
} catch(e) { console.error(' Pg '+pi+': '+e.message); totalErrors++; await wait(2000); } }
console.log(' '+TYPE_MAP[tv]+' total: '+all.length); return all; }
function mapProduct(r, tv) { if (!r.StockCode) return null;
return { mfr_sku: r.StockCode, pattern_name: r.ProductName||null, color_name: r.ColourName||null,
collection: r.PatternBook||null, material: BRAND_MAP[r.BrandCode]||'Cowtan & Tout',
product_type: TYPE_MAP[tv], price_retail: r.SamplePrice||null,
image_url: IMG_BASE+'/'+r.StockCode.replace(/\//g,'-')+'.jpg',
product_url: 'https://designs.cowtan.com/Design/'+r.StockCode.replace(/\//g,'-'), in_stock: true }; }
async function main() { console.log('='.repeat(70)); console.log(' COWTAN & TOUT CATALOG SCRAPER'); console.log('='.repeat(70));
var t0 = Date.now();
await pool.query('CREATE TABLE IF NOT EXISTS '+TABLE+' (id SERIAL PRIMARY KEY, mfr_sku TEXT UNIQUE, dw_sku TEXT, pattern_name TEXT, color_name TEXT, collection TEXT, product_type TEXT, width TEXT, length TEXT, repeat_v TEXT, repeat_h TEXT, material TEXT, price_retail NUMERIC(10,2), price_trade NUMERIC(10,2), image_url TEXT, product_url TEXT, in_stock BOOLEAN DEFAULT TRUE, discontinued BOOLEAN DEFAULT FALSE, shopify_product_id TEXT, last_scraped TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW())');
console.log(' Table ready.'); var wp = await scrapeType('W'); await wait(1000,500); var fp = await scrapeType('F');
console.log('\n Upserting...'); var ar = [];
wp.forEach(function(r){var p=mapProduct(r,'W');if(p)ar.push(p);});
fp.forEach(function(r){var p=mapProduct(r,'F');if(p)ar.push(p);});
var s={},u=[]; ar.forEach(function(p){if(!s[p.mfr_sku]){s[p.mfr_sku]=true;u.push(p);}});
console.log(' Unique: '+u.length);
for(var i=0;i<u.length;i++){try{var id=await upsertProduct(TABLE,u[i]);if(id)totalInserted++;}catch(e){totalErrors++;}
if((i+1)%500===0)console.log(' DB: '+(i+1)+'/'+u.length+' ins='+totalInserted);}
var el=((Date.now()-t0)/1000).toFixed(1); var ct=await getCatalogCount(TABLE);
console.log('\n'+'='.repeat(70)); console.log(' DONE: wall='+wp.length+' fab='+fp.length+' ins='+totalInserted+' err='+totalErrors+' db='+ct);
console.log(' Time: '+el+'s'); console.log('='.repeat(70)); await closePool(); }
main().catch(function(e){console.error('FATAL:',e);closePool().then(function(){process.exit(1);});});