Zuletzt aktiv 1738749288

Use this script to send vban text requests over a network

Änderung e23bcb92a49b44ba05af22c4e1148dd848578e5a

sendtext.py Orginalformat
1#!/usr/bin/env python3
2
3# MIT License
4#
5# Copyright (c) 2025 Onyx and Iris
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the "Software"), to deal
9# in the Software without restriction, including without limitation the rights
10# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11# copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in all
15# copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23# SOFTWARE.
24
25"""
26sendtext.py
27
28This script provides functionality to send text messages over a network using the VBAN protocol.
29It includes classes and functions to construct and send VBAN packets with text data, read configuration
30from a TOML file, and handle command-line arguments for customization.
31
32Classes:
33 RTHeader: Represents the VBAN header for a text stream.
34 RequestPacket: Encapsulates a VBAN request packet with text data.
35 VbanSendText: Manages the sending of text messages over VBAN with rate limiting.
36
37Functions:
38 ratelimit: Decorator to enforce a rate limit on a function.
39 conn_from_toml: Reads a TOML configuration file and returns its contents as a dictionary.
40 parse_args: Parses command-line arguments.
41 main: Main function to send text using VbanSendText.
42
43Usage:
44 Run the script with appropriate command-line arguments to send text messages.
45 Example:
46 python sendtext.py --config /path/to/config.toml --log-level DEBUG "strip[0].mute=0 strip[1].mute=0"
47"""
48
49import argparse
50import functools
51import logging
52import socket
53import sys
54import time
55from dataclasses import dataclass, field
56from pathlib import Path
57from typing import Callable
58
59import tomllib
60
61logger = logging.getLogger(__name__)
62
63
64@dataclass
65class RTHeader:
66 name: str
67 bps: int
68 channel: int
69 VBAN_PROTOCOL_TXT = 0x40
70 framecounter: bytes = (0).to_bytes(4, "little")
71 # fmt: off
72 BPS_OPTS: list[int] = field(default_factory=lambda: [
73 0, 110, 150, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 31250,
74 38400, 57600, 115200, 128000, 230400, 250000, 256000, 460800, 921600,
75 1000000, 1500000, 2000000, 3000000
76 ])
77 # fmt: on
78
79 def __post_init__(self):
80 if len(self.name) > 16:
81 raise ValueError(
82 f"Stream name got: '{self.name}', want: must be 16 characters or fewer"
83 )
84 try:
85 self.bps_index = self.BPS_OPTS.index(self.bps)
86 except ValueError as e:
87 ERR_MSG = f"Invalid bps: {self.bps}, must be one of {self.BPS_OPTS}"
88 e.add_note(ERR_MSG)
89 raise
90
91 def __sr(self) -> bytes:
92 return (RTHeader.VBAN_PROTOCOL_TXT + self.bps_index).to_bytes(1, "little")
93
94 def __nbc(self) -> bytes:
95 return (self.channel).to_bytes(1, "little")
96
97 def build(self) -> bytes:
98 header = "VBAN".encode("utf-8")
99 header += self.__sr()
100 header += (0).to_bytes(1, "little")
101 header += self.__nbc()
102 header += (0x10).to_bytes(1, "little")
103 header += self.name.encode() + bytes(16 - len(self.name))
104 header += RTHeader.framecounter
105 return header
106
107
108class RequestPacket:
109 def __init__(self, header: RTHeader):
110 self.header = header
111
112 def encode(self, text: str) -> bytes:
113 return self.header.build() + text.encode("utf-8")
114
115 def bump_framecounter(self) -> None:
116 self.header.framecounter = (
117 int.from_bytes(self.header.framecounter, "little") + 1
118 ).to_bytes(4, "little")
119
120 logger.debug(
121 f"framecounter: {int.from_bytes(self.header.framecounter, 'little')}"
122 )
123
124
125def ratelimit(func: Callable) -> Callable:
126 """
127 Decorator to enforce a rate limit on a function.
128 This decorator ensures that the decorated function is not called more frequently
129 than the specified delay. If the function is called before the delay has passed
130 since the last call, it will wait for the remaining time before executing.
131 Args:
132 func (callable): The function to be decorated.
133 Returns:
134 callable: The wrapped function with rate limiting applied.
135 Example:
136 @ratelimit
137 def send_message(self, message):
138 # Function implementation
139 pass
140 """
141
142 @functools.wraps(func)
143 def wrapper(self, *args, **kwargs):
144 now = time.time()
145 if now - self.lastsent < self.delay:
146 time.sleep(self.delay - (now - self.lastsent))
147 self.lastsent = time.time()
148 return func(self, *args, **kwargs)
149
150 return wrapper
151
152
153class VbanSendText:
154 def __init__(self, **kwargs):
155 defaultkwargs = {
156 "host": "localhost",
157 "port": 6980,
158 "streamname": "Command1",
159 "bps": 256000,
160 "channel": 0,
161 "delay": 0.02,
162 }
163 defaultkwargs.update(kwargs)
164 self.__dict__.update(defaultkwargs)
165 self._request = RequestPacket(RTHeader(self.streamname, self.bps, self.channel))
166 self.lastsent = 0
167 self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
168
169 def __enter__(self):
170 self._sock.__enter__()
171 return self
172
173 def __exit__(self, exc_type, exc_value, traceback):
174 return self._sock.__exit__(exc_type, exc_value, traceback)
175
176 @ratelimit
177 def sendtext(self, text: str):
178 """
179 Sends a text message to the specified host and port.
180 Args:
181 text (str): The text message to be sent.
182 """
183
184 self._sock.sendto(self._request.encode(text), (self.host, self.port))
185
186 self._request.bump_framecounter()
187
188
189def conn_from_toml(filepath: str = "config.toml") -> dict:
190 """
191 Reads a TOML configuration file and returns its contents as a dictionary.
192 Args:
193 filepath (str): The path to the TOML file. Defaults to "config.toml".
194 Returns:
195 dict: The contents of the TOML file as a dictionary.
196 Example:
197 # config.toml
198 host = "localhost"
199 port = 6980
200 streamname = "Command1"
201 """
202
203 pn = Path(filepath)
204 if not pn.exists():
205 logger.info(
206 f"no {pn} found, using defaults: localhost:6980 streamname: Command1"
207 )
208 return {}
209
210 try:
211 with open(pn, "rb") as f:
212 return tomllib.load(f)
213 except tomllib.TOMLDecodeError as e:
214 raise ValueError(f"Error decoding TOML file: {e}") from e
215
216
217def parse_args() -> argparse.Namespace:
218 """
219 Parse command-line arguments.
220 Returns:
221 argparse.Namespace: Parsed command-line arguments.
222 Command-line arguments:
223 --log-level (str): Set the logging level. Choices are "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL". Default is "INFO".
224 --config (str): Path to config file. Default is "config.toml".
225 -i, --input-file (argparse.FileType): Input file to read from. Default is sys.stdin.
226 text (str, optional): Text to send.
227 """
228
229 parser = argparse.ArgumentParser(description="Voicemeeter VBAN Send Text CLI")
230 parser.add_argument(
231 "--log-level",
232 type=str,
233 choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
234 default="INFO",
235 help="Set the logging level",
236 )
237 parser.add_argument(
238 "--config", type=str, default="config.toml", help="Path to config file"
239 )
240 parser.add_argument(
241 "-i",
242 "--input-file",
243 type=argparse.FileType("r"),
244 default=sys.stdin,
245 )
246 parser.add_argument("text", nargs="?", type=str, help="Text to send")
247 return parser.parse_args()
248
249
250def main(config: dict):
251 """
252 Main function to send text using VbanSendText.
253 Args:
254 config (dict): Configuration dictionary for VbanSendText.
255 Behavior:
256 - If 'args.text' is provided, sends the text and returns.
257 - Otherwise, reads lines from 'args.input_file', strips whitespace, and sends each line.
258 - Stops reading and sending if a line equals "Q".
259 - Logs each line being sent at the debug level.
260 """
261
262 with VbanSendText(**config) as vban:
263 if args.text:
264 vban.sendtext(args.text)
265 return
266
267 for line in args.input_file:
268 line = line.strip()
269 if line.upper() == "Q":
270 break
271
272 logger.debug(f"Sending {line}")
273 vban.sendtext(line)
274
275
276if __name__ == "__main__":
277 args = parse_args()
278
279 logging.basicConfig(level=args.log_level)
280
281 main(conn_from_toml(args.config))
282