aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 19:08:49 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 19:08:49 +0100
commit4632e6183d38bfd239f066d7e9698f3d118a3f77 (patch)
tree8d458ea7395c6e52186f0bfb38344dbc84f68cbe
parentc906d90dcf0abdfb347bd67d6f33a2c45ae32e66 (diff)
downloaddeck-it-4632e6183d38bfd239f066d7e9698f3d118a3f77.tar.gz
deck-it-4632e6183d38bfd239f066d7e9698f3d118a3f77.zip
fix: use OBS's stored password and explain auth failures
-rw-r--r--config.toml3
-rw-r--r--deckit/modules/obs.py15
-rw-r--r--deckit/obsclient.py17
-rw-r--r--tests/test_obs.py13
-rw-r--r--tests/test_obsclient.py14
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