hltv-async-api 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hltv-async-api/__init__.py +9 -0
- hltv-async-api/aiohltv.py +604 -0
- hltv_async_api-0.2.2.dist-info/LICENSE +19 -0
- hltv_async_api-0.2.2.dist-info/METADATA +115 -0
- hltv_async_api-0.2.2.dist-info/RECORD +9 -0
- hltv_async_api-0.2.2.dist-info/WHEEL +5 -0
- hltv_async_api-0.2.2.dist-info/top_level.txt +1 -0
- src/hltv-async-api/__init__.py +9 -0
- src/hltv-async-api/aiohltv.py +604 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
from typing import Any, List
|
|
2
|
+
from datetime import date, datetime, timedelta
|
|
3
|
+
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
import asyncio
|
|
6
|
+
from asyncio import get_running_loop
|
|
7
|
+
from functools import partial
|
|
8
|
+
|
|
9
|
+
from aiohttp import ClientSession, ClientTimeout
|
|
10
|
+
from aiohttp.client_exceptions import ClientProxyConnectionError, ClientResponseError, ClientOSError, \
|
|
11
|
+
ServerDisconnectedError, ClientHttpProxyError
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Hltv:
|
|
17
|
+
def __init__(self, max_delay: int = 15,
|
|
18
|
+
timeout: int = 5,
|
|
19
|
+
use_proxy: bool = False,
|
|
20
|
+
proxy_path: str | None = None,
|
|
21
|
+
proxy_list: list | None = None,
|
|
22
|
+
debug: bool = False,
|
|
23
|
+
max_retries: int = 0,
|
|
24
|
+
proxy_protocol: str | None = None,
|
|
25
|
+
remove_proxy: bool = False
|
|
26
|
+
):
|
|
27
|
+
self.headers = {
|
|
28
|
+
"referer": "https://www.hltv.org/stats",
|
|
29
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
|
30
|
+
"hltvTimeZone": "UTC"
|
|
31
|
+
}
|
|
32
|
+
self.MAX_DELAY = max_delay
|
|
33
|
+
self.timeout = timeout
|
|
34
|
+
self.max_retries = max_retries
|
|
35
|
+
self.USE_PROXY = use_proxy
|
|
36
|
+
self.PROXY_PATH = proxy_path
|
|
37
|
+
self.PROXY_LIST = proxy_list
|
|
38
|
+
self.PROXY_PROTOCOL = proxy_protocol
|
|
39
|
+
self.REMOVE_PROXY = remove_proxy
|
|
40
|
+
self.DEBUG = debug
|
|
41
|
+
self._configure_logging()
|
|
42
|
+
self.logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
def _configure_logging(self):
|
|
45
|
+
level = logging.DEBUG if self.DEBUG else logging.INFO
|
|
46
|
+
logging.basicConfig(level=level, format="%(message)s")
|
|
47
|
+
|
|
48
|
+
def config(self, max_delay: int | None = None,
|
|
49
|
+
timeout: int | None = None,
|
|
50
|
+
use_proxy: bool | None = None,
|
|
51
|
+
proxy_file_path: str | None = None,
|
|
52
|
+
proxy_list: list | None = None,
|
|
53
|
+
debug: bool | None = None,
|
|
54
|
+
max_retries: int | None = None,
|
|
55
|
+
proxy_protocol: str | None = None,
|
|
56
|
+
remove_proxy: bool | None = None,
|
|
57
|
+
):
|
|
58
|
+
if max_delay:
|
|
59
|
+
self.MAX_DELAY = max_delay
|
|
60
|
+
if timeout:
|
|
61
|
+
self.timeout = timeout
|
|
62
|
+
if use_proxy:
|
|
63
|
+
self.USE_PROXY = use_proxy
|
|
64
|
+
if proxy_file_path:
|
|
65
|
+
self.PROXY_PATH = proxy_file_path
|
|
66
|
+
if proxy_list:
|
|
67
|
+
self.PROXY_LIST = proxy_list
|
|
68
|
+
if max_retries:
|
|
69
|
+
self.max_retries = max_retries
|
|
70
|
+
if proxy_protocol:
|
|
71
|
+
self.PROXY_PROTOCOL = proxy_protocol
|
|
72
|
+
if remove_proxy:
|
|
73
|
+
self.REMOVE_PROXY = remove_proxy
|
|
74
|
+
if debug:
|
|
75
|
+
self.DEBUG = debug
|
|
76
|
+
self._configure_logging()
|
|
77
|
+
|
|
78
|
+
def get_proxy(self):
|
|
79
|
+
proxy = ''
|
|
80
|
+
if self.PROXY_PATH:
|
|
81
|
+
with open(self.PROXY_PATH, "r") as file:
|
|
82
|
+
new_proxy = file.readline().strip()
|
|
83
|
+
if new_proxy:
|
|
84
|
+
proxy = new_proxy
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
proxy = self.PROXY_LIST[0]
|
|
88
|
+
|
|
89
|
+
if self.PROXY_PROTOCOL not in proxy and proxy != '':
|
|
90
|
+
proxy = self.PROXY_PROTOCOL + '://' + proxy
|
|
91
|
+
|
|
92
|
+
return proxy
|
|
93
|
+
|
|
94
|
+
async def switch_proxy(self, proxy):
|
|
95
|
+
if self.PROXY_PATH:
|
|
96
|
+
if self.PROXY_PROTOCOL:
|
|
97
|
+
proxy = proxy.replace(self.PROXY_PROTOCOL + '://', '')
|
|
98
|
+
with open(self.PROXY_PATH, "r+") as file:
|
|
99
|
+
proxies = file.readlines()
|
|
100
|
+
file.seek(0)
|
|
101
|
+
for line in proxies:
|
|
102
|
+
if line.strip() != proxy:
|
|
103
|
+
file.write(line)
|
|
104
|
+
if not self.REMOVE_PROXY:
|
|
105
|
+
file.write(proxy + "\n")
|
|
106
|
+
else:
|
|
107
|
+
self.PROXY_LIST = self.PROXY_LIST[1:] + [self.PROXY_LIST[0]]
|
|
108
|
+
|
|
109
|
+
def f(self, result):
|
|
110
|
+
return BeautifulSoup(result, "lxml")
|
|
111
|
+
|
|
112
|
+
async def cloudflare_check(self, result) -> bool:
|
|
113
|
+
page = self.f(result)
|
|
114
|
+
challenge_page = page.find(id="challenge-error-title")
|
|
115
|
+
if challenge_page is not None:
|
|
116
|
+
if "Enable JavaScript and cookies to continue" in challenge_page.get_text():
|
|
117
|
+
self.logger.debug("Got cloudflare challange page")
|
|
118
|
+
return True
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
async def parse_error_handler(self, proxy, delay: int = 0) -> int:
|
|
122
|
+
if self.USE_PROXY:
|
|
123
|
+
self.logger.debug(f"Switching proxy {proxy}")
|
|
124
|
+
await self.switch_proxy(proxy)
|
|
125
|
+
self.logger.info(f"New proxy: {self.get_proxy()}")
|
|
126
|
+
else:
|
|
127
|
+
if delay < self.MAX_DELAY:
|
|
128
|
+
delay += 1
|
|
129
|
+
else:
|
|
130
|
+
self.logger.debug("Reached max delay limit, try to use proxy")
|
|
131
|
+
self.logger.info(f"Calling again, increasing delay to {delay}s")
|
|
132
|
+
|
|
133
|
+
return delay
|
|
134
|
+
|
|
135
|
+
async def parse(self, session, url, delay):
|
|
136
|
+
proxy = ''
|
|
137
|
+
# setup new proxy, cuz old one was switched
|
|
138
|
+
if self.USE_PROXY:
|
|
139
|
+
proxy = self.get_proxy()
|
|
140
|
+
else:
|
|
141
|
+
# delay, only for non proxy users. (default = 1-15s)
|
|
142
|
+
await asyncio.sleep(delay)
|
|
143
|
+
try:
|
|
144
|
+
async with session.get(url, headers=self.headers, proxy=proxy, timeout=self.timeout, ) as response:
|
|
145
|
+
self.logger.info(f"Fetching {url}, code: {response.status}")
|
|
146
|
+
if response.status == 403 or response.status == 404:
|
|
147
|
+
self.logger.debug("Got 403 forbitten")
|
|
148
|
+
return False, self.parse_error_handler(proxy, delay)
|
|
149
|
+
|
|
150
|
+
# checking for challenge page.
|
|
151
|
+
result = await response.text()
|
|
152
|
+
if await self.cloudflare_check(result):
|
|
153
|
+
return False, self.parse_error_handler(proxy, delay)
|
|
154
|
+
|
|
155
|
+
return True, result
|
|
156
|
+
except (ClientProxyConnectionError, ClientResponseError, ClientOSError,
|
|
157
|
+
ServerDisconnectedError, TimeoutError, ClientHttpProxyError) as e:
|
|
158
|
+
delay = await self.parse_error_handler(proxy, delay)
|
|
159
|
+
return False, delay
|
|
160
|
+
|
|
161
|
+
async def fetch(self, url, delay: int = 0):
|
|
162
|
+
async with ClientSession() as session:
|
|
163
|
+
status = False
|
|
164
|
+
try_ = 1
|
|
165
|
+
while not status and (try_ != self.max_retries):
|
|
166
|
+
self.logger.debug(f'Trying connect to {url}, try {try_}/{self.max_retries}')
|
|
167
|
+
|
|
168
|
+
# if status = True, result = page,
|
|
169
|
+
# if status = False result = delay (default=0)
|
|
170
|
+
status, result = await self.parse(session, url, delay)
|
|
171
|
+
|
|
172
|
+
if not status and result:
|
|
173
|
+
delay = result
|
|
174
|
+
try_ += 1
|
|
175
|
+
|
|
176
|
+
if status == True:
|
|
177
|
+
loop = get_running_loop()
|
|
178
|
+
parsed = await loop.run_in_executor(None, partial(self.f, result))
|
|
179
|
+
return parsed
|
|
180
|
+
else:
|
|
181
|
+
self.logger.error('Connection failed')
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def save_error(self, page):
|
|
185
|
+
with open("error.html", "w") as file:
|
|
186
|
+
file.write(page.prettify())
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def normalize_date(parts) -> str:
|
|
190
|
+
month_abbreviations = {
|
|
191
|
+
'Jan': '1', 'Feb': '2', 'Mar': '3', 'Apr': '4',
|
|
192
|
+
'May': '5', 'Jun': '6', 'Jul': '7', 'Aug': '8',
|
|
193
|
+
'Sep': '9', 'Oct': '10', 'Nov': '11', 'Dec': '12'
|
|
194
|
+
}
|
|
195
|
+
month = month_abbreviations[parts[0]]
|
|
196
|
+
day = parts[1][:-2]
|
|
197
|
+
return day + '-' + month
|
|
198
|
+
|
|
199
|
+
async def get_live_matches(self):
|
|
200
|
+
"""returns a list of all LIVE matches on HLTV along with the maps being played and the star ratings"""
|
|
201
|
+
r = await self.fetch("https://www.hltv.org/matches")
|
|
202
|
+
if not r:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
live_matches = r.find("div", {'class', "liveMatchesContainer"})
|
|
206
|
+
if live_matches is None:
|
|
207
|
+
return []
|
|
208
|
+
else:
|
|
209
|
+
teams = [line.getText() for line in live_matches.find_all("div", {'class', "matchTeamName text-ellipsis"})]
|
|
210
|
+
matches = [(team1, team2) for team1, team2 in tuple(zip(teams, teams[1:]))[::2]]
|
|
211
|
+
liveMatchContainer = live_matches.find_all("div", {'class', "liveMatch-container"})
|
|
212
|
+
maps = [str(line.get('data-maps')).split(',') for line in liveMatchContainer]
|
|
213
|
+
stars = [line.get('stars') for line in liveMatchContainer]
|
|
214
|
+
return [{'teams': teams, 'maps': maps, 'stars': stars} for teams, maps, stars in zip(matches, maps, stars)]
|
|
215
|
+
|
|
216
|
+
async def get_upcoming_matches(self):
|
|
217
|
+
"""returns a list of all upcoming matches on HLTV"""
|
|
218
|
+
r = await self.fetch("https://www.hltv.org/matches")
|
|
219
|
+
if not r:
|
|
220
|
+
return
|
|
221
|
+
try:
|
|
222
|
+
teams = [line.getText() for line in r.find("div",
|
|
223
|
+
{'class', "upcomingMatchesContainer"}).find_all(
|
|
224
|
+
class_=lambda v: v is not None and (v == "team text-ellipsis" or v == "matchTeamName text-ellipsis"))]
|
|
225
|
+
except AttributeError:
|
|
226
|
+
return None
|
|
227
|
+
return [(team1, team2) for team1, team2 in tuple(zip(teams, teams[1:]))[::2]]
|
|
228
|
+
|
|
229
|
+
async def get_important_upcoming_matches(self, star_rating=1):
|
|
230
|
+
"""returns a list of all upcoming matches on HLTV with the star rating argument (should be between 0 and 5)"""
|
|
231
|
+
r = await self.fetch("https://www.hltv.org/matches")
|
|
232
|
+
if not r:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
teams = [line.getText() for line in r.find("div",
|
|
236
|
+
{'class', "upcomingMatchesContainer"}).find_all(
|
|
237
|
+
class_=lambda v: v is not None and (v == "team text-ellipsis" or v == "matchTeamName text-ellipsis"))]
|
|
238
|
+
stars = [int(line.get('stars')) for line in r.find("div",
|
|
239
|
+
{'class', "upcomingMatchesContainer"}).find_all("div",
|
|
240
|
+
{"class",
|
|
241
|
+
"upcomingMatch "})
|
|
242
|
+
if line.get('team1') is not None]
|
|
243
|
+
matches = [(team1, team2) for team1, team2 in tuple(zip(teams, teams[1:]))[::2]]
|
|
244
|
+
# assert len(matches) == len(stars), "Internal Exception :: get_important_upcoming_matches() :: misMatches detected"
|
|
245
|
+
return [match for match, star in zip(matches, stars) if star == star_rating]
|
|
246
|
+
|
|
247
|
+
async def get_big_results(self, offset=0) -> list[dict[str, Any]] | None:
|
|
248
|
+
"""returns a list of results from past 100 matches on HLTV starting from the offset param"""
|
|
249
|
+
r = await self.fetch("https://www.hltv.org/results?offset=" + str(offset))
|
|
250
|
+
if not r:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
big_results = []
|
|
254
|
+
big_res = r.find("div", {'class', "big-results"}).find_all("div", {"class", "result-con"})
|
|
255
|
+
if not big_res:
|
|
256
|
+
return None
|
|
257
|
+
for res in big_res:
|
|
258
|
+
team1 = res.find("div", class_="team").text.strip()
|
|
259
|
+
team2 = res.find("div", class_="team team-won").text.strip()
|
|
260
|
+
|
|
261
|
+
scores = res.find("td", class_="result-score").text.strip().split('-')
|
|
262
|
+
s_t1 = scores[0].strip()
|
|
263
|
+
s_t2 = scores[1].strip()
|
|
264
|
+
|
|
265
|
+
big_results.append({
|
|
266
|
+
'team1': team1,
|
|
267
|
+
'team2': team2,
|
|
268
|
+
'score1': s_t1,
|
|
269
|
+
'score2': s_t2,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
return big_results
|
|
273
|
+
|
|
274
|
+
async def get_event_results(self, event: int | str) -> list[dict[str, Any]] | None:
|
|
275
|
+
r = await self.fetch("https://www.hltv.org/results?event=" + str(event))
|
|
276
|
+
if not r:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
match_results = []
|
|
280
|
+
|
|
281
|
+
for result in r.find("div", {'class', 'results-holder'}).find_all("div", {'class', 'results-sublist'}):
|
|
282
|
+
date = result.find("span", class_="standard-headline").text.strip()
|
|
283
|
+
matches = result.find_all("div", class_="result-con")
|
|
284
|
+
|
|
285
|
+
for match in matches:
|
|
286
|
+
teams = match.find_all("div", class_="team")
|
|
287
|
+
team1 = teams[0].text.strip()
|
|
288
|
+
team2 = teams[1].text.strip()
|
|
289
|
+
|
|
290
|
+
scores = match.find("td", class_="result-score").text.strip().split('-')
|
|
291
|
+
score_t1 = scores[0].strip()
|
|
292
|
+
score_t2 = scores[1].strip()
|
|
293
|
+
|
|
294
|
+
match_results.append({
|
|
295
|
+
'date': date,
|
|
296
|
+
'team1': team1,
|
|
297
|
+
'team2': team2,
|
|
298
|
+
'score1': score_t1,
|
|
299
|
+
'score2': score_t2,
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
return match_results
|
|
303
|
+
|
|
304
|
+
async def get_event_matches(self, event_id: str | int):
|
|
305
|
+
r = await self.fetch("https://www.hltv.org/events/" + str(event_id) + "/matches")
|
|
306
|
+
if not r:
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
live_matches: List | Any
|
|
310
|
+
matches = []
|
|
311
|
+
try:
|
|
312
|
+
live_matches = r.find("div", {'class', 'liveMatchesSection'}).find_all("div", {'class', 'liveMatch'})
|
|
313
|
+
except AttributeError:
|
|
314
|
+
live_matches = []
|
|
315
|
+
|
|
316
|
+
for live in live_matches:
|
|
317
|
+
teams = live.find_all("div", class_="matchTeamName text-ellipsis")
|
|
318
|
+
team1 = teams[0].text.strip()
|
|
319
|
+
team2 = teams[1].text.strip()
|
|
320
|
+
|
|
321
|
+
# TODO FIX SCORES
|
|
322
|
+
try:
|
|
323
|
+
scores = live.find("td", class_="matchTeamScore").text.strip().split('-')
|
|
324
|
+
score_team1 = scores[0].strip()
|
|
325
|
+
score_team2 = scores[1].strip()
|
|
326
|
+
except AttributeError:
|
|
327
|
+
score_team1 = 0
|
|
328
|
+
score_team2 = 0
|
|
329
|
+
|
|
330
|
+
matches.append({
|
|
331
|
+
'team1': team1,
|
|
332
|
+
'team2': team2,
|
|
333
|
+
'date': 'LIVE'
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
for date_sect in r.find_all('div', {'class': 'upcomingMatchesSection'}):
|
|
337
|
+
date_ = date_sect.find('span', {'class': 'matchDayHeadline'}).text.split(' ')[-1]
|
|
338
|
+
for match in date_sect.find_all('div', {'class': 'upcomingMatch'}):
|
|
339
|
+
teams = match.find_all("div", class_="matchTeamName text-ellipsis")
|
|
340
|
+
if teams or len(teams) > 1:
|
|
341
|
+
|
|
342
|
+
team1 = teams[0].text.strip()
|
|
343
|
+
team2 = teams[1].text.strip()
|
|
344
|
+
|
|
345
|
+
time_ = match.find('div', {'class', 'matchTime'}).text
|
|
346
|
+
|
|
347
|
+
matches.append({
|
|
348
|
+
'team1': team1,
|
|
349
|
+
'team2': team2,
|
|
350
|
+
'date': date_ + " " + time_
|
|
351
|
+
})
|
|
352
|
+
else:
|
|
353
|
+
break
|
|
354
|
+
|
|
355
|
+
return matches
|
|
356
|
+
|
|
357
|
+
async def get_events(self, outgoing=True, future=True, max_events=10):
|
|
358
|
+
"""Returns events
|
|
359
|
+
:params:
|
|
360
|
+
outgoing - include live tournaments
|
|
361
|
+
future - include future tournamets
|
|
362
|
+
max_events - use only if future=True
|
|
363
|
+
:return:
|
|
364
|
+
[('id', 'title', 'startdate', 'enddate')]
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
r = await self.fetch('https://www.hltv.org/events')
|
|
368
|
+
if not r:
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
events = []
|
|
372
|
+
if outgoing:
|
|
373
|
+
for event in r.find('div', {'class': 'tab-content', 'id': 'TODAY'}).find_all('a', {
|
|
374
|
+
'class': 'a-reset ongoing-event'}):
|
|
375
|
+
event_name = event.find('div', {'class': 'text-ellipsis'}).text.strip()
|
|
376
|
+
event_start_date = self.normalize_date(
|
|
377
|
+
event.find('span', {'data-time-format': 'MMM do'}).text.strip().split())
|
|
378
|
+
|
|
379
|
+
event_end_date = self.normalize_date(
|
|
380
|
+
event.find_all('span', {'data-time-format': 'MMM do'})[1].text.strip().split())
|
|
381
|
+
event_id = event['href'].split('/')[-2]
|
|
382
|
+
|
|
383
|
+
events.append({
|
|
384
|
+
'id': event_id,
|
|
385
|
+
'name': event_name,
|
|
386
|
+
'start_date': event_start_date,
|
|
387
|
+
'end_date': event_end_date,
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
if future:
|
|
391
|
+
for i, big_event_div in enumerate(r.find_all('div', {'class': 'big-events'}, start=1)):
|
|
392
|
+
for event in big_event_div.find_all('a', {'class': 'a-reset standard-box big-event'}):
|
|
393
|
+
|
|
394
|
+
if i >= max_events:
|
|
395
|
+
break
|
|
396
|
+
|
|
397
|
+
event_id = event['href'].split('/')[-2]
|
|
398
|
+
event_name = event.find('div', {'class': 'big-event-name'}).text.strip()
|
|
399
|
+
# event_location = event.find('span', {'class': 'big-event-location'}).text.strip()
|
|
400
|
+
event_start_date = self.normalize_date(event.find('span', {'class': ''}).text.strip().split())
|
|
401
|
+
event_end_date = self.normalize_date(event.find('span', {'class': ''}).text.strip().split())
|
|
402
|
+
|
|
403
|
+
events.append({
|
|
404
|
+
'id': event_id,
|
|
405
|
+
'title': event_name,
|
|
406
|
+
'start_date': event_start_date,
|
|
407
|
+
'end_date': event_end_date
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
return events
|
|
411
|
+
|
|
412
|
+
async def get_event_info(self, event_id: str | int, event_title: str):
|
|
413
|
+
r = await self.fetch(f"https://www.hltv.org/events/{str(event_id)}/{event_title.replace(' ', '-')}")
|
|
414
|
+
if not r:
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
event_date_div = r.find('td', {'class', 'eventdate'}).find_all('span')
|
|
418
|
+
|
|
419
|
+
event_start = self.normalize_date(event_date_div[0].text.split())
|
|
420
|
+
event_end = self.normalize_date(event_date_div[1].text.split()[1:-1])
|
|
421
|
+
|
|
422
|
+
prize = r.find('td', {'class', 'prizepool text-ellipsis'}).text
|
|
423
|
+
|
|
424
|
+
team_num = r.find('td', {'class', 'teamsNumber'}).text
|
|
425
|
+
|
|
426
|
+
location = r.find('td', {'class', 'location gtSmartphone-only'}).get_text().replace('\n', '')
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
group_div = r.find('div', {'class', 'groups-container'})
|
|
430
|
+
groups = []
|
|
431
|
+
for group in group_div.find_all('table', {'class': 'table standard-box'}):
|
|
432
|
+
group_name = group.find('td', {'class': 'table-header group-name'}).text
|
|
433
|
+
teams = []
|
|
434
|
+
for team in group.find_all('div', 'text-ellipsis'):
|
|
435
|
+
teams.append(team.find('a').text)
|
|
436
|
+
groups.append({group_name: teams})
|
|
437
|
+
except AttributeError:
|
|
438
|
+
groups = []
|
|
439
|
+
|
|
440
|
+
return event_id, event_title, event_start, event_end, prize, team_num, location, groups
|
|
441
|
+
|
|
442
|
+
async def get_top_teams(self, max_teams=30):
|
|
443
|
+
"""
|
|
444
|
+
returns a list of the top 1-30 teams
|
|
445
|
+
:params:
|
|
446
|
+
max_teams: int = 30
|
|
447
|
+
:return:
|
|
448
|
+
[('rank','title','points', 'change', 'id')]
|
|
449
|
+
change - difference between last ranking update
|
|
450
|
+
"""
|
|
451
|
+
today = date.today()
|
|
452
|
+
current_weekday = today.weekday()
|
|
453
|
+
last_monday = today - timedelta(days=current_weekday)
|
|
454
|
+
|
|
455
|
+
teams = []
|
|
456
|
+
|
|
457
|
+
r = await self.fetch("https://www.hltv.org/ranking/teams/" + last_monday.strftime('%Y/%B/%d').lower())
|
|
458
|
+
|
|
459
|
+
if not r:
|
|
460
|
+
return
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
for i, team in enumerate(r.find_all("div", {'class': "ranked-team standard-box"}), start=1):
|
|
464
|
+
if i > max_teams:
|
|
465
|
+
break
|
|
466
|
+
|
|
467
|
+
rank = team.find('span', {'class': 'position'}).text[1:]
|
|
468
|
+
title_div: Any
|
|
469
|
+
if rank != '1':
|
|
470
|
+
title_div = team.find('div', {'class': 'teamLine sectionTeamPlayers'})
|
|
471
|
+
else:
|
|
472
|
+
title_div = team.find('div', {'class': 'teamLine sectionTeamPlayers teamLineExpanded'})
|
|
473
|
+
|
|
474
|
+
title = title_div.find('span', {'class': 'name'}).text
|
|
475
|
+
points = title_div.find('span', {'class': 'points'}).text.split(' ', 1)[0][1:]
|
|
476
|
+
|
|
477
|
+
id = team.find('a', {'class': 'details moreLink'})['href'].split('/')[-1]
|
|
478
|
+
|
|
479
|
+
changes = {'change positive', 'change neutral', 'change negative'}
|
|
480
|
+
change = ''
|
|
481
|
+
for change_ in changes:
|
|
482
|
+
try:
|
|
483
|
+
change = team.find('div', {'class', change_}).text
|
|
484
|
+
break
|
|
485
|
+
except AttributeError:
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
teams.append({
|
|
489
|
+
'rank': rank,
|
|
490
|
+
'title': title,
|
|
491
|
+
'points': points,
|
|
492
|
+
'change': change,
|
|
493
|
+
'id': id
|
|
494
|
+
})
|
|
495
|
+
except AttributeError:
|
|
496
|
+
raise AttributeError("Parsing error, probably page not fully loaded")
|
|
497
|
+
|
|
498
|
+
return teams
|
|
499
|
+
|
|
500
|
+
async def get_team_info(self, team_id: int | str, title: str) -> tuple | None:
|
|
501
|
+
"""
|
|
502
|
+
Returns Information about team
|
|
503
|
+
:params:
|
|
504
|
+
team_id: int | str
|
|
505
|
+
title: str
|
|
506
|
+
:returns:
|
|
507
|
+
(team_id, title, rank, players, coach, age, weeks, last_trophy, total_trophys) | None
|
|
508
|
+
weeks - weeks in top 20
|
|
509
|
+
"""
|
|
510
|
+
r = await self.fetch("https://www.hltv.org/team/" + str(team_id) + '/' + title.replace(' ', '-'))
|
|
511
|
+
players = []
|
|
512
|
+
try:
|
|
513
|
+
for player in r.find_all('span', {'class': 'text-ellipsis bold'}):
|
|
514
|
+
players.append(player.text)
|
|
515
|
+
|
|
516
|
+
rank = '0'
|
|
517
|
+
weeks = '0'
|
|
518
|
+
age = '0'
|
|
519
|
+
coach = ''
|
|
520
|
+
|
|
521
|
+
for i, stat in enumerate(r.find_all('div', {'class': 'profile-team-stat'}), start=1):
|
|
522
|
+
if i == 1:
|
|
523
|
+
rank = stat.find('a').text[1:]
|
|
524
|
+
elif i == 2:
|
|
525
|
+
weeks = stat.find('span', {'class': 'right'}).text
|
|
526
|
+
elif i == 3:
|
|
527
|
+
age = stat.find('span', {'class': 'right'}).text
|
|
528
|
+
elif i == 4:
|
|
529
|
+
coach = stat.find('span', {'class': 'bold a-default'}).text[1:-1]
|
|
530
|
+
|
|
531
|
+
last_trophy = None
|
|
532
|
+
total_trophys = None
|
|
533
|
+
try:
|
|
534
|
+
last_trophy = r.find('div', {'class': 'trophyHolder'}).find('span')['title']
|
|
535
|
+
total_trophys = len(r.find_all('div', {'class': 'trophyHolder'}))
|
|
536
|
+
except AttributeError:
|
|
537
|
+
pass
|
|
538
|
+
|
|
539
|
+
return (team_id, title, rank, players, coach, age, weeks, last_trophy, total_trophys)
|
|
540
|
+
except AttributeError:
|
|
541
|
+
raise AttributeError("Parsing error, probably page not fully loaded")
|
|
542
|
+
|
|
543
|
+
async def get_best_players(self, top=40):
|
|
544
|
+
"""
|
|
545
|
+
returns a list of the top (1-40) players in top 20 at the year
|
|
546
|
+
:params:
|
|
547
|
+
top: int = 40
|
|
548
|
+
:returns:
|
|
549
|
+
('rank', 'name', 'team', 'maps', 'rating')
|
|
550
|
+
maps - maps played
|
|
551
|
+
"""
|
|
552
|
+
year = datetime.strftime(datetime.utcnow(), '%Y')
|
|
553
|
+
r = await self.fetch(
|
|
554
|
+
f"https://www.hltv.org/stats/players?startDate={year}-01-01&endDate={year}-12-31&rankingFilter=Top20")
|
|
555
|
+
|
|
556
|
+
if not r:
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
players = []
|
|
560
|
+
rank = 1
|
|
561
|
+
try:
|
|
562
|
+
for player in r.find('tbody').find_all('tr'):
|
|
563
|
+
name = player.find('td', {'class', 'playerCol'}).find('a').text
|
|
564
|
+
team = player.find('td', {'class', 'teamCol'})['data-sort']
|
|
565
|
+
|
|
566
|
+
maps = player.find('td', {'class', 'statsDetail'}).text
|
|
567
|
+
|
|
568
|
+
ratings = {'ratingCol ratingPositive', 'ratingCol ratingNeutral', 'ratingCol ratingNegative'}
|
|
569
|
+
rating = 'ERROR'
|
|
570
|
+
for rat in ratings:
|
|
571
|
+
try:
|
|
572
|
+
rating = player.find('td', {'class', rat}).text
|
|
573
|
+
|
|
574
|
+
break
|
|
575
|
+
except AttributeError:
|
|
576
|
+
pass
|
|
577
|
+
|
|
578
|
+
players.append({
|
|
579
|
+
'rank': rank,
|
|
580
|
+
'name': name,
|
|
581
|
+
'team': team,
|
|
582
|
+
'maps': maps,
|
|
583
|
+
'rating': rating,
|
|
584
|
+
})
|
|
585
|
+
rank += 1
|
|
586
|
+
if rank > top:
|
|
587
|
+
break
|
|
588
|
+
except AttributeError:
|
|
589
|
+
raise AttributeError("Parsing error, probably page not fully loaded")
|
|
590
|
+
|
|
591
|
+
return players
|
|
592
|
+
|
|
593
|
+
# TODO WRITE
|
|
594
|
+
async def get_last_news(self):
|
|
595
|
+
return []
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
async def test():
|
|
599
|
+
hltv = Hltv()
|
|
600
|
+
print(await hltv.get_event_info(7148, 'pgl-cs2-major-copenhagen-2024'))
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
if __name__ == "__main__":
|
|
604
|
+
asyncio.run(test())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|