mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 07:00:37 +02:00
Package the Frame Control web UI as an installable Electron Mac app and bring in the tooling built alongside it. - app/: Electron wrapper that starts ui/server.py on a free loopback port, hardened window (sandbox, no navigation, runAsNode fuse off), login-shell PATH so Homebrew tools work from Finder, first-run offer to run connect.sh, ad-hoc signed DMG/zip via electron-builder. - ui/: headset view (OpenVR screenshots), device status, library, Steam "Get games" (owned games, install, store search), Android apps as persistent Lepton instances with a rated F-Droid catalogue and a private compatibility database, Android display controls over ADB, file and clipboard transfer, Flatpaks, remote and power actions. - apk-catalog/, compat-db/, frame/: catalogue build pipeline, Lakebed capsule for compatibility reports, Frame-side launchers. - tests/ and CI: server guard and validation tests plus Steam helper tests, run on Python 3.9 with script and app syntax checks. - Docs: README leads with the Mac app; new Android, panels, Steam games and field-notes docs; security notes on LAN-exposed ADB ports. Screenshot values for the headset's IP and Wi-Fi name are placeholders. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
59 lines
2.3 KiB
Python
59 lines
2.3 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 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):
|
|
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)]
|