feat(plugin): plugin_get proxy + typed library-panel fetch/cache
This commit is contained in:
@@ -842,6 +842,42 @@ exec mangohud "$@"
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"valid": False, "error": str(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:
|
async def export_config(self, settings: dict) -> dict:
|
||||||
"""RPC: Export current settings to Downloads/deckyvault-config.json.
|
"""RPC: Export current settings to Downloads/deckyvault-config.json.
|
||||||
Returns {success: bool, path?: str, error?: str}."""
|
Returns {success: bool, path?: str, error?: str}."""
|
||||||
|
|||||||
@@ -167,3 +167,6 @@ export const checkPairStatus = callable<[token: string, baseUrl?: string], {
|
|||||||
keyName?: string
|
keyName?: string
|
||||||
error?: string
|
error?: string
|
||||||
}>("check_pair_status")
|
}>("check_pair_status")
|
||||||
|
|
||||||
|
// ── Plugin API Proxy ────────────────────────────────────────────
|
||||||
|
export const pluginGet = callable<[path: string, baseUrl?: string], Record<string, unknown> & { status?: number; error?: string }>("plugin_get")
|
||||||
@@ -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<string, CacheEntry>()
|
||||||
|
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<PluginGameResponse> {
|
||||||
|
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<PluginDeviceRow[]> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user