From 17432220723e110240d9fa81590a1cf380259d82 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Sun, 25 May 2025 02:09:49 -0700 Subject: ddr_eamuse: ddr world to tachi --- README.md | 1 + ddr/eamuse/README.md | 18 +++++ ddr/eamuse/ddr_eamuse_to_tachi.py | 166 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 ++ sdvx/eamuse_csv/README.md | 2 +- 5 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 ddr/eamuse/README.md create mode 100644 ddr/eamuse/ddr_eamuse_to_tachi.py diff --git a/README.md b/README.md index 9bcc45e..0fd0a10 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Use with caution as there may be some cases missing. - [WACCA AquaDX API recently played (no export functionality in AquaNet yet)](./wacca/aquadx) - [SDVX e-amusement CSV (with limited Session/Date data)](./sdvx/eamuse_csv) +- [DDR e-amusement scores](./ddr/eamuse) - [SDVX 22vv0 Asphyxia](./sdvx/asphyxia) - [jubeat Asphyxia-CZEAve](./jubeat/asphyxia) diff --git a/ddr/eamuse/README.md b/ddr/eamuse/README.md new file mode 100644 index 0000000..a636e04 --- /dev/null +++ b/ddr/eamuse/README.md @@ -0,0 +1,18 @@ +Converts your e-amusement DDR scores to a Tachi Batch-Manual JSON. Due to how e-amusement stores scores, only your BEST score can be derived from each chart. + +> [!NOTE] +> You must be subscribed to e-amusement Basic Course to use this + +> [How to get Cookies?](../../common_docs/how_to_get_cookie_header.md) +> +> Get the cookies from this page: https://p.eagate.573.jp/game/ddr/ddrworld/playdata/music_data_single.html + + +# Arguments +| Argument | Short | Description | Default | +|:---------------:|:-------:|:---------------------------------------------------------------------------------------------------------------------------:|:------------------------------:| +| `--service` | `-s` | Service description to be shown on Tachi (Note for where this score came from) | `e-amusement DDR PB Import` | +| `--cookies` | `-c` | Header string of e-amusement page cookies. See this script's README.md | — | +| `--playstyle` | `-p` | Playstyle. Must be either `'SP'` or `'DP'`. | `SP` | +| `--game` | `-g` | Version of the game | `WORLD` | +| `--output` | `-o` | Output filename | `ddr_pb_tachi.json` | diff --git a/ddr/eamuse/ddr_eamuse_to_tachi.py b/ddr/eamuse/ddr_eamuse_to_tachi.py new file mode 100644 index 0000000..0a0378f --- /dev/null +++ b/ddr/eamuse/ddr_eamuse_to_tachi.py @@ -0,0 +1,166 @@ +from bs4 import BeautifulSoup +from urllib.parse import urljoin +import json +import argparse +import requests +from datetime import datetime +import pytz + +GAME_DATA = { + "WORLD": { + "SP_PAGES": 25, + "DP_PAGES": 25, + "MUSIC_DATA_PAGE": "https://p.eagate.573.jp/game/ddr/ddrworld/playdata/music_data_single.html", + "MUSIC_DETAIL_BASE": "https://p.eagate.573.jp/game/ddr/ddrworld/playdata/", + "DIFFICULTY_MAP" : { + 0: "BEGINNER", 1: "BASIC", 2: "DIFFICULT", 3: "EXPERT", 4: "CHALLENGE" + } + } +} + +def get_site_data_with_cookie(url, cookie_header): + cookies = {} + for cookie in cookie_header.split(";"): + name, value = cookie.strip().split("=", 1) + cookies[name] = value + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + response = requests.get(url, cookies=cookies, headers=headers) + response.raise_for_status() + return response.text + + +def parse_detailed_score_page(url: str, version: str, cookies: str): + site_data = get_site_data_with_cookie(url, cookies) + score_data = {"optional": {}} + soup = BeautifulSoup(site_data, 'html.parser') + difficulty_param = None + parsed_url = url.split('?') + if len(parsed_url) > 1: + query_params = parsed_url[1].split('&') + for param in query_params: + key_value = param.split('=') + if len(key_value) == 2 and key_value[0] == 'difficulty': + difficulty_param = int(key_value[1]) + break + score_data['difficulty'] = GAME_DATA[version]["DIFFICULTY_MAP"].get(int(difficulty_param), "UNKNOWN MAYBE ERROR. FIX SCRIPT") + score_data["matchType"] = "songTitle" + music_info_table = soup.find('table', id='music_info') + if music_info_table: + cells = music_info_table.find_all('td') + if len(cells) >= 2: + title_arist_data = cells[1].get_text(separator="
").split("
") + score_data['identifier'] = title_arist_data[0] + music_detail_table = soup.find('table', id='music_detail_table') + rows = music_detail_table.find_all('tr') + for row in rows: + headers = row.find_all('th') + data_cells = row.find_all('td') + for th, td in zip(headers, data_cells): + if 'ハイスコア' == th.text: + score_data['score'] = int(td.text.strip()) + elif 'フレアランク' in th.text: + flare_rank = td.text.strip() + if flare_rank == "なし": + flare_rank = "0" + score_data["optional"]["flare"] = flare_rank + elif '最大コンボ数' in th.text: + score_data["optional"]["maxCombo"] = int(td.text.strip()) + elif '最終プレー時間' in th.text: + time_played = td.text.strip() + naive_datetime = datetime.strptime(time_played, "%Y-%m-%d %H:%M:%S") + jst_timezone = pytz.timezone("Asia/Tokyo") + localized_datetime = jst_timezone.localize(naive_datetime) + score_data["timeAchieved"] = int(localized_datetime.timestamp() * 1000) + elif 'ハイスコア時のランク' in th.text: + if td.text.strip() == "E": + score_data["lamp"] = "FAILED" + else: + score_data["lamp"] = "CLEAR" + clear_detail_table = soup.find('table', id='clear_detail_table') + rows = clear_detail_table.find_all('tr') + for row in rows: + headers = row.find_all('th') + data_cells = row.find_all('td') + for th, td in zip(headers, data_cells): + if 'グッドフルコンボ' in th.text: + if int(td.text.strip()) != 0: + score_data["lamp"] = "FULL COMBO" + elif 'グレートフルコンボ' in th.text: + if int(td.text.strip()) != 0: + score_data["lamp"] = "GREAT FULL COMBO" + elif 'パーフェクトフルコンボ' in th.text: + if int(td.text.strip()) != 0: + score_data["lamp"] = "PERFECT FULL COMBO" + elif 'マーベラスフルコンボ' in th.text: + if int(td.text.strip()) != 0: + score_data["lamp"] = "MARVELOUS FULL COMBO" + elif 'LIFE4 クリア' in th.text and score_data["lamp"] == "CLEAR": + if int(td.text.strip()) != 0: + score_data["lamp"] = "LIFE4" + return score_data + +def convert_ddr_data_to_tachi_json(version: str, playstyle: str, service: str, cookies: str, output: str): + batch_manual = { + "meta": { + "game": "ddr", + "playtype": playstyle, + "service": service + }, + "scores": [] + } + to_inspect_urls = [] + for page_num in range(GAME_DATA[version][f"{playstyle}_PAGES"]): + found_charts = 0 + print(f"Checking Page {page_num+1}/{GAME_DATA[version][f'{playstyle}_PAGES']} for scores", end="") + url = f"{GAME_DATA[version]['MUSIC_DATA_PAGE']}?offset={page_num}" + site_data = get_site_data_with_cookie(url, cookies) + soup = BeautifulSoup(site_data, 'html.parser') + rows = soup.find_all('tr', class_='data') + for row in rows: + cells = row.find_all('td', class_='rank') + for cell in cells: + score_div = cell.find('div', class_='data_score') + if not score_div or score_div.text.strip() == '---': + continue + link = cell.find('a', class_='music_info') + if link and link.has_attr('href'): + to_inspect_urls.append(urljoin(GAME_DATA[version]["MUSIC_DETAIL_BASE"], link['href'])) + found_charts += 1 + print(f" -> Found {found_charts} charts with scores!") + num_urls = len(to_inspect_urls) + progress = 0 + for url in to_inspect_urls: + print(f"\rPulling Individual Scores ---> Progress: {progress + 1}/{num_urls}", end="") + score = parse_detailed_score_page(url, version=version, cookies=cookies) + batch_manual["scores"].append(score) + progress += 1 + return batch_manual + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog="ddr_eamuse_pb_to_tachi", + description="Converts e-amusement DDR personal best records to a Tachi compatibile JSON", + ) + parser.add_argument("-s", "--service", help="Service description to be shown on Tachi (Note for where this score came from)", default="e-amusement DDR PB Import") + parser.add_argument("-c", "--cookies", help="Header string of e-amusement page cookies. See this script's README.md" ) + parser.add_argument("-p", "--playstyle", help="Playstyle. Must be either 'single' or 'double'", default="SP") + parser.add_argument("-g", "--game", help="Version of the game", default="WORLD") + parser.add_argument("-o", "--output", help="Output filename", default="ddr_pb_tachi.json") + args = parser.parse_args() + assert args.playstyle == "SP" or args.playstyle == "DP" + assert args.game in ["WORLD"] +try: + output_json = convert_ddr_data_to_tachi_json(args.game, args.playstyle, args.service, args.cookies, args.output) + 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)}") diff --git a/requirements.txt b/requirements.txt index 2ef2f87..6ece32c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,9 @@ beautifulsoup4==4.13.3 +certifi==2025.4.26 +charset-normalizer==3.4.2 +idna==3.10 pytz==2025.2 +requests==2.32.3 soupsieve==2.6 typing_extensions==4.13.1 +urllib3==2.4.0 diff --git a/sdvx/eamuse_csv/README.md b/sdvx/eamuse_csv/README.md index fd28227..bb2c5d4 100644 --- a/sdvx/eamuse_csv/README.md +++ b/sdvx/eamuse_csv/README.md @@ -42,7 +42,7 @@ https://p.eagate.573.jp/game/sdvx/vi/playdata/profile/index.html ## Automatically Pulling Date Data You can do this by passing in `--cookie` instead of a date-file. -> [How to get Cookies?](../../common_docs/aquadx_how_to_get_token.md) +> [How to get Cookies?](../../common_docs/how_to_get_cookie_header.md) > > Get the cookies from this page: https://p.eagate.573.jp/game/sdvx/vi/playdata/profile/index.html -- cgit v1.2.3