mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 03:00:18 +02:00
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>
104 lines
4.2 KiB
Python
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']
|