Command-line tool for map conversion
import argparse # parsing command line arguments
import os # for paths and directories
import re
import sys # used only for sys.exit()
import time # to measure execution time
import webbrowser # to open the explorer cross-platform
import zipfile # to handle .zip files (.qua and .osz)
from reamber.algorithms.convert import QuaToOsu
from reamber.quaver import QuaMap
SAMPLESETS = [
"Soft",
"Normal",
"Drum"
]
Creates an argument parser with all of the arguments already added
def initArgParser() -> argparse.ArgumentParser:
argParser = argparse.ArgumentParser("Converts .qp files to .osz files")
def qpOrDirPath(inputPath):
if (inputPath.endswith(".qp") and os.path.isFile(inputPath)) or os.path.isdir(inputPath):
return inputPath
else:
raise argparse.ArgumentTypeError("Path is not a directory or not a .qp file")
argParser.add_argument(
"input",
help="Paths of directories containing .qp files, or direct paths to .qp files. Both are possible.",
nargs="*",
type=qpOrDirPath
)
def directory(path):
if os.path.isdir(path):
return path
raise argparse.ArgumentTypeError("Not a valid path")
argParser.add_argument(
"-o",
"--output",
required=False,
help="Path of the output folder, defaults to ./output",
default="./output",
type=directory
)
def diffValue(x):
x = float(x)
if x >= 0 and x <= 10:
return x
else:
raise argparse.ArgumentTypeError("Value must be larger between 0 and 10")
argParser.add_argument(
"-od",
"--overall-difficulty",
required=False,
help="Overall difficulty as an integer between 0 and 10, defaults to 8",
default=8,
type=diffValue
)
argParser.add_argument(
"-hp",
"--hp-drain",
required=False,
help="HP drain as an integer between 0 and 10, defaults to 8",
default=8,
type=diffValue
)
def hsVolume(n):
n = int(n)
if n >= 0 and n <= 100:
return n
else:
raise argparse.ArgumentTypeError("Value must be between 0 and 100")
argParser.add_argument(
"-hv",
"--hitsound-volume",
required=False,
help="Hitsound volume as an integer between 0 and 100, defaults to 20",
default=20,
type=hsVolume
)
argParser.add_argument(
"-hs",
"--sampleset",
required=False,
help="Hitsound sample set as either 'Soft', 'Normal' or 'Drum', defaults to Soft",
default="Soft",
type=str,
choices=SAMPLESETS
)
argParser.add_argument(
"-c",
"--creator",
required=False,
help="Sets a different mapper name for all difficulties, in case Quaver username and osu! username are different",
type=str
)
argParser.add_argument(
"-p",
"--preserve-folder-structure",
required=False,
help="Outputs in original directory structure if specified",
action="store_true"
)
argParser.add_argument(
"-r",
"--recursive-search",
required=False,
help="Looks for .qp in all subdirectories of given directories if specified",
action="store_true"
)
return argParser
def searchForQpFiles(directory: str, qpList: list, recursive: bool) -> list:
for path in os.listdir(directory):
fullRelativePath = os.path.join(directory, path)
if os.path.isfile(fullRelativePath) and fullRelativePath.endswith(".qp"):
qpList.append(os.path.normpath(fullRelativePath))
elif os.path.isdir(fullRelativePath) and recursive:
searchForQpFiles(fullRelativePath, qpList, recursive)
Converts a whole .qp mapset to a .osz mapset
def convertQp(path: str, outputFolder: str, options) -> None:
Moves all files to a new directory and converts all .qua files to .osu files
Options parameter is built up as following:
options = {
"od": int,
"hp": int,
"hitSoundVolume": int,
"sampleSet": ["Soft","Normal","Drum"]
}
Prefixing with “q_” to prevent osu from showing the wrong preview backgrounds, because it takes the folder number to choose the background for whatever reason
folderName = "q_" + os.path.basename(path).replace(".qp", "")
outputPath = os.path.join(outputFolder, folderName)
Opens the .qp (.zip) mapset file and extracts it into a folder in the same directory
with zipfile.ZipFile(path, "r") as oldDir:
oldDir.extractall(outputPath)
Converts each .qua difficulty file
for file in os.listdir(outputPath):
filePath = os.path.join(outputPath, file)
Replaces each .qua file with the converted .osu file, uses Evening’s reamber package
if file.endswith(".qua"):
qua = QuaMap.readFile(filePath)
convertedOsu = QuaToOsu.convert(qua)
if options["od"]:
convertedOsu.overallDifficulty = options["od"]
if options["hp"]:
convertedOsu.hpDrainRate = options["hp"]
if options["creator"]:
convertedOsu.creator = options["creator"]
if options["hitSoundVolume"] or options["sampleset"]:
for list in [convertedOsu.bpms.data(), convertedOsu.svs.data()]:
for element in list:
if options["hitSoundVolume"]:
element.volume = options["hitSoundVolume"]
if options["sampleSet"]:
element.sampleSet = SAMPLESETS.index(options["sampleSet"])
newFileName = re.sub(r"\.qua$", ".osu", filePath, 1, re.MULTILINE)
convertedOsu.writeFile(newFileName)
os.remove(filePath)
Creates a new .osz (.zip) mapset file
with zipfile.ZipFile(outputPath + ".osz", "w") as newDir:
for root, dirs, files in os.walk(outputPath):
for file in files:
newDir.write(os.path.join(root, file), file)
Delete all files in output dir
for root, dirs, files in os.walk(outputPath, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
os.rmdir(outputPath)
Runs the map file conversions
def main():
Run py qua2osu.py --help
for help with command line arguments
argParser = initArgParser()
args = vars(argParser.parse_args())
Run help command if no params
if len(args["input"]) == 0:
argParser.parse_args(["-h"])
sys.exit(1)
print(args)
Filters for all files that end with .qp and puts the complete path of the files into an array
qpFilesInInputDir = []
for path in args["input"]:
if os.path.isfile(path) and path.endswith(".qp"):
qpFilesInInputDir.append(path)
elif os.path.isdir(path):
searchForQpFiles(path, qpFilesInInputDir, args["recursive_search"])
print(qpFilesInInputDir)
if len(qpFilesInInputDir) == 0:
print("No mapsets found in given paths")
sys.exit(1)
Assigns the arguments to an options object to pass to
the convertQp()
function
options = {
"od": args["overall_difficulty"],
"hp": args["hp_drain"],
"hitSoundVolume": args["hitsound_volume"],
"sampleSet": args["sampleset"],
"creator": args["creator"]
}
Starts the timer for the total execution time
start = time.time()
Run the conversion for each .qp file
for file in qpFilesInInputDir:
basePath = os.path.dirname(file) if args["preserve_folder_structure"] else ""
outputPath = os.path.join(args["output"], basePath)
if not os.path.exists(outputPath):
os.mkdir(outputPath)
print(f"Converting {file}")
convertQp(file, outputPath, options)
Stops the timer for the total execution time
end = time.time()
timeElapsed = round(end - start, 2)
print(f"Finished converting all mapsets, total time elapsed: {timeElapsed} seconds")
Opens output folder in explorer
absoluteOutputPath = os.path.realpath(args["output"])
webbrowser.open("file:///" + absoluteOutputPath)
if __name__ == '__main__':
main()
sys.exit(0)