Skip to content

Commit 50a1389

Browse files
authored
Merge pull request #86 from hrfarmer/lazer-support
Add Lazer Support
2 parents 9f7aa23 + bb5663d commit 50a1389

File tree

15 files changed

+422
-105
lines changed

15 files changed

+422
-105
lines changed

bun.lockb

-20 KB
Binary file not shown.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@
3636
"fastest-levenshtein": "^1.0.16",
3737
"get-audio-duration": "^4.0.1",
3838
"graceful-fs": "^4.2.11",
39-
"lucide-solid": "^0.452.0",
39+
"lucide-solid": "^0.460.0",
4040
"node-addon-api": "^8.2.1",
4141
"polished": "^4.3.1",
42+
"realm": "~20.1.0",
4243
"sharp": "^0.33.5",
4344
"solid-focus-trap": "^0.1.7",
4445
"tailwind-merge": "^2.5.3"

src/main/lib/osu-file-parser/OsuParser.ts

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { OsuFile } from "./OsuFile";
2+
import { BeatmapSet } from "./lazer.types";
23
import { access } from "@main/lib/fs-promises";
34
import { fail, ok } from "@shared/lib/rust-types/Result";
45
import { assertNever } from "@shared/lib/tungsten/assertNever";
@@ -7,6 +8,7 @@ import fs from "graceful-fs";
78
import os from "os";
89
import path from "path/posix";
910
import readline from "readline";
11+
import Realm from "realm";
1012

1113
const bgFileNameRegex = /.*"(?<!Video.*)(.*)".*/;
1214
const beatmapSetIDRegex = /([0-9]+) .*/;
@@ -32,9 +34,6 @@ export type DirParseResult = Promise<
3234
Result<[Table<Song>, Table<AudioSource>, Table<ImageSource>], string>
3335
>;
3436

35-
// Overriding Buffer prototype because I'm lazy.
36-
// Should probably get moved to another file, or make a wrapper instead.
37-
3837
class BufferReader {
3938
buffer: Buffer;
4039
pos: number;
@@ -113,7 +112,129 @@ class BufferReader {
113112
}
114113

115114
export class OsuParser {
116-
static async parseDatabase(
115+
static async parseLazerDatabase(
116+
databasePath: string,
117+
update?: (i: number, total: number, file: string) => any,
118+
): DirParseResult {
119+
const currentDir = databasePath.replaceAll("\\", "/");
120+
121+
const sourceRealm = path.join(currentDir, "client.realm");
122+
const destinationRealm = path.join(currentDir, "radio_client.realm");
123+
124+
// clone the realm file so it can be upgraded if needed by the realm sdk
125+
// without bricking the user's lazer installation
126+
fs.copyFileSync(sourceRealm, destinationRealm);
127+
128+
const realm = await Realm.open({
129+
path: currentDir + "/radio_client.realm",
130+
});
131+
const beatmapSets = realm.objects<BeatmapSet>("BeatmapSet");
132+
133+
const songTable = new Map<ResourceID, Song>();
134+
const audioTable = new Map<ResourceID, AudioSource>();
135+
const imageTable = new Map<ResourceID, ImageSource>();
136+
137+
let i = 0;
138+
for (const beatmapSet of beatmapSets) {
139+
try {
140+
const beatmaps = beatmapSet.Beatmaps;
141+
142+
for (const beatmap of beatmaps) {
143+
try {
144+
const song: Song = {
145+
lazer: true,
146+
audio: "",
147+
osuFile: "",
148+
path: "",
149+
ctime: "",
150+
dateAdded: beatmapSet.DateAdded,
151+
title: beatmap.Metadata.Title,
152+
artist: beatmap.Metadata.Artist,
153+
creator: beatmap.Metadata.Author?.Username ?? "No Creator",
154+
bpm: [[beatmap.BPM]],
155+
duration: beatmap.Length,
156+
diffs: [beatmap.DifficultyName ?? "Unknown difficulty"],
157+
};
158+
159+
song.osuFile = path.join(
160+
currentDir,
161+
"files",
162+
beatmap.Hash[0],
163+
beatmap.Hash.substring(0, 2),
164+
beatmap.Hash,
165+
);
166+
167+
const songHash = beatmapSet.Files.find(
168+
(file) => file.Filename.toLowerCase() === beatmap.Metadata.AudioFile.toLowerCase(),
169+
)?.File.Hash;
170+
171+
if (songHash) {
172+
song.audio = path.join(
173+
currentDir,
174+
"files",
175+
songHash[0],
176+
songHash.substring(0, 2),
177+
songHash,
178+
);
179+
}
180+
181+
const existingSong = songTable.get(song.audio);
182+
if (existingSong) {
183+
existingSong.diffs.push(song.diffs[0]);
184+
continue;
185+
}
186+
187+
/* Note: in lots of places throughout the application, it relies on the song.path parameter, which in the
188+
stable parser is the path of the folder that holds all the files. This folder doesn't exist in lazer's
189+
file structure, so for now I'm just passing the audio location as the path parameter. In initial testing
190+
this doesn't seem to break anything but just leaving this note in case it does */
191+
song.path = song.audio;
192+
193+
const bgHash = beatmapSet.Files.find(
194+
(file) => file.Filename === beatmap.Metadata.BackgroundFile,
195+
)?.File.Hash;
196+
197+
if (bgHash) {
198+
song.bg = path.join(currentDir, "files", bgHash[0], bgHash.substring(0, 2), bgHash);
199+
}
200+
201+
song.beatmapSetID = beatmapSet.OnlineID;
202+
203+
songTable.set(song.audio, song);
204+
audioTable.set(song.audio, {
205+
songID: song.audio,
206+
path: song.audio,
207+
ctime: String(beatmapSet.DateAdded),
208+
});
209+
210+
if (update) {
211+
update(i + 1, beatmapSets.length, song.title);
212+
i++;
213+
}
214+
} catch (err) {
215+
console.error("Error while parsing beatmap: ", err);
216+
}
217+
}
218+
} catch (err) {
219+
console.error("Error while parsing beatmapset: ", err);
220+
}
221+
}
222+
223+
// Done with the file now
224+
realm.close();
225+
226+
// Delete the cloned lazer realm file(s)
227+
fs.unlinkSync(path.join(currentDir, "radio_client.realm"));
228+
fs.unlinkSync(path.join(currentDir, "radio_client.realm.lock"));
229+
fs.rmSync(path.join(currentDir, "radio_client.realm.management"), {
230+
recursive: true,
231+
force: true,
232+
});
233+
234+
return ok([songTable, audioTable, imageTable]);
235+
}
236+
237+
static async parseStableDatabase(
117238
databasePath: string,
118239
update?: (i: number, total: number, file: string) => any,
119240
): DirParseResult {
@@ -196,6 +317,7 @@ export class OsuParser {
196317
}
197318

198319
const song: Song = {
320+
lazer: false,
199321
audio: "",
200322
osuFile: "",
201323
path: "",
@@ -303,11 +425,11 @@ export class OsuParser {
303425
db.readInt(); // last edit time
304426
db.readByte(); // mania scroll speed
305427

306-
const audioFilePath = songsFolderPath + "/" + folder + "/" + audio_filename;
307-
const osuFilePath = songsFolderPath + "/" + folder + "/" + osu_filename;
428+
const audioFilePath = path.join(songsFolderPath, folder, audio_filename);
429+
const osuFilePath = path.join(songsFolderPath, folder, osu_filename);
308430
song.osuFile = osuFilePath;
309431
song.audio = audioFilePath;
310-
song.path = songsFolderPath + "/" + folder;
432+
song.path = path.join(songsFolderPath, folder);
311433

312434
// Check if the song has already been processed, and add the diff name to the existing song if so
313435
const existingSong = songTable.get(audioFilePath);
@@ -323,7 +445,9 @@ export class OsuParser {
323445
}
324446

325447
const bgSrc = osuFile.value.props.get("bgSrc");
326-
song.bg = songsFolderPath + "/" + folder + "/" + bgSrc;
448+
if (bgSrc) {
449+
song.bg = path.join(songsFolderPath, folder, bgSrc);
450+
}
327451

328452
if (song.audio != last_audio_filepath) {
329453
songTable.set(song.audio, song);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
type BeatmapMetadata = {
2+
Title: string;
3+
TitleUnicode: string;
4+
Artist: string;
5+
ArtistUnicode: string;
6+
Author?: { OnlineID: number; Username: string; CountryCode: string };
7+
Source?: string;
8+
Tags?: string;
9+
PreviewTime: number;
10+
AudioFile: string;
11+
BackgroundFile?: string;
12+
};
13+
14+
export type Beatmap = {
15+
ID: Realm.BSON.UUID;
16+
DifficultyName?: string;
17+
Metadata: BeatmapMetadata;
18+
BeatmapSet?: BeatmapSet;
19+
Status: number;
20+
OnlineID: number;
21+
Length: number;
22+
BPM: number;
23+
Hash: string;
24+
StarRating: number;
25+
MD5Hash?: string;
26+
OnlineMD5Hash?: string;
27+
LastLocalUpdate?: string;
28+
LastOnlineUpdate?: string;
29+
Hidden: boolean;
30+
EndTimeObjectCount: number;
31+
TotalObjectCount: number;
32+
AudioLeadIn: number;
33+
StackLeniency: number;
34+
SpecialStyle: boolean;
35+
LetterboxInBreaks: boolean;
36+
WidescreenStoryboard: boolean;
37+
EpilepsyWarning: boolean;
38+
SamplesMatchPlaybackRate: boolean;
39+
LastPlayed?: string;
40+
DistanceSpacing: number;
41+
BeatDivisor: number;
42+
GridSize: number;
43+
TimelineZoom: number;
44+
EditorTimestamp?: number;
45+
CountdownOffset: number;
46+
};
47+
48+
type RealmFile = {
49+
File: {
50+
Hash: string;
51+
};
52+
Filename: string;
53+
};
54+
55+
export type BeatmapSet = {
56+
ID: Realm.BSON.UUID;
57+
OnlineID: number;
58+
DateAdded: string;
59+
DateSubmitted?: string;
60+
DateRanked?: string;
61+
Beatmaps: Beatmap[];
62+
Status: number;
63+
DeletePending: boolean;
64+
Hash?: string;
65+
Protected: boolean;
66+
Files: RealmFile[];
67+
};

src/main/main.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ async function configureOsuDir(mainWindow: BrowserWindow) {
5454

5555
while (true) {
5656
await Router.dispatch(mainWindow, "changeScene", "dir-select");
57-
const dir = await dirSubmit();
57+
const dirData = await dirSubmit();
5858

5959
await Router.dispatch(mainWindow, "changeScene", "loading");
6060
await Router.dispatch(mainWindow, "loadingScene::setTitle", "Importing songs from osu!");
@@ -68,7 +68,11 @@ async function configureOsuDir(mainWindow: BrowserWindow) {
6868
});
6969
}, UPDATE_DELAY_MS);
7070

71-
tables = await OsuParser.parseDatabase(dir, update);
71+
if (dirData.version == "stable") {
72+
tables = await OsuParser.parseStableDatabase(dirData.path, update);
73+
} else {
74+
tables = await OsuParser.parseLazerDatabase(dirData.path, update);
75+
}
7276
// Cancel ongoing throttled update, so it does not look bad when it finishes and afterward the update overwrites
7377
// finished state
7478
cancelUpdate();
@@ -82,14 +86,14 @@ async function configureOsuDir(mainWindow: BrowserWindow) {
8286
if (tables.value[SONGS].size === 0) {
8387
await showError(
8488
mainWindow,
85-
`No songs found in folder: ${dir}. Please make sure this is the directory where you have all your songs saved.`,
89+
`No songs found in folder: ${dirData.path}. Please make sure this is the directory where you have all your songs saved.`,
8690
);
8791
// Try again
8892
continue;
8993
}
9094

9195
// All went smoothly. Save osu directory and continue with import procedure
92-
settings.write("osuSongsDir", dir);
96+
settings.write("osuSongsDir", dirData.path);
9397
break;
9498
}
9599

0 commit comments

Comments
 (0)