2023-10-14 03:45:34 -04:00
import sys
2023-10-07 06:04:48 -04:00
import json
import os
import argparse
from cod_api import API , platforms
2023-12-25 07:49:55 -05:00
from cod_api . replacements import replacements
2023-10-08 07:23:27 -04:00
import asyncio
2023-10-14 03:45:34 -04:00
import datetime
2023-10-08 07:23:27 -04:00
# Prevent Async error from showing
if os . name == ' nt ' :
asyncio . set_event_loop_policy ( asyncio . WindowsSelectorEventLoopPolicy ( ) )
2023-10-07 06:04:48 -04:00
# Initiating the API class
api = API ( )
COOKIE_FILE = ' cookie.txt '
2023-10-14 03:45:34 -04:00
DIR_NAME = ' stats '
MATCH_DIR_NAME = ' matches '
2023-10-07 06:04:48 -04:00
2023-10-14 03:45:34 -04:00
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 )
2023-12-25 08:38:28 -05:00
def get_and_save_data ( player_name = None , all_stats = False , season_loot = False , identities = False , maps = False , info = False , friendFeed = False , eventFeed = False , cod_points = False , connected_accounts = False , settings = False ) :
2023-10-08 07:23:27 -04:00
# Create the stats directory if it doesn't exist
DIR_NAME = ' stats '
if not os . path . exists ( DIR_NAME ) :
os . makedirs ( DIR_NAME )
2023-10-07 06:04:48 -04:00
# 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 )
2023-12-25 08:38:28 -05:00
# Check if userInfo.json exists, create it if it doesn't
USER_INFO_FILE = os . path . join ( ' userInfo.json ' )
if not os . path . exists ( USER_INFO_FILE ) :
with open ( USER_INFO_FILE , ' w ' ) as f :
pass # Creates an empty file
2023-10-08 07:23:27 -04:00
# 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): " )
2023-10-07 06:04:48 -04:00
# Login with sso token
api . login ( api_key )
2023-10-14 03:45:34 -04:00
2023-10-07 06:04:48 -04:00
# Retrieve data from API
2023-10-14 03:45:34 -04:00
# First, determine if any specific optional arguments were given
2023-12-25 08:38:28 -05:00
if not ( all_stats or season_loot or identities or maps or info or friendFeed or eventFeed or cod_points or connected_accounts or settings ) :
2023-10-14 03:45:34 -04:00
# 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 )
season_loot_data = api . ModernWarfare . seasonLoot ( platforms . Activision , player_name )
identities_data = api . Me . loggedInIdentities ( )
2023-12-25 08:38:28 -05:00
map_list = api . ModernWarfare . mapList ( platforms . Activision )
2023-12-25 04:14:59 -05:00
info = api . Me . info ( )
friendFeed = api . Me . friendFeed ( )
eventFeed = api . Me . eventFeed ( )
2023-12-25 08:38:28 -05:00
cod_points = api . Me . codPoints ( )
2023-12-25 04:14:59 -05:00
connectedAccounts = api . Me . connectedAccounts ( )
settings = api . Me . settings ( )
2023-10-14 03:45:34 -04:00
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 ' )
2023-12-25 04:14:59 -05:00
save_to_file ( info , ' info.json ' )
save_to_file ( friendFeed , ' friendFeed.json ' )
save_to_file ( eventFeed , ' eventFeed.json ' )
2023-12-25 08:38:28 -05:00
save_to_file ( cod_points , ' cp.json ' )
2023-12-25 04:14:59 -05:00
save_to_file ( connectedAccounts , ' connectedAccounts.json ' )
save_to_file ( settings , ' settings.json ' )
2023-10-14 03:45:34 -04:00
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 ' )
2023-10-07 06:04:48 -04:00
2023-12-25 08:38:28 -05:00
if info :
info = api . Me . info ( )
save_to_file ( info , ' info.json ' )
if friendFeed :
friendFeed = api . Me . friendFeed ( )
save_to_file ( friendFeed , ' friendFeed.json ' )
if eventFeed :
eventFeed = api . Me . eventFeed ( )
save_to_file ( eventFeed , ' eventFeed.json ' )
if cod_points :
cod_points = api . Me . codPoints ( )
save_to_file ( cod_points , ' cp.json ' )
if connected_accounts :
connectedAccounts = api . Me . connectedAccounts ( )
save_to_file ( connectedAccounts , ' connectedAccounts.json ' )
if settings :
settings = api . Me . settings ( )
save_to_file ( settings , ' settings.json ' )
2023-10-14 03:45:34 -04:00
# Save results to a JSON file inside the stats directory
2023-12-25 07:49:55 -05:00
def recursive_key_replace ( obj ) :
2023-10-07 06:04:48 -04:00
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 )
2023-12-25 07:49:55 -05:00
new_obj [ new_key ] = recursive_key_replace ( new_value )
2023-10-07 06:04:48 -04:00
else :
2023-12-25 07:49:55 -05:00
new_obj [ new_key ] = recursive_key_replace ( value )
2023-10-07 06:04:48 -04:00
return new_obj
elif isinstance ( obj , list ) :
2023-12-25 07:49:55 -05:00
return [ recursive_key_replace ( item ) for item in obj ]
2023-10-07 06:04:48 -04:00
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
2023-10-14 03:45:34 -04:00
def replace_time_and_duration_recursive ( data ) :
"""
Recursively replace epoch times for specific keys in a nested dictionary or list .
"""
2023-11-21 05:15:15 -05:00
time_keys = [ " timePlayedTotal " , " timePlayed " , " objTime " , " time " , " timeProne " ,
" timeSpentAsPassenger " , " timeSpentAsDriver " , " timeOnPoint " ,
" timeWatchingKillcams " , " timeCrouched " , " timesSelectedAsSquadLeader " ,
" longestTimeSpentOnWeapon " , " avgLifeTime " , " percentTimeMoving " ]
2023-10-14 03:45:34 -04:00
if isinstance ( data , list ) :
for item in data :
replace_time_and_duration_recursive ( item )
elif isinstance ( data , dict ) :
for key , value in data . items ( ) :
2023-11-21 05:15:15 -05:00
if key in time_keys :
data [ key ] = convert_duration_seconds ( value )
elif key == " utcStartSeconds " :
2023-10-14 03:45:34 -04:00
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 " :
2023-11-21 05:15:15 -05:00
data [ key ] = convert_duration_milliseconds ( value )
2023-10-14 03:45:34 -04:00
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
2023-11-21 05:15:15 -05:00
def convert_duration_milliseconds ( milliseconds ) :
2023-10-14 03:45:34 -04:00
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 "
2023-11-21 05:15:15 -05:00
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 "
2023-10-07 06:04:48 -04:00
def beautify_data ( ) :
2023-10-14 03:45:34 -04:00
file_path = ( os . path . join ( DIR_NAME , ' stats.json ' ) )
2023-10-07 06:04:48 -04:00
with open ( file_path , ' r ' ) as file :
data = json . load ( file )
2023-11-21 05:15:15 -05:00
replace_time_and_duration_recursive ( data )
2023-12-25 07:49:55 -05:00
data = recursive_key_replace ( data )
2023-10-07 06:04:48 -04:00
data = sort_data ( data )
with open ( file_path , ' w ' ) as file :
json . dump ( data , file , indent = 4 )
2023-10-14 03:45:34 -04:00
print ( f " Keys sorted and replaced in { file_path } . " )
2023-10-07 06:04:48 -04:00
def beautify_match_data ( ) :
2023-10-14 03:45:34 -04:00
file_path = ( os . path . join ( DIR_NAME , ' match_info.json ' ) )
2023-10-07 06:04:48 -04:00
with open ( file_path , ' r ' ) as file :
data = json . load ( file )
2023-10-14 03:45:34 -04:00
replace_time_and_duration_recursive ( data )
2023-12-25 07:49:55 -05:00
data = recursive_key_replace ( data )
2023-10-07 06:04:48 -04:00
with open ( file_path , ' w ' ) as file :
json . dump ( data , file , indent = 4 )
2023-10-14 03:45:34 -04:00
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 )
2023-10-14 05:00:16 -04:00
# 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)
2023-10-14 03:45:34 -04:00
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 } . " )
2023-10-07 06:04:48 -04:00
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
"""
2023-10-14 03:45:34 -04:00
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 " )
2023-12-25 08:38:28 -05:00
group_data . add_argument ( " -id " , " --identities " , action = " store_true " , help = " Fetch only the logged-in identities data " )
2023-10-14 03:45:34 -04:00
group_data . add_argument ( " -m " , " --maps " , action = " store_true " , help = " Fetch only the map list data " )
2023-12-25 08:38:28 -05:00
group_data . add_argument ( " -i " , " --info " , action = " store_true " , help = " Fetch only general information " )
group_data . add_argument ( " -f " , " --friendFeed " , action = " store_true " , help = " Fetch only your friend feed " )
group_data . add_argument ( " -e " , " --eventFeed " , action = " store_true " , help = " Fetch only your event feed " )
group_data . add_argument ( " -cp " , " --cod_points " , action = " store_true " , help = " Fetch only your COD Point balance " )
group_data . add_argument ( " -ca " , " --connected_accounts " , action = " store_true " , help = " Fetch only the map list data " )
group_data . add_argument ( " -s " , " --settings " , action = " store_true " , help = " Fetch only your account settings " )
2023-10-07 06:04:48 -04:00
2023-10-14 03:45:34 -04:00
# 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 " )
2023-10-07 06:04:48 -04:00
args = parser . parse_args ( )
2023-10-14 03:45:34 -04:00
# 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 :
2023-10-07 06:04:48 -04:00
beautify_data ( )
beautify_match_data ( )
else :
2023-12-25 08:38:28 -05:00
get_and_save_data ( args . player_name , args . all_stats , args . season_loot , args . identities , args . maps , args . info , args . friendFeed , args . eventFeed , args . cod_points , args . connected_accounts , args . settings )
2023-10-07 06:04:48 -04:00
if __name__ == " __main__ " :
2023-10-08 07:23:27 -04:00
main ( )