#!/usr/bin/env python3 """Universal SteamGridDB artwork fetcher for non-Steam shortcuts on Bazzite HTPC. Usage: python3 steam-grid-artwork.py --appid APPID --name "Game Name" Will download grid (p.png), hero (_hero.png), and logo (_logo.png) into ~/.steam/steam/userdata/147839491/config/grid/ Requires: SGDB_API_KEY env var or ~/Documents/steamgriddb_api_key.txt """ import json import os import subprocess import sys import tempfile from pathlib import Path # ── config ──────────────────────────────────────────── GRID_DIR = os.path.expanduser( "~/.steam/steam/userdata/147839491/config/grid" ) KEY_FILE = os.path.expanduser("~/Documents/steamgriddb_api_key.txt") SGDB_KEY = os.environ.get("SGDB_API_KEY") if not SGDB_KEY: try: SGDB_KEY = Path(KEY_FILE).read_text().strip() except Exception: print("ERROR: SGDB_API_KEY not set and ~/Documents/steamgriddb_api_key.txt not found") sys.exit(1) API = "https://www.steamgriddb.com/api/v2" UA = "steam-grid-artwork/1.0" def api_get(path): """curl wrapper returning parsed JSON.""" url = f"{API}/{path}" r = subprocess.run( ["curl", "-s", "-H", f"Authorization: Bearer {SGDB_KEY}", "-A", UA, url], capture_output=True, text=True, timeout=20 ) if r.returncode != 0: print(f" curl error: {r.stderr}") return None try: return json.loads(r.stdout) except json.JSONDecodeError: print(f" invalid JSON response: {r.stdout[:200]}") return None def download(url, dest): """Download a file via curl.""" print(f" → {dest.name} ({dest.parent})") subprocess.run( ["curl", "-sL", "-H", "User-Agent: Mozilla/5.0", url, "-o", str(dest)], capture_output=True, timeout=60, check=True ) size = dest.stat().st_size print(f" {size:,} bytes") def pick_first(items, prefer_style=None, prefer_dim=None): """Pick first item, optionally preferring a style or dimension.""" if prefer_style: for item in items: if item.get("style") == prefer_style: return item if prefer_dim: w, h = prefer_dim for item in items: if item.get("width") == w and item.get("height") == h: return item return items[0] if items else None def fetch_artwork(appid, game_name): """Fetch grid, hero, logo for a given appid and game name.""" # ── search ── safe_name = game_name.replace(" ", "%20") result = api_get(f"search/autocomplete/{safe_name}") if not result or not result.get("data"): # try trimmed name (before colon, dash etc.) short = game_name.split(":")[0].split("-")[0].strip().replace(" ", "%20") if short != safe_name: result = api_get(f"search/autocomplete/{short}") if not result or not result.get("data"): print(f" ✗ game '{game_name}' not found on SteamGridDB") return False game = result["data"][0] game_id = game["id"] sg_name = game["name"] print(f" ✓ SGDB id={game_id} — \"{sg_name}\"") # ── grid (p.png) ── dest = Path(GRID_DIR) / f"{appid}p.png" print(" Grid:") result = api_get(f"grids/game/{game_id}") if result and result.get("data"): item = pick_first(result["data"], prefer_style="alternate") if item: download(item["url"], dest) else: print(" no grids found") # ── hero (_hero.png) ── dest = Path(GRID_DIR) / f"{appid}_hero.png" print(" Hero:") result = api_get(f"heroes/game/{game_id}") if result and result.get("data"): item = pick_first(result["data"]) if item: download(item["url"], dest) else: print(" no heroes found") # ── logo (_logo.png) ── dest = Path(GRID_DIR) / f"{appid}_logo.png" print(" Logo:") result = api_get(f"logos/game/{game_id}") if result and result.get("data"): item = pick_first(result["data"]) if item: download(item["url"], dest) else: print(" no logos found") return True if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Fetch SteamGridDB artwork") parser.add_argument("--appid", required=True, help="Steam app ID (unsigned)") parser.add_argument("--name", required=True, help="Game name for search") args = parser.parse_args() fetch_artwork(args.appid, args.name)