Skip to content

Commit 694b5bb

Browse files
committed
Merge branch 'dev'
2 parents 5b44d80 + 7b1010b commit 694b5bb

3 files changed

Lines changed: 285 additions & 3 deletions

File tree

api/scrapers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .news import vlr_news
22
from .rankings import vlr_rankings
33
from .stats import vlr_stats
4-
from .matches import vlr_upcoming_matches, vlr_live_score, vlr_match_results
4+
from .matches import vlr_upcoming_matches, vlr_live_score, vlr_match_results, vlr_upcoming_matches_extended
55
from .events import vlr_events
66
from .health import check_health

api/scrapers/matches.py

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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"\nScraping 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+
198475
def 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.

routers/vlr_router.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
vlr_rankings,
1111
vlr_stats,
1212
vlr_upcoming_matches,
13+
vlr_upcoming_matches_extended,
1314
check_health
1415
)
1516

@@ -86,10 +87,11 @@ async def VLR_match(
8687
):
8788
"""
8889
query parameters:\n
89-
"upcoming": upcoming matches,\n
90+
"upcoming": upcoming matches (from homepage),\n
91+
"upcoming_extended": upcoming matches (from paginated /matches page),\n
9092
"live_score": live match scores,\n
9193
"results": match results,\n
92-
94+
9395
Page Range Options:
9496
- num_pages: Number of pages from page 1 (ignored if from_page/to_page specified)
9597
- from_page: Starting page number (1-based, optional)
@@ -101,12 +103,15 @@ async def VLR_match(
101103
- timeout: Request timeout in seconds (10-120, default: 30)
102104
103105
Examples:
106+
- /match?q=upcoming_extended&num_pages=3 (scrapes pages 1-3 of upcoming matches)
104107
- /match?q=results&num_pages=5 (scrapes pages 1-5)
105108
- /match?q=results&from_page=10&to_page=15 (scrapes pages 10-15)
106109
- /match?q=results&from_page=5&num_pages=3 (scrapes pages 5-7)
107110
"""
108111
if q == "upcoming":
109112
return vlr_upcoming_matches(num_pages, from_page, to_page)
113+
elif q == "upcoming_extended":
114+
return vlr_upcoming_matches_extended(num_pages, from_page, to_page, max_retries, request_delay, timeout)
110115
elif q == "live_score":
111116
return vlr_live_score(num_pages, from_page, to_page)
112117
elif q == "results":

0 commit comments

Comments
 (0)