11import { NextApiRequest , NextApiResponse } from 'next' ;
2- import { chromium } from 'playwright' ;
32import clientPromise from '@/utils/mongo' ;
43import { getSportsUrls } from '@/utils/sportsUrls' ;
54
@@ -9,132 +8,144 @@ const DEFAULT_HEADERS = {
98 accept : 'application/json, text/plain, */*' ,
109 'accept-language' : 'en-US,en;q=0.9' ,
1110 referer : 'https://www.sportscardspro.com/' ,
11+ 'user-agent' : USER_AGENT ,
1212} ;
1313const REQUEST_TIMEOUT_MS = 45000 ;
1414
15- const parseJsonSafe = ( payload : string ) => {
15+ const normalizeCardSet = ( value : unknown ) => {
16+ if ( typeof value !== 'string' ) {
17+ return '' ;
18+ }
19+ return value . trim ( ) ;
20+ } ;
21+
22+ const parseJsonSafe = ( payload : string ) => {
1623 try {
17- return JSON . parse ( payload ) ;
18- } catch ( error ) {
24+ return JSON . parse ( payload ) ;
25+ } catch ( error ) {
1926 return null ;
2027 }
2128} ;
2229
23- const fetchWithBrowser = async ( urls : string [ ] ) => {
24- const browser = await chromium . launch ( {
25- headless : true ,
26- args : [ '--disable-blink-features=AutomationControlled' ] ,
27- } ) ;
28-
29- const context = await browser . newContext ( {
30- userAgent : USER_AGENT ,
31- locale : 'en-US' ,
32- extraHTTPHeaders : DEFAULT_HEADERS ,
33- } ) ;
30+ const fetchWithTimeout = async ( url : string ) => {
31+ const controller = new AbortController ( ) ;
32+ const timer = setTimeout ( ( ) => controller . abort ( ) , REQUEST_TIMEOUT_MS ) ;
3433
35- await context . addInitScript ( ( ) => {
36- Object . defineProperty ( navigator , 'webdriver' , { get : ( ) => undefined } ) ;
37- } ) ;
34+ try {
35+ return await fetch ( url , {
36+ headers : DEFAULT_HEADERS ,
37+ signal : controller . signal ,
38+ } ) ;
39+ } finally {
40+ clearTimeout ( timer ) ;
41+ }
42+ } ;
3843
39- const page = await context . newPage ( ) ;
44+ const fetchFromServer = async ( urls : string [ ] ) => {
4045 const results : unknown [ ] = [ ] ;
4146
42- for ( const url of urls ) {
47+ for ( const url of urls ) {
4348 try {
44- const response = await page . goto ( url , {
45- waitUntil : 'domcontentloaded' ,
46- timeout : REQUEST_TIMEOUT_MS ,
47- } ) ;
49+ const response = await fetchWithTimeout ( url ) ;
4850
49- if ( ! response ) {
50- throw new Error ( 'No response received' ) ;
51+ if ( ! response . ok ) {
52+ throw new Error ( `Status ${ response . status } ` ) ;
5153 }
5254
53- if ( ! response . ok ( ) ) {
54- throw new Error ( `Status ${ response . status ( ) } ` ) ;
55- }
56-
57- const contentType = response . headers ( ) [ 'content-type' ] || '' ;
55+ const contentType = response . headers . get ( 'content-type' ) || '' ;
5856 let data : unknown = null ;
5957
60- if ( contentType . includes ( 'application/json' ) ) {
58+ if ( contentType . includes ( 'application/json' ) ) {
6159 data = await response . json ( ) ;
6260 } else {
6361 const bodyText = await response . text ( ) ;
64- data = parseJsonSafe ( bodyText ) ;
62+ data = parseJsonSafe ( bodyText ) ;
6563 }
6664
67- if ( ! data ) {
68- throw new Error ( 'Invalid JSON payload' ) ;
65+ if ( ! data ) {
66+ throw new Error ( 'Invalid JSON payload' ) ;
6967 }
7068
71- results . push ( data ) ;
72- } catch ( error ) {
73- console . error ( `Error fetching sports data from ${ url } :` , error ) ;
69+ results . push ( data ) ;
70+ } catch ( error ) {
71+ console . error ( `Error fetching sports data from ${ url } :` , error ) ;
7472 }
7573 }
7674
77- await page . close ( ) ;
78- await context . close ( ) ;
79- await browser . close ( ) ;
80-
8175 return results ;
8276} ;
8377
84- export default async function handler ( req : NextApiRequest , res : NextApiResponse ) {
85- if ( req . method !== 'GET' ) {
86- res . setHeader ( 'Allow' , [ 'GET' ] ) ;
87- return res . status ( 405 ) . json ( { error : 'Method Not Allowed' } ) ;
88- }
78+ export default async function handler ( req : NextApiRequest , res : NextApiResponse ) {
79+ res . setHeader ( 'Cache-Control' , 'no-store' ) ;
80+
81+ const method = req . method ?? 'GET' ;
8982
90- res . setHeader ( 'Cache-Control' , 'no-store' ) ;
83+ if ( method !== 'GET' && method !== 'POST' ) {
84+ res . setHeader ( 'Allow' , [ 'GET' , 'POST' ] ) ;
85+ return res . status ( 405 ) . json ( { error : 'Method Not Allowed' } ) ;
86+ }
9187
9288 try {
93- const { cardSet } = req . query ;
89+ const cardSet = normalizeCardSet ( method === 'POST'
90+ ? req . body ?. cardSet ?? req . query . cardSet
91+ : req . query . cardSet ) ;
9492
95- if ( ! cardSet || typeof cardSet !== 'string' ) {
96- return res . status ( 400 ) . json ( { error : 'Card set is required' } ) ;
93+ if ( ! cardSet ) {
94+ return res . status ( 400 ) . json ( { error : 'Card set is required' } ) ;
9795 }
9896
99- const urls = getSportsUrls ( cardSet ) ;
100-
101- if ( ! urls || urls . length === 0 ) {
102- return res . status ( 404 ) . json ( { error : `No data found for card set: ${ cardSet } ` } ) ;
97+ const urls = getSportsUrls ( cardSet ) ;
98+ if ( ! urls || urls . length === 0 ) {
99+ return res . status ( 404 ) . json ( { error : `No data found for card set: ${ cardSet } ` } ) ;
103100 }
104101
105- const results = await fetchWithBrowser ( urls ) ;
106- const validData = results . filter ( ( result ) => result !== null ) ;
107-
108102 const client = await clientPromise ;
109- const collection = client . db ( 'cardPriceApp' ) . collection ( 'sportsDataCache' ) ;
110- const fetchedAt = new Date ( ) ;
103+ const collection = client . db ( 'cardPriceApp' ) . collection ( 'sportsDataCache' ) ;
111104
112- if ( validData . length > 0 ) {
113- await collection . updateOne (
114- { cardSet } ,
115- {
116- $set : {
117- cardSet,
118- data : validData ,
119- fetchedAt,
120- sourceUrls : urls ,
121- pageCount : validData . length ,
122- } ,
123- } ,
124- { upsert : true }
125- ) ;
105+ if ( method === 'GET' ) {
106+ const cached = await collection . findOne ( { cardSet } ) ;
107+ if ( cached ?. data ?. length ) {
108+ return res . status ( 200 ) . json ( cached . data ) ;
109+ }
110+
111+ return res . status ( 404 ) . json ( { error : 'No cached data found' } ) ;
112+ }
113+
114+ const payload = Array . isArray ( req . body ?. data ) ? req . body . data . filter ( Boolean ) : [ ] ;
115+ let dataToStore = payload ;
126116
127- return res . status ( 200 ) . json ( validData ) ;
117+ if ( dataToStore . length === 0 ) {
118+ const serverData = await fetchFromServer ( urls ) ;
119+ dataToStore = serverData . filter ( Boolean ) ;
128120 }
129121
130- const cached = await collection . findOne ( { cardSet } ) ;
131- if ( cached ?. data ?. length ) {
132- return res . status ( 200 ) . json ( cached . data ) ;
122+ if ( dataToStore . length === 0 ) {
123+ const cached = await collection . findOne ( { cardSet } ) ;
124+ if ( cached ?. data ?. length ) {
125+ return res . status ( 200 ) . json ( cached . data ) ;
126+ }
127+
128+ return res . status ( 502 ) . json ( { error : 'No valid data found' } ) ;
133129 }
134130
135- return res . status ( 502 ) . json ( { error : 'No valid data found' } ) ;
136- } catch ( error ) {
137- console . error ( 'Error fetching sports data:' , error ) ;
138- return res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
131+ const fetchedAt = new Date ( ) ;
132+ await collection . updateOne (
133+ { cardSet } ,
134+ {
135+ $set : {
136+ cardSet,
137+ data : dataToStore ,
138+ fetchedAt,
139+ sourceUrls : urls ,
140+ pageCount : dataToStore . length ,
141+ } ,
142+ } ,
143+ { upsert : true }
144+ ) ;
145+
146+ return res . status ( 200 ) . json ( dataToStore ) ;
147+ } catch ( error ) {
148+ console . error ( 'Error handling sports data:' , error ) ;
149+ return res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
139150 }
140151}
0 commit comments