Skip to content

Card

Generate Cards for Steam Stats

logger = logging.getLogger(__name__) module-attribute

CARD_SELECTOR = '.outer-card' module-attribute

branch_name = os.getenv('GITHUB_REF_NAME', 'master') module-attribute

personastate_map = {0: 'Offline', 1: 'Online', 2: 'Busy', 3: 'Away', 4: 'Snooze', 5: 'Looking to trade', 6: 'Looking to play'} module-attribute

FloatRect

Define FloatRect type for compatibility with Playwright

Source code in api/card.py
class FloatRect(TypedDict):
    """Define FloatRect type for compatibility with Playwright"""

    x: float
    y: float
    width: float
    height: float
x: float instance-attribute
y: float instance-attribute
width: float instance-attribute
height: float instance-attribute

handle_exception(e)

Handle exceptions and log appropriate error messages

Source code in api/card.py
def handle_exception(e):
    """Handle exceptions and log appropriate error messages"""
    if isinstance(e, FileNotFoundError):
        logger.error("File Not Found Error: %s", e)
    elif isinstance(e, PlaywrightError):
        logger.error("Playwright Error: %s", e)
    elif isinstance(e, KeyError):
        logger.error("Key Error: %s", e)
    elif isinstance(e, asyncio.TimeoutError):
        logger.error("Timeout Error: %s", e)
    elif isinstance(e, ValueError):
        logger.error("Value Error: %s", e)
    else:
        logger.error("Unexpected Error: %s", e)

html_to_png(html_file: str, output_file: str, selector: str) -> bool async

Convert an HTML file to a PNG using Playwright

Source code in api/card.py
async def html_to_png(html_file: str, output_file: str, selector: str) -> bool:
    """Convert an HTML file to a PNG using Playwright"""
    browser = None
    try:
        async with async_playwright() as p:
            browser = await p.chromium.launch(headless=True)
            page = await browser.new_page()
            await page.goto("file://" + os.path.abspath(html_file))

            element = await page.query_selector(selector)
            if not element:
                logger.error("Element not found for selector: %s", selector)
                return False
            await element.screenshot(path=output_file)
            return True

    except (PlaywrightError, asyncio.TimeoutError) as e:
        handle_exception(e)
        return False

    finally:
        if browser:
            await browser.close()

convert_html_to_png(html_file, output_file, selector)

Synchronous wrapper to convert HTML to PNG

Source code in api/card.py
def convert_html_to_png(html_file, output_file, selector):
    """Synchronous wrapper to convert HTML to PNG"""
    try:
        return asyncio.run(html_to_png(html_file, output_file, selector))
    except (FileNotFoundError, PlaywrightError, KeyError, asyncio.TimeoutError) as e:
        handle_exception(e)
        return False

format_unix_time(unix_time)

Convert Unix time to human-readable format with ordinal day

Source code in api/card.py
def format_unix_time(unix_time):
    """Convert Unix time to human-readable format with ordinal day"""
    dt = datetime.datetime.fromtimestamp(unix_time)
    day = dt.day
    suffix = (
        "th" if 11 <= day <= 13 else {1: "st", 2: "nd", 3: "rd"}.get(day % 10, "th")
    )
    return f"{day}{suffix} {dt.strftime('%b %Y')}"

generate_card_for_player_summary(player_data)

Generate HTML content based on Steam Player Summary Data

Source code in api/card.py
def generate_card_for_player_summary(player_data):
    """Generate HTML content based on Steam Player Summary Data"""
    if not player_data:
        return None

    summary = player_data["response"]["players"][0]
    player = {
        "name": summary.get("personaname", "Unknown"),
        "status": personastate_map.get(summary.get("personastate", 0), "Unknown"),
        "avatar": summary.get("avatarfull", ""),
        "country": summary.get("loccountrycode", ""),
        "lastlogoff": format_unix_time(summary.get("lastlogoff", 0)),
        "timecreated": format_unix_time(summary.get("timecreated", 0)),
        "game": summary.get("gameextrainfo"),
    }

    country_section = (
        f"""
        <p id="country">Country: <span id="country-code">{player['country']}</span>
            <img id="flag" class="flag"
            src="https://flagcdn.com/w320/{player['country'].lower()}.png" alt="Flag">
        </p>
        """
        if player["country"]
        else ""
    )

    game_section = (
        f"<p id='game'>Currently Playing: <span id='game-info'>{player['game']}</span></p>"
        if player["game"]
        else ""
    )

    html_content = f"""
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Steam Profile Summary</title>
        <style>
            .outer-card {{
                width: 460px;
                margin: auto;
                padding: 10px;
                background-color: #f8f8f8;
                box-sizing: border-box;
                border-radius: 12px;
            }}
            .card {{
                width: 100%;
                max-width: 400px;
                margin: auto;
                border: 2px solid #000;
                padding: 15px;
                box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
                border-radius: 10px;
                background-color: #fff;
            }}
            .avatar {{
                width: 80px;
                height: 80px;
                border-radius: 50%;
                margin: auto;
            }}
            .content {{
                text-align: center;
            }}
            .flag {{
                width: 32px;
                height: 24px;
                vertical-align: middle;
            }}
            .info-container {{
                display: flex;
                justify-content: space-between;
                margin-top: 10px;
            }}
            .info-left, .info-right {{
                width: 48%;
            }}
        </style>
    </head>
    <body>
        <div class="outer-card">
            <div class="card">
                <div class="content">
                    <h2>Steam Profile Summary</h2>
                    <img id="avatar" class="avatar" src="{player['avatar']}" alt="Avatar">
                    <h2 id="name">{player['name']}</h2>
                    <div class="info-container">
                        <div class="info-left">
                            <p id="status">Status: {player['status']}</p>
                            {country_section}
                        </div>
                        <div class="info-right">
                            <p id="lastlogoff">Last Logoff: {player['lastlogoff']}</p>
                            <p id="timecreated">PC Gaming Since: {player['timecreated']}</p>
                        </div>
                    </div>
                    {game_section}
                </div>
            </div>
        </div>
    </body>
    </html>
    """

    html_path, png_path, relative_png_path = get_asset_paths("steam_summary")
    with open(html_path, "w", encoding="utf-8") as file:
        file.write(html_content)
    convert_html_to_png(html_path, png_path, CARD_SELECTOR)

    return (
        f"![Steam Summary](https://github.com/{repo_owner}/"
        f"{repo_name}/blob/{branch_name}/{relative_png_path})\n"
    )

format_playtime(playtime)

Format playtime into human-readable format

Source code in api/card.py
def format_playtime(playtime):
    """Format playtime into human-readable format"""
    if playtime < 60:
        unit = "min" if playtime == 1 else "mins"
        return f"{playtime} {unit}"
    hours, minutes = divmod(playtime, 60)
    hr_unit = "hr" if hours == 1 else "hrs"
    if minutes == 0:
        return f"{hours} {hr_unit}"
    min_unit = "min" if minutes == 1 else "mins"
    return f"{hours} {hr_unit} and {minutes} {min_unit}"

generate_progress_bar(game, index, max_playtime, log_scale, placeholder_image)

Generate progress bar HTML for a single game

Source code in api/card.py
def generate_progress_bar(game, index, max_playtime, log_scale, placeholder_image):
    """Generate progress bar HTML for a single game"""
    name = game.get("name", "Unknown Game")
    playtime = game.get("playtime_2weeks", 0)
    img_icon_url = (
        placeholder_image
        if name == "Spacewar"
        else (
            f"https://media.steampowered.com/steamcommunity/public/images/apps/"
            f"{game.get('appid')}/{game.get('img_icon_url')}.jpg"
        )
    )
    normalized_playtime = (
        round(math.log1p(playtime) / math.log1p(max_playtime) * 100)
        if log_scale
        else round((playtime / max_playtime) * 100)
    )
    display_time = format_playtime(playtime)
    style_class = f"progress-style-{(index % 6) + 1}"

    return f"""
    <div class="bar-container">
        <img src="{img_icon_url}" alt="{name}" class="game-icon">
        <progress class="{style_class}" value="{normalized_playtime}" max="100"></progress>
        <div class="game-info">
            <span class="game-name">{name}</span>
            <span class="game-time">{display_time}</span>
        </div>
    </div>
    """

generate_card_for_recent_games(games_data)

Generate HTML Card for recently played games in last 2 weeks

Source code in api/card.py
def generate_card_for_recent_games(games_data):
    """Generate HTML Card for recently played games in last 2 weeks"""
    if not games_data:
        return None

    placeholder_image = "https://i.imgur.com/DBnVqet.jpg"
    log_scale = os.getenv("INPUT_LOG_SCALE", "false").lower() in ("true", "1", "t")
    watermark = '<div class="watermark">Log Scale Enabled</div>' if log_scale else ""

    max_playtime = (
        max(game["playtime_2weeks"] for game in games_data["response"]["games"]) or 1
    )

    progress_bars = "".join(
        generate_progress_bar(game, i, max_playtime, log_scale, placeholder_image)
        for i, game in enumerate(games_data["response"]["games"])
        if "name" in game and "playtime_2weeks" in game
    )

    html_content = f"""
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Recently Played Games in Last 2 Weeks</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <div class="outer-card">
            <div class="card">
                <div class="content" style="position: relative; text-align: center;">
                    <h2>Recently Played Games (Last 2 Weeks)</h2>
                    {progress_bars}
                    {watermark}
                </div>
            </div>
        </div>
    </body>
    </html>
    """

    html_path, png_path, relative_png_path = get_asset_paths("recently_played_games")
    with open(html_path, "w", encoding="utf-8") as file:
        file.write(html_content)
    convert_html_to_png(html_path, png_path, CARD_SELECTOR)

    return (
        f"![Recently Played Games](https://github.com/{repo_owner}/"
        f"{repo_name}/blob/{branch_name}/{relative_png_path})"
    )

generate_card_for_steam_workshop(workshop_stats)

Generates HTML content for retrieved Workshop Data

Source code in api/card.py
def generate_card_for_steam_workshop(workshop_stats):
    """Generates HTML content for retrieved Workshop Data"""
    html_content = f"""
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Steam Workshop Stats</title>
        <style>
            body {{
                font-family: Arial, sans-serif;
                display: flex;
                justify-content: center;
                align-items: center;
                min-height: 100vh;
                background-color: #f0f0f0;
                margin: 0;
            }}
            .outer-card {{
                width: 460px;
                margin: auto;
                padding: 10px;
                background-color: #ffffff;
                box-sizing: border-box;
                border-radius: 12px;
            }}
            .card {{
                width: 100%;
                max-width: 400px;
                margin: auto;
                border: 2px solid #000;
                padding: 15px;
                box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
                border-radius: 10px;
                background-color: #fff;
                text-align: center;
            }}
            table {{
                width: 100%;
                border-collapse: collapse;
            }}
            th, td {{
                border: 1px solid #ddd;
                padding: 8px;
                text-align: center;
            }}
            th {{
                background-color: #6495ED;
                color: white;
            }}
            tr:nth-child(even) {{
                background-color: #f2f2f2;
            }}
        </style>
    </head>
    <body>
        <div class="outer-card">
            <div class="card">
                <h2>Steam Workshop Stats</h2>
                <table>
                    <thead>
                        <tr>
                            <th>Workshop Stats</th>
                            <th>Total</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>Unique Visitors</td>
                            <td>{workshop_stats.get("total_unique_visitors", 0)}</td>
                        </tr>
                        <tr>
                            <td>Current Subscribers</td>
                            <td>{workshop_stats.get("total_current_subscribers", 0)}</td>
                        </tr>
                        <tr>
                            <td>Current Favorites</td>
                            <td>{workshop_stats.get("total_current_favorites", 0)}</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </body>
    </html>
    """

    html_path, png_path, relative_png_path = get_asset_paths("steam_workshop_stats")
    with open(html_path, "w", encoding="utf-8") as file:
        file.write(html_content)
    convert_html_to_png(html_path, png_path, CARD_SELECTOR)

    return (
        f"![Steam Workshop Stats](https://github.com/{repo_owner}/"
        f"{repo_name}/blob/{branch_name}/{relative_png_path})"
    )

Source Code

View the complete source code for this module on GitHub: api/card.py