Skip to content

Instantly share code, notes, and snippets.

@MikuAuahDark
Last active February 20, 2020 16:08
Show Gist options
  • Save MikuAuahDark/80c93fed00b702f86d2a75e51ce8aa63 to your computer and use it in GitHub Desktop.
Save MikuAuahDark/80c93fed00b702f86d2a75e51ce8aa63 to your computer and use it in GitHub Desktop.
PHP Script to extract beatmap files from osu!lazer
#!/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