Compare commits

...

17 Commits

Author SHA1 Message Date
17b8e53da3 read version from build info if version was not injected at build time (go install)
inject version from tag at build time for local builds
2025-06-03 16:35:19 +01:00
92761ab1b3 upd link to --version 2025-06-03 13:12:22 +01:00
4446784709 add short names for root options
fix flag example in readme
2025-06-03 12:33:37 +01:00
89a5add7ad upd VersionCmd test 2025-06-02 18:12:43 +01:00
878ecbd33e add 0.9.0 to CHANGELOG 2025-06-02 18:12:06 +01:00
18a90e727f define main.version in local builds 2025-06-02 18:11:56 +01:00
95ebb2afb6 add VersionFlag to CLI struct
upd VersionCmd struct
2025-06-02 18:11:37 +01:00
666b4cf744 add VersionFlag
upd VersionCmd
2025-06-02 18:10:07 +01:00
9ee6fa9e34 typo 2025-05-29 20:03:37 +01:00
e5223fbdfd add 0.8.2 to CHANGELOG 2025-05-29 15:31:53 +01:00
c22ab4384d upd README to reflect changes to --parent option 2025-05-29 15:28:21 +01:00
93a3d3e49f print a more useful sceneitem list table
rename --parent option to --group
2025-05-29 15:28:02 +01:00
2228574837 fix err message test 2025-05-28 15:50:02 +01:00
8f1d42b677 ensure studio mode disabled at end of tests 2025-05-28 15:39:12 +01:00
620adf7e98 return errors if required
upd tests to reflect changes
2025-05-28 15:38:59 +01:00
4a7b8a074a check current active state before starting/stopping recording
return appropriate errors if required

update tests to reflect changes
2025-05-28 15:33:53 +01:00
0811d711aa split record start/stop tests
test output according to current active state
2025-05-28 14:37:55 +01:00
12 changed files with 264 additions and 123 deletions

View File

@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [0.9.0]
### Added
- --version/-v option. See [Flags](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#flags)
### Changed
- version command renamed to obs-version
# [0.8.2]
### Added
- record start/stop and stream start/stop commands check outputActive states first.
- Errors are returned if the command cannot be performed.
### Changed
- The --parent flag for the sceneitem commands has been renamed to --group, see [SceneItemCmd](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#sceneitemcmd)
# [0.8.0] - 2025-05-27 # [0.8.0] - 2025-05-27
### Added ### Added

View File

@@ -14,10 +14,16 @@ go install github.com/onyx-and-iris/gobs-cli@latest
#### Flags #### Flags
Pass `--host`, `--port` and `--password` as flags to the root command, for example: - --host/-H: Websocket host
- --port/-P Websocket port
- --password/-p: Websocket password
- --timeout/-T: Websocket timeout
- --version/-v: Print the gobs-cli version
Pass `--host`, `--port` and `--password` as flags on the root command, for example:
```console ```console
gobs-cli --host=localhost --port=4455 --password=<websocket password> --help gobs-cli --host localhost --port 4455 --password 'websocket password' --help
``` ```
#### Environment Variables #### Environment Variables
@@ -37,10 +43,12 @@ OBS_TIMEOUT=5
## Commands ## Commands
### VersionCmd ### ObsVersionCmd
- Print OBS client and websocket version.
```console ```console
gobs-cli version gobs-cli obs-version
``` ```
### SceneCmd ### SceneCmd
@@ -94,7 +102,7 @@ gobs-cli sceneitem list LIVE
- flags: - flags:
*optional* *optional*
- --parent: Parent group name. - --group: Parent group name.
- args: SceneName ItemName - args: SceneName ItemName
```console ```console
@@ -105,7 +113,7 @@ gobs-cli sceneitem show START "Colour Source"
- flags: - flags:
*optional* *optional*
- --parent: Parent group name. - --group: Parent group name.
- args: SceneName ItemName - args: SceneName ItemName
```console ```console
@@ -116,29 +124,29 @@ gobs-cli sceneitem hide START "Colour Source"
- flags: - flags:
*optional* *optional*
- --parent: Parent group name. - --group: Parent group name.
- args: SceneName ItemName - args: SceneName ItemName
```console ```console
gobs-cli sceneitem toggle --parent=test_group START "Colour Source 3" gobs-cli sceneitem toggle --group=test_group START "Colour Source 3"
``` ```
- visible: Get scene item visibility. - visible: Get scene item visibility.
- flags: - flags:
*optional* *optional*
- --parent: Parent group name. - --group: Parent group name.
- args: SceneName ItemName - args: SceneName ItemName
```console ```console
gobs-cli sceneitem visible --parent=test_group START "Colour Source 4" gobs-cli sceneitem visible --group=test_group START "Colour Source 4"
``` ```
- transform: Transform scene item. - transform: Transform scene item.
- flags: - flags:
*optional* *optional*
- --parent: Parent group name. - --group: Parent group name.
- --alignment: Alignment of the scene item. - --alignment: Alignment of the scene item.
- --bounds-alignment: Bounds alignment of the scene item. - --bounds-alignment: Bounds alignment of the scene item.
@@ -557,7 +565,7 @@ gobs-cli projector list-monitors
- defaults to current scene - defaults to current scene
```console ```console
gobs-cli project open gobs-cli projector open
gobs-cli projector open --monitor-index=1 "test_scene" gobs-cli projector open --monitor-index=1 "test_scene"

View File

@@ -7,6 +7,8 @@ vars:
PROGRAM: gobs-cli PROGRAM: gobs-cli
SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}' SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}'
BIN_DIR: bin BIN_DIR: bin
VERSION:
sh: 'git describe --tags $(git rev-list --tags --max-count=1)'
tasks: tasks:
default: default:
@@ -35,13 +37,13 @@ tasks:
build-windows: build-windows:
desc: Build the gobs-cli project for Windows desc: Build the gobs-cli project for Windows
cmds: cmds:
- GOOS=windows GOARCH=amd64 go build -o {{.BIN_DIR}}/{{.PROGRAM}}_windows_amd64.exe - GOOS=windows GOARCH=amd64 go build -ldflags "-X 'main.version={{.VERSION}}'" -o {{.BIN_DIR}}/{{.PROGRAM}}_windows_amd64.exe
internal: true internal: true
build-linux: build-linux:
desc: Build the gobs-cli project for Linux desc: Build the gobs-cli project for Linux
cmds: cmds:
- GOOS=linux GOARCH=amd64 go build -o {{.BIN_DIR}}/{{.PROGRAM}}_linux_amd64 - GOOS=linux GOARCH=amd64 go build -ldflags "-X 'main.version={{.VERSION}}'" -o {{.BIN_DIR}}/{{.PROGRAM}}_linux_amd64
internal: true internal: true
test: test:

41
main.go
View File

@@ -18,10 +18,10 @@ import (
// ObsConfig holds the configuration for connecting to the OBS WebSocket server. // ObsConfig holds the configuration for connecting to the OBS WebSocket server.
type ObsConfig struct { type ObsConfig struct {
Host string `flag:"host" help:"Host to connect to." default:"localhost" env:"OBS_HOST"` Host string `flag:"host" help:"Host to connect to." default:"localhost" env:"OBS_HOST" short:"H"`
Port int `flag:"port" help:"Port to connect to." default:"4455" env:"OBS_PORT"` Port int `flag:"port" help:"Port to connect to." default:"4455" env:"OBS_PORT" short:"P"`
Password string `flag:"password" help:"Password for authentication." default:"" env:"OBS_PASSWORD"` Password string `flag:"password" help:"Password for authentication." default:"" env:"OBS_PASSWORD" short:"p"`
Timeout int `flag:"timeout" help:"Timeout in seconds." default:"5" env:"OBS_TIMEOUT"` Timeout int `flag:"timeout" help:"Timeout in seconds." default:"5" env:"OBS_TIMEOUT" short:"T"`
} }
// CLI is the main command line interface structure. // CLI is the main command line interface structure.
@@ -29,23 +29,24 @@ type ObsConfig struct {
type CLI struct { type CLI struct {
ObsConfig `embed:"" help:"OBS WebSocket configuration."` ObsConfig `embed:"" help:"OBS WebSocket configuration."`
Man mangokong.ManFlag `help:"Print man page."` Man mangokong.ManFlag `help:"Print man page."`
Version VersionFlag `help:"Print gobs-cli version information and quit" name:"version" short:"v"`
Version VersionCmd `help:"Show version." cmd:"" aliases:"v"` ObsVersion ObsVersionCmd `help:"Print OBS client and websocket version." cmd:"" aliases:"v"`
Scene SceneCmd `help:"Manage scenes." cmd:"" aliases:"sc"` Scene SceneCmd `help:"Manage scenes." cmd:"" aliases:"sc"`
Sceneitem SceneItemCmd `help:"Manage scene items." cmd:"" aliases:"si"` Sceneitem SceneItemCmd `help:"Manage scene items." cmd:"" aliases:"si"`
Group GroupCmd `help:"Manage groups." cmd:"" aliases:"g"` Group GroupCmd `help:"Manage groups." cmd:"" aliases:"g"`
Input InputCmd `help:"Manage inputs." cmd:"" aliases:"i"` Input InputCmd `help:"Manage inputs." cmd:"" aliases:"i"`
Record RecordCmd `help:"Manage recording." cmd:"" aliases:"rec"` Record RecordCmd `help:"Manage recording." cmd:"" aliases:"rec"`
Stream StreamCmd `help:"Manage streaming." cmd:"" aliases:"st"` Stream StreamCmd `help:"Manage streaming." cmd:"" aliases:"st"`
Scenecollection SceneCollectionCmd `help:"Manage scene collections." cmd:"" aliases:"scn"` Scenecollection SceneCollectionCmd `help:"Manage scene collections." cmd:"" aliases:"scn"`
Profile ProfileCmd `help:"Manage profiles." cmd:"" aliases:"p"` Profile ProfileCmd `help:"Manage profiles." cmd:"" aliases:"p"`
Replaybuffer ReplayBufferCmd `help:"Manage replay buffer." cmd:"" aliases:"rb"` Replaybuffer ReplayBufferCmd `help:"Manage replay buffer." cmd:"" aliases:"rb"`
Studiomode StudioModeCmd `help:"Manage studio mode." cmd:"" aliases:"sm"` Studiomode StudioModeCmd `help:"Manage studio mode." cmd:"" aliases:"sm"`
Virtualcam VirtualCamCmd `help:"Manage virtual camera." cmd:"" aliases:"vc"` Virtualcam VirtualCamCmd `help:"Manage virtual camera." cmd:"" aliases:"vc"`
Hotkey HotkeyCmd `help:"Manage hotkeys." cmd:"" aliases:"hk"` Hotkey HotkeyCmd `help:"Manage hotkeys." cmd:"" aliases:"hk"`
Filter FilterCmd `help:"Manage filters." cmd:"" aliases:"f"` Filter FilterCmd `help:"Manage filters." cmd:"" aliases:"f"`
Projector ProjectorCmd `help:"Manage projectors." cmd:"" aliases:"prj"` Projector ProjectorCmd `help:"Manage projectors." cmd:"" aliases:"prj"`
} }
type context struct { type context struct {

View File

@@ -9,6 +9,7 @@ import (
"github.com/andreykaipov/goobs/api/requests/filters" "github.com/andreykaipov/goobs/api/requests/filters"
"github.com/andreykaipov/goobs/api/requests/inputs" "github.com/andreykaipov/goobs/api/requests/inputs"
"github.com/andreykaipov/goobs/api/requests/scenes" "github.com/andreykaipov/goobs/api/requests/scenes"
"github.com/andreykaipov/goobs/api/requests/ui"
typedefs "github.com/andreykaipov/goobs/api/typedefs" typedefs "github.com/andreykaipov/goobs/api/typedefs"
) )
@@ -130,4 +131,6 @@ func teardown(client *goobs.Client) {
client.Stream.StopStream() client.Stream.StopStream()
client.Record.StopRecord() client.Record.StopRecord()
client.Outputs.StopReplayBuffer() client.Outputs.StopReplayBuffer()
client.Ui.SetStudioModeEnabled(ui.NewSetStudioModeEnabledParams().
WithStudioModeEnabled(false))
} }

View File

@@ -22,7 +22,19 @@ type RecordStartCmd struct{} // size = 0x0
// Run executes the command to start recording. // Run executes the command to start recording.
func (cmd *RecordStartCmd) Run(ctx *context) error { func (cmd *RecordStartCmd) Run(ctx *context) error {
_, err := ctx.Client.Record.StartRecord() status, err := ctx.Client.Record.GetRecordStatus()
if err != nil {
return err
}
if status.OutputActive {
if status.OutputPaused {
return fmt.Errorf("recording is already in progress and paused")
}
return fmt.Errorf("recording is already in progress")
}
_, err = ctx.Client.Record.StartRecord()
if err != nil { if err != nil {
return err return err
} }
@@ -35,6 +47,15 @@ type RecordStopCmd struct{} // size = 0x0
// Run executes the command to stop recording. // Run executes the command to stop recording.
func (cmd *RecordStopCmd) Run(ctx *context) error { func (cmd *RecordStopCmd) Run(ctx *context) error {
status, err := ctx.Client.Record.GetRecordStatus()
if err != nil {
return err
}
if !status.OutputActive {
return fmt.Errorf("recording is not in progress")
}
resp, err := ctx.Client.Record.StopRecord() resp, err := ctx.Client.Record.StopRecord()
if err != nil { if err != nil {
return err return err

View File

@@ -7,7 +7,7 @@ import (
"time" "time"
) )
func TestRecordStartStatusStop(t *testing.T) { func TestRecordStart(t *testing.T) {
client, disconnect := getClient(t) client, disconnect := getClient(t)
defer disconnect() defer disconnect()
@@ -17,51 +17,80 @@ func TestRecordStartStatusStop(t *testing.T) {
Out: &out, Out: &out,
} }
cmdStart := &RecordStartCmd{} cmdStatus := &RecordStatusCmd{}
err := cmdStart.Run(context) err := cmdStatus.Run(context)
if err != nil { if err != nil {
t.Fatalf("Failed to start recording: %v", err) t.Fatalf("Failed to get recording status: %v", err)
} }
if out.String() != "Recording started successfully.\n" { var active bool
t.Fatalf("Expected output to be 'Recording started successfully.', got '%s'", out.String()) if out.String() == "Recording is in progress.\n" {
active = true
} }
// Reset output buffer for the next command // Reset output buffer for the next command
out.Reset() out.Reset()
time.Sleep(1 * time.Second) // Wait for a second to ensure recording has started cmdStart := &RecordStartCmd{}
err = cmdStart.Run(context)
if active {
if err == nil {
t.Fatalf("Expected error when starting recording while active, got nil")
}
if !strings.Contains(err.Error(), "recording is already in progress") {
t.Fatalf("Expected error message to contain 'recording is already in progress', got '%s'", err.Error())
}
return
}
if err != nil {
t.Fatalf("Failed to start recording: %v", err)
}
if out.String() != "Recording started successfully.\n" {
t.Fatalf("Expected output to contain 'Recording started successfully.', got '%s'", out.String())
}
time.Sleep(1 * time.Second) // Wait for the recording to start
}
func TestRecordStop(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdStatus := &RecordStatusCmd{} cmdStatus := &RecordStatusCmd{}
err = cmdStatus.Run(context) err := cmdStatus.Run(context)
if err != nil { if err != nil {
t.Fatalf("Failed to get recording status: %v", err) t.Fatalf("Failed to get recording status: %v", err)
} }
if out.String() != "Recording is in progress.\n" { var active bool
t.Fatalf("Expected output to be 'Recording is in progress.', got '%s'", out.String()) if out.String() == "Recording is in progress.\n" {
active = true
} }
// Reset output buffer for the next command // Reset output buffer for the next command
out.Reset() out.Reset()
cmdStop := &RecordStopCmd{} cmdStop := &RecordStopCmd{}
err = cmdStop.Run(context) err = cmdStop.Run(context)
if !active {
if err == nil {
t.Fatalf("Expected error when stopping recording while inactive, got nil")
}
if !strings.Contains(err.Error(), "recording is not in progress") {
t.Fatalf("Expected error message to contain 'recording is not in progress', got '%s'", err.Error())
}
return
}
if err != nil { if err != nil {
t.Fatalf("Failed to stop recording: %v", err) t.Fatalf("Failed to stop recording: %v", err)
} }
if !strings.Contains(out.String(), "Recording stopped successfully. Output file:") { if !strings.Contains(out.String(), "Recording stopped successfully. Output file: ") {
t.Fatalf("Expected output to be 'Recording stopped successfully.', got '%s'", out.String()) t.Fatalf("Expected output to contain 'Recording stopped successfully. Output file: ', got '%s'", out.String())
}
// Reset output buffer for the next command
out.Reset()
time.Sleep(1 * time.Second) // Wait for a second to ensure recording has stopped
cmdStatus = &RecordStatusCmd{}
err = cmdStatus.Run(context)
if err != nil {
t.Fatalf("Failed to get recording status: %v", err)
}
if out.String() != "Recording is not in progress.\n" {
t.Fatalf("Expected output to be 'Recording is not in progress.', got '%s'", out.String())
} }
time.Sleep(1 * time.Second) // Wait for the recording to stop
} }
func TestRecordToggle(t *testing.T) { func TestRecordToggle(t *testing.T) {

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"sort"
"github.com/andreykaipov/goobs" "github.com/andreykaipov/goobs"
"github.com/andreykaipov/goobs/api/requests/sceneitems" "github.com/andreykaipov/goobs/api/requests/sceneitems"
@@ -46,31 +47,57 @@ func (cmd *SceneItemListCmd) Run(ctx *context) error {
t := table.New(ctx.Out) t := table.New(ctx.Out)
t.SetPadding(3) t.SetPadding(3)
t.SetAlignment(table.AlignLeft) t.SetAlignment(table.AlignCenter, table.AlignLeft, table.AlignCenter, table.AlignCenter)
t.SetHeaders("Item Name") t.SetHeaders("Item ID", "Item Name", "In Group", "Enabled")
sort.Slice(resp.SceneItems, func(i, j int) bool {
return resp.SceneItems[i].SceneItemID < resp.SceneItems[j].SceneItemID
})
for _, item := range resp.SceneItems { for _, item := range resp.SceneItems {
t.AddRow(item.SourceName) if item.IsGroup {
resp, err := ctx.Client.SceneItems.GetGroupSceneItemList(sceneitems.NewGetGroupSceneItemListParams().
WithSceneName(item.SourceName))
if err != nil {
return fmt.Errorf("failed to get group scene item list for '%s': %w", item.SourceName, err)
}
sort.Slice(resp.SceneItems, func(i, j int) bool {
return resp.SceneItems[i].SceneItemID < resp.SceneItems[j].SceneItemID
})
for _, groupItem := range resp.SceneItems {
t.AddRow(
fmt.Sprintf("%d", groupItem.SceneItemID),
groupItem.SourceName,
item.SourceName,
fmt.Sprintf("%t", item.SceneItemEnabled && groupItem.SceneItemEnabled),
)
}
} else {
t.AddRow(fmt.Sprintf("%d", item.SceneItemID), item.SourceName, "", fmt.Sprintf("%t", item.SceneItemEnabled))
}
} }
t.Render() t.Render()
return nil return nil
} }
// getSceneNameAndItemID retrieves the scene name and item ID for a given item in a scene or group.
func getSceneNameAndItemID( func getSceneNameAndItemID(
client *goobs.Client, client *goobs.Client,
sceneName string, sceneName string,
itemName string, itemName string,
parent string, group string,
) (string, int, error) { ) (string, int, error) {
if parent != "" { if group != "" {
resp, err := client.SceneItems.GetGroupSceneItemList(sceneitems.NewGetGroupSceneItemListParams(). resp, err := client.SceneItems.GetGroupSceneItemList(sceneitems.NewGetGroupSceneItemListParams().
WithSceneName(parent)) WithSceneName(group))
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
for _, item := range resp.SceneItems { for _, item := range resp.SceneItems {
if item.SourceName == itemName { if item.SourceName == itemName {
return parent, int(item.SceneItemID), nil return group, int(item.SceneItemID), nil
} }
} }
return "", 0, fmt.Errorf("item '%s' not found in scene '%s'", itemName, sceneName) return "", 0, fmt.Errorf("item '%s' not found in scene '%s'", itemName, sceneName)
@@ -87,7 +114,7 @@ func getSceneNameAndItemID(
// SceneItemShowCmd provides a command to show a scene item. // SceneItemShowCmd provides a command to show a scene item.
type SceneItemShowCmd struct { type SceneItemShowCmd struct {
Parent string `flag:"" help:"Parent group name."` Group string `flag:"" help:"Parent group name."`
SceneName string `arg:"" help:"Scene name."` SceneName string `arg:"" help:"Scene name."`
ItemName string `arg:"" help:"Item name."` ItemName string `arg:"" help:"Item name."`
@@ -95,7 +122,7 @@ type SceneItemShowCmd struct {
// Run executes the command to show a scene item. // Run executes the command to show a scene item.
func (cmd *SceneItemShowCmd) Run(ctx *context) error { func (cmd *SceneItemShowCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent) sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil { if err != nil {
return err return err
} }
@@ -108,8 +135,8 @@ func (cmd *SceneItemShowCmd) Run(ctx *context) error {
return err return err
} }
if cmd.Parent != "" { if cmd.Group != "" {
fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' is now visible.\n", cmd.ItemName, cmd.Parent) fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' is now visible.\n", cmd.ItemName, cmd.Group)
} else { } else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now visible.\n", cmd.ItemName, cmd.SceneName) fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now visible.\n", cmd.ItemName, cmd.SceneName)
} }
@@ -119,7 +146,7 @@ func (cmd *SceneItemShowCmd) Run(ctx *context) error {
// SceneItemHideCmd provides a command to hide a scene item. // SceneItemHideCmd provides a command to hide a scene item.
type SceneItemHideCmd struct { type SceneItemHideCmd struct {
Parent string `flag:"" help:"Parent group name."` Group string `flag:"" help:"Parent group name."`
SceneName string `arg:"" help:"Scene name."` SceneName string `arg:"" help:"Scene name."`
ItemName string `arg:"" help:"Item name."` ItemName string `arg:"" help:"Item name."`
@@ -127,7 +154,7 @@ type SceneItemHideCmd struct {
// Run executes the command to hide a scene item. // Run executes the command to hide a scene item.
func (cmd *SceneItemHideCmd) Run(ctx *context) error { func (cmd *SceneItemHideCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent) sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil { if err != nil {
return err return err
} }
@@ -140,8 +167,8 @@ func (cmd *SceneItemHideCmd) Run(ctx *context) error {
return err return err
} }
if cmd.Parent != "" { if cmd.Group != "" {
fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' is now hidden.\n", cmd.ItemName, cmd.Parent) fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' is now hidden.\n", cmd.ItemName, cmd.Group)
} else { } else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now hidden.\n", cmd.ItemName, cmd.SceneName) fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now hidden.\n", cmd.ItemName, cmd.SceneName)
} }
@@ -162,7 +189,7 @@ func getItemEnabled(client *goobs.Client, sceneName string, itemID int) (bool, e
// SceneItemToggleCmd provides a command to toggle the visibility of a scene item. // SceneItemToggleCmd provides a command to toggle the visibility of a scene item.
type SceneItemToggleCmd struct { type SceneItemToggleCmd struct {
Parent string `flag:"" help:"Parent group name."` Group string `flag:"" help:"Parent group name."`
SceneName string `arg:"" help:"Scene name."` SceneName string `arg:"" help:"Scene name."`
ItemName string `arg:"" help:"Item name."` ItemName string `arg:"" help:"Item name."`
@@ -170,7 +197,7 @@ type SceneItemToggleCmd struct {
// Run executes the command to toggle the visibility of a scene item. // Run executes the command to toggle the visibility of a scene item.
func (cmd *SceneItemToggleCmd) Run(ctx *context) error { func (cmd *SceneItemToggleCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent) sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil { if err != nil {
return err return err
} }
@@ -199,7 +226,7 @@ func (cmd *SceneItemToggleCmd) Run(ctx *context) error {
// SceneItemVisibleCmd provides a command to check the visibility of a scene item. // SceneItemVisibleCmd provides a command to check the visibility of a scene item.
type SceneItemVisibleCmd struct { type SceneItemVisibleCmd struct {
Parent string `flag:"" help:"Parent group name."` Group string `flag:"" help:"Parent group name."`
SceneName string `arg:"" help:"Scene name."` SceneName string `arg:"" help:"Scene name."`
ItemName string `arg:"" help:"Item name."` ItemName string `arg:"" help:"Item name."`
@@ -207,7 +234,7 @@ type SceneItemVisibleCmd struct {
// Run executes the command to check the visibility of a scene item. // Run executes the command to check the visibility of a scene item.
func (cmd *SceneItemVisibleCmd) Run(ctx *context) error { func (cmd *SceneItemVisibleCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent) sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil { if err != nil {
return err return err
} }
@@ -230,7 +257,7 @@ type SceneItemTransformCmd struct {
SceneName string `arg:"" help:"Scene name."` SceneName string `arg:"" help:"Scene name."`
ItemName string `arg:"" help:"Item name."` ItemName string `arg:"" help:"Item name."`
Parent string `flag:"" help:"Parent group name."` Group string `flag:"" help:"Parent group name."`
Alignment float64 `flag:"" help:"Alignment of the scene item."` Alignment float64 `flag:"" help:"Alignment of the scene item."`
BoundsAlignment float64 `flag:"" help:"Bounds alignment of the scene item."` BoundsAlignment float64 `flag:"" help:"Bounds alignment of the scene item."`
@@ -251,7 +278,7 @@ type SceneItemTransformCmd struct {
// Run executes the command to transform a scene item. // Run executes the command to transform a scene item.
func (cmd *SceneItemTransformCmd) Run(ctx *context) error { func (cmd *SceneItemTransformCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent) sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil { if err != nil {
return err return err
} }
@@ -323,8 +350,8 @@ func (cmd *SceneItemTransformCmd) Run(ctx *context) error {
return err return err
} }
if cmd.Parent != "" { if cmd.Group != "" {
fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' transformed.\n", cmd.ItemName, cmd.Parent) fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' transformed.\n", cmd.ItemName, cmd.Group)
} else { } else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' transformed.\n", cmd.ItemName, cmd.SceneName) fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' transformed.\n", cmd.ItemName, cmd.SceneName)
} }

View File

@@ -23,8 +23,7 @@ func (cmd *StreamStartCmd) Run(ctx *context) error {
return err return err
} }
if status.OutputActive { if status.OutputActive {
fmt.Fprintln(ctx.Out, "Stream is already active.") return fmt.Errorf("stream is already in progress")
return nil
} }
_, err = ctx.Client.Stream.StartStream() _, err = ctx.Client.Stream.StartStream()
@@ -32,7 +31,7 @@ func (cmd *StreamStartCmd) Run(ctx *context) error {
return err return err
} }
fmt.Fprintln(ctx.Out, "Streaming started successfully.") fmt.Fprintln(ctx.Out, "Stream started successfully.")
return nil return nil
} }
@@ -47,8 +46,7 @@ func (cmd *StreamStopCmd) Run(ctx *context) error {
return err return err
} }
if !status.OutputActive { if !status.OutputActive {
fmt.Fprintln(ctx.Out, "Stream is already inactive.") return fmt.Errorf("stream is not in progress")
return nil
} }
_, err = ctx.Client.Stream.StopStream() _, err = ctx.Client.Stream.StopStream()
@@ -56,7 +54,7 @@ func (cmd *StreamStopCmd) Run(ctx *context) error {
return err return err
} }
fmt.Fprintln(ctx.Out, "Streaming stopped successfully.") fmt.Fprintln(ctx.Out, "Stream stopped successfully.")
return nil return nil
} }
@@ -71,9 +69,9 @@ func (cmd *StreamToggleCmd) Run(ctx *context) error {
} }
if status.OutputActive { if status.OutputActive {
fmt.Fprintln(ctx.Out, "Streaming started successfully.") fmt.Fprintln(ctx.Out, "Stream started successfully.")
} else { } else {
fmt.Fprintln(ctx.Out, "Streaming stopped successfully.") fmt.Fprintln(ctx.Out, "Stream stopped successfully.")
} }
return nil return nil
} }

View File

@@ -31,21 +31,22 @@ func TestStreamStart(t *testing.T) {
cmdStart := &StreamStartCmd{} cmdStart := &StreamStartCmd{}
err = cmdStart.Run(context) err = cmdStart.Run(context)
if active {
if err == nil {
t.Fatalf("Expected error when starting stream while active, got nil")
}
if !strings.Contains(err.Error(), "stream is already in progress") {
t.Fatalf("Expected error message to contain 'stream is already in progress', got '%s'", err.Error())
}
return
}
if err != nil { if err != nil {
t.Fatalf("Failed to start stream: %v", err) t.Fatalf("Failed to start stream: %v", err)
} }
if out.String() != "Stream started successfully.\n" {
time.Sleep(1 * time.Second) // Wait for the stream to start t.Fatalf("Expected output to contain 'Stream started successfully.', got '%s'", out.String())
if active {
if out.String() != "Stream is already active.\n" {
t.Fatalf("Expected 'Stream is already active.', got: %s", out.String())
}
} else {
if out.String() != "Streaming started successfully.\n" {
t.Fatalf("Expected 'Streaming started successfully.', got: %s", out.String())
}
} }
time.Sleep(2 * time.Second) // Wait for the stream to start
} }
func TestStreamStop(t *testing.T) { func TestStreamStop(t *testing.T) {
@@ -72,21 +73,22 @@ func TestStreamStop(t *testing.T) {
cmdStop := &StreamStopCmd{} cmdStop := &StreamStopCmd{}
err = cmdStop.Run(context) err = cmdStop.Run(context)
if !active {
if err == nil {
t.Fatalf("Expected error when stopping stream while inactive, got nil")
}
if !strings.Contains(err.Error(), "stream is not in progress") {
t.Fatalf("Expected error message to contain 'stream is not in progress', got '%s'", err.Error())
}
return
}
if err != nil { if err != nil {
t.Fatalf("Failed to stop stream: %v", err) t.Fatalf("Failed to stop stream: %v", err)
} }
if out.String() != "Stream stopped successfully.\n" {
time.Sleep(1 * time.Second) // Wait for the stream to stop t.Fatalf("Expected output to contain 'Stream stopped successfully.', got '%s'", out.String())
if active {
if out.String() != "Streaming stopped successfully.\n" {
t.Fatalf("Expected 'Streaming stopped successfully.', got: %s", out.String())
}
} else {
if out.String() != "Stream is already inactive.\n" {
t.Fatalf("Expected 'Stream is already inactive.', got: %s", out.String())
}
} }
time.Sleep(2 * time.Second) // Wait for the stream to stop
} }
func TestStreamToggle(t *testing.T) { func TestStreamToggle(t *testing.T) {
@@ -117,15 +119,14 @@ func TestStreamToggle(t *testing.T) {
t.Fatalf("Failed to toggle stream: %v", err) t.Fatalf("Failed to toggle stream: %v", err)
} }
time.Sleep(1 * time.Second) // Wait for the stream to toggle
if active { if active {
if out.String() != "Streaming stopped successfully.\n" { if out.String() != "Stream stopped successfully.\n" {
t.Fatalf("Expected 'Streaming stopped successfully.', got: %s", out.String()) t.Fatalf("Expected 'Stream stopped successfully.', got: %s", out.String())
} }
} else { } else {
if out.String() != "Streaming started successfully.\n" { if out.String() != "Stream started successfully.\n" {
t.Fatalf("Expected 'Streaming started successfully.', got: %s", out.String()) t.Fatalf("Expected 'Stream started successfully.', got: %s", out.String())
} }
} }
time.Sleep(2 * time.Second) // Wait for the stream to toggle
} }

View File

@@ -2,13 +2,43 @@ package main
import ( import (
"fmt" "fmt"
"runtime/debug"
"strings"
"github.com/alecthomas/kong"
) )
// VersionCmd handles the version command. var version string
type VersionCmd struct{} // size = 0x0
// VersionFlag is a custom flag type for displaying version information.
type VersionFlag string
// Decode implements the kong.Flag interface for VersionFlag.
func (v VersionFlag) Decode(_ *kong.DecodeContext) error { return nil }
// IsBool implements the kong.Flag interface for VersionFlag.
func (v VersionFlag) IsBool() bool { return true }
// BeforeApply implements the kong.Flag interface for VersionFlag.
func (v VersionFlag) BeforeApply(app *kong.Kong, _ kong.Vars) error { // nolint: unparam
if version == "" {
info, ok := debug.ReadBuildInfo()
if !ok {
return fmt.Errorf("failed to read build info")
}
version = strings.Split(info.Main.Version, "-")[0]
}
fmt.Printf("gobs-cli version: %s\n", version)
app.Exit(0) // Exit the application after printing the version
return nil
}
// ObsVersionCmd handles the version command.
type ObsVersionCmd struct{} // size = 0x0
// Run executes the command to get the OBS client version. // Run executes the command to get the OBS client version.
func (cmd *VersionCmd) Run(ctx *context) error { func (cmd *ObsVersionCmd) Run(ctx *context) error {
version, err := ctx.Client.General.GetVersion() version, err := ctx.Client.General.GetVersion()
if err != nil { if err != nil {
return err return err

View File

@@ -16,7 +16,7 @@ func TestVersion(t *testing.T) {
Out: &out, Out: &out,
} }
cmd := &VersionCmd{} cmd := &ObsVersionCmd{}
err := cmd.Run(context) err := cmd.Run(context)
if err != nil { if err != nil {
t.Fatalf("Failed to get version: %v", err) t.Fatalf("Failed to get version: %v", err)