Files
saphid--frame-control/ui/frame_steamgriddb.py
T
saphidandGPT-6 Astra f695f398de Render complete Steam artwork for every sideload and fix VR shortcut identity
Add optional SteamGridDB settings, source-first fallbacks rendered with Steam canvas, backfill commands and shared APK/native library details. Verify live artwork and Open Saber Steam Play/Stop; document SuperTux's clipboard crash.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 22:20:27 +10:00

104 lines
4.2 KiB
Python

"""Optional SteamGridDB artwork. Credentials stay on the host, never in app metadata."""
import json
import os
import re
import tempfile
import urllib.parse
import urllib.request
import frame_host
API = 'https://www.steamgriddb.com/api/v2'
MAX_JSON = 2 * 1024 * 1024
def settings_path():
return frame_host.data_dir('artwork-settings.json')
def api_key():
env = os.environ.get('STEAMGRIDDB_API_KEY') or os.environ.get('FRAME_STEAMGRIDDB_API_KEY')
if env:
return env.strip()
try:
return str(json.loads(settings_path().read_text()).get('steamgriddb_api_key') or '')
except (OSError, ValueError, AttributeError):
return ''
def settings():
return {'steamgriddb_configured': bool(api_key()),
'environment': bool(os.environ.get('STEAMGRIDDB_API_KEY') or os.environ.get('FRAME_STEAMGRIDDB_API_KEY'))}
def save_settings(body):
key = body.get('steamgriddb_api_key')
if not isinstance(key, str) or len(key) > 200 or (key and not re.fullmatch(r'[A-Za-z0-9_-]+', key)):
raise ValueError('enter a valid SteamGridDB API key, or an empty value to remove it')
path = settings_path()
path.parent.mkdir(parents=True, exist_ok=True)
fd, temp = tempfile.mkstemp(prefix='.artwork-', dir=str(path.parent))
try:
with os.fdopen(fd, 'w') as f:
json.dump({'steamgriddb_api_key': key}, f)
os.replace(temp, path)
finally:
if os.path.exists(temp):
os.remove(temp)
return settings()
def _get(path, key):
request = urllib.request.Request(API + path, headers={'Authorization': 'Bearer ' + key,
'User-Agent': 'FrameControl/1.0'})
# Do not carry the credential to redirects or include it in error messages.
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
with urllib.request.build_opener(NoRedirect()).open(request, timeout=12) as response:
data = response.read(MAX_JSON + 1)
if len(data) > MAX_JSON:
raise ValueError('SteamGridDB response too large')
result = json.loads(data)
if not result.get('success') or not isinstance(result.get('data'), list):
raise ValueError('SteamGridDB lookup failed')
return result['data']
def _name(value):
return re.sub(r'[^a-z0-9]', '', str(value).casefold())
def lookup(name):
"""Best-voted art per slot for an exact title match; unrelated games are never guessed."""
key = api_key()
if not key:
return {}, []
try:
matches = _get('/search/autocomplete/' + urllib.parse.quote(name, safe=''), key)
names = {_name(name), _name(re.sub(r'\s+VR$', '', name, flags=re.I))}
game = next((g for g in matches if _name(g.get('name')) in names), None)
if not game:
return {}, []
gid = int(game['id'])
result, warnings = {}, []
for slot, kind, dimensions in [('grid', 'grids', '600x900'), ('wide', 'grids', '920x430'),
('hero', 'heroes', ''), ('logo', 'logos', ''), ('icon', 'icons', '')]:
try:
query = {'types': 'static', 'nsfw': 'false', 'humor': 'false', 'mimes': 'image/png,image/jpeg'}
if dimensions:
query['dimensions'] = dimensions
records = _get('/' + kind + '/game/' + str(gid) + '?' + urllib.parse.urlencode(query), key)
records = [r for r in records if r.get('url', '').startswith('https://') and not r.get('nsfw')]
if dimensions:
w, h = map(int, dimensions.split('x'))
records = [r for r in records if (r.get('width'), r.get('height')) == (w, h)]
records.sort(key=lambda r: (int(r.get('score') or 0), int(r.get('upvotes') or 0)), reverse=True)
if records:
result[slot] = records[0]['url']
except (OSError, ValueError, KeyError, TypeError):
warnings.append('SteamGridDB ' + slot + ' unavailable; using source or generated art')
return result, warnings
except (OSError, ValueError, KeyError, TypeError):
return {}, ['SteamGridDB unavailable; using source or generated art']