├── .gitignore ├── LICENSE ├── README.md ├── examples ├── read_collection_db_example.js └── read_osu_db_example.js ├── package.json ├── src ├── OsuDB.ts ├── Reader.ts ├── Struct.ts ├── Utils.ts └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/* 3 | node_modules/ 4 | collection.db 5 | osu!.db 6 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mike Shenin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # osu-db-parser 2 | 3 | [![npm version](https://img.shields.io/npm/v/osu-db-parser)](https://www.npmjs.org/package/osu-db-parser) 4 | [![install size](https://packagephobia.now.sh/badge?p=osu-db-parser)](https://packagephobia.now.sh/result?p=osu-db-parser) 5 | [![npm downloads](https://img.shields.io/npm/dm/osu-db-parser.svg)](http://npm-stat.com/charts.html?package=osu-db-parser) 6 | ![license: MIT](https://img.shields.io/badge/License-MIT-blue.svg) 7 | 8 | That package can read files from osu folder called osu!.db and collection.db 9 | About struct that db look on peppy site: [*link*](https://osu.ppy.sh/help/wiki/osu!_File_Formats/Db_%28file_format%29) 10 | 11 | ## Installing 12 | Using npm: 13 | 14 | ```bash 15 | $ npm install osu-db-parser 16 | ``` 17 | ## Example (how to read osu!.db) 18 | 19 | ```js 20 | const fs = require("fs"); 21 | const { OsuDBParser } = require("osu-db-parser"); 22 | 23 | let osuDBbuffer = Buffer.from(fs.readFileSync("")); 24 | const osuDB = new OsuDBParser(osuDBbuffer); 25 | 26 | let osuDBData = osuDB.getOsuDBData(); // This is osu!.db data you can make with this all that you want. 27 | ``` 28 | 29 | You can update buffer on fly 30 | ```js 31 | let newBuffer = Buffer.from(fs.readFileSync("")); 32 | osuDB.setBuffer("osudb", newBuffer); 33 | 34 | let newData = osuDB.getOsuDBData(); 35 | ``` 36 | 37 | ## Example (how to read collection.db) 38 | Similar to how to read osu!.db ;D 39 | 40 | ```js 41 | const fs = require("fs"); 42 | const { OsuDBParser } = require("osu-db-parser"); 43 | 44 | let collectionBuffer = Buffer.from(fs.readFileSync("")); 45 | const collectionDB = new OsuDBParser(null, collectionBuffer); // Yeah, that's okay 46 | 47 | let osuCollectionData = collectionDB.getCollectionData() // This is collection.db data you can make with this all that you want. 48 | ``` 49 | 50 | And too. You can update buffer on fly 51 | ```js 52 | let newBuffer = Buffer.from(fs.readFileSync("")); 53 | collectionDB.setBuffer("collection", newBuffer); 54 | 55 | let newData = collectionDB.getCollectionData() ; 56 | ``` 57 | 58 | ### Or you can use two solution in one 59 | 60 | ```js 61 | const fs = require("fs"); 62 | const { OsuDBParser } = require("osu-db-parser"); 63 | 64 | let osuDBbuffer = Buffer.from(fs.readFileSync("")); 65 | let collectionBuffer = Buffer.from(fs.readFileSync("")); 66 | const ultimateDB = new OsuDBParser(osuDBbuffer=osuDBbuffer, osuCollectionBuffer=collectionBuffer); 67 | 68 | let osuDBData = ultimateDB.getOsuDBData(); 69 | let osuCollectionData = ultimateDB.getCollectionData(); 70 | 71 | ``` 72 | 73 | #### If something wrong, pleas-s-s-s-e create PR with fix) -------------------------------------------------------------------------------- /examples/read_collection_db_example.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { OsuDBParser } = require("../dist/index.js"); 3 | 4 | const DB_PATH = "./collection.db" 5 | const collectionDB = new OsuDBParser(null, osuCollectionBuffer=Buffer.from(fs.readFileSync(DB_PATH))); 6 | 7 | let osuCollectionData = collectionDB.getCollectionData() 8 | console.log(osuCollectionData); 9 | 10 | // Update data via new file 11 | collectionDB.setBuffer("collection", Buffer.from(fs.readFileSync(DB_PATH))); 12 | console.log(collectionDB.getCollectionData()) 13 | -------------------------------------------------------------------------------- /examples/read_osu_db_example.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { OsuDBParser } = require("../dist/index.js"); 3 | 4 | const DB_PATH = "./osu!.db" 5 | const osuDB = new OsuDBParser(osuDBbuffer=Buffer.from(fs.readFileSync(DB_PATH))); 6 | 7 | let osuDBData = osuDB.getOsuDBData(); 8 | console.log(osuDBData); 9 | 10 | // Update data via new file 11 | osuDB.setBuffer("osudb", Buffer.from(fs.readFileSync(DB_PATH))); 12 | console.log(osuDB.getOsuDBData()) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osu-db-parser", 3 | "version": "2.0.1", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "keywords": [ 11 | "parser", 12 | "osu", 13 | "collections", 14 | "node", 15 | "js" 16 | ], 17 | "author": "KotRikD", 18 | "license": "MIT", 19 | "repository": "https://github.com/KotRikD/osu-db-parser", 20 | "dependencies": { 21 | "osu-buffer": "^2.0.2" 22 | }, 23 | "devDependencies": { 24 | "typescript": "^5.7.3", 25 | "@types/node": "^22.13.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/OsuDB.ts: -------------------------------------------------------------------------------- 1 | import { Reader } from './Reader'; 2 | import { osuDbStruct, collectionStruct } from './Struct'; 3 | 4 | type BufferType = 'osudb' | 'collection'; 5 | 6 | type OsuDBData = any; // Replace with actual type 7 | 8 | type CollectionData = any; // Replace with actual type 9 | 10 | class OsuDBParser { 11 | private reader: Reader; 12 | private osuDBData?: OsuDBData; 13 | private collectionData?: CollectionData; 14 | 15 | private canGetDBData: boolean; 16 | private canGetCollectionData: boolean; 17 | 18 | private dbFile?: Buffer; 19 | private collectionDB?: Buffer; 20 | 21 | constructor(osuDbBuffer: Buffer | null = null, osuCollectionBuffer: Buffer | null = null) { 22 | this.reader = new Reader(); 23 | this.canGetDBData = osuDbBuffer !== null; 24 | this.canGetCollectionData = osuCollectionBuffer !== null; 25 | 26 | if (this.canGetDBData && osuDbBuffer) { 27 | this.dbFile = osuDbBuffer; 28 | const dbosuData = this.reader.UnmarshalPacket(this.dbFile, osuDbStruct); 29 | dbosuData.isLocked = !dbosuData.isLocked; 30 | this.osuDBData = dbosuData; 31 | } 32 | 33 | if (this.canGetCollectionData && osuCollectionBuffer) { 34 | this.collectionDB = osuCollectionBuffer; 35 | this.collectionData = this.reader.UnmarshalPacket(this.collectionDB, collectionStruct); 36 | } 37 | } 38 | 39 | /** 40 | * Set a buffer and parse it 41 | * @param {BufferType} type - The type of buffer ('osudb' or 'collection') 42 | * @param {Buffer} buffer - The buffer to parse 43 | * @returns {boolean} 44 | */ 45 | setBuffer(type: BufferType, buffer: Buffer): boolean { 46 | try { 47 | if (type === 'osudb') { 48 | this.dbFile = buffer; 49 | const dbosuData = this.reader.UnmarshalPacket(this.dbFile, osuDbStruct); 50 | dbosuData.isLocked = !dbosuData.isLocked; 51 | this.osuDBData = dbosuData; 52 | this.canGetDBData = true; 53 | } else if (type === 'collection') { 54 | this.collectionDB = buffer; 55 | this.collectionData = this.reader.UnmarshalPacket(this.collectionDB, collectionStruct); 56 | this.canGetCollectionData = true; 57 | } 58 | } catch (error) { 59 | console.error(`Error while parsing ${type}.db:`, error); 60 | return false; 61 | } 62 | return true; 63 | } 64 | 65 | /** 66 | * Get osu DB data if present 67 | * @returns {OsuDBData | null} 68 | */ 69 | getOsuDBData(): OsuDBData | null { 70 | return this.canGetDBData ? this.osuDBData ?? null : null; 71 | } 72 | 73 | /** 74 | * Get collection DB data if present 75 | * @returns {CollectionData | null} 76 | */ 77 | getCollectionData(): CollectionData | null { 78 | return this.canGetCollectionData ? this.collectionData ?? null : null; 79 | } 80 | } 81 | 82 | export default OsuDBParser; -------------------------------------------------------------------------------- /src/Reader.ts: -------------------------------------------------------------------------------- 1 | /* Reader base from osu-packet! */ 2 | import { OsuReader } from 'osu-buffer'; 3 | import { osuDbStruct, collectionStruct } from './Struct'; 4 | 5 | type StructLayout = { 6 | name: string; 7 | type: string; 8 | uses?: undefined; 9 | } | { 10 | name: string; 11 | type: string; 12 | uses: string; 13 | } 14 | 15 | export class Reader { 16 | constructor() { 17 | } 18 | 19 | /** 20 | * Reads a set of data from a buffer 21 | * @param {OsuBuffer} buff 22 | * @param {Object} layout 23 | * @param {null|Number|Boolean|Object|Array|String} requires 24 | * @param {Object|Array} data 25 | * @return {Object|Array} 26 | */ 27 | Read(reader: OsuReader, layout: StructLayout, data: any = undefined) { 28 | switch (layout.type.toLowerCase()) { 29 | case 'int8': 30 | data = reader.readInt8(); 31 | break; 32 | case 'uint8': 33 | data = reader.readUint8(); 34 | break; 35 | case 'int16': 36 | data = reader.readInt16(); 37 | break; 38 | case 'uint16': 39 | data = reader.readUint16(); 40 | break; 41 | case 'int32': 42 | data = reader.readInt32(); 43 | break; 44 | case 'uint32': 45 | data = reader.readUint32(); 46 | break; 47 | case 'int64': 48 | data = reader.readInt64(); 49 | break; 50 | case 'uint64': 51 | data = reader.readUint64(); 52 | break; 53 | case 'string': 54 | data = reader.readString(); 55 | break; 56 | case 'float': 57 | data = reader.readFloat(); 58 | break; 59 | case 'double': 60 | data = reader.readDouble(); 61 | break; 62 | case 'boolean': 63 | data = reader.readBoolean(); 64 | break; 65 | case 'byte': 66 | data = reader.readUint8(); 67 | break; 68 | case 'int32array': { 69 | const len = reader.readInt16(); 70 | data = []; 71 | for (let i = 0; i < len; i++) { 72 | data.push(reader.readInt32()); 73 | } 74 | break; 75 | } 76 | case "collections": { 77 | const collectionsCount = data['collectionscount']; 78 | data = []; 79 | for (let i = 0; i < collectionsCount; i++) { 80 | const collection = { 81 | 'name': reader.readString(), 82 | 'beatmapsCount': reader.readInt32(), 83 | 'beatmapsMd5': [] 84 | } 85 | 86 | for (let i = 0; i < collection['beatmapsCount']; i++) { 87 | const bmmd5 = reader.readString(); 88 | // @ts-ignore 89 | collection['beatmapsMd5'].push(bmmd5) 90 | } 91 | 92 | data.push(collection); 93 | } 94 | break; 95 | } 96 | case "beatmaps": { 97 | const osuver = data['osuver']; 98 | const beatmapscount = data['beatmaps_count']; 99 | data = []; 100 | for (let i = 0; i < beatmapscount; i++) { 101 | if (osuver < 20191107) { 102 | reader.readInt32(); // entry size xd 103 | } 104 | let beatmap = { 105 | 'artist_name': reader.readString(), 106 | 'artist_name_unicode': reader.readString(), 107 | 'song_title': reader.readString(), 108 | 'song_title_unicode': reader.readString(), 109 | 'creator_name': reader.readString(), 110 | 'difficulty': reader.readString(), 111 | 'audio_file_name': reader.readString(), 112 | 'md5': reader.readString(), 113 | 'osu_file_name': reader.readString(), 114 | 'ranked_status': reader.readUint8(), 115 | 'n_hitcircles': reader.readInt16(), 116 | 'n_sliders': reader.readInt16(), 117 | 'n_spinners': reader.readInt16(), 118 | 'last_modification_time': reader.readInt64() 119 | } 120 | 121 | if (osuver < 20140609) { 122 | beatmap = { 123 | ...beatmap, 124 | // @ts-ignore 125 | 'approach_rate': reader.readUint8(), 126 | 'circle_size': reader.readUint8(), 127 | 'hp_drain': reader.readUint8(), 128 | 'overall_difficulty': reader.readUint8() 129 | } 130 | } else { 131 | beatmap = { 132 | ...beatmap, 133 | // @ts-ignore 134 | 'approach_rate': reader.readFloat(), 135 | 'circle_size': reader.readFloat(), 136 | 'hp_drain': reader.readFloat(), 137 | 'overall_difficulty': reader.readFloat() 138 | } 139 | } 140 | 141 | beatmap['slider_velocity'] = reader.readDouble() 142 | 143 | if (osuver >= 20140609) { 144 | const difficulties = [] 145 | 146 | for (let i = 0; i < 4; i++) { 147 | const length = reader.readInt32() 148 | const diffs = {} 149 | for (let i = 0; i < length; i++) { 150 | reader.readUint8() 151 | const mode = reader.readInt32(); 152 | reader.readUint8() 153 | const diff = osuver > 20250107 ? reader.readFloat() : reader.readDouble(); 154 | diffs[mode] = diff 155 | } 156 | // @ts-ignore 157 | difficulties.push(diffs) 158 | } 159 | 160 | beatmap = { 161 | ...beatmap, 162 | // @ts-ignore 163 | 'star_rating_standard': difficulties[0], 164 | 'star_rating_taiko': difficulties[1], 165 | 'star_rating_ctb': difficulties[2], 166 | 'star_rating_mania': difficulties[3], 167 | } 168 | } 169 | 170 | beatmap = { 171 | ...beatmap, 172 | // @ts-ignore 173 | 'drain_time': reader.readInt32(), 174 | 'total_time': reader.readInt32(), 175 | 'preview_offset': reader.readInt32(), 176 | } 177 | 178 | const timingPoints = []; 179 | const timingPointsLength = reader.readInt32() 180 | for (let i = 0; i < timingPointsLength; i++) { 181 | // @ts-ignore 182 | timingPoints.push([ 183 | reader.readDouble(), //BPM 184 | reader.readDouble(), // offset 185 | reader.readBoolean() // Boolean 186 | ]) 187 | } 188 | 189 | beatmap = { 190 | ...beatmap, 191 | // @ts-ignore 192 | 'beatmap_id': reader.readInt32(), 193 | 'beatmapset_id': reader.readInt32(), 194 | 'thread_id': reader.readInt32(), 195 | 'grade_standard': reader.readUint8(), 196 | 'grade_taiko': reader.readUint8(), 197 | 'grade_ctb': reader.readUint8(), 198 | 'grade_mania': reader.readUint8(), 199 | 'local_beatmap_offset': reader.readInt16(), 200 | 'stack_leniency': reader.readFloat(), 201 | 'timing_points': timingPoints, 202 | 'mode': reader.readUint8(), 203 | 'song_source': reader.readString(), 204 | 'song_tags': reader.readString(), 205 | 'online_offset': reader.readInt16(), 206 | 'title_font': reader.readString(), 207 | 'unplayed': reader.readBoolean(), 208 | 'last_played': reader.readInt64(), 209 | 'osz2': reader.readBoolean(), 210 | 'folder_name': reader.readString(), 211 | 'last_checked_against_repository': reader.readInt64(), 212 | 'ignore_sound': reader.readBoolean(), 213 | 'ignore_skin': reader.readBoolean(), 214 | 'disable_storyboard': reader.readBoolean(), 215 | 'disable_video': reader.readBoolean(), 216 | 'visual_override': reader.readBoolean() 217 | } 218 | 219 | if (osuver < 20140609) { 220 | reader.readInt16() 221 | } 222 | beatmap['last_modification_time_2'] = reader.readInt32(); 223 | 224 | beatmap['mania_scroll_speed'] = reader.readUint8() 225 | 226 | data.push(beatmap); 227 | } 228 | } 229 | } 230 | return data; 231 | } 232 | 233 | /** 234 | * Unmarshal's the buffer from the layout 235 | * @param {Buffer} raw 236 | * @param {Array|Object|Null} layout 237 | * @return {Object|Null} 238 | */ 239 | UnmarshalPacket(raw: Buffer, layout: typeof osuDbStruct | typeof collectionStruct): T { 240 | const reader = new OsuReader(raw.buffer); 241 | const data = {}; 242 | layout.forEach(item => { 243 | if (item.uses) { 244 | const needElements = item.uses.split(",") 245 | const dater = {} 246 | for (let datak of needElements) { 247 | dater[datak] = data[datak] 248 | } 249 | 250 | data[item.name] = this.Read(reader, item, item.uses ? dater : {}); 251 | } else { 252 | data[item.name] = this.Read(reader, item); 253 | } 254 | }); 255 | 256 | return data as T; 257 | } 258 | 259 | } -------------------------------------------------------------------------------- /src/Struct.ts: -------------------------------------------------------------------------------- 1 | export const osuDbStruct = [ 2 | {name: 'osuver', type: 'int32'}, 3 | {name: 'folder_count', type: 'int32'}, 4 | {name: 'is_unlocked', type: 'boolean'}, 5 | {name: 'date_unlock_ticks', type: 'int64'}, 6 | {name: 'username', type: 'string'}, 7 | {name: 'beatmaps_count', type: 'int32'}, 8 | {name: 'beatmaps', type: 'beatmaps', uses: 'osuver,beatmaps_count'}, 9 | {name: 'userperms', type: 'int32'} 10 | ] 11 | 12 | export const collectionStruct = [ 13 | {name: 'osuver', type: 'int32'}, 14 | {name: 'collectionscount', type: 'int32'}, 15 | {name: 'collection', type: 'collections', uses: 'collectionscount'} 16 | ] -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts raw .NET's dateData to Date object 3 | * 4 | * @param {number} dateDataHi Raw .NET internal ticks high-order number 5 | * @param {number} dateDataLo Raw .NET internal ticks low-order number 6 | * @returns {Date} Local timezone Date 7 | */ 8 | export const netDateBinaryToDate = ( 9 | dateDataHi: number, 10 | dateDataLo: number 11 | ): Date => { 12 | const buffer = Buffer.alloc(8); 13 | 14 | const ticksMask = 0x3fffffff; 15 | 16 | dateDataHi &= ticksMask; 17 | 18 | buffer.writeInt32LE(dateDataLo); 19 | buffer.writeInt32LE(dateDataHi, 4); 20 | 21 | const dateData = buffer.readBigInt64LE(); 22 | 23 | const ticksPerMillisecond = 10000n; 24 | const epochTicks = 621355968000000000n; 25 | const milliseconds = (dateData - epochTicks) / ticksPerMillisecond; 26 | 27 | return new Date(Number(milliseconds)); 28 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import OsuDBParser from "./OsuDB"; 2 | 3 | export { OsuDBParser }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "sourceMap": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "noImplicitAny": false, 10 | }, 11 | "include": [ 12 | "src/*" 13 | ] 14 | } --------------------------------------------------------------------------------