diff --git a/.gitignore b/.gitignore index 6da585a..4e7a482 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ vercel.txt Pipfile Pipfile.lock .vercel + +#Chromium +Chromium diff --git a/README.md b/README.md index 8d44ed7..f0c0357 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,7 @@ [![Visitor Badge](https://badges.pufler.dev/visits/nicconike/steam-stats)](https://badges.pufler.dev) -![Steam Summary](https://github.com/Nicconike/Steam-Stats/blob/master/assets/steam_summary.png) -![Steam Games Stats](https://github.com/Nicconike/Steam-Stats/blob/master/assets/recently_played_games.png) -![Steam Workshop Stats](https://github.com/Nicconike/Steam-Stats/blob/master/assets/steam_workshop_stats.svg?sanitize=true) diff --git a/api/card.py b/api/card.py index b4e13fd..8ad3a0d 100644 --- a/api/card.py +++ b/api/card.py @@ -1,20 +1,98 @@ """Generate Cards for Steam Stats""" import datetime -from html2image import Html2Image +import math +import os +import asyncio +import time +import zipfile +from pyppeteer import launch +import requests +from steam_stats import get_player_summaries, get_recently_played_games +from steam_workshop import fetch_workshop_item_links, fetch_all_workshop_stats +from dotenv import load_dotenv +load_dotenv() -def format_unix_time(unix_time): - """Convert Unix time to human-readable format""" - return datetime.datetime.fromtimestamp(unix_time).strftime("%d/%m/%Y") +# Secrets Configuration +STEAM_API_KEY = os.getenv("STEAM_API_KEY") +STEAM_ID = os.getenv("STEAM_ID") +STEAM_CUSTOM_ID = os.getenv("STEAM_CUSTOM_ID") +REQUEST_TIMEOUT = (10, 15) + +CHROMIUM_ZIP_URL = "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Win_x64/1309077/chrome-win.zip" +CHROMIUM_DIR = os.path.join(os.getcwd(), 'chromium') +CHROMIUM_EXECUTABLE = os.path.join(CHROMIUM_DIR, 'chrome-win', 'chrome.exe') +MARGIN = 5 + + +def download_and_extract_chromium(): + """Download and extract Chromium from the provided URL""" + if not os.path.exists(CHROMIUM_DIR): + os.makedirs(CHROMIUM_DIR) + zip_path = os.path.join(CHROMIUM_DIR, 'chrome-win.zip') + if not os.path.exists(zip_path): + print("Downloading Chromium...") + response = requests.get( + CHROMIUM_ZIP_URL, stream=True, timeout=REQUEST_TIMEOUT) + with open(zip_path, 'wb') as file: + for chunk in response.iter_content(chunk_size=128): + file.write(chunk) + print("Chromium downloaded successfully.") + if not os.path.exists(CHROMIUM_EXECUTABLE): + print("Extracting Chromium...") + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(CHROMIUM_DIR) + print("Chromium extracted successfully.") + + +async def get_element_bounding_box(html_file, selector): + """Get the bounding box of the specified element using pyppeteer""" + browser = await launch(headless=True, executablePath=CHROMIUM_EXECUTABLE) + page = await browser.newPage() + await page.goto(f'file://{os.path.abspath(html_file)}') + bounding_box = await page.evaluate(f'''() => {{ + var element = document.querySelector('{selector}'); + var rect = element.getBoundingClientRect(); + return {{x: rect.x, y: rect.y, width: rect.width, height: rect.height}}; + }}''') + await browser.close() + # Add margin to the bounding box + bounding_box['x'] = max(bounding_box['x'] - MARGIN, 0) + bounding_box['y'] = max(bounding_box['y'] - MARGIN, 0) + bounding_box['width'] += 2 * MARGIN + bounding_box['height'] += 2 * MARGIN + return bounding_box -def convert_html_to_png(html_file, output_dir, output_file): - """Convert HTML file to PNG image using html2image""" - # Initialize Html2Image - html2png = Html2Image(output_path=output_dir) +async def html_to_png(html_file, output_file, selector): + """Convert HTML file to PNG using pyppeteer with clipping""" + bounding_box = await get_element_bounding_box(html_file, selector) + browser = await launch(headless=True, executablePath=CHROMIUM_EXECUTABLE) + page = await browser.newPage() + await page.goto(f'file://{os.path.abspath(html_file)}') + await page.screenshot({ + 'path': output_file, + 'clip': { + 'x': bounding_box['x'], + 'y': bounding_box['y'], + 'width': bounding_box['width'], + 'height': bounding_box['height'] + } + }) + await browser.close() - # Convert HTML to PNG - html2png.screenshot(html_file=html_file, save_as=output_file) + +def convert_html_to_png(html_file, output_file, selector): + """Convert HTML file to PNG using pyppeteer with clipping""" + download_and_extract_chromium() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(html_to_png(html_file, output_file, selector)) + + +def format_unix_time(unix_time): + """Convert Unix time to human-readable format""" + return datetime.datetime.fromtimestamp(unix_time).strftime("%d/%m/%Y") def generate_card_for_player_summary(player_data): @@ -51,7 +129,7 @@ def generate_card_for_player_summary(player_data): - Responsive SVG Card + Steam Player Summary
@@ -159,15 +239,117 @@ def generate_card_for_played_games(games_data): """ - with open("assets/recently_played_games.html", "w", encoding="utf-8") as file: + with open("../assets/recently_played_games.html", "w", encoding="utf-8") as file: file.write(html_content) - html_file = "assets/recently_played_games.html" - output_dir = "assets" - output_file = "recently_played_games.png" - convert_html_to_png(html_file, output_dir, output_file) + convert_html_to_png("../assets/recently_played_games.html", + "../assets/recently_played_games.png", ".card") return ( - "![Steam Games Stats]" + "![Steam Summary]" "(https://github.com/Nicconike/Steam-Stats/blob/master/assets/recently_played_games.png)" ) + + +def generate_card_for_steam_workshop(workshop_stats): + """Generates HTML content for retrieved Workshop Data""" + html_content = f""" + + + + + + Steam Workshop Stats + + + +
+

Steam Workshop Stats

+ + + + + + + + + + + + + + + + + + + + + +
Workshop StatsTotal
Unique Visitors{workshop_stats["total_unique_visitors"]}
Current Subscribers{workshop_stats["total_current_subscribers"]}
Current Favorites{workshop_stats["total_current_favorites"]}
+
+ + + """ + with open("../assets/steam_workshop_stats.html", "w", encoding="utf-8") as file: + file.write(html_content) + + convert_html_to_png("../assets/steam_workshop_stats.html", + "../assets/steam_workshop_stats.png", ".card") + + return ( + "![Steam Summary]" + "(https://github.com/Nicconike/Steam-Stats/blob/master/assets/steam_workshop_stats.png" + "?sanitize=true)\n" + ) + + +if __name__ == "__main__": + start_time = time.time() + player_summary = get_player_summaries() + recently_played_games = get_recently_played_games() + links = fetch_workshop_item_links(STEAM_CUSTOM_ID) + workshop_data = fetch_all_workshop_stats(links) + generate_card_for_player_summary(player_summary) + generate_card_for_played_games(recently_played_games) + generate_card_for_steam_workshop(workshop_data) + end_time = time.time() # End the timer + total_time = end_time-start_time + total_time = round(total_time, 3) # Total time + print(f"Total Execution Time: {total_time} seconds") diff --git a/api/main.py b/api/main.py index 2eb5c20..f6adf8e 100644 --- a/api/main.py +++ b/api/main.py @@ -1,12 +1,13 @@ """Main Runner Script""" import os import time -import numpy as np -import pandas as pd -import plotly.graph_objects as go from steam_stats import get_player_summaries, get_recently_played_games from steam_workshop import fetch_workshop_item_links, fetch_all_workshop_stats -from card import generate_card_for_player_summary, generate_card_for_played_games +from card import ( + generate_card_for_player_summary, + generate_card_for_played_games, + generate_card_for_steam_workshop +) # Secrets Configuration STEAM_ID = os.getenv("STEAM_CUSTOM_ID") @@ -17,57 +18,7 @@ "Missing STEAM_ID in environment variables") -def generate_svg_for_steam_workshop(workshop_stats): - """Generates SVG Card for retrieved Workshop Data using Plotly for table creation""" - # Create the table data - header_values = ["Workshop Stats", "Total"] - data = { - "Workshop_Stats": ["Unique Visitors", "Current Subscribers", "Current Favorites"], - "Total": [ - workshop_stats["total_unique_visitors"], - workshop_stats["total_current_subscribers"], - workshop_stats["total_current_favorites"] - ] - } - df = pd.DataFrame(data) - # Generate random colors for each row - colors = [f"rgb({np.random.randint(0, 256)}, {np.random.randint(0, 256)}, { - np.random.randint(0, 256)})" for _ in range(len(df))] - - header = { - "values": header_values, - "line_color": "paleturquoise", - "fill_color": "paleturquoise", - "align": "center", - "font": {"color": "black", "size": 16} - } - - cells = { - "values": [df[col].tolist() for col in df.columns], - "line_color": [colors], - "fill_color": [colors], - "align": "center", - "font": {"color": "black", "size": 14} - } - - # Create the table figure - fig = go.Figure(data=[go.Table(header=header, cells=cells)]) - - # Adjust layout to fit the table size - fig.update_layout( - autosize=True, - margin={"pad": 0} - ) - fig.write_image("assets/steam_workshop_stats.svg") - - return ( - "![Steam Workshop Stats](" - "https://github.com/Nicconike/Steam-Stats/blob/master/assets/steam_workshop_stats.svg" - "?sanitize=true)" - ) - - -def update_readme(markdown_data, start_marker, end_marker, readme_path="README.md"): +def update_readme(markdown_data, start_marker, end_marker, readme_path="../README.md"): """Updates the README.md file with the provided Markdown content within specified markers.""" # Read the current README content with open(readme_path, "r", encoding="utf-8") as file: @@ -120,7 +71,7 @@ def update_readme(markdown_data, start_marker, end_marker, readme_path="README.m WORKSHOP_MARKDOWN_CONTENT = "" if links: workshop_data = fetch_all_workshop_stats(links) - WORKSHOP_MARKDOWN_CONTENT += generate_svg_for_steam_workshop( + WORKSHOP_MARKDOWN_CONTENT += generate_card_for_steam_workshop( workshop_data) print("Retrieved all Workshop Stats") else: diff --git a/assets/recently_played_games.html b/assets/recently_played_games.html index 6e67c68..719f361 100644 --- a/assets/recently_played_games.html +++ b/assets/recently_played_games.html @@ -5,53 +5,39 @@ Recently Played Games in Last 2 Weeks - +

Recently Played Games

-
- State of Decay 2 - - State of Decay 2 (11.70 hrs) -
-
Counter-Strike 2 - + Counter-Strike 2 (7.12 hrs)
-
- Wizard with a Gun - - Wizard with a Gun (1.17 hrs) -
-
Perfect Heist 2 - + Perfect Heist 2 (18 mins)
- Killing Floor 2 - - Killing Floor 2 (17 mins) + Wallpaper Engine + + Wallpaper Engine (9 mins)
- Wallpaper Engine - - Wallpaper Engine (9 mins) + State of Decay 2 + + State of Decay 2 (1 mins)
diff --git a/assets/recently_played_games.png b/assets/recently_played_games.png index e7eb064..64ea5c1 100644 Binary files a/assets/recently_played_games.png and b/assets/recently_played_games.png differ diff --git a/assets/steam_summary.html b/assets/steam_summary.html index 99deec8..da010ab 100644 --- a/assets/steam_summary.html +++ b/assets/steam_summary.html @@ -4,7 +4,7 @@ - Responsive SVG Card + Steam Player Summary + + +
+

Steam Workshop Stats

+ + + + + + + + + + + + + + + + + + + + + +
Workshop StatsTotal
Unique Visitors3014
Current Subscribers1775
Current Favorites83
+
+ + + \ No newline at end of file diff --git a/assets/steam_workshop_stats.png b/assets/steam_workshop_stats.png new file mode 100644 index 0000000..dce30cf Binary files /dev/null and b/assets/steam_workshop_stats.png differ diff --git a/assets/steam_workshop_stats.svg b/assets/steam_workshop_stats.svg deleted file mode 100644 index 68e23f1..0000000 --- a/assets/steam_workshop_stats.svg +++ /dev/null @@ -1 +0,0 @@ -Unique VisitorsCurrent SubscribersCurrent FavoritesWorkshop Stats3005177583Total \ No newline at end of file diff --git a/assets/style.css b/assets/style.css index b07a401..a0dd6fb 100644 --- a/assets/style.css +++ b/assets/style.css @@ -10,8 +10,8 @@ body { progress { box-sizing: border-box; border: solid 0.15em #242b35; - width: 12.5em; - height: 1em; + width: 8em; + height: 0.8em; border-radius: 0.5em; background: linear-gradient(#191c23, #2d3341); font: clamp(.625em, 7.5vw, 5em) monospace; @@ -33,27 +33,20 @@ progress::-moz-progress-bar { background: var(--fill); } -progress:nth-child(1) { +.progress-style-1 { --fill: + linear-gradient(rgba(90, 240, 255, 0.85), transparent), repeating-linear-gradient(90deg, - transparent 0 0.15em, #f1c00c 0 0.5em) 0.25em, - linear-gradient(#f3c402, #ed7401); -} - -progress:nth-child(2) { - --fill: - linear-gradient(#ffec9d, transparent 85%), - linear-gradient(90deg, #ffe94b, #f94745); + #123c92 0 0.0625em, #1b5ec6 0 1em); } -progress:nth-child(3) { +.progress-style-2 { --fill: - linear-gradient(rgba(226, 102, 76, 0.65), transparent), - repeating-linear-gradient(135deg, - #a22215 0 0.25em, #be2a20 0 0.5em); + linear-gradient(#d0a9e2, transparent 85%), + linear-gradient(90deg, #433485, #dd3c6e); } -progress:nth-child(4) { +.progress-style-3 { --fill: linear-gradient(rgba(215, 131, 227, 0.5), transparent), conic-gradient(from -30deg at 25%, @@ -62,15 +55,22 @@ progress:nth-child(4) { #8b42ab 240deg, #9956b3 0%) 0/ 0.7em; } -progress:nth-child(5) { +.progress-style-4 { --fill: - linear-gradient(#d0a9e2, transparent 85%), - linear-gradient(90deg, #433485, #dd3c6e); + linear-gradient(rgba(226, 102, 76, 0.65), transparent), + repeating-linear-gradient(135deg, + #a22215 0 0.25em, #be2a20 0 0.5em); } -progress:nth-child(6) { +.progress-style-5 { + --fill: + linear-gradient(#ffec9d, transparent 85%), + linear-gradient(90deg, #ffe94b, #f94745); +} + +.progress-style-6 { --fill: - linear-gradient(rgba(90, 240, 255, 0.85), transparent), repeating-linear-gradient(90deg, - #123c92 0 0.0625em, #1b5ec6 0 1em); + transparent 0 0.15em, #f1c00c 0 0.5em) 0.25em, + linear-gradient(#f3c402, #ed7401); } diff --git a/requirements.txt b/requirements.txt index 34a8b66..f27164a 100644 Binary files a/requirements.txt and b/requirements.txt differ