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
`;
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();
});