const $ = (id) => document.getElementById(id); const urlInput = $("url"); const findBtn = $("find"); const statusBox = $("status"); const productSection = $("product"); const productInfo = $("productInfo"); const gallerySection = $("gallerySection"); const gallery = $("gallery"); const count = $("count"); const tableSection = $("tableSection"); const tableBody = $("tableBody"); function showStatus(message, kind="ok") { statusBox.className = `status ${kind}`; statusBox.textContent = message; statusBox.classList.remove("hidden"); } function hideSections() { productSection.classList.add("hidden"); gallerySection.classList.add("hidden"); tableSection.classList.add("hidden"); gallery.innerHTML = ""; tableBody.innerHTML = ""; } function normalizeOriginal(u) { if (!u) return ""; try { const x = new URL(u); x.pathname = x.pathname .replace(/_(pico|icon|thumb|small|compact|medium|large|grande|master|\d+x\d*|x\d+)(_[a-z0-9_-]+)*(\.[a-z0-9]+)$/i, "$3") .replace(/_progressive(\.[a-z0-9]+)$/i, "$1"); return x.toString(); } catch { return u .replace(/_(pico|icon|thumb|small|compact|medium|large|grande|master|\d+x\d*)(_[a-z0-9_-]+)*(\.[a-z0-9]+)(?=\?|$)/i, "$3") .replace(/_progressive(\.[a-z0-9]+)(?=\?|$)/i, "$1"); } } function addCandidate(map, url, type) { if (!url) return; try { url = new URL(url).href; } catch { return; } if (!/\.(jpg|jpeg|png|webp|avif)(\?.*)?$/i.test(url)) return; if (!map.has(url)) map.set(url, {url, type}); } function collectImagesFromObject(obj, map, type="shopify-json") { if (!obj) return; if (typeof obj === "string") { if (/\.(jpg|jpeg|png|webp|avif)(\?.*)?$/i.test(obj)) addCandidate(map, obj, type); return; } if (Array.isArray(obj)) { obj.forEach(x => collectImagesFromObject(x, map, type)); return; } if (typeof obj === "object") { for (const [key, value] of Object.entries(obj)) { const k = key.toLowerCase(); if (["image","images","src","url","featured_image"].includes(k)) { collectImagesFromObject(value, map, `${type}:${key}`); } else if (typeof value === "object") { collectImagesFromObject(value, map, type); } } } } async function fetchJSON(url) { const r = await fetch(url, {headers: {"Accept":"application/json"}}); if (!r.ok) throw new Error(`${r.status} ${r.statusText}`); return await r.json(); } async function getShopifyProduct(productUrl) { const u = new URL(productUrl); const match = u.pathname.match(/\/products\/([^/?#]+)/i); if (!match) throw new Error("This does not look like a Shopify /products/... URL."); const handle = match[1]; // Shopify Ajax product endpoint. This is lightweight and does not need a server. const endpoint = `${u.origin}/products/${handle}.js`; return await fetchJSON(endpoint); } function renderProduct(product) { const title = product.title || "Unknown product"; const vendor = product.vendor || ""; const productId = product.id || ""; productInfo.innerHTML = `
${escapeHtml(title)}
${vendor ? `
Vendor: ${escapeHtml(vendor)}
` : ""} ${productId ? `
Product ID: ${escapeHtml(String(productId))}
` : ""} `; productSection.classList.remove("hidden"); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[c])); } function rank(items) { return items.sort((a,b) => score(b) - score(a)); } function score(x) { let s = 0; const u = x.url.toLowerCase(); const t = x.type.toLowerCase(); if (t.includes("original")) s += 100; if (!/_\d+x\d*(?:_|\.|\?)/i.test(u) && !/_progressive\./i.test(u)) s += 25; if (t.includes("shopify")) s += 10; return s; } function renderResults(items) { const ranked = rank(items); count.textContent = `${ranked.length} candidate(s)`; gallery.innerHTML = ""; tableBody.innerHTML = ""; ranked.forEach((item, i) => { const original = normalizeOriginal(item.url); const card = document.createElement("div"); card.className = "item"; const img = document.createElement("img"); img.loading = "lazy"; img.src = item.url; img.alt = `Candidate ${i+1}`; img.onerror = () => { card.querySelector(".imageState").textContent = "Image could not be loaded by the browser"; img.style.opacity = ".25"; }; const body = document.createElement("div"); body.className = "itemBody"; body.innerHTML = ` ${escapeHtml(item.type)}
Candidate ${i+1}
Open image
Open original candidate
`; card.appendChild(img); card.appendChild(body); gallery.appendChild(card); const tr = document.createElement("tr"); tr.innerHTML = ` ${i+1} ${escapeHtml(item.type)} ${escapeHtml(item.url)} ${escapeHtml(original)} `; tableBody.appendChild(tr); }); gallerySection.classList.remove("hidden"); tableSection.classList.remove("hidden"); } async function findImages() { const productUrl = urlInput.value.trim(); hideSections(); if (!productUrl) { showStatus("Enter an Al Diplomacy product URL.", "warn"); return; } findBtn.disabled = true; findBtn.textContent = "Searching..."; showStatus("Reading Shopify product data...", "ok"); try { const product = await getShopifyProduct(productUrl); renderProduct(product); const map = new Map(); // Shopify product.js normally exposes the complete image array. if (product.images) collectImagesFromObject(product.images, map, "shopify:images"); if (product.featured_image) collectImagesFromObject(product.featured_image, map, "shopify:featured_image"); // Some themes expose additional media. if (product.media) collectImagesFromObject(product.media, map, "shopify:media"); const items = [...map.values()]; // Add original variants. const originals = []; items.forEach(x => { const o = normalizeOriginal(x.url); if (o && o !== x.url && !map.has(o)) { originals.push({url:o, type:x.type + " -> original"}); } }); originals.forEach(x => map.set(x.url, x)); const finalItems = [...map.values()]; if (!finalItems.length) { showStatus( "The Shopify endpoint worked, but no image assets were returned. " + "This product may use a theme/media structure that needs a different extractor.", "warn" ); } else { showStatus( `Success. Shopify returned ${finalItems.length} image candidate(s). ` + "The app is showing the original/high-resolution candidates first where possible.", "ok" ); renderResults(finalItems); } } catch (e) { showStatus( "The browser could not read the Shopify product endpoint.\n\n" + `Reason: ${e.message}\n\n` + "This is usually a CORS/site-access restriction. If Al Diplomacy blocks " + "browser requests, a tiny CPU-only backend proxy will be needed; no GPU is required.", "err" ); } finally { findBtn.disabled = false; findBtn.textContent = "Find Images"; } } findBtn.addEventListener("click", findImages); urlInput.addEventListener("keydown", e => { if (e.key === "Enter") findImages(); });