From dfc647e500c88e0217c6554c75126cdee0175f7b Mon Sep 17 00:00:00 2001 From: Rim Date: Mon, 10 Mar 2025 10:45:04 -0400 Subject: [PATCH] chore(cod_api_tool.py): remove whitespace & add try except for cleaning match_info and stats files --- cod_api_tool.py | 325 +++++++++++++++++++++++++----------------------- 1 file changed, 171 insertions(+), 154 deletions(-) diff --git a/cod_api_tool.py b/cod_api_tool.py index 2691a4d..3aab70b 100644 --- a/cod_api_tool.py +++ b/cod_api_tool.py @@ -24,13 +24,13 @@ 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): @@ -38,7 +38,7 @@ class CodStatsManager: 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 @@ -49,13 +49,13 @@ class CodStatsManager: 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): @@ -66,13 +66,13 @@ class CodStatsManager: with open(COOKIE_FILE, 'w') as f: f.write(api_key) return api_key - + def _extract_player_name(self, full_name): """Extract just the player name part before the # symbol.""" if '#' in full_name: return full_name.split('#')[0] return full_name - + def save_to_file(self, data, filename, player_name=None): """Save data to a JSON file with player name in the filename if provided.""" if player_name: @@ -81,68 +81,68 @@ class CodStatsManager: # Insert player name before the extension base, ext = os.path.splitext(filename) filename = f"{base}-{player_name_clean}{ext}" - + file_path = os.path.join(STATS_DIR, filename) - + # Ensure data is JSON serializable before saving serializable_data = self._ensure_json_serializable(data) - + with open(file_path, 'w') as json_file: json.dump(serializable_data, json_file, indent=4) print(f"Data saved to {file_path}") - + return file_path # Return the path for potential YAML conversion - + 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, convert_to_yaml=False, delete_json=False, **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()): saved_files = self._fetch_basic_stats(player_name) if convert_to_yaml: self.convert_files_to_yaml(saved_files, delete_json) return - + # If all_stats option is selected, fetch everything if options.get('all_stats'): saved_files = self._fetch_all_stats(player_name) if convert_to_yaml: self.convert_files_to_yaml(saved_files, delete_json) return - + # Otherwise, fetch only requested data saved_files = self._fetch_specific_data(player_name, options) if convert_to_yaml: self.convert_files_to_yaml(saved_files, delete_json) - + 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) - + saved_files = [] saved_files.append(self.save_to_file(player_stats, 'stats.json', player_name)) saved_files.append(self.save_to_file(match_info, 'match_info.json', player_name)) - + return saved_files - + 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 saved_files = [] saved_files.append(self.save_to_file(player_stats, 'stats.json', player_name)) @@ -150,7 +150,7 @@ class CodStatsManager: saved_files.append(self.save_to_file(season_loot_data, 'season_loot.json')) saved_files.append(self.save_to_file(map_list, 'map_list.json')) saved_files.append(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): @@ -169,9 +169,9 @@ class CodStatsManager: saved_files.append(self.save_to_file(cod_points, 'cp.json')) saved_files.append(self.save_to_file(connected_accounts, 'connectedAccounts.json')) saved_files.append(self.save_to_file(settings, 'settings.json')) - + return saved_files - + def _fetch_specific_data(self, player_name, options): """Fetch specific data based on provided options.""" endpoints = { @@ -185,16 +185,16 @@ class CodStatsManager: 'connected_accounts': (api.Me.connectedAccounts, [], 'connectedAccounts.json'), 'settings': (api.Me.settings, [], 'settings.json') } - + saved_files = [] for option, value in options.items(): if value and option in endpoints: func, args, filename = endpoints[option] data = func(*args) saved_files.append(self.save_to_file(data, filename, player_name)) - + return saved_files - + def convert_files_to_yaml(self, file_paths=None, delete_json=False): """ Convert specific JSON files to YAML format. @@ -216,23 +216,23 @@ class CodStatsManager: if os.path.exists(json_path) and json_path.endswith(".json"): yaml_path = self._convert_json_file_to_yaml(json_path) converted_files.append((json_path, yaml_path)) - + # Delete JSON files if requested if delete_json: for json_path, _ in converted_files: os.remove(json_path) print(f"Deleted: {json_path}") - + return converted_files - + def _convert_json_file_to_yaml(self, json_path): """Convert a single JSON file to YAML format.""" yaml_path = json_path.replace(".json", ".yaml") - + # Read JSON file with open(json_path, 'r') as json_file: data = json.load(json_file) - + # Write YAML file with open(yaml_path, 'w', encoding='utf-8') as yaml_file: yaml.dump( @@ -248,75 +248,92 @@ class CodStatsManager: line_break="unix", indent=2 ) - + print(f"Converted: {json_path} --> {yaml_path}") return yaml_path - + def beautify_all_data(self, timezone='GMT', player_name=None): """Beautify all data files.""" self.beautify_stats_data(timezone, player_name) self.beautify_match_data(timezone, player_name) self.beautify_feed_data(timezone) self.clean_json_files('friendFeed.json', 'eventFeed.json') - print("All data beautified successfully.") - + print("Data beautified successfully.") + def beautify_stats_data(self, timezone='GMT', player_name=None): """Beautify stats data.""" - if player_name: - file_name = f'stats-{self._extract_player_name(player_name)}.json' - else: - file_name = 'stats.json' - - file_path = os.path.join(STATS_DIR, file_name) - 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}.") - + if player_name is None: + print("Please specify a Player Name to sort Stats") + return 0 + try: + if player_name: + file_name = f'stats-{self._extract_player_name(player_name)}.json' + # else: + # file_name = 'stats.json' + + file_path = os.path.join(STATS_DIR, file_name) + 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}.") + + except Exception as e: + print("An error occurred while processing match data.") + return 0 + def beautify_match_data(self, timezone='GMT', player_name=None): """Beautify match data.""" - if player_name: - file_name = f'match_info-{self._extract_player_name(player_name)}.json' - else: - file_name = 'match_info.json' - - file_path = os.path.join(STATS_DIR, file_name) - 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}.") - + if player_name is None: + print("Please specify a Player Name to sort Match Info") + return 0 + + try: + if player_name: + file_name = f'match_info-{self._extract_player_name(player_name)}.json' + # else: + # file_name = 'match_info.json' + + file_path = os.path.join(STATS_DIR, file_name) + 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}.") + + except Exception as e: + print("An error occurred while processing match data.") + return 0 + def beautify_feed_data(self, timezone='GMT', player_name=None): """Beautify feed data files.""" for feed_file_base in ['friendFeed.json', 'eventFeed.json']: @@ -325,48 +342,48 @@ class CodStatsManager: file_name = f"{feed_file_base.replace('.json', '')}-{clean_name}.json" else: file_name = feed_file_base - + file_path = os.path.join(STATS_DIR, file_name) if not os.path.exists(file_path): print(f"{file_name} 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 {file_name}.") - + def split_matches_into_files(self, timezone='GMT', player_name=None): """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) - + file_name = f'match_info-{self._extract_player_name(player_name)}.json' - + match_info_path = os.path.join(STATS_DIR, file_name) 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 = ( @@ -374,62 +391,62 @@ class CodStatsManager: 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, player_name=None): """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 base_filename in filenames: if player_name: clean_name = self._extract_player_name(player_name) filename = f"{base_filename.replace('.json', '')}-{clean_name}.json" else: filename = base_filename - + 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): @@ -446,7 +463,7 @@ class CodStatsManager: 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): @@ -492,7 +509,7 @@ class CodStatsManager: # 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 = [ @@ -500,12 +517,12 @@ class CodStatsManager: "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): @@ -525,20 +542,20 @@ class CodStatsManager: 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) @@ -550,39 +567,39 @@ class CodStatsManager: '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): @@ -601,7 +618,7 @@ class CodStatsManager: class CLI: """Command Line Interface manager.""" - + def __init__(self, stats_manager): self.stats_manager = stats_manager self.help_text = """ @@ -611,7 +628,7 @@ class CLI: - 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:") @@ -631,7 +648,7 @@ class CLI: print("\nOptions Not Requiring Player Name:") print("11) Get season loot") print("12) Get map list") - + print("\nYAML Conversion Options:") print("13) Convert all JSON files to YAML") print("14) Convert all JSON files to YAML and delete JSON files") @@ -644,20 +661,20 @@ class CLI: 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): ") # convert_to_yaml = input("Convert results to YAML? (y/n): ").lower() == 'y' # delete_json = False # if convert_to_yaml: # delete_json = input("Delete JSON files after conversion? (y/n): ").lower() == 'y' - + options = { 3: {'all_stats': True}, 4: {'season_loot': True}, @@ -669,12 +686,12 @@ class CLI: 10: {'connected_accounts': True}, 11: {'settings': True} } - + if choice in options: self.stats_manager.fetch_data(player_name=player_name, **options[choice]) # self.stats_manager.fetch_data(player_name=player_name, convert_to_yaml=convert_to_yaml, # delete_json=delete_json, **options[choice]) - + elif choice == 1: player_name = input("Enter player name for beautification (leave blank if not player-specific): ") self.stats_manager.beautify_all_data(player_name=player_name if player_name else None) @@ -689,16 +706,16 @@ class CLI: 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( @@ -706,13 +723,13 @@ class CLI: 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") group_yaml = parser.add_argument_group("YAML Conversion Options") - + # Default options group_default.add_argument( "-tz", "--timezone", @@ -721,7 +738,7 @@ class CLI: 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") @@ -734,7 +751,7 @@ class CLI: 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") @@ -742,13 +759,13 @@ class CLI: 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") - + # YAML conversion options group_yaml.add_argument("-y", "--yaml", action="store_true", help="Convert JSON files to YAML") group_yaml.add_argument("-d", "--delete_json", action="store_true", help="Delete JSON files after YAML conversion") - + return parser - + def run_cli_mode(self, args): """Run the command line mode with parsed arguments.""" # Check if only YAML conversion is requested @@ -761,7 +778,7 @@ class CLI: ]): self.stats_manager.convert_files_to_yaml(delete_json=args.delete_json) return - + # Prioritize cleaning operations if args.clean: self.stats_manager.beautify_all_data(timezone=args.timezone, player_name=args.player_name) @@ -800,7 +817,7 @@ 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()