diff --git a/api/users.py b/api/users.py index 1522d45..8804a19 100644 --- a/api/users.py +++ b/api/users.py @@ -33,19 +33,20 @@ def get_user_achievements(self, app_id: str, l: str = "en"): params = {"steamid": self.steam_id, "appid": app_id, "l": l} response = self.client._get(endpoint, params) if 'achievements' in response['playerstats']: - self.achievements = [UserAchievement(a) for a in response['playerstats']['achievements']] + self.achievements = [UserAchievement(a, app_id) for a in response['playerstats']['achievements']] else: self.achievements = [] return response - + class UserAchievement: - def __init__(self, data): + def __init__(self, data, appid): # Add appid parameter + self.appid = appid # Store appid self.apiname = data['apiname'] self.achieved = data['achieved'] self.unlocktime = DateUtils.format_timestamp(data['unlocktime']) self.name = data['name'] self.description = data.get('description', '') - + class UserSummary: def __init__(self, data): self.personaname = data['personaname'] diff --git a/src/discord/cogs/tasks.py b/src/discord/cogs/tasks.py index 657feab..4ad84e7 100644 --- a/src/discord/cogs/tasks.py +++ b/src/discord/cogs/tasks.py @@ -21,15 +21,25 @@ async def process_achievements(self): all_achievements = await get_all_achievements(user_ids, api_keys) all_achievements.sort(key=lambda a: (datetime.strptime(a[1].unlocktime, "%d/%m/%y %H:%M:%S"), a[4]/a[5]), reverse=False) logged_users = set() # Set to keep track of users that have already been logged + latest_unlocktimes = {} # Dictionary to track the latest unlocktime for each user-game combination + for game_achievement, user_achievement, user_game, user, total_achievements, current_count in all_achievements: if user.summary.personaname not in logged_users: logger.info(f"Found achievements for {user.summary.personaname}") logged_users.add(user.summary.personaname) await create_and_send_embed(channel, game_achievement, user_achievement, user_game, user, total_achievements, current_count) - completion_key = (user.summary.personaname, user_game.appid) # Unique key for each user-game combination + + # Update the latest unlocktime + completion_key = (user.summary.personaname, user_game.appid) + latest_unlocktime = datetime.strptime(user_achievement.unlocktime, "%d/%m/%y %H:%M:%S") + if completion_key not in latest_unlocktimes or latest_unlocktime > latest_unlocktimes[completion_key]: + latest_unlocktimes[completion_key] = latest_unlocktime + if current_count == total_achievements and completion_key not in self.completed_games: completion_channel = self.bot.get_channel(PLATINUM_CHANNEL) - await create_and_send_completion_embed(completion_channel, user_game, user, total_achievements) + # Retrieve the latest unlocktime for this user-game combination + latest_unlocktime = latest_unlocktimes[completion_key].strftime("%d/%m/%y %H:%M:%S") + await create_and_send_completion_embed(completion_channel, user_game, user, total_achievements, latest_unlocktime) self.completed_games.add(completion_key) # Mark this game as completed for this user await asyncio.sleep(1) diff --git a/src/steam/functions.py b/src/steam/functions.py index ed98923..aad2455 100644 --- a/src/steam/functions.py +++ b/src/steam/functions.py @@ -9,6 +9,7 @@ from config.globals import ACHIEVEMENT_TIME, PLATINUM_ICON from src.discord.embed import EmbedBuilder from utils.image import get_discord_color +from utils.datetime import DateUtils from utils.custom_logger import logger DATE_FORMAT = '%d/%m/%y %H:%M:%S' @@ -66,6 +67,26 @@ def load_achievements_from_file(app_id): achievement_dict = scrape_all_achievements(app_id) return achievement_dict +def find_rarest_achievement(app_id): + achievement_dict = load_achievements_from_file(app_id) + if not achievement_dict: + return None + + lowest_percentage = None + rarest_achievement_name = None + for name, info in achievement_dict.items(): + percentage_str = info.get('percentage', 'Unknown') + if percentage_str != 'Unknown': + percentage = float(percentage_str.replace('%', '')) + if lowest_percentage is None or percentage < lowest_percentage: + lowest_percentage = percentage + rarest_achievement_name = name + + if rarest_achievement_name: + return f"{rarest_achievement_name} ({lowest_percentage}%)" + else: + return None + def get_achievement_description(app_id, achievement_name): achievement_dict = load_achievements_from_file(app_id) logger.debug(f"Looking for achievement: {achievement_name}") @@ -119,6 +140,37 @@ async def find_matching_achievements(user_achievement, game_achievements, curren matching_achievements.append((game_achievement, user_achievement, user_game, user)) return matching_achievements +def calculate_completion_time_span(data): + # Check if the data structure is as expected and contains 'playerstats' and 'achievements' + if isinstance(data, dict) and 'playerstats' in data and \ + isinstance(data['playerstats'], dict) and 'achievements' in data['playerstats']: + user_achievements = data['playerstats']['achievements'] + else: + logger.error("Invalid data format. Expected 'playerstats' and 'achievements' keys.") + return None + + unlock_times = [] + for ua in user_achievements: + if isinstance(ua, dict) and 'unlocktime' in ua: + try: + unlock_time = DateUtils.convert_to_datetime(ua['unlocktime']) + unlock_times.append(unlock_time) + except ValueError as e: + # Assuming ua['name'] exists for logging purposes + logger.error(f"Error converting unlocktime for achievement {ua.get('name', 'Unknown')}: {e}") + else: + # Log an error or handle unexpected data format + logger.error(f"Unexpected data format for achievement: {ua}") + + if not unlock_times: + return None + + first_unlocktime = min(unlock_times) + latest_unlocktime = max(unlock_times) + time_span = DateUtils.calculate_time_span(first_unlocktime, latest_unlocktime) + + return DateUtils.format_time_span(time_span) + async def get_recent_achievements(game_achievements, user_achievements, user_game, user): if user_achievements is None: return [] @@ -182,7 +234,7 @@ async def check_recently_played_games(user_id, api_key): return [] async def create_and_send_embed(channel, game_achievement, user_achievement, user_game, user, total_achievements, current_count): - color = await get_discord_color(game_achievement.icon) + color = await get_discord_color(user_game.game_icon) title, description, completion_info, unlock_percentage, footer = create_embed_info(user_achievement, user_game, current_count, total_achievements, user) embed = EmbedBuilder(title=title, description=description, color=color) embed.set_thumbnail(url=game_achievement.icon) @@ -193,12 +245,18 @@ async def create_and_send_embed(channel, game_achievement, user_achievement, use logger.info(f"Sending embed for {user.summary.personaname}: {user_achievement.name} ({user_game.name})") await embed.send_embed(channel) -async def create_and_send_completion_embed(completion_channel, user_game, user, total_achievements): +async def create_and_send_completion_embed(completion_channel, user_game, user, total_achievements, latest_unlocktime): color = await get_discord_color(user_game.game_icon) - description = f"{user.summary.personaname} has completed all {total_achievements} achievements for [{user_game.name}]({user_game.url})!" + description = f"[{user.summary.personaname}]({user.summary.profileurl}) has completed all {total_achievements} achievements for [{user_game.name}]({user_game.url})!" embed = EmbedBuilder(description=description, color=color) + completion_time_span = calculate_completion_time_span(user.get_user_achievements(user_game.appid)) + completion_time = str(completion_time_span).split('.')[0] # Convert to string and remove microseconds + # Get the description of the rarest achievement + rarest_achievement = find_rarest_achievement(user_game.appid) + if rarest_achievement: + embed.add_field(name="Rarest Achievement", value=rarest_achievement, inline=False) embed.set_author(name="Platinum unlocked", icon_url=PLATINUM_ICON) embed.set_thumbnail(url=user_game.game_icon) - embed.set_footer(text=user.summary.personaname, icon_url=user.summary.avatarfull) + embed.set_footer(text=f"Platinum in {completion_time}", icon_url=user.summary.avatarfull) logger.info(f"Sending completion embed for {user.summary.personaname}: All achievements ({user_game.name})") await embed.send_embed(completion_channel) \ No newline at end of file diff --git a/utils/datetime.py b/utils/datetime.py index 503f403..304ae4e 100644 --- a/utils/datetime.py +++ b/utils/datetime.py @@ -22,4 +22,29 @@ def format_timestamp(timestamp): @staticmethod def seconds_until_next_hour(): now = datetime.now() - return (60 - now.minute) * 60 - now.second \ No newline at end of file + return (60 - now.minute) * 60 - now.second + + @staticmethod + def convert_to_datetime(unix_timestamp): + try: + return datetime.fromtimestamp(int(unix_timestamp)) + except ValueError as e: + # Handle or log the error as needed + raise ValueError(f"Error converting Unix timestamp: {e}") from e + + @staticmethod + def calculate_time_span(start_date, end_date): + return end_date - start_date + + @staticmethod + def format_time_span(time_span): + days, seconds = time_span.days, time_span.seconds + years, days = divmod(days, 365) + hours = seconds // 3600 + + if years > 0: + return f"{years} years, {days} days and {hours} hours" + elif days > 0: + return f"{days} days and {hours} hours" + else: + return f"{hours} hours" \ No newline at end of file diff --git a/utils/image.py b/utils/image.py index 13d917d..604c4a3 100644 --- a/utils/image.py +++ b/utils/image.py @@ -3,20 +3,53 @@ from io import BytesIO import numpy as np import asyncio +import json +from pathlib import Path +from utils.custom_logger import logger + +# Cache file path +cache_file_path = Path("src/steam/data/image_cache.json") + +# Function to load cache +def load_cache(): + if not cache_file_path.exists(): + return {} + with open(cache_file_path, "r") as file: + return json.load(file) + +# Function to save cache +def save_cache(cache): + with open(cache_file_path, "w") as file: + json.dump(cache, file) + +# Check if the color is colorful def is_colorful(color): r, g, b = color return np.std([r, g, b]) +# Asynchronous function to get discord color async def get_discord_color(image_url, crop_percentage=0.5): + # Load the cache + cache = load_cache() + # Check if the URL is already in the cache + if image_url in cache: + logger.debug(f"Cache hit for {image_url}") + return cache[image_url] + # If not in cache, process the image loop = asyncio.get_event_loop() - return await loop.run_in_executor( + color = await loop.run_in_executor( None, get_discord_color_blocking, image_url, crop_percentage, ) + # Update the cache with the new color + cache[image_url] = color + save_cache(cache) + return color +# Blocking function to get discord color def get_discord_color_blocking(image_url, crop_percentage=0.5): response = requests.get(image_url) img = Image.open(BytesIO(response.content))