import re import sys import json import os import argparse from cod_api import API, platforms import asyncio import datetime # Configure asyncio for Windows if os.name == 'nt': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # Constants COOKIE_FILE = 'cookie.txt' STATS_DIR = 'stats' MATCH_DIR = 'matches' REPLACEMENTS_FILE = 'data/replacements.json' TIMEZONE_OPTIONS = ["GMT", "EST", "CST", "PST"] # Initialize API api = API() class CodStatsManager: """Main class to manage COD API interactions and data processing.""" def __init__(self): self._ensure_directories_exist() self.replacements = self._load_replacements() self.api_key = self._get_api_key() api.login(self.api_key) def _ensure_directories_exist(self): """Ensure necessary directories exist.""" if not os.path.exists(STATS_DIR): os.makedirs(STATS_DIR) match_dir_path = os.path.join(STATS_DIR, MATCH_DIR) if not os.path.exists(match_dir_path): os.makedirs(match_dir_path) def _load_replacements(self): """Load replacements from the JSON file.""" # First, handle running as PyInstaller executable if getattr(sys, 'frozen', False): # If running as bundle (frozen) bundle_dir = sys._MEIPASS replacements_path = os.path.join(bundle_dir, 'data', 'replacements.json') else: # If running as a normal Python script replacements_path = REPLACEMENTS_FILE if not os.path.exists(replacements_path): raise FileNotFoundError(f"{replacements_path} not found. Ensure it exists in the script's directory.") with open(replacements_path, 'r') as file: return json.load(file) def _get_api_key(self): """Get API key from file or user input.""" if os.path.exists(COOKIE_FILE): with open(COOKIE_FILE, 'r') as f: return f.read().strip() else: api_key = input("Please enter your ACT_SSO_COOKIE: ") with open(COOKIE_FILE, 'w') as f: f.write(api_key) return api_key def save_to_file(self, data, filename): """Save data to a JSON file.""" file_path = os.path.join(STATS_DIR, filename) with open(file_path, 'w') as json_file: json.dump(data, json_file, indent=4) print(f"Data saved to {file_path}") def get_player_name(self): """Get player name from user input.""" return input("Please enter the player's username (with #1234567): ") def fetch_data(self, player_name=None, **options): """Fetch data based on specified options.""" if not player_name: player_name = self.get_player_name() # If no specific option is selected, fetch basic stats if not any(options.values()): self._fetch_basic_stats(player_name) return # If all_stats option is selected, fetch everything if options.get('all_stats'): self._fetch_all_stats(player_name) return # Otherwise, fetch only requested data self._fetch_specific_data(player_name, options) def _fetch_basic_stats(self, player_name): """Fetch basic player stats and match history.""" player_stats = api.ModernWarfare.fullData(platforms.Activision, player_name) match_info = api.ModernWarfare.combatHistory(platforms.Activision, player_name) self.save_to_file(player_stats, 'stats.json') self.save_to_file(match_info, 'match_info.json') def _fetch_all_stats(self, player_name): """Fetch all available stats for a player.""" # Basic stats 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) # Player-specific data identities_data = api.Me.loggedInIdentities() map_list = api.ModernWarfare.mapList(platforms.Activision) # Save basic data self.save_to_file(player_stats, 'stats.json') self.save_to_file(match_info, 'match_info.json') self.save_to_file(season_loot_data, 'season_loot.json') self.save_to_file(map_list, 'map_list.json') self.save_to_file(identities_data, 'identities.json') # Check if userInfo.json exists to determine if we should fetch additional data user_info_file = os.path.join('userInfo.json') if os.path.exists(user_info_file): # Additional user data info = api.Me.info() friend_feed = api.Me.friendFeed() event_feed = api.Me.eventFeed() cod_points = api.Me.codPoints() connected_accounts = api.Me.connectedAccounts() settings = api.Me.settings() # Save additional data self.save_to_file(info, 'info.json') self.save_to_file(friend_feed, 'friendFeed.json') self.save_to_file(event_feed, 'eventFeed.json') self.save_to_file(cod_points, 'cp.json') self.save_to_file(connected_accounts, 'connectedAccounts.json') def _fetch_specific_data(self, player_name, options): """Fetch specific data based on provided options.""" endpoints = { 'season_loot': (api.ModernWarfare.seasonLoot, [platforms.Activision, player_name], 'season_loot.json'), 'identities': (api.Me.loggedInIdentities, [], 'identities.json'), 'maps': (api.ModernWarfare.mapList, [platforms.Activision], 'map_list.json'), 'info': (api.Me.info, [], 'info.json'), 'friendFeed': (api.Me.friendFeed, [], 'friendFeed.json'), 'eventFeed': (api.Me.eventFeed, [], 'eventFeed.json'), 'cod_points': (api.Me.codPoints, [], 'cp.json'), 'connected_accounts': (api.Me.connectedAccounts, [], 'connectedAccounts.json'), 'settings': (api.Me.settings, [], 'settings.json') } for option, value in options.items(): if value and option in endpoints: func, args, filename = endpoints[option] data = func(*args) self.save_to_file(data, filename) def beautify_all_data(self, timezone='GMT'): """Beautify all data files.""" self.beautify_stats_data(timezone) self.beautify_match_data(timezone) self.beautify_feed_data(timezone) self.clean_json_files('friendFeed.json', 'eventFeed.json') print("All data beautified successfully.") def beautify_stats_data(self, timezone='GMT'): """Beautify stats data.""" file_path = os.path.join(STATS_DIR, 'stats.json') if not os.path.exists(file_path): print(f"File {file_path} not found. Skipping beautification.") return with open(file_path, 'r') as file: data = json.load(file) # Convert times and durations self._replace_time_and_duration_recursive(data, timezone) # Replace keys with more readable names data = self._recursive_key_replace(data) # Sort data by relevant metrics data = self._sort_data(data) # Save modified 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(self, timezone='GMT'): """Beautify match data.""" file_path = os.path.join(STATS_DIR, 'match_info.json') if not os.path.exists(file_path): print(f"File {file_path} not found. Skipping beautification.") return with open(file_path, 'r') as file: data = json.load(file) # Convert times and durations self._replace_time_and_duration_recursive(data, timezone) # Replace keys with more readable names data = self._recursive_key_replace(data) # Save modified data with open(file_path, 'w') as file: json.dump(data, file, indent=4) print(f"Keys replaced in {file_path}.") def beautify_feed_data(self, timezone='GMT'): """Beautify feed data files.""" for feed_file in ['friendFeed.json', 'eventFeed.json']: file_path = os.path.join(STATS_DIR, feed_file) if not os.path.exists(file_path): print(f"{feed_file} does not exist, skipping.") continue with open(file_path, 'r') as file: data = json.load(file) # Convert times and durations self._replace_time_and_duration_recursive(data, timezone) # Replace keys with more readable names data = self._recursive_key_replace(data) # Save modified data with open(file_path, 'w') as file: json.dump(data, file, indent=4) print(f"Keys sorted and replaced in {feed_file}.") def split_matches_into_files(self, timezone='GMT'): """Split match data into separate files.""" matches_dir = os.path.join(STATS_DIR, MATCH_DIR) if not os.path.exists(matches_dir): os.makedirs(matches_dir) match_info_path = os.path.join(STATS_DIR, 'match_info.json') if not os.path.exists(match_info_path): print(f"Match info file not found at {match_info_path}. Skipping split.") return with open(match_info_path, 'r') as file: data = json.load(file) matches = data.get('data', {}).get('matches', []) if not matches: print("No matches found to split.") return # Check if time conversion is needed sample_match = matches[0] needs_time_conversion = ( isinstance(sample_match.get("utcStartSeconds"), int) or isinstance(sample_match.get("utcEndSeconds"), int) or isinstance(sample_match.get("duration"), int) ) if needs_time_conversion: print("Converting match timestamps to human-readable format...") self._replace_time_and_duration_recursive(data, timezone) # Update the main match file with open(match_info_path, 'w') as file: json.dump(data, file, indent=4) # Process each match for idx, match in enumerate(matches): # Create a copy to avoid modifying the original data match_copy = dict(match) # Remove large loadout data to keep files smaller if 'player' in match_copy: match_copy['player'].pop('loadouts', None) match_copy['player'].pop('loadout', None) # Save to individual file file_name = f"match_{idx + 1}.json" file_path = os.path.join(matches_dir, file_name) with open(file_path, '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 clean_json_files(self, *filenames): """Clean JSON files by removing HTML-like tags and entities.""" regex_pattern = r'<span class="|</span>|">|mp-stat-items|kills-value|headshots-value|username|game-mode|kdr-value|accuracy-value|defends-value' replace = '' for filename in filenames: file_path = os.path.join(STATS_DIR, filename) if not os.path.exists(file_path): print(f"{filename} does not exist, skipping.") continue with open(file_path, 'r') as file: content = file.read() # Replace unwanted patterns modified_content = re.sub(regex_pattern, replace, content) # Save cleaned content with open(file_path, 'w') as file: file.write(modified_content) print(f"Removed unreadable strings from {filename}.") def _recursive_key_replace(self, obj): """Recursively replace keys and values with more readable versions.""" if isinstance(obj, dict): new_obj = {} for key, value in obj.items(): new_key = self.replacements.get(key, key) if isinstance(value, str): new_value = self.replacements.get(value, value) new_obj[new_key] = self._recursive_key_replace(new_value) else: new_obj[new_key] = self._recursive_key_replace(value) return new_obj elif isinstance(obj, list): return [self._recursive_key_replace(item) for item in obj] else: return self.replacements.get(obj, obj) if isinstance(obj, str) else obj def _sort_data(self, data): """Sort data by meaningful metrics for better readability.""" if isinstance(data, dict): for key, value in data.items(): if key == "mode": # Sort game modes by time played 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"]: # Sort weapons by kills data[key] = dict(sorted( value.items(), key=lambda item: item[1]['properties']['kills'], reverse=True )) elif key in ["Field Upgrades", "Tactical Equipment", "Lethal Equipment"]: # Sort equipment by uses data[key] = dict(sorted( value.items(), key=lambda item: item[1]['properties']['uses'], reverse=True )) elif key == "Scorestreaks": # Sort scorestreaks by awarded count 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": # Sort accolades by count if 'properties' in value: data[key]['properties'] = dict(sorted( value['properties'].items(), key=lambda item: item[1], reverse=True )) else: # Recursively sort nested data data[key] = self._sort_data(value) return data def _replace_time_and_duration_recursive(self, data, timezone): """Recursively replace epoch times with human-readable formats.""" time_keys = [ "timePlayedTotal", "timePlayed", "objTime", "time", "timeProne", "timeSpentAsPassenger", "timeSpentAsDriver", "timeOnPoint", "timeWatchingKillcams", "timeCrouched", "timesSelectedAsSquadLeader", "longestTimeSpentOnWeapon", "avgLifeTime", "percentTimeMoving" ] date_keys = ["date", "updated", "originalDate", "dateAdded"] if isinstance(data, list): # Sort lists containing items with dateAdded if data and isinstance(data[0], dict) and "dateAdded" in data[0]: data.sort(key=lambda x: x.get("dateAdded", 0), reverse=True) for item in data: self._replace_time_and_duration_recursive(item, timezone) elif isinstance(data, dict): for key, value in data.items(): if key in date_keys: if key == "dateAdded": data[key] = self._epoch_to_human_readable(value, timezone) else: data[key] = self._epoch_milli_to_human_readable(value, timezone) elif key in time_keys: data[key] = self._convert_duration_seconds(value) elif key == "utcStartSeconds": data[key] = self._epoch_to_human_readable(value, timezone) elif key == "utcEndSeconds": data[key] = self._epoch_to_human_readable(value, timezone) elif key == "duration": data[key] = self._convert_duration_milliseconds(value) else: self._replace_time_and_duration_recursive(value, timezone) def _epoch_milli_to_human_readable(self, epoch_millis, timezone='GMT'): """Convert epoch milliseconds to human-readable date string.""" if isinstance(epoch_millis, str): return epoch_millis dt_object = datetime.datetime.utcfromtimestamp(epoch_millis / 1000.0) return self._format_datetime(dt_object, timezone) def _epoch_to_human_readable(self, epoch_timestamp, timezone='GMT'): """Convert epoch seconds to human-readable date string.""" if isinstance(epoch_timestamp, str): return epoch_timestamp dt_object = datetime.datetime.utcfromtimestamp(epoch_timestamp) return self._format_datetime(dt_object, timezone) def _format_datetime(self, dt_object, timezone): """Format datetime object based on timezone.""" timezone_offsets = { 'GMT': 0, 'EST': -5, # Changed from -4 to -5 for Eastern Standard Time 'CST': -6, # Updated for consistency 'PST': -8 } if timezone not in timezone_offsets: raise ValueError(f"Unsupported timezone: {timezone}") # Apply timezone offset dt_object = dt_object + datetime.timedelta(hours=timezone_offsets[timezone]) # Format date string return f"{timezone}: {dt_object.strftime('%A, %B %d, %Y %I:%M:%S %p')}" def _convert_duration_milliseconds(self, milliseconds): """Convert milliseconds to a human-readable duration string.""" 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(self, seconds): """Convert seconds to a human-readable duration string.""" if isinstance(seconds, str): return seconds # Already converted days, seconds = divmod(seconds, 86400) hours, seconds = divmod(seconds, 3600) minutes, seconds = divmod(seconds, 60) days = int(days) hours = int(hours) minutes = int(minutes) seconds = int(seconds) return f"{days} Days {hours} Hours {minutes} Minutes {seconds} Seconds" def _ensure_json_serializable(self, obj): """Recursively convert objects to JSON serializable types.""" if isinstance(obj, dict): return {key: self._ensure_json_serializable(value) for key, value in obj.items()} elif isinstance(obj, list): return [self._ensure_json_serializable(item) for item in obj] elif isinstance(obj, tuple): return [self._ensure_json_serializable(item) for item in obj] elif isinstance(obj, (int, float, str, bool, type(None))): return obj else: # Convert any other type to string representation return str(obj) class CLI: """Command Line Interface manager.""" def __init__(self, stats_manager): self.stats_manager = stats_manager self.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 """ def display_menu(self): """Display the main menu and get user choice.""" print("\nBeautify Options:") print("1) Beautify all data") print("2) Split matches into separate files") print("\nOptions Requiring Player Name:") print("3) Get all stats") print("4) Get identities") print("5) Get general information") print("6) Get friend feed") print("7) Get event feed") print("8) Get COD Point balance") print("9) Get connected accounts") print("10) Get account settings") print("\nOptions Not Requiring Player Name:") print("11) Get season loot") print("12) Get map list") print("\n0) Exit") try: choice = int(input("Enter your choice: ")) return choice except ValueError: print("Please enter a valid number.") return -1 def handle_menu_choice(self, choice): """Handle the user's menu choice.""" if choice == 0: print("Exiting...") return False if choice in [3, 4, 5, 6, 7, 8, 9, 10, 11]: player_name = input("Please enter the player's username (with #1234567): ") options = { 3: {'all_stats': True}, 4: {'season_loot': True}, 5: {'identities': True}, 6: {'info': True}, 7: {'friendFeed': True}, 8: {'eventFeed': True}, 9: {'cod_points': True}, 10: {'connected_accounts': True}, 11: {'settings': True} } if choice in options: self.stats_manager.fetch_data(player_name=player_name, **options[choice]) elif choice == 1: self.stats_manager.beautify_all_data() elif choice == 2: self.stats_manager.split_matches_into_files() elif choice == 12: self.stats_manager.fetch_data(season_loot=True) elif choice == 13: self.stats_manager.fetch_data(maps=True) else: print("Invalid choice. Please try again.") return True return True def run_interactive_mode(self): """Run the interactive menu mode.""" running = True while running: choice = self.display_menu() running = self.handle_menu_choice(choice) def setup_argument_parser(self): """Set up command line argument parser.""" parser = argparse.ArgumentParser( description="Detailed Modern Warfare (2019) Statistics Tool", epilog=self.help_text, formatter_class=argparse.RawDescriptionHelpFormatter ) # Group arguments for better help display group_default = parser.add_argument_group("Default Options") group_data = parser.add_argument_group("Data Fetching Options") group_cleaning = parser.add_argument_group("Data Cleaning Options") # Default options group_default.add_argument( "-tz", "--timezone", type=str, default="GMT", choices=TIMEZONE_OPTIONS, help="Specify the timezone (GMT, EST, CST, PST)" ) # 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("-id", "--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") 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 connected accounts data") group_data.add_argument("-s", "--settings", action="store_true", help="Fetch only your account settings") # Data 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 matches into separate JSON files") group_cleaning.add_argument("-csd", "--clean_stats_data", action="store_true", help="Beautify stats.json data") group_cleaning.add_argument("-cmd", "--clean_match_data", action="store_true", help="Beautify match_info.json data") group_cleaning.add_argument("-cff", "--clean_friend_feed", action="store_true", help="Clean friend feed data") group_cleaning.add_argument("-cef", "--clean_event_feed", action="store_true", help="Clean event feed data") return parser def run_cli_mode(self, args): """Run the command line mode with parsed arguments.""" # Prioritize cleaning operations if args.clean: self.stats_manager.beautify_all_data(timezone=args.timezone) elif args.clean_stats_data: self.stats_manager.beautify_stats_data(timezone=args.timezone) elif args.clean_match_data: self.stats_manager.beautify_match_data(timezone=args.timezone) elif args.split_matches: self.stats_manager.split_matches_into_files(timezone=args.timezone) elif args.clean_friend_feed: self.stats_manager.clean_json_files('friendFeed.json') elif args.clean_event_feed: self.stats_manager.clean_json_files('eventFeed.json') else: # Data fetching operations options = { 'all_stats': args.all_stats, 'season_loot': args.season_loot, 'identities': args.identities, 'maps': args.maps, 'info': args.info, 'friendFeed': args.friendFeed, 'eventFeed': args.eventFeed, 'cod_points': args.cod_points, 'connected_accounts': args.connected_accounts, 'settings': args.settings } self.stats_manager.fetch_data(args.player_name, **options) def main(): """Main entry point for the application.""" stats_manager = CodStatsManager() cli = CLI(stats_manager) # Parse command line arguments if len(sys.argv) > 1: parser = cli.setup_argument_parser() args = parser.parse_args() cli.run_cli_mode(args) else: # Run interactive mode cli.run_interactive_mode() if __name__ == "__main__": main()