import sys import json import os import argparse from cod_api import API, platforms import asyncio import datetime # Prevent Async error from showing if os.name == 'nt': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) replacements = { # Maps "career": "Career", "mp_hackney_yard": "Hackney Yard (Night)", "mp_aniyah": "Aniyah Palace", "mp_euphrates": "Euphrates Bridge", "mp_raid": "Grazna Raid", "mp_m_pine": "Pine", "mp_m_stack": "Stack", "mp_deadzone": "Arklov Peak", "mp_quarry": "Karst River Quarry", "mp_m_overunder": "Docks", "mp_cave_am": "Azhir Cave", "mp_cave": "Azhir Cave (Night)", "mp_runner": "Gun Runner", "mp_runner_pm": "Gun Runner (Night)", "mp_hackney_am": "Hackney Yard", "mp_piccadilly": "Piccadilly", "mp_spear": "Rammaza", "mp_spear_pm": "Rammaza (Night)", "mp_petrograd": "St. Petrograd", "mp_m_hill": "Hill", "mp_m_king": "King", "mp_m_speedball": "Speedball", "mp_m_showers": "Gulag Showers", "mp_downtown_gw": "Tarvosk District", "mp_m_speed": "Shoot House", "mp_farms2_gw": "Krovnik Farmland", "mp_port2_gw": "Port", "mp_crash": "Crash", "mp_vacant": "Vacant", "mp_shipment": "Shipment", "mp_m_cargo": "Cargo", "mp_m_cage": "Atrium", "mp_m_overwinter": "Docks", "mp_emporium": "Atlas Superstore", "mp_rust": "Rust", "mp_boneyard_gw": "Zhokov Boneyard", "mp_m_fork": "Bazaar", "mp_donetsk": "Verdansk", "mp_hideout": "Khandor Hideout", "loading_mp_hideout": "Khandor Hideout", "mp_aniyah_tac": "Aniyah Incursion", "mp_backlot": "Talsik Backlot", "mp_village": "Hovec Sawmill", "mp_hardhat": "Hardhat", "mp_m_wallco": "Aisle 9", "mp_donetsk": "Verdansk", "mp_scrapyard": "Zhokov Scrapyard", "mp_m_trench": "Trench", "mp_promenade_gw": "Barakett Promenade", "mp_don": "Verdansk", "mp_garden": "Cheshire Park", "mp_oilrig": "Petrov Oil Rig", "mp_harbor": "Suldal Harbor", "mp_layover_gw": "Verdansk International Airport", "mp_m_cornfield": "Livestock", "mp_m_stadium": "Verdansk Stadium", "mp_malyshev": "Mialstor Tank Factory", "mp_malyshev_10v10": "Mialstor Tank Factory", "mp_broadcast": "Broadcast", "mp_riverside_gw": "Verdansk Riverside", "mp_m_train": "Station", "mp_kstenod": "Verdansk (Night)", "mp_escape": "Rebirth", "mp_herat": "Al-Raab Airbase", "mp_killhouse": "Killhouse", "mp_m_drainage": "Drainage", # Gamemodes "war": "Team Deathmatch", "sd": "Search and Destroy", "dom": "Domination", "tdef": "Team Defender", "dm": "Free-for-all", "koth": "Hardpoint", "hq": "Headquarters", "arena": "Gunfight", "arm": "Ground War", "conf": "Kill Confirmed", "cyber": "Cyber Attack", "hc_war": "Team Deathmatch Hardcore", "hc_arena": "Gunfight Hardcore", "hc_arm": "Ground War Hardcore", "hc_conf": "Kill Confirmed Hardcore", "hc_cyber": "Cyber Attack Hardcore", "hc_dm": "Free-for-all Hardcore", "hc_hq": "Headquarters Hardcore", "hc_dom": "Domination Hardcore", "hc_sd": "Search and Destroy Hardcore", "cyber_hc": "Cyber Attack Hardcore", "war_hc": "Team Deathmatch Hardcore", "dom_hc": "Domination Hardcore", "sd_hc": "Search and Destroy Hardcore", "conf_hc": "Kill Confirmed Hardcore", "gun": "Gun Game", "gun_hc": "Gun Game Hardcore", "siege": "Reinforce", "infect": "Infected", "arena_osp": "Gunfight O.S.P.", "hq_hc": "Headquarters Hardcore", "grnd": "Grind", "grind": "Grind", "ctf": "Capture the Flag", "br_all": "All", "br": "Battle Royale", "br_dmz": "Plunder", "br_dmz_38": "Plunder Quads", "br_87": "BR Solos", "br_dmz_104": "Blood Money", "koth_hc": "Hardpoint Hardcore", "br_25": "BR Trios", "br_89": "BR Quads", "br_dmz_76": "Plunder Quads", "br_77": "BR Scopes & Scatterguns", "br_dmz_85": "Plunder Duos", "dd_hc": "Demolition Hardcore", "dd": "Demolition", "br_71": "BR Solos", "br_74": "BR Trios", "br_88": "BR Duos", "brtdm_113": "Warzone Rumble", "brtdm_rmbl": "Warzone Rumble", "br_brsolo": "BR Solos", "br_brduos": "BR Duos", "br_brtrios": "BR Trios", "br_brquads": "BR Quads", "br_dmz_plnbld": "Blood Money", "br_br_real": "Realism Battle Royale", "br_86": "Realism Battle Royale", "br_brthquad": "BR 200 Quads", "br_jugg_brtriojugr": "Juggernaut Royal Trios", "br_dmz_plunquad": "Plunder Quads", "br_dmz_bldmnytrio": "Blood Money Trios", "br_mini_miniroyale": "Mini Royale", "br_brbbsolo": "BR Buyback Solos", "br_jugg_brquadjugr": "Juggernaut Royal Quads", "br_kingslayer_kingsltrios": "King Slayer Trios", "br_truckwar_trwarsquads": "Armored Royale Quads", "br_zxp_zmbroy": "Zombie Royale", "br_brhwntrios": "BR Trick-Or-Trios", "rugby": "Onslaughter", "br_brsolohwn": "BR Solo Survivor", "br_dmz_plndcndy": "Plunder: Candy Collector", "br_jugg_jugpmpkn": "Juggourdnaut Royale", "br_rebirth_rbrthtrios": "Resurgence Trio", "br_rebirth_rbrthduos": "Resurgence Duos", "br_rebirth_rbrthquad": "Rebirth Resurgance Quads", "br_dmz_plndtrios": "Plunder Trios", "br_rebirth_resurgence_trios": "Verdansk Resurgence Trios", "br_mini_rebirth_mini_royale_quads": "Rebirth Mini Royale Quads", "br_bodycount_pwergrb": "Power Grab", "br_rebirth_resurgence_mini": "Verdansk Resurgence Mini", "br_payload_payload": "Payload", "br_mini_rebirth_mini_royale_trios": "Rebirth Mini Royale Trios", "br_x2_br_reveal_x2_event/event_title_x2": "Battle of Verdansk", "br_rumble_clash": "Clash", "br_dbd_dbd": "Iron Trials '84", "br_gxp_gov": "Ghosts of Verdansk", # Weapons "scorestreak": "Scorestreak", "equipment": "Equipment", "gear": "Gear", "weapon_bare_hands": "Bare Hands", "weapon_tactical_rifle": "Tactical Rifle", "weapon_shotgun": "Shotgun", "weapon_sniper": "Sniper", "weapon_lmg": "Light Machine Guns", "weapon_launcher": "Launcher", "weapon_pistol": "Pistol", "weapon_smg": "Submachine Guns", "weapon_melee": "Melee", "weapon_assault_rifle": "Assault Rifle", "attachments": "Attachments", "weapons": "Weapons", "specialist": "Specialist", "weapon": "Weapon", "weapon_special": "Special", "iw8_ar_akilo47": "AK-47", "iw8_ar_kilo433": "Kilo-141", "iw8_ar_mcharlie": "M13", "iw8_ar_falima": "FAL", "iw8_ar_asierra12": "Oden", "iw8_sm_mpapa7": "MP7", "iw8_sm_augolf": "AUG", "iw8_sm_uzulu": "Uzi", "iw8_sh_romeo870": "Model 680", "iw8_sh_charlie725": "725", "iw8_sh_aalpha12": "JAK-12", "iw8_sh_oscar12": "Origin 12", "iw8_lm_pkilo": "PKM", "iw8_lm_mgolf34": "MG34", "iw8_lm_lima86": "SA87", "iw8_lm_dblmg": "MP Juggernaut", "iw8_sn_mike14": "EBR-14", "iw8_sn_delta": "Dragunov", "iw8_sn_alpha50": "AX-50", "iw8_sn_hdromeo": "HDR", "iw8_sn_sbeta": "Mk2 Carbine", "iw8_pi_papa320": "M19", "iw8_pi_cpapa": ".357", "iw8_la_rpapa7": "RPG-7", "iw8_la_juliet": "JOKR", "iw8_la_gromeo": "PILA", "iw8_la_kgolf": "Strela-P", "iw8_me_riotshield": "Riot Shield", "equip_gas_grenade": "Gas Grenade", "equip_snapshot_grenade": "Snapshot Grenade", "equip_decoy": "Decoy Grenade", "equip_smoke": "Smoke Grenade", "equip_concussion": "Stun Grenade", "equip_hb_sensor": "Heartbeat Sensor", "equip_flash": "Flash Grenade", "equip_adrenaline": "Stim", "equip_frag": "Frag Grenade", "equip_thermite": "Thermite", "equip_semtex": "Semtex", "equip_claymore": "Claymore", "equip_c4": "C4", "equip_at_mine": "Proximity Mine", "equip_throwing_knife": "Throwing Knife", "equip_molotov": "Molotov Cocktail", "iw8_knife": "Combat Knife", "weapon_other": "Primary Melee", "iw8_ar_tango21": "RAM-7", "iw8_ar_falpha": "FR 5.56", "iw8_ar_mike4": "M4A1", "iw8_sm_papa90": "P90", "iw8_sm_mpapa5": "MP5", "iw8_sm_beta": "PP19 Bizon", "iw8_sh_dpapa12": "R9-0", "iw8_lm_mgolf36": "Holger-26", "iw8_sn_kilo98": "Kar98k", "iw8_pi_mike1911": "1911", "iw8_pi_golf21": "X16", "iw8_pi_decho": ".50 GS", "weapon_marksman": "Marksman Rifles", "iw8_lm_kilo121": "M91", "iw8_ar_scharlie": "FN Scar 17", "iw8_ar_sierra552": "Grau 5.56", "iw8_sm_smgolf45": "Striker 45", "iw8_pi_mike9a3": "Renetti", "iw8_lm_mkilo3": "Bruen MK9", "iw8_sh_mike26": "VLK Rogue", "iw8_sn_crossbow": "Crossbow", "iw8_sn_sksierra": "SKS", "iw8_ar_galima": "CR-56 AMAX", "iw8_me_kalistick": "Kali Sticks", "iw8_sm_victor": "Fennec Mk9", "iw8_sn_xmike109": "Rytec AMR", "iw8_pi_mike9": "Renetti", "iw8_me_akimboblunt": "Kali Sticks", "iw8_ar_anovember94": "AN-94", "iw8_sm_charlie9": "ISO", "iw8_me_akimboblades": "Dual Kodachis", "iw8_lm_sierrax": "FiNN", "iw8_ar_valpha": "AS VAL", "iw8_sn_romeo700": "SP-R 208", "cruise_predator": "Cruise Missile", "manual_turret": "Shield Turret", "toma_strike": "Cluster Strike", "sentry_gun": "Sentry Gun", "hover_jet": "VTOL Jet", "precision_airstrike": "Precision Airstrike", "juggernaut": "Juggernaut", "pac_sentry": "", "chopper_gunner": "Chopper Gunner", "gunship": "Gunship", "white_phosphorus": "White Phosphorus", "nuke": "Nuke", "chopper_support": "Support Helo", "bradley": "Infantry Assault Vehicle", "uav": "UAV", "directional_uav": "Advanced UAV", "airdrop": "Care Package", "airdrop_multiple": "Emergency Airdrop", "radar_drone_overwatch": "Personal Radar", "scrambler_drone_guard": "Counter UAV", "super_emp_drone": "EMP Drone", "super_trophy": "Trophy System", "super_ammo_drop": "Munitions Box", "super_weapon_drop": "Weapon Drop", "super_fulton": "Cash Deposit Balloon", "super_armor_drop": "Armor Box", "super_select": "Field Upgrade Pro (Any)", "super_tac_insert": "Tactical Insertion", "super_recon_drone": "Recon Drone", "super_deadsilence": "Dead Silence", "super_supply_drop": "Loadout Drop", ### Unsure if this is Loadout Drop "super_tac_cover": "Deployable Cover", "super_support_box": "Stopping Power Rounds", # Accolades # "accoladeData": "Accolades", # "classChanges": "Most classes changed (Evolver)", # "highestAvgAltitude": "Highest average altitude (High Command)", # "killsFromBehind": "Most kills from behind (Flanker)", # "lmgDeaths": "Most LMG deaths (Target Practice)", # "riotShieldDamageAbsorbed": "Most damage absorbed with Riot Shield (Guardian)", # "flashbangHits": "Most Flashbang hits (Blinder)", # "meleeKills": "Most Melee kills (Brawler)", # "tagsLargestBank": "Largest bank (Bank Account)", # "shotgunKills": "Most Shotgun kills (Buckshot)", # "sniperDeaths": "Most Sniper deaths (Zeroed In)", # "timeProne": "Most time spent Prone (Grassy Knoll)", # "killstreakWhitePhosphorousKillsAssists": "Most kills and assists with White Phosphorus (Burnout)", # "shortestLife": "Shortest life (Terminal)", # "deathsFromBehind": "Most deaths from behind (Blindsided)", # "higherRankedKills": "Most kills on higher ranked scoreboard players (Upriser)", # "mostAssists": "Most assists (Wingman)", # "leastKills": "Fewest kills (The Fearful)", # "tagsDenied": "Denied the most tags (Denied)", # "killstreakWheelsonKills": "Most Wheelson kills", # "sniperHeadshots": "Most Sniper headshots (Dead Aim)", # "killstreakJuggernautKills": "Most Juggernaut kills (Heavy Metal)", # "smokesUsed": "Most Smoke Grenades used (Chimney)", # "avengerKills": "Most avenger kills (Avenger)", # "decoyHits": "Most Decoy Grenade hits (Made You Look)", # "killstreakCarePackageUsed": "Most Care Packages called in (Helping Hand)", # "molotovKills": "Most Molotov kills (Arsonist)", # "gasHits": "Most Gas Grenade hits (Gaseous)", # "comebackKills": "Most comebacks (Rally)", # "lmgHeadshots": "Most LMG headshots (LMG Expert)", # "smgDeaths": "Most SMG deaths (Run and Gunned)", # "carrierKills": "Most kills as carrier (Carrier)", # "deployableCoverUsed": "Most Deployable Covers used (Combat Engineer)", # "thermiteKills": "Most Thermite kills (Red Iron)", # "arKills": "Most assault rifle kills (AR Specialist)", # "c4Kills": "Most C4 kills (Handle With Care)", # "suicides": "Most suicides (Accident Prone)", # "clutch": "Most kills as the last alive (Clutched)", # "survivorKills": "Most kills as survivor (Survivalist)", # "killstreakGunshipKills": "Most Gunship kills (Death From Above)", # "timeSpentAsPassenger": "Most time spent as a passenger (Navigator)", # "returns": "Most flags returned (Flag Returner)", # "smgHeadshots": "Most SMG headshots (SMG Expert)", # "launcherDeaths": "Most launcher deaths (Fubar)", # "oneShotOneKills": "Most one shot kills (One Shot Kill)", # "ammoBoxUsed": "Most Munitions Boxes used (Provider)", # #"spawnSelectSquad": "", # "weaponPickups": "Most picked up weapons (Loaner)", # "pointBlankKills": "Most point blank kills (Personal Space)", # "tagsCaptured": "Collected the most tags (Confirmed Kills)", # "killstreakGroundKills": "Most ground based killstreak kills (Ground Control)", # "distanceTraveledInVehicle": "Longest distance travelled in a vehicle (Cross Country)", # "longestLife": "Longest life (Lifer)", # "stunHits": "Most Stun Grenade hits (Stunner)", # "spawnSelectFlag": "Most FOB Spawns (Objective Focused)", # Unsure # "shotgunHeadshots": "Most Shotgun headshots (Boomstick)", # "bombDefused": "Most defuses (Defuser)", # "snapshotHits": "Most Snapshot Grenade hits (Photographer)", # "noKillsWithDeath": "No kills with at least 1 death (Participant)", # "killstreakAUAVAssists": "Most Advanced UAV assists (Target Rich Environment)", # "killstreakPersonalUAVKills": "Most kills with a Personal Radar active (Nothing Personal)", # "tacticalInsertionSpawns": "Most Tactical Insertions used (Revenant)", # "launcherKills": "Most Launcher kills (Explosive)", # "spawnSelectVehicle": "Most vehicle spawns (Oscar Mike)", # "mostKillsLeastDeaths": "Most kills and fewest deaths (MVP)", # "mostKills": "Most kills (The Feared)", # "defends": "Most defend kills (Defense)", # "timeSpentAsDriver": "Most time spent driving (Driver)", # "": "" # WIP - Still adding more } # Initiating the API class api = API() COOKIE_FILE = 'cookie.txt' DIR_NAME = 'stats' MATCH_DIR_NAME = 'matches' def save_to_file(data, filename, dir_name='stats'): """Utility function to save data to a JSON file.""" with open(os.path.join(dir_name, filename), 'w') as json_file: json.dump(data, json_file, indent=4) def get_and_save_data(player_name=None, all_stats=False, season_loot=False, identities=False, maps=False): # Create the stats directory if it doesn't exist DIR_NAME = 'stats' if not os.path.exists(DIR_NAME): os.makedirs(DIR_NAME) # Check if cookie file exists if os.path.exists(COOKIE_FILE): with open(COOKIE_FILE, 'r') as f: api_key = f.read().strip() else: api_key = input("Please enter your ACT_SSO_COOKIE: ") with open(COOKIE_FILE, 'w') as f: f.write(api_key) # If player_name is not provided via command line, get it from user input if not player_name: player_name = input("Please enter the player's username (with #1234567): ") # Login with sso token api.login(api_key) # Retrieve data from API # First, determine if any specific optional arguments were given if not (all_stats or season_loot or identities or maps): # If no specific optional arguments are given, then default behavior: player_stats = api.ModernWarfare.fullData(platforms.Activision, player_name) match_info = api.ModernWarfare.combatHistory(platforms.Activision, player_name) save_to_file(player_stats, 'stats.json') save_to_file(match_info, 'match_info.json') elif all_stats: # If the all_stats argument is given: player_stats = api.ModernWarfare.fullData(platforms.Activision, player_name) match_info = api.ModernWarfare.combatHistory(platforms.Activision, player_name) match_info = api.ModernWarfare.combatHistory(platforms.Activision, player_name) season_loot_data = api.ModernWarfare.seasonLoot(platforms.Activision, player_name) map_list = api.ModernWarfare.mapList(platforms.Activision) identities_data = api.Me.loggedInIdentities() info = api.Me.info() friendFeed = api.Me.friendFeed() eventFeed = api.Me.eventFeed() cp = cod_points = api.Me.codPoints() connectedAccounts = api.Me.connectedAccounts() settings = api.Me.settings() save_to_file(player_stats, 'stats.json') save_to_file(match_info, 'match_info.json') save_to_file(season_loot_data, 'season_loot.json') save_to_file(map_list, 'map_list.json') save_to_file(identities_data, 'identities.json') save_to_file(info, 'info.json') save_to_file(friendFeed, 'friendFeed.json') save_to_file(eventFeed, 'eventFeed.json') save_to_file(cp, 'cp.json') save_to_file(connectedAccounts, 'connectedAccounts.json') save_to_file(settings, 'settings.json') else: # For other specific optional arguments: if season_loot: season_loot_data = api.ModernWarfare.seasonLoot(platforms.Activision, player_name) save_to_file(season_loot_data, 'season_loot.json') if identities: identities_data = api.Me.loggedInIdentities() save_to_file(identities_data, 'identities.json') if maps: map_list = api.ModernWarfare.mapList(platforms.Activision) save_to_file(map_list, 'map_list.json') # Save results to a JSON file inside the stats directory def recursive_key_replace(obj, replacements): if isinstance(obj, dict): new_obj = {} for key, value in obj.items(): new_key = replacements.get(key, key) if isinstance(value, str): new_value = replacements.get(value, value) new_obj[new_key] = recursive_key_replace(new_value, replacements) else: new_obj[new_key] = recursive_key_replace(value, replacements) return new_obj elif isinstance(obj, list): return [recursive_key_replace(item, replacements) for item in obj] else: return replacements.get(obj, obj) if isinstance(obj, str) else obj def sort_data(data): if isinstance(data, dict): for key, value in data.items(): if key == "mode": data[key] = dict(sorted(value.items(), key=lambda item: item[1]['properties']['timePlayed'], reverse=True)) elif key in ["Assault Rifles", "Shotguns", "Marksman Rifles", "Snipers", "LMGs", "Launchers", "Pistols", "SMGs", "Melee"]: data[key] = dict(sorted(value.items(), key=lambda item: item[1]['properties']['kills'], reverse=True)) elif key in ["Field Upgrades"]: data[key] = dict(sorted(value.items(), key=lambda item: item[1]['properties']['uses'], reverse=True)) elif key in ["Tactical Equipment", "Lethal Equipment"]: data[key] = dict(sorted(value.items(), key=lambda item: item[1]['properties']['uses'], reverse=True)) elif key == "Scorestreaks": for subcategory, scorestreaks in value.items(): data[key][subcategory] = dict(sorted(scorestreaks.items(), key=lambda item: item[1]['properties']['awardedCount'], reverse=True)) elif key == "Accolades": if 'properties' in value: data[key]['properties'] = dict(sorted(value['properties'].items(), key=lambda item: item[1], reverse=True)) else: # Recursive call to handle nested dictionaries data[key] = sort_data(value) return data def replace_time_and_duration_recursive(data): """ Recursively replace epoch times for specific keys in a nested dictionary or list. """ time_keys = ["timePlayedTotal", "timePlayed", "objTime", "time", "timeProne", "timeSpentAsPassenger", "timeSpentAsDriver", "timeOnPoint", "timeWatchingKillcams", "timeCrouched", "timesSelectedAsSquadLeader", "longestTimeSpentOnWeapon", "avgLifeTime", "percentTimeMoving"] if isinstance(data, list): for item in data: replace_time_and_duration_recursive(item) elif isinstance(data, dict): for key, value in data.items(): if key in time_keys: data[key] = convert_duration_seconds(value) elif key == "utcStartSeconds": data[key] = epoch_to_human_readable(value) # For EST conversion: # data[key] = epoch_to_human_readable(value, "EST") elif key == "utcEndSeconds": data[key] = epoch_to_human_readable(value) # For EST conversion: # data[key] = epoch_to_human_readable(value, "EST") elif key == "duration": data[key] = convert_duration_milliseconds(value) else: replace_time_and_duration_recursive(value) def epoch_to_human_readable(epoch_timestamp, timezone='GMT'): if isinstance(epoch_timestamp, str): return epoch_timestamp # Already converted dt_object = datetime.datetime.utcfromtimestamp(epoch_timestamp) if timezone == 'GMT': date_str = dt_object.strftime("GMT: %A, %B %d, %Y %I:%M:%S %p") elif timezone == 'EST': dt_object -= datetime.timedelta(hours=4) # Using 4 hours for EST conversion instead of 5? date_str = dt_object.strftime("EST: %A, %B %d, %Y %I:%M:%S %p") else: raise ValueError("Unsupported timezone.") return date_str def convert_duration_milliseconds(milliseconds): if isinstance(milliseconds, str) and "Minutes" in milliseconds: return milliseconds # Already converted seconds, milliseconds = divmod(milliseconds, 1000) minutes, seconds = divmod(seconds, 60) return f"{minutes} Minutes {seconds} Seconds {milliseconds} Milliseconds" def convert_duration_seconds(seconds): """ Convert duration from seconds to a string format with days, minutes, seconds, and milliseconds. """ if isinstance(seconds, str): return seconds # Already converted days, seconds = divmod(seconds, 86400) hours, seconds = divmod(seconds, 3600) minutes, seconds = divmod(seconds, 60) # Convert to integers to remove decimal places days = int(days) hours = int(hours) minutes = int(minutes) seconds = int(seconds) return f"{days} Days {hours} Hours {minutes} Minutes {seconds} Seconds" def beautify_data(): file_path = (os.path.join(DIR_NAME, 'stats.json')) with open(file_path, 'r') as file: data = json.load(file) replace_time_and_duration_recursive(data) data = recursive_key_replace(data, replacements) data = sort_data(data) with open(file_path, 'w') as file: json.dump(data, file, indent=4) print(f"Keys sorted and replaced in {file_path}.") def beautify_match_data(): file_path = (os.path.join(DIR_NAME, 'match_info.json')) with open(file_path, 'r') as file: data = json.load(file) replace_time_and_duration_recursive(data) data = recursive_key_replace(data, replacements) with open(file_path, 'w') as file: json.dump(data, file, indent=4) print(f"Keys replaced in {file_path}.") def split_matches_into_files(): """ Split the matches in match_info.json into separate files. """ MATCHES_DIR = os.path.join(DIR_NAME, MATCH_DIR_NAME) # Create matches directory if it doesn't exist if not os.path.exists(MATCHES_DIR): os.makedirs(MATCHES_DIR) # Load the match_info data with open(os.path.join(DIR_NAME, 'match_info.json'), 'r') as file: data = json.load(file) matches = data.get('data', {}).get('matches', []) # Correct the key to access matches # Check if data needs cleaning sample_match = matches[0] if matches else {} if (isinstance(sample_match.get("utcStartSeconds"), int) or isinstance(sample_match.get("utcEndSeconds"), int) or isinstance(sample_match.get("duration"), int)): print("Cleaning match data...") replace_time_and_duration_recursive(data) # Save the cleaned data back to match_info.json with open(os.path.join(DIR_NAME, 'match_info.json'), 'w') as file: json.dump(data, file, indent=4) # Split and save each match into a separate file for idx, match in enumerate(matches): # Create a copy of the match to ensure we don't modify the original data match_copy = dict(match) # Remove the 'loadouts' subkey from the 'player' key to avoid the cascading data match_copy['player'].pop('loadouts', None) match_copy['player'].pop('loadout', None) # Remove the entire player subkey to avoid the cascading data, if you want to exclude more, add them here # match_copy.pop('player', None) file_name = f"match_{idx + 1}.json" with open(os.path.join(MATCHES_DIR, file_name), 'w') as match_file: json.dump(match_copy, match_file, indent=4) print(f"Matches split into {len(matches)} separate files in {MATCHES_DIR}.") def main(): # Define the block of quote text to display in the help command help_text = """ Obtaining your ACT_SSO_COOKIE - Go to https://www.callofduty.com and login with your account - Once logged in, press F12 for your browsers developer tools. Then go to Application --> Storage --> Cookies --> https://www.callofduty.com and find ACT_SSO_COOKIE - Enter the value when prompted """ parser = argparse.ArgumentParser(description="Detailed Modern Warfare (2019) Statistics Tool", epilog=help_text, formatter_class=argparse.RawDescriptionHelpFormatter) # Group related arguments group_data = parser.add_argument_group("Data Fetching Options") group_cleaning = parser.add_argument_group("Data Cleaning Options") # Add arguments for Data Fetching Options group_data.add_argument("-p", "--player_name", type=str, help="Player's username (with #1234567)") group_data.add_argument("-a", "--all_stats", action="store_true", help="Fetch all the different types of stats data") group_data.add_argument("-sl", "--season_loot", action="store_true", help="Fetch only the season loot data") group_data.add_argument("-i", "--identities", action="store_true", help="Fetch only the logged-in identities data") group_data.add_argument("-m", "--maps", action="store_true", help="Fetch only the map list data") # Add arguments for Cleaning Options group_cleaning.add_argument("-c", "--clean", action="store_true", help="Beautify all data") group_cleaning.add_argument("-sm", "--split_matches", action="store_true", help="Split the matches into separate JSON files within the 'matches' subfolder") group_cleaning.add_argument("-csd", "--clean_stats_data", action="store_true", help="Beautify the data and convert to human-readable strings in stats.json") group_cleaning.add_argument("-cmd", "--clean_match_data", action="store_true", help="Beautify the match data and convert to human-readable strings in match_info.json") args = parser.parse_args() # Custom error handling # try: # args = parser.parse_args() # except SystemExit: # # Check if 'player_name' is in sys.argv, if not, raise exception # if '--player_name' not in sys.argv and '-p' not in sys.argv: # print('You must specify a player name!') # # Otherwise, re-raise the error or print the default error message. # sys.exit(1) if args.split_matches: split_matches_into_files() elif args.clean_stats_data: beautify_data() elif args.clean_match_data: beautify_match_data() elif args.clean: beautify_data() beautify_match_data() else: get_and_save_data(args.player_name, args.all_stats, args.season_loot, args.identities, args.maps) if __name__ == "__main__": main()