aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore218
-rw-r--r--README.md1
-rw-r--r--requirements.txt2
-rw-r--r--taiko_donder_hiroba_export.py234
4 files changed, 455 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e9aae41
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,218 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[codz]
+*$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
+# poetry.toml
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
+# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
+# pdm.lock
+# pdm.toml
+.pdm-python
+.pdm-build/
+
+# pixi
+# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
+# pixi.lock
+# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
+# in the .venv directory. It is recommended not to include this directory in version control.
+.pixi
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# Redis
+*.rdb
+*.aof
+*.pid
+
+# RabbitMQ
+mnesia/
+rabbitmq/
+rabbitmq-data/
+
+# ActiveMQ
+activemq-data/
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.envrc
+.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/
+
+# Abstra
+# Abstra is an AI-powered process automation framework.
+# Ignore directories containing user credentials, local state, and settings.
+# Learn more at https://abstra.io/docs
+.abstra/
+
+# Visual Studio Code
+# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
+# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
+# and can be added to the global gitignore or merged into this file. However, if you prefer,
+# you could uncomment the following to ignore the entire vscode folder
+# .vscode/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+# Marimo
+marimo/_static/
+marimo/_lsp/
+__marimo__/
+
+# Streamlit
+.streamlit/secrets.toml
+taiko_charts.json
+mirage_donder_hiroba_export.json
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bec821b
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# Importing from Donder Hiroba to Mirage
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..adc36d4
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+beautifulsoup4==4.14.2
+Requests==2.32.5
diff --git a/taiko_donder_hiroba_export.py b/taiko_donder_hiroba_export.py
new file mode 100644
index 0000000..41345e5
--- /dev/null
+++ b/taiko_donder_hiroba_export.py
@@ -0,0 +1,234 @@
+import requests
+from bs4 import BeautifulSoup
+import json
+import time
+import argparse
+import os
+
+
+SONG_CATEGORIES = ["pops", "kids", "anime", "vocaloid", "game", "variety", "classic", "namco"]
+SONG_LIST_BASE_URL = "https://taiko.namco-ch.net/taiko/en/songlist/"
+headers = {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
+}
+PLAY_HISTORY_URL = "https://donderhiroba.jp/history_recent_score.php"
+DIFFICULTIES = ["support", "easy", "normal", "hard", "oni", "ura_oni"]
+
+DIFFICULTY_MAP = {
+ "icon_course02_1_640.png": "EASY",
+ "icon_course02_2_640.png": "NORMAL",
+ "icon_course02_3_640.png": "HARD",
+ "icon_course02_4_640.png": "ONI",
+ "icon_course02_4_640.png": "URA_ONI"
+}
+
+CROWN_MAP = {
+ "crown_02_640.png": "FULL COMBO",
+ "crown_03_640.png": "CLEAR",
+ "crown_04_640.png": "DONDERFUL COMBO",
+}
+
+LAMP_MAP = {
+ "best_score_rank_2_640.png": "IKI 1",
+ "best_score_rank_3_640.png": "IKI 2",
+ "best_score_rank_4_640.png": "IKI 3",
+ "best_score_rank_5_640.png": "MIYABI 1",
+ "best_score_rank_6_640.png": "MIYABI 2",
+ "best_score_rank_7_640.png": "MIYABI 3",
+ "best_score_rank_8_640.png": "KIWAMI",
+}
+
+def load_chart_cache():
+ with open("taiko_charts.json") as f:
+ return dict(json.load(f))
+
+def build_taiko_chart_metadata():
+ """
+ Unfortnatly Donder Hiroba doesn't store any data about the level, need to fetch this elsewhere
+ """
+ chart_data = {}
+ for category in SONG_CATEGORIES:
+ url = f"{SONG_LIST_BASE_URL}/{category}.php"
+ print(f"[DATA] Getting {category} category charts")
+ resp = requests.get(url, headers=headers)
+ soup = BeautifulSoup(resp.text, 'html.parser')
+ table = soup.find("tbody")
+ if table is None:
+ raise Exception("Unable to fetch chart data for ", category)
+ rows = table.find_all("tr")
+ for row in rows:
+ cols = row.find_all("td")
+ if len(cols) < 6:
+ continue
+
+ curr_song = {}
+ song_metadata = row.find_all("th")
+ if not song_metadata:
+ continue
+
+ title_th = song_metadata[0]
+ artist_tag = title_th.find("p")
+ song_artist = artist_tag.get_text(strip=True) if artist_tag else ""
+
+ for tag in title_th.find_all(["p", "span"]):
+ tag.decompose()
+ song_title = title_th.get_text(strip=True)
+
+ for i in range(len(DIFFICULTIES)):
+ if DIFFICULTIES[i] == "support":
+ continue
+ diff = str(cols[i].get_text())
+ curr_song[DIFFICULTIES[i]] = None if diff == "-" else diff
+
+ curr_song["artist"] = song_artist
+ chart_data[song_title] = curr_song
+
+ with open("taiko_charts.json", "w") as f:
+ print("Writing charts to cache. Delete this file when new charts come out!")
+ json.dump(chart_data, f)
+ return chart_data
+
+def get_play_hist(token: str, chart_data):
+ """
+ Fetch and parse Donder Hiroba play history page.
+ Extracts scores, difficulty, ranks, and performance breakdowns.
+ Handles pagination by going through all pages until duplicate results are found.
+ """
+ all_results = []
+ page = 1
+ previous_page_titles = set()
+
+ while True:
+ page_url = f"{PLAY_HISTORY_URL}?page={page}" if page > 1 else PLAY_HISTORY_URL
+ print(f"[INFO] Fetching page {page}...")
+ play_hist_page = requests.get(page_url, cookies={"_token_v2": token}, headers=headers)
+ soup = BeautifulSoup(play_hist_page.text, "html.parser")
+ scores = soup.find_all(class_="scoreUser")
+
+ if not scores:
+ print(f"[INFO] No scores found on page {page}. Ending pagination.")
+ break
+
+ current_page_titles = set()
+ page_results = []
+
+ for s in scores:
+ title_tag = s.find("h2")
+ title = title_tag.text.strip() if title_tag else None
+
+ total_score_tag = s.find("div", class_="scoreScore")
+ total_score = total_score_tag.text.strip().replace("点", "") if total_score_tag else None
+
+ # Skip unknown songs
+ if not title or chart_data.get(title) is None:
+ print(f"[WARN] {title} is unknown in chart_data. Skipping.")
+ continue
+
+ current_page_titles.add(title)
+ difficulty = crown = lamp = None
+ score_element = s.find("div", class_="playDataArea", attrs={"style": True})
+ img_tags = score_element.find_all("img") if score_element else []
+
+ for img in img_tags:
+ src = img["src"].split("/")[-1]
+ if src in DIFFICULTY_MAP:
+ difficulty = DIFFICULTY_MAP[src]
+ elif src in CROWN_MAP:
+ crown = CROWN_MAP[src]
+ elif src in LAMP_MAP:
+ lamp = LAMP_MAP[src]
+
+ judgements = {}
+ combo = pound = None
+
+ score_data_area = s.find("div", class_="scoreDataArea")
+ if score_data_area:
+ score_elements = score_data_area.find_all("div", class_="playDataArea", recursive=True)
+ for el in score_elements:
+ img = el.find("img", class_="score_name")
+ val_tag = el.find("div", class_="playDataScore")
+ if not img or not val_tag:
+ continue
+
+ src = img["src"].split("/")[-1]
+ value = val_tag.get_text(strip=True).replace("回", "")
+ if not value.isdigit():
+ continue
+ value = int(value)
+
+ if "score_name_good" in src:
+ judgements["good"] = value
+ elif "score_name_ok" in src:
+ judgements["ok"] = value
+ elif "score_name_ng" in src:
+ judgements["bad"] = value
+ elif "score_name_combo" in src:
+ combo = value
+ elif "score_name_pound" in src:
+ pound = value
+
+ result_entry = {
+ "title": title,
+ "timestamp": 0,
+ "artist": chart_data[title]["artist"],
+ "difficulty": difficulty,
+ "level": int(chart_data[title].get(difficulty.lower(), 0)) if difficulty else None,
+ "crown_rank": crown,
+ "score_rank": lamp,
+ "score": int(total_score) if total_score and total_score.isdigit() else total_score,
+ "judgements": judgements,
+ "optional": {
+ "combo": combo,
+ "pound": pound
+ }
+ }
+ page_results.append(result_entry)
+ if page > 1 and current_page_titles.issubset(previous_page_titles):
+ print(f"[INFO] Page {page} contains duplicate results. Stopping pagination.")
+ break
+
+ all_results.extend(page_results)
+ print(f"[INFO] Page {page} processed: {len(page_results)} scores found")
+
+ previous_page_titles.update(current_page_titles)
+ page += 1
+
+ print(f"[INFO] Total scores collected: {len(all_results)} across {page - 1} pages")
+
+ return {
+ "meta": {
+ "game": "taiko",
+ "playtype": "Single",
+ "service": "Donder Hiroba Export"
+ },
+ "scores": all_results,
+ }
+
+
+if __name__ == "__main__":
+ print("[ALERT!] Please first refresh your scores on Donder Hiroba so that it has the latest info. Visit: https://donderhiroba.jp/score_list.php and click on the top right\n\n")
+ print("!Your token will change after doing this!")
+ parser = argparse.ArgumentParser(
+ prog="taiko_donder_hiroba_export.py",
+ description="Exports Taiko no Tatsujin scores from Donder Hiroba into a Mirage compatible JSON",
+ )
+ parser.add_argument("-t", "--token", help="Donder Hiroba _token_v2. See README for instructions on how to get this!")
+ args = parser.parse_args()
+ if not args.token:
+ args.token = input("Please enter your Donder Hiroba _token_v2: ")
+ chart_data = {}
+ if os.path.exists("taiko_charts.json"):
+ file_time = os.path.getmtime("taiko_charts.json")
+ current_time = time.time()
+ if current_time - file_time > 7 * 24 * 60 * 60:
+ print("Chart cache is older than 1 week, regenerating...")
+ chart_data = build_taiko_chart_metadata()
+ else:
+ print("Using cached chart data")
+ chart_data = load_chart_cache()
+ else:
+ print("No chart cache found, generating...")
+ chart_data = build_taiko_chart_metadata()
+ score_data = get_play_hist(args.token, chart_data)
+ with open("mirage_donder_hiroba_export.json", "w") as f:
+ json.dump(score_data, f)
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage