Created
March 3, 2025 22:10
-
-
Save fractal161/d50b2cf6c52537563d6e902c53fdd866 to your computer and use it in GitHub Desktop.
osu!lazer file recovery utility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ''' | |
| Tool for the very common situation where you mess with the osu lazer database | |
| files (the ones you're told not to touch) without making a backup and suffer | |
| the deserved consequences. | |
| The interesting part here is that the files themselves are still present on | |
| disk, but the lazer client is unable to perceive them (the existence of all | |
| objects is tracked through client.realm). So the strategy here is to look for | |
| all files after a certain cutoff date, pattern match for the desired file type, | |
| then move them to one location, where they can be re-imported directly through | |
| the client. | |
| Requies osrparse, which is available from pip. | |
| ''' | |
| import datetime | |
| import os | |
| import re | |
| import shutil | |
| import urllib.request | |
| from osrparse import Replay | |
| OSU_BLOB_PATH = "~/.local/share/osu/files" | |
| CUTOFF_DATE = datetime.datetime(2025, 1, 16) # change as needed | |
| def execute_walk(directory, callback): | |
| directory = os.path.expanduser(directory) | |
| for root, _, files in os.walk(directory): | |
| for file in files: | |
| file_path = os.path.join(root, file) | |
| callback(file_path) | |
| beatmapsets = dict() | |
| def find_beatmapsets(file_path): | |
| global beatmapsets | |
| mod_time = os.path.getmtime(file_path) | |
| mod_datetime = datetime.datetime.fromtimestamp(mod_time) | |
| if mod_datetime <= CUTOFF_DATE: | |
| return | |
| lines = [] | |
| try: | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| lines = [s.strip() for s in f.readlines()] | |
| except UnicodeDecodeError: | |
| return | |
| if not lines[0].startswith('osu file format'): | |
| return | |
| # TODO: get metadata | |
| metadata_line = lines.index('[Metadata]') | |
| field_pattern = r'^(.*?):(.*)' | |
| title = None | |
| artist = None | |
| mapset_id = None | |
| for i in range(metadata_line+1, len(lines)): | |
| if lines[i] == '': | |
| continue | |
| m = re.match(field_pattern, lines[i]) | |
| if m is None: | |
| break | |
| match m.group(1): | |
| case 'Title': | |
| title = m.group(2) | |
| case 'Artist': | |
| artist = m.group(2) | |
| case 'BeatmapSetID': | |
| mapset_id = int(m.group(2)) | |
| if mapset_id is not None: | |
| beatmapsets[mapset_id] = (title, artist) | |
| # readable_time = mod_datetime.strftime('%Y-%m-%d %H:%M:%S') | |
| # print(f"Found {title} [{version}]: Last modified on {readable_time}") | |
| def recover_beatmapsets(): | |
| execute_walk(OSU_BLOB_PATH, find_beatmapsets) | |
| if not os.path.exists('maps/'): | |
| os.mkdir('maps') | |
| for id, (title, artist) in beatmapsets.items(): | |
| print(f'Fetching {id}: {title} ({artist})') | |
| b_url = f'https://catboy.best/d/{id}' | |
| b_path = os.path.join('maps', f'{id} {artist} - {title}.osz') | |
| if not os.path.exists(b_path): | |
| urllib.request.urlretrieve(b_url, b_path) | |
| print(len(beatmapsets)) | |
| replays = set() | |
| replay_count = 0 | |
| def find_replays(file_path): | |
| global replays, replay_count | |
| mod_time = os.path.getmtime(file_path) | |
| mod_datetime = datetime.datetime.fromtimestamp(mod_time) | |
| if mod_datetime <= CUTOFF_DATE: | |
| return | |
| try: | |
| replay = Replay.from_path(file_path) | |
| except: | |
| return | |
| replay_count += 1 | |
| replays.add(file_path) | |
| def recover_replays(): | |
| execute_walk(OSU_BLOB_PATH, find_replays) | |
| if not os.path.exists('replays/'): | |
| os.mkdir('replays') | |
| for replay_path in replays: | |
| basename = os.path.basename(replay_path) + '.osr' | |
| new_path = os.path.join('replays', basename) | |
| # replace | |
| shutil.copy(replay_path, new_path) | |
| print(replay_count) | |
| # comment/uncomment depending on what you need | |
| recover_beatmapsets() | |
| recover_replays() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment