From 4dfc23d35fa83fe27b9a7dd5efe17bd6c9fa5faa Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Mon, 13 Jul 2026 05:10:04 +0800 Subject: [PATCH] feat(plugin): plugin_get proxy + typed library-panel fetch/cache --- plugins/decky-vault/main.py | 36 +++++++++++ plugins/decky-vault/src/lib/api.ts | 5 +- plugins/decky-vault/src/lib/plugin-api.ts | 74 +++++++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 plugins/decky-vault/src/lib/plugin-api.ts diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index c51ccd4..da81ac0 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -842,6 +842,42 @@ exec mangohud "$@" except Exception as e: return {"valid": False, "error": str(e)} + async def plugin_get(self, path: str, base_url: str = "https://deckyvault.xyz") -> dict: + """RPC: Public read proxy for the DeckyVault API (used by the library panel). + Performs a GET to {base_url}/api{path} and returns parsed JSON or {error, status}. + Keeps network in the Python backend to avoid CEF CORS issues.""" + import urllib.request + import urllib.error + try: + if not path.startswith("/"): + path = "/" + path + url = f"{base_url}/api{path}" + req = urllib.request.Request( + url, + headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0", + "Accept": "application/json", + }, + method="GET", + ) + context = _get_ssl_context() + with urllib.request.urlopen(req, timeout=10, context=context) as response: + body = response.read().decode("utf-8") + try: + return json.loads(body) + except json.JSONDecodeError: + return {"error": "Invalid JSON", "status": response.status} + except urllib.error.HTTPError as e: + try: + err = json.loads(e.read().decode("utf-8")) + return {**err, "status": e.code} + except Exception: + return {"error": f"Server returned status {e.code}", "status": e.code} + except urllib.error.URLError as e: + return {"error": f"Network error: {str(e.reason)}", "status": 0} + except Exception as e: + return {"error": str(e), "status": 0} + async def export_config(self, settings: dict) -> dict: """RPC: Export current settings to Downloads/deckyvault-config.json. Returns {success: bool, path?: str, error?: str}.""" diff --git a/plugins/decky-vault/src/lib/api.ts b/plugins/decky-vault/src/lib/api.ts index d216132..56b3ce4 100644 --- a/plugins/decky-vault/src/lib/api.ts +++ b/plugins/decky-vault/src/lib/api.ts @@ -166,4 +166,7 @@ export const checkPairStatus = callable<[token: string, baseUrl?: string], { apiKey?: string keyName?: string error?: string -}>("check_pair_status") \ No newline at end of file +}>("check_pair_status") + +// ── Plugin API Proxy ──────────────────────────────────────────── +export const pluginGet = callable<[path: string, baseUrl?: string], Record & { status?: number; error?: string }>("plugin_get") \ No newline at end of file diff --git a/plugins/decky-vault/src/lib/plugin-api.ts b/plugins/decky-vault/src/lib/plugin-api.ts new file mode 100644 index 0000000..07bcc2b --- /dev/null +++ b/plugins/decky-vault/src/lib/plugin-api.ts @@ -0,0 +1,74 @@ +import { pluginGet } from "./api" + +export interface PluginEntry { + id: string + hardwareSlug: string + fpsAvg: number + fpsLow: number | null + fpsOnePercentLow: number | null + fpsHigh: number | null + upscalerType: string + frameGenMethod: string + protonVersion: string | null + osVersion: string | null + tdpWatts: number | null + settingsJson: unknown + upvotes: number + isPinned: boolean + createdAt: string + userName: string | null + userImage: string | null +} + +export interface PluginGameResponse { + game: { id: string; steamAppId: number | null; title: string; slug: string | null } | null + estFps: { avg: number; low: number | null; onePct: number | null; high: number | null; count: number } | null + topEntries: PluginEntry[] + recentEntries: PluginEntry[] + error?: string +} + +export interface PluginDeviceRow { + slug: string + name: string + count: number +} + +// Tiny per-appId cache (1h TTL) +interface CacheEntry { value: PluginGameResponse; expires: number } +const cache = new Map() +const TTL_MS = 60 * 60 * 1000 +const settingsRef: { baseUrl: string } = { baseUrl: "https://deckyvault.xyz" } + +export function setPluginApiBaseUrl(url: string) { + settingsRef.baseUrl = url || "https://deckyvault.xyz" +} + +export async function fetchPluginGame( + steamAppId: number, + hardware: string | null, + limit: number, +): Promise { + const key = `${steamAppId}|${hardware ?? "all"}|${limit}` + const hit = cache.get(key) + if (hit && hit.expires > Date.now()) return hit.value + + const path = `/plugin/game/${steamAppId}?limit=${limit}${hardware ? `&hardware=${encodeURIComponent(hardware)}` : ""}` + const raw = await pluginGet(path, settingsRef.baseUrl) + const value = raw as unknown as PluginGameResponse + cache.set(key, { value, expires: Date.now() + TTL_MS }) + return value +} + +export async function fetchPluginDevices(steamAppId: number): Promise { + const raw = await pluginGet(`/plugin/game/${steamAppId}/devices`, settingsRef.baseUrl) + if (raw.error) return [] + return (raw.devices as PluginDeviceRow[]) ?? [] +} + +export function clearPluginCache(steamAppId?: number) { + if (steamAppId == null) { cache.clear(); return } + for (const k of cache.keys()) { + if (k.startsWith(`${steamAppId}|`)) cache.delete(k) + } +} \ No newline at end of file