feat(cod_api_tool.py): add yaml conversion, append player_name dynamically to stats files

This commit is contained in:
Rim 2025-03-10 09:22:40 -04:00
parent e60cc06920
commit 265be3169c

View File

@ -3,6 +3,7 @@ import sys
import json
import os
import argparse
import yaml
from cod_api import API, platforms
import asyncio
import datetime
@ -66,41 +67,70 @@ class CodStatsManager:
f.write(api_key)
return api_key
def save_to_file(self, data, filename):
"""Save data to a JSON file."""
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:
# Extract the base name without the # and numbers
player_name_clean = self._extract_player_name(player_name)
# 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(data, json_file, indent=4)
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, **options):
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()):
self._fetch_basic_stats(player_name)
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'):
self._fetch_all_stats(player_name)
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
self._fetch_specific_data(player_name, options)
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)
self.save_to_file(player_stats, 'stats.json')
self.save_to_file(match_info, 'match_info.json')
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."""
@ -114,11 +144,12 @@ class CodStatsManager:
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')
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))
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')
@ -132,11 +163,14 @@ class CodStatsManager:
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')
saved_files.append(self.save_to_file(info, 'info.json'))
saved_files.append(self.save_to_file(friend_feed, 'friendFeed.json'))
saved_files.append(self.save_to_file(event_feed, 'eventFeed.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(settings, 'settings.json'))
return saved_files
def _fetch_specific_data(self, player_name, options):
"""Fetch specific data based on provided options."""
@ -152,23 +186,88 @@ class CodStatsManager:
'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)
self.save_to_file(data, filename)
saved_files.append(self.save_to_file(data, filename, player_name))
def beautify_all_data(self, timezone='GMT'):
return saved_files
def convert_files_to_yaml(self, file_paths=None, delete_json=False):
"""
Convert specific JSON files to YAML format.
If no files are specified, convert all JSON files in the stats directory.
"""
if file_paths is None:
# If no specific files provided, process all JSON files in STATS_DIR
converted_files = []
for root, _, files in os.walk(STATS_DIR):
for file in files:
if file.endswith(".json"):
json_path = os.path.join(root, file)
yaml_path = self._convert_json_file_to_yaml(json_path)
converted_files.append((json_path, yaml_path))
else:
# Process only the specified files
converted_files = []
for json_path in file_paths:
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(
data,
yaml_file,
default_flow_style=False,
sort_keys=False,
allow_unicode=False,
width=120,
explicit_start=True,
explicit_end=True,
default_style="",
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)
self.beautify_match_data(timezone)
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.")
def beautify_stats_data(self, timezone='GMT'):
def beautify_stats_data(self, timezone='GMT', player_name=None):
"""Beautify stats data."""
file_path = os.path.join(STATS_DIR, 'stats.json')
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
@ -191,9 +290,14 @@ class CodStatsManager:
print(f"Keys sorted and replaced in {file_path}.")
def beautify_match_data(self, timezone='GMT'):
def beautify_match_data(self, timezone='GMT', player_name=None):
"""Beautify match data."""
file_path = os.path.join(STATS_DIR, 'match_info.json')
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
@ -213,12 +317,18 @@ class CodStatsManager:
print(f"Keys replaced in {file_path}.")
def beautify_feed_data(self, timezone='GMT'):
def beautify_feed_data(self, timezone='GMT', player_name=None):
"""Beautify feed data files."""
for feed_file in ['friendFeed.json', 'eventFeed.json']:
file_path = os.path.join(STATS_DIR, feed_file)
for feed_file_base in ['friendFeed.json', 'eventFeed.json']:
if player_name:
clean_name = self._extract_player_name(player_name)
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"{feed_file} does not exist, skipping.")
print(f"{file_name} does not exist, skipping.")
continue
with open(file_path, 'r') as file:
@ -234,15 +344,17 @@ class CodStatsManager:
with open(file_path, 'w') as file:
json.dump(data, file, indent=4)
print(f"Keys sorted and replaced in {feed_file}.")
print(f"Keys sorted and replaced in {file_name}.")
def split_matches_into_files(self, timezone='GMT'):
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)
match_info_path = os.path.join(STATS_DIR, 'match_info.json')
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
@ -289,12 +401,18 @@ class CodStatsManager:
print(f"Matches split into {len(matches)} separate files in {matches_dir}.")
def clean_json_files(self, *filenames):
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 filename in filenames:
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.")
@ -469,19 +587,19 @@ class CodStatsManager:
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)
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."""
@ -516,6 +634,10 @@ class CLI:
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")
print("\n0) Exit")
try:
@ -533,6 +655,10 @@ class CLI:
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},
@ -547,16 +673,20 @@ class CLI:
}
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:
self.stats_manager.beautify_all_data()
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)
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)
self.stats_manager.convert_files_to_yaml(delete_json=False)
elif choice == 14:
self.stats_manager.convert_files_to_yaml(delete_json=True)
else:
print("Invalid choice. Please try again.")
return True
@ -582,6 +712,7 @@ class CLI:
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(
@ -613,23 +744,38 @@ class CLI:
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
if args.yaml and not any([
args.clean, args.clean_stats_data, args.clean_match_data,
args.split_matches, args.clean_friend_feed, args.clean_event_feed,
args.all_stats, args.season_loot, args.identities, args.maps,
args.info, args.friendFeed, args.eventFeed, args.cod_points,
args.connected_accounts, args.settings
]):
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)
self.stats_manager.beautify_all_data(timezone=args.timezone, player_name=args.player_name)
elif args.clean_stats_data:
self.stats_manager.beautify_stats_data(timezone=args.timezone)
self.stats_manager.beautify_stats_data(timezone=args.timezone, player_name=args.player_name)
elif args.clean_match_data:
self.stats_manager.beautify_match_data(timezone=args.timezone)
self.stats_manager.beautify_match_data(timezone=args.timezone, player_name=args.player_name)
elif args.split_matches:
self.stats_manager.split_matches_into_files(timezone=args.timezone)
self.stats_manager.split_matches_into_files(timezone=args.timezone, player_name=args.player_name)
elif args.clean_friend_feed:
self.stats_manager.clean_json_files('friendFeed.json')
self.stats_manager.clean_json_files('friendFeed.json', player_name=args.player_name)
elif args.clean_event_feed:
self.stats_manager.clean_json_files('eventFeed.json')
self.stats_manager.clean_json_files('eventFeed.json', player_name=args.player_name)
else:
# Data fetching operations
options = {
@ -644,7 +790,12 @@ class CLI:
'connected_accounts': args.connected_accounts,
'settings': args.settings
}
self.stats_manager.fetch_data(args.player_name, **options)
self.stats_manager.fetch_data(
args.player_name,
convert_to_yaml=args.yaml,
delete_json=args.delete_json,
**options
)
def main():
"""Main entry point for the application."""