diff --git a/vban_cmd/factory.py b/vban_cmd/factory.py index 086c01c..afc3e4e 100644 --- a/vban_cmd/factory.py +++ b/vban_cmd/factory.py @@ -84,18 +84,20 @@ class FactoryBase(VbanCmd): def __init__(self, kind_id: str, **kwargs): defaultkwargs = { - 'ip': 'localhost', + 'host': 'localhost', 'port': 6980, 'streamname': 'Command1', 'bps': 256000, 'channel': 0, - 'ratelimit': 0.01, - 'timeout': 5, + 'script_ratelimit': 0.05, # 20 commands per second, to avoid overloading Voicemeeter + 'timeout': 5, # timeout on socket operations, in seconds 'disable_rt_listeners': False, 'sync': False, 'pdirty': False, 'ldirty': False, } + if 'ip' in kwargs: + defaultkwargs['host'] = kwargs.pop('ip') # for backwards compatibility if 'subs' in kwargs: defaultkwargs |= kwargs.pop('subs') # for backwards compatibility kwargs = defaultkwargs | kwargs diff --git a/vban_cmd/util.py b/vban_cmd/util.py index 19ed1ab..c3522f2 100644 --- a/vban_cmd/util.py +++ b/vban_cmd/util.py @@ -1,6 +1,23 @@ +import time from typing import Iterator +def ratelimit(func): + """ratelimit decorator for {VbanCmd}.sendtext, to prevent flooding the network with script requests.""" + + def wrapper(*args, **kwargs): + self, *rem = args + if self.script_ratelimit > 0: + now = time.time() + elapsed = now - self._last_script_request_time + if elapsed < self.script_ratelimit: + time.sleep(self.script_ratelimit - elapsed) + self._last_script_request_time = time.time() + return func(*args, **kwargs) + + return wrapper + + def cache_bool(func, param): """Check cache for a bool prop""" diff --git a/vban_cmd/vbancmd.py b/vban_cmd/vbancmd.py index 81e5075..470a791 100644 --- a/vban_cmd/vbancmd.py +++ b/vban_cmd/vbancmd.py @@ -17,7 +17,7 @@ from .packet.headers import ( ) from .packet.ping0 import VbanPing0Payload, VbanServerType from .subject import Subject -from .util import bump_framecounter, deep_merge +from .util import bump_framecounter, deep_merge, ratelimit from .worker import Producer, Subscriber, Updater logger = logging.getLogger(__name__) @@ -38,7 +38,7 @@ class VbanCmd(abc.ABC): def __init__(self, **kwargs): self.logger = logger.getChild(self.__class__.__name__) self.event = Event({k: kwargs.pop(k) for k in ('pdirty', 'ldirty')}) - if not kwargs['ip']: + if not kwargs['host']: kwargs |= self._conn_from_toml() for attr, val in kwargs.items(): setattr(self, attr, val) @@ -52,9 +52,9 @@ class VbanCmd(abc.ABC): self.cache = {} self._pdirty = False self._ldirty = False - self._script = str() self.stop_event = None self.producer = None + self._last_script_request_time = 0 @abc.abstractmethod def __str__(self): @@ -113,7 +113,7 @@ class VbanCmd(abc.ABC): self.producer.start() self.logger.info( - "Successfully logged into VBANCMD {kind} with ip='{ip}', port={port}, streamname='{streamname}'".format( + "Successfully logged into VBANCMD {kind} with host='{host}', port={port}, streamname='{streamname}'".format( **self.__dict__ ) ) @@ -149,8 +149,8 @@ class VbanCmd(abc.ABC): self.sock.settimeout(0.5) try: - self.sock.sendto(ping_packet, (socket.gethostbyname(self.ip), self.port)) - self.logger.debug(f'PING sent to {self.ip}:{self.port}') + self.sock.sendto(ping_packet, (socket.gethostbyname(self.host), self.port)) + self.logger.debug(f'PING sent to {self.host}:{self.port}') start_time = time.time() response_count = 0 @@ -193,11 +193,13 @@ class VbanCmd(abc.ABC): f'PING timeout after {timeout}s, received {response_count} non-PONG packets' ) raise VBANCMDConnectionError( - f'PING timeout: No response from {self.ip}:{self.port} after {timeout}s' + f'PING timeout: No response from {self.host}:{self.port} after {timeout}s' ) except socket.gaierror as e: - raise VBANCMDConnectionError(f'Unable to resolve hostname {self.ip}') from e + raise VBANCMDConnectionError( + f'Unable to resolve hostname {self.host}' + ) from e except Exception as e: raise VBANCMDConnectionError(f'PING failed: {e}') from e finally: @@ -230,7 +232,7 @@ class VbanCmd(abc.ABC): framecounter=self._get_next_framecounter(), payload=payload, ), - (socket.gethostbyname(self.ip), self.port), + (socket.gethostbyname(self.host), self.port), ) def _set_rt(self, cmd: str, val: Union[str, float]): @@ -238,6 +240,7 @@ class VbanCmd(abc.ABC): self._send_request(f'{cmd}={val};') self.cache[cmd] = val + @ratelimit def sendtext(self, script) -> str | None: """Sends a multiple parameter string over a network.""" self._send_request(script) @@ -252,12 +255,10 @@ class VbanCmd(abc.ABC): except TimeoutError as e: self.logger.exception(f'Timeout waiting for matrix response: {e}') raise VBANCMDConnectionError( - f'Timeout waiting for response from {self.ip}:{self.port}' + f'Timeout waiting for response from {self.host}:{self.port}' ) from e return payload - time.sleep(self.DELAY) - @property def type(self) -> str: """Returns the type of Voicemeeter installation.""" diff --git a/vban_cmd/worker.py b/vban_cmd/worker.py index 85aff58..2ff8343 100644 --- a/vban_cmd/worker.py +++ b/vban_cmd/worker.py @@ -27,19 +27,13 @@ class Subscriber(threading.Thread): def run(self): while not self.stopped(): - try: - for nbs in NBS: - sub_packet = VbanSubscribeHeader().to_bytes( - nbs, self._remote._get_next_framecounter() - ) - self._remote.sock.sendto( - sub_packet, (self._remote.ip, self._remote.port) - ) - except TimeoutError as e: - self.logger.exception(f'{type(e).__name__}: {e}') - raise VBANCMDConnectionError( - f'timeout sending subscription to {self._remote.ip}:{self._remote.port}' - ) from e + for nbs in NBS: + sub_packet = VbanSubscribeHeader().to_bytes( + nbs, self._remote._get_next_framecounter() + ) + self._remote.sock.sendto( + sub_packet, (self._remote.ip, self._remote.port) + ) self.wait_until_stopped(10) self.logger.debug(f'terminating {self.name} thread') @@ -128,7 +122,6 @@ class Producer(threading.Thread): self.queue.put('pdirty') if self._remote.event.ldirty: self.queue.put('ldirty') - # time.sleep(self._remote.ratelimit) self.logger.debug(f'terminating {self.name} thread') self.queue.put(None)