2026-02-12 15:22:20 -05:00
|
|
|
import 'dotenv/config';
|
|
|
|
|
import * as schema from '../src/db/schema.ts';
|
2026-03-11 19:18:45 -04:00
|
|
|
import { db, ClosePool } from '../src/db/index.ts';
|
2026-02-19 16:04:34 -05:00
|
|
|
|
2026-02-12 15:22:20 -05:00
|
|
|
import fs from "node:fs/promises";
|
|
|
|
|
import path from "node:path";
|
|
|
|
|
import chalk from 'chalk';
|
2026-03-19 22:18:06 -04:00
|
|
|
import * as helper from './pokemon-helper.ts';
|
2026-02-12 15:22:20 -05:00
|
|
|
//import util from 'util';
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 22:18:06 -04:00
|
|
|
async function syncTcgplayer(cardSets:string[] = []) {
|
2026-02-12 15:22:20 -05:00
|
|
|
|
2026-02-20 05:49:05 -05:00
|
|
|
const productLines = [ "pokemon", "pokemon-japan" ];
|
2026-02-12 15:22:20 -05:00
|
|
|
|
2026-02-20 05:49:05 -05:00
|
|
|
// work from the available sets within the product line
|
2026-02-12 15:22:20 -05:00
|
|
|
for (const productLine of productLines) {
|
2026-02-20 05:49:05 -05:00
|
|
|
const d = {"algorithm":"sales_dismax","from":0,"size":1,"filters":{"term":{"productLineName":[productLine]}},"settings":{"useFuzzySearch":false}};
|
|
|
|
|
|
|
|
|
|
const response = await fetch('https://mp-search-api.tcgplayer.com/v1/search/request?q=&isList=false', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {'Content-Type': 'application/json',},
|
|
|
|
|
body: JSON.stringify(d),
|
|
|
|
|
});
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
console.error('Error notifying sync completion:', response.statusText);
|
|
|
|
|
process.exit(1);
|
2026-02-12 15:22:20 -05:00
|
|
|
}
|
2026-02-20 05:49:05 -05:00
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
const setNames = data.results[0].aggregations.setName;
|
|
|
|
|
for (const setName of setNames) {
|
2026-03-19 22:18:06 -04:00
|
|
|
let processSet = true;
|
|
|
|
|
if (cardSets.length > 0) {
|
|
|
|
|
processSet = cardSets.some(set => setName.value.toLowerCase().includes(set.toLowerCase()));
|
|
|
|
|
}
|
|
|
|
|
if (processSet) {
|
|
|
|
|
console.log(chalk.blue(`Syncing product line "${productLine}" with setName "${setName.urlValue}"...`));
|
|
|
|
|
await syncProductLine(productLine, "setName", setName.urlValue);
|
|
|
|
|
}
|
2026-02-20 05:49:05 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-12 15:22:20 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(chalk.green('✓ All TCGPlayer data synchronized successfully!'));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 05:49:05 -05:00
|
|
|
|
2026-02-19 16:04:34 -05:00
|
|
|
async function syncProductLine(productLine: string, field: string, fieldValue: string) {
|
2026-02-12 15:22:20 -05:00
|
|
|
let start = 0;
|
|
|
|
|
let size = 50;
|
|
|
|
|
let total = 1000000;
|
|
|
|
|
|
|
|
|
|
while (start < total) {
|
|
|
|
|
console.log(` Fetching items ${start} to ${start + size} of ${total}...`);
|
|
|
|
|
|
2026-02-19 16:04:34 -05:00
|
|
|
const d = {
|
2026-02-12 15:22:20 -05:00
|
|
|
"algorithm":"sales_dismax",
|
|
|
|
|
"from":start,
|
|
|
|
|
"size":size,
|
|
|
|
|
"filters":{
|
2026-02-19 16:04:34 -05:00
|
|
|
"term":{"productLineName":[productLine], [field]:[fieldValue]} ,
|
2026-02-12 15:22:20 -05:00
|
|
|
"range":{},
|
|
|
|
|
"match":{}
|
|
|
|
|
},
|
|
|
|
|
"listingSearch":{
|
|
|
|
|
"context":{"cart":{}},
|
|
|
|
|
"filters":{"term":{
|
|
|
|
|
"sellerStatus":"Live",
|
|
|
|
|
"channelId":0
|
|
|
|
|
},
|
|
|
|
|
"range":{
|
|
|
|
|
"quantity":{"gte":1}
|
|
|
|
|
},
|
|
|
|
|
"exclude":{"channelExclusion":0}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"context":{
|
|
|
|
|
"cart":{},
|
|
|
|
|
"shippingCountry":"US",
|
|
|
|
|
"userProfile":{}
|
|
|
|
|
},
|
|
|
|
|
"settings":{
|
|
|
|
|
"useFuzzySearch":false,
|
|
|
|
|
"didYouMean":{}
|
|
|
|
|
},
|
|
|
|
|
"sort":{}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//console.log(util.inspect(d, { depth: null }));
|
|
|
|
|
//process.exit(1);
|
|
|
|
|
|
|
|
|
|
const response = await fetch('https://mp-search-api.tcgplayer.com/v1/search/request?q=&isList=false', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(d),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
console.error('Error notifying sync completion:', response.statusText);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
total = data.results[0].totalResults;
|
|
|
|
|
|
|
|
|
|
for (const item of data.results[0].results) {
|
2026-02-20 05:49:05 -05:00
|
|
|
|
2026-03-11 19:18:45 -04:00
|
|
|
// Check if productId already exists and skip if it does (to avoid hitting the API too much)
|
2026-03-19 22:18:06 -04:00
|
|
|
if (allProductIds.size > 0 && allProductIds.has(item.productId)) {
|
2026-03-11 19:18:45 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
2026-02-20 05:49:05 -05:00
|
|
|
|
2026-02-12 15:22:20 -05:00
|
|
|
console.log(chalk.blue(` - ${item.productName} (ID: ${item.productId})`));
|
|
|
|
|
|
2026-02-19 16:04:34 -05:00
|
|
|
// Get product detail
|
|
|
|
|
const detailResponse = await fetch(`https://mp-search-api.tcgplayer.com/v2/product/${item.productId}/details`, {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
});
|
|
|
|
|
if (!detailResponse.ok) {
|
|
|
|
|
console.error('Error fetching product details:', detailResponse.statusText);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
const detailData = await detailResponse.json();
|
|
|
|
|
|
|
|
|
|
|
2026-02-23 19:05:55 -05:00
|
|
|
await db.insert(schema.tcgcards).values({
|
2026-02-12 15:22:20 -05:00
|
|
|
productId: item.productId,
|
2026-03-03 07:59:44 -05:00
|
|
|
productName: detailData.productName,
|
2026-02-23 19:05:55 -05:00
|
|
|
//productName: cleanProductName(item.productName),
|
2026-02-12 15:22:20 -05:00
|
|
|
rarityName: item.rarityName,
|
2026-03-03 07:59:44 -05:00
|
|
|
productLineName: detailData.productLineName,
|
|
|
|
|
productLineUrlName: detailData.productLineUrlName,
|
|
|
|
|
productStatusId: detailData.productStatusId,
|
|
|
|
|
productTypeId: detailData.productTypeId,
|
|
|
|
|
productUrlName: detailData.productUrlName,
|
|
|
|
|
setId: detailData.setId,
|
|
|
|
|
shippingCategoryId: detailData.shippingCategoryId,
|
|
|
|
|
sealed: detailData.sealed,
|
|
|
|
|
sellerListable: detailData.sellerListable,
|
|
|
|
|
foilOnly: detailData.foilOnly,
|
2026-02-12 15:22:20 -05:00
|
|
|
attack1: item.customAttributes.attack1 || null,
|
|
|
|
|
attack2: item.customAttributes.attack2 || null,
|
|
|
|
|
attack3: item.customAttributes.attack3 || null,
|
|
|
|
|
attack4: item.customAttributes.attack4 || null,
|
|
|
|
|
cardType: item.customAttributes.cardType?.[0] || null,
|
|
|
|
|
cardTypeB: item.customAttributes.cardTypeB || null,
|
2026-03-03 07:59:44 -05:00
|
|
|
energyType: detailData.customAttributes.energyType?.[0] || null,
|
|
|
|
|
flavorText: detailData.customAttributes.flavorText || null,
|
2026-03-19 22:18:06 -04:00
|
|
|
hp: helper.GetNumberOrNull(item.customAttributes.hp),
|
2026-03-03 07:59:44 -05:00
|
|
|
number: detailData.customAttributes.number || '',
|
|
|
|
|
releaseDate: detailData.customAttributes.releaseDate ? new Date(detailData.customAttributes.releaseDate) : null,
|
2026-02-12 15:22:20 -05:00
|
|
|
resistance: item.customAttributes.resistance || null,
|
|
|
|
|
retreatCost: item.customAttributes.retreatCost || null,
|
|
|
|
|
stage: item.customAttributes.stage || null,
|
|
|
|
|
weakness: item.customAttributes.weakness || null,
|
2026-03-03 07:59:44 -05:00
|
|
|
lowestPrice: detailData.lowestPrice,
|
|
|
|
|
lowestPriceWithShipping: detailData.lowestPriceWithShipping,
|
|
|
|
|
marketPrice: detailData.marketPrice,
|
|
|
|
|
maxFulfillableQuantity: detailData.maxFulfillableQuantity,
|
|
|
|
|
medianPrice: detailData.medianPrice,
|
2026-02-12 15:22:20 -05:00
|
|
|
totalListings: item.totalListings,
|
2026-03-11 19:18:45 -04:00
|
|
|
artist: detailData.formattedAttributes.Artist || null,
|
|
|
|
|
}).onConflictDoUpdate({
|
|
|
|
|
target: schema.tcgcards.productId,
|
2026-02-12 15:22:20 -05:00
|
|
|
set: {
|
2026-03-03 07:59:44 -05:00
|
|
|
productName: detailData.productName,
|
2026-02-23 19:05:55 -05:00
|
|
|
//productName: cleanProductName(item.productName),
|
2026-02-12 15:22:20 -05:00
|
|
|
rarityName: item.rarityName,
|
2026-03-03 07:59:44 -05:00
|
|
|
productLineName: detailData.productLineName,
|
|
|
|
|
productLineUrlName: detailData.productLineUrlName,
|
|
|
|
|
productStatusId: detailData.productStatusId,
|
|
|
|
|
productTypeId: detailData.productTypeId,
|
|
|
|
|
productUrlName: detailData.productUrlName,
|
|
|
|
|
setId: detailData.setId,
|
|
|
|
|
shippingCategoryId: detailData.shippingCategoryId,
|
|
|
|
|
sealed: detailData.sealed,
|
|
|
|
|
sellerListable: detailData.sellerListable,
|
|
|
|
|
foilOnly: detailData.foilOnly,
|
2026-02-12 15:22:20 -05:00
|
|
|
attack1: item.customAttributes.attack1 || null,
|
|
|
|
|
attack2: item.customAttributes.attack2 || null,
|
|
|
|
|
attack3: item.customAttributes.attack3 || null,
|
|
|
|
|
attack4: item.customAttributes.attack4 || null,
|
|
|
|
|
cardType: item.customAttributes.cardType?.[0] || null,
|
|
|
|
|
cardTypeB: item.customAttributes.cardTypeB || null,
|
2026-03-03 07:59:44 -05:00
|
|
|
energyType: detailData.customAttributes.energyType?.[0] || null,
|
|
|
|
|
flavorText: detailData.customAttributes.flavorText || null,
|
2026-03-19 22:18:06 -04:00
|
|
|
hp: helper.GetNumberOrNull(item.customAttributes.hp),
|
2026-03-03 07:59:44 -05:00
|
|
|
number: detailData.customAttributes.number || '',
|
|
|
|
|
releaseDate: detailData.customAttributes.releaseDate ? new Date(detailData.customAttributes.releaseDate) : null,
|
2026-02-12 15:22:20 -05:00
|
|
|
resistance: item.customAttributes.resistance || null,
|
|
|
|
|
retreatCost: item.customAttributes.retreatCost || null,
|
|
|
|
|
stage: item.customAttributes.stage || null,
|
|
|
|
|
weakness: item.customAttributes.weakness || null,
|
2026-03-03 07:59:44 -05:00
|
|
|
lowestPrice: detailData.lowestPrice,
|
|
|
|
|
lowestPriceWithShipping: detailData.lowestPriceWithShipping,
|
|
|
|
|
marketPrice: detailData.marketPrice,
|
|
|
|
|
maxFulfillableQuantity: detailData.maxFulfillableQuantity,
|
|
|
|
|
medianPrice: detailData.medianPrice,
|
2026-02-12 15:22:20 -05:00
|
|
|
totalListings: item.totalListings,
|
2026-03-11 19:18:45 -04:00
|
|
|
artist: detailData.formattedAttributes.Artist || null,
|
2026-02-12 15:22:20 -05:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-19 22:18:06 -04:00
|
|
|
console.log(`item: ${item.setId}\tdetail: ${detailData.setId}`);
|
|
|
|
|
console.log(`item: ${item.setCode}\tdetail: ${detailData.setCode}`);
|
|
|
|
|
console.log(`item: ${item.setName}\tdetail: ${detailData.setName}`);
|
2026-02-19 16:04:34 -05:00
|
|
|
// set is...
|
2026-02-12 15:22:20 -05:00
|
|
|
await db.insert(schema.sets).values({
|
|
|
|
|
setId: detailData.setId,
|
|
|
|
|
setCode: detailData.setCode,
|
|
|
|
|
setName: detailData.setName,
|
|
|
|
|
setUrlName: detailData.setUrlName,
|
2026-03-11 19:18:45 -04:00
|
|
|
}).onConflictDoUpdate({
|
|
|
|
|
target: schema.sets.setId,
|
2026-02-12 15:22:20 -05:00
|
|
|
set: {
|
|
|
|
|
setCode: detailData.setCode,
|
|
|
|
|
setName: detailData.setName,
|
|
|
|
|
setUrlName: detailData.setUrlName,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// skus are...
|
|
|
|
|
for (const skuItem of detailData.skus) {
|
|
|
|
|
|
|
|
|
|
await db.insert(schema.skus).values({
|
|
|
|
|
skuId: skuItem.sku,
|
|
|
|
|
productId: detailData.productId,
|
|
|
|
|
condition: skuItem.condition,
|
|
|
|
|
language: skuItem.language,
|
|
|
|
|
variant: skuItem.variant,
|
2026-03-11 19:18:45 -04:00
|
|
|
}).onConflictDoUpdate({
|
|
|
|
|
target: schema.skus.skuId,
|
2026-02-12 15:22:20 -05:00
|
|
|
set: {
|
|
|
|
|
condition: skuItem.condition,
|
|
|
|
|
language: skuItem.language,
|
|
|
|
|
variant: skuItem.variant,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// get image if it doesn't already exist
|
|
|
|
|
const imagePath = path.join(process.cwd(), 'public', 'cards', `${item.productId}.jpg`);
|
2026-03-19 22:18:06 -04:00
|
|
|
if (!await helper.FileExists(imagePath)) {
|
2026-02-12 15:22:20 -05:00
|
|
|
const imageResponse = await fetch(`https://tcgplayer-cdn.tcgplayer.com/product/${item.productId}_in_1000x1000.jpg`);
|
|
|
|
|
if (imageResponse.ok) {
|
|
|
|
|
const buffer = await imageResponse.arrayBuffer();
|
|
|
|
|
await fs.writeFile(imagePath, Buffer.from(buffer));
|
|
|
|
|
} else {
|
2026-02-19 16:04:34 -05:00
|
|
|
console.error(chalk.yellow(`Error fetching ${item.productId}: ${item.productName} image:`, imageResponse.statusText));
|
|
|
|
|
await fs.appendFile('missing_images.log', `${item.productId}: ${item.productName}\n`, 'utf-8');
|
2026-02-12 15:22:20 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// be nice to the API and not send too many requests in a short time
|
2026-03-19 22:18:06 -04:00
|
|
|
await helper.Sleep(300);
|
2026-02-12 15:22:20 -05:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start += size;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 16:04:34 -05:00
|
|
|
// clear the log file
|
|
|
|
|
await fs.rm('missing_images.log', { force: true });
|
2026-03-19 22:18:06 -04:00
|
|
|
let allProductIds = new Set();
|
|
|
|
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
if (args.length === 0) {
|
|
|
|
|
allProductIds = new Set(await db.select({ productId: schema.cards.productId }).from(schema.cards).then(rows => rows.map(row => row.productId)));
|
|
|
|
|
await syncTcgplayer();
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
await syncTcgplayer(args);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// update the card table with new/updated variants
|
|
|
|
|
await helper.UpdateVariants(db);
|
2026-02-12 15:22:20 -05:00
|
|
|
|
2026-03-19 22:18:06 -04:00
|
|
|
// index the card updates
|
|
|
|
|
helper.upsertCardCollection(db);
|
2026-02-20 05:49:05 -05:00
|
|
|
|
2026-03-11 19:18:45 -04:00
|
|
|
await ClosePool();
|