import csv import json import argparse import time DIFFICULTY_MAPPING = { "NOVICE": "NOV", "ADVANCED": "ADV", "EXHAUST": "EXH", "INFINITE": "INF", "GRAVITY": "GRV", "HEAVENLY": "HVN", "VIVID": "VVD", "EXCEED": "XCD", "MAXIMUM": "MXM" } LAMP_MAPPING = { "PLAYED": "FAILED", "COMPLETE": "CLEAR", } def create_date_dict(raw_site_data: str) -> dict: from bs4 import BeautifulSoup from datetime import datetime import pytz soup = BeautifulSoup(raw_site_data, 'html.parser') cat_divs = soup.find_all('div', class_='cat')[5:] jst = pytz.timezone('Asia/Tokyo') date_data = {} for div in cat_divs: inners = div.find_all('div', class_='inner') date = inners[0].get_text(strip=True) music_name = inners[1].find('p', class_='music_name').get_text(strip=True) date_obj = datetime.strptime(date, "%Y/%m/%d %H:%M:%S") date_obj_jst = jst.localize(date_obj) date_obj_unixtime = int(date_obj_jst.timestamp() * 1000) date_data[music_name] = date_obj_unixtime return date_data def convert_sdvx_csv_to_tachi_json(csv_file, game, playtype, service, unixtime, date_dict): 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") == "1": lamp = "ULTIMATE CHAIN" if row.get("PERFECT") == "1": lamp = "PERFECT ULTIMATE CHAIN" time_stamp = date_dict.get(row["楽曲名"], unixtime if unixtime != "now" else int(time.time())*1000) score_entry = { "score": int(row["ハイスコア"]), "lamp": lamp, "matchType": "songTitle", "identifier": row["楽曲名"], "difficulty": DIFFICULTY_MAPPING[row["難易度"].upper()], "timeAchieved": time_stamp } 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__": print("!!!!! WARNING !!!!!") print("Please make sure you have read the README about SDVX's official CSV files before importing") print("If you pass in a date file make sure the HTML is unformatted otherwise the spacing for song names will be wrong!") 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") parser.add_argument("-t", "--time", help="UNIX time (in milliseconds) that should be added to the scores. Defaults to current UNIX time", default=None) parser.add_argument("-d", "--date_file", help="SDVX e-amusement profile site saved HTML. See README in sdvx/eamuse_csv for instructions. Overrides with --time input if missing", required=False) parser.add_argument("-tz", "--timezone", help="Only needed if -d is used, specifies what timezone to convert to", required=False) args = parser.parse_args() curr_time = int(time.time() * 1000) if (args.time == "now" or args.time is None) else args.time if args.date_file and not args.timezone: print("ERROR: A date file is provided but no target timezone (-tz) has been specified. Please pass the timezone in which you played the scores") exit(1) date_data = {} if args.date_file: with open(args.date_file, "r") as file: site_data = file.read() date_data = create_date_dict(site_data) try: output_json = convert_sdvx_csv_to_tachi_json(args.csv_filename, "sdvx", "Single", args.service, curr_time, date_data) 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)}")