@@ -195,6 +195,283 @@ def vlr_live_score(num_pages=1, from_page=None, to_page=None):
195195 return data
196196
197197
198+ def vlr_upcoming_matches_extended (num_pages = 1 , from_page = None , to_page = None , max_retries = 3 , request_delay = 1.0 , timeout = 30 ):
199+ """
200+ Scrape upcoming matches from the paginated matches page with robust error handling.
201+
202+ Args:
203+ num_pages (int): Number of pages to scrape from page 1 (ignored if from_page/to_page specified)
204+ from_page (int, optional): Starting page number (1-based)
205+ to_page (int, optional): Ending page number (1-based, inclusive)
206+ max_retries (int): Maximum retry attempts per page
207+ request_delay (float): Delay between requests in seconds
208+ timeout (int): Request timeout in seconds
209+
210+ Returns:
211+ dict: API response with match data
212+ """
213+
214+ result = []
215+ status = 200
216+ failed_pages = []
217+
218+ # Determine page range
219+ if from_page is not None and to_page is not None :
220+ if from_page < 1 :
221+ raise ValueError ("from_page must be >= 1" )
222+ if to_page < from_page :
223+ raise ValueError ("to_page must be >= from_page" )
224+ start_page = from_page
225+ end_page = to_page
226+ total_pages = end_page - start_page + 1
227+ elif from_page is not None :
228+ if from_page < 1 :
229+ raise ValueError ("from_page must be >= 1" )
230+ start_page = from_page
231+ end_page = from_page + num_pages - 1
232+ total_pages = num_pages
233+ elif to_page is not None :
234+ if to_page < 1 :
235+ raise ValueError ("to_page must be >= 1" )
236+ start_page = max (1 , to_page - num_pages + 1 )
237+ end_page = to_page
238+ total_pages = end_page - start_page + 1
239+ else :
240+ # Default behavior: scrape from page 1
241+ start_page = 1
242+ end_page = num_pages
243+ total_pages = num_pages
244+
245+ # Create a session for connection pooling and efficiency
246+ session = requests .Session ()
247+ session .headers .update (headers )
248+
249+ print (f"Starting to scrape pages { start_page } -{ end_page } ({ total_pages } pages) with { request_delay } s delay between requests..." )
250+
251+ for page in range (start_page , end_page + 1 ):
252+ page_success = False
253+ retry_count = 0
254+
255+ while not page_success and retry_count < max_retries :
256+ try :
257+ if page == 1 :
258+ url = "https://www.vlr.gg/matches"
259+ else :
260+ url = f"https://www.vlr.gg/matches/?page={ page } "
261+
262+ current_page_num = page - start_page + 1
263+ print (f"Scraping page { page } ({ current_page_num } /{ total_pages } ) (attempt { retry_count + 1 } /{ max_retries } )" )
264+
265+ # Add timeout and handle potential connection issues
266+ resp = session .get (url , timeout = timeout )
267+ html = HTMLParser (resp .text )
268+ current_status = resp .status_code
269+
270+ if current_status != 200 :
271+ print (f"Warning: Page { page } returned status { current_status } " )
272+ retry_count += 1
273+ if retry_count < max_retries :
274+ time .sleep (request_delay * (2 ** retry_count )) # Exponential backoff
275+ continue
276+
277+ page_results = []
278+ items = html .css ("a.wf-module-item" )
279+
280+ if not items :
281+ print (f"Warning: No match items found on page { page } " )
282+ page_success = True # Consider empty page as success
283+ break
284+
285+ for item in items :
286+ try :
287+ # Skip completed matches - only get upcoming/live matches
288+ eta_element = item .css_first (".ml-eta" )
289+ if eta_element and "ago" in eta_element .text ():
290+ continue
291+
292+ href = item .attributes .get ("href" , "" )
293+ url_path = "https://www.vlr.gg" + href if href else ""
294+
295+ # Get match status/eta
296+ eta = item .css_first (".ml-status" ).text ().strip () if item .css_first (".ml-status" ) else ""
297+
298+ # If no ml-status, check for time until match
299+ if not eta :
300+ eta_elem = item .css_first (".ml-eta" )
301+ if eta_elem :
302+ eta_text = eta_elem .text ().strip ()
303+ if eta_text and "ago" not in eta_text :
304+ eta = eta_text
305+
306+ # Get teams
307+ teams = []
308+ flags = []
309+ scores = []
310+
311+ team_divs = item .css (".match-item-vs-team" )
312+ for team_div in team_divs :
313+ team_name_elem = team_div .css_first (".match-item-vs-team-name" )
314+ if team_name_elem :
315+ teams .append (team_name_elem .text ().strip ())
316+ else :
317+ teams .append ("TBD" )
318+
319+ # Get flag
320+ flag_elem = team_div .css_first (".flag" )
321+ if flag_elem :
322+ flag_class = flag_elem .attributes .get ("class" )
323+ if flag_class :
324+ flag = flag_class .replace ("flag " , "" ).replace (" mod-" , "_" )
325+ flags .append (flag )
326+ else :
327+ flags .append ("" )
328+ else :
329+ flags .append ("" )
330+
331+ # Get score (usually 0 for upcoming)
332+ score_elem = team_div .css_first (".match-item-vs-team-score" )
333+ if score_elem :
334+ scores .append (score_elem .text ().strip ())
335+ else :
336+ scores .append ("" )
337+
338+ # Handle case where teams list might be incomplete
339+ while len (teams ) < 2 :
340+ teams .append ("TBD" )
341+ while len (flags ) < 2 :
342+ flags .append ("" )
343+ while len (scores ) < 2 :
344+ scores .append ("" )
345+
346+ # Get match event and series info
347+ match_event_elem = item .css_first (".match-item-event-series" )
348+ match_series = ""
349+
350+ if match_event_elem :
351+ event_text = match_event_elem .text ().replace ("\n " , "" ).replace ("\t " , "" ).strip ()
352+ # Try to split event and series
353+ parts = event_text .split ()
354+ if parts :
355+ match_series = " " .join (parts )
356+
357+ # Get tournament name
358+ tourney_elem = item .css_first (".match-item-event" )
359+ tourney = ""
360+ if tourney_elem :
361+ tourney_lines = [line .strip () for line in tourney_elem .text ().split ("\n " ) if line .strip ()]
362+ tourney = tourney_lines [- 1 ] if tourney_lines else ""
363+
364+ # Get tournament icon
365+ tourney_icon_elem = item .css_first (".match-item-icon img" )
366+ tourney_icon_url = ""
367+ if tourney_icon_elem :
368+ icon_src = tourney_icon_elem .attributes .get ("src" , "" )
369+ if icon_src :
370+ tourney_icon_url = f"https:{ icon_src } " if icon_src .startswith ("//" ) else icon_src
371+
372+ # Get timestamp if available
373+ timestamp_elem = item .css_first (".moment-tz-convert" )
374+ timestamp = ""
375+ if timestamp_elem :
376+ unix_ts = timestamp_elem .attributes .get ("data-utc-ts" )
377+ if unix_ts :
378+ timestamp = datetime .fromtimestamp (
379+ int (unix_ts ),
380+ tz = timezone .utc ,
381+ ).strftime ("%Y-%m-%d %H:%M:%S" )
382+
383+ page_results .append (
384+ {
385+ "team1" : teams [0 ],
386+ "team2" : teams [1 ],
387+ "flag1" : flags [0 ],
388+ "flag2" : flags [1 ],
389+ "score1" : scores [0 ],
390+ "score2" : scores [1 ],
391+ "time_until_match" : eta ,
392+ "match_series" : match_series ,
393+ "match_event" : tourney ,
394+ "unix_timestamp" : timestamp ,
395+ "match_page" : url_path ,
396+ "tournament_icon" : tourney_icon_url ,
397+ "page_number" : page , # Track which page this came from
398+ }
399+ )
400+ except Exception as e :
401+ print (f"Warning: Failed to parse match item on page { page } : { str (e )} " )
402+ continue
403+
404+ result .extend (page_results )
405+ print (f"Successfully scraped page { page } : { len (page_results )} matches" )
406+ page_success = True
407+
408+ # Rate limiting between successful requests
409+ if page < end_page :
410+ time .sleep (request_delay )
411+
412+ except requests .exceptions .Timeout :
413+ retry_count += 1
414+ print (f"Timeout error on page { page } , attempt { retry_count } /{ max_retries } " )
415+ if retry_count < max_retries :
416+ backoff_time = request_delay * (2 ** retry_count )
417+ print (f"Retrying page { page } in { backoff_time :.1f} seconds..." )
418+ time .sleep (backoff_time )
419+
420+ except requests .exceptions .ConnectionError :
421+ retry_count += 1
422+ print (f"Connection error on page { page } , attempt { retry_count } /{ max_retries } " )
423+ if retry_count < max_retries :
424+ backoff_time = request_delay * (2 ** retry_count )
425+ print (f"Retrying page { page } in { backoff_time :.1f} seconds..." )
426+ time .sleep (backoff_time )
427+
428+ except Exception as e :
429+ retry_count += 1
430+ print (f"Unexpected error on page { page } : { str (e )} " )
431+ if retry_count < max_retries :
432+ backoff_time = request_delay * (2 ** retry_count )
433+ print (f"Retrying page { page } in { backoff_time :.1f} seconds..." )
434+ time .sleep (backoff_time )
435+
436+ if not page_success :
437+ failed_pages .append (page )
438+ print (f"Failed to scrape page { page } after { max_retries } attempts" )
439+
440+ # Close the session
441+ session .close ()
442+
443+ # Report results
444+ total_matches = len (result )
445+ successful_pages = total_pages - len (failed_pages )
446+
447+ print (f"\n Scraping completed:" )
448+ print (f" Page range: { start_page } -{ end_page } " )
449+ print (f" Total matches: { total_matches } " )
450+ print (f" Successful pages: { successful_pages } /{ total_pages } " )
451+
452+ if failed_pages :
453+ print (f" Failed pages: { failed_pages } " )
454+ print (f" Consider retrying failed pages or adjusting parameters" )
455+
456+ segments = {
457+ "status" : status ,
458+ "segments" : result ,
459+ "meta" : {
460+ "page_range" : f"{ start_page } -{ end_page } " ,
461+ "total_pages_requested" : total_pages ,
462+ "successful_pages" : successful_pages ,
463+ "failed_pages" : failed_pages ,
464+ "total_matches" : total_matches
465+ }
466+ }
467+ data = {"data" : segments }
468+
469+ if not result :
470+ raise Exception (f"No data retrieved. Failed pages: { failed_pages } " )
471+
472+ return data
473+
474+
198475def vlr_match_results (num_pages = 1 , from_page = None , to_page = None , max_retries = 3 , request_delay = 1.0 , timeout = 30 ):
199476 """
200477 Scrape match results with robust error handling for large page counts.
0 commit comments