← back to Dw Dead Image Recovery
scripts/recover-versa-kamatera.mjs
101 lines
// TK-11048 — Kamatera-scoped Versa image recovery (feed-first, $0, READ-ONLY DB).
// Reuses the PROVEN pure helpers from recover-versa.mjs (importing does NOT touch the DB).
// Input: data/kamatera-broken-283.tsv (mfr_sku<TAB>product_url, mfr_sku AS-STORED on Kamatera).
// Output: data/versa-recovery-kamatera.jsonl, data/versa-restore-kamatera-map.jsonl, report to stdout.
// NO DB write, NO Shopify. old_image_url is joined from the earlier restore CSV.
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { extractImage, isLiveImage, isDataUri } from './recover-versa.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const DATA = join(HERE, '..', 'data');
const UA = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36' };
const TIMEOUT = 25000;
const CONC = 8;
async function getText(url) {
const ac = new AbortController(); const t = setTimeout(() => ac.abort(), TIMEOUT);
try {
const res = await fetch(url, { redirect: 'follow', signal: ac.signal, headers: UA });
if (!res.ok) return { html: null, status: res.status };
return { html: await res.text(), status: res.status };
} catch (e) { return { html: null, status: 0, err: String(e && e.message || e) }; }
finally { clearTimeout(t); }
}
// SKU-core exactly as extractImage computes it — to report keying honestly.
function skuCore(sku) { return (sku.match(/^[A-Za-z]+\d+-\d+/) || [sku])[0]; }
function coreMatchesHtml(html, sku) {
const core = skuCore(sku).toLowerCase();
const imgs = [...html.matchAll(/\/fileadmin\/[^"'\s]+?\.(?:jpg|jpeg|png|webp)/gi)].map(m => m[0].toLowerCase());
return imgs.some(u => u.includes(core));
}
// old image_url per mfr_sku, from the restore CSV captured earlier (authoritative current broken value)
function loadOldImages() {
const csv = join(DATA, 'versa-restore-kamatera-202609021433.csv');
const map = new Map();
if (existsSync(csv)) {
const lines = readFileSync(csv, 'utf8').split('\n'); lines.shift(); // header
for (const ln of lines) {
if (!ln) continue;
const i = ln.indexOf(','); if (i < 0) continue;
map.set(ln.slice(0, i), ln.slice(i + 1));
}
}
return map;
}
const rows = readFileSync(join(DATA, 'kamatera-broken-283.tsv'), 'utf8')
.split('\n').filter(Boolean)
.map(l => { const [mfr_sku, product_url] = l.split('\t'); return { mfr_sku, product_url }; });
const oldImages = loadOldImages();
const recovered = []; // {mfr_sku, new_image_url, verified:true}
const restore = []; // {mfr_sku, old_image_url}
const miss = { nopdp: [], noimg: [], notlive: [] };
let coreMatchCount = 0;
const perFamily = new Map(); // prefix -> {total, recovered, coreMatch}
function fam(sku){ const m = sku.match(/^([A-Za-z]+\d*|inv-[a-z]+|asl-\d+)/i); return (m?m[1]:sku.slice(0,6)).toUpperCase(); }
async function one(r) {
const f = fam(r.mfr_sku);
const pf = perFamily.get(f) || { total:0, recovered:0, coreMatch:0 }; pf.total++; perFamily.set(f, pf);
const { html, status } = await getText(r.product_url);
if (!html) { miss.nopdp.push({ ...r, status }); return; }
const cm = coreMatchesHtml(html, r.mfr_sku);
if (cm) { coreMatchCount++; pf.coreMatch++; }
const url = extractImage(html, r.mfr_sku);
if (!url || isDataUri(url)) { miss.noimg.push({ ...r, coreMatch: cm }); return; }
const live = await isLiveImage(url);
if (!live) { miss.notlive.push({ ...r, url }); return; }
recovered.push({ mfr_sku: r.mfr_sku, new_image_url: url, verified: true });
restore.push({ mfr_sku: r.mfr_sku, old_image_url: oldImages.get(r.mfr_sku) ?? null });
pf.recovered++;
}
// bounded concurrency
let idx = 0;
async function worker() { while (idx < rows.length) { const r = rows[idx++]; await one(r); } }
await Promise.all(Array.from({ length: CONC }, worker));
writeFileSync(join(DATA, 'versa-recovery-kamatera.jsonl'), recovered.map(o => JSON.stringify(o)).join('\n') + (recovered.length ? '\n' : ''));
writeFileSync(join(DATA, 'versa-restore-kamatera-map.jsonl'), restore.map(o => JSON.stringify(o)).join('\n') + (restore.length ? '\n' : ''));
const N = rows.length;
console.log(`\n[recover-versa-kamatera] ${N} broken rows processed`);
console.log(`RECOVERED (verified live): ${recovered.length}/${N} = ${(recovered.length/N*100).toFixed(1)}%`);
console.log(`SKU-core matched an image on PDP: ${coreMatchCount}/${N}`);
console.log(`MISS breakdown:`);
console.log(` nopdp (page fetch failed): ${miss.nopdp.length}`);
console.log(` noimg (no SKU-keyed image found): ${miss.noimg.length}`);
console.log(` notlive (extracted but not 200/image): ${miss.notlive.length}`);
console.log(`\nPer-family (family: recovered/total, core-matched):`);
for (const [f, v] of [...perFamily.entries()].sort((a,b)=>b[1].total-a[1].total))
console.log(` ${f}: ${v.recovered}/${v.total} core=${v.coreMatch}`);
if (miss.noimg.length) { console.log(`\nnoimg samples (first 6):`); miss.noimg.slice(0,6).forEach(m=>console.log(` ${m.mfr_sku} coreMatch=${m.coreMatch}`)); }
if (miss.nopdp.length) { console.log(`\nnopdp samples (first 6):`); miss.nopdp.slice(0,6).forEach(m=>console.log(` ${m.mfr_sku} status=${m.status}`)); }
if (miss.notlive.length) { console.log(`\nnotlive samples (first 6):`); miss.notlive.slice(0,6).forEach(m=>console.log(` ${m.mfr_sku} ${m.url}`)); }