osudroid-api-wrapper 0.0.1__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.
File without changes
@@ -0,0 +1,112 @@
1
+ """Used https://github.com/xjunko/Yuzumi/blob/main/objects/beatmap.py as a reference
2
+ Original license from xjunko/Yuzumi:
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2021 FireRedz
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ """
26
+
27
+ import dotenv
28
+ import os
29
+ import requests
30
+
31
+ dotenv.load_dotenv()
32
+ key = os.getenv("OSU_API_KEY")
33
+
34
+
35
+ class Beatmap:
36
+ def __init__(self):
37
+ self.beatmap_id: int = None
38
+ self.beatmapset_id: int = None
39
+ self.md5: str = None
40
+ self.artist: str = None
41
+ self.title: str = None
42
+ self.version: str = None
43
+ self.creator: str = None
44
+ self.ar: float = None
45
+ self.od: float = None
46
+ self.hp: float = None
47
+ self.cs: float = None
48
+ self.bpm: float = None
49
+ self.star: float = None
50
+ self.length: int = None
51
+
52
+ @classmethod
53
+ def get_beatmap(cls, beatmap_id: int = None, md5: str = None) -> 'Beatmap':
54
+ beatmap = cls()
55
+ url = "https://old.ppy.sh/api/get_beatmaps"
56
+ if beatmap_id is not None:
57
+ params = {
58
+ "k": key,
59
+ "b": beatmap_id
60
+ }
61
+ response = requests.get(url, params=params)
62
+ data = response.json()
63
+ data = data[0]
64
+
65
+ elif md5 is not None:
66
+ params = {
67
+ "k": key,
68
+ "h": md5
69
+ }
70
+ response = requests.get(url, params=params)
71
+ data = response.json()
72
+ if len(data) == 0:
73
+ return None
74
+ data = data[0]
75
+
76
+ beatmap.beatmap_id = int(data["beatmap_id"])
77
+ beatmap.beatmapset_id = int(data["beatmapset_id"])
78
+ beatmap.md5 = data["file_md5"]
79
+ beatmap.artist = data["artist"]
80
+ beatmap.title = data["title"]
81
+ beatmap.version = data["version"]
82
+ beatmap.creator = data["creator"]
83
+ beatmap.ar = float(data["diff_approach"])
84
+ beatmap.od = float(data["diff_overall"])
85
+ beatmap.hp = float(data["diff_drain"])
86
+ beatmap.cs = float(data["diff_size"])
87
+ beatmap.bpm = float(data["bpm"])
88
+ beatmap.star = float(data["difficultyrating"])
89
+ beatmap.length = int(data["total_length"])
90
+
91
+ return beatmap
92
+
93
+
94
+ @property
95
+ def to_dict(self):
96
+ return {
97
+ "beatmap_id": self.beatmap_id,
98
+ "beatmapset_id": self.beatmapset_id,
99
+ "md5": self.md5,
100
+ "artist": self.artist,
101
+ "title": self.title,
102
+ "version": self.version,
103
+ "creator": self.creator,
104
+ "ar": self.ar,
105
+ "od": self.od,
106
+ "hp": self.hp,
107
+ "cs": self.cs,
108
+ "bpm": self.bpm,
109
+ "star": self.star,
110
+ "length": self.length
111
+
112
+ }
@@ -0,0 +1,33 @@
1
+ class Mods:
2
+ def __init__(self, mods=None):
3
+ self.mods = mods or []
4
+ self.speed_multiplier = None
5
+
6
+ @classmethod
7
+ def from_droid_site(cls, mods: str):
8
+ mod_abbreviations = {
9
+ 'None': 'NM', 'NoFail': 'NF', 'Easy': 'EZ', 'HalfTime': 'HT',
10
+ 'Hidden': 'HD', 'HardRock': 'HR', 'DoubleTime': 'DT', 'Flashlight': 'FL',
11
+ 'Precise': 'PR', 'SuddenDeath': 'SD', 'Perfect': 'PF', 'NightCore': 'NC',
12
+ }
13
+ return cls([mod_abbreviations.get(mod, mod) for mod in mods.split(",")])
14
+
15
+ @classmethod
16
+ def from_droid_replay(cls, mods: str):
17
+ mod_mapping = {
18
+ "MOD_NOFAIL": "NF", "MOD_EASY": "EZ", "MOD_HIDDEN": "HD",
19
+ "MOD_HARDROCK": "HR", "MOD_SUDDENDEATH": "SD", "MOD_DOUBLETIME": "DT",
20
+ "MOD_RELAX": "RX", "MOD_HALFTIME": "HT", "MOD_NIGHTCORE": "NC",
21
+ "MOD_FLASHLIGHT": "FL", "MOD_SCOREV2": "V2", "MOD_AUTOPILOT": "AP",
22
+ "MOD_AUTO": "AT", "MOD_PRECISE": "PR", "MOD_REALLYEASY": "REZ",
23
+ "MOD_SMALLCIRCLES": "SC", "MOD_PERFECT": "PF", "MOD_SUDDENDEATH": "SU",
24
+ }
25
+ return cls([mod_mapping.get(mod, mod) for mod in mods.split(",")])
26
+
27
+ @classmethod
28
+ def from_droid_api(cls, mods: dict):
29
+ instance = cls(list(mods))
30
+ instance.speed_multiplier = next((mod for mod in mods if mod.startswith("x")), None)
31
+ return instance
32
+
33
+
@@ -0,0 +1,114 @@
1
+ import bs4
2
+ import datetime
3
+ import requests
4
+
5
+
6
+ class Player:
7
+ def __init__(self):
8
+ self.uid: int = 0
9
+ self.username: str = None
10
+ self.country: str = None
11
+ self.score_rank: int = 0
12
+ self.pp_rank: int = 0
13
+ self.country_rank: int = 0
14
+ self.pp: float = 0.0
15
+ self.total_score: int = 0
16
+ self.playcount: int = 0
17
+ self.accuracy: float = 0.0
18
+ self.registered_on: int = 0
19
+ self.last_login: int = 0
20
+
21
+ def __parse(self, soup):
22
+ text_content = soup.get_text()
23
+ country_index = text_content.find("Location:")
24
+ self.country = text_content[country_index +
25
+ len("Location:"):].splitlines()[0].strip()
26
+ score_rank_index = text_content.find("Score Rank:")
27
+ self.score_rank = int(
28
+ text_content[score_rank_index + len("Score Rank:"):].splitlines()[0].strip().replace("#", ""))
29
+ pp_rank_index = text_content.find("PP Rank:")
30
+ self.pp_rank = int(
31
+ text_content[pp_rank_index + len("PP Rank:"):].splitlines()[0].strip().replace("#", ""))
32
+ self.username = soup.select_one(
33
+ "html body main div nav div div div:nth-of-type(3) div:nth-of-type(1) a:nth-of-type(1)").text
34
+
35
+ table = soup.find("table")
36
+ if table:
37
+ rows = table.find_all("tr")
38
+ for row in rows:
39
+ columns = row.find_all("td")
40
+ if columns:
41
+ if columns[0].text == "Performance Points":
42
+ self.pp = float(
43
+ columns[1].text.replace(
44
+ ",", "").replace(
45
+ "pp", ""))
46
+ elif columns[0].text == "Ranked Score":
47
+ self.total_score = int(
48
+ columns[1].text.replace(",", ""))
49
+ elif columns[0].text == "Play Count":
50
+ self.playcount = int(columns[1].text.replace(",", ""))
51
+ elif columns[0].text == "Hit Accuracy":
52
+ self.accuracy = float(columns[1].text.replace("%", ""))
53
+ return self
54
+
55
+ @classmethod
56
+ def _parse_from_bsoup(cls, soup) -> 'Player':
57
+ player = cls()
58
+ player.__parse(soup)
59
+ return player
60
+
61
+ @classmethod
62
+ def _from_api_response(cls, data) -> 'Player':
63
+ player = cls()
64
+
65
+ player.uid = data['UserId']
66
+ player.username = data['Username']
67
+ player.pp_rank = int(data['GlobalRank'])
68
+ player.country_rank = int(data['CountryRank'])
69
+ player.total_score = int(data['OverallScore'])
70
+ player.pp = int(data['OverallPP'])
71
+ player.playcount = int(data['OverallPlaycount'])
72
+ player.accuracy = float(data['OverallAccuracy']) * 100
73
+ player.registered_on = datetime.datetime.strptime(
74
+ data['Registered'], "%Y-%m-%dT%H:%M:%S.%fZ").timestamp()
75
+ player.country = data['Region']
76
+ return player
77
+
78
+ @classmethod
79
+ def from_droid_site(cls, uid: int) -> 'Player':
80
+ player = cls()
81
+ url = f"https://osudroid.moe/profile.php?uid={uid}"
82
+ response = requests.get(url)
83
+ player.uid = uid
84
+ soup = bs4.BeautifulSoup(response.text, 'html.parser')
85
+ player.__parse(soup)
86
+ return player
87
+
88
+
89
+ @classmethod
90
+ def from_api(cls, uid: int = None, username: str = None) -> 'Player':
91
+ player = cls()
92
+ if username:
93
+ url = f"https://new.osudroid.moe/apitest/profile-username/{username}"
94
+ elif uid:
95
+ url = f"https://new.osudroid.moe/apitest/profile-uid/{uid}"
96
+ response = requests.get(url)
97
+ data = response.json()
98
+ player = cls._from_api_response(data)
99
+ return player
100
+
101
+ @property
102
+ def to_dict(self):
103
+ return {
104
+ 'uid': self.uid,
105
+ 'username': self.username,
106
+ 'country': self.country,
107
+ 'score_rank': self.score_rank,
108
+ 'pp_rank': self.pp_rank,
109
+ 'pp': self.pp,
110
+ 'total_score': self.total_score,
111
+ 'playcount': self.playcount,
112
+ 'accuracy': self.accuracy
113
+ }
114
+
@@ -0,0 +1,259 @@
1
+ """Partial rewrite of the rian8337/osu-droid-replay-analyzer on python
2
+
3
+ https://github.com/Rian8337/osu-droid-module/tree/master/packages/osu-droid-replay-analyzer
4
+
5
+ MIT License
6
+
7
+ Copyright (c) 2021 Rian8337
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ """
27
+
28
+ import javaobj.v2
29
+ import javaobj.v2.transformers
30
+ from stream_unzip import stream_unzip
31
+ import javaobj
32
+ import struct
33
+ import io
34
+ from classes.replay_data.movementtype import MovementType
35
+ from classes.replay_data.cursordata import CursorData
36
+ from classes.replay_data.hitresult import HitResult
37
+ from classes.replay_data.replayobjectdata import ReplayObjectData
38
+
39
+
40
+ class Replay:
41
+ def __init__(self):
42
+ self.replay_file = None
43
+ self.replay_obj = None
44
+ self.map: str = None
45
+ self.file_name: str = None
46
+ self.md5: str = None
47
+ self.unix_date: int = 0
48
+ self.hit300k: int = 0
49
+ self.hit300: int = 0
50
+ self.hit100k: int = 0
51
+ self.hit100: int = 0
52
+ self.hit50: int = 0
53
+ self.hit0: int = 0
54
+ self.score: int = 0
55
+ self.combo: int = 0
56
+ self.username: str = None
57
+ self.parsed_mods: list = []
58
+ self.converted_mods: list = []
59
+ self.__buffer_offset: int = 0
60
+ self.cursor_data: list = []
61
+ self.hit_result_data: list = []
62
+
63
+ def __zipped_chunks(self, filename):
64
+ with open(filename, "rb") as f:
65
+ while chunk := f.read(65536):
66
+ yield chunk
67
+
68
+
69
+
70
+ def __read_byte(self, replay_data):
71
+ replay_data.seek(self.__buffer_offset)
72
+ self.__buffer_offset += 1
73
+ return struct.unpack(">b", replay_data.read(1))[0]
74
+
75
+ def __read_short(self, replay_data):
76
+ replay_data.seek(self.__buffer_offset)
77
+ self.__buffer_offset += 2
78
+ return struct.unpack(">h", replay_data.read(2))[0]
79
+
80
+ def __read_int(self, replay_data):
81
+ replay_data.seek(self.__buffer_offset)
82
+ self.__buffer_offset += 4
83
+ return struct.unpack(">i", replay_data.read(4))[0]
84
+
85
+ def __read_float(self, replay_data):
86
+ replay_data.seek(self.__buffer_offset)
87
+ self.__buffer_offset += 4
88
+ return struct.unpack(">f", replay_data.read(4))[0]
89
+
90
+ def __droid_replay_mods_to_std(self):
91
+ mod_mapping = {
92
+ "MOD_NOFAIL": "NF",
93
+ "MOD_EASY": "EZ",
94
+ "MOD_HIDDEN": "HD",
95
+ "MOD_HARDROCK": "HR",
96
+ "MOD_SUDDENDEATH": "SD",
97
+ "MOD_DOUBLETIME": "DT",
98
+ "MOD_RELAX": "RX",
99
+ "MOD_HALFTIME": "HT",
100
+ "MOD_NIGHTCORE": "NC",
101
+ "MOD_FLASHLIGHT": "FL",
102
+ "MOD_SCOREV2": "V2",
103
+ "MOD_AUTOPILOT": "AP",
104
+ "MOD_AUTO": "AT",
105
+ "MOD_PRECISE": "PR",
106
+ "MOD_REALLYEASY": "REZ",
107
+ "MOD_SMALLCIRCLES": "SC",
108
+ "MOD_PERFECT": "PF",
109
+ "MOD_SUDDENDEATH": "SU",
110
+ }
111
+
112
+ for mod in self.parsed_mods:
113
+ if mod in mod_mapping:
114
+ self.converted_mods.append(mod_mapping[mod])
115
+ return self.converted_mods
116
+
117
+ def __parse_movement_data(self, replay_data):
118
+ replay_data = io.BytesIO(replay_data)
119
+ size = self.__read_int(replay_data)
120
+
121
+ # copypasta begins here
122
+ for i in range(size):
123
+ move_size = self.__read_int(replay_data)
124
+ time = []
125
+ x = []
126
+ y = []
127
+ id = []
128
+ for j in range(0, move_size):
129
+ time.append(self.__read_int(replay_data))
130
+ id.append(time[j] & 3)
131
+ time[j] >>= 2
132
+ if id[j] != MovementType.UP.value:
133
+ if self.version >= 5:
134
+ x.append(self.__read_float(replay_data))
135
+ y.append(self.__read_float(replay_data))
136
+ else:
137
+ x.append(self.__read_short(replay_data))
138
+ y.append(self.__read_short(replay_data))
139
+ else:
140
+ x.append(-1)
141
+ y.append(-1)
142
+
143
+ cursor_data = CursorData({
144
+ "size": move_size,
145
+ "time": time,
146
+ "x": x,
147
+ "y": y,
148
+ "id": id
149
+ }).to_dict
150
+
151
+ self.cursor_data.append(cursor_data['occurrence_groups'])
152
+
153
+ def __parse_hitresult_data(self, replay_data):
154
+ replay_data = io.BytesIO(replay_data)
155
+
156
+ hitobject_data_lenght = self.__read_int(replay_data)
157
+
158
+ for i in range(hitobject_data_lenght):
159
+ replay_object_data = ReplayObjectData(
160
+ accuracy=0.0,
161
+ tickset=[],
162
+ result=HitResult.MISS
163
+ )
164
+ replay_object_data.accuracy = self.__read_short(replay_data)
165
+ len = self.__read_byte(replay_data)
166
+
167
+ if len > 0:
168
+ bytes = []
169
+ for j in range(len):
170
+ bytes.append(self.__read_byte(replay_data))
171
+ for j in range(len * 8):
172
+ replay_object_data.tickset.append((bytes[len - round(j/8) - 1] & (1 << round(j % 8))) != 0)
173
+
174
+ replay_object_data.result = HitResult(self.__read_byte(replay_data))
175
+ self.hit_result_data.append(replay_object_data.to_dict)
176
+
177
+
178
+
179
+ def load(self, filename):
180
+ self.replay_file = filename
181
+
182
+ for file_name, file_size, unzipped_chunks in stream_unzip(
183
+ self.__zipped_chunks(filename)):
184
+
185
+ data_buffer = io.BytesIO()
186
+
187
+ try:
188
+ for chunk in unzipped_chunks:
189
+ data_buffer.write(chunk)
190
+ except Exception as e:
191
+ break
192
+
193
+ self.replay_obj = javaobj.v2.loads(data_buffer.getvalue())
194
+
195
+ for fields in self.replay_obj[0].to_dict['field_data'].values():
196
+ for field, value in fields.items():
197
+ if field.name == 'version':
198
+ self.version = value
199
+
200
+ self.map = self.replay_obj[1].value
201
+ self.file_name = self.replay_obj[2].value
202
+ self.md5 = self.replay_obj[3].value
203
+
204
+ if self.version >= 3:
205
+ (self.unix_date, self.hit300k, self.hit300,
206
+ self.hit100k, self.hit100, self.hit50,
207
+ self.hit0, self.score, self.combo) = struct.unpack(">Qiiiiiiii", io.BytesIO(self.replay_obj[4].data).read(40))
208
+ self.username = self.replay_obj[5].value
209
+
210
+ for field in self.replay_obj[6].to_dict['field_data'].values():
211
+ for field, value in field.items():
212
+ if field.name == "elements":
213
+ for element in value:
214
+ self.parsed_mods.append(element.value)
215
+ break
216
+
217
+ self.__droid_replay_mods_to_std()
218
+
219
+ if self.version >= 4:
220
+ modifiers = self.replay_obj[7].value.split("|")
221
+ for modifier in modifiers:
222
+ if modifier.startswith("AR"):
223
+ self.force_ar = float(modifier.replace("AR", ""))
224
+ if modifier.startswith("CS"):
225
+ self.force_cs = float(modifier.replace("CS", ""))
226
+ if modifier.startswith("OD"):
227
+ self.force_od = float(modifier.replace("OD", ""))
228
+ if modifier.startswith("HP"):
229
+ self.force_hp = float(modifier.replace("HP", ""))
230
+ if modifier.startswith("x"):
231
+ self.speed_multiplier = float(
232
+ modifier.replace("x", "") or 1)
233
+ if modifier.startswith("FLD"):
234
+ self.fl_delay = float(modifier.replace("FLD", "") or 0.12)
235
+
236
+ buffer_index = 0
237
+ if self.version <= 2:
238
+ buffer_index = 4
239
+ elif self.version == 3:
240
+ buffer_index = 7
241
+ elif self.version >= 4:
242
+ buffer_index = 8
243
+
244
+ replay_data = io.BytesIO()
245
+ for i in range(buffer_index, len(self.replay_obj)):
246
+ replay_data.write(self.replay_obj[i].data)
247
+
248
+ self.__parse_movement_data(replay_data.getvalue())
249
+ print(self.__buffer_offset)
250
+ self.__parse_hitresult_data(replay_data.getvalue())
251
+
252
+ def __str__(self):
253
+ string = ""
254
+ for key, value in self.to_dict.items():
255
+ if key == "replay_obj" or key == "replay_file" or key == "cursor_data":
256
+ continue
257
+
258
+ string += f"{key}: {value} "
259
+ return string
@@ -0,0 +1,101 @@
1
+ """
2
+ Rewrite of the CursorData class of rian8337/osu-droid-replay-analyzer
3
+ https://github.com/Rian8337/osu-droid-module/blob/master/packages/osu-droid-replay-analyzer/src/data/CursorData.ts
4
+
5
+ MIT License
6
+
7
+ Copyright (c) 2021 Rian8337
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ """
28
+
29
+ from typing import List, Optional
30
+ from classes.replay_data.cursoroccurrence import CursorOccurrence
31
+ from classes.replay_data.cursoroccurrencegroup import CursorOccurrenceGroup
32
+ from classes.replay_data.movementtype import MovementType
33
+
34
+
35
+ class CursorData:
36
+ def __init__(self, values: dict):
37
+ self.occurrence_groups: List[CursorOccurrenceGroup] = []
38
+ down_occurrence: Optional[CursorOccurrence] = None
39
+ move_occurrences: List[CursorOccurrence] = []
40
+
41
+ for i in range(values["size"]):
42
+ occurrence = CursorOccurrence(
43
+ time=values["time"][i],
44
+ x=values["x"][i],
45
+ y=values["y"][i],
46
+ id=MovementType(values["id"][i]),
47
+ )
48
+
49
+ if occurrence.id == MovementType.DOWN:
50
+ down_occurrence = occurrence
51
+ elif occurrence.id == MovementType.MOVE:
52
+ move_occurrences.append(occurrence)
53
+ elif occurrence.id == MovementType.UP:
54
+ if down_occurrence:
55
+ self.occurrence_groups.append(
56
+ CursorOccurrenceGroup(
57
+ down=down_occurrence,
58
+ moves=move_occurrences,
59
+ up=occurrence,
60
+ )
61
+ )
62
+ down_occurrence = None
63
+ move_occurrences = []
64
+
65
+ # Handle any remaining occurrences
66
+ if down_occurrence and move_occurrences:
67
+ self.occurrence_groups.append(
68
+ CursorOccurrenceGroup(
69
+ down=down_occurrence,
70
+ moves=move_occurrences
71
+ )
72
+ )
73
+
74
+ @property
75
+ def earliest_occurrence_time(self) -> Optional[int]:
76
+ return self.occurrence_groups[0].start_time if self.occurrence_groups else None
77
+
78
+ @property
79
+ def latest_occurrence_time(self) -> Optional[int]:
80
+ return self.occurrence_groups[-1].end_time if self.occurrence_groups else None
81
+
82
+ @property
83
+ def total_occurrences(self) -> int:
84
+ return sum(
85
+ 1 + len(group.moves) + (1 if group.up else 0)
86
+ for group in self.occurrence_groups
87
+ )
88
+
89
+ @property
90
+ def all_occurrences(self) -> List[CursorOccurrence]:
91
+ return [
92
+ occurrence
93
+ for group in self.occurrence_groups
94
+ for occurrence in group.all_occurrences
95
+ ]
96
+
97
+ @property
98
+ def to_dict(self):
99
+ return {
100
+ "occurrence_groups": [group.to_dict for group in self.occurrence_groups]
101
+ }
@@ -0,0 +1,48 @@
1
+ """Rewrite of the CursorOccurrence, Vector classes of rian8337/osu-droid-replay-analyzer
2
+ https://github.com/Rian8337/osu-droid-module/blob/master/packages/osu-droid-replay-analyzer/src/data/CursorOccurrence.ts
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2021 Rian8337
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+
26
+ """
27
+
28
+
29
+ class Vector:
30
+ def __init__(self, x, y):
31
+ self.x = x
32
+ self.y = y
33
+
34
+ @property
35
+ def to_dict(self):
36
+ return {"x": self.x, "y": self.y}
37
+
38
+
39
+ class CursorOccurrence:
40
+ def __init__(self, x, y, time, id):
41
+ self.position = Vector(x, y)
42
+ self.time = time
43
+ self.id = id
44
+
45
+ @property
46
+ def to_dict(self):
47
+ return {"position": self.position.to_dict, "time": self.time,
48
+ "id": self.id}
@@ -0,0 +1,120 @@
1
+ """ Rewrite of the CursorOccurrenceGroup class of rian8337/osu-droid-replay-analyzer
2
+ https://github.com/Rian8337/osu-droid-module/blob/master/packages/osu-droid-replay-analyzer/src/data/CursorOccurrenceGroup.ts
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2021 Rian8337
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ """
26
+
27
+ from typing import List, Optional
28
+ from classes.replay_data.cursoroccurrence import CursorOccurrence
29
+ from classes.replay_data.movementtype import MovementType
30
+
31
+
32
+ class CursorOccurrenceGroup:
33
+ def __init__(self, down: CursorOccurrence,
34
+ moves: List[CursorOccurrence], up: Optional[CursorOccurrence] = None):
35
+ self._down = down
36
+ self._moves = moves
37
+ self.down = down
38
+ self.up = up
39
+
40
+ @property
41
+ def down(self) -> CursorOccurrence:
42
+ return self._down
43
+
44
+ @down.setter
45
+ def down(self, value: CursorOccurrence):
46
+ if value.id != MovementType.DOWN:
47
+ raise TypeError(
48
+ "Attempting to set the down cursor occurrence to one with a different movement type.")
49
+ self._down = value
50
+
51
+ @property
52
+ def moves(self) -> List[CursorOccurrence]:
53
+ return self._moves
54
+
55
+ @property
56
+ def up(self) -> Optional[CursorOccurrence]:
57
+ return self._up
58
+
59
+ @up.setter
60
+ def up(self, value: Optional[CursorOccurrence]):
61
+ if value and value.id != MovementType.UP:
62
+ raise TypeError(
63
+ "Attempting to set the up cursor occurrence to one with a different movement type.")
64
+ self._up = value
65
+
66
+ @property
67
+ def start_time(self) -> int:
68
+ return self._down.time
69
+
70
+ @property
71
+ def end_time(self) -> int:
72
+ return self._up.time if self._up else (
73
+ self._moves[-1].time if self._moves else self._down.time)
74
+
75
+ @property
76
+ def duration(self) -> int:
77
+ return self.end_time - self.start_time
78
+
79
+ @property
80
+ def all_occurrences(self) -> List[CursorOccurrence]:
81
+ cursors = [self._down, *self._moves]
82
+ if self._up:
83
+ cursors.append(self._up)
84
+ return cursors
85
+
86
+ def is_active_at(self, time: int) -> bool:
87
+ return self.start_time <= time <= self.end_time
88
+
89
+ def cursor_at(self, time: int) -> Optional[CursorOccurrence]:
90
+ if not self.is_active_at(time):
91
+ return None
92
+
93
+ if self._down.time == time:
94
+ return self._down
95
+
96
+ if self._up and self._up.time == time:
97
+ return self._up
98
+
99
+ # Бинарный поиск в moves
100
+ l, r = 0, len(self._moves) - 1
101
+
102
+ while l <= r:
103
+ pivot = l + (r - l) // 2
104
+ if self._moves[pivot].time < time:
105
+ l = pivot + 1
106
+ elif self._moves[pivot].time > time:
107
+ r = pivot - 1
108
+ else:
109
+ return self._moves[pivot]
110
+
111
+ # l указывает на первый элемент с time > time, вернём предыдущий
112
+ return self._moves[l - 1] if l > 0 else None
113
+
114
+ @property
115
+ def to_dict(self):
116
+ return {
117
+ "down": self._down.to_dict,
118
+ "moves": [move.to_dict for move in self._moves],
119
+ "up": self._up.to_dict if self._up else None
120
+ }
@@ -0,0 +1,33 @@
1
+ """Rewrite of the HitResult class of rian8337/osu-droid-replay-analyzer
2
+ https://github.com/Rian8337/osu-droid-module/blob/master/packages/osu-droid-replay-analyzer/src/constants/HitResult.ts
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2021 Rian8337
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ """
26
+
27
+ import enum
28
+
29
+ class HitResult(enum.Enum):
30
+ MISS = 1
31
+ MEH = 2
32
+ GOOD = 3
33
+ GREAT = 4
@@ -0,0 +1,37 @@
1
+ """Rewrite of the MovementType class of rian8337/osu-droid-replay-analyzer
2
+ https://github.com/Rian8337/osu-droid-module/blob/master/packages/osu-droid-replay-analyzer/src/constants/MovementType.ts
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2021 Rian8337
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ """
26
+
27
+ import enum
28
+
29
+
30
+ class MovementType(enum.Enum):
31
+ DOWN = 0
32
+ MOVE = 1
33
+ UP = 2
34
+
35
+ @property
36
+ def to_dict(self):
37
+ return self.value
@@ -0,0 +1,38 @@
1
+ """Rewrite of the ReplayObjectData class of rian8337/osu-droid-replay-analyzer
2
+ https://github.com/Rian8337/osu-droid-module/blob/master/packages/osu-droid-replay-analyzer/src/data/ReplayObjectData.ts
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2021 Rian8337
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ """
26
+
27
+ import classes.replay_data.hitresult as hitresult
28
+ from typing import Optional, List
29
+
30
+ class ReplayObjectData:
31
+
32
+ def __init__(self, accuracy:Optional[float], tickset:Optional[List[bool]], result:hitresult.HitResult):
33
+ self.accuracy:float = accuracy
34
+ self.tickset:List[bool] = tickset
35
+ self.result:hitresult.HitResult = result
36
+
37
+
38
+
@@ -0,0 +1,107 @@
1
+ from classes.base.player import Player
2
+ from classes.base.beatmap import Beatmap
3
+ from classes.base.mods import Mods
4
+ import datetime
5
+ from typing import List
6
+
7
+ class Score:
8
+ def __init__(self):
9
+ self.scoreid: int = 0
10
+ self.filename: str = None
11
+ self.h300k: int = 0
12
+ self.h300: int = 0
13
+ self.h100k: int = 0
14
+ self.h100: int = 0
15
+ self.h50: int = 0
16
+ self.h0: int = 0
17
+ self.beatmap: Beatmap = Beatmap()
18
+ self.player: Player = Player()
19
+ self.score: int = 0
20
+ self.combo: int = 0
21
+ self.mods: Mods = Mods()
22
+ self.accuracy: float = 0.0
23
+ self.pp: float = 0.0
24
+ self.date: int = 0
25
+ self.grade: str = None
26
+ self.misses: int = 0
27
+
28
+
29
+
30
+
31
+ @classmethod
32
+ def _parse_from_bsoup(cls, soup, ptype:str = "Recent Plays") -> 'List[Score]':
33
+ scores = []
34
+ divs = soup.find_all('div', style="text-align: center; margin-left: 5px; margin-top: 30px;")
35
+ top_div = next(
36
+ (div for div in divs if div.find('b', style="color: #EB2F96;") and div.find('b').text == ptype),
37
+ None
38
+ )
39
+
40
+ if not top_div:
41
+ return scores
42
+
43
+ top_div = top_div.find_next_sibling()
44
+ for li in top_div.find_all('li', class_='li', style='margin-left: 15px; margin-right: 10px;'):
45
+ score = cls()
46
+ score.grade = li.find('img')['src'].replace('./assets/img/ranking-', '').replace('.png', '')
47
+ div = li.find('div', style='margin-bottom: 15px')
48
+ if div:
49
+ inner_div = div.find('div', style='margin: 0; color: black;')
50
+ small_element = inner_div.find('small', style='margin-left: 50px;')
51
+ details_lines = small_element.get_text(strip=True).split(' / ')
52
+ score.date = datetime.datetime.strptime(details_lines[0], "%Y-%m-%d %H:%M:%S").timestamp()
53
+ score.pp = float(details_lines[1].replace("pp: ", "").replace(",", "")) if details_lines[1].replace("pp: ", "") != "None" else 0.0
54
+ score.score = int(details_lines[2].replace("score:", "").replace(",", ""))
55
+ score.mods = Mods.from_droid_site(details_lines[3].replace("mod: ", ""))
56
+ score.combo = int(details_lines[4].replace("combo:", "").replace("x", ""))
57
+ score.accuracy = float(details_lines[5].replace("accuracy:", "").replace("%", ""))
58
+ score.misses = inner_div.find('small', style='color: #A82C2A;').get_text(strip=True).split(': ')[-1]
59
+ hash_content = inner_div.find('span', style='display:none;').contents[0].split(':')[1].replace('}', '')
60
+ score.beatmap = Beatmap.get_beatmap(md5=hash_content)
61
+ scores.append(score)
62
+ return scores
63
+
64
+
65
+ @classmethod
66
+ def _from_api_response(cls, data) -> 'Score':
67
+ score = cls()
68
+ score.scoreid = data['ScoreId']
69
+ score.filename = data['Filename']
70
+ score.mods = Mods.from_droid_api(data['Mods'])
71
+ score.score = data['MapScore']
72
+ score.combo = data['MapCombo']
73
+ score.grade = data['MapRank']
74
+ score.h300k = data['MapGeki']
75
+ score.h300 = data['MapPerfect']
76
+ score.h100k = data['MapKatu']
77
+ score.h100 = data['MapGood']
78
+ score.h50 = data['MapBad']
79
+ score.h0 = data['MapMiss']
80
+ score.accuracy = data['MapAccuracy'] * 100
81
+ score.pp = data.get('MapPP', 0)
82
+ score.date = datetime.datetime.strptime(data['PlayedDate'], "%Y-%m-%dT%H:%M:%S.%fZ").timestamp()
83
+
84
+ return score
85
+
86
+ @property
87
+ def to_dict(self):
88
+ return {
89
+ 'scoreid': self.scoreid,
90
+ 'filename': self.filename,
91
+ 'h300k': self.h300k,
92
+ 'h300': self.h300,
93
+ 'h100k': self.h100k,
94
+ 'h100': self.h100,
95
+ 'h50': self.h50,
96
+ 'h0': self.h0,
97
+ 'beatmap': self.beatmap.to_dict if self.beatmap else None,
98
+ 'player': self.player.to_dict,
99
+ 'score': self.score,
100
+ 'combo': self.combo,
101
+ 'mods': self.mods.to_dict,
102
+ 'accuracy': self.accuracy,
103
+ 'pp': self.pp,
104
+ 'date': self.date,
105
+ 'grade': self.grade,
106
+ 'misses': self.misses
107
+ }
@@ -0,0 +1,76 @@
1
+ from classes.score import Score
2
+ from classes.base.player import Player
3
+ from typing import List
4
+ import bs4
5
+ import requests
6
+
7
+
8
+ class Profile():
9
+ def __init__(self):
10
+ self.player: Player = Player()
11
+ self.recent_scores = []
12
+ self.top_scores = []
13
+
14
+ def __get_recent_scores(self, soup) -> List[Score]:
15
+ recent_scores = Score._parse_from_bsoup(soup, "Recent Plays")
16
+ player = Player._parse_from_bsoup(soup)
17
+ for score in recent_scores:
18
+ score.player = player
19
+ self.recent_scores = recent_scores
20
+ return recent_scores
21
+
22
+ def __get_top_scores(self, soup) -> List[Score]:
23
+ top_scores = Score._parse_from_bsoup(soup, "Top Plays")
24
+ player = Player._parse_from_bsoup(soup)
25
+ for score in top_scores:
26
+ score.player = player
27
+ self.top_scores = top_scores
28
+ return top_scores
29
+
30
+
31
+ @classmethod
32
+ def from_api(cls, uid: int = None, username: str = None) -> 'Profile':
33
+ profile = cls()
34
+ if username:
35
+ url = f"https://new.osudroid.moe/apitest/profile-username/{username}"
36
+ elif uid:
37
+ url = f"https://new.osudroid.moe/apitest/profile-uid/{uid}"
38
+ resp = requests.get(url)
39
+ data = resp.json()
40
+ profile.player = Player._from_api_response(data)
41
+ for score in data['Last50Scores']:
42
+ score_obj = Score._from_api_response(score)
43
+ score_obj.player = profile.player
44
+ profile.recent_scores.append(score_obj)
45
+ for score in data['Top50Plays']:
46
+ score_obj = Score._from_api_response(score)
47
+ score_obj.player = profile.player
48
+ profile.top_scores.append(score_obj)
49
+ return profile
50
+
51
+
52
+ @classmethod
53
+ def from_droid_site(cls, uid: int) -> 'Profile':
54
+ url = f"https://osudroid.moe/profile.php?uid={uid}"
55
+ profile = cls()
56
+ resp = requests.get(url)
57
+ soup = bs4.BeautifulSoup(resp.text, 'html.parser')
58
+ profile.player = Player._parse_from_bsoup(soup)
59
+ profile.__get_recent_scores(soup)
60
+ profile.__get_top_scores(soup)
61
+ return profile
62
+
63
+
64
+ @property
65
+ def to_dict(self):
66
+ return {
67
+ 'player': self.player.to_dict if self.player else None,
68
+ 'recent_scores': [score.to_dict for score in self.recent_scores],
69
+ 'top_scores': [score.to_dict for score in self.top_scores]
70
+ }
71
+
72
+
73
+
74
+
75
+
76
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 unclem2, Rian8337, xjunko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.2
2
+ Name: osudroid-api-wrapper
3
+ Version: 0.0.1
4
+ Summary: Python api wrapper for osu!droid
5
+ Author-email: unclem <mannringyt11@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 unclem2, Rian8337, xjunko
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/unclem2/osudroid-api-wrapper
29
+ Project-URL: Issues, https://github.com/unclem2/osudroid-api-wrapper/issues
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Operating System :: OS Independent
32
+ Requires-Python: >=3.8
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: requests>=2.26.0
36
+ Requires-Dist: beautifulsoup4>=4.10.0
37
+ Requires-Dist: javaobj-py3>=0.4.4
38
+ Requires-Dist: stream_unzip>=0.0.99
39
+ Requires-Dist: python-dotenv>=1.0.0
@@ -0,0 +1,18 @@
1
+ osudroid-api-wrapper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ osudroid-api-wrapper/classes/replay.py,sha256=r1edYtur8w9czdNJDawiq_qnA2V2rs14Nohf1hWmudg,9424
3
+ osudroid-api-wrapper/classes/score.py,sha256=pgRGHsX0nMQ7Iw-1HVPPN6fJX_ju3SO_W7EiR8h6pls,4290
4
+ osudroid-api-wrapper/classes/base/beatmap.py,sha256=s5rhL9nQ87Ol4iB9w2C0zH4h4Pjlt8XHHKyTsWNImUU,3702
5
+ osudroid-api-wrapper/classes/base/mods.py,sha256=q_so6WmJ86sf0QYmXn9QHz-G8iQNr-3uM59e2mDSs6U,1393
6
+ osudroid-api-wrapper/classes/base/player.py,sha256=F46U2lObRfzVktumAv98TuYonzzAVi3kZ3z0T4xvM70,4204
7
+ osudroid-api-wrapper/classes/replay_data/cursordata.py,sha256=pt73jrBo0qaXgjQhM_Dbvt6NSJSOPe0F-XGCmpY-I3U,3821
8
+ osudroid-api-wrapper/classes/replay_data/cursoroccurrence.py,sha256=s0MT-hRDxlx_6x-8cK5Vja2G_jAJPMQxhFpXqNHrtZE,1733
9
+ osudroid-api-wrapper/classes/replay_data/cursoroccurrencegroup.py,sha256=kpMdp5moX0eUM2jamgcrG4sc1IrYgKMYrI1OFzrTHp0,4171
10
+ osudroid-api-wrapper/classes/replay_data/hitresult.py,sha256=qSukja68PjG4cTTDlJwnN2F35OtPbBHJw-Gdm60yP9U,1355
11
+ osudroid-api-wrapper/classes/replay_data/movementtype.py,sha256=B5qjeKpvmfOmcxcz335GlJeU-NVmYI1HC_OZH1abcc8,1414
12
+ osudroid-api-wrapper/classes/replay_data/replayobjectdata.py,sha256=NGdHl10jU88D9vEI5VsGvaxmyObbVqm4kqLaeVsLelg,1645
13
+ osudroid-api-wrapper/structs/profile.py,sha256=X_-mPbNaaf-2jm2MvFfiu-7pvEL7frLTy_nkBA0jvAQ,2503
14
+ osudroid_api_wrapper-0.0.1.dist-info/LICENSE,sha256=PE59EcMlelA4jw-9J5G5MJaBUzNYRjIpd0ow5D8LYh0,1082
15
+ osudroid_api_wrapper-0.0.1.dist-info/METADATA,sha256=KcvSoQkFNAzO9Q-vHU_Eygg_hDJOh-LLqWeCGQKdq6c,1915
16
+ osudroid_api_wrapper-0.0.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
17
+ osudroid_api_wrapper-0.0.1.dist-info/top_level.txt,sha256=rTEbJMsRctkMLKxyyd3QOmIitpss7GkUQSkyeiCZZGY,21
18
+ osudroid_api_wrapper-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ osudroid-api-wrapper