From 9ef87fd7b9905dc9383b57062a926a3418bbf015 Mon Sep 17 00:00:00 2001 From: sbkelley Date: Sun, 7 Dec 2025 12:22:19 -0500 Subject: [PATCH] restructuring and fixing default websocket settings --- src/obs_ctl/app.py | 11 +- src/obs_ctl/const.py | 6 + .../interface/molecule/settings_block/pane.py | 20 ++- .../molecule/settings_block/settings_block.py | 6 +- src/obs_ctl/interface/obs/obs.py | 127 ++++++++++++------ 5 files changed, 115 insertions(+), 55 deletions(-) diff --git a/src/obs_ctl/app.py b/src/obs_ctl/app.py index 040a664..79819d9 100644 --- a/src/obs_ctl/app.py +++ b/src/obs_ctl/app.py @@ -9,7 +9,7 @@ from textual.containers import HorizontalGroup, VerticalGroup, ScrollableContain from textual.widgets import Footer, Header # need to import log to initalize logging -from . import data, log # type: ignore +from . import data, const, log # type: ignore from .interface import RecordControls, StatsDisplay, SettingsBlock, obs, OBSWebSocketPane, OBSSettingsPane logger = getLogger(__name__) @@ -27,7 +27,7 @@ class OBSCtlApp(App): # type: ignore def compose(self): yield Header() yield Footer() - yield obs.API(1, id="api") + yield obs.API(1, ws_host=const.HOST, ws_port=const.PORT, ws_password=const.PASSWORD, ws_timeout=const.TIMEOUT, id="api") logger.debug("Composing OBSCtlApp") with ScrollableContainer(classes="scrollbox"): with VerticalGroup(classes="content"): @@ -39,7 +39,12 @@ class OBSCtlApp(App): # type: ignore def on_mount(self, message: events.Mount): - self.on_obs_ws_connected(None) + obsws_pane = self.get_websocket_pane() + api = self.get_api() + obsws_pane.host = api.ws_host + obsws_pane.port = api.ws_port + obsws_pane.password = api.ws_password + obsws_pane.timeout = api.ws_timeout @on(obs.API.Report, "#api") diff --git a/src/obs_ctl/const.py b/src/obs_ctl/const.py index fd992a6..67b8606 100644 --- a/src/obs_ctl/const.py +++ b/src/obs_ctl/const.py @@ -10,3 +10,9 @@ CONFIG = ConfigParser() POSSIBLE_CONFIG: list[str] = [USERDIRS.user_config_dir] CURRENT_CONFIG = CONFIG.read(POSSIBLE_CONFIG) + + +HOST = "localhost" +PORT = 4455 +PASSWORD = "" +TIMEOUT = 1.0 \ No newline at end of file diff --git a/src/obs_ctl/interface/molecule/settings_block/pane.py b/src/obs_ctl/interface/molecule/settings_block/pane.py index 8855c6b..1f4cc96 100644 --- a/src/obs_ctl/interface/molecule/settings_block/pane.py +++ b/src/obs_ctl/interface/molecule/settings_block/pane.py @@ -1,19 +1,20 @@ from enum import Enum -from functools import partial from itertools import zip_longest from logging import getLogger -from typing import Any, Mapping, Sequence, overload +from typing import Any, Mapping, Sequence + from textual import on from textual.app import ComposeResult from textual.containers import ScrollableContainer, Grid from textual.reactive import reactive +from textual.widget import Widget from textual.widgets import TabPane, Input, Label, Select + from ...message import MSGBase -from textual.widget import Widget logger = getLogger(__name__) + class SettingsMSGBase(MSGBase): - # value: Any def __init__(self, control: Widget, value: Any, *, _log_me: bool=True): super().__init__(control, _log_me=_log_me) self.value = value @@ -136,18 +137,16 @@ def make_pane( input.value = str(value) return watch_func - def make_on_control_message( listener_message: SettingsWidgetMsg, listener_id: str, reactive: reactive[Any], post_message: type[SettingsMSGBase]): def on_func(self: PaneBase, event: SettingsWidgetMsg): - self.set_reactive(reactive, event.value) + reactive = event.value self.post_message(post_message(self, event.value)) return on(listener_message, listener_id)(on_func) - def make_compose(): def compose(self: PaneBase) -> ComposeResult: with ScrollableContainer(classes="--container"): @@ -164,10 +163,9 @@ def make_pane( id=ctrl_id) return compose - new_dict: dict[str, Any] = dict() for setting_name, setting_type, setting_default, _ in loop_list: - control_setting_id: str = f"{setting_type.widget.__name__.lower()}_{setting_name}" + setting_control_id: str = f"{setting_type.widget.__name__.lower()}_{setting_name}" SettingChanged_name: str = f"{_snake_to_camel(setting_name)}Changed" watch_setting_name: str = f"watch_{setting_name}" on_setting_changed_name: str = f"on_{setting_name}_changed" @@ -175,7 +173,7 @@ def make_pane( # Set up a record_path reactive new_dict[setting_name] = reactive(setting_type.type(setting_default)) # Set up a watch_record_path function - new_dict[watch_setting_name] = make_watch_reactive(f"#{control_setting_id}") + new_dict[watch_setting_name] = make_watch_reactive(f"#{setting_control_id}") # Set up a RecordPathChanged message for when user interacts with control # (see SettingTypes enum) new_dict[SettingChanged_name] = type(SettingChanged_name, (SettingsMSGBase, ), {}) @@ -183,7 +181,7 @@ def make_pane( # and post RecordPathChanged new_dict[on_setting_changed_name] = make_on_control_message( listener_message=setting_type.msg, - listener_id=f"#{control_setting_id}", + listener_id=f"#{setting_control_id}", reactive=new_dict[setting_name], post_message=new_dict[SettingChanged_name], ) diff --git a/src/obs_ctl/interface/molecule/settings_block/settings_block.py b/src/obs_ctl/interface/molecule/settings_block/settings_block.py index 2149bfe..93c2011 100644 --- a/src/obs_ctl/interface/molecule/settings_block/settings_block.py +++ b/src/obs_ctl/interface/molecule/settings_block/settings_block.py @@ -13,10 +13,10 @@ OBSSettingsPane = pane.make_pane( OBSWebSocketPane = pane.make_pane( "OBSWebSocketPane", - pane.make_pane_widget("host", pane.SettingsWidget.STRING, "localhost", compact=True), - pane.make_pane_widget("port", pane.SettingsWidget.INT, 4455, compact=True), + pane.make_pane_widget("host", pane.SettingsWidget.STRING, "", compact=True), + pane.make_pane_widget("port", pane.SettingsWidget.INT, 0, compact=True), pane.make_pane_widget("password", pane.SettingsWidget.STRING, "", compact=True), - pane.make_pane_widget("timeout", pane.SettingsWidget.FLOAT, 1.0, compact=True), + pane.make_pane_widget("timeout", pane.SettingsWidget.FLOAT, 0.0, compact=True), ) OBSCTLPane = pane.make_pane( diff --git a/src/obs_ctl/interface/obs/obs.py b/src/obs_ctl/interface/obs/obs.py index 8217b03..b213f15 100644 --- a/src/obs_ctl/interface/obs/obs.py +++ b/src/obs_ctl/interface/obs/obs.py @@ -47,6 +47,10 @@ class OBSStats: available_disk_space: float = 0.0 """Available space in MB""" record_directory: str = "" + websocket_host: str = "" + websocket_port: int = 0 + websocket_password: str = "" + websocket_timeout: float = 0.0 @property def estimated_hours_remaining(self): @@ -169,9 +173,9 @@ class API(Static): class APIOutputMSGBase(MSGBase): state = None def __init__(self, control: "API", data: Any) -> None: - super().__init__(control) + super().__init__(control, _log_me=True) _state = API.OutputStates(data.output_state) - logger.debug(f"{self} is an obs.API Message; state: {_state.name}") + logger.debug(f"{self} is an obs.API Output Message; state: {_state.name}") self.state = _state class StreamStateUpdated(APIOutputMSGBase): @@ -187,8 +191,7 @@ class API(Static): class Report(MSGBase): """Posted ever self.interval second. data is an OBSStats object.""" def __init__(self: Self, control: "API", data: OBSStats): - # this message is sent too often to log it. - super().__init__(control, _log_me = False) + super().__init__(control) self.data = data @@ -198,7 +201,7 @@ class API(Static): ws_host: str = "localhost", ws_port: int = 4455, ws_password: str = "", - ws_timeout: float = 3.0, + ws_timeout: float = 1.0, *args: Any, **kwargs: Any, ): @@ -219,18 +222,69 @@ class API(Static): def on_mount(self): self._reset_timer(self.interval) - - - def on_stream_state_changed(self: Self, data: Any) -> None: - self.post_message(API.StreamStateUpdated(self, data)) + + + def watch_interval(self, value: float) -> None: + self._reset_timer(value) - def on_record_state_changed(self: Self, data: Any) -> None: - self.post_message(API.RecordStateUpdated(self, data)) + def watch_ws_host(self, value: str) -> None: + if self.is_connected: + self.disconnect() + + + def watch_ws_port(self, value: int) -> None: + if self.is_connected: + self.disconnect() - - def watch_interval(self, value: float) -> None: - self._reset_timer(value) + + def watch_ws_password(self, value: str) -> None: + if self.is_connected: + self.disconnect() + + + def watch_ws_timeout(self, value: float) -> None: + if self.is_connected: + self.disconnect() + + + # LOGGER + def connect(self, host: str | None=None, port: int | None=None, password: str | None=None, timeout: float | None=None) -> bool: + if host is None: + host = self.ws_host + else: + self.ws_host = host + if port is None: + port = self.ws_port + else: + self.ws_port = port + if password is None: + password = self.ws_password + else: + self.ws_password = password + if timeout is None: + timeout = self.ws_timeout + else: + self.ws_timeout = timeout + + if self._make_clients(host, port, password, timeout): + self.is_connected = True + self.event_client.callback.register(self.on_stream_state_changed) + self.event_client.callback.register(self.on_record_state_changed) + return True + else: + self.is_connected = False + return False + + + def disconnect(self) -> bool: + logger.info("Disconnecting from OBS WebSocket") + self.request_client.disconnect() + self.event_client.disconnect() + self.event_client.callback.deregister(self.on_stream_state_changed) + self.event_client.callback.deregister(self.on_record_state_changed) + self.is_connected = False + return True def stop_stream(self) -> None: @@ -305,21 +359,26 @@ class API(Static): available_disk_space=obs_stats.available_disk_space, record_directory=record_directory.record_directory, + + websocket_host=self.ws_host, + websocket_port=self.ws_port, + websocket_password=self.ws_password, + websocket_timeout=self.ws_timeout, ) return output + + # Callbacks for obsws API rather than Textual + def on_stream_state_changed(self: Self, data: Any) -> None: + self.post_message(API.StreamStateUpdated(self, data)) - # LOGGER - def connect(self, host: str | None=None, port: int | None=None, password: str | None=None, timeout: float | None=None) -> None: - host = self.ws_host if host is None else host - port = self.ws_port if port is None else port - password = self.ws_password if password is None else password - timeout = self.ws_timeout if timeout is None else timeout - self.ws_host = host - self.ws_port = port - self.ws_password = password - self.ws_timeout = timeout + + def on_record_state_changed(self: Self, data: Any) -> None: + self.post_message(API.RecordStateUpdated(self, data)) + + + def _make_clients(self, host: str, port: int, password: str, timeout: float) -> bool: try: logger.info(f"Connecting to OBS WebSocket at {host}:{port}") self.request_client = obs.ReqClient( @@ -335,20 +394,12 @@ class API(Static): timeout=timeout ) except (ConnectionRefusedError, TimeoutError) as err: - logger.debug(err) + logger.error(err) + return False else: - self.is_connected = True - self.event_client.callback.register(self.on_stream_state_changed) # type: ignore - self.event_client.callback.register(self.on_record_state_changed) # type: ignore - - - def disconnect(self) -> None: - logger.info("Disconnecting from OBS WebSocket") - self.request_client.disconnect() - self.event_client.disconnect() - self.is_connected = False - + return True + # Regarding the pattern in _try_request -- # number = 5 # while number > 0 @@ -382,8 +433,8 @@ class API(Static): time.sleep(TRY_AGAIN) continue - elif err.code in API.KnownReqErrs: # type: ignore - logger.debug(f"Error handled: {API.KnownReqErrs(err.code)}") # type: ignore + elif err.code in API.KnownReqErrs: + logger.debug(f"Error handled: {API.KnownReqErrs(err.code)}") handled = True else: -- 2.47.3