Skip to content

Instantly share code, notes, and snippets.

@fractal161
Created March 3, 2025 22:10
Show Gist options
  • Select an option

  • Save fractal161/d50b2cf6c52537563d6e902c53fdd866 to your computer and use it in GitHub Desktop.

Select an option

Save fractal161/d50b2cf6c52537563d6e902c53fdd866 to your computer and use it in GitHub Desktop.
osu!lazer file recovery utility
'''
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