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)}")