Skip to content

Commit

Permalink
Merge pull request #16 from zeroquinc:featurez
Browse files Browse the repository at this point in the history
feature: additional platinum info & cached colors
  • Loading branch information
zeroquinc authored Jun 20, 2024
2 parents c4ab38a + 972d20c commit efa45e7
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 12 deletions.
9 changes: 5 additions & 4 deletions api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
14 changes: 12 additions & 2 deletions src/discord/cogs/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
66 changes: 62 additions & 4 deletions src/steam/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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 []
Expand Down Expand Up @@ -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)
Expand All @@ -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)
27 changes: 26 additions & 1 deletion utils/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,29 @@ def format_timestamp(timestamp):
@staticmethod
def seconds_until_next_hour():
now = datetime.now()
return (60 - now.minute) * 60 - now.second
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"
35 changes: 34 additions & 1 deletion utils/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down

0 comments on commit efa45e7

Please sign in to comment.