mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
128 lines
3.3 KiB
JavaScript
128 lines
3.3 KiB
JavaScript
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const crypto = require("crypto");
|
|
|
|
const { normalizeCspc } = require("../utils/sku");
|
|
const { priceToNumber } = require("../utils/price");
|
|
|
|
function ensureDir(dir) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
function dbPathFor(key, baseUrl, dbDir) {
|
|
ensureDir(dbDir);
|
|
const hash = crypto.createHash("sha1").update(String(baseUrl)).digest("hex").slice(0, 8);
|
|
const safeKey = String(key).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
return path.join(dbDir, `${safeKey}__${hash}.json`);
|
|
}
|
|
|
|
function readDb(file) {
|
|
const byUrl = new Map();
|
|
try {
|
|
const txt = fs.readFileSync(file, "utf8");
|
|
const obj = JSON.parse(txt);
|
|
if (obj && Array.isArray(obj.items)) {
|
|
for (const it of obj.items) {
|
|
if (it && typeof it.url === "string" && it.url.startsWith("http")) {
|
|
byUrl.set(it.url, {
|
|
name: String(it.name || ""),
|
|
price: String(it.price || ""),
|
|
sku: String(it.sku || ""),
|
|
url: it.url,
|
|
img: String(it.img || it.image || it.thumb || "").trim(),
|
|
removed: Boolean(it.removed),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore missing or parse errors
|
|
}
|
|
return { byUrl };
|
|
}
|
|
|
|
function writeJsonAtomic(file, obj) {
|
|
ensureDir(path.dirname(file));
|
|
const tmp = `${file}.tmp`;
|
|
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
fs.renameSync(tmp, file);
|
|
}
|
|
|
|
function buildDbObject(ctx, merged) {
|
|
return {
|
|
version: 6,
|
|
store: ctx.store.host,
|
|
storeLabel: ctx.store.name,
|
|
category: ctx.cat.key,
|
|
categoryLabel: ctx.cat.label,
|
|
source: ctx.baseUrl,
|
|
updatedAt: new Date().toISOString(),
|
|
count: merged.size,
|
|
items: [...merged.values()]
|
|
.sort((a, b) => (a.name || "").localeCompare(b.name || ""))
|
|
.map((it) => ({
|
|
name: it.name,
|
|
price: it.price || "",
|
|
sku: normalizeCspc(it.sku) || "",
|
|
url: it.url,
|
|
img: String(it.img || "").trim(),
|
|
removed: Boolean(it.removed),
|
|
})),
|
|
};
|
|
}
|
|
|
|
function listDbFiles(dbDir) {
|
|
const out = [];
|
|
try {
|
|
for (const ent of fs.readdirSync(dbDir, { withFileTypes: true })) {
|
|
if (!ent.isFile()) continue;
|
|
const name = ent.name || "";
|
|
if (!name.endsWith(".json")) continue;
|
|
out.push(path.join(dbDir, name));
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function buildCheapestSkuIndexFromAllDbs(dbDir) {
|
|
const cheapest = new Map(); // sku -> { storeLabel, priceNum }
|
|
|
|
for (const file of listDbFiles(dbDir)) {
|
|
try {
|
|
const obj = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
const storeLabel = String(obj?.storeLabel || obj?.store || "");
|
|
const items = Array.isArray(obj?.items) ? obj.items : [];
|
|
|
|
for (const it of items) {
|
|
if (it?.removed) continue;
|
|
|
|
const sku = normalizeCspc(it?.sku || "");
|
|
if (!sku) continue;
|
|
|
|
const p = priceToNumber(it?.price || "");
|
|
if (!Number.isFinite(p) || p <= 0) continue;
|
|
|
|
const prev = cheapest.get(sku);
|
|
if (!prev || p < prev.priceNum) cheapest.set(sku, { storeLabel, priceNum: p });
|
|
}
|
|
} catch {
|
|
// ignore parse errors
|
|
}
|
|
}
|
|
|
|
return cheapest;
|
|
}
|
|
|
|
module.exports = {
|
|
ensureDir,
|
|
dbPathFor,
|
|
readDb,
|
|
writeJsonAtomic,
|
|
buildDbObject,
|
|
listDbFiles,
|
|
buildCheapestSkuIndexFromAllDbs,
|
|
};
|