From 057c83de767320b071ecc0318a95a07019ab3d71 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Thu, 27 Feb 2025 16:04:16 -0800 Subject: initial commit --- .gitignore | 174 +++++++++++++++++++++++++++++++++++++++++ README.md | 1 + chuni/chuni_aquadx_to_tachi.py | 102 ++++++++++++++++++++++++ sdvx/sdvx_csv_to_tachi.py | 101 ++++++++++++++++++++++++ 4 files changed, 378 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 chuni/chuni_aquadx_to_tachi.py create mode 100644 sdvx/sdvx_csv_to_tachi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1800114 --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..19ae51a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Some scripts to convert from various sources to Tachi/Kamaitachi "Batch Manual" import jsons \ No newline at end of file diff --git a/chuni/chuni_aquadx_to_tachi.py b/chuni/chuni_aquadx_to_tachi.py new file mode 100644 index 0000000..4063ac2 --- /dev/null +++ b/chuni/chuni_aquadx_to_tachi.py @@ -0,0 +1,102 @@ +import argparse +import json + +DIFFICULTY_MAPPING = { + 0: "BASIC", + 1: "ADVANCED", + 2: "EXPERT", + 3: "MASTER", + 4: "ULTIMA", + 5: "WORLD'S END", +} + +def convert_chuni_aquadx_json_to_tachi_json(input_json: str, output_file: str, service: str): + with open(input_json, "r", encoding="utf-8") as f: + raw_data = json.load(f) + + batch_manual = { + "meta": {"game": "chunithm", "playtype": "Single", "service": service}, + "scores": [], + } + + processed_count = 0 + skipped_count = 0 + + if "userPlaylogList" in raw_data: + for entry in raw_data["userPlaylogList"]: + level = entry.get("level", 0) + + # Skip World's End, Unsupported by Tachi + if level == 5 or level not in DIFFICULTY_MAPPING: + skipped_count += 1 + continue + + processed_count += 1 + music_id = entry["musicId"] + + score_value = entry.get("score", 0) + is_clear = entry.get("isClear", False) + is_full_combo = entry.get("isFullCombo", False) + is_all_justice = entry.get("isAllJustice", False) + is_all_perfect = entry.get("isAllPerfect", False) + lamp = "FAILED" + if is_all_perfect: + lamp = "ALL JUSTICE CRITICAL" + elif is_all_justice: + lamp = "ALL JUSTICE" + elif is_full_combo: + lamp = "FULL COMBO" + elif is_clear: + lamp = "CLEAR" + timestamp = entry.get("sortNumber", None) + + jcrit = entry.get("judgeHeaven", 0) + entry.get("judgeCritical", 0) + justice = entry.get("judgeJustice", 0) + attack = entry.get("judgeAttack", 0) + miss = entry.get("judgeGuilty", 0) + combo = entry.get("maxCombo", 0) + + score_entry = { + "score": score_value, + "lamp": lamp, + "matchType": "inGameID", + "identifier": str(music_id), + "difficulty": DIFFICULTY_MAPPING[level], + "timeAchieved": timestamp * 1000 if timestamp else None, + "judgements": { + "jcrit": jcrit, + "justice": justice, + "attack": attack, + "miss": miss, + }, + "optional": {"maxCombo": combo}, + } + + batch_manual["scores"].append(score_entry) + + with open(output_file, "w", encoding="utf-8") as f: + print("--- Processing Summary ---") + print(f"Total scores processed: {processed_count}") + print(f"Scores skipped (level 5 or invalid): {skipped_count}") + print(f"Output saved to {output_file}") + json.dump(batch_manual, f, indent=4, ensure_ascii=False) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog="chuni_aquadx_to_tachi", + description="Converts AquaDX score data for Chuni to Tachi compatible JSON", + epilog="Fast/Slow can't be derived (I think)", + ) + parser.add_argument("input_file", help="Path to the input JSON file exported from AquaDX") + parser.add_argument( + "-s", + "--service", + help="Service description to be shown on Tachi (Note for where this score came from)", + default="AquaDX Chuni Import", + ) + parser.add_argument( + "-o", "--output", help="Output filename", default="aquadx_chuni_tachi.json" + ) + args = parser.parse_args() + convert_chuni_aquadx_json_to_tachi_json(args.input_file, args.output, args.service) diff --git a/sdvx/sdvx_csv_to_tachi.py b/sdvx/sdvx_csv_to_tachi.py new file mode 100644 index 0000000..062b0f4 --- /dev/null +++ b/sdvx/sdvx_csv_to_tachi.py @@ -0,0 +1,101 @@ +import csv +import json +import argparse + +DIFFICULTY_MAPPING = { + "NOVICE": "NOV", + "ADVANCED": "ADV", + "EXHAUST": "EXH", + "INFINITE": "INF", + "GRAVITY": "GRV", + "HEAVENLY": "HVN", + "VIVD": "VVD", + "EXCEED": "EXCEED", + "MAXIMUM": "MXM" +} + +LAMP_MAPPING = { + "PLAYED": "FAILED", + "COMPLETE": "CLEAR", +} + +def convert_sdvx_csv_to_tachi_json(csv_file, game, playtype, service): + encodings = ['utf-8-sig', 'utf-8', 'shift-jis', 'cp932'] + + for encoding in encodings: + try: + batch_manual = { + "meta": { + "game": game, + "playtype": playtype, + "service": service + }, + "scores": [] + } + + with open(csv_file, newline='', encoding=encoding) as f: + reader = csv.DictReader(f) + fieldnames = reader.fieldnames + required_fields = ["楽曲名", "難易度", "クリアランク", "ハイスコア"] + if not all(field in fieldnames for field in required_fields): + continue + + + for row in reader: + lamp = LAMP_MAPPING[row["クリアランク"].upper()] + if row.get("ULTIMATE CHAIN"): + lamp = "ULTIMATE CHAIN" + if row.get("PERFECT"): + lamp = "PERFECT ULTIMATE CHAIN" + + score_entry = { + "score": int(row["ハイスコア"]), + "lamp": lamp, + "matchType": "songTitle", + "identifier": row["楽曲名"], + "difficulty": DIFFICULTY_MAPPING[row["難易度"].upper()], + } + optional_fields = {} + if row.get("EXスコア"): + optional_fields["exScore"] = int(row["EXスコア"]) + if row.get("fast"): + optional_fields["fast"] = int(row["fast"]) + if row.get("slow"): + optional_fields["slow"] = int(row["slow"]) + if row.get("maxCombo"): + optional_fields["maxCombo"] = int(row["maxCombo"]) + if row.get("gauge"): + optional_fields["gauge"] = float(row["gauge"]) + if optional_fields: + score_entry["optional"] = optional_fields + batch_manual["scores"].append(score_entry) + return batch_manual + except UnicodeDecodeError: + continue + except Exception as e: + print(f"Error with encoding {encoding}: {str(e)}") + continue + + raise ValueError("Failed to read CSV file with any supported encoding") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog="sdvx_csv_to_tachi", + description="Converts CSV data exported from SDVX eAmuse site to Tachi compatibile JSON", + epilog="Note that not all information can be derived from the CSV so some fields will be missing from Tachi" + ) + parser.add_argument("csv_filename", help="Path to the CSV file") + parser.add_argument("-s", "--service", help="Service description to be shown on Tachi (Note for where this score came from)", default="SDVX Arcade Import") + parser.add_argument("-o", "--output", help="Output filename", default="sdvx_tachi.json") + args = parser.parse_args() + +try: + output_json = convert_sdvx_csv_to_tachi_json(args.csv_filename, "sdvx", "Single", args.service) + + with open(args.output, "w", encoding="utf-8") as json_file: + json.dump(output_json, json_file, ensure_ascii=False, indent=4) + + print("Conversion completed. JSON saved as " + args.output) +except Exception as e: + print(f"Error: {str(e)}") -- cgit v1.2.3