chore(cod_api_tool.py): remove whitespace & add try except for cleaning match_info and stats files

This commit is contained in:
Rim 2025-03-10 10:45:04 -04:00
parent 61b910ab45
commit dfc647e500

View File

@ -24,13 +24,13 @@ api = API()
class CodStatsManager: class CodStatsManager:
"""Main class to manage COD API interactions and data processing.""" """Main class to manage COD API interactions and data processing."""
def __init__(self): def __init__(self):
self._ensure_directories_exist() self._ensure_directories_exist()
self.replacements = self._load_replacements() self.replacements = self._load_replacements()
self.api_key = self._get_api_key() self.api_key = self._get_api_key()
api.login(self.api_key) api.login(self.api_key)
def _ensure_directories_exist(self): def _ensure_directories_exist(self):
"""Ensure necessary directories exist.""" """Ensure necessary directories exist."""
if not os.path.exists(STATS_DIR): if not os.path.exists(STATS_DIR):
@ -38,7 +38,7 @@ class CodStatsManager:
match_dir_path = os.path.join(STATS_DIR, MATCH_DIR) match_dir_path = os.path.join(STATS_DIR, MATCH_DIR)
if not os.path.exists(match_dir_path): if not os.path.exists(match_dir_path):
os.makedirs(match_dir_path) os.makedirs(match_dir_path)
def _load_replacements(self): def _load_replacements(self):
"""Load replacements from the JSON file.""" """Load replacements from the JSON file."""
# First, handle running as PyInstaller executable # First, handle running as PyInstaller executable
@ -49,13 +49,13 @@ class CodStatsManager:
else: else:
# If running as a normal Python script # If running as a normal Python script
replacements_path = REPLACEMENTS_FILE replacements_path = REPLACEMENTS_FILE
if not os.path.exists(replacements_path): if not os.path.exists(replacements_path):
raise FileNotFoundError(f"{replacements_path} not found. Ensure it exists in the script's directory.") raise FileNotFoundError(f"{replacements_path} not found. Ensure it exists in the script's directory.")
with open(replacements_path, 'r') as file: with open(replacements_path, 'r') as file:
return json.load(file) return json.load(file)
def _get_api_key(self): def _get_api_key(self):
"""Get API key from file or user input.""" """Get API key from file or user input."""
if os.path.exists(COOKIE_FILE): if os.path.exists(COOKIE_FILE):
@ -66,13 +66,13 @@ class CodStatsManager:
with open(COOKIE_FILE, 'w') as f: with open(COOKIE_FILE, 'w') as f:
f.write(api_key) f.write(api_key)
return api_key return api_key
def _extract_player_name(self, full_name): def _extract_player_name(self, full_name):
"""Extract just the player name part before the # symbol.""" """Extract just the player name part before the # symbol."""
if '#' in full_name: if '#' in full_name:
return full_name.split('#')[0] return full_name.split('#')[0]
return full_name return full_name
def save_to_file(self, data, filename, player_name=None): def save_to_file(self, data, filename, player_name=None):
"""Save data to a JSON file with player name in the filename if provided.""" """Save data to a JSON file with player name in the filename if provided."""
if player_name: if player_name:
@ -81,68 +81,68 @@ class CodStatsManager:
# Insert player name before the extension # Insert player name before the extension
base, ext = os.path.splitext(filename) base, ext = os.path.splitext(filename)
filename = f"{base}-{player_name_clean}{ext}" filename = f"{base}-{player_name_clean}{ext}"
file_path = os.path.join(STATS_DIR, filename) file_path = os.path.join(STATS_DIR, filename)
# Ensure data is JSON serializable before saving # Ensure data is JSON serializable before saving
serializable_data = self._ensure_json_serializable(data) serializable_data = self._ensure_json_serializable(data)
with open(file_path, 'w') as json_file: with open(file_path, 'w') as json_file:
json.dump(serializable_data, json_file, indent=4) json.dump(serializable_data, json_file, indent=4)
print(f"Data saved to {file_path}") print(f"Data saved to {file_path}")
return file_path # Return the path for potential YAML conversion return file_path # Return the path for potential YAML conversion
def get_player_name(self): def get_player_name(self):
"""Get player name from user input.""" """Get player name from user input."""
return input("Please enter the player's username (with #1234567): ") 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): def fetch_data(self, player_name=None, convert_to_yaml=False, delete_json=False, **options):
"""Fetch data based on specified options.""" """Fetch data based on specified options."""
if not player_name: if not player_name:
player_name = self.get_player_name() player_name = self.get_player_name()
# If no specific option is selected, fetch basic stats # If no specific option is selected, fetch basic stats
if not any(options.values()): if not any(options.values()):
saved_files = self._fetch_basic_stats(player_name) saved_files = self._fetch_basic_stats(player_name)
if convert_to_yaml: if convert_to_yaml:
self.convert_files_to_yaml(saved_files, delete_json) self.convert_files_to_yaml(saved_files, delete_json)
return return
# If all_stats option is selected, fetch everything # If all_stats option is selected, fetch everything
if options.get('all_stats'): if options.get('all_stats'):
saved_files = self._fetch_all_stats(player_name) saved_files = self._fetch_all_stats(player_name)
if convert_to_yaml: if convert_to_yaml:
self.convert_files_to_yaml(saved_files, delete_json) self.convert_files_to_yaml(saved_files, delete_json)
return return
# Otherwise, fetch only requested data # Otherwise, fetch only requested data
saved_files = self._fetch_specific_data(player_name, options) saved_files = self._fetch_specific_data(player_name, options)
if convert_to_yaml: if convert_to_yaml:
self.convert_files_to_yaml(saved_files, delete_json) self.convert_files_to_yaml(saved_files, delete_json)
def _fetch_basic_stats(self, player_name): def _fetch_basic_stats(self, player_name):
"""Fetch basic player stats and match history.""" """Fetch basic player stats and match history."""
player_stats = api.ModernWarfare.fullData(platforms.Activision, player_name) 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)
saved_files = [] saved_files = []
saved_files.append(self.save_to_file(player_stats, 'stats.json', player_name)) 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)) saved_files.append(self.save_to_file(match_info, 'match_info.json', player_name))
return saved_files return saved_files
def _fetch_all_stats(self, player_name): def _fetch_all_stats(self, player_name):
"""Fetch all available stats for a player.""" """Fetch all available stats for a player."""
# Basic stats # Basic stats
player_stats = api.ModernWarfare.fullData(platforms.Activision, player_name) 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) season_loot_data = api.ModernWarfare.seasonLoot(platforms.Activision, player_name)
# Player-specific data # Player-specific data
identities_data = api.Me.loggedInIdentities() identities_data = api.Me.loggedInIdentities()
map_list = api.ModernWarfare.mapList(platforms.Activision) map_list = api.ModernWarfare.mapList(platforms.Activision)
# Save basic data # Save basic data
saved_files = [] saved_files = []
saved_files.append(self.save_to_file(player_stats, 'stats.json', player_name)) 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(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(map_list, 'map_list.json'))
saved_files.append(self.save_to_file(identities_data, 'identities.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 # Check if userInfo.json exists to determine if we should fetch additional data
user_info_file = os.path.join('userInfo.json') user_info_file = os.path.join('userInfo.json')
if os.path.exists(user_info_file): 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(cod_points, 'cp.json'))
saved_files.append(self.save_to_file(connected_accounts, 'connectedAccounts.json')) saved_files.append(self.save_to_file(connected_accounts, 'connectedAccounts.json'))
saved_files.append(self.save_to_file(settings, 'settings.json')) saved_files.append(self.save_to_file(settings, 'settings.json'))
return saved_files return saved_files
def _fetch_specific_data(self, player_name, options): def _fetch_specific_data(self, player_name, options):
"""Fetch specific data based on provided options.""" """Fetch specific data based on provided options."""
endpoints = { endpoints = {
@ -185,16 +185,16 @@ class CodStatsManager:
'connected_accounts': (api.Me.connectedAccounts, [], 'connectedAccounts.json'), 'connected_accounts': (api.Me.connectedAccounts, [], 'connectedAccounts.json'),
'settings': (api.Me.settings, [], 'settings.json') 'settings': (api.Me.settings, [], 'settings.json')
} }
saved_files = [] saved_files = []
for option, value in options.items(): for option, value in options.items():
if value and option in endpoints: if value and option in endpoints:
func, args, filename = endpoints[option] func, args, filename = endpoints[option]
data = func(*args) data = func(*args)
saved_files.append(self.save_to_file(data, filename, player_name)) saved_files.append(self.save_to_file(data, filename, player_name))
return saved_files return saved_files
def convert_files_to_yaml(self, file_paths=None, delete_json=False): def convert_files_to_yaml(self, file_paths=None, delete_json=False):
""" """
Convert specific JSON files to YAML format. Convert specific JSON files to YAML format.
@ -216,23 +216,23 @@ class CodStatsManager:
if os.path.exists(json_path) and json_path.endswith(".json"): if os.path.exists(json_path) and json_path.endswith(".json"):
yaml_path = self._convert_json_file_to_yaml(json_path) yaml_path = self._convert_json_file_to_yaml(json_path)
converted_files.append((json_path, yaml_path)) converted_files.append((json_path, yaml_path))
# Delete JSON files if requested # Delete JSON files if requested
if delete_json: if delete_json:
for json_path, _ in converted_files: for json_path, _ in converted_files:
os.remove(json_path) os.remove(json_path)
print(f"Deleted: {json_path}") print(f"Deleted: {json_path}")
return converted_files return converted_files
def _convert_json_file_to_yaml(self, json_path): def _convert_json_file_to_yaml(self, json_path):
"""Convert a single JSON file to YAML format.""" """Convert a single JSON file to YAML format."""
yaml_path = json_path.replace(".json", ".yaml") yaml_path = json_path.replace(".json", ".yaml")
# Read JSON file # Read JSON file
with open(json_path, 'r') as json_file: with open(json_path, 'r') as json_file:
data = json.load(json_file) data = json.load(json_file)
# Write YAML file # Write YAML file
with open(yaml_path, 'w', encoding='utf-8') as yaml_file: with open(yaml_path, 'w', encoding='utf-8') as yaml_file:
yaml.dump( yaml.dump(
@ -248,75 +248,92 @@ class CodStatsManager:
line_break="unix", line_break="unix",
indent=2 indent=2
) )
print(f"Converted: {json_path} --> {yaml_path}") print(f"Converted: {json_path} --> {yaml_path}")
return yaml_path return yaml_path
def beautify_all_data(self, timezone='GMT', player_name=None): def beautify_all_data(self, timezone='GMT', player_name=None):
"""Beautify all data files.""" """Beautify all data files."""
self.beautify_stats_data(timezone, player_name) self.beautify_stats_data(timezone, player_name)
self.beautify_match_data(timezone, player_name) self.beautify_match_data(timezone, player_name)
self.beautify_feed_data(timezone) self.beautify_feed_data(timezone)
self.clean_json_files('friendFeed.json', 'eventFeed.json') 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): def beautify_stats_data(self, timezone='GMT', player_name=None):
"""Beautify stats data.""" """Beautify stats data."""
if player_name: if player_name is None:
file_name = f'stats-{self._extract_player_name(player_name)}.json' print("Please specify a Player Name to sort Stats")
else: return 0
file_name = 'stats.json' try:
if player_name:
file_path = os.path.join(STATS_DIR, file_name) file_name = f'stats-{self._extract_player_name(player_name)}.json'
if not os.path.exists(file_path): # else:
print(f"File {file_path} not found. Skipping beautification.") # file_name = 'stats.json'
return
file_path = os.path.join(STATS_DIR, file_name)
with open(file_path, 'r') as file: if not os.path.exists(file_path):
data = json.load(file) print(f"File {file_path} not found. Skipping beautification.")
return
# Convert times and durations
self._replace_time_and_duration_recursive(data, timezone) with open(file_path, 'r') as file:
data = json.load(file)
# Replace keys with more readable names
data = self._recursive_key_replace(data) # Convert times and durations
self._replace_time_and_duration_recursive(data, timezone)
# Sort data by relevant metrics
data = self._sort_data(data) # Replace keys with more readable names
data = self._recursive_key_replace(data)
# Save modified data
with open(file_path, 'w') as file: # Sort data by relevant metrics
json.dump(data, file, indent=4) data = self._sort_data(data)
print(f"Keys sorted and replaced in {file_path}.") # 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): def beautify_match_data(self, timezone='GMT', player_name=None):
"""Beautify match data.""" """Beautify match data."""
if player_name: if player_name is None:
file_name = f'match_info-{self._extract_player_name(player_name)}.json' print("Please specify a Player Name to sort Match Info")
else: return 0
file_name = 'match_info.json'
try:
file_path = os.path.join(STATS_DIR, file_name) if player_name:
if not os.path.exists(file_path): file_name = f'match_info-{self._extract_player_name(player_name)}.json'
print(f"File {file_path} not found. Skipping beautification.") # else:
return # file_name = 'match_info.json'
with open(file_path, 'r') as file: file_path = os.path.join(STATS_DIR, file_name)
data = json.load(file) if not os.path.exists(file_path):
print(f"File {file_path} not found. Skipping beautification.")
# Convert times and durations return
self._replace_time_and_duration_recursive(data, timezone)
with open(file_path, 'r') as file:
# Replace keys with more readable names data = json.load(file)
data = self._recursive_key_replace(data)
# Convert times and durations
# Save modified data self._replace_time_and_duration_recursive(data, timezone)
with open(file_path, 'w') as file:
json.dump(data, file, indent=4) # Replace keys with more readable names
data = self._recursive_key_replace(data)
print(f"Keys replaced in {file_path}.")
# 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): def beautify_feed_data(self, timezone='GMT', player_name=None):
"""Beautify feed data files.""" """Beautify feed data files."""
for feed_file_base in ['friendFeed.json', 'eventFeed.json']: 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" file_name = f"{feed_file_base.replace('.json', '')}-{clean_name}.json"
else: else:
file_name = feed_file_base file_name = feed_file_base
file_path = os.path.join(STATS_DIR, file_name) file_path = os.path.join(STATS_DIR, file_name)
if not os.path.exists(file_path): if not os.path.exists(file_path):
print(f"{file_name} does not exist, skipping.") print(f"{file_name} does not exist, skipping.")
continue continue
with open(file_path, 'r') as file: with open(file_path, 'r') as file:
data = json.load(file) data = json.load(file)
# Convert times and durations # Convert times and durations
self._replace_time_and_duration_recursive(data, timezone) self._replace_time_and_duration_recursive(data, timezone)
# Replace keys with more readable names # Replace keys with more readable names
data = self._recursive_key_replace(data) data = self._recursive_key_replace(data)
# Save modified data # Save modified data
with open(file_path, 'w') as file: with open(file_path, 'w') as file:
json.dump(data, file, indent=4) json.dump(data, file, indent=4)
print(f"Keys sorted and replaced in {file_name}.") print(f"Keys sorted and replaced in {file_name}.")
def split_matches_into_files(self, timezone='GMT', player_name=None): def split_matches_into_files(self, timezone='GMT', player_name=None):
"""Split match data into separate files.""" """Split match data into separate files."""
matches_dir = os.path.join(STATS_DIR, MATCH_DIR) matches_dir = os.path.join(STATS_DIR, MATCH_DIR)
if not os.path.exists(matches_dir): if not os.path.exists(matches_dir):
os.makedirs(matches_dir) os.makedirs(matches_dir)
file_name = f'match_info-{self._extract_player_name(player_name)}.json' file_name = f'match_info-{self._extract_player_name(player_name)}.json'
match_info_path = os.path.join(STATS_DIR, file_name) match_info_path = os.path.join(STATS_DIR, file_name)
if not os.path.exists(match_info_path): if not os.path.exists(match_info_path):
print(f"Match info file not found at {match_info_path}. Skipping split.") print(f"Match info file not found at {match_info_path}. Skipping split.")
return return
with open(match_info_path, 'r') as file: with open(match_info_path, 'r') as file:
data = json.load(file) data = json.load(file)
matches = data.get('data', {}).get('matches', []) matches = data.get('data', {}).get('matches', [])
if not matches: if not matches:
print("No matches found to split.") print("No matches found to split.")
return return
# Check if time conversion is needed # Check if time conversion is needed
sample_match = matches[0] sample_match = matches[0]
needs_time_conversion = ( needs_time_conversion = (
@ -374,62 +391,62 @@ class CodStatsManager:
isinstance(sample_match.get("utcEndSeconds"), int) or isinstance(sample_match.get("utcEndSeconds"), int) or
isinstance(sample_match.get("duration"), int) isinstance(sample_match.get("duration"), int)
) )
if needs_time_conversion: if needs_time_conversion:
print("Converting match timestamps to human-readable format...") print("Converting match timestamps to human-readable format...")
self._replace_time_and_duration_recursive(data, timezone) self._replace_time_and_duration_recursive(data, timezone)
# Update the main match file # Update the main match file
with open(match_info_path, 'w') as file: with open(match_info_path, 'w') as file:
json.dump(data, file, indent=4) json.dump(data, file, indent=4)
# Process each match # Process each match
for idx, match in enumerate(matches): for idx, match in enumerate(matches):
# Create a copy to avoid modifying the original data # Create a copy to avoid modifying the original data
match_copy = dict(match) match_copy = dict(match)
# Remove large loadout data to keep files smaller # Remove large loadout data to keep files smaller
if 'player' in match_copy: if 'player' in match_copy:
match_copy['player'].pop('loadouts', None) match_copy['player'].pop('loadouts', None)
match_copy['player'].pop('loadout', None) match_copy['player'].pop('loadout', None)
# Save to individual file # Save to individual file
file_name = f"match_{idx + 1}.json" file_name = f"match_{idx + 1}.json"
file_path = os.path.join(matches_dir, file_name) file_path = os.path.join(matches_dir, file_name)
with open(file_path, 'w') as match_file: with open(file_path, 'w') as match_file:
json.dump(match_copy, match_file, indent=4) json.dump(match_copy, match_file, indent=4)
print(f"Matches split into {len(matches)} separate files in {matches_dir}.") print(f"Matches split into {len(matches)} separate files in {matches_dir}.")
def clean_json_files(self, *filenames, player_name=None): def clean_json_files(self, *filenames, player_name=None):
"""Clean JSON files by removing HTML-like tags and entities.""" """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' regex_pattern = r'<span class="|</span>|">|mp-stat-items|kills-value|headshots-value|username|game-mode|kdr-value|accuracy-value|defends-value'
replace = '' replace = ''
for base_filename in filenames: for base_filename in filenames:
if player_name: if player_name:
clean_name = self._extract_player_name(player_name) clean_name = self._extract_player_name(player_name)
filename = f"{base_filename.replace('.json', '')}-{clean_name}.json" filename = f"{base_filename.replace('.json', '')}-{clean_name}.json"
else: else:
filename = base_filename filename = base_filename
file_path = os.path.join(STATS_DIR, filename) file_path = os.path.join(STATS_DIR, filename)
if not os.path.exists(file_path): if not os.path.exists(file_path):
print(f"{filename} does not exist, skipping.") print(f"{filename} does not exist, skipping.")
continue continue
with open(file_path, 'r') as file: with open(file_path, 'r') as file:
content = file.read() content = file.read()
# Replace unwanted patterns # Replace unwanted patterns
modified_content = re.sub(regex_pattern, replace, content) modified_content = re.sub(regex_pattern, replace, content)
# Save cleaned content # Save cleaned content
with open(file_path, 'w') as file: with open(file_path, 'w') as file:
file.write(modified_content) file.write(modified_content)
print(f"Removed unreadable strings from {filename}.") print(f"Removed unreadable strings from {filename}.")
def _recursive_key_replace(self, obj): def _recursive_key_replace(self, obj):
"""Recursively replace keys and values with more readable versions.""" """Recursively replace keys and values with more readable versions."""
if isinstance(obj, dict): if isinstance(obj, dict):
@ -446,7 +463,7 @@ class CodStatsManager:
return [self._recursive_key_replace(item) for item in obj] return [self._recursive_key_replace(item) for item in obj]
else: else:
return self.replacements.get(obj, obj) if isinstance(obj, str) else obj return self.replacements.get(obj, obj) if isinstance(obj, str) else obj
def _sort_data(self, data): def _sort_data(self, data):
"""Sort data by meaningful metrics for better readability.""" """Sort data by meaningful metrics for better readability."""
if isinstance(data, dict): if isinstance(data, dict):
@ -492,7 +509,7 @@ class CodStatsManager:
# Recursively sort nested data # Recursively sort nested data
data[key] = self._sort_data(value) data[key] = self._sort_data(value)
return data return data
def _replace_time_and_duration_recursive(self, data, timezone): def _replace_time_and_duration_recursive(self, data, timezone):
"""Recursively replace epoch times with human-readable formats.""" """Recursively replace epoch times with human-readable formats."""
time_keys = [ time_keys = [
@ -500,12 +517,12 @@ class CodStatsManager:
"avgLifeTime", "percentTimeMoving" "avgLifeTime", "percentTimeMoving"
] ]
date_keys = ["date", "updated", "originalDate", "dateAdded"] date_keys = ["date", "updated", "originalDate", "dateAdded"]
if isinstance(data, list): if isinstance(data, list):
# Sort lists containing items with dateAdded # Sort lists containing items with dateAdded
if data and isinstance(data[0], dict) and "dateAdded" in data[0]: if data and isinstance(data[0], dict) and "dateAdded" in data[0]:
data.sort(key=lambda x: x.get("dateAdded", 0), reverse=True) data.sort(key=lambda x: x.get("dateAdded", 0), reverse=True)
for item in data: for item in data:
self._replace_time_and_duration_recursive(item, timezone) self._replace_time_and_duration_recursive(item, timezone)
elif isinstance(data, dict): elif isinstance(data, dict):
@ -525,20 +542,20 @@ class CodStatsManager:
data[key] = self._convert_duration_milliseconds(value) data[key] = self._convert_duration_milliseconds(value)
else: else:
self._replace_time_and_duration_recursive(value, timezone) self._replace_time_and_duration_recursive(value, timezone)
def _epoch_milli_to_human_readable(self, epoch_millis, timezone='GMT'): def _epoch_milli_to_human_readable(self, epoch_millis, timezone='GMT'):
"""Convert epoch milliseconds to human-readable date string.""" """Convert epoch milliseconds to human-readable date string."""
if isinstance(epoch_millis, str): if isinstance(epoch_millis, str):
return epoch_millis return epoch_millis
dt_object = datetime.datetime.utcfromtimestamp(epoch_millis / 1000.0) dt_object = datetime.datetime.utcfromtimestamp(epoch_millis / 1000.0)
return self._format_datetime(dt_object, timezone) return self._format_datetime(dt_object, timezone)
def _epoch_to_human_readable(self, epoch_timestamp, timezone='GMT'): def _epoch_to_human_readable(self, epoch_timestamp, timezone='GMT'):
"""Convert epoch seconds to human-readable date string.""" """Convert epoch seconds to human-readable date string."""
if isinstance(epoch_timestamp, str): if isinstance(epoch_timestamp, str):
return epoch_timestamp return epoch_timestamp
dt_object = datetime.datetime.utcfromtimestamp(epoch_timestamp) dt_object = datetime.datetime.utcfromtimestamp(epoch_timestamp)
return self._format_datetime(dt_object, timezone) return self._format_datetime(dt_object, timezone)
@ -550,39 +567,39 @@ class CodStatsManager:
'CST': -6, # Updated for consistency 'CST': -6, # Updated for consistency
'PST': -8 'PST': -8
} }
if timezone not in timezone_offsets: if timezone not in timezone_offsets:
raise ValueError(f"Unsupported timezone: {timezone}") raise ValueError(f"Unsupported timezone: {timezone}")
# Apply timezone offset # Apply timezone offset
dt_object = dt_object + datetime.timedelta(hours=timezone_offsets[timezone]) dt_object = dt_object + datetime.timedelta(hours=timezone_offsets[timezone])
# Format date string # Format date string
return f"{timezone}: {dt_object.strftime('%A, %B %d, %Y %I:%M:%S %p')}" return f"{timezone}: {dt_object.strftime('%A, %B %d, %Y %I:%M:%S %p')}"
def _convert_duration_milliseconds(self, milliseconds): def _convert_duration_milliseconds(self, milliseconds):
"""Convert milliseconds to a human-readable duration string.""" """Convert milliseconds to a human-readable duration string."""
if isinstance(milliseconds, str) and "Minutes" in milliseconds: if isinstance(milliseconds, str) and "Minutes" in milliseconds:
return milliseconds # Already converted return milliseconds # Already converted
seconds, milliseconds = divmod(milliseconds, 1000) seconds, milliseconds = divmod(milliseconds, 1000)
minutes, seconds = divmod(seconds, 60) minutes, seconds = divmod(seconds, 60)
return f"{minutes} Minutes {seconds} Seconds {milliseconds} Milliseconds" return f"{minutes} Minutes {seconds} Seconds {milliseconds} Milliseconds"
def _convert_duration_seconds(self, seconds): def _convert_duration_seconds(self, seconds):
"""Convert seconds to a human-readable duration string.""" """Convert seconds to a human-readable duration string."""
if isinstance(seconds, str): if isinstance(seconds, str):
return seconds # Already converted return seconds # Already converted
days, seconds = divmod(seconds, 86400) days, seconds = divmod(seconds, 86400)
hours, seconds = divmod(seconds, 3600) hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60) minutes, seconds = divmod(seconds, 60)
days = int(days) days = int(days)
hours = int(hours) hours = int(hours)
minutes = int(minutes) minutes = int(minutes)
seconds = int(seconds) seconds = int(seconds)
return f"{days} Days {hours} Hours {minutes} Minutes {seconds} Seconds" return f"{days} Days {hours} Hours {minutes} Minutes {seconds} Seconds"
def _ensure_json_serializable(self, obj): def _ensure_json_serializable(self, obj):
@ -601,7 +618,7 @@ class CodStatsManager:
class CLI: class CLI:
"""Command Line Interface manager.""" """Command Line Interface manager."""
def __init__(self, stats_manager): def __init__(self, stats_manager):
self.stats_manager = stats_manager self.stats_manager = stats_manager
self.help_text = """ 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 - 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 - Enter the value when prompted
""" """
def display_menu(self): def display_menu(self):
"""Display the main menu and get user choice.""" """Display the main menu and get user choice."""
print("\nBeautify Options:") print("\nBeautify Options:")
@ -631,7 +648,7 @@ class CLI:
print("\nOptions Not Requiring Player Name:") print("\nOptions Not Requiring Player Name:")
print("11) Get season loot") print("11) Get season loot")
print("12) Get map list") print("12) Get map list")
print("\nYAML Conversion Options:") print("\nYAML Conversion Options:")
print("13) Convert all JSON files to YAML") print("13) Convert all JSON files to YAML")
print("14) Convert all JSON files to YAML and delete JSON files") print("14) Convert all JSON files to YAML and delete JSON files")
@ -644,20 +661,20 @@ class CLI:
except ValueError: except ValueError:
print("Please enter a valid number.") print("Please enter a valid number.")
return -1 return -1
def handle_menu_choice(self, choice): def handle_menu_choice(self, choice):
"""Handle the user's menu choice.""" """Handle the user's menu choice."""
if choice == 0: if choice == 0:
print("Exiting...") print("Exiting...")
return False return False
if choice in [3, 4, 5, 6, 7, 8, 9, 10, 11]: if choice in [3, 4, 5, 6, 7, 8, 9, 10, 11]:
player_name = input("Please enter the player's username (with #1234567): ") player_name = input("Please enter the player's username (with #1234567): ")
# convert_to_yaml = input("Convert results to YAML? (y/n): ").lower() == 'y' # convert_to_yaml = input("Convert results to YAML? (y/n): ").lower() == 'y'
# delete_json = False # delete_json = False
# if convert_to_yaml: # if convert_to_yaml:
# delete_json = input("Delete JSON files after conversion? (y/n): ").lower() == 'y' # delete_json = input("Delete JSON files after conversion? (y/n): ").lower() == 'y'
options = { options = {
3: {'all_stats': True}, 3: {'all_stats': True},
4: {'season_loot': True}, 4: {'season_loot': True},
@ -669,12 +686,12 @@ class CLI:
10: {'connected_accounts': True}, 10: {'connected_accounts': True},
11: {'settings': True} 11: {'settings': True}
} }
if choice in options: if choice in options:
self.stats_manager.fetch_data(player_name=player_name, **options[choice]) 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, # self.stats_manager.fetch_data(player_name=player_name, convert_to_yaml=convert_to_yaml,
# delete_json=delete_json, **options[choice]) # delete_json=delete_json, **options[choice])
elif choice == 1: elif choice == 1:
player_name = input("Enter player name for beautification (leave blank if not player-specific): ") 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) self.stats_manager.beautify_all_data(player_name=player_name if player_name else None)
@ -689,16 +706,16 @@ class CLI:
else: else:
print("Invalid choice. Please try again.") print("Invalid choice. Please try again.")
return True return True
return True return True
def run_interactive_mode(self): def run_interactive_mode(self):
"""Run the interactive menu mode.""" """Run the interactive menu mode."""
running = True running = True
while running: while running:
choice = self.display_menu() choice = self.display_menu()
running = self.handle_menu_choice(choice) running = self.handle_menu_choice(choice)
def setup_argument_parser(self): def setup_argument_parser(self):
"""Set up command line argument parser.""" """Set up command line argument parser."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@ -706,13 +723,13 @@ class CLI:
epilog=self.help_text, epilog=self.help_text,
formatter_class=argparse.RawDescriptionHelpFormatter formatter_class=argparse.RawDescriptionHelpFormatter
) )
# Group arguments for better help display # Group arguments for better help display
group_default = parser.add_argument_group("Default Options") group_default = parser.add_argument_group("Default Options")
group_data = parser.add_argument_group("Data Fetching Options") group_data = parser.add_argument_group("Data Fetching Options")
group_cleaning = parser.add_argument_group("Data Cleaning Options") group_cleaning = parser.add_argument_group("Data Cleaning Options")
group_yaml = parser.add_argument_group("YAML Conversion Options") group_yaml = parser.add_argument_group("YAML Conversion Options")
# Default options # Default options
group_default.add_argument( group_default.add_argument(
"-tz", "--timezone", "-tz", "--timezone",
@ -721,7 +738,7 @@ class CLI:
choices=TIMEZONE_OPTIONS, choices=TIMEZONE_OPTIONS,
help="Specify the timezone (GMT, EST, CST, PST)" help="Specify the timezone (GMT, EST, CST, PST)"
) )
# Data fetching options # Data fetching options
group_data.add_argument("-p", "--player_name", type=str, help="Player's username (with #1234567)") 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("-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("-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("-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") group_data.add_argument("-s", "--settings", action="store_true", help="Fetch only your account settings")
# Data cleaning options # Data cleaning options
group_cleaning.add_argument("-c", "--clean", action="store_true", help="Beautify all data") 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("-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("-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("-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") group_cleaning.add_argument("-cef", "--clean_event_feed", action="store_true", help="Clean event feed data")
# YAML conversion options # YAML conversion options
group_yaml.add_argument("-y", "--yaml", action="store_true", help="Convert JSON files to YAML") 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") group_yaml.add_argument("-d", "--delete_json", action="store_true", help="Delete JSON files after YAML conversion")
return parser return parser
def run_cli_mode(self, args): def run_cli_mode(self, args):
"""Run the command line mode with parsed arguments.""" """Run the command line mode with parsed arguments."""
# Check if only YAML conversion is requested # 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) self.stats_manager.convert_files_to_yaml(delete_json=args.delete_json)
return return
# Prioritize cleaning operations # Prioritize cleaning operations
if args.clean: if args.clean:
self.stats_manager.beautify_all_data(timezone=args.timezone, player_name=args.player_name) 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.""" """Main entry point for the application."""
stats_manager = CodStatsManager() stats_manager = CodStatsManager()
cli = CLI(stats_manager) cli = CLI(stats_manager)
# Parse command line arguments # Parse command line arguments
if len(sys.argv) > 1: if len(sys.argv) > 1:
parser = cli.setup_argument_parser() parser = cli.setup_argument_parser()