@rel-packages/osu-beatmap-parser 0.1.9 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,4 +13,9 @@ npm install osu-beatmap-parser
13
13
  ```bash
14
14
  npm install
15
15
  npm run compile
16
- ```
16
+ ```
17
+
18
+ ## TODO
19
+ - [ ] native: handle "Storyboard" key
20
+ - [ ] native: handle beatmap file version
21
+ - [ ] native: handle lazer files (like, if we're trying to get AudioLocation from a lazer file, we will need to follow the hashed lazer path)
Binary file
package/dist/index.d.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  import { OsuKey, OsuInput } from "./types";
2
- export declare function get_property(location: string, key: OsuKey): string;
3
- export declare function get_properties(input: string | OsuInput, keys: OsuKey[]): Record<OsuKey, string> & {
4
- id?: string;
5
- };
6
- export declare function process_beatmaps(inputs: (string | OsuInput)[], keys: OsuKey[], update_fn?: (index: number) => void): Promise<(Record<OsuKey, string> & {
7
- id?: string;
8
- })[]>;
9
- export declare function get_duration(location: string): number;
10
- export declare function get_audio_duration(location: string): number;
11
- export { OsuKey, OsuInput };
2
+ import { init_wasm } from "./lib/bindings";
3
+ export declare const get_property: (data: Uint8Array, key: OsuKey) => any;
4
+ export declare const get_properties: (input: Uint8Array | OsuInput, keys: OsuKey[]) => any;
5
+ export { OsuKey, OsuInput, init_wasm };
package/dist/index.js CHANGED
@@ -1,46 +1,18 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.init_wasm = exports.get_properties = exports.get_property = void 0;
4
+ const bindings_1 = require("./lib/bindings");
5
+ Object.defineProperty(exports, "init_wasm", { enumerable: true, get: function () { return bindings_1.init_wasm; } });
6
+ const get_property = (data, key) => {
7
+ return bindings_1.native.get_property(data, key);
8
+ };
6
9
  exports.get_property = get_property;
7
- exports.get_properties = get_properties;
8
- exports.process_beatmaps = process_beatmaps;
9
- exports.get_duration = get_duration;
10
- exports.get_audio_duration = get_audio_duration;
11
- const node_gyp_build_1 = __importDefault(require("node-gyp-build"));
12
- const path_1 = __importDefault(require("path"));
13
- const native = (0, node_gyp_build_1.default)(path_1.default.join(__dirname, ".."));
14
- function get_property(location, key) {
15
- return native.get_property(location, key);
16
- }
17
- ;
18
- function get_properties(input, keys) {
19
- const location = typeof input === "string" ? input : input.path;
20
- const result = native.get_properties(location, keys);
21
- if (typeof input !== "string" && input.id) {
10
+ const get_properties = (input, keys) => {
11
+ const data = input instanceof Uint8Array ? input : input.data;
12
+ const result = bindings_1.native.get_properties(data, keys);
13
+ if (!(input instanceof Uint8Array) && input.id) {
22
14
  return { ...result, id: input.id };
23
15
  }
24
16
  return result;
25
- }
26
- ;
27
- async function process_beatmaps(inputs, keys, update_fn) {
28
- const locations = inputs.map(i => typeof i === "string" ? i : i.path);
29
- const results = await native.process_beatmaps(locations, keys, update_fn);
30
- return results.map((res, i) => {
31
- const input = inputs[i];
32
- if (typeof input !== "string" && input.id) {
33
- return { ...res, id: input.id };
34
- }
35
- return res;
36
- });
37
- }
38
- ;
39
- function get_duration(location) {
40
- return native.get_duration(location);
41
- }
42
- ;
43
- function get_audio_duration(location) {
44
- return native.get_audio_duration(location);
45
- }
46
- ;
17
+ };
18
+ exports.get_properties = get_properties;
@@ -0,0 +1,2 @@
1
+ export declare const init_wasm: () => Promise<void>;
2
+ export declare let native: any;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.native = exports.init_wasm = void 0;
37
+ const IS_BROWSER = globalThis.window != undefined;
38
+ const wasm_proxy = {
39
+ instance: null,
40
+ init_promise: null,
41
+ get_property: (data, key) => {
42
+ if (wasm_proxy.instance == null)
43
+ throw Error("WASM not loaded yet...");
44
+ const wasm_inst = wasm_proxy.instance;
45
+ const buffer_ptr = wasm_inst._malloc(data.length);
46
+ wasm_inst.HEAPU8.set(data, buffer_ptr);
47
+ const result = wasm_inst.get_property(buffer_ptr, data.length, key);
48
+ wasm_inst._free(buffer_ptr);
49
+ return result;
50
+ },
51
+ get_properties: (data, keys) => {
52
+ if (wasm_proxy.instance == null)
53
+ throw Error("WASM not loaded yet...");
54
+ const wasm_inst = wasm_proxy.instance;
55
+ const buffer_ptr = wasm_inst._malloc(data.length);
56
+ wasm_inst.HEAPU8.set(data, buffer_ptr);
57
+ const result = wasm_inst.get_properties(buffer_ptr, data.length, keys);
58
+ wasm_inst._free(buffer_ptr);
59
+ return result;
60
+ },
61
+ };
62
+ const init_wasm = async () => {
63
+ if (!IS_BROWSER)
64
+ return;
65
+ if (wasm_proxy.instance)
66
+ return;
67
+ if (wasm_proxy.init_promise)
68
+ return wasm_proxy.init_promise;
69
+ wasm_proxy.init_promise = (async () => {
70
+ // try dynamic import first, fallback to globalThis
71
+ let create_func;
72
+ try {
73
+ // @ts-ignore
74
+ const mod = await Promise.resolve().then(() => __importStar(require("../../build/osu-beatmap-parser.js")));
75
+ create_func = mod.default || mod.create_osu_parser;
76
+ }
77
+ catch (e) {
78
+ create_func = globalThis.create_osu_parser;
79
+ }
80
+ if (!create_func) {
81
+ throw Error("create_osu_parser not found");
82
+ }
83
+ wasm_proxy.instance = await create_func();
84
+ })();
85
+ return wasm_proxy.init_promise;
86
+ };
87
+ exports.init_wasm = init_wasm;
88
+ const load_native_module = () => {
89
+ if (IS_BROWSER) {
90
+ return wasm_proxy;
91
+ }
92
+ try {
93
+ const fs = require("fs");
94
+ const path = require("path");
95
+ const platform = process.platform;
96
+ const arch = process.arch;
97
+ const paths = [
98
+ // prebuilt binaries
99
+ path.join(__dirname, "..", "..", "prebuilds", `${platform}-${arch}`, "osu-beatmap-parser.node"),
100
+ path.join(__dirname, "..", "prebuilds", `${platform}-${arch}`, "osu-beatmap-parser.node"),
101
+ // local builds
102
+ path.join(__dirname, "..", "..", "build", "osu-beatmap-parser.node"),
103
+ path.join(__dirname, "..", "..", "build", "Release", "osu-beatmap-parser.node"),
104
+ path.join(__dirname, "..", "build", "osu-beatmap-parser.node"),
105
+ path.join(__dirname, "build", "osu-beatmap-parser.node"),
106
+ ];
107
+ for (const p of paths) {
108
+ if (fs.existsSync(p)) {
109
+ return require(p);
110
+ }
111
+ }
112
+ }
113
+ catch (e) { }
114
+ return false;
115
+ };
116
+ exports.native = load_native_module();
117
+ if (exports.native == false && !IS_BROWSER) {
118
+ throw new Error("failed to load native module...");
119
+ }
package/dist/types.d.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  export type OsuKey = "AudioFilename" | "AudioLeadIn" | "PreviewTime" | "Countdown" | "SampleSet" | "StackLeniency" | "Mode" | "LetterboxInBreaks" | "WidescreenStoryboard" | "Bookmarks" | "DistanceSpacing" | "BeatDivisor" | "GridSize" | "TimelineZoom" | "Title" | "TitleUnicode" | "Artist" | "ArtistUnicode" | "Creator" | "Version" | "Source" | "Tags" | "BeatmapID" | "BeatmapSetID" | "HPDrainRate" | "CircleSize" | "OverallDifficulty" | "ApproachRate" | "SliderMultiplier" | "SliderTickRate" | "Background" | "Video" | "Storyboard" | "Duration";
2
2
  export interface OsuInput {
3
- path: string;
3
+ data: Uint8Array;
4
4
  id?: string;
5
5
  }
6
6
  export interface INativeExporter {
7
- get_property(location: string, key: string): string;
8
- get_properties(location: string, keys: string[]): Record<string, string>;
9
- process_beatmaps(locations: string[], keys: string[], callback?: (index: number) => void): Promise<Record<string, string>[]>;
10
- get_duration(location: string): number;
11
- get_audio_duration(location: string): number;
7
+ get_property(data: Uint8Array, key: string): string;
8
+ get_properties(data: Uint8Array, keys: string[]): Record<string, string>;
12
9
  }
package/package.json CHANGED
@@ -1,24 +1,43 @@
1
1
  {
2
2
  "name": "@rel-packages/osu-beatmap-parser",
3
- "version": "0.1.9",
3
+ "version": "1.0.4",
4
4
  "description": ".osu parser for nodejs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "homepage": "https://github.com/mezleca/osu-beatmap-parser",
8
8
  "scripts": {
9
- "dev": "npm run compile:native && tsx scripts/verify.ts",
10
- "compile": "npm run compile:native && npm run compile:tsc",
9
+ "update:patch": "npm version patch && git push --follow-tags",
10
+ "update:minor": "npm version minor && git push --follow-tags",
11
+ "update:major": "npm version major && git push --follow-tags",
12
+ "prepublishOnly": "npm run compile:tsc && mkdir -p lib",
13
+ "compile:native": "tsx scripts/build.ts native",
14
+ "compile:wasm": "tsx scripts/build.ts wasm",
11
15
  "compile:tsc": "tsc",
12
- "compile:native": "cmake-js build",
13
- "prebuild": "prebuild --backend cmake-js --strip --verbose"
16
+ "build": "tsx scripts/build.ts all && npm run compile:tsc",
17
+ "example:wasm": "bun run compile:wasm && bun run examples/wasm/server.ts",
18
+ "example:node": "bun run compile:native && tsx examples/node/index.ts"
14
19
  },
15
- "keywords": [],
20
+ "keywords": [
21
+ "osu",
22
+ "beatmap",
23
+ "parser",
24
+ "native"
25
+ ],
16
26
  "author": "",
17
27
  "license": "ISC",
18
28
  "repository": {
19
29
  "url": "https://github.com/mezleca/osu-beatmap-parser.git"
20
30
  },
21
31
  "type": "commonjs",
32
+ "files": [
33
+ "dist/",
34
+ "prebuilds/",
35
+ "build/*.node",
36
+ "build/*.wasm",
37
+ "build/*.js",
38
+ "lib/bindings.ts",
39
+ "README.md"
40
+ ],
22
41
  "devDependencies": {
23
42
  "@types/bindings": "^1.5.5",
24
43
  "@types/node": "^24.10.1",
@@ -27,7 +46,6 @@
27
46
  "tsx": "^4.21.0",
28
47
  "typescript": "^5.9.3"
29
48
  },
30
- "dependencies": {
31
- "node-gyp-build": "^4.8.4"
32
- }
33
- }
49
+ "dependencies": {},
50
+ "optionalDependencies": {}
51
+ }
package/.prebuildrc DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "upload": "gh release upload {tag_name} {file_path} --repo={owner}/{repo} --clobber",
3
- "download": "https://github.com/{owner}/{repo}/releases/download/{tag}/{file_name}",
4
- "tag-prefix": "v",
5
- "backend": "cmake-js"
6
- }
package/CMakeLists.txt DELETED
@@ -1,68 +0,0 @@
1
- cmake_minimum_required(VERSION 3.16)
2
-
3
- if(WIN32)
4
- if(NOT DEFINED CMAKE_TOOLCHAIN_FILE)
5
- if(EXISTS "${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake")
6
- set(CMAKE_TOOLCHAIN_FILE "${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake")
7
- elseif(EXISTS "C:/vcpkg/scripts/buildsystems/vcpkg.cmake")
8
- set(CMAKE_TOOLCHAIN_FILE "C:/vcpkg/scripts/buildsystems/vcpkg.cmake")
9
- endif()
10
- endif()
11
- set(VCPKG_TARGET_TRIPLET "x64-windows-static")
12
- endif()
13
-
14
- project(osu-beatmap-parser VERSION 0.0.1)
15
-
16
- set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
17
- set(CMAKE_CXX_STANDARD 17)
18
- set(CMAKE_CXX_STANDARD_REQUIRED ON)
19
- set(BUILD_TESTING OFF CACHE BOOL "Build tests" FORCE)
20
- set(BUILD_SHARED_LIBS ON CACHE BOOL "Build shared libraries")
21
-
22
- file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/native/*.cpp")
23
- file(GLOB_RECURSE HEADERS CONFIGURE_DEPENDS "src/native/*.hpp")
24
-
25
- if(WIN32)
26
- find_package(SndFile CONFIG REQUIRED)
27
- set(SNDFILE_LIBRARIES SndFile::sndfile)
28
- else()
29
- find_package(PkgConfig REQUIRED)
30
- pkg_check_modules(SNDFILE REQUIRED sndfile)
31
- endif()
32
-
33
- execute_process(
34
- COMMAND node -p "require('node-addon-api').include"
35
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
36
- OUTPUT_VARIABLE NODE_ADDON_API_DIR
37
- OUTPUT_STRIP_TRAILING_WHITESPACE
38
- )
39
-
40
- string(REPLACE "\"" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
41
-
42
- add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS} ${CMAKE_JS_SRC})
43
-
44
- set_target_properties(${PROJECT_NAME} PROPERTIES
45
- PREFIX ""
46
- SUFFIX ".node"
47
- CXX_STANDARD 17
48
- CXX_STANDARD_REQUIRED ON
49
- )
50
-
51
- if (MSVC)
52
- set(MSVC_RUNTIME_LIBRARY OFF)
53
- add_compile_definitions(_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING)
54
- execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})
55
- target_compile_options(${PROJECT_NAME} PRIVATE /Zc:__cplusplus)
56
- endif()
57
-
58
- target_include_directories(${PROJECT_NAME} PRIVATE
59
- ${NODE_ADDON_API_DIR}
60
- ${CMAKE_JS_INC}
61
- ${SNDFILE_INCLUDE_DIRS}
62
- )
63
-
64
- target_link_libraries(
65
- ${PROJECT_NAME}
66
- ${CMAKE_JS_LIB}
67
- ${SNDFILE_LIBRARIES}
68
- )
@@ -1,310 +0,0 @@
1
- #include <filesystem>
2
- #include <napi.h>
3
- #include <iostream>
4
- #include <sndfile.h>
5
- #include <string>
6
- #include <thread>
7
- #include <fstream>
8
- #include <vector>
9
- #include <atomic>
10
- #include "osu/parser.hpp"
11
- #include "osu/audio.hpp"
12
- #include "addon.hpp"
13
- #include "pool.hpp"
14
-
15
- #define NOOP_FUNC(env) Napi::Function::New(env, [](const Napi::CallbackInfo& info){})
16
-
17
- AudioAnalyzer audio_analizer;
18
-
19
- std::filesystem::path dirname(const std::string &path) {
20
- std::filesystem::path p(path);
21
- return p.parent_path();
22
- }
23
-
24
- std::string read_file(const std::string& path) {
25
- std::ifstream file(path, std::ios::binary | std::ios::ate);
26
-
27
- if (!file.is_open()) {
28
- return "";
29
- }
30
-
31
- std::streamsize size = file.tellg();
32
- file.seekg(0, std::ios::beg);
33
-
34
- std::string buffer(size, ' ');
35
-
36
- if (file.read(buffer.data(), size)) {
37
- return buffer;
38
- }
39
-
40
- return "";
41
- }
42
-
43
- Napi::Value ParserAddon::get_property(const Napi::CallbackInfo& info) {
44
- if (info.Length() < 2) {
45
- return Napi::String::New(info.Env(), "");
46
- }
47
-
48
- if (!info[0].IsString() || !info[1].IsString()) {
49
- return Napi::String::New(info.Env(), "");
50
- }
51
-
52
- std::string location = info[0].As<Napi::String>().Utf8Value();
53
- std::string key = info[1].As<Napi::String>().Utf8Value();
54
- std::string content = read_file(location);
55
-
56
- if (content.empty()) {
57
- return Napi::String::New(info.Env(), "");
58
- }
59
-
60
- return Napi::String::From(info.Env(), osu_parser::get_property(content, key));
61
- }
62
-
63
- Napi::Value ParserAddon::get_properties(const Napi::CallbackInfo& info) {
64
- if (info.Length() < 2) {
65
- return Napi::Object::New(info.Env());
66
- }
67
-
68
- std::string location = info[0].As<Napi::String>().Utf8Value();
69
- Napi::Array keys_array = info[1].As<Napi::Array>();
70
-
71
- std::vector<std::string> keys;
72
-
73
- for (uint32_t i = 0; i < keys_array.Length(); i++) {
74
- Napi::Value val = keys_array[i];
75
- if (val.IsString()) {
76
- keys.push_back(val.As<Napi::String>().Utf8Value());
77
- }
78
- }
79
-
80
- std::string content = read_file(location);
81
-
82
- if (content.empty()) {
83
- return Napi::Object::New(info.Env());
84
- }
85
-
86
- auto results = osu_parser::get_properties(content, keys);
87
-
88
- Napi::Object obj = Napi::Object::New(info.Env());
89
-
90
- for (const auto& [k, v] : results) {
91
- obj.Set(k, v);
92
- }
93
-
94
- return obj;
95
- };
96
-
97
- struct CallbackContext {
98
- Napi::ThreadSafeFunction tsfn;
99
- std::atomic<size_t> current_index = 0;
100
-
101
- CallbackContext() {}
102
- };
103
-
104
- struct BatchContext {
105
- std::vector<std::string> paths;
106
- std::vector<std::string> keys;
107
- std::vector<std::unordered_map<std::string, std::string>> results;
108
- std::atomic<size_t> completed_count{0};
109
- Napi::ThreadSafeFunction tsfn;
110
- CallbackContext *callback_ctx;
111
- Napi::Promise::Deferred deferred;
112
- bool needs_duration = false;
113
-
114
- BatchContext(Napi::Env env) : deferred(Napi::Promise::Deferred::New(env)) {}
115
- };
116
-
117
- void process_chunk(BatchContext* context, size_t start, size_t end) {
118
- auto callback_ctx = context->callback_ctx;
119
-
120
- for (size_t i = start; i < end; i++) {
121
- std::string content = read_file(context->paths[i]);
122
- if (!content.empty()) {
123
- context->results[i] = osu_parser::get_properties(content, context->keys);
124
-
125
- if (context->needs_duration) {
126
- std::string audio_file_name = osu_parser::get_property(content, "AudioFilename");
127
- if (!audio_file_name.empty()) {
128
- std::filesystem::path audio_path = dirname(context->paths[i]) / audio_file_name;
129
- double duration = audio_analizer.get_audio_duration(audio_path.string());
130
- context->results[i]["Duration"] = std::to_string(duration);
131
- }
132
- }
133
-
134
- // only increment and call if file was actually processed
135
- if (callback_ctx != nullptr) {
136
- size_t current = callback_ctx->current_index.fetch_add(1) + 1;
137
- callback_ctx->tsfn.NonBlockingCall([current](Napi::Env env, Napi::Function js_callback) {
138
- js_callback.Call({
139
- Napi::Number::New(env, static_cast<double>(current))
140
- });
141
- });
142
- }
143
- }
144
- }
145
-
146
- size_t completed = context->completed_count.fetch_add(end - start) + (end - start);
147
-
148
- // last thread to finish resolves
149
- if (completed == context->paths.size()) {
150
- // release callback
151
- if (context->callback_ctx != nullptr) {
152
- context->callback_ctx->tsfn.Release();
153
- }
154
-
155
- // then resolve the promise
156
- context->tsfn.BlockingCall([context](Napi::Env env, Napi::Function) {
157
- Napi::Array result_array = Napi::Array::New(env, context->results.size());
158
-
159
- for (size_t i = 0; i < context->results.size(); i++) {
160
- Napi::Object obj = Napi::Object::New(env);
161
- for (const auto& [k, v] : context->results[i]) {
162
- obj.Set(k, v);
163
- }
164
- result_array[i] = obj;
165
- }
166
-
167
- context->deferred.Resolve(result_array);
168
- });
169
-
170
- context->tsfn.Release();
171
- }
172
- }
173
-
174
- Napi::Value ParserAddon::process_beatmaps(const Napi::CallbackInfo& info) {
175
- Napi::Env env = info.Env();
176
-
177
- if (info.Length() < 2) {
178
- auto deferred = Napi::Promise::Deferred::New(env);
179
- deferred.Reject(Napi::String::New(env, "Invalid arguments"));
180
- return deferred.Promise();
181
- }
182
-
183
- Napi::Array paths_array = info[0].As<Napi::Array>();
184
- Napi::Array keys_array = info[1].As<Napi::Array>();
185
-
186
- auto context = new BatchContext(env);
187
-
188
- for (uint32_t i = 0; i < paths_array.Length(); i++) {
189
- context->paths.push_back(Napi::Value(paths_array[i]).As<Napi::String>().Utf8Value());
190
- }
191
-
192
- for (uint32_t i = 0; i < keys_array.Length(); i++) {
193
- std::string key = Napi::Value(keys_array[i]).As<Napi::String>().Utf8Value();
194
- context->keys.push_back(key);
195
- if (key == "Duration") {
196
- context->needs_duration = true;
197
- }
198
- }
199
-
200
- if (info[2].IsFunction()) {
201
- Napi::Function callback_fn = info[2].As<Napi::Function>();
202
- auto callback_ctx = new CallbackContext();
203
-
204
- callback_ctx = new CallbackContext();
205
- callback_ctx->tsfn = Napi::ThreadSafeFunction::New(
206
- callback_fn.Env(),
207
- callback_fn,
208
- "ProcessBeatmapsUpdate",
209
- 0,
210
- 1,
211
- [callback_ctx](Napi::Env) {
212
- delete callback_ctx;
213
- }
214
- );
215
-
216
- context->callback_ctx = callback_ctx;
217
- }
218
-
219
- context->results.resize(context->paths.size());
220
-
221
- context->tsfn = Napi::ThreadSafeFunction::New(
222
- env,
223
- NOOP_FUNC(env),
224
- "ProcessBeatmaps",
225
- 0,
226
- 1,
227
- [context](Napi::Env) {
228
- delete context;
229
- }
230
- );
231
-
232
- size_t total_files = context->paths.size();
233
- size_t thread_count = std::thread::hardware_concurrency();
234
-
235
- if (thread_count == 0) {
236
- thread_count = 1;
237
- }
238
-
239
- size_t chunk_size = (total_files + thread_count - 1) / thread_count;
240
-
241
- for (size_t t = 0; t < thread_count; t++) {
242
- size_t start = t * chunk_size;
243
- size_t end = std::min(start + chunk_size, total_files);
244
-
245
- if (start >= end) {
246
- break;
247
- }
248
-
249
- pool.enqueue([context, start, end]() {
250
- process_chunk(context, start, end);
251
- });
252
- }
253
-
254
- return context->deferred.Promise();
255
- }
256
-
257
- Napi::Value ParserAddon::get_duration(const Napi::CallbackInfo& info) {
258
- if (info.Length() < 1) {
259
- return Napi::Number::New(info.Env(), 0.0);
260
- }
261
-
262
- if (!info[0].IsString()) {
263
- return Napi::String::New(info.Env(), "");
264
- }
265
-
266
- std::string location = info[0].As<Napi::String>().Utf8Value();
267
- std::string content = read_file(location);
268
-
269
- if (content.empty()) {
270
- return Napi::Number::New(info.Env(), 0.0);
271
- }
272
-
273
- std::string audio_file_name = osu_parser::get_property(content, "AudioFilename");
274
- std::filesystem::path audio_path = dirname(location) / audio_file_name;
275
-
276
- return Napi::Number::From(info.Env(), audio_analizer.get_audio_duration(audio_path.string()));
277
- }
278
-
279
- Napi::Value ParserAddon::get_audio_duration(const Napi::CallbackInfo& info) {
280
- if (info.Length() < 1) {
281
- return Napi::Number::New(info.Env(), 0.0);
282
- }
283
-
284
- if (!info[0].IsString()) {
285
- return Napi::String::New(info.Env(), "");
286
- }
287
-
288
- std::string location = info[0].As<Napi::String>().Utf8Value();
289
- std::filesystem::path audio_path(location);
290
-
291
- return Napi::Number::From(info.Env(), audio_analizer.get_audio_duration(audio_path.string()));
292
- }
293
-
294
- Napi::Value ParserAddon::test_promise(const Napi::CallbackInfo& info) {
295
- auto deffered = Napi::Promise::Deferred::New(info.Env());
296
- auto tsfn = Napi::ThreadSafeFunction::New(info.Env(), NOOP_FUNC(info.Env()), "tsfn", 0, 1);
297
-
298
- std::thread([tsfn, deffered]() {
299
- tsfn.NonBlockingCall([deffered](Napi::Env env, Napi::Function) {
300
- std::this_thread::sleep_for(std::chrono::milliseconds(2000));
301
- deffered.Resolve(Napi::Boolean::New(env, true));
302
- });
303
-
304
- tsfn.Release();
305
- }).detach();
306
-
307
- return deffered.Promise();
308
- }
309
-
310
- NODE_API_ADDON(ParserAddon)
@@ -1,26 +0,0 @@
1
- #pragma once
2
-
3
- #include <napi.h>
4
- #include "pool.hpp"
5
-
6
- class ParserAddon : public Napi::Addon<ParserAddon> {
7
- public:
8
- ParserAddon(Napi::Env env, Napi::Object exports) {
9
- pool.initialize(std::thread::hardware_concurrency());
10
- DefineAddon(exports, {
11
- InstanceMethod("get_property", &ParserAddon::get_property),
12
- InstanceMethod("get_properties", &ParserAddon::get_properties),
13
- InstanceMethod("process_beatmaps", &ParserAddon::process_beatmaps),
14
- InstanceMethod("get_duration", &ParserAddon::get_duration),
15
- InstanceMethod("get_audio_duration", &ParserAddon::get_audio_duration),
16
- InstanceMethod("test_promise", &ParserAddon::test_promise)
17
- });
18
- }
19
-
20
- Napi::Value get_property(const Napi::CallbackInfo& info);
21
- Napi::Value get_properties(const Napi::CallbackInfo& info);
22
- Napi::Value process_beatmaps(const Napi::CallbackInfo& info);
23
- Napi::Value get_duration(const Napi::CallbackInfo& info);
24
- Napi::Value get_audio_duration(const Napi::CallbackInfo& info);
25
- Napi::Value test_promise(const Napi::CallbackInfo& info);
26
- };
@@ -1,56 +0,0 @@
1
- #pragma once
2
- #include <string>
3
- #include <unordered_set>
4
- #include <unordered_map>
5
-
6
- enum OSU_SECTIONS {
7
- General = 0,
8
- Editor,
9
- Metadata,
10
- Difficulty,
11
- Events,
12
- TimingPoints,
13
- Colours,
14
- HitObjects
15
- };
16
-
17
- const std::unordered_map<std::string_view, std::string_view> KEY_TO_SECTION = {
18
- {"AudioFilename", "[General]"},
19
- {"AudioLeadIn", "[General]"},
20
- {"PreviewTime", "[General]"},
21
- {"Countdown", "[General]"},
22
- {"SampleSet", "[General]"},
23
- {"StackLeniency", "[General]"},
24
- {"Mode", "[General]"},
25
- {"LetterboxInBreaks", "[General]"},
26
- {"WidescreenStoryboard", "[General]"},
27
- {"Bookmarks", "[Editor]"},
28
- {"DistanceSpacing", "[Editor]"},
29
- {"BeatDivisor", "[Editor]"},
30
- {"GridSize", "[Editor]"},
31
- {"TimelineZoom", "[Editor]"},
32
- {"Title", "[Metadata]"},
33
- {"TitleUnicode", "[Metadata]"},
34
- {"Artist", "[Metadata]"},
35
- {"ArtistUnicode", "[Metadata]"},
36
- {"Creator", "[Metadata]"},
37
- {"Version", "[Metadata]"},
38
- {"Source", "[Metadata]"},
39
- {"Tags", "[Metadata]"},
40
- {"BeatmapID", "[Metadata]"},
41
- {"BeatmapSetID", "[Metadata]"},
42
- {"HPDrainRate", "[Difficulty]"},
43
- {"CircleSize", "[Difficulty]"},
44
- {"OverallDifficulty", "[Difficulty]"},
45
- {"ApproachRate", "[Difficulty]"},
46
- {"SliderMultiplier", "[Difficulty]"},
47
- {"SliderTickRate", "[Difficulty]"},
48
- // special ones (doenst have keys)
49
- {"Background", "[Events]"},
50
- {"Video", "[Events]"},
51
- {"Storyboard", "[Events]"},
52
- // extra special ones (why not)
53
- {"Duration", "[General]"}
54
- };
55
-
56
- const std::unordered_set<std::string_view> SPECIAL_KEYS {"Background", "Video", "Storyboard"};
@@ -1,30 +0,0 @@
1
- #include <iostream>
2
- #include <filesystem>
3
- #include <sndfile.h>
4
- #include "audio.hpp"
5
-
6
- double AudioAnalyzer::get_audio_duration(std::string location) {
7
- double duration = 0.0;
8
-
9
- if (get_cache(location, duration)) {
10
- return duration;
11
- }
12
-
13
- if (!std::filesystem::exists(location)) {
14
- std::cout << location << " does not exists\n";
15
- return 0.0;
16
- }
17
-
18
- SF_INFO sfinfo{};
19
- SNDFILE *file = sf_open(location.c_str(), SFM_READ, &sfinfo);
20
-
21
- if (!file) {
22
- return 0.0;
23
- }
24
-
25
- duration = static_cast<double>(sfinfo.frames) / sfinfo.samplerate;
26
-
27
- sf_close(file);
28
- set_cache(location, duration);
29
- return duration;
30
- }
@@ -1,34 +0,0 @@
1
- #pragma once
2
- #include <mutex>
3
- #include <string>
4
- #include <unordered_map>
5
-
6
- class AudioAnalyzer
7
- {
8
- private:
9
- std::unordered_map<std::string, double> cache;
10
- mutable std::mutex cache_mutex;
11
-
12
- bool get_cache(const std::string &key, double &duration) const {
13
- std::lock_guard<std::mutex> lock(cache_mutex);
14
- auto it = cache.find(key);
15
- if (it != cache.end())
16
- {
17
- duration = it->second;
18
- return true;
19
- }
20
- return false;
21
- }
22
-
23
- void set_cache(const std::string &key, double duration) {
24
- std::lock_guard<std::mutex> lock(cache_mutex);
25
- cache[key] = duration;
26
- }
27
-
28
- void clear_cache() {
29
- std::lock_guard<std::mutex> lock(cache_mutex);
30
- cache.clear();
31
- }
32
- public:
33
- double get_audio_duration(std::string location);
34
- };
@@ -1,294 +0,0 @@
1
- #include <algorithm>
2
- #include <cstdio>
3
- #include <iostream>
4
- #include <optional>
5
- #include <string>
6
- #include <string_view>
7
- #include <unordered_set>
8
- #include <unordered_map>
9
- #include "./parser.hpp"
10
- #include "../definitions.hpp"
11
-
12
- const std::unordered_set<std::string_view> VIDEO_EXTENSIONS = {
13
- ".mp4", ".avi", ".flv", ".mov", ".wmv", ".m4v", ".mpg", ".mpeg"
14
- };
15
-
16
- const std::unordered_set<std::string_view> IMAGE_EXTENSIONS = {
17
- ".jpg", ".jpeg", ".png", ".bmp", ".gif"
18
- };
19
-
20
- std::string_view trim_view(std::string_view s) {
21
- size_t start = s.find_first_not_of(" \t\r\n");
22
-
23
- if (start == std::string_view::npos) {
24
- return "";
25
- }
26
-
27
- size_t end = s.find_last_not_of(" \t\r\n");
28
- return s.substr(start, end - start + 1);
29
- }
30
-
31
- std::string trim(const std::string &s) {
32
- std::string_view result = trim_view(s);
33
- return std::string(result);
34
- }
35
-
36
- std::vector<std::string_view> split_view(std::string_view s, char delim) {
37
- std::vector<std::string_view> result;
38
-
39
- size_t start = 0;
40
- size_t end = s.find(delim);
41
-
42
- while (end != std::string_view::npos) {
43
- result.push_back(s.substr(start, end - start));
44
- start = end + 1;
45
- end = s.find(delim, start);
46
- }
47
-
48
- result.push_back(s.substr(start));
49
- return result;
50
- }
51
-
52
- std::string normalize_path(const std::string &path) {
53
- #ifdef _WIN32
54
- std::string normalized = path;
55
- std::replace(normalized.begin(), normalized.end(), '/', '\\');
56
- return normalized;
57
- #else
58
- return path;
59
- #endif
60
- }
61
-
62
- std::string_view get_extension(std::string_view filename) {
63
- size_t dot_pos = filename.find_last_of('.');
64
-
65
- if (dot_pos == std::string_view::npos) {
66
- return "";
67
- }
68
-
69
- return filename.substr(dot_pos);
70
- }
71
-
72
- std::optional<std::string> get_special_key(std::string_view key, std::string_view line) {
73
- if (key == "Background" || key == "Video") {
74
- std::vector<std::string_view> parts = split_view(line, ',');
75
-
76
- if (parts.size() < 3) {
77
- return std::nullopt;
78
- }
79
-
80
- std::string_view event_type = trim_view(parts[0]);
81
- std::string_view start_time = trim_view(parts[1]);
82
-
83
- if (event_type != "0" || start_time != "0") {
84
- return std::nullopt;
85
- }
86
-
87
- std::string_view filename = trim_view(parts[2]);
88
-
89
- // remove quotes if present
90
- if (filename.size() >= 2 && filename.front() == '"' && filename.back() == '"') {
91
- filename = filename.substr(1, filename.size() - 2);
92
- }
93
-
94
- std::string_view ext = get_extension(filename);
95
- std::string ext_lower(ext);
96
- std::transform(ext_lower.begin(), ext_lower.end(), ext_lower.begin(), ::tolower);
97
-
98
- if (key == "Video") {
99
- if (VIDEO_EXTENSIONS.count(ext_lower) == 0) {
100
- return std::nullopt;
101
- }
102
- } else {
103
- if (IMAGE_EXTENSIONS.count(ext_lower) == 0) {
104
- return std::nullopt;
105
- }
106
- }
107
-
108
- return normalize_path(std::string(filename));
109
- } else if (key == "Storyboard") {
110
- // TODO:
111
- return std::nullopt;
112
- }
113
-
114
- return std::nullopt;
115
- }
116
-
117
- std::string osu_parser::get_property(std::string_view content, std::string_view key) {
118
- bool is_special_key = SPECIAL_KEYS.count(key) > 0;
119
- std::string current_section;
120
- std::string special_section;
121
-
122
- if (is_special_key) {
123
- if (KEY_TO_SECTION.find(key) == KEY_TO_SECTION.end()) {
124
- std::cout << "failed to find special key: " << key << "\n";
125
- return "";
126
- }
127
- special_section = KEY_TO_SECTION.at(key);
128
- }
129
-
130
- size_t start = 0;
131
- size_t end = content.find('\n');
132
-
133
- while (end != std::string_view::npos) {
134
- std::string_view line_view = trim_view(content.substr(start, end - start));
135
- start = end + 1;
136
- end = content.find('\n', start);
137
-
138
- if (line_view.empty() || line_view[0] == '/') {
139
- continue;
140
- }
141
-
142
- if (line_view[0] == '[') {
143
- current_section = std::string(line_view);
144
- continue;
145
- }
146
-
147
- if (is_special_key) {
148
- if (special_section != current_section) {
149
- continue;
150
- }
151
-
152
- auto result = get_special_key(key, line_view);
153
-
154
- if (result.has_value()) {
155
- return result.value();
156
- }
157
-
158
- continue;
159
- }
160
-
161
- size_t delimiter_i = line_view.find(':');
162
-
163
- if (delimiter_i == std::string_view::npos) {
164
- continue;
165
- }
166
-
167
- std::string_view current_key = trim_view(line_view.substr(0, delimiter_i));
168
-
169
- if (current_key == key) {
170
- std::string_view value = trim_view(line_view.substr(delimiter_i + 1));
171
- return std::string(value);
172
- }
173
- }
174
-
175
- // check last line if no newline at end
176
- if (start < content.size()) {
177
- std::string_view line_view = trim_view(content.substr(start));
178
- if (line_view.empty() || line_view[0] == '/') {
179
- return "";
180
- }
181
-
182
- // section change at end
183
- if (line_view[0] == '[') {
184
- return "";
185
- }
186
-
187
- if (is_special_key) {
188
- if (special_section == current_section) {
189
- auto result = get_special_key(key, line_view);
190
- if (result.has_value()) {
191
- return result.value();
192
- }
193
- }
194
- return "";
195
- }
196
-
197
- size_t delimiter_i = line_view.find(':');
198
-
199
- if (delimiter_i != std::string_view::npos) {
200
- std::string_view current_key = trim_view(line_view.substr(0, delimiter_i));
201
- if (current_key == key) {
202
- std::string_view value = trim_view(line_view.substr(delimiter_i + 1));
203
- return std::string(value);
204
- }
205
- }
206
- }
207
-
208
- return "";
209
- }
210
-
211
- std::unordered_map<std::string, std::string> osu_parser::get_properties(std::string_view content, const std::vector<std::string>& keys) {
212
- std::unordered_map<std::string, std::string> results;
213
- std::unordered_map<std::string_view, std::string_view> key_to_section_map;
214
- std::unordered_set<std::string_view> keys_to_find;
215
-
216
- for (const auto& key : keys) {
217
- keys_to_find.insert(key);
218
- if (KEY_TO_SECTION.count(key)) {
219
- key_to_section_map[key] = KEY_TO_SECTION.at(key);
220
- }
221
- }
222
-
223
- std::string current_section;
224
- size_t start = 0;
225
- size_t end = content.find('\n');
226
-
227
- auto process_line = [&](std::string_view line_view) {
228
- if (line_view.empty() || line_view[0] == '/') {
229
- return;
230
- }
231
-
232
- if (line_view[0] == '[') {
233
- current_section = std::string(line_view);
234
- return;
235
- }
236
-
237
- size_t delimiter_i = line_view.find(':');
238
- if (delimiter_i != std::string_view::npos) {
239
- std::string_view current_key = trim_view(line_view.substr(0, delimiter_i));
240
-
241
- if (keys_to_find.count(current_key)) {
242
- bool correct_section = !key_to_section_map.count(current_key) ||
243
- key_to_section_map.at(current_key) == current_section;
244
-
245
- bool not_found_yet = results.find(std::string(current_key)) == results.end();
246
-
247
- if (correct_section && not_found_yet) {
248
- std::string_view value = trim_view(line_view.substr(delimiter_i + 1));
249
- results[std::string(current_key)] = std::string(value);
250
- }
251
- }
252
- }
253
-
254
- for (const auto& key : keys) {
255
- if (!SPECIAL_KEYS.count(key)) {
256
- continue;
257
- }
258
-
259
- if (results.find(key) != results.end()) {
260
- continue;
261
- }
262
-
263
- if (key_to_section_map.count(key) && key_to_section_map.at(key) == current_section) {
264
- auto result = get_special_key(key, line_view);
265
- if (result.has_value()) {
266
- results[key] = result.value();
267
- }
268
- }
269
- }
270
- };
271
-
272
- while (end != std::string_view::npos) {
273
- std::string_view line_view = trim_view(content.substr(start, end - start));
274
- process_line(line_view);
275
- start = end + 1;
276
- end = content.find('\n', start);
277
- }
278
-
279
- if (start < content.size()) {
280
- std::string_view line_view = trim_view(content.substr(start));
281
- process_line(line_view);
282
- }
283
-
284
- return results;
285
- }
286
-
287
- std::vector<std::string> osu_parser::get_section() {
288
- std::vector<std::string> result;
289
- return result;
290
- }
291
-
292
- std::map<std::string, std::string> osu_parser::parse(std::string_view content) {
293
- return {};
294
- }
@@ -1,14 +0,0 @@
1
- #pragma once
2
- #include <map>
3
- #include <string>
4
- #include <vector>
5
- #include <unordered_map>
6
-
7
- namespace osu_parser {
8
- std::string get_property(std::string_view content, std::string_view key);
9
- std::unordered_map<std::string, std::string> get_properties(std::string_view content, const std::vector<std::string>& keys);
10
-
11
- // TODO:
12
- std::vector<std::string> get_section();
13
- std::map<std::string, std::string> parse(std::string_view content);
14
- };
@@ -1,80 +0,0 @@
1
- #pragma once
2
-
3
- #include <condition_variable>
4
- #include <functional>
5
- #include <mutex>
6
- #include <queue>
7
- #include <thread>
8
- #include <vector>
9
- #include <atomic>
10
-
11
- class ThreadPool {
12
- public:
13
- void initialize(int count) {
14
- for (int i = 0; i < count; i++) {
15
- // add new worker
16
- workers.emplace_back([this]() {
17
- // initialize task loop
18
- while (true) {
19
- std::function<void()> task;
20
- // wait for new task
21
- {
22
- std::unique_lock<std::mutex> lock(queue_mutex);
23
-
24
- // use condition variable to make threads go zzz until we actually have a task to do
25
- cv.wait(lock, [this] {
26
- return stop || !tasks.empty();
27
- });
28
-
29
- // ensure we have a task
30
- if (stop && tasks.empty()) {
31
- break;
32
- }
33
-
34
- // move task from queue to var
35
- task = std::move(tasks.front());
36
-
37
- // if i recall correctly "move" moves the pointer data from the queue
38
- // but the pointer is still there pointing to nothing
39
- // so remove it anyway
40
- tasks.pop();
41
- }
42
-
43
- // execute it :)
44
- task();
45
- }
46
- });
47
- }
48
- }
49
-
50
- ~ThreadPool() {
51
- stop = true;
52
-
53
- // wake up all lazy ass threads
54
- cv.notify_all();
55
-
56
- // free them
57
- for (std::thread& t : workers) {
58
- if (t.joinable()) {
59
- t.join();
60
- }
61
- }
62
- }
63
-
64
- template<class T>
65
- void enqueue(T&& f) {
66
- {
67
- std::unique_lock<std::mutex> lock(queue_mutex);
68
- tasks.emplace(std::forward<T>(f));
69
- }
70
- cv.notify_one();
71
- }
72
- private:
73
- std::atomic<bool> stop{false};
74
- std::vector<std::thread> workers;
75
- std::queue<std::function<void()>> tasks;
76
- std::mutex queue_mutex;
77
- std::condition_variable cv;
78
- };
79
-
80
- inline ThreadPool pool;
package/src/types.ts DELETED
@@ -1,48 +0,0 @@
1
- export type OsuKey =
2
- | "AudioFilename"
3
- | "AudioLeadIn"
4
- | "PreviewTime"
5
- | "Countdown"
6
- | "SampleSet"
7
- | "StackLeniency"
8
- | "Mode"
9
- | "LetterboxInBreaks"
10
- | "WidescreenStoryboard"
11
- | "Bookmarks"
12
- | "DistanceSpacing"
13
- | "BeatDivisor"
14
- | "GridSize"
15
- | "TimelineZoom"
16
- | "Title"
17
- | "TitleUnicode"
18
- | "Artist"
19
- | "ArtistUnicode"
20
- | "Creator"
21
- | "Version"
22
- | "Source"
23
- | "Tags"
24
- | "BeatmapID"
25
- | "BeatmapSetID"
26
- | "HPDrainRate"
27
- | "CircleSize"
28
- | "OverallDifficulty"
29
- | "ApproachRate"
30
- | "SliderMultiplier"
31
- | "SliderTickRate"
32
- | "Background"
33
- | "Video"
34
- | "Storyboard"
35
- | "Duration";
36
-
37
- export interface OsuInput {
38
- path: string;
39
- id?: string;
40
- }
41
-
42
- export interface INativeExporter {
43
- get_property(location: string, key: string): string;
44
- get_properties(location: string, keys: string[]): Record<string, string>;
45
- process_beatmaps(locations: string[], keys: string[], callback?: (index: number) => void): Promise<Record<string, string>[]>;
46
- get_duration(location: string): number;
47
- get_audio_duration(location: string): number;
48
- }