feat: Vintage Spirits

This commit is contained in:
Brennan Wilkes (Text Groove) 2026-01-29 21:54:19 -08:00
parent 54d15b4580
commit f84dab49bf
2 changed files with 259 additions and 0 deletions

View file

@ -12,6 +12,7 @@ const { createStore: createLegacy } = require("./legacyliquor");
const { createStore: createGull } = require("./gull");
const { createStore: createCoop } = require("./coop");
const { createStore: createTudor } = require("./tudor");
const { createStore: createVintage } = require("./vintagespirits");
function createStores({ defaultUa } = {}) {
return [
@ -27,6 +28,7 @@ function createStores({ defaultUa } = {}) {
createMaltsAndGrains(defaultUa),
createBCL(defaultUa),
createLegacy(defaultUa),
createVintage(defaultUa),
];
}

View file

@ -0,0 +1,257 @@
"use strict";
const { normalizeCspc } = require("../utils/sku");
const { humanBytes } = require("../utils/bytes");
const { padLeft, padRight } = require("../utils/string");
const { mergeDiscoveredIntoDb } = require("../tracker/merge");
const { buildDbObject, writeJsonAtomic } = require("../tracker/db");
const { addCategoryResultToReport } = require("../tracker/report");
function kbStr(bytes) {
return humanBytes(bytes).padStart(8, " ");
}
function secStr(ms) {
const s = Number.isFinite(ms) ? ms / 1000 : 0;
const t = Math.round(s * 10) / 10;
return (t < 10 ? `${t.toFixed(1)}s` : `${Math.round(s)}s`).padStart(7, " ");
}
function pageStr(i, total) {
const w = String(total).length;
return `${padLeft(i, w)}/${total}`;
}
function pctStr(done, total) {
const pct = total ? Math.floor((done / total) * 100) : 0;
return `${padLeft(pct, 3)}%`;
}
const BASE = "https://shop.vintagespirits.ca";
const SHOP_ID = "679-320"; // from your curl; can be made dynamic later
const IMG_BASE = "https://s.barnetnetwork.com/img/m/";
function asMoneyFromApi(it) {
// prefer explicit sale price when present
const sale = Number(it?.sale_price);
const regular = Number(it?.regular_price);
const net = Number(it?.net_price);
const n =
(Number.isFinite(sale) && sale > 0 ? sale : NaN) ||
(Number.isFinite(net) && net > 0 ? net : NaN) ||
(Number.isFinite(regular) && regular > 0 ? regular : NaN);
if (!Number.isFinite(n)) return "";
return `$${n.toFixed(2)}`;
}
function imgUrlFromApi(it) {
const p = String(it?.image || "").trim();
if (!p) return "";
if (/^https?:\/\//i.test(p)) return p;
if (p.startsWith("//")) return `https:${p}`;
// API provides "custom/goods/..."
return `${IMG_BASE}${p.replace(/^\/+/, "")}`;
}
function vintageItemFromApi(it) {
if (!it) return null;
// stock gate
if (!it.available_for_sale) return null;
const onHand = Number(it.on_hand);
if (Number.isFinite(onHand) && onHand <= 0) return null;
const url = String(it.url || "").trim();
const name = String(it.description || "").trim();
if (!url || !name) return null;
const sku = normalizeCspc(it.cspcid || "");
const price = asMoneyFromApi(it);
const img = imgUrlFromApi(it);
return { name, price, url, sku, img };
}
function makeApiUrl(cat, page) {
const u = new URL(`${BASE}/api/shop/${SHOP_ID}/products`);
u.searchParams.set("p", String(page));
u.searchParams.set("show_on_web", "true");
u.searchParams.set("sort_by", "desc");
u.searchParams.set("category", cat.vsCategory); // e.g. "40 SPIRITS"
u.searchParams.set("sub_category", cat.vsSubCategory); // e.g. "RUM"
u.searchParams.set("varital_name", "");
u.searchParams.set("no_item_found", "No item found.");
u.searchParams.set("avail_for_sale", "false");
u.searchParams.set("_dc", String(Math.floor(Math.random() * 1e10)));
return u.toString();
}
async function fetchVintagePage(ctx, page) {
const url = makeApiUrl(ctx.cat, page);
return await ctx.http.fetchJsonWithRetry(url, `vintage:api:${ctx.cat.key}:p${page}`, ctx.store.ua, {
method: "GET",
headers: {
Accept: "*/*",
Referer: ctx.cat.startUrl,
Origin: BASE,
},
// cookies not required in my testing; enable if you hit 403/empty
cookies: true,
});
}
async function scanCategoryVintageApi(ctx, prevDb, report) {
const t0 = Date.now();
let first;
try {
first = await fetchVintagePage(ctx, 1);
} catch (e) {
ctx.logger.warn(`${ctx.catPrefixOut} | Vintage API fetch failed: ${e?.message || e}`);
const discovered = new Map();
const { merged, newItems, updatedItems, removedItems, restoredItems } = mergeDiscoveredIntoDb(prevDb, discovered, {
storeLabel: ctx.store.name,
});
const dbObj = buildDbObject(ctx, merged);
writeJsonAtomic(ctx.dbFile, dbObj);
const elapsed = Date.now() - t0;
report.categories.push({
store: ctx.store.name,
label: ctx.cat.label,
key: ctx.cat.key,
dbFile: ctx.dbFile,
scannedPages: 1,
discoveredUnique: 0,
newCount: newItems.length,
updatedCount: updatedItems.length,
removedCount: removedItems.length,
restoredCount: restoredItems.length,
elapsedMs: elapsed,
});
report.totals.newCount += newItems.length;
report.totals.updatedCount += updatedItems.length;
report.totals.removedCount += removedItems.length;
report.totals.restoredCount += restoredItems.length;
addCategoryResultToReport(report, ctx.store.name, ctx.cat.label, newItems, updatedItems, removedItems, restoredItems);
return;
}
const totalPages = Math.max(1, Number(first?.json?.paginator?.pages) || 1);
const scanPages = ctx.config.maxPages === null ? totalPages : Math.min(ctx.config.maxPages, totalPages);
ctx.logger.ok(
`${ctx.catPrefixOut} | Pages: ${scanPages}${scanPages !== totalPages ? ` (cap from ${totalPages})` : ""}`
);
const pages = [];
for (let p = 1; p <= scanPages; p++) pages.push(p);
let donePages = 0;
const perPageItems = await require("../utils/async").parallelMapStaggered(
pages,
ctx.config.concurrency,
ctx.config.staggerMs,
async (page, idx) => {
const r = page === 1 ? first : await fetchVintagePage(ctx, page);
const arr = Array.isArray(r?.json?.items) ? r.json.items : [];
const items = [];
for (const raw of arr) {
const it = vintageItemFromApi(raw);
if (it) items.push(it);
}
donePages++;
ctx.logger.ok(
`${ctx.catPrefixOut} | Page ${pageStr(idx + 1, pages.length)} | ${String(r.status || "").padEnd(
3
)} | ${pctStr(donePages, pages.length)} | items=${padLeft(items.length, 3)} | bytes=${kbStr(
r.bytes
)} | ${padRight(ctx.http.inflightStr(), 11)} | ${secStr(r.ms)}`
);
return items;
}
);
const discovered = new Map();
let dups = 0;
for (const arr of perPageItems) {
for (const it of arr) {
if (discovered.has(it.url)) dups++;
discovered.set(it.url, it);
}
}
ctx.logger.ok(`${ctx.catPrefixOut} | Unique products (this run): ${discovered.size}${dups ? ` (${dups} dups)` : ""}`);
const { merged, newItems, updatedItems, removedItems, restoredItems } = mergeDiscoveredIntoDb(prevDb, discovered, {
storeLabel: ctx.store.name,
});
const dbObj = buildDbObject(ctx, merged);
writeJsonAtomic(ctx.dbFile, dbObj);
const elapsed = Date.now() - t0;
ctx.logger.ok(
`${ctx.catPrefixOut} | Done in ${secStr(elapsed)}. New=${newItems.length} Updated=${updatedItems.length} Removed=${removedItems.length} Restored=${restoredItems.length} Total(DB)=${merged.size}`
);
report.categories.push({
store: ctx.store.name,
label: ctx.cat.label,
key: ctx.cat.key,
dbFile: ctx.dbFile,
scannedPages: scanPages,
discoveredUnique: discovered.size,
newCount: newItems.length,
updatedCount: updatedItems.length,
removedCount: removedItems.length,
restoredCount: restoredItems.length,
elapsedMs: elapsed,
});
report.totals.newCount += newItems.length;
report.totals.updatedCount += updatedItems.length;
report.totals.removedCount += removedItems.length;
report.totals.restoredCount += restoredItems.length;
addCategoryResultToReport(report, ctx.store.name, ctx.cat.label, newItems, updatedItems, removedItems, restoredItems);
}
function createStore(defaultUa) {
return {
key: "vintage",
name: "Vintage Spirits",
host: "shop.vintagespirits.ca",
ua: defaultUa,
scanCategory: scanCategoryVintageApi,
categories: [
{
key: "whisky-whiskey",
label: "Whisky & Whiskey",
startUrl: "https://shop.vintagespirits.ca/products?category=40+SPIRITS&sub_category=WHISKY+%26+WHISKEY",
vsCategory: "40 SPIRITS",
vsSubCategory: "WHISKY & WHISKEY",
},
{
key: "single-malt-whisky",
label: "Single Malt Whisky",
startUrl: "https://shop.vintagespirits.ca/products?category=40+SPIRITS&sub_category=SINGLE+MALT+WHISKY",
vsCategory: "40 SPIRITS",
vsSubCategory: "SINGLE MALT WHISKY",
},
{
key: "rum",
label: "Rum",
startUrl: "https://shop.vintagespirits.ca/products?category=40+SPIRITS&sub_category=RUM",
vsCategory: "40 SPIRITS",
vsSubCategory: "RUM",
},
],
};
}
module.exports = { createStore };