steam-backlog-enforcer/steam_backlog_enforcer/_cdp.py
Krzysztof kuhy Rudnicki 356530a01c
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Grade against 3.14 too, not just run on it
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading against 3.10 -- three tools judging an interpreter nothing
runs any more, and ruff withholding the pyupgrade fixes that only become safe
at the newer target. `pin_ci_python.py` rewrites exactly these three knobs.

What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC`
is now correct (UP017), `socket.timeout` where `TimeoutError` is (UP041),
deprecated `typing` imports (UP035), PEP 695 `type` aliases (UP040), and ruff's
stable formatter applying PEP 758 -- `except (A, B):` becomes `except A, B:`.
That last one makes the source 3.14-only at the syntax level, which is what
`requires-python = ">=3.14"` already promised; kuhy chose it deliberately over
holding ruff a release back (2026-08-29).

ruff, ruff-format, mypy --strict and pylint all clean at the new target. The
two modified web/ TypeScript files are another session's in-flight work and
are deliberately left unstaged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:44:25 +02:00

96 lines
3.3 KiB
Python

"""Chrome DevTools Protocol transport for Steam's embedded browser.
Modern Steam runs its UI in CEF, so collections can only be changed by
evaluating JS inside it. Split out of
:mod:`steam_backlog_enforcer.library_hider` to keep both files under the
250-line cap: this module is pure transport and knows nothing about
collections.
"""
from __future__ import annotations
import asyncio
import json
import logging
import requests
import websockets
logger = logging.getLogger(__name__)
# NOTE: was 8080, which collided with a different local service (Open WebUI)
# already bound to 0.0.0.0:8080. requests to 127.0.0.1:8080 resolved to that
# service instead of steamwebhelper's CDP endpoint (which only bound
# [::1]:8080), so CDP detection silently never worked. 9222 is the
# conventional Chrome DevTools debug port and was confirmed free.
_CDP_PORT = 9222
_CDP_TIMEOUT = 120
# ──────────────────────────────────────────────────────────────
# CDP (Chrome DevTools Protocol) helpers
# ──────────────────────────────────────────────────────────────
def _get_shared_js_ws_url() -> str | None:
"""Query the CDP HTTP endpoint and return the SharedJSContext WS URL."""
try:
resp = requests.get(f"http://127.0.0.1:{_CDP_PORT}/json", timeout=5)
targets = resp.json()
except OSError, ValueError:
return None
for target in targets:
if target.get("title") == "SharedJSContext":
ws_url: str = target["webSocketDebuggerUrl"]
return ws_url
return None
async def _evaluate_js_async(ws_url: str, expression: str) -> dict:
"""Connect to a CDP WebSocket target and evaluate *expression*."""
async with websockets.connect(ws_url) as ws:
msg = json.dumps(
{
"id": 1,
"method": "Runtime.evaluate",
"params": {
"expression": expression,
"returnByValue": True,
"awaitPromise": True,
},
}
)
await ws.send(msg)
resp = await asyncio.wait_for(ws.recv(), timeout=_CDP_TIMEOUT)
return json.loads(resp)
def _evaluate_js(expression: str) -> dict:
"""Synchronous wrapper around :func:`_evaluate_js_async`."""
ws_url = _get_shared_js_ws_url()
if ws_url is None:
msg = "SharedJSContext not found on CDP port"
raise RuntimeError(msg)
return asyncio.run(_evaluate_js_async(ws_url, expression))
def _cdp_result_value(result: dict) -> str:
"""Extract the return value from a CDP Runtime.evaluate response."""
outer = result.get("result", {})
inner = outer.get("result", {})
if "exceptionDetails" in outer:
exc_details = outer["exceptionDetails"]
exc = exc_details.get("exception", {})
desc = (
inner.get("description")
or exc.get("description")
or exc_details.get("text")
or repr(exc_details)
)
logger.debug("CDP exception details: %s", exc_details)
msg = f"JS evaluation error: {desc}"
raise RuntimeError(msg)
value: str = inner.get("value", "")
return value