mirror of
https://github.com/onyx-and-iris/gobs-cli.git
synced 2025-08-07 20:21:48 +00:00
Compare commits
5 Commits
6ad530ce2e
...
c27a5ea6c5
Author | SHA1 | Date | |
---|---|---|---|
c27a5ea6c5 | |||
af962a26cc | |||
360d45aa47 | |||
3deb03cf32 | |||
f58b2dfeab |
11
CHANGELOG.md
11
CHANGELOG.md
@ -6,11 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
# [0.7.0] - 2025-05-26
|
||||
|
||||
### Added
|
||||
|
||||
- projector commands, see [ProjectorCmd](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#projectorcmd)
|
||||
|
||||
|
||||
# [0.6.1] - 2025-05-25
|
||||
|
||||
### Added
|
||||
|
||||
- filter commands, see [Filter](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#filtercmd)
|
||||
- filter commands, see [FilterCmd](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#filtercmd)
|
||||
|
||||
### Changed
|
||||
|
||||
@ -21,7 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- hotkey commands, see [Hotkey](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#hotkeycmd)
|
||||
- hotkey commands, see [HotkeyCmd](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#hotkeycmd)
|
||||
|
||||
# [0.4.2] - 2025-05-08
|
||||
|
||||
|
27
README.md
27
README.md
@ -524,6 +524,33 @@ gobs-cli toggle 'Mic/Aux' 'Gain'
|
||||
gobs-cli status 'Mic/Aux' 'Gain'
|
||||
```
|
||||
|
||||
### ProjectorCmd
|
||||
|
||||
- list-monitors: List available monitors.
|
||||
|
||||
```console
|
||||
gobs-cli projector list-monitors
|
||||
```
|
||||
|
||||
- open: Open a fullscreen projector for a source on a specific monitor.
|
||||
- flags:
|
||||
|
||||
*optional*
|
||||
- --monitor-index: Index of the monitor to open the projector on.
|
||||
- defaults to 0
|
||||
|
||||
*optional*
|
||||
- args: <source_name>
|
||||
- defaults to current scene
|
||||
|
||||
```console
|
||||
gobs-cli project open
|
||||
|
||||
gobs-cli projector open --monitor-index=1 "test_scene"
|
||||
|
||||
gobs-cli projector open --monitor-index=1 "test_group"
|
||||
```
|
||||
|
||||
|
||||
[userconfigdir]: https://pkg.go.dev/os#UserConfigDir
|
||||
[obs-keyids]: https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h
|
76
filter_test.go
Normal file
76
filter_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFilterList(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &FilterListCmd{
|
||||
SourceName: "Mic/Aux",
|
||||
}
|
||||
err := cmd.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list filters: %v", err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "test_filter") {
|
||||
t.Fatalf("Expected output to contain 'test_filter', got '%s'", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterListScene(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &FilterListCmd{
|
||||
SourceName: "gobs-test",
|
||||
}
|
||||
err := cmd.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list filters in scene: %v", err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "test_filter") {
|
||||
t.Fatalf("Expected output to contain 'test_filter', got '%s'", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterListEmpty(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &FilterListCmd{
|
||||
SourceName: "NonExistentSource",
|
||||
}
|
||||
err := cmd.Run(context)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for non-existent source, but got none")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "No source was found by the name of `NonExistentSource`.") {
|
||||
t.Fatalf(
|
||||
"Expected error to contain 'No source was found by the name of `NonExistentSource`.', got '%s'",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
}
|
140
input_test.go
Normal file
140
input_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInputList(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &InputListCmd{}
|
||||
err := cmd.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list inputs: %v", err)
|
||||
}
|
||||
|
||||
expectedInputs := []string{
|
||||
"Desktop Audio",
|
||||
"Mic/Aux",
|
||||
"Colour Source",
|
||||
"Colour Source 2",
|
||||
"Colour Source 3",
|
||||
}
|
||||
output := out.String()
|
||||
for _, input := range expectedInputs {
|
||||
if !strings.Contains(output, input) {
|
||||
t.Fatalf("Expected output to contain '%s', got '%s'", input, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputListFilterInput(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &InputListCmd{Input: true}
|
||||
err := cmd.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list inputs with filter: %v", err)
|
||||
}
|
||||
|
||||
expectedInputs := []string{
|
||||
"Mic/Aux",
|
||||
}
|
||||
expectedFilteredOut := []string{
|
||||
"Desktop Audio",
|
||||
"Colour Source",
|
||||
"Colour Source 2",
|
||||
"Colour Source 3",
|
||||
}
|
||||
for _, input := range expectedInputs {
|
||||
if !strings.Contains(out.String(), input) {
|
||||
t.Fatalf("Expected output to contain '%s', got '%s'", input, out.String())
|
||||
}
|
||||
}
|
||||
for _, filteredOut := range expectedFilteredOut {
|
||||
if strings.Contains(out.String(), filteredOut) {
|
||||
t.Fatalf("Expected output to NOT contain '%s', got '%s'", filteredOut, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputListFilterOutput(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &InputListCmd{Output: true}
|
||||
err := cmd.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list outputs with filter: %v", err)
|
||||
}
|
||||
|
||||
expectedInputs := []string{
|
||||
"Desktop Audio",
|
||||
}
|
||||
expectedFilteredOut := []string{
|
||||
"Mic/Aux",
|
||||
"Colour Source",
|
||||
"Colour Source 2",
|
||||
"Colour Source 3",
|
||||
}
|
||||
for _, input := range expectedInputs {
|
||||
if !strings.Contains(out.String(), input) {
|
||||
t.Fatalf("Expected output to contain '%s', got '%s'", input, out.String())
|
||||
}
|
||||
}
|
||||
for _, filteredOut := range expectedFilteredOut {
|
||||
if strings.Contains(out.String(), filteredOut) {
|
||||
t.Fatalf("Expected output to NOT contain '%s', got '%s'", filteredOut, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputListFilterColour(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &InputListCmd{Colour: true}
|
||||
err := cmd.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list colour inputs with filter: %v", err)
|
||||
}
|
||||
|
||||
expectedInputs := []string{
|
||||
"Colour Source",
|
||||
"Colour Source 2",
|
||||
"Colour Source 3",
|
||||
}
|
||||
for _, input := range expectedInputs {
|
||||
if !strings.Contains(out.String(), input) {
|
||||
t.Fatalf("Expected output to contain '%s', got '%s'", input, out.String())
|
||||
}
|
||||
}
|
||||
}
|
1
main.go
1
main.go
@ -45,6 +45,7 @@ type CLI struct {
|
||||
Virtualcam VirtualCamCmd `help:"Manage virtual camera." cmd:"" aliases:"vc"`
|
||||
Hotkey HotkeyCmd `help:"Manage hotkeys." cmd:"" aliases:"hk"`
|
||||
Filter FilterCmd `help:"Manage filters." cmd:"" aliases:"f"`
|
||||
Projector ProjectorCmd `help:"Manage projectors." cmd:"" aliases:"prj"`
|
||||
}
|
||||
|
||||
type context struct {
|
||||
|
32
main_test.go
32
main_test.go
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/andreykaipov/goobs"
|
||||
"github.com/andreykaipov/goobs/api/requests/config"
|
||||
"github.com/andreykaipov/goobs/api/requests/filters"
|
||||
"github.com/andreykaipov/goobs/api/requests/inputs"
|
||||
"github.com/andreykaipov/goobs/api/requests/scenes"
|
||||
typedefs "github.com/andreykaipov/goobs/api/typedefs"
|
||||
@ -87,9 +88,39 @@ func setup(client *goobs.Client) {
|
||||
"visible": true,
|
||||
}).
|
||||
WithSceneItemEnabled(true))
|
||||
|
||||
// Create source filter on an audio input
|
||||
client.Filters.CreateSourceFilter(filters.NewCreateSourceFilterParams().
|
||||
WithSourceName("Mic/Aux").
|
||||
WithFilterName("test_filter").
|
||||
WithFilterKind("compressor_filter").
|
||||
WithFilterSettings(map[string]any{
|
||||
"threshold": -20,
|
||||
"ratio": 4,
|
||||
"attack_time": 10,
|
||||
"release_time": 100,
|
||||
"output_gain": -3.6,
|
||||
"sidechain_source": nil,
|
||||
}))
|
||||
|
||||
// Create source filter on a scene
|
||||
client.Filters.CreateSourceFilter(filters.NewCreateSourceFilterParams().
|
||||
WithSourceName("gobs-test").
|
||||
WithFilterName("test_filter").
|
||||
WithFilterKind("luma_key_filter_v2").
|
||||
WithFilterSettings(map[string]any{
|
||||
"luma": 0.5,
|
||||
}))
|
||||
}
|
||||
|
||||
func teardown(client *goobs.Client) {
|
||||
client.Filters.RemoveSourceFilter(filters.NewRemoveSourceFilterParams().
|
||||
WithSourceName("Mic/Aux").
|
||||
WithFilterName("test_filter"))
|
||||
client.Filters.RemoveSourceFilter(filters.NewRemoveSourceFilterParams().
|
||||
WithSourceName("gobs-test").
|
||||
WithFilterName("test_filter"))
|
||||
|
||||
client.Scenes.RemoveScene(scenes.NewRemoveSceneParams().
|
||||
WithSceneName("gobs-test"))
|
||||
|
||||
@ -98,4 +129,5 @@ func teardown(client *goobs.Client) {
|
||||
|
||||
client.Stream.StopStream()
|
||||
client.Record.StopRecord()
|
||||
client.Outputs.StopReplayBuffer()
|
||||
}
|
||||
|
66
projector.go
Normal file
66
projector.go
Normal file
@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/andreykaipov/goobs/api/requests/ui"
|
||||
"github.com/aquasecurity/table"
|
||||
)
|
||||
|
||||
// ProjectorCmd provides a command to manage projectors in OBS.
|
||||
type ProjectorCmd struct {
|
||||
ListMonitors ProjectorListMonitorsCmd `cmd:"" help:"List available monitors." aliases:"ls-m"`
|
||||
Open ProjectorOpenCmd `cmd:"" help:"Open a fullscreen projector for a source on a specific monitor." aliases:"o"`
|
||||
}
|
||||
|
||||
// ProjectorListMonitorsCmd provides a command to list all monitors available for projectors.
|
||||
type ProjectorListMonitorsCmd struct{} // size = 0x0
|
||||
|
||||
// Run executes the command to list all monitors available for projectors.
|
||||
func (cmd *ProjectorListMonitorsCmd) Run(ctx *context) error {
|
||||
monitors, err := ctx.Client.Ui.GetMonitorList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(monitors.Monitors) == 0 {
|
||||
ctx.Out.Write([]byte("No monitors found for projectors.\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
t := table.New(ctx.Out)
|
||||
t.SetPadding(3)
|
||||
t.SetAlignment(table.AlignCenter, table.AlignLeft)
|
||||
t.SetHeaders("Monitor ID", "Monitor Name")
|
||||
|
||||
for _, monitor := range monitors.Monitors {
|
||||
t.AddRow(fmt.Sprintf("%d", monitor.MonitorIndex), monitor.MonitorName)
|
||||
}
|
||||
|
||||
t.Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProjectorOpenCmd provides a command to open a fullscreen projector for a specific source.
|
||||
type ProjectorOpenCmd struct {
|
||||
MonitorIndex int `flag:"" help:"Index of the monitor to open the projector on." default:"0"`
|
||||
SourceName string ` help:"Name of the source to project." default:"" arg:""`
|
||||
}
|
||||
|
||||
// Run executes the command to show details of a specific projector.
|
||||
func (cmd *ProjectorOpenCmd) Run(ctx *context) error {
|
||||
if cmd.SourceName == "" {
|
||||
currentScene, err := ctx.Client.Scenes.GetCurrentProgramScene()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current program scene: %w", err)
|
||||
}
|
||||
cmd.SourceName = currentScene.SceneName
|
||||
}
|
||||
|
||||
ctx.Client.Ui.OpenSourceProjector(ui.NewOpenSourceProjectorParams().
|
||||
WithSourceName(cmd.SourceName).
|
||||
WithMonitorIndex(cmd.MonitorIndex))
|
||||
|
||||
fmt.Fprintf(ctx.Out, "Opened projector for source '%s' on monitor index %d.\n", cmd.SourceName, cmd.MonitorIndex)
|
||||
return nil
|
||||
}
|
@ -19,7 +19,11 @@ type ReplayBufferStartCmd struct{} // size = 0x0
|
||||
// Run executes the command to start the replay buffer.
|
||||
func (cmd *ReplayBufferStartCmd) Run(ctx *context) error {
|
||||
_, err := ctx.Client.Outputs.StartReplayBuffer()
|
||||
return err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start replay buffer: %w", err)
|
||||
}
|
||||
fmt.Fprintln(ctx.Out, "Replay buffer started.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplayBufferStopCmd stops the replay buffer.
|
||||
@ -28,7 +32,11 @@ type ReplayBufferStopCmd struct{} // size = 0x0
|
||||
// Run executes the command to stop the replay buffer.
|
||||
func (cmd *ReplayBufferStopCmd) Run(ctx *context) error {
|
||||
_, err := ctx.Client.Outputs.StopReplayBuffer()
|
||||
return err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop replay buffer: %w", err)
|
||||
}
|
||||
fmt.Fprintln(ctx.Out, "Replay buffer stopped.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplayBufferToggleCmd toggles the replay buffer state.
|
||||
@ -42,9 +50,9 @@ func (cmd *ReplayBufferToggleCmd) Run(ctx *context) error {
|
||||
}
|
||||
|
||||
if status.OutputActive {
|
||||
fmt.Fprintln(ctx.Out, "Replay buffer started successfully.")
|
||||
fmt.Fprintln(ctx.Out, "Replay buffer started.")
|
||||
} else {
|
||||
fmt.Fprintln(ctx.Out, "Replay buffer stopped successfully.")
|
||||
fmt.Fprintln(ctx.Out, "Replay buffer stopped.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
85
replaybuffer_test.go
Normal file
85
replaybuffer_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReplayBufferStart(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &ReplayBufferStartCmd{}
|
||||
err := cmd.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start replay buffer: %v", err)
|
||||
}
|
||||
if out.String() != "Replay buffer started.\n" {
|
||||
t.Fatalf("Expected output to be 'Replay buffer started', got '%s'", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayBufferStop(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &ReplayBufferStopCmd{}
|
||||
err := cmd.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stop replay buffer: %v", err)
|
||||
}
|
||||
if out.String() != "Replay buffer stopped.\n" {
|
||||
t.Fatalf("Expected output to be 'Replay buffer stopped.', got '%s'", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayBufferToggle(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmdStatus := &ReplayBufferStatusCmd{}
|
||||
err := cmdStatus.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get replay buffer status: %v", err)
|
||||
}
|
||||
var active bool
|
||||
if strings.Contains(out.String(), "Replay buffer is active") {
|
||||
active = true
|
||||
}
|
||||
// Reset output buffer for the next command
|
||||
out.Reset()
|
||||
|
||||
cmdToggle := &ReplayBufferToggleCmd{}
|
||||
err = cmdToggle.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to toggle replay buffer: %v", err)
|
||||
}
|
||||
if active {
|
||||
if out.String() != "Replay buffer stopped.\n" {
|
||||
t.Fatalf("Expected output to be 'Replay buffer stopped.', got '%s'", out.String())
|
||||
}
|
||||
} else {
|
||||
if out.String() != "Replay buffer started.\n" {
|
||||
t.Fatalf("Expected output to be 'Replay buffer started.', got '%s'", out.String())
|
||||
}
|
||||
}
|
||||
}
|
30
version_test.go
Normal file
30
version_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
client, disconnect := getClient(t)
|
||||
defer disconnect()
|
||||
|
||||
var out bytes.Buffer
|
||||
context := &context{
|
||||
Client: client,
|
||||
Out: &out,
|
||||
}
|
||||
|
||||
cmd := &VersionCmd{}
|
||||
err := cmd.Run(context)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get version: %v", err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "OBS Client Version:") {
|
||||
t.Fatalf("Expected output to contain 'OBS Client Version:', got '%s'", out.String())
|
||||
}
|
||||
if !strings.Contains(out.String(), "with Websocket Version:") {
|
||||
t.Fatalf("Expected output to contain 'with Websocket Version:', got '%s'", out.String())
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user