wuhp commited on
Commit
eacf2b8
·
verified ·
1 Parent(s): 564d5bf

Delete tracker_client.py

Browse files
Files changed (1) hide show
  1. tracker_client.py +0 -238
tracker_client.py DELETED
@@ -1,238 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- from dataclasses import dataclass
5
- from datetime import datetime, timezone
6
- from typing import Any, Dict, List, Optional
7
-
8
- import requests
9
-
10
- BASE_WEB = "https://api.tracker.gg"
11
- PROFILE_WEB = "https://rocketleague.tracker.network/rocket-league/profile"
12
-
13
-
14
- class TrackerError(RuntimeError):
15
- pass
16
-
17
-
18
- def _dt(value: Optional[str]) -> Optional[datetime]:
19
- if not value:
20
- return None
21
- try:
22
- d = datetime.fromisoformat(value.replace("Z", "+00:00"))
23
- if d.tzinfo is None:
24
- d = d.replace(tzinfo=timezone.utc)
25
- return d.astimezone(timezone.utc)
26
- except ValueError:
27
- return None
28
-
29
-
30
- def _stat_value(stats: Dict[str, Any], name: str, default=None):
31
- return (stats.get(name) or {}).get("value", default)
32
-
33
-
34
- @dataclass
35
- class PlayerProfile:
36
- platform: str
37
- identifier: str
38
- handle: str
39
- player_id: Optional[int]
40
- last_updated: Optional[datetime]
41
- season: Optional[int]
42
- avatar_url: Optional[str]
43
- lifetime: Dict[str, Any]
44
- playlists: List[Dict[str, Any]]
45
- raw: Dict[str, Any]
46
-
47
-
48
- class TrackerClient:
49
- """Best-effort client for Tracker web endpoints observed in supplied samples.
50
-
51
- This intentionally does not solve/bypass Cloudflare or Warden challenges. A 401/403
52
- is returned to the bot as an error instead.
53
- """
54
-
55
- def __init__(self, timeout: int = 20):
56
- self.session = requests.Session()
57
- self.timeout = timeout
58
- self.session.headers.update(
59
- {
60
- "Accept": "application/json, text/plain, */*",
61
- "Accept-Language": "en-US,en;q=0.9",
62
- "Origin": "https://rocketleague.tracker.network",
63
- "Referer": "https://rocketleague.tracker.network/",
64
- "User-Agent": os.getenv(
65
- "TRACKER_USER_AGENT",
66
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
67
- "(KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36",
68
- ),
69
- }
70
- )
71
- cookie = os.getenv("TRACKER_COOKIE", "").strip()
72
- if cookie:
73
- self.session.headers["Cookie"] = cookie
74
-
75
- def _prime_profile_session(self, platform: str, identifier: str) -> None:
76
- """Visit the normal public Tracker profile page first so browser cookies/state exist.
77
-
78
- This does not solve CAPTCHA/WAF challenges. It only mirrors the normal browser flow
79
- of opening a profile page before its JSON requests are made.
80
- """
81
- page_url = f"{PROFILE_WEB}/{platform}/{identifier}"
82
- headers = {
83
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
84
- "Sec-Fetch-Dest": "document",
85
- "Sec-Fetch-Mode": "navigate",
86
- "Sec-Fetch-Site": "none",
87
- "Upgrade-Insecure-Requests": "1",
88
- }
89
- try:
90
- self.session.get(page_url, headers=headers, timeout=self.timeout, allow_redirects=True)
91
- except requests.RequestException:
92
- # The API request below is still worth attempting; it will produce the useful error.
93
- pass
94
-
95
- @staticmethod
96
- def _extract_profile_from_path(path: str) -> tuple[Optional[str], Optional[str]]:
97
- marker = "/api/v2/rocket-league/standard/profile/"
98
- if not path.startswith(marker):
99
- return None, None
100
- rest = path[len(marker):].strip("/")
101
- parts = rest.split("/")
102
- if len(parts) < 2:
103
- return None, None
104
- return parts[0], parts[1]
105
-
106
- def _get(self, path: str, params: Optional[dict] = None) -> Dict[str, Any]:
107
- url = f"{BASE_WEB}{path}"
108
-
109
- platform, identifier = self._extract_profile_from_path(path)
110
- if platform and identifier:
111
- self._prime_profile_session(platform, identifier)
112
-
113
- api_headers = {
114
- "Accept": "application/json, text/plain, */*",
115
- "Referer": (
116
- f"{PROFILE_WEB}/{platform}/{identifier}"
117
- if platform and identifier
118
- else "https://rocketleague.tracker.network/"
119
- ),
120
- "Sec-Fetch-Dest": "empty",
121
- "Sec-Fetch-Mode": "cors",
122
- "Sec-Fetch-Site": "same-site",
123
- }
124
-
125
- try:
126
- r = self.session.get(url, params=params, headers=api_headers, timeout=self.timeout)
127
- except requests.RequestException as exc:
128
- raise TrackerError(f"Network error contacting Tracker: {exc}") from exc
129
- if r.status_code in (401, 403):
130
- preview = " ".join((r.text or "").split())[:240]
131
- server = r.headers.get("server", "unknown")
132
- raise TrackerError(
133
- f"HTTP {r.status_code} from Tracker (server={server}). "
134
- f"Response preview: {preview or '<empty>'}"
135
- )
136
- try:
137
- r.raise_for_status()
138
- except requests.HTTPError as exc:
139
- raise TrackerError(f"HTTP {r.status_code} for {r.url}") from exc
140
- try:
141
- return r.json()
142
- except ValueError as exc:
143
- raise TrackerError("Tracker returned non-JSON content") from exc
144
-
145
- def profile(self, platform: str, identifier: str) -> PlayerProfile:
146
- payload = self._get(f"/api/v2/rocket-league/standard/profile/{platform}/{identifier}")
147
- data = payload.get("data") or {}
148
- pinfo = data.get("platformInfo") or {}
149
- meta = data.get("metadata") or {}
150
- segments = data.get("segments") or []
151
-
152
- lifetime: Dict[str, Any] = {}
153
- playlists: List[Dict[str, Any]] = []
154
- for seg in segments:
155
- stats = seg.get("stats") or {}
156
- if seg.get("type") == "overview":
157
- lifetime = {k: (v or {}).get("value") for k, v in stats.items()}
158
- elif seg.get("type") == "playlist":
159
- attrs = seg.get("attributes") or {}
160
- smeta = seg.get("metadata") or {}
161
- tier = stats.get("tier") or {}
162
- div = stats.get("division") or {}
163
- rating = stats.get("rating") or {}
164
- peak = stats.get("peakRating") or {}
165
- playlists.append(
166
- {
167
- "playlist_id": attrs.get("playlistId"),
168
- "name": smeta.get("name", f"Playlist {attrs.get('playlistId')}"),
169
- "season": attrs.get("season"),
170
- "rating": rating.get("value"),
171
- "tier": (rating.get("metadata") or {}).get("tierName")
172
- or (tier.get("metadata") or {}).get("name"),
173
- "division": (div.get("metadata") or {}).get("name"),
174
- "matches_played": _stat_value(stats, "matchesPlayed"),
175
- "win_streak": _stat_value(stats, "winStreak"),
176
- "peak_rating": peak.get("value"),
177
- }
178
- )
179
-
180
- return PlayerProfile(
181
- platform=pinfo.get("platformSlug") or platform,
182
- identifier=pinfo.get("platformUserIdentifier") or identifier,
183
- handle=pinfo.get("platformUserHandle") or identifier,
184
- player_id=meta.get("playerId"),
185
- last_updated=_dt((meta.get("lastUpdated") or {}).get("value")),
186
- season=meta.get("currentSeason"),
187
- avatar_url=pinfo.get("avatarUrl"),
188
- lifetime=lifetime,
189
- playlists=playlists,
190
- raw=payload,
191
- )
192
-
193
- def sessions(self, platform: str, identifier: str) -> Dict[str, Any]:
194
- return self._get(f"/api/v2/rocket-league/standard/profile/{platform}/{identifier}/sessions")
195
-
196
- def mmr_history(self, player_id: int) -> Dict[str, Any]:
197
- return self._get(f"/api/v1/rocket-league/player-history/mmr/{player_id}")
198
-
199
- def overview_history(self, player_id: int) -> Dict[str, Any]:
200
- return self._get(f"/api/v1/rocket-league/player-history/overview/{player_id}", params={"type": "graph"})
201
-
202
- def aggregated(self, platform: str, identifier: str, local_offset: int = 0) -> Dict[str, Any]:
203
- return self._get(
204
- f"/api/v1/rocket-league/matches/{platform}/{identifier}/aggregated",
205
- params={"localOffset": local_offset},
206
- )
207
-
208
- @staticmethod
209
- def flatten_session_matches(payload: Dict[str, Any]) -> List[Dict[str, Any]]:
210
- out: List[Dict[str, Any]] = []
211
- for session in ((payload.get("data") or {}).get("items") or []):
212
- for m in session.get("matches") or []:
213
- meta = m.get("metadata") or {}
214
- stats = m.get("stats") or {}
215
- r = stats.get("rating") or {}
216
- rmeta = r.get("metadata") or {}
217
- out.append(
218
- {
219
- "match_id": m.get("id"),
220
- "collected_at": meta.get("dateCollected"),
221
- "playlist": meta.get("playlist"),
222
- "result": meta.get("result"),
223
- "is_grouped": bool(meta.get("isGrouped")),
224
- "rating": r.get("value"),
225
- "rating_delta": rmeta.get("ratingDelta"),
226
- "tier": rmeta.get("tier"),
227
- "division": rmeta.get("division"),
228
- "matches_played": _stat_value(stats, "matchesPlayed"),
229
- "wins": _stat_value(stats, "wins"),
230
- "goals": _stat_value(stats, "goals"),
231
- "assists": _stat_value(stats, "assists"),
232
- "saves": _stat_value(stats, "saves"),
233
- "shots": _stat_value(stats, "shots"),
234
- "mvps": _stat_value(stats, "mvps"),
235
- }
236
- )
237
- out.sort(key=lambda x: x.get("collected_at") or "", reverse=True)
238
- return out