Skip to content

Commit d43545b

Browse files
committed
Extend beatmap utils for ossapi conversion
1 parent 45a8d77 commit d43545b

File tree

1 file changed

+127
-3
lines changed

1 file changed

+127
-3
lines changed

app/beatmaps.py

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11

2-
from typing import Dict, Tuple, Union
3-
from io import BytesIO
2+
from app.common.database.repositories import beatmaps, beatmapsets
3+
from app.common.database.objects import DBBeatmapset, DBBeatmap
4+
from typing import Dict, Tuple, Union, List
5+
from sqlalchemy.orm import Session
6+
from ossapi import Beatmapset
7+
8+
import hashlib
9+
import app
10+
import re
11+
import io
412

513
def deserialize(content: str) -> Tuple[int, Dict[str, dict]]:
614
"""Parse a beatmap file into a dictionary"""
@@ -49,7 +57,7 @@ def deserialize(content: str) -> Tuple[int, Dict[str, dict]]:
4957

5058
def serialize(beatmap_dict: Dict[str, dict], osu_file_version: int) -> bytes:
5159
"""Create a beatmap file from a beatmap dictionary"""
52-
stream = BytesIO()
60+
stream = io.BytesIO()
5361

5462
# Write beatmap file version
5563
stream.write(
@@ -82,6 +90,122 @@ def serialize_section(section: str, items: dict | list) -> bytes:
8290
result += b'\r\n'
8391
return result
8492

93+
def store_ossapi_beatmapset(set: Beatmapset, session: Session) -> DBBeatmapset:
94+
"""Convert an osu! api beatmapset to a local beatmapset and store it in the database"""
95+
database_set = beatmapsets.create(
96+
set.id,
97+
set.title, set.title_unicode,
98+
set.artist, set.artist_unicode,
99+
set.creator, set.source,
100+
set.tags, set.description['description'],
101+
set.status.value,
102+
set.video, set.storyboard,
103+
set.language['id'], set.genre['id'],
104+
osz_filesize=0,
105+
osz_filesize_novideo=0,
106+
available=(not set.availability.download_disabled),
107+
submit_date=set.submitted_date,
108+
approved_date=set.ranked_date,
109+
last_update=set.last_updated,
110+
session=session
111+
)
112+
113+
for beatmap in set.beatmaps:
114+
beatmap = beatmaps.create(
115+
beatmap.id, beatmap.beatmapset_id,
116+
beatmap.mode_int, beatmap.checksum,
117+
beatmap.status.value, beatmap.version,
118+
resolve_beatmap_filename(beatmap.id),
119+
beatmap.total_length, beatmap.max_combo,
120+
beatmap.bpm, beatmap.cs,
121+
beatmap.ar, beatmap.accuracy,
122+
beatmap.drain, beatmap.difficulty_rating,
123+
set.submitted_date, beatmap.last_updated,
124+
session=session
125+
)
126+
database_set.beatmaps.append(beatmap)
127+
128+
return database_set
129+
130+
def resolve_beatmap_filename(id: int) -> str:
131+
"""Fetch the filename of a beatmap"""
132+
response = app.session.requests.head(f'https://osu.ppy.sh/osu/{id}')
133+
response.raise_for_status()
134+
135+
if not (cd := response.headers.get('content-disposition')):
136+
raise ValueError('No content-disposition header found')
137+
138+
return re.findall("filename=(.+)", cd)[0].strip('"')
139+
140+
def fetch_osz_filesizes(set_id: int) -> Tuple[int, int]:
141+
"""Fetch the filesize of a beatmapset's .osz file from a mirror"""
142+
filesize, filesize_novideo = 0, 0
143+
144+
if (response := app.session.storage.api.osz(set_id, no_video=False)):
145+
filesize = int(response.headers.get('Content-Length', default=0))
146+
147+
if (response := app.session.storage.api.osz(set_id, no_video=True)):
148+
filesize_novideo = int(response.headers.get('Content-Length', default=0))
149+
150+
return filesize, filesize_novideo
151+
152+
def fix_beatmap_files(beatmapset: DBBeatmapset, session: Session = ...) -> List[DBBeatmap]:
153+
"""Update the .osu files of a beatmapset to round OD/AR/HP/CS values"""
154+
updated_beatmaps = list()
155+
156+
for beatmap in beatmapset.beatmaps:
157+
beatmap_file = app.session.storage.get_beatmap(beatmap.id)
158+
159+
if not beatmap_file:
160+
continue
161+
162+
version, beatmap_dict = deserialize(beatmap_file.decode())
163+
beatmap_updates = {}
164+
165+
difficulty_attributes = {
166+
'OverallDifficulty': 'od',
167+
'ApproachRate': 'ar',
168+
'HPDrainRate': 'hp',
169+
'CircleSize': 'cs'
170+
}
171+
172+
for key, short_key in difficulty_attributes.items():
173+
if key not in beatmap_dict['Difficulty']:
174+
continue
175+
176+
value = beatmap_dict['Difficulty'][key]
177+
178+
if isinstance(value, int):
179+
continue
180+
181+
# Update value
182+
beatmap_updates[short_key] = round(value) # Database
183+
beatmap_dict['Difficulty'][key] = round(value) # File
184+
185+
if not beatmap_updates:
186+
continue
187+
188+
# Get new file
189+
content = serialize(beatmap_dict, version)
190+
191+
# Upload to storage
192+
app.session.storage.upload_beatmap_file(beatmap.id, content)
193+
194+
# Update beatmap hash
195+
beatmap_hash = hashlib.md5(content).hexdigest()
196+
beatmap_updates['md5'] = beatmap_hash
197+
198+
# Update database
199+
beatmaps.update(
200+
beatmap.id,
201+
beatmap_updates,
202+
session=session
203+
)
204+
205+
updated_beatmaps.append(beatmap)
206+
207+
return updated_beatmaps
208+
85209
def parse_number(value: str) -> int | float:
86210
for cast in (int, float):
87211
try:

0 commit comments

Comments
 (0)