1
0
mirror of https://github.com/tcgdex/cards-database.git synced 2025-08-16 09:08:52 +00:00

feat: Add sku endpoint

This commit is contained in:
2025-08-06 13:32:01 +02:00
parent 2c746914e9
commit dbcac8608b
7 changed files with 871448 additions and 59 deletions

View File

@@ -89,6 +89,11 @@ export async function getAllCards(lang: SupportedLanguages): Promise<Array<SDKCa
return Promise.all((cards[lang] as Array<MappedCard>).map((it) => loadCard(lang, it.id)))
}
export function getCompiledCard(lang: SupportedLanguages, id: string): any {
const key = `${id}${lang}`
return list[key]
}
/**
* Function that do the hard work of loading the card with the external processors
*

View File

@@ -6,9 +6,10 @@ import { Errors, sendError } from '../../libs/Errors'
import type { Query } from '../../libs/QueryEngine/filter'
import { recordToQuery } from '../../libs/QueryEngine/parsers'
import { betterSorter, checkLanguage, unique } from '../../util'
import { getAllCards, findOneCard, findCards, toBrief, getCardById } from '../Components/Card'
import { getAllCards, findOneCard, findCards, toBrief, getCardById, getCompiledCard } from '../Components/Card'
import { findOneSet, findSets, setToBrief } from '../Components/Set'
import { findOneSerie, findSeries, serieToBrief } from '../Components/Serie'
import { listSKUs } from '../../libs/providers/tcgplayer'
type CustomRequest = Request & {
/**
@@ -294,8 +295,12 @@ server
}
let result: unknown
switch (endpoint) {
case 'cards':
if (subid === 'skus') {
result = await listSKUs(getCompiledCard(lang, id))
}
break
case 'sets':
// allow the dev to use a non prefixed value like `10` instead of `010` for newer sets
// @ts-expect-error normal behavior until the filtering is more fiable

View File

@@ -50,8 +50,10 @@ if (cluster.isPrimary) {
// fetch cardmarket data
void updateDatas()
.then(() => console.log('loaded cardmarket datas'))
.catch((err) => console.error('error loading cardmarket', err))
void updateTCGPlayerDatas()
.then(() => console.log('loaded TCGPlayer datas'))
.catch((err) => console.error('error loading TCGPlayer', err))
// Init Express server
const server = express()

View File

@@ -0,0 +1,157 @@
export interface Root<T = Result> {
success: boolean
errors: any[]
results: T[]
}
export interface Result {
productId: number
lowPrice: number
midPrice: number
highPrice: number
marketPrice?: number
directLowPrice?: number
subTypeName: 'Normal' | 'Reverse Holofoil' | 'Holofoil'
}
interface BearerResponse {
access_token: string
token_type: 'bearer'
expires_in: number
userName: string
'.issues': string
'.expires': string
}
export default class TCGPlayer {
private bearer: BearerResponse & { expires: Date } | undefined = undefined
public category = {
listConditions: async (categoryId: number) => {
return await this.fetch<{
conditionId: number
name: string
abbreviation: string
displayOrder: number
}>(`/catalog/categories/${categoryId}/conditions`)
},
listLanguages: async (categoryId: number) => {
return await this.fetch<{
languageId: number
name: string
abbr: string
}>(`/catalog/categories/${categoryId}/languages`)
},
listPrintings: async (categoryId: number) => {
return await this.fetch<{
printingId: number
name: string
displayOrder: number
modifiedOn: Date
}>(`/catalog/categories/${categoryId}/printings`)
}
}
public product = {
listSKUs: async (productId: number) => {
return await this.fetch<{
skuId: number
productId: number
languageId: number
printingId: number
conditionId: number
}>(`/catalog/products/${productId}/skus`)
}
}
public price = {
groupProduct: async (product: number) => {
return await this.fetch<{
"productId": number
"lowPrice": number
"midPrice": number
"highPrice": number
"marketPrice": number
"directLowPrice": number
"subTypeName": string
}>(`/pricing/product/${product}`)
},
listForProducts: async (...productIds: Array<number>) => {
return await this.fetch<{
"productId": number
"lowPrice": number
"midPrice": number
"highPrice": number
"marketPrice": number
"directLowPrice": number
"subTypeName": string
}>(`/pricing/product/${productIds.join(',')}`)
},
listForSKUs: async (...SKUIds: Array<number>) => {
return await this.fetch<{
"skuId": number,
"lowPrice": number,
"lowestShipping": number,
"lowestListingPrice": number,
"marketPrice": number,
"directLowPrice": number
}>(`/pricing/sku/${SKUIds.join(',')}`)
}
}
private async getToken() {
if (typeof this.bearer === 'undefined' || this.bearer.expires < new Date()) {
const now = new Date()
const res = await fetch('https://api.tcgplayer.com/token', {
method: 'POST',
headers: {
'User-Agent': 'TCG-Collection'
},
body: new URLSearchParams({
'grant_type': 'client_credentials',
'client_id': process.env.TCGPLAYER_CLIENT_ID!,
'client_secret': process.env.TCGPLAYER_CLIENT_SECRET!
})
})
if (res.status >= 400) {
console.error(await res.text())
throw new Error('error connecting to TCGPlayer')
}
const json = await res.json()
this.bearer = json
now.setTime(now.getTime() + this.bearer!.expires_in)
this.bearer!.expires = now
}
return this.bearer?.access_token
}
private async fetch<T = Result>(path: string): Promise<Root<T>> {
const res = await fetch('https://api.tcgplayer.com' + path, {
headers: {
'accept': 'application/json',
'User-Agent': process.env.TCGPLAYER_CLIENT_NAME!,
'Authorization': `bearer ${await this.getToken()}`
}
})
const text = await res.text()
let json: any
try {
json = JSON.parse(text)
} catch (e) {
throw new Error(text)
}
if (res.status >= 400) {
console.error('error getting', path)
if (json.message) {
throw new Error(json.message)
}
throw new Error(JSON.stringify(json))
}
return json
// .then((res) => res.json() as Promise<Root<T>>)
}
}

View File

@@ -1,5 +1,6 @@
import * as OfficialTCGPlayer from './official'
import * as Fallback from './fallback'
import type RFC7807 from '../../RFCs/RFC7807'
let source: typeof OfficialTCGPlayer = Fallback
if (
@@ -21,3 +22,15 @@ export async function updateTCGPlayerDatas(): Promise<boolean> {
export async function getTCGPlayerPrice(card: { thirdParty: { tcgplayer?: number } }): Promise<any> {
return source.getTCGPlayerPrice(card)
}
export async function listSKUs(card: { thirdParty: { tcgplayer?: number }}): Promise<any> {
if ('listSKUs' in source) {
return (source as any).listSKUs(card)
}
return {
type: '/errors/provider-error',
title: 'The current provider does not provide this feature',
status: 503,
details: 'The TCGPlayer provider does not provide the feature to list SKUs, please contact you administrator to get more informations.'
} satisfies RFC7807
}

View File

@@ -1,61 +1,16 @@
import { objectOmit } from '@dzeio/object-util'
import { sets } from '../../../V2/Components/Set'
import TCGPlayer from './TCGPlayer'
import list from './product-skus.mapping.json'
export interface Root {
success: boolean
errors: any[]
results: Result[]
}
const tcgplayer = new TCGPlayer()
export interface Result {
productId: number
lowPrice: number
midPrice: number
highPrice: number
marketPrice?: number
directLowPrice?: number
subTypeName: 'Normal' | 'Reverse Holofoil' | 'Holofoil'
}
interface BearerResponse {
access_token: string
token_type: 'bearer'
expires_in: number
userName: string
'.issues': string
'.expires': string
}
let bearer: BearerResponse & { expires: Date } | undefined = undefined
async function getToken() {
if (typeof bearer === 'undefined' || bearer.expires < new Date()) {
const now = new Date()
const res = await fetch('https://api.tcgplayer.com/token', {
method: 'POST',
headers: {
'User-Agent': 'TCG-Collection'
},
body: new URLSearchParams({
'grant_type': 'client_credentials',
'client_id': process.env.TCGPLAYER_CLIENT_ID!,
'client_secret': process.env.TCGPLAYER_CLIENT_SECRET!
})
}).then((it) => it.json())
bearer = res
now.setTime(now.getTime() + bearer!.expires_in)
bearer!.expires = now
}
return bearer?.access_token
}
type Result = Awaited<ReturnType<typeof TCGPlayer['prototype']['price']['groupProduct']>>
let cache: Record<number, Record<string, Result>> = {}
let lastFetch: Date | undefined = undefined
export async function updateTCGPlayerDatas(): Promise<boolean> {
const token = await getToken()
// only fetch at max, once an hour
if (lastFetch && lastFetch.getTime() > new Date().getTime() - 3600000) {
@@ -67,14 +22,16 @@ export async function updateTCGPlayerDatas(): Promise<boolean> {
.map((it) => it!.thirdParty!.tcgplayer)
for (const product of products) {
const data = await fetch(`https://api.tcgplayer.com/pricing/group/${product}`, {
headers: {
'accept': 'application/json',
'User-Agent': process.env.TCGPLAYER_CLIENT_NAME!,
'Authorization': `bearer ${token}`
}
})
.then((res) => res.json() as Promise<Root>)
if (!product) {
continue
}
let data: Awaited<ReturnType<typeof TCGPlayer['prototype']['price']['groupProduct']>>
try {
data = await tcgplayer.price.groupProduct(product)
} catch {
continue
}
for (const item of data.results) {
const cacheItem = cache[item.productId] ?? {}
@@ -117,3 +74,15 @@ export async function getTCGPlayerPrice(card: { thirdParty: { tcgplayer?: number
}
return res
}
export async function listSKUs(card: { thirdParty: { tcgplayer?: number }}): Promise<any> {
if (!card.thirdParty.tcgplayer) {
return null
}
const skus: any = list[card.thirdParty.tcgplayer]
const res = await tcgplayer.price.listForSKUs(skus.map((it) => it.sku))
return res.results.map((it) => ({
...objectOmit(it, 'skuId'),
...skus.find((sku) => sku.sku === it.skuId)
}))
}

File diff suppressed because it is too large Load Diff