diff --git a/portprotonqt/image_utils.py b/portprotonqt/image_utils.py index 5700b92..d2bc923 100644 --- a/portprotonqt/image_utils.py +++ b/portprotonqt/image_utils.py @@ -83,6 +83,43 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q except Exception as e: logger.error(f"Ошибка обработки URL {cover}: {e}") + # SteamGridDB (SGDB) + if cover and cover.startswith("https://cdn2.steamgriddb.com"): + try: + parts = cover.split("/") + filename = parts[-1] if parts else "sgdb_cover.png" + # SGDB ссылки содержат уникальный хеш в названии — используем как имя + local_path = os.path.join(image_folder, filename) + + if os.path.exists(local_path): + pixmap = QPixmap(local_path) + finish_with(pixmap) + return + + def on_downloaded(result: str | None): + pixmap = QPixmap() + if result and os.path.exists(result): + pixmap.load(result) + if pixmap.isNull(): + placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name) + if placeholder_path and QFile.exists(placeholder_path): + pixmap.load(placeholder_path) + else: + pixmap = QPixmap(width, height) + pixmap.fill(QColor("#333333")) + painter = QPainter(pixmap) + painter.setPen(QPen(QColor("white"))) + painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image") + painter.end() + finish_with(pixmap) + + logger.info("Downloading SGDB cover for %s -> %s", app_name or "unknown", filename) + downloader.download_async(cover, local_path, timeout=5, callback=on_downloaded) + return + + except Exception as e: + logger.error(f"Ошибка обработки SGDB URL {cover}: {e}") + if cover and cover.startswith(("http://", "https://")): try: local_path = os.path.join(image_folder, f"{app_name}.jpg") diff --git a/portprotonqt/steam_api.py b/portprotonqt/steam_api.py index ff5c610..ecda60c 100644 --- a/portprotonqt/steam_api.py +++ b/portprotonqt/steam_api.py @@ -23,6 +23,7 @@ import requests import random import base64 import glob +import urllib.parse downloader = Downloader() logger = get_logger(__name__) @@ -411,6 +412,39 @@ def save_app_details(app_id, data): with open(cache_file, "wb") as f: f.write(orjson.dumps(data)) +def fetch_sgdb_cover(game_name: str) -> str: + """ + Fetch a cover image URL from steamgrid.usebottles.com for the given game. + The API returns a single string (quoted URL). + """ + try: + encoded = urllib.parse.quote(game_name) + url = f"https://steamgrid.usebottles.com/api/search/{encoded}" + resp = requests.get(url, timeout=5) + if resp.status_code != 200: + logger.warning("SGDB request failed for %s: %s", game_name, resp.status_code) + return "" + text = resp.text.strip() + # Убираем возможные кавычки вокруг строки + if text.startswith('"') and text.endswith('"'): + text = text[1:-1] + if text: + logger.info("Fetched SGDB cover for %s: %s", game_name, text) + return text + except Exception as e: + logger.warning("Failed to fetch SGDB cover for %s: %s", game_name, e) + return "" + + +def check_url_exists(url: str) -> bool: + """Check whether a URL returns HTTP 200.""" + try: + r = requests.head(url, timeout=3) + return r.status_code == 200 + except Exception: + return False + + def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]): """ Asynchronously fetches detailed app info from Steam API. @@ -629,6 +663,11 @@ def get_full_steam_game_info_async(appid: int, callback: Callable[[dict], None]) title = decode_text(app_info.get("name", "")) description = decode_text(app_info.get("short_description", "")) cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg" + if not check_url_exists(cover): + logger.info("Steam cover not found for %s, trying SGDB", title) + alt_cover = fetch_sgdb_cover(title) + if alt_cover: + cover = alt_cover def on_protondb_tier(tier: str): def on_anticheat_status(anticheat_status: str): @@ -722,12 +761,15 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla game_name = desktop_name or exe_name.capitalize() if not matching_app: + cover = fetch_sgdb_cover(game_name) or "" + logger.info("Using SGDB cover for non-Steam game '%s': %s", game_name, cover) + def on_anticheat_status(anticheat_status: str): callback({ "appid": "", "name": decode_text(game_name), "description": "", - "cover": "", + "cover": cover, "controller_support": "", "protondb_tier": "", "steam_game": "false", @@ -758,6 +800,11 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla title = decode_text(app_info.get("name", game_name)) description = decode_text(app_info.get("short_description", "")) cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg" + if not check_url_exists(cover): + logger.info("Steam cover not found for %s, trying SGDB", title) + alt_cover = fetch_sgdb_cover(title) + if alt_cover: + cover = alt_cover controller_support = app_info.get("controller_support", "") def on_protondb_tier(tier: str):