← back to Pj Image Repair
resolve_images.py
103 lines
#!/usr/bin/env python3
"""
TK-10467 — Phillip Jeffries logo-hero repair: resolve real product images.
READ-ONLY / $0 / no-auth. Sources the real product photo for each ACTIVE PJ
product from the PUBLIC phillipjeffries.com product page (og:image), gated to
the real webdamdb CDN + title cross-verification. Does NOT write to Shopify.
Resumable: appends to data/pj_resolved.tsv, skips already-done dw_sku.
"""
import csv, os, re, sys, time, html, json, urllib.request, urllib.error
from concurrent.futures import ThreadPoolExecutor, as_completed
HERE = os.path.dirname(os.path.abspath(__file__))
WORK = os.path.join(HERE, "data", "pj_active_worklist.tsv")
OUT = os.path.join(HERE, "data", "pj_resolved.tsv")
UA = "Mozilla/5.0 (compatible; DW-catalog-repair/1.0; +TK-10467)"
REAL_CDN = "cdn2.webdamdb.com" # real product-image CDN
FALLBACK_MARK = "assets/og-phillip" # generic shop-landing fallback og:image
def norm(s):
s = html.unescape(s or "").lower()
s = re.sub(r"[^a-z0-9]+", " ", s)
return re.sub(r"\s+", " ", s).strip()
def fetch(url, timeout=25):
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.geturl(), r.read().decode("utf-8", "replace")
def resolve(row):
mfr, dw, handle, title, cur = row
mfr = (mfr or "").strip()
out = {"dw_sku": dw, "mfr_sku": mfr, "our_title": title,
"status": "UNRESOLVED", "image": "", "vendor_title": "",
"title_match": "", "note": ""}
if not re.fullmatch(r"\d+", mfr):
out["note"] = "non-numeric-mfr_sku:needs-search-fallback"
return out
url = f"https://www.phillipjeffries.com/product/{mfr}"
for attempt in range(3):
try:
final, body = fetch(url)
break
except Exception as e:
if attempt == 2:
out["note"] = f"fetch-error:{type(e).__name__}"
return out
time.sleep(1.0 + attempt)
m = re.search(r'<meta property="og:image" content="([^"]+)"', body, re.I)
t = re.search(r"<title>([^<]*)</title>", body, re.I)
og = html.unescape(m.group(1)) if m else ""
vt = html.unescape(t.group(1)).replace(" | Phillip Jeffries", "").strip() if t else ""
out["vendor_title"] = vt
if not og or FALLBACK_MARK in og or REAL_CDN not in og:
out["note"] = "shop-landing-fallback-or-no-real-image"
return out
out["image"] = og
# cross-verify: our "Pattern - Color" vs vendor "Pattern in Color"
ours = norm(re.sub(r"\bnew\b|\brepeat.*$|by phillip jeffries.*$|at the dw.*$", "", title))
theirs = norm(vt.replace(" in ", " "))
otoks = set(ours.split()); ttoks = set(theirs.split())
inter = otoks & ttoks
ratio = len(inter) / max(1, len(otoks))
out["title_match"] = f"{ratio:.2f}"
out["status"] = "OK" if ratio >= 0.5 else "OK_LOWMATCH"
return out
def main():
with open(WORK) as f:
rows = [line.rstrip("\n").split("\t") for line in f if line.strip()]
rows = [r for r in rows if len(r) >= 5]
done = set()
if os.path.exists(OUT):
with open(OUT) as f:
for line in f:
p = line.split("\t")
if p and p[0] != "dw_sku":
done.add(p[0])
todo = [r for r in rows if r[1] not in done]
print(f"worklist={len(rows)} done={len(done)} todo={len(todo)}", flush=True)
new = not os.path.exists(OUT)
fh = open(OUT, "a", newline="")
w = csv.writer(fh, delimiter="\t")
if new:
w.writerow(["dw_sku","mfr_sku","status","title_match","image","vendor_title","our_title","note"])
fh.flush()
n = 0
counts = {}
with ThreadPoolExecutor(max_workers=6) as ex:
futs = {ex.submit(resolve, r): r for r in todo}
for fut in as_completed(futs):
o = fut.result()
w.writerow([o["dw_sku"],o["mfr_sku"],o["status"],o["title_match"],
o["image"],o["vendor_title"],o["our_title"],o["note"]])
n += 1
counts[o["status"]] = counts.get(o["status"],0)+1
if n % 100 == 0:
fh.flush(); print(f" {n}/{len(todo)} {counts}", flush=True)
fh.flush(); fh.close()
print(f"DONE resolved={n} {counts}", flush=True)
if __name__ == "__main__":
main()