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:
@@ -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
|
||||
*
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
157
server/src/libs/providers/tcgplayer/TCGPlayer.ts
Normal file
157
server/src/libs/providers/tcgplayer/TCGPlayer.ts
Normal 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>>)
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}))
|
||||
}
|
||||
|
871238
server/src/libs/providers/tcgplayer/product-skus.mapping.json
Normal file
871238
server/src/libs/providers/tcgplayer/product-skus.mapping.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user