osudroid-api-wrapper 0.0.1__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.
- osudroid_api_wrapper-0.0.1/LICENSE +21 -0
- osudroid_api_wrapper-0.0.1/PKG-INFO +39 -0
- osudroid_api_wrapper-0.0.1/pyproject.toml +33 -0
- osudroid_api_wrapper-0.0.1/setup.cfg +4 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/__init__.py +0 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/base/beatmap.py +112 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/base/mods.py +33 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/base/player.py +114 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/replay.py +259 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/replay_data/cursordata.py +101 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/replay_data/cursoroccurrence.py +48 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/replay_data/cursoroccurrencegroup.py +120 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/replay_data/hitresult.py +33 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/replay_data/movementtype.py +37 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/replay_data/replayobjectdata.py +38 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/score.py +107 -0
- osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/structs/profile.py +76 -0
- osudroid_api_wrapper-0.0.1/src/osudroid_api_wrapper.egg-info/PKG-INFO +39 -0
- osudroid_api_wrapper-0.0.1/src/osudroid_api_wrapper.egg-info/SOURCES.txt +20 -0
- osudroid_api_wrapper-0.0.1/src/osudroid_api_wrapper.egg-info/dependency_links.txt +1 -0
- osudroid_api_wrapper-0.0.1/src/osudroid_api_wrapper.egg-info/requires.txt +5 -0
- osudroid_api_wrapper-0.0.1/src/osudroid_api_wrapper.egg-info/top_level.txt +1 -0
|
@@ -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,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "osudroid-api-wrapper"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="unclem", email="mannringyt11@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Python api wrapper for osu!droid"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
license = { file = "LICENSE" }
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"requests>=2.26.0",
|
|
22
|
+
"beautifulsoup4>=4.10.0",
|
|
23
|
+
"javaobj-py3>=0.4.4",
|
|
24
|
+
"stream_unzip>=0.0.99",
|
|
25
|
+
"python-dotenv>=1.0.0"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/unclem2/osudroid-api-wrapper"
|
|
31
|
+
Issues = "https://github.com/unclem2/osudroid-api-wrapper/issues"
|
|
32
|
+
|
|
33
|
+
|
|
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}
|
osudroid_api_wrapper-0.0.1/src/osudroid-api-wrapper/classes/replay_data/cursoroccurrencegroup.py
ADDED
|
@@ -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,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,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/osudroid-api-wrapper/__init__.py
|
|
4
|
+
src/osudroid-api-wrapper/classes/replay.py
|
|
5
|
+
src/osudroid-api-wrapper/classes/score.py
|
|
6
|
+
src/osudroid-api-wrapper/classes/base/beatmap.py
|
|
7
|
+
src/osudroid-api-wrapper/classes/base/mods.py
|
|
8
|
+
src/osudroid-api-wrapper/classes/base/player.py
|
|
9
|
+
src/osudroid-api-wrapper/classes/replay_data/cursordata.py
|
|
10
|
+
src/osudroid-api-wrapper/classes/replay_data/cursoroccurrence.py
|
|
11
|
+
src/osudroid-api-wrapper/classes/replay_data/cursoroccurrencegroup.py
|
|
12
|
+
src/osudroid-api-wrapper/classes/replay_data/hitresult.py
|
|
13
|
+
src/osudroid-api-wrapper/classes/replay_data/movementtype.py
|
|
14
|
+
src/osudroid-api-wrapper/classes/replay_data/replayobjectdata.py
|
|
15
|
+
src/osudroid-api-wrapper/structs/profile.py
|
|
16
|
+
src/osudroid_api_wrapper.egg-info/PKG-INFO
|
|
17
|
+
src/osudroid_api_wrapper.egg-info/SOURCES.txt
|
|
18
|
+
src/osudroid_api_wrapper.egg-info/dependency_links.txt
|
|
19
|
+
src/osudroid_api_wrapper.egg-info/requires.txt
|
|
20
|
+
src/osudroid_api_wrapper.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
osudroid-api-wrapper
|