infinitode.py 1.0.4__tar.gz → 1.1.0__tar.gz
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.
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/PKG-INFO +5 -2
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/README.md +4 -1
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode/__init__.py +2 -2
- infinitode.py-1.1.0/infinitode/badge.py +29 -0
- infinitode.py-1.1.0/infinitode/core.py +462 -0
- infinitode.py-1.1.0/infinitode/errors.py +25 -0
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode/leaderboard.py +63 -54
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode/player.py +89 -43
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode/score.py +47 -41
- infinitode.py-1.1.0/infinitode/utils.py +65 -0
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode.py.egg-info/PKG-INFO +5 -2
- infinitode.py-1.0.4/infinitode/badge.py +0 -31
- infinitode.py-1.0.4/infinitode/core.py +0 -315
- infinitode.py-1.0.4/infinitode/errors.py +0 -22
- infinitode.py-1.0.4/infinitode/utils.py +0 -36
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/LICENSE +0 -0
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode/py.typed +0 -0
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode.py.egg-info/SOURCES.txt +0 -0
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode.py.egg-info/dependency_links.txt +0 -0
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode.py.egg-info/requires.txt +0 -0
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/infinitode.py.egg-info/top_level.txt +0 -0
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/setup.cfg +0 -0
- {infinitode.py-1.0.4 → infinitode.py-1.1.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: infinitode.py
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A python wrapper for the Infinitode 2 API.
|
|
5
5
|
Home-page: https://github.com/Sprylos/infinitode.py
|
|
6
6
|
Author: Sprylos
|
|
@@ -116,7 +116,10 @@ async def main():
|
|
|
116
116
|
player.nickname,
|
|
117
117
|
player.level,
|
|
118
118
|
player.xp,
|
|
119
|
-
player.xp_max, # xp required per level
|
|
119
|
+
player.xp_max, # xp required per level
|
|
120
|
+
player.season_level,
|
|
121
|
+
player.season_xp,
|
|
122
|
+
player.season_xp_max,
|
|
120
123
|
player.total_score,
|
|
121
124
|
player.total_rank,
|
|
122
125
|
player.total_top,
|
|
@@ -105,7 +105,10 @@ async def main():
|
|
|
105
105
|
player.nickname,
|
|
106
106
|
player.level,
|
|
107
107
|
player.xp,
|
|
108
|
-
player.xp_max, # xp required per level
|
|
108
|
+
player.xp_max, # xp required per level
|
|
109
|
+
player.season_level,
|
|
110
|
+
player.season_xp,
|
|
111
|
+
player.season_xp_max,
|
|
109
112
|
player.total_score,
|
|
110
113
|
player.total_rank,
|
|
111
114
|
player.total_top,
|
|
@@ -7,10 +7,10 @@ An asynchronous wrapper for the Infinitode API.
|
|
|
7
7
|
|
|
8
8
|
__author__ = "Sprylos"
|
|
9
9
|
__title__ = "infinitode.py"
|
|
10
|
-
__version__ =
|
|
10
|
+
__version__ = "1.1.0"
|
|
11
11
|
__license__ = """MIT License
|
|
12
12
|
|
|
13
|
-
Copyright (c)
|
|
13
|
+
Copyright (c) 2024 Sprylos
|
|
14
14
|
|
|
15
15
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
16
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = ("Badge",)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Badge:
|
|
7
|
+
"""Represents an in-game pinned badge."""
|
|
8
|
+
|
|
9
|
+
__slots__ = ("icon_img", "icon_color", "overlay_img", "overlay_color")
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self, iconImg: str, iconColor: str, overlayImg: str, overlayColor: str
|
|
13
|
+
) -> None:
|
|
14
|
+
self.icon_img = iconImg
|
|
15
|
+
self.icon_color = iconColor
|
|
16
|
+
self.overlay_img = overlayImg
|
|
17
|
+
self.overlay_color = overlayColor
|
|
18
|
+
|
|
19
|
+
# magic methods
|
|
20
|
+
|
|
21
|
+
def __repr__(self) -> str:
|
|
22
|
+
attrs = {
|
|
23
|
+
"icon_img": self.icon_img,
|
|
24
|
+
"icon_color": self.icon_color,
|
|
25
|
+
"overlay_img": self.overlay_img,
|
|
26
|
+
"overlay_color": self.overlay_color,
|
|
27
|
+
}
|
|
28
|
+
inner = " ".join(f"{k}={v}" for k, v in attrs.items())
|
|
29
|
+
return f"<{self.__class__.__name__} {inner}>"
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# std
|
|
4
|
+
import re
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import datetime
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Dict,
|
|
11
|
+
Optional,
|
|
12
|
+
Union,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# packages
|
|
16
|
+
import aiohttp
|
|
17
|
+
import bs4
|
|
18
|
+
|
|
19
|
+
# local
|
|
20
|
+
from .errors import APIError, BadArgument
|
|
21
|
+
from .leaderboard import Leaderboard
|
|
22
|
+
from .player import Player
|
|
23
|
+
from .score import Score
|
|
24
|
+
from .utils import async_expiring_cache, try_int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ("Session",)
|
|
28
|
+
|
|
29
|
+
_log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
ID_REGEX = re.compile(r"U-([A-Z0-9]{4}-){2}[A-Z0-9]{6}")
|
|
32
|
+
|
|
33
|
+
# fmt: off
|
|
34
|
+
LEVELS = (
|
|
35
|
+
'1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.b1',
|
|
36
|
+
'2.1', '2.2', '2.3', '2.4', '2.5', '2.6', '2.7', '2.8', '2.b1',
|
|
37
|
+
'3.1', '3.2', '3.3', '3.4', '3.5', '3.6', '3.7', '3.8', '3.b1',
|
|
38
|
+
'4.1', '4.2', '4.3', '4.4', '4.5', '4.6', '4.7', '4.8', '4.b1',
|
|
39
|
+
'5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '5.7', '5.8', '5.b1', '5.b2',
|
|
40
|
+
'6.1', '6.2', '6.3', '6.4', '6.5', '6.6', 'rumble', 'dev', 'zecred',
|
|
41
|
+
'DQ1', 'DQ3', 'DQ4', 'DQ5', 'DQ7', 'DQ8', 'DQ9', 'DQ10', 'DQ11', 'DQ12',
|
|
42
|
+
)
|
|
43
|
+
MODES = ('score', 'waves')
|
|
44
|
+
DIFFICULTIES = ('EASY', 'NORMAL', 'ENDLESS_I')
|
|
45
|
+
# fmt: on
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _base_url(beta: bool = False) -> str:
|
|
49
|
+
return f"https://{'beta.' if beta else ''}infinitode.prineside.com/"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Session:
|
|
53
|
+
def __init__(self, session: Optional[aiohttp.ClientSession] = None) -> None:
|
|
54
|
+
self._session = session or aiohttp.ClientSession()
|
|
55
|
+
|
|
56
|
+
# async enter and exit allow for the fancy "with" statements
|
|
57
|
+
# useful so you don't have to close the session yourself
|
|
58
|
+
async def __aenter__(self):
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
|
62
|
+
await self.close()
|
|
63
|
+
|
|
64
|
+
async def close(self):
|
|
65
|
+
"""Closes the internal ClientSession."""
|
|
66
|
+
await self._session.close()
|
|
67
|
+
|
|
68
|
+
# Rough parameter checking, "*" makes params keyword only
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _kwarg_check(
|
|
71
|
+
*,
|
|
72
|
+
mapname: Optional[str] = None,
|
|
73
|
+
playerid: Optional[str] = None,
|
|
74
|
+
mode: Optional[str] = None,
|
|
75
|
+
difficulty: Optional[str] = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
if mapname is not None and str(mapname) not in LEVELS:
|
|
78
|
+
raise BadArgument("Invalid map: " + mapname)
|
|
79
|
+
if playerid is not None and not ID_REGEX.match(playerid):
|
|
80
|
+
raise BadArgument("Invalid playerid: " + playerid)
|
|
81
|
+
if mode is not None and not mode in MODES:
|
|
82
|
+
raise BadArgument(f"Invalid mode (must be one of {MODES}): " + mode)
|
|
83
|
+
if difficulty is not None and not difficulty in DIFFICULTIES:
|
|
84
|
+
raise BadArgument(
|
|
85
|
+
f"Invalid difficulty (must be one of {DIFFICULTIES}): " + difficulty
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# not being more specific with the payload type
|
|
89
|
+
# so the typechecker stops annoying me
|
|
90
|
+
async def _post(
|
|
91
|
+
self, arg: str, data: Optional[Dict[str, Any]] = None, *, beta: bool = False
|
|
92
|
+
) -> Dict[str, Any]:
|
|
93
|
+
"""Internal post method to communicate with Rainy's API"""
|
|
94
|
+
url = _base_url(beta) + f"?m=api&a={arg}&apiv=1&g=com.prineside.tdi2&v=282"
|
|
95
|
+
_log.info("Sending POST request %s with data %s", arg, data)
|
|
96
|
+
async with self._session.post(url, data=data) as r:
|
|
97
|
+
try:
|
|
98
|
+
r.raise_for_status()
|
|
99
|
+
except aiohttp.ClientResponseError:
|
|
100
|
+
raise APIError("Something went wrong. Try again later")
|
|
101
|
+
|
|
102
|
+
payload: Dict[str, Any] = await r.json()
|
|
103
|
+
_log.debug("Response to POST request %s: %s", arg, payload)
|
|
104
|
+
|
|
105
|
+
if payload["status"] == "success":
|
|
106
|
+
return payload
|
|
107
|
+
else:
|
|
108
|
+
raise APIError(f'Error response from server: {payload["message"]}')
|
|
109
|
+
|
|
110
|
+
@async_expiring_cache()
|
|
111
|
+
async def leaderboards_rank(
|
|
112
|
+
self,
|
|
113
|
+
mapname: Any,
|
|
114
|
+
playerid: str,
|
|
115
|
+
mode: str = "score",
|
|
116
|
+
difficulty: str = "NORMAL",
|
|
117
|
+
*,
|
|
118
|
+
beta: bool = False,
|
|
119
|
+
) -> Score:
|
|
120
|
+
"""
|
|
121
|
+
Retrieves a Score of the given player.
|
|
122
|
+
A valid playerid needs to be specified.
|
|
123
|
+
"""
|
|
124
|
+
self._kwarg_check(
|
|
125
|
+
mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
|
|
126
|
+
)
|
|
127
|
+
payload = await self._post(
|
|
128
|
+
"getLeaderboardsRank",
|
|
129
|
+
data={
|
|
130
|
+
"gamemode": "BASIC_LEVELS",
|
|
131
|
+
"difficulty": difficulty,
|
|
132
|
+
"playerid": playerid,
|
|
133
|
+
"mapname": str(mapname),
|
|
134
|
+
"mode": mode,
|
|
135
|
+
},
|
|
136
|
+
beta=beta,
|
|
137
|
+
)
|
|
138
|
+
return Score.from_payload(
|
|
139
|
+
"leaderboards_rank", mapname, mode, difficulty, playerid, payload
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@async_expiring_cache()
|
|
143
|
+
async def leaderboards(
|
|
144
|
+
self,
|
|
145
|
+
mapname: Any,
|
|
146
|
+
playerid: Optional[str] = None,
|
|
147
|
+
mode: str = "score",
|
|
148
|
+
difficulty: str = "NORMAL",
|
|
149
|
+
*,
|
|
150
|
+
beta: bool = False,
|
|
151
|
+
) -> Leaderboard:
|
|
152
|
+
"""
|
|
153
|
+
Retrieves a Leaderboard.
|
|
154
|
+
The leaderboard contains the top 200 scores of the specified map.
|
|
155
|
+
"""
|
|
156
|
+
self._kwarg_check(
|
|
157
|
+
mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
payload = await self._post(
|
|
161
|
+
"getLeaderboards",
|
|
162
|
+
data={
|
|
163
|
+
"gamemode": "BASIC_LEVELS",
|
|
164
|
+
"difficulty": difficulty,
|
|
165
|
+
"playerid": playerid,
|
|
166
|
+
"mapname": str(mapname),
|
|
167
|
+
"mode": mode,
|
|
168
|
+
},
|
|
169
|
+
beta=beta,
|
|
170
|
+
)
|
|
171
|
+
lb = Leaderboard.from_payload(
|
|
172
|
+
"leaderboards", mapname, mode, difficulty, playerid, payload
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return lb
|
|
176
|
+
|
|
177
|
+
@async_expiring_cache()
|
|
178
|
+
async def runtime_leaderboards(
|
|
179
|
+
self,
|
|
180
|
+
mapname: Any,
|
|
181
|
+
playerid: str,
|
|
182
|
+
mode: str = "score",
|
|
183
|
+
difficulty: str = "NORMAL",
|
|
184
|
+
*,
|
|
185
|
+
beta: bool = False,
|
|
186
|
+
) -> Leaderboard:
|
|
187
|
+
"""
|
|
188
|
+
Retrieves a Runtime Leaderboard (The one displayed top right in-game).
|
|
189
|
+
A valid playerid needs to be specified.
|
|
190
|
+
The leaderboard contains the top 200 scores and one Score for each top% of the specified map.
|
|
191
|
+
"""
|
|
192
|
+
self._kwarg_check(
|
|
193
|
+
mapname=mapname, playerid=playerid, mode=mode, difficulty=difficulty
|
|
194
|
+
)
|
|
195
|
+
payload = await self._post(
|
|
196
|
+
"getRuntimeLeaderboards",
|
|
197
|
+
data={
|
|
198
|
+
"gamemode": "BASIC_LEVELS",
|
|
199
|
+
"difficulty": difficulty,
|
|
200
|
+
"playerid": playerid,
|
|
201
|
+
"mapname": str(mapname),
|
|
202
|
+
"mode": mode,
|
|
203
|
+
},
|
|
204
|
+
beta=beta,
|
|
205
|
+
)
|
|
206
|
+
return Leaderboard.from_payload(
|
|
207
|
+
"runtime_leaderboards", mapname, mode, difficulty, playerid, payload
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
@async_expiring_cache()
|
|
211
|
+
async def skill_point_leaderboard(
|
|
212
|
+
self, playerid: Optional[str] = None, *, beta: bool = False
|
|
213
|
+
) -> Leaderboard:
|
|
214
|
+
"""
|
|
215
|
+
Retrieves the Skill Point Leaderboard.
|
|
216
|
+
The leaderboard contains the top 3 skill point owners (looking at you, Eupho!).
|
|
217
|
+
"""
|
|
218
|
+
if playerid is not None:
|
|
219
|
+
self._kwarg_check(playerid=playerid)
|
|
220
|
+
|
|
221
|
+
payload = await self._post(
|
|
222
|
+
"getSkillPointLeaderboard", data={"playerid": playerid}, beta=beta
|
|
223
|
+
)
|
|
224
|
+
lb = Leaderboard.from_payload(
|
|
225
|
+
"skill_point_leaderboard", "SP", "score", "NORMAL", playerid, payload
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return lb
|
|
229
|
+
|
|
230
|
+
@async_expiring_cache()
|
|
231
|
+
async def daily_quest_leaderboards(
|
|
232
|
+
self,
|
|
233
|
+
date: Union[datetime.datetime, str, None] = None,
|
|
234
|
+
playerid: Optional[str] = None,
|
|
235
|
+
*,
|
|
236
|
+
beta: bool = False,
|
|
237
|
+
warning: bool = True,
|
|
238
|
+
) -> Leaderboard:
|
|
239
|
+
"""
|
|
240
|
+
Retrieves the Daily Quest Leaderboard for the given date.
|
|
241
|
+
If an invalid or no date is provided, the date will be set to the current date.
|
|
242
|
+
You may disable the invalid date warning by setting the warning param to False.
|
|
243
|
+
The leaderboard contains the top 200 DQ players of the given date.
|
|
244
|
+
"""
|
|
245
|
+
if date is None:
|
|
246
|
+
date = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")
|
|
247
|
+
elif isinstance(date, datetime.datetime):
|
|
248
|
+
date = date.strftime("%Y-%m-%d")
|
|
249
|
+
else:
|
|
250
|
+
try:
|
|
251
|
+
datetime.datetime.strptime(date, "%Y-%m-%d")
|
|
252
|
+
except ValueError:
|
|
253
|
+
if warning is True:
|
|
254
|
+
_log.warning(
|
|
255
|
+
"Invalid date in daily_quest_leaderboards (Use YYYY-MM-DD format): %s",
|
|
256
|
+
date,
|
|
257
|
+
)
|
|
258
|
+
date = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")
|
|
259
|
+
|
|
260
|
+
if playerid is not None:
|
|
261
|
+
self._kwarg_check(playerid=playerid)
|
|
262
|
+
|
|
263
|
+
payload = await self._post(
|
|
264
|
+
"getDailyQuestLeaderboards",
|
|
265
|
+
data={"date": date, "playerid": playerid},
|
|
266
|
+
beta=beta,
|
|
267
|
+
)
|
|
268
|
+
lb = Leaderboard.from_payload(
|
|
269
|
+
"daily_quest_leaderboards",
|
|
270
|
+
"DQ",
|
|
271
|
+
"score",
|
|
272
|
+
"NORMAL",
|
|
273
|
+
playerid,
|
|
274
|
+
payload,
|
|
275
|
+
date=date,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return lb
|
|
279
|
+
|
|
280
|
+
@async_expiring_cache()
|
|
281
|
+
async def seasonal_leaderboard(self, beta: bool = False) -> Leaderboard:
|
|
282
|
+
"""
|
|
283
|
+
Retrieves the season Leaderboard.
|
|
284
|
+
The leaderboard contains the top 100 scores in the season.
|
|
285
|
+
This coroutine never takes arguments.
|
|
286
|
+
"""
|
|
287
|
+
url = _base_url(beta) + "xdx/?url=seasonal_leaderboard"
|
|
288
|
+
_log.info("Sending GET request to %s", url)
|
|
289
|
+
|
|
290
|
+
r = await self._session.get(url=url)
|
|
291
|
+
try:
|
|
292
|
+
r.raise_for_status()
|
|
293
|
+
except aiohttp.ClientResponseError:
|
|
294
|
+
raise APIError("Bad Gateway.")
|
|
295
|
+
|
|
296
|
+
seasonal = bs4.BeautifulSoup(await r.text(), features="lxml")
|
|
297
|
+
|
|
298
|
+
# fmt: off
|
|
299
|
+
season = int(seasonal.select_one('label[i18n="season_formatted"]')['i18nf'].replace('["', '').replace('"]', '')) # type: ignore
|
|
300
|
+
player_count = int(seasonal.select('label[i18n="player_count_formatted"]')[
|
|
301
|
+
0]['i18nf'].replace('["', '').replace('"]', '').replace(',', '')) # type: ignore
|
|
302
|
+
lb = Leaderboard.from_payload(
|
|
303
|
+
'seasonal_leaderboard', 'season', 'score', 'NORMAL', None, {
|
|
304
|
+
'status': 'success',
|
|
305
|
+
'player': {'total': player_count},
|
|
306
|
+
'leaderboards': [
|
|
307
|
+
{
|
|
308
|
+
'playerid': seasonal.select('label[color="LIGHT_BLUE:P300"]')[x]['click'].split('id=')[1], # type: ignore
|
|
309
|
+
'nickname': seasonal.select('label[color="LIGHT_BLUE:P300"]')[x].text,
|
|
310
|
+
'score': seasonal.select('label[nowrap="true"][text-align="right"]')[x].text.replace(',', '')
|
|
311
|
+
} for x in range(len(seasonal.select('div[x="90"]')))
|
|
312
|
+
]
|
|
313
|
+
}, season=season
|
|
314
|
+
)
|
|
315
|
+
# fmt: on
|
|
316
|
+
|
|
317
|
+
return lb
|
|
318
|
+
|
|
319
|
+
def _parse_player(self, content: str, playerid: str, beta: bool) -> Player:
|
|
320
|
+
data = bs4.BeautifulSoup(content, features="lxml")
|
|
321
|
+
t: Dict[str, Any] = {}
|
|
322
|
+
t["playerid"] = playerid
|
|
323
|
+
t["beta"] = beta
|
|
324
|
+
t["nickname"] = data.select_one("label:not([i18n])").text # type: ignore
|
|
325
|
+
totals = data.select_one('div[width="522"][height="140"][align="center"]')
|
|
326
|
+
if totals is None:
|
|
327
|
+
t.update({"total_score": 0, "total_rank": 0, "total_top": 0})
|
|
328
|
+
else:
|
|
329
|
+
totals = totals.select("label")
|
|
330
|
+
if len(totals) >= 4:
|
|
331
|
+
t["total_score"] = try_int(totals[1].text.replace(",", ""))
|
|
332
|
+
t["total_rank"] = try_int(totals[2].text.replace(",", ""))
|
|
333
|
+
t["total_top"] = totals[3].text.replace("- Top ", "")
|
|
334
|
+
else:
|
|
335
|
+
t.update({"total_score": 0, "total_rank": 0, "total_top": "0%"})
|
|
336
|
+
comments = data.findAll(text=lambda text: isinstance(text, bs4.Comment))
|
|
337
|
+
for x in comments:
|
|
338
|
+
if "Level:" in x:
|
|
339
|
+
t["level"] = int(x.split(">")[3].split("<")[0])
|
|
340
|
+
break
|
|
341
|
+
else:
|
|
342
|
+
t["level"] = 1
|
|
343
|
+
xp_data = data.select_one('div[width="330"][height="64"]')
|
|
344
|
+
if xp_data is None:
|
|
345
|
+
raise BadArgument("Invalid playerid: " + playerid)
|
|
346
|
+
xp_data = xp_data.select_one("label").text.split(" / ") # type: ignore
|
|
347
|
+
t["xp"] = int(xp_data[0])
|
|
348
|
+
t["xp_max"] = int(xp_data[1])
|
|
349
|
+
season_xp_data = data.select_one(
|
|
350
|
+
'div[width="530"][align="center"][height="64"][pad-bottom="10"]'
|
|
351
|
+
)
|
|
352
|
+
if season_xp_data is None:
|
|
353
|
+
t["season_xp"] = 0
|
|
354
|
+
t["season_xp_max"] = 500
|
|
355
|
+
t["season_level"] = 1
|
|
356
|
+
else:
|
|
357
|
+
season_xp_borders = season_xp_data.select_one("label").text.split(" / ") # type: ignore
|
|
358
|
+
t["season_xp"] = int(season_xp_borders[0])
|
|
359
|
+
t["season_xp_max"] = int(season_xp_borders[1])
|
|
360
|
+
season_level_data = season_xp_data.select_one(
|
|
361
|
+
'div[x="466"][width="64"][height="64"]'
|
|
362
|
+
)
|
|
363
|
+
if season_level_data is None:
|
|
364
|
+
t["season_level"] = 1
|
|
365
|
+
else:
|
|
366
|
+
t["season_level"] = int(season_level_data["data"].split(":")[1]) # type: ignore
|
|
367
|
+
|
|
368
|
+
t["levels"] = {}
|
|
369
|
+
for x in data.select('div[width="800"][height="40"]')[1:]:
|
|
370
|
+
level_data = x.select("label")
|
|
371
|
+
level = level_data[0].text
|
|
372
|
+
if not x.select_one('label[i18n="not_ranked"]'):
|
|
373
|
+
rank = int(level_data[2].text.replace(",", ""))
|
|
374
|
+
score = int(level_data[1].text.replace(",", ""))
|
|
375
|
+
total = int(level_data[3].text.replace("/ ", "").replace(",", ""))
|
|
376
|
+
top = level_data[-1].text
|
|
377
|
+
else:
|
|
378
|
+
rank, score, total, top = 0, 0, 0, "-%"
|
|
379
|
+
t["levels"][level] = Score(
|
|
380
|
+
"player",
|
|
381
|
+
level,
|
|
382
|
+
"score",
|
|
383
|
+
"NORMAL",
|
|
384
|
+
playerid,
|
|
385
|
+
rank=rank,
|
|
386
|
+
score=score,
|
|
387
|
+
total=total,
|
|
388
|
+
top=top,
|
|
389
|
+
level=t["level"],
|
|
390
|
+
nickname=t["nickname"],
|
|
391
|
+
)
|
|
392
|
+
t["badges"] = {}
|
|
393
|
+
icos = [
|
|
394
|
+
"daily-game",
|
|
395
|
+
"invited-players",
|
|
396
|
+
"killed-enemies",
|
|
397
|
+
"mined-resources",
|
|
398
|
+
"skillful",
|
|
399
|
+
"of-merit",
|
|
400
|
+
"beta-tester-season-2",
|
|
401
|
+
f"high-leveled-{t['level'] // 10 if t['level'] < 100 else 10}",
|
|
402
|
+
]
|
|
403
|
+
for x in data.select('div[width="80"][height="80"]'):
|
|
404
|
+
rar: str = x.select("img")[0]["src"].split("bg-")[1] # type: ignore
|
|
405
|
+
if rar in [
|
|
406
|
+
"not-received",
|
|
407
|
+
"common",
|
|
408
|
+
"rare",
|
|
409
|
+
"very-rare",
|
|
410
|
+
"epic",
|
|
411
|
+
"legendary",
|
|
412
|
+
"supreme",
|
|
413
|
+
"artifact",
|
|
414
|
+
]:
|
|
415
|
+
ico: str = x.select("img")[1]["src"].split("icon-")[1] # type: ignore
|
|
416
|
+
if (
|
|
417
|
+
ico
|
|
418
|
+
in icos
|
|
419
|
+
+ [
|
|
420
|
+
"youtube-author-" + rar,
|
|
421
|
+
f"season-level-{rar}-2",
|
|
422
|
+
]
|
|
423
|
+
or ico[:8] == "season-1"
|
|
424
|
+
):
|
|
425
|
+
col: str = x.select("img")[-1]["color"] # type: ignore
|
|
426
|
+
t["badges"][ico] = (rar, col)
|
|
427
|
+
labels = data.select('table[width="800"][align="center"]')[-1].select("label")
|
|
428
|
+
replays = labels[-3].string.split(" ") # type: ignore
|
|
429
|
+
t["replays"] = 0 if len(replays) != 4 else int(replays[3])
|
|
430
|
+
issues = labels[-2].string.split(" ") # type: ignore
|
|
431
|
+
t["issues"] = 0 if len(issues) != 6 else int(issues[0][3:])
|
|
432
|
+
sp = list(labels[-1].string.split("ned ")[1].split(" ")) # type: ignore
|
|
433
|
+
day = sp[0][:-2] if len(sp[0][:2]) == 2 else "0" + sp[0][:2]
|
|
434
|
+
t["created_at"] = datetime.datetime.strptime(
|
|
435
|
+
day + " " + sp[-2] + " " + sp[-1], "%d %B %Y"
|
|
436
|
+
).strftime("%Y-%m-%d")
|
|
437
|
+
|
|
438
|
+
return Player(**t)
|
|
439
|
+
|
|
440
|
+
@async_expiring_cache()
|
|
441
|
+
async def player(self, playerid: str, beta: bool = False) -> Player:
|
|
442
|
+
"""
|
|
443
|
+
Retrieves the Player.
|
|
444
|
+
A valid playerid needs to be specified.
|
|
445
|
+
"""
|
|
446
|
+
self._kwarg_check(playerid=playerid)
|
|
447
|
+
url = _base_url(beta) + "xdx/index.php?url=profile/view&id=" + playerid
|
|
448
|
+
_log.info("Sending GET request to %s", url)
|
|
449
|
+
|
|
450
|
+
r = await self._session.get(url=url)
|
|
451
|
+
try:
|
|
452
|
+
r.raise_for_status()
|
|
453
|
+
except aiohttp.ClientResponseError:
|
|
454
|
+
raise APIError("Bad Gateway.")
|
|
455
|
+
|
|
456
|
+
loop = asyncio.get_event_loop()
|
|
457
|
+
try:
|
|
458
|
+
return await loop.run_in_executor(
|
|
459
|
+
None, self._parse_player, await r.text(), playerid, beta
|
|
460
|
+
)
|
|
461
|
+
except Exception as exc:
|
|
462
|
+
raise BadArgument("Invalid playerid: " + playerid) from exc
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
"InfinitodeError",
|
|
5
|
+
"APIError",
|
|
6
|
+
"BadArgument",
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InfinitodeError(Exception):
|
|
11
|
+
"""Base Infinitode Error."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APIError(InfinitodeError):
|
|
17
|
+
"""Error directly related to the communication with the API."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BadArgument(InfinitodeError):
|
|
23
|
+
"""Error raised when an invalid argument is passed."""
|
|
24
|
+
|
|
25
|
+
pass
|