Download script.js from Bubbles30/Diplomacy_Image_Finder: direct link, hf CLI and curl.
- Browser
- Download file 7.75 kB
-
https://huggingface.co/spaces/Bubbles30/Diplomacy_Image_Finder/resolve/main/script.js
- Command line
-
hf download hf://spaces/Bubbles30/Diplomacy_Image_Finder/script.js
-
curl -L -o script.js https://huggingface.co/spaces/Bubbles30/Diplomacy_Image_Finder/resolve/main/script.js
7.75 kB
| 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 = ` | |
| <div><strong>${escapeHtml(title)}</strong></div> | |
| ${vendor ? `<div>Vendor: ${escapeHtml(vendor)}</div>` : ""} | |
| ${productId ? `<div>Product ID: ${escapeHtml(String(productId))}</div>` : ""} | |
| `; | |
| 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 = ` | |
| <strong>${escapeHtml(item.type)}</strong> | |
| <div class="imageState">Candidate ${i+1}</div> | |
| <a href="${escapeHtml(item.url)}" target="_blank" rel="noopener">Open image</a> | |
| <div class="actions"> | |
| <a class="smallBtn" href="${escapeHtml(original)}" target="_blank" rel="noopener">Open original candidate</a> | |
| </div> | |
| `; | |
| card.appendChild(img); | |
| card.appendChild(body); | |
| gallery.appendChild(card); | |
| const tr = document.createElement("tr"); | |
| tr.innerHTML = ` | |
| <td>${i+1}</td> | |
| <td>${escapeHtml(item.type)}</td> | |
| <td class="url"><a href="${escapeHtml(item.url)}" target="_blank" rel="noopener">${escapeHtml(item.url)}</a></td> | |
| <td class="url"><a href="${escapeHtml(original)}" target="_blank" rel="noopener">${escapeHtml(original)}</a></td> | |
| `; | |
| 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(); | |
| }); | |