diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-09 19:08:49 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-09 19:08:49 +0100 |
| commit | 4632e6183d38bfd239f066d7e9698f3d118a3f77 (patch) | |
| tree | 8d458ea7395c6e52186f0bfb38344dbc84f68cbe | |
| parent | c906d90dcf0abdfb347bd67d6f33a2c45ae32e66 (diff) | |
| download | deck-it-4632e6183d38bfd239f066d7e9698f3d118a3f77.tar.gz deck-it-4632e6183d38bfd239f066d7e9698f3d118a3f77.zip | |
fix: use OBS's stored password and explain auth failures
| -rw-r--r-- | config.toml | 3 | ||||
| -rw-r--r-- | deckit/modules/obs.py | 15 | ||||
| -rw-r--r-- | deckit/obsclient.py | 17 | ||||
| -rw-r--r-- | tests/test_obs.py | 13 | ||||
| -rw-r--r-- | tests/test_obsclient.py | 14 |
5 files changed, 58 insertions, 4 deletions
diff --git a/config.toml b/config.toml index c22b86b..517fbe6 100644 --- a/config.toml +++ b/config.toml @@ -43,7 +43,8 @@ assistant = false [obs] host = "127.0.0.1" port = 4455 -# obs-websocket password (Tools > obs-websocket Settings). Empty if auth is off. +# obs-websocket password (Tools > obs-websocket Settings). Leave empty to use +# the password OBS stores in its own config, which is the default here. password = "" [llm] diff --git a/deckit/modules/obs.py b/deckit/modules/obs.py index 96217c4..04c49ef 100644 --- a/deckit/modules/obs.py +++ b/deckit/modules/obs.py @@ -1,10 +1,22 @@ """OBS stream-deck: recording, scenes, audio channels, transitions and sources.""" +import json import threading +from pathlib import Path from ..module import Module, ModuleError from ..obsclient import ObsClient +OBS_CONFIG = Path.home() / ".config" / "obs-studio" / "plugin_config" / "obs-websocket" / "config.json" + + +def _obs_password(path=OBS_CONFIG) -> str: + """OBS stores its websocket password in plain text here.""" + try: + return json.loads(Path(path).read_text()).get("server_password") or "" + except (OSError, ValueError): + return "" + AUDIO_KINDS = { "pulse_input_capture", "pulse_output_capture", "alsa_input_capture", "alsa_output_capture", "wasapi_input_capture", "wasapi_output_capture", "coreaudio_input_capture", "coreaudio_output_capture", @@ -15,7 +27,8 @@ AUDIO_KINDS = { def make(cfg: dict) -> Module: o = cfg.get("obs", {}) client = cfg.get("client") or ObsClient( - host=o.get("host", "127.0.0.1"), port=o.get("port", 4455), password=o.get("password", "")) + host=o.get("host", "127.0.0.1"), port=o.get("port", 4455), + password=o.get("password") or _obs_password()) lock = threading.Lock() stop = threading.Event() last_error = {"msg": None} diff --git a/deckit/obsclient.py b/deckit/obsclient.py index 181225a..87eefff 100644 --- a/deckit/obsclient.py +++ b/deckit/obsclient.py @@ -30,18 +30,31 @@ class ObsClient: self._stop = threading.Event() self._thread = None + def _recv_json(self) -> dict: + raw = self.ws.recv() + if not raw: + raise ModuleError("OBS closed the connection") + try: + return json.loads(raw) + except ValueError: + raise ModuleError("OBS sent a non-JSON reply") + def connect(self): import websocket # only needed when actually talking to OBS self.ws = websocket.create_connection(f"ws://{self.host}:{self.port}", timeout=5, suppress_origin=True) - hello = json.loads(self.ws.recv()) + hello = self._recv_json() auth = {} if hello.get("d", {}).get("authentication"): a = hello["d"]["authentication"] auth["authentication"] = auth_string(self.password, a["salt"], a["challenge"]) self.ws.send(json.dumps({"op": 1, "d": {"rpcVersion": 1, **auth, "eventSubscriptions": 0xFFFFFFFF}})) - if json.loads(self.ws.recv()).get("op") != 2: + try: + identified = self._recv_json() + except ModuleError: + raise ModuleError("OBS rejected the password" if auth else "OBS did not identify") + if identified.get("op") != 2: raise ModuleError("OBS did not identify") self.connected = True self._stop.clear() diff --git a/tests/test_obs.py b/tests/test_obs.py index aeb0791..cd317ed 100644 --- a/tests/test_obs.py +++ b/tests/test_obs.py @@ -3,9 +3,12 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +import json +import tempfile import threading import time import unittest +from pathlib import Path import deckit.modules.obs as obs @@ -49,6 +52,16 @@ class Actions(unittest.TestCase): self.m.actions["record"](bad) +class Password(unittest.TestCase): + def test_reads_the_password_obs_stores(self): + p = Path(tempfile.mkdtemp()) / "config.json" + p.write_text(json.dumps({"server_password": "sekret"})) + self.assertEqual(obs._obs_password(p), "sekret") + + def test_missing_config_is_empty(self): + self.assertEqual(obs._obs_password(Path("/nonexistent/x.json")), "") + + class Serialized(unittest.TestCase): def test_actions_do_not_overlap(self): active = 0 diff --git a/tests/test_obsclient.py b/tests/test_obsclient.py index 54eadd1..fe97d35 100644 --- a/tests/test_obsclient.py +++ b/tests/test_obsclient.py @@ -25,6 +25,20 @@ class Disconnected(unittest.TestCase): oc.ObsClient(host="127.0.0.1", port=4455, password="").request("GetVersion") +class RecvJson(unittest.TestCase): + def test_empty_reply_is_a_clear_error(self): + c = oc.ObsClient() + c.ws = type("W", (), {"recv": lambda self: ""})() + with self.assertRaisesRegex(oc.ModuleError, "closed"): + c._recv_json() + + def test_non_json_reply_is_a_clear_error(self): + c = oc.ObsClient() + c.ws = type("W", (), {"recv": lambda self: "HTTP/1.1 400"})() + with self.assertRaisesRegex(oc.ModuleError, "non-JSON"): + c._recv_json() + + class RecvLoop(unittest.TestCase): def test_a_recv_timeout_does_not_end_the_loop(self): import websocket |
