Last active
February 20, 2020 16:08
-
-
Save MikuAuahDark/80c93fed00b702f86d2a75e51ce8aa63 to your computer and use it in GitHub Desktop.
PHP Script to extract beatmap files from osu!lazer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env php | |
<?php | |
function showUsage(string $scriptName): void | |
{ | |
echo "Usage: $scriptName <[l]ist|e[x]tract|[s]ync|[h]ash> <options>", PHP_EOL; | |
echo 'List usage: list', PHP_EOL; | |
echo 'Extract usage: extract <ID from list> <zip output>', PHP_EOL; | |
echo 'Sync usage: sync <osustable install>', PHP_EOL; | |
echo 'Hash usage: hash <ID from list>', PHP_EOL; | |
} | |
function osuHashToDir(string $hash): string | |
{ | |
return 'files/' . substr($hash, 0, 1) . '/' . substr($hash, 0, 2) . "/$hash"; | |
} | |
function getFilesFromIndex(SQLite3 $db, int $index): SQLite3Result | |
{ | |
return $db->query("SELECT a.Filename,b.Hash FROM 'BeatmapSetFileInfo' a LEFT JOIN 'FileInfo' b ON a.FileInfoID = b.ID WHERE a.BeatmapSetInfoID = $index"); | |
} | |
function main(int $argc, array $argv): int | |
{ | |
// argv check | |
if ($argc < 2) | |
{ | |
showUsage($argv[0]); | |
return 1; | |
} | |
// Extract mode | |
$extractMode = false; | |
$extractIndex = 0; | |
$syncMap = false; | |
$showHashOnly = false; | |
$syncOsuStableDir = NULL; | |
if (strcasecmp($argv[1], 'x') == 0 || strcasecmp($argv[1], 'extract') == 0) | |
$extractMode = true; | |
else if (strcasecmp($argv[1], 's') == 0 || strcasecmp($argv[1], 'sync') == 0) | |
$syncMap = true; | |
else if (strcasecmp($argv[1], 'h') == 0 || strcasecmp($argv[1], 'hash') == 0) | |
$showHashOnly = true; | |
else if (strcasecmp($argv[1], 'l') && strcasecmp($argv[1], 'list')) | |
{ | |
echo 'Invalid method ', $argv[1], PHP_EOL; | |
showUsage($argv[0]); | |
return 1; | |
} | |
if ($extractMode) | |
{ | |
// Need to check if beatmapID is integer > 0 and output specified. | |
if ($argc < 4) | |
{ | |
showUsage($argv[0]); | |
return 1; | |
} | |
else if (($extractIndex = intval($argv[2])) <= 0) | |
{ | |
echo 'Invalid beatmap index ', $argv[2], PHP_EOL; | |
showUsage($argv[0]); | |
return 1; | |
} | |
} | |
else if ($syncMap) | |
{ | |
// Need to check if 2nd argument is valid osu install dir. | |
if ($argc < 3) | |
{ | |
showUsage($argv[0]); | |
return 1; | |
} | |
// Fix paths | |
$syncOsuStableDir = str_replace('\\', '/', $argv[2]); | |
if (strcmp(substr($syncOsuStableDir, -1), '/') == 0) | |
$syncOsuStableDir = substr($syncOsuStableDir, 0, -1); | |
// Check osu!.exe | |
if (file_exists("$syncOsuStableDir/osu!.exe") == false) | |
{ | |
echo "$syncOsuStableDir is not a valid osu! installation directory!", PHP_EOL; | |
return 1; | |
} | |
} | |
else if ($showHashOnly) | |
{ | |
// Show hash implies extract | |
$extractMode = true; | |
// Need to check if beatmapID is integer > 0 | |
if ($argc < 3) | |
{ | |
showUsage($argv[0]); | |
return 1; | |
} | |
else if (($extractIndex = intval($argv[2])) <= 0) | |
{ | |
echo 'Invalid beatmap index ', $argv[2], PHP_EOL; | |
showUsage($argv[0]); | |
return 1; | |
} | |
} | |
// Get osu! client.db file | |
$osuUserdataPath = ''; | |
if (strcmp(PHP_OS_FAMILY, 'Windows') == 0) | |
// Use %APPDATA% | |
$osuUserdataPath = str_replace('\\', '/', getenv('APPDATA') . "\\osu"); | |
else | |
// use $HOME | |
$osuUserdataPath = (getenv('XDG_DATA_HOME') ?: (getenv('HOME') . '/.local/share')) . '/osu'; | |
if (file_exists($osuUserdataPath) == false) | |
{ | |
echo "Missing osu!lazer client.db at $osuUserdataPath!"; | |
return 1; | |
} | |
// Open database | |
$db = new SQLite3($osuUserdataPath . '/client.db', SQLITE3_OPEN_READONLY); | |
// Query list of beatmap sets | |
$beatmapSets = []; | |
// SELECT a.ID,a.BeatmapSetInfoID,a.Filename,b.Hash FROM 'BeatmapSetFileInfo' a LEFT JOIN 'FileInfo' b ON a.FileInfoID = b.ID; | |
$query = $db->query("SELECT a.ID,b.Artist,b.Title,b.Author FROM 'BeatmapSetInfo' a LEFT JOIN 'BeatmapMetadata' b ON a.MetadataID = b.ID"); | |
while (($result = $query->fetchArray(SQLITE3_NUM))) | |
{ | |
$b = ['id' => $result[0]]; | |
if (empty($result[1])) | |
$b['name'] = sprintf('%s | Mapped By: %s', $result[2], $result[3]); | |
else | |
$b['name'] = sprintf('%s - %s | Mapped By: %s', $result[1], $result[2], $result[3]); | |
$beatmapSets[] = $b; | |
} | |
// List beatmaps | |
if ($extractMode) | |
{ | |
// Sanity check | |
$beatmapSetComment = NULL; | |
foreach ($beatmapSets as $v) | |
{ | |
if ($v['id'] == $extractIndex) | |
{ | |
$beatmapSetComment = $v['name']; | |
break; | |
} | |
} | |
if ($beatmapSetComment === NULL) | |
{ | |
echo 'Unknown beatmap index ', $argv[2], PHP_EOL; | |
$db->close(); | |
return 1; | |
} | |
if ($showHashOnly) | |
echo hash('sha256', $beatmapSetComment); | |
else | |
{ | |
// List beatmap files | |
$query = getFilesFromIndex($db, $extractIndex); | |
$zip = new ZipArchive; | |
if ($zip->open($argv[3], ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) | |
{ | |
echo 'Unable to open archive ', $argv[3], PHP_EOL; | |
$db->close(); | |
return 1; | |
} | |
// Add files to archive | |
while (($result = $query->fetchArray(SQLITE3_NUM))) | |
{ | |
$path = $osuUserdataPath . '/' . osuHashToDir($result[1]); | |
if ($zip->addFile($path, $result[0]) == false) | |
{ | |
echo "Unable to add $path to archive: ", $zip->getStatusString(), PHP_EOL; | |
$db->close(); | |
$zip->close(); | |
return 1; | |
} | |
} | |
if (!empty($beatmapSetComment)) | |
$zip->setArchiveComment($beatmapSetComment); | |
// Close | |
$zip->close(); | |
} | |
} | |
// Sync beatmaps | |
else if ($syncOsuStableDir) | |
{ | |
foreach ($beatmapSets as $v) | |
{ | |
$hash = hash('sha256', $v['name']); | |
$path = "$syncOsuStableDir/Songs/$hash"; | |
if (is_dir($path) == false) | |
{ | |
if (is_file($path)) | |
echo 'Warning: skipping ', $v['name'], PHP_EOL; | |
else | |
{ | |
echo 'Copying ', $v['name'], PHP_EOL; | |
mkdir($path, 0644, true); | |
$query = getFilesFromIndex($db, $v['id']); | |
// Copy files | |
while (($result = $query->fetchArray(SQLITE3_NUM))) | |
{ | |
$dir = dirname($result[0]); | |
if (strcmp($dir, '.') && is_dir("$path/$dir") == false) | |
mkdir("$path/$dir", 0644, true); | |
if (symlink("$osuUserdataPath/" . osuHashToDir($result[1]), "$path/" . $result[0]) == false) | |
echo 'Unable to symlink ', $result[0], PHP_EOL; | |
} | |
} | |
} | |
else | |
echo 'Skipping ', $v['name'], PHP_EOL; | |
} | |
} | |
else | |
{ | |
foreach ($beatmapSets as $v) | |
echo $v['id'], '. ', $v['name'], PHP_EOL; | |
} | |
$db->close(); | |
return 0; | |
} | |
exit (main($argc, $argv)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment