Compare commits

...

47 Commits
v0.3.0 ... main

Author SHA1 Message Date
b0a524d125 add Strip Comp Command to README 2026-03-02 21:18:05 +00:00
2693fcce2c bump vban-cmd dep version
minor bump
2026-03-02 21:13:39 +00:00
bf058e910e add more compressor commands 2026-03-02 21:13:12 +00:00
e84accd37a patch bump 2026-03-02 12:55:49 +00:00
d9810ce270 enable rich tracebacks
console, error_console are now app attributes.
2026-03-02 12:55:33 +00:00
4fff581c95 patch bump 2026-03-02 12:30:35 +00:00
1f6811d5a0 general parser improvements.
index and ctx parameters now show as required in help output
2026-03-02 12:30:22 +00:00
a1da5c7256 patch bump 2026-03-02 12:04:21 +00:00
642337d987 pause-recording command added, to differentiate from playback pausing. 2026-03-02 12:04:01 +00:00
51002edb39 upd IN 4 2026-03-02 12:02:14 +00:00
d30c9f828d upd recorder examples
add implementation note 4
2026-03-02 11:59:56 +00:00
a9c3168542 add uv lock pre-commit config 2026-03-02 02:27:49 +00:00
fba3eddea8 upd link to packet ident:1 2026-03-02 01:39:50 +00:00
6c2c924a48 upd implementation notes. 2026-03-02 01:36:46 +00:00
27290e1a0e md fix 2026-03-02 01:26:33 +00:00
6188da4f51 minor bump 2026-03-02 01:26:03 +00:00
230e537414 add gainlayer examples 2026-03-02 01:25:54 +00:00
e6ebf86c86 add gainlayer command group
add GainlayerHelpFormatter
2026-03-02 01:24:18 +00:00
1a0fb979e0 minor bump 2026-03-02 00:29:02 +00:00
080e26f75f add more eq cell commands 2026-03-02 00:28:32 +00:00
f6d82c5064 remove duplicate entries 2026-03-01 22:08:41 +00:00
627ada3b09 md fix 2026-03-01 22:06:07 +00:00
f389eb53b8 patch bump 2026-03-01 21:59:49 +00:00
341c81fde1 minor bump 2026-03-01 21:16:11 +00:00
e062da51ed add Recorder Command to README 2026-03-01 21:15:51 +00:00
c82a021708 add recorder subcommand group
add validation module
2026-03-01 21:15:42 +00:00
61a3bc38a8 minor bump 2026-03-01 17:59:07 +00:00
8991737011 add Command Command to README 2026-03-01 17:58:58 +00:00
84d716b2ad add command subcommand group 2026-03-01 17:58:08 +00:00
b58bb3dde7 reword 2026-03-01 17:35:42 +00:00
d414844f8f md fix 2026-03-01 17:33:21 +00:00
f605178da6 add note about sendtext --help. 2026-03-01 17:26:21 +00:00
c535ae5571 minor bump 2026-03-01 17:25:23 +00:00
7b5d2150c7 add Sentext Command to README 2026-03-01 17:25:16 +00:00
28ec67839a add sendtext command, attach it to the root command.
disable the rt listeners if sendtext command is invoked.
2026-03-01 17:25:01 +00:00
dd0d150202 fix bus mono implementation
bump vban-cmd dep version

patch bump
2026-03-01 17:23:38 +00:00
78952aa3ff add Strip EQ and Strip EQ Cell examples.
fix Bus examples.
2026-03-01 01:32:22 +00:00
c4d67527f5 upd further notes 2026-03-01 01:22:18 +00:00
b3cfc6bc4a remove StripSubcommandHelpFormatter, StripHelpFormatter now handles commands + command groups. 2026-03-01 01:04:54 +00:00
c642bbc1f2 show the index arg 2026-02-28 16:03:34 +00:00
61a37bcd0f patch bump 2026-02-28 15:48:28 +00:00
b62ee185c3 upd use section in README 2026-02-28 15:39:39 +00:00
c7365bfe4e add new help formatters for different kinds of commands 2026-02-28 15:39:26 +00:00
c660778698 if completion flag return early to avoid unnecessary VBAN connections
patch bump
2026-02-28 13:34:46 +00:00
5460965945 add to further notes 2026-02-27 23:06:00 +00:00
d03049e713 enable shell completion scripts 2026-02-27 22:59:20 +00:00
1dd518095a eq commands should target the right kind of eq
cell command group has now been attached to eq. this modifies the structure of CLI slightly.
2026-02-27 22:50:00 +00:00
18 changed files with 971 additions and 194 deletions

10
.gitignore vendored
View File

@ -1,13 +1,3 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
# Generated by ignr: github.com/onyx-and-iris/ignr
## Python ##

7
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,7 @@
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.10.7
hooks:
# Update the uv lockfile
- id: uv-lock

161
README.md
View File

@ -5,6 +5,8 @@
---
This CLI is still in an early stage of development with many more things that could be implemented. However, the commands that are implemented should be working without issues.
## Install
#### With uv
@ -47,33 +49,152 @@ export VBAN_CLI_STREAMNAME=Command1
## Use
```console
Usage: vban-cli COMMAND
### Strip Command
╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮
│ bus Control the bus parameters. │
│ strip Control the strip parameters. │
│ --help (-h) Display this message and exit. │
│ --version Display application version. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Parameters ─────────────────────────────────────────────────────────────────────────────────────╮
│ --kind Kind of Voicemeeter [env var: VBAN_CLI_KIND] [default: potato] │
│ --host VBAN host [env var: VBAN_CLI_HOST] [default: localhost] │
│ --port VBAN port [env var: VBAN_CLI_PORT] [default: 6980] │
│ --streamname VBAN stream name [env var: VBAN_CLI_STREAMNAME] [default: Command1] │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Usage: vban-cli strip \<index> COMMAND [ARGS]
examples:
```console
vban-cli strip 0 mute true
vban-cli strip 1 A1 true
vban-cli strip 2 gain -18.7
```
For every command and subcommand there exists a `--help` flag for further usage information.
see `vban-cli strip --help` for more info.
##### Strip EQ
Usage: vban-cli strip \<index> eq COMMAND [OPTIONS]
examples:
```console
vban-cli strip 0 eq on true
```
see `vban-cli strip eq --help` for more info.
##### Strip EQ Cell Command
Usage: vban-cli strip \<index> eq cell \<band> COMMAND [ARGS]
examples:
```console
vban-cli strip 0 eq cell 0 on false
vban-cli strip 3 eq cell 2 freq 1500
vban-cli strip 4 eq cell 5 type 5
```
see `vban-cli strip eq cell --help` for more info.
##### Strip Comp Command
Usage: vban-cli strip \<index> comp COMMAND
examples:
```console
vban-cli strip 0 comp attack 2.0
vban-cli strip 3 comp auto-makeup true
```
see `vban-cli strip comp --help` for more info.
##### Strip Gainlayer Command
Usage: vban-cli strip \<index> gainlayer \<gainlayer_index> COMMAND [OPTIONS] [ARGS]
examples:
```console
vban-cli strip 0 gainlayer 0 level
vban-cli strip 3 gainlayer 2 level -13.5
```
see `vban-cli strip gainlayer --help` for more info.
### Bus Command
Usage: vban-cli bus \<index> COMMAND [ARGS]
examples:
```console
vban-cli bus 0 mode tvmix
vban-cli bus 1 mute true
```
see `vban-cli bus --help` for more info.
### Command Command
Usage: vban-cli command COMMAND
examples:
```console
vban-cli command show
vban-cli command restart
```
see `vban-cli command --help` for more info.
### Recorder Command
Usage: vban-cli recorder COMMAND
examples:
```console
vban-cli recorder play
vban-cli recorder pause
vban-cli recorder goto "00:01:30"
```
see `vban-cli recorder --help` for more info.
### Sendtext Command
Usage: vban-cli sendtext TEXT
examples:
*To Voicemeeter*
```console
vban-cli sendtext 'Strip[0].Mute=1;Bus[0].Mono=2'
```
*To Matrix*
```console
vban-cli sendtext 'Command.Version = ?'
vban-cli sendtext 'Point(ASIO128.IN[1..4],ASIO128.OUT[1]).dBGain = -3.0'
```
see `vban-cli sendtext --help` for more info.
---
## Implementation Notes
1. The VBAN TEXT subprotocol defines two packet structures [ident:0][ident-0] and [ident:1][ident-1]. Neither of them contain the data for Bus EQ parameters.
2. Packet structure with [ident:1][ident-1] is emitted by the VBAN server only on pdirty events. This means we do not receive the initial state of those parameters on initial subscription. Therefore any commands which are intended to fetch the value of parameters defined in packet [ident:1][ident-1] will not work in this CLI.
1. The VBAN RT SERVICE subprotocol defines two packet structures [ident:0][ident-0] and [ident:1][ident-1]. Neither of them contain the data for Bus EQ parameters.
2. Packet structure with [ident:1][ident-1] is emitted by the VBAN server only on pdirty events. This means we do not receive the current state of those parameters on initial subscription. Therefore any commands which are intended to fetch the value of parameters defined in packet [ident:1][ident-1] will not work in this CLI.
3. Packet structure with [ident:1][ident-1] defines parameteric EQ data only for the [first channel][ident-1-peq].
4. There doesn't appear to be any way to retrieve the current recorder status, ie, recording, playing, stopped etc. I don't see the data available in either packet structures [ident:0][ident-0] or [ident:1][ident-1].
---
@ -81,6 +202,8 @@ For every command and subcommand there exists a `--help` flag for further usage
I've made the effort to set up the basic skeletal structure of the CLI as well as demonstrate how to combine subcommand groups with subcommand groups so more can be implemented, it just needs doing. There may be restrictions on some things however, for example, retrieving values is only possible for parameters [defined in the protocol](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L787). Setting parameters can be done for anything possible by a string request.
Shell completion scripts are available (for zsh, bash and fish) but they haven't been thoroughly tested.
If there's something missing that you would like to see added the best bet is to submit a PR. You may raise an issue and if it's quick and simple to do I may (or may not) do it.
---
@ -92,5 +215,5 @@ If there's something missing that you would like to see added the best bet is to
[ident-0]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L896
[ident-1]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L982
[ident-1]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L1053
[ident-1-peq]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L995

View File

@ -1,11 +1,11 @@
[project]
name = "vban-cli"
version = "0.3.0"
version = "0.10.0"
description = "A command-line interface for Voicemeeter leveraging VBAN."
readme = "README.md"
license = { text = "LICENSE" }
requires-python = ">=3.13"
dependencies = ["cyclopts>=4.6.0", "vban-cmd>=2.6.0"]
dependencies = ["cyclopts>=4.6.0", "loguru>=0.7.3", "vban-cmd>=2.9.0"]
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python",
@ -24,4 +24,4 @@ vban-cli = "vban_cli.app:run"
package = true
[tool.uv.sources]
vban-cmd = { path = "../vban-cmd-python" }
vban-cmd = { path = "../vban-cmd-python", editable = true }

View File

@ -2,20 +2,30 @@ from dataclasses import dataclass
from typing import Annotated
import vban_cmd
from cyclopts import App, Parameter, config
from cyclopts import App, Argument, Parameter, config
from rich.traceback import install as install_rich_traceback
from . import __version__ as version
from . import bus, console, strip
from . import bus, command, console, recorder, strip
from .context import Context
from .error import VbanCLIConnectionError
app = App(
config=config.Env(
'VBAN_CLI_',
), # Environment variable prefix for configuration parameters
version=version,
console=console.out,
error_console=console.err,
exit_on_error=True,
)
app.command(strip.app.meta, name='strip')
app.command(bus.app.meta, name='bus')
app.command(command.app, name='command')
app.command(recorder.app, name='recorder')
app.register_install_completion_command()
install_rich_traceback(console=console.err)
@Parameter(name='*')
@ -32,22 +42,38 @@ def launcher(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
vban_config: Annotated[VBANConfig, Parameter()] = VBANConfig(),
):
command, bound, _ = app.parse_args(tokens)
if tokens[0] == '--install-completion':
return command(*bound.args, **bound.kwargs)
disable_rt_listeners = False
if command.__name__ == 'sendtext':
disable_rt_listeners = True
try:
with vban_cmd.api(
vban_config.kind,
ip=vban_config.host,
port=vban_config.port,
streamname=vban_config.streamname,
disable_rt_listeners=disable_rt_listeners,
) as client:
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
additional_kwargs['ctx'] = Context(client=client)
return command(*bound.args, **bound.kwargs, ctx=Context(client=client))
except vban_cmd.error.VBANCMDConnectionError as e:
raise VbanCLIConnectionError(str(e)) from e
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@app.command(name='sendtext')
def sendtext(
text: Annotated[str, Argument()],
/,
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Send a text command to the current Voicemeeter/Matrix instance."""
if resp := ctx.client.sendtext(text):
app.console.print(resp)
def run():
try:
app.meta()
except Exception as e:
console.err.print(f'Error: {e}')
return e.code

View File

@ -2,27 +2,27 @@ from typing import Annotated, Literal, Optional
from cyclopts import App, Argument, Parameter
from . import console
from .context import Context
from .help import CustomHelpFormatter
from .help import BusHelpFormatter
app = App(name='bus', help_formatter=CustomHelpFormatter())
app = App(name='bus', help_formatter=BusHelpFormatter())
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 1.
# app.command(eq.app.meta, name='eq')
@app.meta.default
def launcher(
index: Annotated[int, Argument()] = None,
index: Annotated[int, Argument()],
/,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
ctx: Annotated[Context, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Control the bus parameters."""
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
if index is not None:
if tokens[0] == 'eq':
additional_kwargs['eq_kind'] = app.name[0]
additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -30,30 +30,32 @@ def launcher(
@app.command(name='mono')
def mono(
new_value: Annotated[Optional[bool], Argument()] = None,
new_value: Annotated[
Optional[Literal['off', 'mono', 'stereoreverse']], Argument()
] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the mono state of the specified bus.
Parameters
----------
new_value : bool, optional
new_value : {'off', 'mono', 'stereoreverse'}, optional
If provided, sets the mono state to this value. If not provided, the current mono state is printed.
"""
if new_value is None:
console.out.print(ctx.client.bus[index].mono)
app.console.print(['off', 'mono', 'stereoreverse'][ctx.client.bus[index].mono])
return
ctx.client.bus[index].mono = new_value
ctx.client.bus[index].mono = ['off', 'mono', 'stereoreverse'].index(new_value)
@app.command(name='mute')
def mute(
new_value: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the mute state of the specified bus.
@ -63,7 +65,7 @@ def mute(
If provided, sets the mute state to this value. If not provided, the current mute state is printed.
"""
if new_value is None:
console.out.print(ctx.client.bus[index].mute)
app.console.print(ctx.client.bus[index].mute)
return
ctx.client.bus[index].mute = new_value
@ -90,8 +92,8 @@ def mode(
Argument(),
] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the bus mode of the specified bus.
@ -101,6 +103,6 @@ def mode(
If provided, sets the bus mode to this value. If not provided, the current bus mode is printed.
"""
if type_ is None:
console.out.print(ctx.client.bus[index].mode.get())
app.console.print(ctx.client.bus[index].mode.get())
return
setattr(ctx.client.bus[index].mode, type_, True)

48
src/vban_cli/command.py Normal file
View File

@ -0,0 +1,48 @@
from typing import Annotated
from cyclopts import App, Parameter
from .context import Context
from .help import BaseHelpFormatter
app = App(name='command', help_formatter=BaseHelpFormatter())
@app.command(name='show')
def show(
*,
ctx: Annotated[Context, Parameter(parse=False)] = None,
):
"""Bring the Voicemeeter GUI to the foreground."""
ctx.client.command.show()
app.console.print('Voicemeeter GUI should now be in the foreground.')
@app.command(name='hide')
def hide(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Send the Voicemeeter GUI to the background."""
ctx.client.command.hide()
app.console.print('Voicemeeter GUI should now be in the background.')
@app.command(name='shutdown')
def shutdown(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Shut down Voicemeeter."""
ctx.client.command.shutdown()
app.console.print('Voicemeeter should now be shut down.')
@app.command(name='restart')
def restart(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Restart the Voicemeeter engine."""
ctx.client.command.restart()
app.console.print('Voicemeeter engine should now be restarting.')

View File

@ -3,23 +3,21 @@ from typing import Annotated
from cyclopts import App, Argument, Parameter
from .context import Context
from .help import CustomHelpFormatter
from .help import StripHelpFormatter
app = App(name='comp', help_formatter=CustomHelpFormatter())
app = App(name='comp', help_formatter=StripHelpFormatter())
@app.meta.default
def launcher(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Argument()] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Control the compressor parameters."""
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
if index is not None:
additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -29,8 +27,8 @@ def launcher(
def knob(
new_knob: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the knob of the specified compressor.
@ -41,7 +39,7 @@ def knob(
"""
if new_knob is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].comp.knob)
# app.console.print(ctx.client.strip[index].comp.knob)
return
ctx.client.strip[index].comp.knob = new_knob
@ -50,8 +48,8 @@ def knob(
def input_gain(
new_gain: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the input gain of the specified compressor.
@ -62,6 +60,153 @@ def input_gain(
"""
if new_gain is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].comp.gainin)
# app.console.print(ctx.client.strip[index].comp.gainin)
return
ctx.client.strip[index].comp.gainin = new_gain
@app.command(name='ratio')
def ratio(
new_ratio: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the ratio of the specified compressor.
Parameters
----------
new_ratio : float, optional
If provided, sets the ratio to this value. If not provided, the current ratio is printed.
"""
if new_ratio is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.ratio)
return
ctx.client.strip[index].comp.ratio = new_ratio
@app.command(name='threshold')
def threshold(
new_threshold: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the threshold of the specified compressor.
Parameters
----------
new_threshold : float, optional
If provided, sets the threshold to this value. If not provided, the current threshold is printed.
"""
if new_threshold is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.threshold)
return
ctx.client.strip[index].comp.threshold = new_threshold
@app.command(name='attack')
def attack(
new_attack: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the attack of the specified compressor.
Parameters
----------
new_attack : float, optional
If provided, sets the attack to this value. If not provided, the current attack is printed.
"""
if new_attack is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.attack)
return
ctx.client.strip[index].comp.attack = new_attack
@app.command(name='release')
def release(
new_release: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the release of the specified compressor.
Parameters
----------
new_release : float, optional
If provided, sets the release to this value. If not provided, the current release is printed.
"""
if new_release is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.release)
return
ctx.client.strip[index].comp.release = new_release
@app.command(name='knee')
def knee(
new_knee: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the knee of the specified compressor.
Parameters
----------
new_knee : float, optional
If provided, sets the knee to this value. If not provided, the current knee is printed.
"""
if new_knee is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.knee)
return
ctx.client.strip[index].comp.knee = new_knee
@app.command(name='output-gain')
def output_gain(
new_gain: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the output gain of the specified compressor.
Parameters
----------
new_gain : float, optional
If provided, sets the output gain to this value. If not provided, the current output gain is printed.
"""
if new_gain is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.gainout)
return
ctx.client.strip[index].comp.gainout = new_gain
@app.command(name='auto-makeup')
def makeup(
new_makeup: Annotated[bool, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the auto-makeup of the specified compressor.
Parameters
----------
new_makeup : bool, optional
If provided, sets the auto-makeup to this value. If not provided, the current auto-makeup is printed.
"""
if new_makeup is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.makeup)
return
ctx.client.strip[index].comp.makeup = new_makeup

View File

@ -3,23 +3,21 @@ from typing import Annotated
from cyclopts import App, Argument, Parameter
from .context import Context
from .help import CustomHelpFormatter
from .help import StripHelpFormatter
app = App(name='denoiser', help_formatter=CustomHelpFormatter())
app = App(name='denoiser', help_formatter=StripHelpFormatter())
@app.meta.default
def launcher(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Argument()] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Control the denoiser parameters."""
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
if index is not None:
additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -29,8 +27,8 @@ def launcher(
def knob(
new_knob: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the knob of the specified denoiser.
@ -41,6 +39,6 @@ def knob(
"""
if new_knob is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].denoiser.knob)
# app.console.print(ctx.client.strip[index].denoiser.knob)
return
ctx.client.strip[index].denoiser.knob = new_knob

View File

@ -3,30 +3,32 @@ from typing import Annotated
from cyclopts import App, Argument, Parameter
from .context import Context
from .help import CustomHelpFormatter
from .help import CellHelpFormatter, EqHelpFormatter
app = App(name='eq', help_formatter=CustomHelpFormatter())
cell_app = App(name='cell', help_formatter=CellHelpFormatter())
app = App(name='eq', help_formatter=EqHelpFormatter())
app.command(cell_app.meta, name='cell')
@app.meta.default
def launcher(
band: Annotated[int, Argument()] = None,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Argument()] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
eq_kind: Annotated[str, Parameter(parse=False)],
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Control the EQ parameters.
Only channel 0 is supported, see https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 3.
"""
"""Control the EQ parameters."""
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
if index is not None:
additional_kwargs['index'] = index
if band is not None:
additional_kwargs['band'] = band
if ctx is not None:
additional_kwargs['ctx'] = ctx
match eq_kind:
case 'strip':
target = ctx.client.strip[index].eq
case 'bus':
target = ctx.client.bus[index].eq
case _:
raise ValueError(f'Invalid eq_kind: {eq_kind}')
additional_kwargs['target'] = target
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -35,9 +37,7 @@ def launcher(
def on(
new_state: Annotated[bool, Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
band: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
target: Annotated[object, Parameter(parse=False)],
):
"""Get or set the on state of the specified EQ band.
@ -48,6 +48,124 @@ def on(
"""
if new_state is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].eq.channel[0].cell[band].on)
# app.console.print(target.on)
return
ctx.client.strip[index].eq.channel[0].cell[band].on = new_state
target.on = new_state
@cell_app.meta.default
def cell_launcher(
band: Annotated[int, Argument()],
/,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
target: Annotated[object, Parameter(parse=False)],
):
"""Control the EQ Cell parameters.
Only channel 0 is supported, see https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 3.
"""
additional_kwargs = {}
command, bound, _ = cell_app.parse_args(tokens)
additional_kwargs['target'] = target.channel[0].cell[band]
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@cell_app.command(name='on')
def cell_on(
new_state: Annotated[bool, Argument()] = None,
*,
target: Annotated[object, Parameter(parse=False)],
):
"""Get or set the on state of the specified EQ cell.
Parameters
----------
new_state : bool
If provided, sets the on state to this value. If not provided, the current on state is printed.
"""
if new_state is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(target.on)
return
target.on = new_state
@cell_app.command(name='freq')
def cell_freq(
new_freq: Annotated[float, Argument()] = None,
*,
target: Annotated[object, Parameter(parse=False)],
):
"""Get or set the frequency of the specified EQ cell.
Parameters
----------
new_freq : float
If provided, sets the frequency to this value. If not provided, the current frequency is printed.
"""
if new_freq is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(target.f)
return
target.f = new_freq
@cell_app.command(name='gain')
def cell_gain(
new_gain: Annotated[float, Argument()] = None,
*,
target: Annotated[object, Parameter(parse=False)],
):
"""Get or set the gain of the specified EQ cell.
Parameters
----------
new_gain : float
If provided, sets the gain to this value. If not provided, the current gain is printed.
"""
if new_gain is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(target.gain)
return
target.gain = new_gain
@cell_app.command(name='quality')
def cell_q(
new_q: Annotated[float, Argument()] = None,
*,
target: Annotated[object, Parameter(parse=False)],
):
"""Get or set the Q of the specified EQ cell.
Parameters
----------
new_q : float
If provided, sets the Q to this value. If not provided, the current Q is printed.
"""
if new_q is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(target.q)
return
target.q = new_q
@cell_app.command(name='type')
def cell_type(
new_type: Annotated[int, Argument()] = None,
*,
target: Annotated[object, Parameter(parse=False)],
):
"""Get or set the type of the specified EQ cell.
Parameters
----------
new_type : int
If provided, sets the type to this value. If not provided, the current type is printed.
"""
if new_type is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(target.type)
return
target.type = new_type

14
src/vban_cli/error.py Normal file
View File

@ -0,0 +1,14 @@
class VbanCLIError(Exception):
"""Base class for exceptions in this module."""
def __init__(self, message: str, code: int = 1):
super().__init__(message)
self.code = code
class VbanCLIConnectionError(VbanCLIError):
"""Exception raised for connection errors."""
class VbanCLIValidationError(VbanCLIError):
"""Exception raised for validation errors."""

47
src/vban_cli/gainlayer.py Normal file
View File

@ -0,0 +1,47 @@
from typing import Annotated
from cyclopts import App, Argument, Parameter
from .context import Context
from .help import GainlayerHelpFormatter
app = App(name='gainlayer', help_formatter=GainlayerHelpFormatter())
@app.meta.default
def launcher(
gainlayer_index: Annotated[int, Argument()],
/,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Control the gainlayers."""
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
additional_kwargs['strip_index'] = index
additional_kwargs['gainlayer_index'] = gainlayer_index
additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@app.command(name='level')
def level(
new_level: Annotated[float, Argument()] = None,
*,
strip_index: Annotated[int, Parameter(parse=False)],
gainlayer_index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the level of the specified gainlayer.
Parameters
----------
new_level : float
If provided, sets the level to this value. If not provided, the current level is printed.
"""
if new_level is None:
app.console.print(ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain)
return
ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain = new_level

View File

@ -3,23 +3,21 @@ from typing import Annotated
from cyclopts import App, Argument, Parameter
from .context import Context
from .help import CustomHelpFormatter
from .help import StripHelpFormatter
app = App(name='gate', help_formatter=CustomHelpFormatter())
app = App(name='gate', help_formatter=StripHelpFormatter())
@app.meta.default
def launcher(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Argument()] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Control the compressor parameters."""
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
if index is not None:
additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -29,8 +27,8 @@ def launcher(
def knob(
new_knob: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the knob of the specified gate.
@ -41,7 +39,7 @@ def knob(
"""
if new_knob is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].gate.knob)
# app.console.print(ctx.client.strip[index].gate.knob)
return
ctx.client.strip[index].gate.knob = new_knob
@ -50,8 +48,8 @@ def knob(
def threshold(
new_threshold: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the threshold of the specified gate.
@ -62,6 +60,6 @@ def threshold(
"""
if new_threshold is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].gate.threshold)
# app.console.print(ctx.client.strip[index].gate.threshold)
return
ctx.client.strip[index].gate.threshold = new_threshold

View File

@ -4,31 +4,24 @@ from cyclopts.help import DefaultFormatter, HelpPanel
from rich.console import Console, ConsoleOptions
class CustomHelpFormatter(DefaultFormatter):
"""Custom help formatter that injects an index argument into the usage line and filters it out from the parameters list.
This formatter modifies the usage line to include an <index> argument after the 'strip' command,
and filters out any parameters related to 'index' from the Parameters section of the help output.
"""
def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
"""Render the usage line with index argument injected."""
if usage:
modified_usage = re.sub(
r'(\S+\s+[a-z]+)\s+(COMMAND)', r'\1 <index> \2', str(usage)
)
console.print(f'[bold]Usage:[/bold] {modified_usage}')
class BaseHelpFormatter(DefaultFormatter):
"""Base help formatter that provides common functionality."""
def __call__(
self, console: Console, options: ConsoleOptions, panel: HelpPanel
) -> None:
"""Render a help panel, filtering out the index parameter from Parameters sections."""
"""Render a help panel, filtering out hidden parameters from Parameters sections."""
if panel.title == 'Parameters':
filtered_entries = [
entry
for entry in panel.entries
if not (
entry.names and any('index' in name.lower() for name in entry.names)
entry.names
and any(
param in name.lower()
for name in entry.names
for param in self.get_filtered_params()
)
)
]
@ -41,3 +34,94 @@ class CustomHelpFormatter(DefaultFormatter):
super().__call__(console, options, filtered_panel)
else:
super().__call__(console, options, panel)
def get_filtered_params(self):
"""Return list of parameter names to filter out of help output."""
return ['index', 'band', 'ctx', 'target', 'eq_kind']
class StripHelpFormatter(BaseHelpFormatter):
"""Help formatter for strip commands that injects <index> after 'strip'."""
def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
"""Render the usage line with index argument injected after 'strip'.
Handles both command groups (COMMAND) and individual commands (commandname [ARGS/OPTIONS]).
"""
if usage:
modified_usage = re.sub(
r'(\S+\s+strip)\s+(\w+\s+\[[^\]]+\]|\w+\s+\[[^\]]+\]|\w+(?:\s+\[[^\]]+\])*|COMMAND)',
r'\1 <index> \2',
str(usage),
)
if modified_usage == str(usage):
modified_usage = re.sub(
r'(\S+\s+strip)\s+(\w+)', r'\1 <index> \2', str(usage)
)
console.print(f'[bold]Usage:[/bold] {modified_usage}')
class BusHelpFormatter(BaseHelpFormatter):
"""Help formatter for bus commands that injects <index> after 'bus'."""
def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
"""Render the usage line with index argument injected after 'bus'.
Handles both command groups (COMMAND) and individual commands (commandname [ARGS/OPTIONS])."""
if usage:
modified_usage = re.sub(
r'(\S+\s+bus)\s+(\w+\s+\[[^\]]+\]|\w+\s+\[[^\]]+\]|\w+(?:\s+\[[^\]]+\])*|COMMAND)',
r'\1 <index> \2',
str(usage),
)
if modified_usage == str(usage):
modified_usage = re.sub(
r'(\S+\s+bus)\s+(\w+)', r'\1 <index> \2', str(usage)
)
console.print(f'[bold]Usage:[/bold] {modified_usage}')
class EqHelpFormatter(BaseHelpFormatter):
"""Help formatter for eq commands that works with both strip and bus commands.
Injects <index> after 'strip' or 'bus' and <band> after 'cell'."""
def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
"""Render the usage line with proper <index> placement for both strip and bus commands."""
if usage:
modified_usage = re.sub(
r'(\S+\s+)(\w+)(\s+eq\s+)(COMMAND)', r'\1\2 <index>\3\4', str(usage)
)
console.print(f'[bold]Usage:[/bold] {modified_usage}')
class GainlayerHelpFormatter(BaseHelpFormatter):
"""Help formatter for gainlayer commands that works with strip commands.
Injects <index> after 'strip' and <gainlayer_index> after 'gainlayer'."""
def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
"""Render the usage line with proper <index> placement for strip commands."""
if usage:
modified_usage = re.sub(
r'(\S+\s+strip)(\s+gainlayer\s+)(COMMAND)',
r'\1 <index>\2<[cyan]gainlayer_index[/cyan]> \3',
str(usage),
)
console.print(f'[bold]Usage:[/bold] {modified_usage}')
class CellHelpFormatter(BaseHelpFormatter):
"""Help formatter for cell commands that works with both strip and bus commands.
Injects <index> after 'strip' or 'bus' and <band> after 'cell'."""
def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
"""Render the usage line with proper <index> and <band> placement."""
if usage:
modified_usage = re.sub(
r'(\S+\s+)(\w+)(\s+eq\s+cell\s+)(COMMAND)',
r'\1\2 <index>\3<[cyan]band[/cyan]> \4',
str(usage),
)
console.print(f'[bold]Usage:[/bold] {modified_usage}')

131
src/vban_cli/recorder.py Normal file
View File

@ -0,0 +1,131 @@
from pathlib import Path
from typing import Annotated
from cyclopts import App, Parameter, validators
from . import validation
from .context import Context
from .help import BaseHelpFormatter
app = App(name='recorder', help_formatter=BaseHelpFormatter())
@app.command(name='play')
def play(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Start the recorder playback."""
ctx.client.recorder.play()
app.console.print('Recorder playback started.')
@app.command(name='pause')
def pause(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Pause the recorder playback."""
ctx.client.recorder.stop()
app.console.print('Recorder playback paused.')
@app.command(name='stop')
def stop(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Stop the recorder playback/recording and reset to the beginning."""
ctx.client.recorder.stop()
ctx.client.recorder.goto('00:00:00')
# We have no way of knowing if the recorder was playing or recording, so we print a generic message.
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 4.
app.console.print('Recorder stopped.')
@app.command(name='replay')
def replay(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Replay the recorder playback."""
ctx.client.recorder.replay()
app.console.print('Recorder playback replay started.')
@app.command(name='record')
def record(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Start recording."""
ctx.client.recorder.record()
app.console.print('Recorder recording started.')
@app.command(name='pause-recording')
def pause_recording(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Pause the recorder recording."""
ctx.client.recorder.pause()
app.console.print('Recorder recording paused.')
@app.command(name='ff')
def ff(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Fast forward the recorder playback."""
ctx.client.recorder.ff()
app.console.print('Recorder playback fast forwarded.')
@app.command(name='rew')
def rew(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Rewind the recorder playback."""
ctx.client.recorder.rew()
app.console.print('Recorder playback rewound.')
@app.command(name='load')
def load(
file_path: Annotated[
Path,
Parameter(
help='The path to the recording file to load.',
validator=validators.Path(exists=True),
),
],
/,
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Load a file into the recorder.
note: This command may only work if vban-cli is running on localhost and may not work if vban-cli is running on a remote server."""
ctx.client.recorder.load(file_path)
app.console.print(f'Loaded file: {file_path}')
@app.command(name='goto')
def goto(
time_string: Annotated[
str,
Parameter(
help='The timestamp to go to in the recorder playback (format: HH:MM:SS).',
validator=validation.is_valid_time_string,
),
],
/,
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Go to a specific timestamp in the recorder playback."""
ctx.client.recorder.goto(time_string)
app.console.print(f'Went to timestamp {time_string} in recorder playback.')

View File

@ -2,29 +2,31 @@ from typing import Annotated, Optional
from cyclopts import App, Argument, Parameter
from . import comp, console, denoiser, eq, gate
from . import comp, denoiser, eq, gainlayer, gate
from .context import Context
from .help import CustomHelpFormatter
from .help import StripHelpFormatter
app = App(name='strip', help_formatter=CustomHelpFormatter())
app = App(name='strip', help_formatter=StripHelpFormatter())
app.command(eq.app.meta, name='eq')
app.command(comp.app.meta, name='comp')
app.command(gate.app.meta, name='gate')
app.command(denoiser.app.meta, name='denoiser')
app.command(gainlayer.app.meta, name='gainlayer')
@app.meta.default
def launcher(
index: Annotated[int, Argument()] = None,
index: Annotated[int, Argument()],
/,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
ctx: Annotated[Context, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Control the strip parameters."""
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
if index is not None:
if tokens[0] == 'eq':
additional_kwargs['eq_kind'] = app.name[0]
additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -34,8 +36,8 @@ def launcher(
def mono(
new_state: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the mono state of the specified strip.
@ -45,7 +47,7 @@ def mono(
If provided, sets the mono state to this value. If not provided, the current mono state is printed.
"""
if new_state is None:
console.out.print(ctx.client.strip[index].mono)
app.console.print(ctx.client.strip[index].mono)
return
ctx.client.strip[index].mono = new_state
@ -54,8 +56,8 @@ def mono(
def solo(
new_state: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the solo state of the specified strip.
@ -65,7 +67,7 @@ def solo(
If provided, sets the solo state to this value. If not provided, the current solo state is printed.
"""
if new_state is None:
console.out.print(ctx.client.strip[index].solo)
app.console.print(ctx.client.strip[index].solo)
return
ctx.client.strip[index].solo = new_state
@ -74,8 +76,8 @@ def solo(
def mute(
new_state: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the mute state of the specified strip.
@ -85,7 +87,7 @@ def mute(
If provided, sets the mute state to this value. If not provided, the current mute state is printed.
"""
if new_state is None:
console.out.print(ctx.client.strip[index].mute)
app.console.print(ctx.client.strip[index].mute)
return
ctx.client.strip[index].mute = new_state
@ -94,8 +96,8 @@ def mute(
def gain(
new_value: Annotated[Optional[float], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the gain of the specified strip.
@ -105,7 +107,7 @@ def gain(
If provided, sets the gain to this value. If not provided, the current gain is printed.
"""
if new_value is None:
console.out.print(ctx.client.strip[index].gain)
app.console.print(ctx.client.strip[index].gain)
return
ctx.client.strip[index].gain = new_value
@ -114,8 +116,8 @@ def gain(
def a1(
new_value: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the A1 state of the specified strip.
@ -125,7 +127,7 @@ def a1(
If provided, sets the A1 state to this value. If not provided, the current A1 state is printed.
"""
if new_value is None:
console.out.print(ctx.client.strip[index].A1)
app.console.print(ctx.client.strip[index].A1)
return
ctx.client.strip[index].A1 = new_value
@ -134,8 +136,8 @@ def a1(
def a2(
new_value: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the A2 state of the specified strip.
@ -145,7 +147,7 @@ def a2(
If provided, sets the A2 state to this value. If not provided, the current A2 state is printed.
"""
if new_value is None:
console.out.print(ctx.client.strip[index].A2)
app.console.print(ctx.client.strip[index].A2)
return
ctx.client.strip[index].A2 = new_value
@ -154,8 +156,8 @@ def a2(
def a3(
new_value: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the A3 state of the specified strip.
@ -165,7 +167,7 @@ def a3(
If provided, sets the A3 state to this value. If not provided, the current A3 state is printed.
"""
if new_value is None:
console.out.print(ctx.client.strip[index].A3)
app.console.print(ctx.client.strip[index].A3)
return
ctx.client.strip[index].A3 = new_value
@ -174,8 +176,8 @@ def a3(
def a4(
new_value: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the A4 state of the specified strip.
@ -185,7 +187,7 @@ def a4(
If provided, sets the A4 state to this value. If not provided, the current A4 state is printed.
"""
if new_value is None:
console.out.print(ctx.client.strip[index].A4)
app.console.print(ctx.client.strip[index].A4)
return
ctx.client.strip[index].A4 = new_value
@ -194,8 +196,8 @@ def a4(
def a5(
new_value: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the A5 state of the specified strip.
@ -205,7 +207,7 @@ def a5(
If provided, sets the A5 state to this value. If not provided, the current A5 state is printed.
"""
if new_value is None:
console.out.print(ctx.client.strip[index].A5)
app.console.print(ctx.client.strip[index].A5)
return
ctx.client.strip[index].A5 = new_value
@ -214,8 +216,8 @@ def a5(
def b1(
new_value: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the B1 state of the specified strip.
@ -225,7 +227,7 @@ def b1(
If provided, sets the B1 state to this value. If not provided, the current B1 state is printed.
"""
if new_value is None:
console.out.print(ctx.client.strip[index].B1)
app.console.print(ctx.client.strip[index].B1)
return
ctx.client.strip[index].B1 = new_value
@ -234,8 +236,8 @@ def b1(
def b2(
new_value: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the B2 state of the specified strip.
@ -245,7 +247,7 @@ def b2(
If provided, sets the B2 state to this value. If not provided, the current B2 state is printed.
"""
if new_value is None:
console.out.print(ctx.client.strip[index].B2)
app.console.print(ctx.client.strip[index].B2)
return
ctx.client.strip[index].B2 = new_value
@ -254,8 +256,8 @@ def b2(
def b3(
new_value: Annotated[Optional[bool], Argument()] = None,
*,
index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the B3 state of the specified strip.
@ -265,6 +267,6 @@ def b3(
If provided, sets the B3 state to this value. If not provided, the current B3 state is printed.
"""
if new_value is None:
console.out.print(ctx.client.strip[index].B3)
app.console.print(ctx.client.strip[index].B3)
return
ctx.client.strip[index].B3 = new_value

View File

@ -0,0 +1,11 @@
import re
from .error import VbanCLIValidationError
def is_valid_time_string(type_, value: str) -> str:
"""Validate if the given string is a valid time format (HH:MM:SS)."""
pattern = r'^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$'
if not re.match(pattern, value):
raise VbanCLIValidationError('Invalid time format. Expected HH:MM:SS.')
return value

41
uv.lock generated
View File

@ -11,6 +11,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cyclopts"
version = "4.6.0"
@ -44,6 +53,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
]
[[package]]
name = "loguru"
version = "0.7.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
@ -102,23 +124,34 @@ wheels = [
[[package]]
name = "vban-cli"
version = "0.3.0"
version = "0.10.0"
source = { editable = "." }
dependencies = [
{ name = "cyclopts" },
{ name = "loguru" },
{ name = "vban-cmd" },
]
[package.metadata]
requires-dist = [
{ name = "cyclopts", specifier = ">=4.6.0" },
{ name = "vban-cmd", directory = "../vban-cmd-python" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "vban-cmd", editable = "../vban-cmd-python" },
]
[[package]]
name = "vban-cmd"
version = "2.6.0"
source = { directory = "../vban-cmd-python" }
version = "2.9.0"
source = { editable = "../vban-cmd-python" }
[package.metadata]
requires-dist = [{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.1,<3.0" }]
[[package]]
name = "win32-setctime"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
]