121 lines
3.5 KiB
JavaScript
121 lines
3.5 KiB
JavaScript
|
|
const DEFAULT_FOILIO_FEED_URL = 'https://foilio.net/api/prices/feed?game=lorcana';
|
||
|
|
|
||
|
|
export function getFolioFeedUrl() {
|
||
|
|
return process.env.FOILIO_PRICES_FEED_URL ?? DEFAULT_FOILIO_FEED_URL;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeFolioName(name) {
|
||
|
|
return String(name ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseAmount(value) {
|
||
|
|
if (value == null || value === '') return null;
|
||
|
|
const amount = Number(value);
|
||
|
|
return Number.isFinite(amount) ? amount : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parse a Foilio feed row into current/market prices.
|
||
|
|
* Prefer USD fields; fall back to EUR with currency noted for snapshots.
|
||
|
|
*/
|
||
|
|
export function parseFolioPriceEntry(entry) {
|
||
|
|
const usdLow =
|
||
|
|
parseAmount(entry.usd_low)
|
||
|
|
?? parseAmount(entry.low_usd)
|
||
|
|
?? parseAmount(entry.price_usd)
|
||
|
|
?? parseAmount(entry.usd);
|
||
|
|
const usdMarket =
|
||
|
|
parseAmount(entry.usd_market)
|
||
|
|
?? parseAmount(entry.market_usd)
|
||
|
|
?? parseAmount(entry.market)
|
||
|
|
?? parseAmount(entry.price);
|
||
|
|
|
||
|
|
if (usdLow != null || usdMarket != null) {
|
||
|
|
return {
|
||
|
|
currentPrice: usdLow ?? usdMarket,
|
||
|
|
marketPrice: usdMarket ?? usdLow,
|
||
|
|
currency: 'USD',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const eurLow = parseAmount(entry.eur_low) ?? parseAmount(entry.low_eur);
|
||
|
|
const eurMarket = parseAmount(entry.eur_market) ?? parseAmount(entry.eur) ?? parseAmount(entry.price_eur);
|
||
|
|
if (eurLow != null || eurMarket != null) {
|
||
|
|
return {
|
||
|
|
currentPrice: eurLow ?? eurMarket,
|
||
|
|
marketPrice: eurMarket ?? eurLow,
|
||
|
|
currency: 'EUR',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return { currentPrice: null, marketPrice: null, currency: 'USD' };
|
||
|
|
}
|
||
|
|
|
||
|
|
function feedEntries(payload) {
|
||
|
|
if (Array.isArray(payload)) return payload;
|
||
|
|
if (Array.isArray(payload?.prices)) return payload.prices;
|
||
|
|
if (Array.isArray(payload?.cards)) return payload.cards;
|
||
|
|
if (Array.isArray(payload?.data)) return payload.data;
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build lookup indexes from a Foilio feed payload.
|
||
|
|
*/
|
||
|
|
export function buildFolioPriceIndex(payload) {
|
||
|
|
const byCardId = new Map();
|
||
|
|
const byNameSet = new Map();
|
||
|
|
|
||
|
|
for (const entry of feedEntries(payload)) {
|
||
|
|
const parsed = parseFolioPriceEntry(entry);
|
||
|
|
if (parsed.currentPrice == null && parsed.marketPrice == null) continue;
|
||
|
|
|
||
|
|
const cardId = entry.card_id ?? entry.cardId ?? entry.id ?? entry.unique_id ?? entry.Unique_ID;
|
||
|
|
if (cardId) {
|
||
|
|
byCardId.set(String(cardId), parsed);
|
||
|
|
}
|
||
|
|
|
||
|
|
const setCode = entry.set ?? entry.set_id ?? entry.setId ?? entry.Set_ID;
|
||
|
|
const name = entry.name ?? entry.Name;
|
||
|
|
if (setCode && name) {
|
||
|
|
const key = `${normalizeFolioName(name)}|${String(setCode).trim()}`;
|
||
|
|
if (!byNameSet.has(key)) {
|
||
|
|
byNameSet.set(key, parsed);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return { byCardId, byNameSet };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Match a Lorcana catalog row against the Foilio price index.
|
||
|
|
*/
|
||
|
|
export function matchFolioPrice(index, row) {
|
||
|
|
const byId = index.byCardId.get(String(row.scryfallId));
|
||
|
|
if (byId) return byId;
|
||
|
|
|
||
|
|
const key = `${normalizeFolioName(row.name)}|${String(row.setCode ?? '').trim()}`;
|
||
|
|
return index.byNameSet.get(key) ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch the Foilio Lorcana price feed once per sync run.
|
||
|
|
*/
|
||
|
|
export async function fetchFolioPriceFeed() {
|
||
|
|
const response = await fetch(getFolioFeedUrl(), {
|
||
|
|
headers: { Accept: 'application/json', 'User-Agent': 'DeckHearth/1.0' },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Foilio feed error: ${response.status}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return response.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function fetchFolioPriceIndex() {
|
||
|
|
const payload = await fetchFolioPriceFeed();
|
||
|
|
return buildFolioPriceIndex(payload);
|
||
|
|
}
|