Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Lazer Support #86

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9ea051f
add realm package and add lazer parsing
hrfarmer Oct 19, 2024
693336c
Merge branch 'master' into lazer-support
hrfarmer Oct 24, 2024
4cbc5f2
package-lock
hrfarmer Oct 24, 2024
75659a1
fix song.path and song.beatmapSetID
hrfarmer Oct 24, 2024
2a1cf7c
new dir select ui wip
hrfarmer Oct 24, 2024
d3262a8
update auto dir fetching and update directory selection ui
hrfarmer Oct 24, 2024
51ce354
downgrade realm, set realm to readonly, and add try catch for import
hrfarmer Oct 24, 2024
5c561fc
properly check for file hashes
hrfarmer Oct 24, 2024
749fdf4
fix string concantenation in stable & lazer parser
hrfarmer Oct 24, 2024
fa89ab4
missed one
hrfarmer Oct 24, 2024
e8d55a2
fix discord rich presence
hrfarmer Oct 24, 2024
0df23b2
support custom lazer file locations
hrfarmer Oct 24, 2024
9123501
oops
hrfarmer Oct 24, 2024
47a1815
Merge branch 'master' into lazer-support
hrfarmer Oct 24, 2024
ede1edf
Merge branch 'master' into lazer-support
hrfarmer Oct 25, 2024
edf18cb
package lock
hrfarmer Oct 25, 2024
70f63ed
remixicon purged
hrfarmer Oct 25, 2024
1b18e2b
set parameters in discord::play type to non-optional
hrfarmer Oct 25, 2024
06f5d9c
make ts types more in line with client.realm schema
hrfarmer Oct 25, 2024
07cb064
add bpm and fix existing song check
hrfarmer Oct 27, 2024
b0c68db
wip
hrfarmer Oct 27, 2024
a141066
move window control css out of nesting and refactor window controls
hrfarmer Nov 11, 2024
bcb8192
oopsie
hrfarmer Nov 11, 2024
e645cba
apply most review changes
hrfarmer Nov 12, 2024
caddc2e
add new window controls
hrfarmer Nov 12, 2024
511067e
tiny things
hrfarmer Nov 12, 2024
39ffe1d
bun.lockb
Tnixc Nov 12, 2024
06a844c
Merge branch 'master' into lazer-support
hrfarmer Nov 12, 2024
7d4752d
comment out settings.delete
hrfarmer Nov 19, 2024
b669560
Merge branch 'master' into lazer-support
hrfarmer Nov 21, 2024
c05541c
prettier
hrfarmer Nov 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 228 additions & 16 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"lucide-solid": "^0.452.0",
"node-addon-api": "^8.2.1",
"polished": "^4.3.1",
"realm": "~12.6.2",
"sharp": "^0.33.5",
"solid-focus-trap": "^0.1.7",
"tailwind-merge": "^2.5.3"
Expand Down
9 changes: 5 additions & 4 deletions src/RequestAPI.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
} from "./@types";
import type { SearchQuery } from "./main/lib/search-parser/@search-types";
import type { ConfigError, ConfigSuccess } from "./main/lib/template-parser/parser/TemplateParser";
import { OsuDirectory } from "./main/router/dir-router";

export type RequestAPI = {
"resource::get": (
Expand All @@ -37,11 +38,11 @@ export type RequestAPI = {
"queue::create": (payload: QueueCreatePayload) => void;
"queue::shuffle": () => void;

"dir::select": () => Optional<string>;
"dir::autoGetOsuDir": () => Optional<string>;
"dir::submit": (dir: string) => void;
"dir::select": () => Optional<OsuDirectory>;
"dir::autoGetOsuDirs": () => Optional<OsuDirectory[]>;
"dir::submit": (dir: OsuDirectory) => void;

"discord::play": (song: Song, duration?: number) => void;
"discord::play": (song: Song, length: number, duration: number) => void;
"discord::pause": (song: Song) => void;

"error::dismissed": () => void;
Expand Down
6 changes: 3 additions & 3 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { electronApp, is, optimizer } from "@electron-toolkit/utils";
import { app, BrowserWindow, dialog } from "electron";
import { join } from "path";
import { Router } from "./lib/route-pass/Router";
import createMenu from "./lib/window/menu";
import trackBounds, { getBounds, wasMaximized } from "./lib/window/resizer";
import { main } from "./main";
import { electronApp, is, optimizer } from "@electron-toolkit/utils";
import { app, BrowserWindow, dialog } from "electron";
import { join } from "path";

if (!app.requestSingleInstanceLock()) app.quit();

Expand Down
67 changes: 67 additions & 0 deletions src/main/lib/osu-file-parser/LazerTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
type BeatmapMetadata = {
Title: string;
TitleUnicode: string;
Artist: string;
ArtistUnicode: string;
Author?: { OnlineID: number; Username: string; CountryCode: string };
Source?: string;
Tags?: string;
PreviewTime: number;
AudioFile: string;
BackgroundFile?: string;
};

export type Beatmap = {
ID: Realm.BSON.UUID;
DifficultyName?: string;
Metadata: BeatmapMetadata;
BeatmapSet?: BeatmapSet;
Status: number;
OnlineID: number;
Length: number;
BPM: number;
Hash: string;
StarRating: number;
MD5Hash?: string;
OnlineMD5Hash?: string;
LastLocalUpdate?: string;
LastOnlineUpdate?: string;
Hidden: boolean;
EndTimeObjectCount: number;
TotalObjectCount: number;
AudioLeadIn: number;
StackLeniency: number;
SpecialStyle: boolean;
LetterboxInBreaks: boolean;
WidescreenStoryboard: boolean;
EpilepsyWarning: boolean;
SamplesMatchPlaybackRate: boolean;
LastPlayed?: string;
DistanceSpacing: number;
BeatDivisor: number;
GridSize: number;
TimelineZoom: number;
EditorTimestamp?: number;
CountdownOffset: number;
};

type RealmFile = {
File: {
Hash: string;
};
Filename: string;
};

export type BeatmapSet = {
ID: Realm.BSON.UUID;
OnlineID: number;
DateAdded: string;
DateSubmitted?: string;
DateRanked?: string;
Beatmaps: Beatmap[];
Status: number;
DeletePending: boolean;
Hash?: string;
Protected: boolean;
Files: RealmFile[];
};
130 changes: 118 additions & 12 deletions src/main/lib/osu-file-parser/OsuParser.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import fs from "graceful-fs";
import os from "os";
import path from "path/posix";
import readline from "readline";
import Realm from "realm";
import { AudioSource, ImageSource, ResourceID, Result, Song } from "../../../@types";
import { access } from "../fs-promises";
import { fail, ok } from "../rust-like-utils-backend/Result";
import { assertNever } from "../tungsten/assertNever";
import { BeatmapSet } from "./LazerTypes";
import { OsuFile } from "./OsuFile";
import fs from "graceful-fs";
import os from "os";
import path from "path/posix";
import readline from "readline";

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

// Overriding Buffer prototype because I'm lazy.
// Should probably get moved to another file, or make a wrapper instead.

class BufferReader {
buffer: Buffer;
pos: number;
Expand Down Expand Up @@ -113,7 +112,112 @@ class BufferReader {
}

export class OsuParser {
static async parseDatabase(
static async parseLazerDatabase(
databasePath: string,
update?: (i: number, total: number, file: string) => any,
): DirParseResult {
const currentDir = databasePath.replaceAll("\\", "/");

const realm = await Realm.open({
path: currentDir + "/client.realm",
readOnly: true,
schemaVersion: 23,
});
const beatmapSets = realm.objects<BeatmapSet>("BeatmapSet");

const songTable = new Map<ResourceID, Song>();
const audioTable = new Map<ResourceID, AudioSource>();
const imageTable = new Map<ResourceID, ImageSource>();

let i = 0;
for (const beatmapSet of beatmapSets) {
try {
const beatmaps = beatmapSet.Beatmaps;

for (const beatmap of beatmaps) {
try {
const song: Song = {
audio: "",
osuFile: "",
path: "",
ctime: "",
dateAdded: beatmapSet.DateAdded,
title: beatmap.Metadata.Title,
artist: beatmap.Metadata.Artist,
creator: beatmap.Metadata.Author?.Username ?? "No Creator",
bpm: [[beatmap.BPM]],
duration: beatmap.Length,
diffs: [beatmap.DifficultyName ?? "Unknown difficulty"],
};

song.osuFile = path.join(
currentDir,
"files",
beatmap.Hash[0],
beatmap.Hash.substring(0, 2),
beatmap.Hash,
);

const songHash = beatmapSet.Files.find(
(file) => file.Filename.toLowerCase() === beatmap.Metadata.AudioFile.toLowerCase(),
)?.File.Hash;

if (songHash) {
song.audio = path.join(
currentDir,
"files",
songHash[0],
songHash.substring(0, 2),
songHash,
);
}

const existingSong = songTable.get(song.audio);
if (existingSong) {
existingSong.diffs.push(song.diffs[0]);
continue;
}

/* Note: in lots of places throughout the application, it relies on the song.path parameter, which in the
stable parser is the path of the folder that holds all the files. This folder doesn't exist in lazer's
file structure, so for now I'm just passing the audio location as the path parameter. In initial testing
this doesn't seem to break anything but just leaving this note in case it does */
song.path = song.audio;

const bgHash = beatmapSet.Files.find(
(file) => file.Filename === beatmap.Metadata.BackgroundFile,
)?.File.Hash;

if (bgHash) {
song.bg = path.join(currentDir, "files", bgHash[0], bgHash.substring(0, 2), bgHash);
}

song.beatmapSetID = beatmapSet.OnlineID;

songTable.set(song.audio, song);
audioTable.set(song.audio, {
songID: song.audio,
path: song.audio,
ctime: String(beatmapSet.DateAdded),
});

if (update) {
update(i + 1, beatmapSets.length, song.title);
i++;
}
} catch (err) {
console.error("Error while parsing beatmap: ", err);
}
}
} catch (err) {
console.error("Error while parsing beatmapset: ", err);
}
}

return ok([songTable, audioTable, imageTable]);
}

static async parseStableDatabase(
databasePath: string,
update?: (i: number, total: number, file: string) => any,
): DirParseResult {
Expand Down Expand Up @@ -300,11 +404,11 @@ export class OsuParser {
db.readInt(); // last edit time
db.readByte(); // mania scroll speed

const audioFilePath = songsFolderPath + "/" + folder + "/" + audio_filename;
const osuFilePath = songsFolderPath + "/" + folder + "/" + osu_filename;
const audioFilePath = path.join(songsFolderPath, folder, audio_filename);
const osuFilePath = path.join(songsFolderPath, folder, osu_filename);
song.osuFile = osuFilePath;
song.audio = audioFilePath;
song.path = songsFolderPath + "/" + folder;
song.path = path.join(songsFolderPath, folder);

// Check if the song has already been processed, and add the diff name to the existing song if so
const existingSong = songTable.get(audioFilePath);
Expand All @@ -320,7 +424,9 @@ export class OsuParser {
}

const bgSrc = osuFile.value.props.get("bgSrc");
song.bg = songsFolderPath + "/" + folder + "/" + bgSrc;
if (bgSrc) {
song.bg = path.join(songsFolderPath, folder, bgSrc);
}

if (song.audio != last_audio_filepath) {
songTable.set(song.audio, song);
Expand Down
16 changes: 10 additions & 6 deletions src/main/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BrowserWindow } from "electron";
import { DirParseResult, OsuParser } from "./lib/osu-file-parser/OsuParser";
import { Router } from "./lib/route-pass/Router";
import { orDefault } from "./lib/rust-like-utils-backend/Optional";
Expand All @@ -7,7 +8,6 @@ import { throttle } from "./lib/throttle";
import { dirSubmit } from "./router/dir-router";
import { showError } from "./router/error-router";
import "./router/import";
import { BrowserWindow } from "electron";

export let mainWindow: BrowserWindow;

Expand All @@ -17,7 +17,7 @@ export async function main(window: BrowserWindow) {
const settings = Storage.getTable("settings");

// Deleting osuSongsDir will force initial beatmap import
// settings.delete("osuSongsDir");
settings.delete("osuSongsDir");
const osuSongsDir = settings.get("osuSongsDir");

if (osuSongsDir.isNone) {
Expand Down Expand Up @@ -54,7 +54,7 @@ async function configureOsuDir(mainWindow: BrowserWindow) {

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

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

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

// All went smoothly. Save osu directory and continue with import procedure
settings.write("osuSongsDir", dir);
settings.write("osuSongsDir", dirData.path);
break;
}

Expand Down
Loading