Files
saphid--frame-control/ui/frame_store.py
T
saphidandClaude Opus 5.5 99653151c4 Address review findings in the app, server and tests
- app: startup shell, python and ssh probes run asynchronously so a slow
  shell profile can't freeze the window; PATH comes from the user's real
  login shell and a failed lookup isn't cached; a server that never
  answers is killed; the setup offer runs once per launch, only after the
  UI loads, and decides from HostName alone; connect.sh is started through
  `env ... zsh` so it works whatever the login shell is.
- server: volume validates the level before muting or changing anything.
- Steam: null-safe install-manager fields, http.client errors caught in
  store ratings, price fallback when a sale has no final price.
- tests: server output kept for diagnosis, any startup error retried, and
  captures asserted non-cacheable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 22:44:54 +10:00

60 lines
2.4 KiB
Python

"""Mac side: search the Steam store and look up each result's Steam Frame rating.
Uses the store's public endpoints (no key, no login):
api/storesearch name search, price in the IP's currency
saleaction/ajaxgetdeckappcompatibilityreport per-app Deck/SteamOS/Machine/Frame ratings;
`frame_resolved_category` is the Frame's
Buying happens on the store page, signed in as the user; nothing here buys.
"""
import http.client
import json
import threading
import time
import urllib.parse
import urllib.request
from concurrent.futures import ThreadPoolExecutor
STORE = "https://store.steampowered.com"
UA = {"User-Agent": "FrameControl/1 (+local)"}
COMPAT_TTL = 24 * 3600
_compat = {} # appid -> (time, category)
_lock = threading.Lock()
def _get(path, params, timeout=10):
url = f"{STORE}/{path}?{urllib.parse.urlencode(params)}"
with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=timeout) as r:
return json.load(r)
def frame_rating(appid):
"""0 unknown, 1 unsupported, 2 playable, 3 verified (same scale as the client's)."""
with _lock:
hit = _compat.get(appid)
if hit and time.time() - hit[0] < COMPAT_TTL:
return hit[1]
try:
r = _get("saleaction/ajaxgetdeckappcompatibilityreport", {"nAppID": appid, "l": "english"})
cat = int((r.get("results") or {}).get("frame_resolved_category") or 0)
cat = cat if 0 <= cat <= 3 else 0
except (OSError, ValueError, TypeError, AttributeError, http.client.HTTPException):
return 0 # not cached, so the next search retries
with _lock:
_compat[appid] = (time.time(), cat)
return cat
def search(term, cc):
"""cc: two-letter store country; storesearch returns nothing without one."""
term = term.strip()[:100]
if not term:
return []
items = _get("api/storesearch", {"term": term, "l": "english", "cc": cc}).get("items") or []
apps = [i for i in items if i.get("type") == "app" and str(i.get("id", "")).isdigit()]
with ThreadPoolExecutor(8) as pool:
ratings = list(pool.map(lambda i: frame_rating(int(i["id"])), apps))
return [{"id": int(i["id"]), "name": i.get("name", ""), "price": i.get("price"),
"image": i.get("tiny_image"), "metascore": i.get("metascore") or None, "frame": f}
for i, f in zip(apps, ratings)]