Source code for vlrdevapi.players.matches

"""Player matches functionality."""

from __future__ import annotations

import datetime
from bs4 import BeautifulSoup
from bs4.element import Tag

from .models import Match, MatchTeam
from ..config import get_config
from ..fetcher import batch_fetch_html
from ..utils import absolute_url, extract_text, normalize_whitespace

_config = get_config()


[docs] def matches( player_id: int, limit: int | None = None, page: int | None = None, timeout: float | None = None, ) -> list[Match]: """ Get player match history with batch fetching for pagination. Args: player_id: Player ID limit: Maximum number of matches to return page: Page number (1-indexed) timeout: Request timeout in seconds Returns: List of player matches. Each match includes: - stage: The tournament stage (e.g., "Group Stage", "Playoffs") - phase: The specific phase within the stage (e.g., "W1", "GF") Example: >>> import vlrdevapi as vlr >>> matches = vlr.players.matches(player_id=123, limit=10) >>> for match in matches: ... print(f"{match.event} - {match.stage} {match.phase}: {match.result}") """ start_page = page or 1 results: list[Match] = [] remaining: int | None if limit is None: remaining = None else: remaining = max(0, min(1000, limit)) if remaining == 0: return [] single_page_only = limit is None and page is not None current_page = start_page pages_fetched = 0 MAX_PAGES = 25 BATCH_SIZE = 3 # Fetch 3 pages at a time while pages_fetched < MAX_PAGES: # Determine how many pages to fetch in this batch pages_to_fetch = min(BATCH_SIZE, MAX_PAGES - pages_fetched) if single_page_only: pages_to_fetch = 1 # Build URLs for batch fetching urls: list[str] = [] for i in range(pages_to_fetch): page_num = current_page + i suffix = f"?page={page_num}" if page_num > 1 else "" url = f"{_config.vlr_base}/player/matches/{player_id}{suffix}" urls.append(url) # Batch fetch all pages concurrently effective_timeout = timeout if timeout is not None else _config.default_timeout batch_results = batch_fetch_html(urls, timeout=effective_timeout, max_workers=min(3, len(urls))) # Process each page in order for url in urls: html = batch_results.get(url) if isinstance(html, Exception) or not html: # Stop if we hit an error pages_fetched = MAX_PAGES break soup = BeautifulSoup(html, "lxml") page_matches: list[Match] = [] for anchor in soup.select("a.wf-card.fc-flex.m-item"): href_val = anchor.get("href") href = href_val if isinstance(href_val, str) else None if not href: continue parts = href.strip("/").split("/") if not parts or not parts[0].isdigit(): continue match_id = int(parts[0]) match_url = absolute_url(href) or "" # Parse event info event_el = anchor.select_one(".m-item-event") event_name = None stage = None phase = None if event_el: strings = list(event_el.stripped_strings) if strings: event_name = normalize_whitespace(strings[0]) if strings[0] else None details = [s.strip("⋅ ") for s in strings[1:] if s.strip("⋅ ")] if details: # Join all details and split on ⋅ separator combined = " ".join(details) if "⋅" in combined: parts = [normalize_whitespace(p) for p in combined.split("⋅") if p.strip()] if len(parts) >= 2: stage = parts[0] phase = parts[1] elif len(parts) == 1: stage = parts[0] else: # No separator, treat as stage only stage = normalize_whitespace(combined) # Parse teams team_blocks = anchor.select(".m-item-team") player_block = team_blocks[0] if team_blocks else None opponent_block = team_blocks[-1] if len(team_blocks) > 1 else None def parse_team_block(block: Tag | None) -> MatchTeam: if not block: return MatchTeam(name=None, tag=None, core=None) name = extract_text(block.select_one(".m-item-team-name")) tag = extract_text(block.select_one(".m-item-team-tag")) core = extract_text(block.select_one(".m-item-team-core")) return MatchTeam(name=name or None, tag=tag or None, core=core or None) player_team = parse_team_block(player_block) opponent_team = parse_team_block(opponent_block) # Parse result and scores result_el = anchor.select_one(".m-item-result") player_score: int | None = None opponent_score: int | None = None result = None if result_el: spans: list[str] = [span.get_text(strip=True) for span in result_el.select("span")] scores: list[int] = [] for value in spans: try: scores.append(int(value)) except ValueError: continue if len(scores) >= 2: player_score, opponent_score = scores[0], scores[1] elif len(scores) == 1: player_score = scores[0] classes_val = result_el.get("class") classes: list[str] = list(classes_val) if isinstance(classes_val, (list, tuple)) else [] if any("mod-win" == cls or cls.endswith("mod-win") for cls in classes): result = "win" elif any("mod-loss" == cls or cls.endswith("mod-loss") for cls in classes): result = "loss" elif any("mod-draw" == cls or cls.endswith("mod-draw") for cls in classes): result = "draw" # Parse date/time date_el = anchor.select_one(".m-item-date") match_date = None match_time = None time_text = None if date_el: parts_list = list(date_el.stripped_strings) if parts_list: date_text = parts_list[0] try: match_date = datetime.datetime.strptime(date_text, "%Y/%m/%d").date() except ValueError: pass if len(parts_list) > 1: time_text = parts_list[1] try: match_time = datetime.datetime.strptime(time_text, "%I:%M %p").time() except ValueError: pass page_matches.append(Match( match_id=match_id, url=match_url, event=event_name, stage=stage, phase=phase, player_team=player_team, opponent_team=opponent_team, player_score=player_score, opponent_score=opponent_score, result=result, date=match_date, time=match_time, time_text=time_text, )) if not page_matches: # No more matches on this page, stop fetching pages_fetched = MAX_PAGES break if remaining is None: results.extend(page_matches) else: take = page_matches[:remaining] results.extend(take) remaining -= len(take) pages_fetched += 1 if single_page_only: pages_fetched = MAX_PAGES break if remaining is not None and remaining <= 0: pages_fetched = MAX_PAGES break current_page += pages_to_fetch return results