[2026-05-27] add: steam-grid-artwork.py — SteamGridDB artwork fetcher for non-Steam shortcuts
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user