|
1 | 1 |
|
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 |
4 | 12 |
|
5 | 13 | def deserialize(content: str) -> Tuple[int, Dict[str, dict]]: |
6 | 14 | """Parse a beatmap file into a dictionary""" |
@@ -49,7 +57,7 @@ def deserialize(content: str) -> Tuple[int, Dict[str, dict]]: |
49 | 57 |
|
50 | 58 | def serialize(beatmap_dict: Dict[str, dict], osu_file_version: int) -> bytes: |
51 | 59 | """Create a beatmap file from a beatmap dictionary""" |
52 | | - stream = BytesIO() |
| 60 | + stream = io.BytesIO() |
53 | 61 |
|
54 | 62 | # Write beatmap file version |
55 | 63 | stream.write( |
@@ -82,6 +90,122 @@ def serialize_section(section: str, items: dict | list) -> bytes: |
82 | 90 | result += b'\r\n' |
83 | 91 | return result |
84 | 92 |
|
| 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 | + |
85 | 209 | def parse_number(value: str) -> int | float: |
86 | 210 | for cast in (int, float): |
87 | 211 | try: |
|
0 commit comments