Compare commits

...

46 Commits

Author SHA1 Message Date
306f19eeae add 0.8.0 to CHANGELOG 2025-05-27 01:28:14 +01:00
43dd77ffdc record directory added to README 2025-05-27 01:27:50 +01:00
f94ac1ca0c record stop now prints output path of recording
record directory command added
2025-05-27 01:27:30 +01:00
c27a5ea6c5 add 0.7.0 section to CHANGELOG 2025-05-26 22:08:44 +01:00
af962a26cc add projector commands
add ProjectorCmd section to README
2025-05-26 22:08:33 +01:00
360d45aa47 create source filters on test setup
remove source filters, ensure replyabuffer is stopped on test teardown
2025-05-26 19:51:27 +01:00
3deb03cf32 add filter/input/version tests 2025-05-26 19:49:51 +01:00
f58b2dfeab add output to replaybuffer start/stop
add replaybuffer tests
2025-05-26 19:49:30 +01:00
6ad530ce2e virtualcam - print OutputActive status on toggle 2025-05-25 22:18:24 +01:00
d9d3c7c8b2 Merge pull request #1 from onyx-and-iris/dependabot/go_modules/golang.org/x/sys-0.1.0
Bump golang.org/x/sys from 0.0.0-20210615035016-665e8c7367d1 to 0.1.0
2025-05-25 15:18:07 +01:00
dependabot[bot]
f72e34adfb Bump golang.org/x/sys from 0.0.0-20210615035016-665e8c7367d1 to 0.1.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.0.0-20210615035016-665e8c7367d1 to 0.1.0.
- [Commits](https://github.com/golang/sys/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.1.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-25 14:17:44 +00:00
ccb3f59513 md fix 2025-05-25 15:09:41 +01:00
e3fd88cf92 upd CHANGELOG + README 2025-05-25 15:07:34 +01:00
12dfab5642 print list commands as tables 2025-05-25 15:07:13 +01:00
7a2765f72c fix link to filtercmd section 2025-05-25 11:58:19 +01:00
90aa5d4423 add filter commands
upd README, CHANGELOG
2025-05-25 11:55:45 +01:00
da010d67a0 uppercase CLI struct name 2025-05-22 16:41:01 +01:00
0c695298fd add 0.5.0 section to changelog 2025-05-22 09:48:34 +01:00
2f77fa1c54 add hotkey commands 2025-05-22 09:48:26 +01:00
eafc3312a5 add HotkeyCmd section 2025-05-22 09:46:59 +01:00
02541f9915 upd changelog 0.4.2 2025-05-08 05:13:08 +01:00
7fa43eb35c add unit tests 2025-05-08 05:04:03 +01:00
8aeb7cb183 add studiomode enable/disable output 2025-05-08 01:19:11 +01:00
6e25927bc1 add stream start/stop output 2025-05-08 01:18:58 +01:00
dd0bbfc0da add missing record status command
add replaybuffer toggle to readme
2025-05-08 00:47:15 +01:00
c04324d173 add 0.4.0 to changelog 2025-05-07 20:24:30 +01:00
36d0753bd9 use ToggleRecord()/ToggleStream methods 2025-05-07 20:17:26 +01:00
3095c0c49d add replaybuffer toggle command 2025-05-07 20:16:44 +01:00
53bbb58cfb keep struct names consistent 2025-05-07 20:16:31 +01:00
5f2fe05caa fix date 2025-05-02 13:33:29 +01:00
c653047c66 add 0.3.1 to changelog 2025-05-02 12:26:42 +01:00
30fabe8cfc add env file resolver 2025-05-02 12:24:46 +01:00
8cf969c906 add task to view man page
move man tasks into Taskfile.man.yaml
2025-04-29 18:35:58 +01:00
3540c60c4b reword 2025-04-29 16:59:08 +01:00
b2c5980b4a use task var 2025-04-29 16:54:51 +01:00
da1ef9f993 upd task name + outfile name 2025-04-29 16:50:45 +01:00
0a2c622645 add --man flag for generating manfile 2025-04-29 15:19:16 +01:00
cb973c09f5 add installation section to readme 2025-04-27 14:14:42 +01:00
4fa32bfb42 upd changelog 2025-04-27 13:30:44 +01:00
8616f3b486 add sceneitem transform
upd readme
2025-04-27 13:24:57 +01:00
05f13ab87a upd create alias 2025-04-26 16:16:33 +01:00
71300a416b typo 2025-04-26 14:03:30 +01:00
2fc2000b11 Create LICENSE 2025-04-24 16:20:24 +01:00
7692de752b remove err check, FatalIfErrorf will do that. 2025-04-24 12:56:43 +01:00
107d1bca38 remove redundant var initialization 2025-04-24 12:54:53 +01:00
035b467a14 fix profile examples 2025-04-24 12:22:59 +01:00
37 changed files with 2094 additions and 124 deletions

9
.gitignore vendored
View File

@@ -26,5 +26,12 @@ go.work
# End of gignore: github.com/onyx-and-iris/gignore # End of gignore: github.com/onyx-and-iris/gignore
# Environment
.env
.envrc .envrc
*_test.go
# Man pages
gobs-cli.1
# Config files
config.yaml

View File

@@ -50,3 +50,6 @@ issues:
exclude: exclude:
# gosec: Duplicated errcheck checks # gosec: Duplicated errcheck checks
- G104 - G104
exclude-files:
# Exclude vendor directory
- main_test.go

View File

@@ -5,8 +5,65 @@ 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.8.0] - 2025-05-27
### Added
- record directory command, see [directory under RecordCmd](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#recordcmd)
### Changed
- record stop now prints the output path of the recording.
# [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 [FilterCmd](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#filtercmd)
### Changed
- list commands are now printed as tables.
- This affects group, hotkey, input, profile, scene, scenecollection and sceneitem command groups.
# [0.5.0] - 2025-05-22
### Added
- hotkey commands, see [HotkeyCmd](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#hotkeycmd)
# [0.4.2] - 2025-05-08
### Added
- replaybuffer toggle command
- studiomode enable/disable now print output to console
- stream start/stop now print output to console
- Unit tests
# [0.3.1] - 2025-05-02
### Added
- --man flag for generating/viewing a man page.
- Ability to load env vars from env files, see the [README](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#environment-variables)
# [0.2.0] - 2025-04-27
### Added
- sceneitem transform, see *transform* under [SceneItemCmd](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#sceneitemcmd)
# [0.1.0] - 2025-04-24 # [0.1.0] - 2025-04-24
### Added ### Added
- Initial release. - Initial release.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Onyx and Iris
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

192
README.md
View File

@@ -4,6 +4,12 @@ A command line interface for OBS Websocket v5
For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md) For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
## Installation
```console
go install github.com/onyx-and-iris/gobs-cli@latest
```
## Configuration ## Configuration
#### Flags #### Flags
@@ -16,17 +22,19 @@ gobs-cli --host=localhost --port=4455 --password=<websocket password> --help
#### Environment Variables #### Environment Variables
Load connection details from your environment: Store and load environment variables from:
```bash - A `.env` file in the cwd
#!/usr/bin/env bash - $XDG_CONFIG_HOME / gobs-cli / config.env (see [os.UserConfigDir][userconfigdir])
export OBS_HOST=localhost ```env
export OBS_PORT=4455 OBS_HOST=localhost
export OBS_PASSWORD=<websocket password> OBS_PORT=4455
export OBS_TIMEOUT=5 OBS_PASSWORD=<websocket password>
OBS_TIMEOUT=5
``` ```
## Commands ## Commands
### VersionCmd ### VersionCmd
@@ -71,9 +79,14 @@ gobs-cli scene switch --preview LIVE
### SceneItemCmd ### SceneItemCmd
- list: List all scene items. - list: List all scene items.
*optional*
- args: SceneName - args: SceneName
- defaults to current scene
```console ```console
gobs-cli sceneitem list
gobs-cli sceneitem list LIVE gobs-cli sceneitem list LIVE
``` ```
@@ -121,12 +134,47 @@ gobs-cli sceneitem toggle --parent=test_group START "Colour Source 3"
gobs-cli sceneitem visible --parent=test_group START "Colour Source 4" gobs-cli sceneitem visible --parent=test_group START "Colour Source 4"
``` ```
- transform: Transform scene item.
- flags:
*optional*
- --parent: Parent group name.
- --alignment: Alignment of the scene item.
- --bounds-alignment: Bounds alignment of the scene item.
- --bounds-height: Bounds height of the scene item.
- --bounds-type: Bounds type of the scene item.
- --bounds-width: Bounds width of the scene item.
- --crop-to-bounds: Whether to crop the scene item to bounds.
- --crop-bottom: Crop bottom value of the scene item.
- --crop-left: Crop left value of the scene item.
- --crop-right: Crop right value of the scene item.
- --crop-top: Crop top value of the scene item.
- --position-x: X position of the scene item.
- --position-y: Y position of the scene item.
- --rotation: Rotation of the scene item.
- --scale-x: X scale of the scene item.
- --scale-y: Y scale of the scene item.
- args: SceneName ItemName
```console
gobs-cli sceneitem transform \
--rotation=5 \
--position-x=250.8 \
Scene "Colour Source 3"
```
### GroupCmd ### GroupCmd
- list: List all groups. - list: List all groups.
*optional*
- args: SceneName - args: SceneName
- defaults to current scene
```console ```console
gobs-cli group list
gobs-cli group list START gobs-cli group list START
``` ```
@@ -233,6 +281,19 @@ gobs-cli record pause
gobs-cli record resume gobs-cli record resume
``` ```
- directory: Get/Set recording directory.
*optional*
- args: RecordDirectory
- if not passed the current record directory will be printed.
```console
gobs-cli record directory
gobs-cli record directory "/home/me/obs-vids/"
gobs-cli record directory "C:/Users/me/Videos"
```
### StreamCmd ### StreamCmd
- start: Start streaming. - start: Start streaming.
@@ -273,7 +334,7 @@ gobs-cli scenecollection list
gobs-cli scenecollection current gobs-cli scenecollection current
``` ```
- switch: "Switch scene collection. - switch: Switch scene collection.
- args: Name - args: Name
```console ```console
@@ -305,21 +366,21 @@ gobs-cli profile current
- args: Name - args: Name
```console ```console
gobs-cli profile switch test-collection gobs-cli profile switch test-profile
``` ```
- create: Create profile. - create: Create profile.
- args: Name - args: Name
```console ```console
gobs-cli profile create test-collection gobs-cli profile create test-profile
``` ```
- remove: Remove profile. - remove: Remove profile.
- args: Name - args: Name
```console ```console
gobs-cli profile create test-collection gobs-cli profile remove test-profile
``` ```
### ReplayBufferCmd ### ReplayBufferCmd
@@ -336,6 +397,12 @@ gobs-cli replaybuffer start
gobs-cli replaybuffer stop gobs-cli replaybuffer stop
``` ```
- toggle: Toggle replay buffer.
```console
gobs-cli replaybuffer toggle
```
- status: Get replay buffer status. - status: Get replay buffer status.
```console ```console
@@ -398,4 +465,105 @@ gobs-cli virtualcam toggle
```console ```console
gobs-cli virtualcam status gobs-cli virtualcam status
``` ```
### HotkeyCmd
- list: List all hotkeys.
```console
gobs-cli hotkey list
```
- trigger: Trigger a hotkey by name.
```console
gobs-cli hotkey trigger OBSBasic.StartStreaming
gobs-cli hotkey trigger OBSBasic.StopStreaming
```
- trigger-sequence: Trigger a hotkey by sequence.
- flags:
*optional*
- --shift: Press shift.
- --ctrl: Press control.
- --alt: Press alt.
- --cmd: Press command (mac).
- args: keyID
- Check [obs-hotkeys.h][obs-keyids] for a full list of OBS key ids.
```console
gobs-cli hotkey trigger-sequence OBS_KEY_F1 --ctrl
gobs-cli hotkey trigger-sequence OBS_KEY_F1 --shift --ctrl
```
### FilterCmd
- list: List all filters.
```console
gobs-cli filter list
```
- enable: Enable filter.
- args: SourceName FilterName
```console
gobs-cli enable 'Mic/Aux' 'Gain'
```
- disable: Disable filter.
- args: SourceName FilterName
```console
gobs-cli disable 'Mic/Aux' 'Gain'
```
- toggle: Toggle filter.
- args: SourceName FilterName
```console
gobs-cli toggle 'Mic/Aux' 'Gain'
```
- status: Get filter status.
- args: SourceName FilterName
```console
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

17
Taskfile.man.yaml Normal file
View File

@@ -0,0 +1,17 @@
version: '3'
tasks:
default:
desc: View man page
cmds:
- task: view
view:
desc: View man page
cmds:
- go run . --man | man -l -
generate:
desc: Generate man page
cmds:
- go run . --man > {{.PROGRAM}}.1

View File

@@ -1,5 +1,8 @@
version: '3' version: '3'
includes:
man: Taskfile.man.yaml
vars: vars:
PROGRAM: gobs-cli PROGRAM: gobs-cli
SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}' SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}'

176
filter.go Normal file
View File

@@ -0,0 +1,176 @@
package main
import (
"fmt"
"sort"
"strings"
"github.com/andreykaipov/goobs/api/requests/filters"
"github.com/aquasecurity/table"
)
// FilterCmd provides commands to manage filters in OBS Studio.
type FilterCmd struct {
List FilterListCmd `cmd:"" help:"List all filters." aliases:"ls"`
Enable FilterEnableCmd `cmd:"" help:"Enable filter." aliases:"on"`
Disable FilterDisableCmd `cmd:"" help:"Disable filter." aliases:"off"`
Toggle FilterToggleCmd `cmd:"" help:"Toggle filter." aliases:"tg"`
Status FilterStatusCmd `cmd:"" help:"Get filter status." aliases:"ss"`
}
// FilterListCmd provides a command to list all filters in a scene.
type FilterListCmd struct {
SourceName string `arg:"" help:"Name of the source to list filters from."`
}
// Run executes the command to list all filters in a scene.
func (cmd *FilterListCmd) Run(ctx *context) error {
filters, err := ctx.Client.Filters.GetSourceFilterList(
filters.NewGetSourceFilterListParams().WithSourceName(cmd.SourceName),
)
if err != nil {
return err
}
if len(filters.Filters) == 0 {
fmt.Fprintf(ctx.Out, "No filters found for source %s.\n", cmd.SourceName)
return nil
}
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignCenter, table.AlignLeft)
t.SetHeaders("Filter Name", "Kind", "Enabled", "Settings")
for _, filter := range filters.Filters {
var lines []string
for k, v := range filter.FilterSettings {
lines = append(lines, fmt.Sprintf("%s %v", k, v))
}
sort.Slice(lines, func(i, j int) bool {
return strings.ToLower(lines[i]) < strings.ToLower(lines[j])
})
t.AddRow(
filter.FilterName,
snakeCaseToTitleCase(filter.FilterKind),
getEnabledMark(filter.FilterEnabled),
strings.Join(lines, "\n"),
)
}
t.Render()
return nil
}
// FilterEnableCmd provides a command to enable a filter in a scene.
type FilterEnableCmd struct {
SourceName string `arg:"" help:"Name of the source to enable filter from."`
FilterName string `arg:"" help:"Name of the filter to enable."`
}
// Run executes the command to enable a filter in a scene.
func (cmd *FilterEnableCmd) Run(ctx *context) error {
_, err := ctx.Client.Filters.SetSourceFilterEnabled(
filters.NewSetSourceFilterEnabledParams().
WithSourceName(cmd.SourceName).
WithFilterName(cmd.FilterName).
WithFilterEnabled(true),
)
if err != nil {
return fmt.Errorf("failed to enable filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
}
fmt.Fprintf(ctx.Out, "Filter %s enabled on source %s.\n",
cmd.FilterName, cmd.SourceName)
return nil
}
// FilterDisableCmd provides a command to disable a filter in a scene.
type FilterDisableCmd struct {
SourceName string `arg:"" help:"Name of the source to disable filter from."`
FilterName string `arg:"" help:"Name of the filter to disable."`
}
// Run executes the command to disable a filter in a scene.
func (cmd *FilterDisableCmd) Run(ctx *context) error {
_, err := ctx.Client.Filters.SetSourceFilterEnabled(
filters.NewSetSourceFilterEnabledParams().
WithSourceName(cmd.SourceName).
WithFilterName(cmd.FilterName).
WithFilterEnabled(false),
)
if err != nil {
return fmt.Errorf("failed to disable filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
}
fmt.Fprintf(ctx.Out, "Filter %s disabled on source %s.\n",
cmd.FilterName, cmd.SourceName)
return nil
}
// FilterToggleCmd provides a command to toggle a filter in a scene.
type FilterToggleCmd struct {
SourceName string `arg:"" help:"Name of the source to toggle filter from."`
FilterName string `arg:"" help:"Name of the filter to toggle."`
}
// Run executes the command to toggle a filter in a scene.
func (cmd *FilterToggleCmd) Run(ctx *context) error {
filter, err := ctx.Client.Filters.GetSourceFilter(
filters.NewGetSourceFilterParams().
WithSourceName(cmd.SourceName).
WithFilterName(cmd.FilterName),
)
if err != nil {
return fmt.Errorf("failed to get filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
}
newStatus := !filter.FilterEnabled
_, err = ctx.Client.Filters.SetSourceFilterEnabled(
filters.NewSetSourceFilterEnabledParams().
WithSourceName(cmd.SourceName).
WithFilterName(cmd.FilterName).
WithFilterEnabled(newStatus),
)
if err != nil {
return fmt.Errorf("failed to toggle filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
}
if newStatus {
fmt.Fprintf(ctx.Out, "Filter %s on source %s is now enabled.\n",
cmd.FilterName, cmd.SourceName)
} else {
fmt.Fprintf(ctx.Out, "Filter %s on source %s is now disabled.\n",
cmd.FilterName, cmd.SourceName)
}
return nil
}
// FilterStatusCmd provides a command to get the status of a filter in a scene.
type FilterStatusCmd struct {
SourceName string `arg:"" help:"Name of the source to get filter status from."`
FilterName string `arg:"" help:"Name of the filter to get status."`
}
// Run executes the command to get the status of a filter in a scene.
func (cmd *FilterStatusCmd) Run(ctx *context) error {
filter, err := ctx.Client.Filters.GetSourceFilter(
filters.NewGetSourceFilterParams().
WithSourceName(cmd.SourceName).
WithFilterName(cmd.FilterName),
)
if err != nil {
return fmt.Errorf("failed to get status of filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
}
if filter.FilterEnabled {
fmt.Fprintf(ctx.Out, "Filter %s on source %s is enabled.\n",
cmd.FilterName, cmd.SourceName)
} else {
fmt.Fprintf(ctx.Out, "Filter %s on source %s is disabled.\n",
cmd.FilterName, cmd.SourceName)
}
return nil
}

76
filter_test.go Normal file
View 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(),
)
}
}

10
go.mod
View File

@@ -4,14 +4,24 @@ go 1.24.0
require ( require (
github.com/alecthomas/kong v1.10.0 github.com/alecthomas/kong v1.10.0
github.com/alecthomas/mango-kong v0.1.0
github.com/andreykaipov/goobs v1.5.6 github.com/andreykaipov/goobs v1.5.6
github.com/aquasecurity/table v1.10.0
github.com/titusjaka/kong-dotenv-go v0.1.0
) )
require ( require (
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mmcloughlin/profile v0.1.1 // indirect github.com/mmcloughlin/profile v0.1.1 // indirect
github.com/muesli/mango v0.1.1-0.20220205060214-77e2058169ab // indirect
github.com/muesli/roff v0.1.0 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
) )

20
go.sum
View File

@@ -2,10 +2,14 @@ github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8v
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.10.0 h1:8K4rGDpT7Iu+jEXCIJUeKqvpwZHbsFRoebLbnzlmrpw= github.com/alecthomas/kong v1.10.0 h1:8K4rGDpT7Iu+jEXCIJUeKqvpwZHbsFRoebLbnzlmrpw=
github.com/alecthomas/kong v1.10.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/kong v1.10.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/mango-kong v0.1.0 h1:iFVfP1k1K4qpml3JUQmD5I8MCQYfIvsD9mRdrw7jJC4=
github.com/alecthomas/mango-kong v0.1.0/go.mod h1:t+TYVdsONUolf/BwVcm+15eqcdAj15h4Qe9MMFAwwT4=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andreykaipov/goobs v1.5.6 h1:eIkEqYN99+2VJvmlY/56Ah60nkRKS6efMQvpM3oUgPQ= github.com/andreykaipov/goobs v1.5.6 h1:eIkEqYN99+2VJvmlY/56Ah60nkRKS6efMQvpM3oUgPQ=
github.com/andreykaipov/goobs v1.5.6/go.mod h1:iSZP93FJ4d9X/U1x4DD4IyILLtig+vViqZWBGjLywcY= github.com/andreykaipov/goobs v1.5.6/go.mod h1:iSZP93FJ4d9X/U1x4DD4IyILLtig+vViqZWBGjLywcY=
github.com/aquasecurity/table v1.10.0 h1:gPWV28qp9XSlvXdT3ku8yKQoZE6II0vsmegKpW+dB08=
github.com/aquasecurity/table v1.10.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -16,15 +20,31 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mmcloughlin/profile v0.1.1 h1:jhDmAqPyebOsVDOCICJoINoLb/AnLBaUw58nFzxWS2w= github.com/mmcloughlin/profile v0.1.1 h1:jhDmAqPyebOsVDOCICJoINoLb/AnLBaUw58nFzxWS2w=
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/muesli/mango v0.1.1-0.20220205060214-77e2058169ab h1:m7QFONkzLK0fVXCjwX5tANcnj1yXxTnYQtnfJiY3tcA=
github.com/muesli/mango v0.1.1-0.20220205060214-77e2058169ab/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/titusjaka/kong-dotenv-go v0.1.0 h1:TmUjP/sXoNiKLr6oR7n9xrB5XyXi/Ssuebzfz5nxZj4=
github.com/titusjaka/kong-dotenv-go v0.1.0/go.mod h1:pBgLjcu82oqUgb7+bngK9+Ch7jg49E0YADP8Wnj2MXU=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/andreykaipov/goobs/api/requests/sceneitems" "github.com/andreykaipov/goobs/api/requests/sceneitems"
"github.com/aquasecurity/table"
) )
// GroupCmd provides commands to manage groups in OBS Studio. // GroupCmd provides commands to manage groups in OBS Studio.
@@ -17,21 +18,36 @@ type GroupCmd struct {
// GroupListCmd provides a command to list all groups in a scene. // GroupListCmd provides a command to list all groups in a scene.
type GroupListCmd struct { type GroupListCmd struct {
SceneName string `arg:"" help:"Name of the scene to list groups from."` SceneName string `arg:"" help:"Name of the scene to list groups from." default:""`
} }
// Run executes the command to list all groups in a scene. // Run executes the command to list all groups in a scene.
func (cmd *GroupListCmd) Run(ctx *context) error { func (cmd *GroupListCmd) Run(ctx *context) error {
if cmd.SceneName == "" {
currentScene, err := ctx.Client.Scenes.GetCurrentProgramScene()
if err != nil {
return fmt.Errorf("failed to get current program scene: %w", err)
}
cmd.SceneName = currentScene.SceneName
}
resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams(). resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams().
WithSceneName(cmd.SceneName)) WithSceneName(cmd.SceneName))
if err != nil { if err != nil {
return fmt.Errorf("failed to get scene item list: %w", err) return fmt.Errorf("failed to get scene item list: %w", err)
} }
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignCenter, table.AlignLeft, table.AlignCenter)
t.SetHeaders("ID", "Group Name", "Enabled")
for _, item := range resp.SceneItems { for _, item := range resp.SceneItems {
if item.IsGroup { if item.IsGroup {
fmt.Fprintf(ctx.Out, "Group ID: %d, Source Name: %s\n", item.SceneItemID, item.SourceName) t.AddRow(fmt.Sprintf("%d", item.SceneItemID), item.SourceName, getEnabledMark(item.SceneItemEnabled))
} }
} }
t.Render()
return nil return nil
} }

130
group_test.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"bytes"
"strings"
"testing"
)
func TestGroupList(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmd := &GroupListCmd{
SceneName: "Scene",
}
err := cmd.Run(context)
if err != nil {
t.Fatalf("Failed to list groups: %v", err)
}
if !strings.Contains(out.String(), "test_group") {
t.Fatalf("Expected output to contain 'test_group', got '%s'", out.String())
}
}
func TestGroupShow(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmd := &GroupShowCmd{
SceneName: "Scene",
GroupName: "test_group",
}
err := cmd.Run(context)
if err != nil {
t.Fatalf("Failed to show group: %v", err)
}
if out.String() != "Group test_group is now shown.\n" {
t.Fatalf("Expected output to be 'Group test_group is now shown.', got '%s'", out.String())
}
}
func TestGroupToggle(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdStatus := &GroupStatusCmd{
SceneName: "Scene",
GroupName: "test_group",
}
err := cmdStatus.Run(context)
if err != nil {
t.Fatalf("Failed to get group status: %v", err)
}
var enabled bool
if strings.Contains(out.String(), "Group test_group is shown.") {
enabled = true
}
// Reset output buffer for the next command
out.Reset()
cmdToggle := &GroupToggleCmd{
SceneName: "Scene",
GroupName: "test_group",
}
err = cmdToggle.Run(context)
if err != nil {
t.Fatalf("Failed to toggle group: %v", err)
}
if enabled {
if out.String() != "Group test_group is now hidden.\n" {
t.Fatalf("Expected output to be 'Group test_group is now hidden.', got '%s'", out.String())
}
} else {
if out.String() != "Group test_group is now shown.\n" {
t.Fatalf("Expected output to be 'Group test_group is now shown.', got '%s'", out.String())
}
}
}
func TestGroupStatus(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdShow := &GroupShowCmd{
SceneName: "Scene",
GroupName: "test_group",
}
err := cmdShow.Run(context)
if err != nil {
t.Fatalf("Failed to show group: %v", err)
}
// Reset output buffer for the next command
out.Reset()
cmdStatus := &GroupStatusCmd{
SceneName: "Scene",
GroupName: "test_group",
}
err = cmdStatus.Run(context)
if err != nil {
t.Fatalf("Failed to get group status: %v", err)
}
if out.String() != "Group test_group is shown.\n" {
t.Fatalf("Expected output to be 'Group test_group is shown.', got '%s'", out.String())
}
}

79
hotkey.go Normal file
View File

@@ -0,0 +1,79 @@
package main
import (
"github.com/andreykaipov/goobs/api/requests/general"
"github.com/andreykaipov/goobs/api/typedefs"
"github.com/aquasecurity/table"
)
// HotkeyCmd provides commands to manage hotkeys in OBS Studio.
type HotkeyCmd struct {
List HotkeyListCmd `cmd:"" help:"List all hotkeys." aliases:"ls"`
Trigger HotkeyTriggerCmd `cmd:"" help:"Trigger a hotkey by name." aliases:"tr"`
TriggerSequence HotkeyTriggerSequenceCmd `cmd:"" help:"Trigger a hotkey by sequence." aliases:"trs"`
}
// HotkeyListCmd provides a command to list all hotkeys.
type HotkeyListCmd struct{} // size = 0x0
// Run executes the command to list all hotkeys.
func (cmd *HotkeyListCmd) Run(ctx *context) error {
resp, err := ctx.Client.General.GetHotkeyList()
if err != nil {
return err
}
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignLeft)
t.SetHeaders("Hotkey Name")
for _, hotkey := range resp.Hotkeys {
t.AddRow(hotkey)
}
t.Render()
return nil
}
// HotkeyTriggerCmd provides a command to trigger a hotkey.
type HotkeyTriggerCmd struct {
Hotkey string `help:"Hotkey name to trigger." arg:""`
}
// Run executes the command to trigger a hotkey.
func (cmd *HotkeyTriggerCmd) Run(ctx *context) error {
_, err := ctx.Client.General.TriggerHotkeyByName(
general.NewTriggerHotkeyByNameParams().WithHotkeyName(cmd.Hotkey),
)
if err != nil {
return err
}
return nil
}
// HotkeyTriggerSequenceCmd provides a command to trigger a hotkey sequence.
type HotkeyTriggerSequenceCmd struct {
Shift bool `flag:"" help:"Shift modifier."`
Ctrl bool `flag:"" help:"Control modifier."`
Alt bool `flag:"" help:"Alt modifier."`
Cmd bool `flag:"" help:"Command modifier."`
KeyID string ` help:"Key ID to trigger." arg:""`
}
// Run executes the command to trigger a hotkey sequence.
func (cmd *HotkeyTriggerSequenceCmd) Run(ctx *context) error {
_, err := ctx.Client.General.TriggerHotkeyByKeySequence(
general.NewTriggerHotkeyByKeySequenceParams().
WithKeyId(cmd.KeyID).
WithKeyModifiers(&typedefs.KeyModifiers{
Shift: cmd.Shift,
Control: cmd.Ctrl,
Alt: cmd.Alt,
Command: cmd.Cmd,
}),
)
if err != nil {
return err
}
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/andreykaipov/goobs/api/requests/inputs" "github.com/andreykaipov/goobs/api/requests/inputs"
"github.com/aquasecurity/table"
) )
// InputCmd provides commands to manage inputs in OBS Studio. // InputCmd provides commands to manage inputs in OBS Studio.
@@ -28,21 +29,28 @@ func (cmd *InputListCmd) Run(ctx *context) error {
if err != nil { if err != nil {
return err return err
} }
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignLeft, table.AlignLeft)
t.SetHeaders("Input Name", "Kind")
for _, input := range resp.Inputs { for _, input := range resp.Inputs {
if cmd.Input && strings.Contains(input.InputKind, "input") { if cmd.Input && strings.Contains(input.InputKind, "input") {
fmt.Fprintln(ctx.Out, "Input:", input.InputName) t.AddRow(input.InputName, input.InputKind)
} }
if cmd.Output && strings.Contains(input.InputKind, "output") { if cmd.Output && strings.Contains(input.InputKind, "output") {
fmt.Fprintln(ctx.Out, "Output:", input.InputName) t.AddRow(input.InputName, input.InputKind)
} }
if cmd.Colour && strings.Contains(input.InputKind, "color") { // nolint if cmd.Colour && strings.Contains(input.InputKind, "color") { // nolint
fmt.Fprintln(ctx.Out, "Colour Source:", input.InputName) t.AddRow(input.InputName, input.InputKind)
} }
if !cmd.Input && !cmd.Output && !cmd.Colour { if !cmd.Input && !cmd.Output && !cmd.Colour {
fmt.Fprintln(ctx.Out, "Source:", input.InputName) t.AddRow(input.InputName, input.InputKind)
} }
} }
t.Render()
return nil return nil
} }

140
input_test.go Normal file
View 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())
}
}
}

26
main.go
View File

@@ -7,10 +7,13 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"time" "time"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
mangokong "github.com/alecthomas/mango-kong"
"github.com/andreykaipov/goobs" "github.com/andreykaipov/goobs"
kongdotenv "github.com/titusjaka/kong-dotenv-go"
) )
// ObsConfig holds the configuration for connecting to the OBS WebSocket server. // ObsConfig holds the configuration for connecting to the OBS WebSocket server.
@@ -21,11 +24,13 @@ type ObsConfig struct {
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"`
} }
// cli is the main command line interface structure. // CLI is the main command line interface structure.
// It embeds the ObsConfig struct to inherit its fields and flags. // It embeds the ObsConfig struct to inherit its fields and flags.
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."`
Version VersionCmd `help:"Show version." cmd:"" aliases:"v"` Version VersionCmd `help:"Show 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"`
@@ -38,6 +43,9 @@ type cli struct {
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"`
Filter FilterCmd `help:"Manage filters." cmd:"" aliases:"f"`
Projector ProjectorCmd `help:"Manage projectors." cmd:"" aliases:"prj"`
} }
type context struct { type context struct {
@@ -46,18 +54,22 @@ type context struct {
} }
func main() { func main() {
var client *goobs.Client userConfigDir, err := os.UserConfigDir()
cli := cli{} if err != nil {
fmt.Fprintf(os.Stderr, "Error getting user config directory: %v\n", err)
os.Exit(1)
}
var cli CLI
ctx := kong.Parse( ctx := kong.Parse(
&cli, &cli,
kong.Name("GOBS-CLI"), kong.Name("GOBS-CLI"),
kong.Description("A command line tool to interact with OBS Websocket."), kong.Description("A command line tool to interact with OBS Websocket."),
kong.Configuration(kongdotenv.ENVFileReader, ".env", filepath.Join(userConfigDir, "gobs-cli", "config.env")),
) )
client, err := connectObs(cli.ObsConfig) client, err := connectObs(cli.ObsConfig)
if err != nil { ctx.FatalIfErrorf(err)
ctx.FatalIfErrorf(err)
}
ctx.Bind(&context{ ctx.Bind(&context{
Client: client, Client: client,

133
main_test.go Normal file
View File

@@ -0,0 +1,133 @@
package main
import (
"os"
"testing"
"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"
)
func getClient(t *testing.T) (*goobs.Client, func()) {
t.Helper()
client, err := connectObs(ObsConfig{
Host: os.Getenv("OBS_HOST"),
Port: 4455,
Password: os.Getenv("OBS_PASSWORD"),
Timeout: 5,
})
if err != nil {
t.Fatalf("Failed to connect to OBS: %v", err)
}
return client, func() {
if err := client.Disconnect(); err != nil {
t.Fatalf("Failed to disconnect from OBS: %v", err)
}
}
}
func TestMain(m *testing.M) {
client, err := connectObs(ObsConfig{
Host: os.Getenv("OBS_HOST"),
Port: 4455,
Password: os.Getenv("OBS_PASSWORD"),
Timeout: 5,
})
if err != nil {
os.Exit(1)
}
defer client.Disconnect()
setup(client)
// Run the tests
exitCode := m.Run()
teardown(client)
// Exit with the appropriate code
os.Exit(exitCode)
}
func setup(client *goobs.Client) {
client.Config.SetStreamServiceSettings(config.NewSetStreamServiceSettingsParams().
WithStreamServiceType("rtmp_common").
WithStreamServiceSettings(&typedefs.StreamServiceSettings{
Server: "auto",
Key: os.Getenv("OBS_STREAM_KEY"),
}))
client.Config.SetCurrentSceneCollection(config.NewSetCurrentSceneCollectionParams().
WithSceneCollectionName("test-collection"))
client.Scenes.CreateScene(scenes.NewCreateSceneParams().
WithSceneName("gobs-test"))
client.Inputs.CreateInput(inputs.NewCreateInputParams().
WithSceneName("gobs-test").
WithInputName("gobs-test-input").
WithInputKind("color_source_v3").
WithInputSettings(map[string]any{
"color": 3279460728,
"width": 1920,
"height": 1080,
"visible": true,
}).
WithSceneItemEnabled(true))
client.Inputs.CreateInput(inputs.NewCreateInputParams().
WithSceneName("gobs-test").
WithInputName("gobs-test-input-2").
WithInputKind("color_source_v3").
WithInputSettings(map[string]any{
"color": 1789347616,
"width": 720,
"height": 480,
"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"))
client.Config.SetCurrentSceneCollection(config.NewSetCurrentSceneCollectionParams().
WithSceneCollectionName("default"))
client.Stream.StopStream()
client.Record.StopRecord()
client.Outputs.StopReplayBuffer()
}

View File

@@ -5,39 +5,50 @@ import (
"slices" "slices"
"github.com/andreykaipov/goobs/api/requests/config" "github.com/andreykaipov/goobs/api/requests/config"
"github.com/aquasecurity/table"
) )
// ProfileCmd provides commands to manage profiles in OBS Studio. // ProfileCmd provides commands to manage profiles in OBS Studio.
type ProfileCmd struct { type ProfileCmd struct {
List ListProfileCmd `help:"List profiles." cmd:"" aliases:"ls"` List ProfileListCmd `help:"List profiles." cmd:"" aliases:"ls"`
Current CurrentProfileCmd `help:"Get current profile." cmd:"" aliases:"c"` Current ProfileCurrentCmd `help:"Get current profile." cmd:"" aliases:"c"`
Switch SwitchProfileCmd `help:"Switch profile." cmd:"" aliases:"sw"` Switch ProfileSwitchCmd `help:"Switch profile." cmd:"" aliases:"sw"`
Create CreateProfileCmd `help:"Create profile." cmd:"" aliases:"cr"` Create ProfileCreateCmd `help:"Create profile." cmd:"" aliases:"new"`
Remove RemoveProfileCmd `help:"Remove profile." cmd:"" aliases:"rm"` Remove ProfileRemoveCmd `help:"Remove profile." cmd:"" aliases:"rm"`
} }
// ListProfileCmd provides a command to list all profiles. // ProfileListCmd provides a command to list all profiles.
type ListProfileCmd struct{} // size = 0x0 type ProfileListCmd struct{} // size = 0x0
// Run executes the command to list all profiles. // Run executes the command to list all profiles.
func (cmd *ListProfileCmd) Run(ctx *context) error { func (cmd *ProfileListCmd) Run(ctx *context) error {
profiles, err := ctx.Client.Config.GetProfileList() profiles, err := ctx.Client.Config.GetProfileList()
if err != nil { if err != nil {
return err return err
} }
for _, profile := range profiles.Profiles { t := table.New(ctx.Out)
fmt.Fprintln(ctx.Out, profile) t.SetPadding(3)
} t.SetAlignment(table.AlignLeft, table.AlignCenter)
t.SetHeaders("Profile Name", "Current")
for _, profile := range profiles.Profiles {
var enabledMark string
if profile == profiles.CurrentProfileName {
enabledMark = getEnabledMark(true)
}
t.AddRow(profile, enabledMark)
}
t.Render()
return nil return nil
} }
// CurrentProfileCmd provides a command to get the current profile. // ProfileCurrentCmd provides a command to get the current profile.
type CurrentProfileCmd struct{} // size = 0x0 type ProfileCurrentCmd struct{} // size = 0x0
// Run executes the command to get the current profile. // Run executes the command to get the current profile.
func (cmd *CurrentProfileCmd) Run(ctx *context) error { func (cmd *ProfileCurrentCmd) Run(ctx *context) error {
profiles, err := ctx.Client.Config.GetProfileList() profiles, err := ctx.Client.Config.GetProfileList()
if err != nil { if err != nil {
return err return err
@@ -47,13 +58,13 @@ func (cmd *CurrentProfileCmd) Run(ctx *context) error {
return nil return nil
} }
// SwitchProfileCmd provides a command to switch to a different profile. // ProfileSwitchCmd provides a command to switch to a different profile.
type SwitchProfileCmd struct { type ProfileSwitchCmd struct {
Name string `arg:"" help:"Name of the profile to switch to." required:""` Name string `arg:"" help:"Name of the profile to switch to." required:""`
} }
// Run executes the command to switch to a different profile. // Run executes the command to switch to a different profile.
func (cmd *SwitchProfileCmd) Run(ctx *context) error { func (cmd *ProfileSwitchCmd) Run(ctx *context) error {
profiles, err := ctx.Client.Config.GetProfileList() profiles, err := ctx.Client.Config.GetProfileList()
if err != nil { if err != nil {
return err return err
@@ -74,13 +85,13 @@ func (cmd *SwitchProfileCmd) Run(ctx *context) error {
return nil return nil
} }
// CreateProfileCmd provides a command to create a new profile. // ProfileCreateCmd provides a command to create a new profile.
type CreateProfileCmd struct { type ProfileCreateCmd struct {
Name string `arg:"" help:"Name of the profile to create." required:""` Name string `arg:"" help:"Name of the profile to create." required:""`
} }
// Run executes the command to create a new profile. // Run executes the command to create a new profile.
func (cmd *CreateProfileCmd) Run(ctx *context) error { func (cmd *ProfileCreateCmd) Run(ctx *context) error {
profiles, err := ctx.Client.Config.GetProfileList() profiles, err := ctx.Client.Config.GetProfileList()
if err != nil { if err != nil {
return err return err
@@ -100,13 +111,13 @@ func (cmd *CreateProfileCmd) Run(ctx *context) error {
return nil return nil
} }
// RemoveProfileCmd provides a command to remove an existing profile. // ProfileRemoveCmd provides a command to remove an existing profile.
type RemoveProfileCmd struct { type ProfileRemoveCmd struct {
Name string `arg:"" help:"Name of the profile to delete." required:""` Name string `arg:"" help:"Name of the profile to delete." required:""`
} }
// Run executes the command to remove an existing profile. // Run executes the command to remove an existing profile.
func (cmd *RemoveProfileCmd) Run(ctx *context) error { func (cmd *ProfileRemoveCmd) Run(ctx *context) error {
profiles, err := ctx.Client.Config.GetProfileList() profiles, err := ctx.Client.Config.GetProfileList()
if err != nil { if err != nil {
return err return err

66
projector.go Normal file
View 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
}

View File

@@ -2,15 +2,19 @@ package main
import ( import (
"fmt" "fmt"
"github.com/andreykaipov/goobs/api/requests/config"
) )
// RecordCmd handles the recording commands. // RecordCmd handles the recording commands.
type RecordCmd struct { type RecordCmd struct {
Start RecordStartCmd `cmd:"" help:"Start recording." aliases:"s"` Start RecordStartCmd `cmd:"" help:"Start recording." aliases:"s"`
Stop RecordStopCmd `cmd:"" help:"Stop recording." aliases:"st"` Stop RecordStopCmd `cmd:"" help:"Stop recording." aliases:"st"`
Toggle RecordToggleCmd `cmd:"" help:"Toggle recording." aliases:"tg"` Toggle RecordToggleCmd `cmd:"" help:"Toggle recording." aliases:"tg"`
Pause RecordPauseCmd `cmd:"" help:"Pause recording." aliases:"p"` Status RecordStatusCmd `cmd:"" help:"Show recording status." aliases:"ss"`
Resume RecordResumeCmd `cmd:"" help:"Resume recording." aliases:"r"` Pause RecordPauseCmd `cmd:"" help:"Pause recording." aliases:"p"`
Resume RecordResumeCmd `cmd:"" help:"Resume recording." aliases:"r"`
Directory RecordDirectoryCmd `cmd:"" help:"Get/Set recording directory." aliases:"d"`
} }
// RecordStartCmd starts the recording. // RecordStartCmd starts the recording.
@@ -31,11 +35,11 @@ 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 {
_, err := ctx.Client.Record.StopRecord() resp, err := ctx.Client.Record.StopRecord()
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintln(ctx.Out, "Recording stopped successfully.") fmt.Fprintf(ctx.Out, "%s", fmt.Sprintf("Recording stopped successfully. Output file: %s\n", resp.OutputPath))
return nil return nil
} }
@@ -44,25 +48,39 @@ type RecordToggleCmd struct{} // size = 0x0
// Run executes the command to toggle recording. // Run executes the command to toggle recording.
func (cmd *RecordToggleCmd) Run(ctx *context) error { func (cmd *RecordToggleCmd) Run(ctx *context) error {
// Check if recording is in progress status, err := ctx.Client.Record.ToggleRecord()
if err != nil {
return err
}
if status.OutputActive {
fmt.Fprintln(ctx.Out, "Recording started successfully.")
} else {
fmt.Fprintln(ctx.Out, "Recording stopped successfully.")
}
return nil
}
// RecordStatusCmd shows the recording status.
type RecordStatusCmd struct{} // size = 0x0
// Run executes the command to show recording status.
func (cmd *RecordStatusCmd) Run(ctx *context) error {
status, err := ctx.Client.Record.GetRecordStatus() status, err := ctx.Client.Record.GetRecordStatus()
if err != nil { if err != nil {
return err return err
} }
if status.OutputActive { if status.OutputActive {
_, err = ctx.Client.Record.StopRecord() if status.OutputPaused {
if err != nil { fmt.Fprintln(ctx.Out, "Recording is paused.")
return err } else {
fmt.Fprintln(ctx.Out, "Recording is in progress.")
} }
fmt.Fprintln(ctx.Out, "Recording stopped successfully.")
} else { } else {
_, err = ctx.Client.Record.StartRecord() fmt.Fprintln(ctx.Out, "Recording is not in progress.")
if err != nil {
return err
}
fmt.Fprintln(ctx.Out, "Recording started successfully.")
} }
return nil return nil
} }
@@ -117,3 +135,30 @@ func (cmd *RecordResumeCmd) Run(ctx *context) error {
fmt.Fprintln(ctx.Out, "Recording resumed successfully.") fmt.Fprintln(ctx.Out, "Recording resumed successfully.")
return nil return nil
} }
// RecordDirectoryCmd sets the recording directory.
type RecordDirectoryCmd struct {
RecordDirectory string `arg:"" help:"Directory to save recordings." default:""`
}
// Run executes the command to set the recording directory.
func (cmd *RecordDirectoryCmd) Run(ctx *context) error {
if cmd.RecordDirectory == "" {
resp, err := ctx.Client.Config.GetRecordDirectory()
if err != nil {
return err
}
fmt.Fprintf(ctx.Out, "Current recording directory: %s\n", resp.RecordDirectory)
return nil
}
_, err := ctx.Client.Config.SetRecordDirectory(
config.NewSetRecordDirectoryParams().WithRecordDirectory(cmd.RecordDirectory),
)
if err != nil {
return err
}
fmt.Fprintf(ctx.Out, "Recording directory set to: %s\n", cmd.RecordDirectory)
return nil
}

106
record_test.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"bytes"
"strings"
"testing"
"time"
)
func TestRecordStartStatusStop(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdStart := &RecordStartCmd{}
err := cmdStart.Run(context)
if err != nil {
t.Fatalf("Failed to start recording: %v", err)
}
if out.String() != "Recording started successfully.\n" {
t.Fatalf("Expected output to be 'Recording started successfully.', 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 started
cmdStatus := &RecordStatusCmd{}
err = cmdStatus.Run(context)
if err != nil {
t.Fatalf("Failed to get recording status: %v", err)
}
if out.String() != "Recording is in progress.\n" {
t.Fatalf("Expected output to be 'Recording is in progress.', got '%s'", out.String())
}
// Reset output buffer for the next command
out.Reset()
cmdStop := &RecordStopCmd{}
err = cmdStop.Run(context)
if err != nil {
t.Fatalf("Failed to stop recording: %v", err)
}
if !strings.Contains(out.String(), "Recording stopped successfully. Output file:") {
t.Fatalf("Expected output to be 'Recording stopped successfully.', 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())
}
}
func TestRecordToggle(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdStatus := &RecordStatusCmd{}
err := cmdStatus.Run(context)
if err != nil {
t.Fatalf("Failed to get recording status: %v", err)
}
var active bool
if out.String() == "Recording is in progress.\n" {
active = true
}
// Reset output buffer for the next command
out.Reset()
cmdToggle := &RecordToggleCmd{}
err = cmdToggle.Run(context)
if err != nil {
t.Fatalf("Failed to toggle recording: %v", err)
}
time.Sleep(1 * time.Second) // Wait for a second to ensure toggle has taken effect
if active {
if out.String() != "Recording stopped successfully.\n" {
t.Fatalf("Expected output to be 'Recording stopped successfully.', got '%s'", out.String())
}
} else {
if out.String() != "Recording started successfully.\n" {
t.Fatalf("Expected output to be 'Recording started successfully.', got '%s'", out.String())
}
}
}

View File

@@ -8,6 +8,7 @@ import (
type ReplayBufferCmd struct { type ReplayBufferCmd struct {
Start ReplayBufferStartCmd `help:"Start replay buffer." cmd:"" aliases:"s"` Start ReplayBufferStartCmd `help:"Start replay buffer." cmd:"" aliases:"s"`
Stop ReplayBufferStopCmd `help:"Stop replay buffer." cmd:"" aliases:"st"` Stop ReplayBufferStopCmd `help:"Stop replay buffer." cmd:"" aliases:"st"`
Toggle ReplayBufferToggleCmd `help:"Toggle replay buffer." cmd:"" aliases:"tg"`
Status ReplayBufferStatusCmd `help:"Get replay buffer status." cmd:"" aliases:"ss"` Status ReplayBufferStatusCmd `help:"Get replay buffer status." cmd:"" aliases:"ss"`
Save ReplayBufferSaveCmd `help:"Save replay buffer." cmd:"" aliases:"sv"` Save ReplayBufferSaveCmd `help:"Save replay buffer." cmd:"" aliases:"sv"`
} }
@@ -18,7 +19,11 @@ type ReplayBufferStartCmd struct{} // size = 0x0
// Run executes the command to start the replay buffer. // Run executes the command to start the replay buffer.
func (cmd *ReplayBufferStartCmd) Run(ctx *context) error { func (cmd *ReplayBufferStartCmd) Run(ctx *context) error {
_, err := ctx.Client.Outputs.StartReplayBuffer() _, 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. // ReplayBufferStopCmd stops the replay buffer.
@@ -27,7 +32,29 @@ type ReplayBufferStopCmd struct{} // size = 0x0
// Run executes the command to stop the replay buffer. // Run executes the command to stop the replay buffer.
func (cmd *ReplayBufferStopCmd) Run(ctx *context) error { func (cmd *ReplayBufferStopCmd) Run(ctx *context) error {
_, err := ctx.Client.Outputs.StopReplayBuffer() _, 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.
type ReplayBufferToggleCmd struct{} // size = 0x0
// Run executes the command to toggle the replay buffer.
func (cmd *ReplayBufferToggleCmd) Run(ctx *context) error {
status, err := ctx.Client.Outputs.ToggleReplayBuffer()
if err != nil {
return err
}
if status.OutputActive {
fmt.Fprintln(ctx.Out, "Replay buffer started.")
} else {
fmt.Fprintln(ctx.Out, "Replay buffer stopped.")
}
return nil
} }
// ReplayBufferStatusCmd retrieves the status of the replay buffer. // ReplayBufferStatusCmd retrieves the status of the replay buffer.

85
replaybuffer_test.go Normal file
View 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())
}
}
}

View File

@@ -5,6 +5,7 @@ import (
"slices" "slices"
"github.com/andreykaipov/goobs/api/requests/scenes" "github.com/andreykaipov/goobs/api/requests/scenes"
"github.com/aquasecurity/table"
) )
// SceneCmd provides commands to manage scenes in OBS Studio. // SceneCmd provides commands to manage scenes in OBS Studio.
@@ -24,10 +25,16 @@ func (cmd *SceneListCmd) Run(ctx *context) error {
return err return err
} }
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignLeft, table.AlignLeft)
t.SetHeaders("Scene Name", "UUID")
slices.Reverse(scenes.Scenes) slices.Reverse(scenes.Scenes)
for _, scene := range scenes.Scenes { for _, scene := range scenes.Scenes {
fmt.Fprintln(ctx.Out, scene.SceneName) t.AddRow(scene.SceneName, scene.SceneUuid)
} }
t.Render()
return nil return nil
} }

58
scene_test.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"bytes"
"strings"
"testing"
)
func TestSceneList(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmd := &SceneListCmd{}
err := cmd.Run(context)
if err != nil {
t.Fatalf("Failed to list scenes: %v", err)
}
if !strings.Contains(out.String(), "gobs-test") {
t.Fatalf("Expected output to contain 'gobs-test', got '%s'", out.String())
}
}
func TestSceneCurrent(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
// Set the current scene to "gobs-test"
cmdSwitch := &SceneSwitchCmd{
NewScene: "gobs-test",
}
err := cmdSwitch.Run(context)
if err != nil {
t.Fatalf("Failed to switch to scene: %v", err)
}
// Reset output buffer for the next command
out.Reset()
cmdCurrent := &SceneCurrentCmd{}
err = cmdCurrent.Run(context)
if err != nil {
t.Fatalf("Failed to get current scene: %v", err)
}
if out.String() != "gobs-test\n" {
t.Fatalf("Expected output to contain 'gobs-test', got '%s'", out.String())
}
}

View File

@@ -4,38 +4,44 @@ import (
"fmt" "fmt"
"github.com/andreykaipov/goobs/api/requests/config" "github.com/andreykaipov/goobs/api/requests/config"
"github.com/aquasecurity/table"
) )
// SceneCollectionCmd provides commands to manage scene collections in OBS Studio. // SceneCollectionCmd provides commands to manage scene collections in OBS Studio.
type SceneCollectionCmd struct { type SceneCollectionCmd struct {
List ListSceneCollectionCmd `help:"List scene collections." cmd:"" aliases:"ls"` List SceneCollectionListCmd `help:"List scene collections." cmd:"" aliases:"ls"`
Current CurrentSceneCollectionCmd `help:"Get current scene collection." cmd:"" aliases:"c"` Current SceneCollectionCurrentCmd `help:"Get current scene collection." cmd:"" aliases:"c"`
Switch SwitchSceneCollectionCmd `help:"Switch scene collection." cmd:"" aliases:"sw"` Switch SceneCollectionSwitchCmd `help:"Switch scene collection." cmd:"" aliases:"sw"`
Create CreateSceneCollectionCmd `help:"Create scene collection." cmd:"" aliases:"cr"` Create SceneCollectionCreateCmd `help:"Create scene collection." cmd:"" aliases:"new"`
} }
// ListSceneCollectionCmd provides a command to list all scene collections. // SceneCollectionListCmd provides a command to list all scene collections.
type ListSceneCollectionCmd struct{} // size = 0x0 type SceneCollectionListCmd struct{} // size = 0x0
// Run executes the command to list all scene collections. // Run executes the command to list all scene collections.
func (cmd *ListSceneCollectionCmd) Run(ctx *context) error { func (cmd *SceneCollectionListCmd) Run(ctx *context) error {
collections, err := ctx.Client.Config.GetSceneCollectionList() collections, err := ctx.Client.Config.GetSceneCollectionList()
if err != nil { if err != nil {
return fmt.Errorf("failed to get scene collection list: %w", err) return fmt.Errorf("failed to get scene collection list: %w", err)
} }
for _, collection := range collections.SceneCollections { t := table.New(ctx.Out)
fmt.Fprintln(ctx.Out, collection) t.SetPadding(3)
} t.SetAlignment(table.AlignLeft)
t.SetHeaders("Scene Collection Name")
for _, collection := range collections.SceneCollections {
t.AddRow(collection)
}
t.Render()
return nil return nil
} }
// CurrentSceneCollectionCmd provides a command to get the current scene collection. // SceneCollectionCurrentCmd provides a command to get the current scene collection.
type CurrentSceneCollectionCmd struct{} // size = 0x0 type SceneCollectionCurrentCmd struct{} // size = 0x0
// Run executes the command to get the current scene collection. // Run executes the command to get the current scene collection.
func (cmd *CurrentSceneCollectionCmd) Run(ctx *context) error { func (cmd *SceneCollectionCurrentCmd) Run(ctx *context) error {
collections, err := ctx.Client.Config.GetSceneCollectionList() collections, err := ctx.Client.Config.GetSceneCollectionList()
if err != nil { if err != nil {
return fmt.Errorf("failed to get scene collection list: %w", err) return fmt.Errorf("failed to get scene collection list: %w", err)
@@ -45,13 +51,13 @@ func (cmd *CurrentSceneCollectionCmd) Run(ctx *context) error {
return nil return nil
} }
// SwitchSceneCollectionCmd provides a command to switch to a different scene collection. // SceneCollectionSwitchCmd provides a command to switch to a different scene collection.
type SwitchSceneCollectionCmd struct { type SceneCollectionSwitchCmd struct {
Name string `arg:"" help:"Name of the scene collection to switch to." required:""` Name string `arg:"" help:"Name of the scene collection to switch to." required:""`
} }
// Run executes the command to switch to a different scene collection. // Run executes the command to switch to a different scene collection.
func (cmd *SwitchSceneCollectionCmd) Run(ctx *context) error { func (cmd *SceneCollectionSwitchCmd) Run(ctx *context) error {
collections, err := ctx.Client.Config.GetSceneCollectionList() collections, err := ctx.Client.Config.GetSceneCollectionList()
if err != nil { if err != nil {
return err return err
@@ -74,13 +80,13 @@ func (cmd *SwitchSceneCollectionCmd) Run(ctx *context) error {
return nil return nil
} }
// CreateSceneCollectionCmd provides a command to create a new scene collection. // SceneCollectionCreateCmd provides a command to create a new scene collection.
type CreateSceneCollectionCmd struct { type SceneCollectionCreateCmd struct {
Name string `arg:"" help:"Name of the scene collection to create." required:""` Name string `arg:"" help:"Name of the scene collection to create." required:""`
} }
// Run executes the command to create a new scene collection. // Run executes the command to create a new scene collection.
func (cmd *CreateSceneCollectionCmd) Run(ctx *context) error { func (cmd *SceneCollectionCreateCmd) Run(ctx *context) error {
_, err := ctx.Client.Config.CreateSceneCollection( _, err := ctx.Client.Config.CreateSceneCollection(
config.NewCreateSceneCollectionParams().WithSceneCollectionName(cmd.Name), config.NewCreateSceneCollectionParams().WithSceneCollectionName(cmd.Name),
) )

View File

@@ -5,32 +5,54 @@ import (
"github.com/andreykaipov/goobs" "github.com/andreykaipov/goobs"
"github.com/andreykaipov/goobs/api/requests/sceneitems" "github.com/andreykaipov/goobs/api/requests/sceneitems"
"github.com/aquasecurity/table"
) )
// SceneItemCmd provides commands to manage scene items in OBS Studio. // SceneItemCmd provides commands to manage scene items in OBS Studio.
type SceneItemCmd struct { type SceneItemCmd struct {
List SceneItemListCmd `cmd:"" help:"List all scene items." aliases:"ls"` List SceneItemListCmd `cmd:"" help:"List all scene items." aliases:"ls"`
Show SceneItemShowCmd `cmd:"" help:"Show scene item." aliases:"sh"` Show SceneItemShowCmd `cmd:"" help:"Show scene item." aliases:"sh"`
Hide SceneItemHideCmd `cmd:"" help:"Hide scene item." aliases:"h"` Hide SceneItemHideCmd `cmd:"" help:"Hide scene item." aliases:"h"`
Toggle SceneItemToggleCmd `cmd:"" help:"Toggle scene item." aliases:"tg"` Toggle SceneItemToggleCmd `cmd:"" help:"Toggle scene item." aliases:"tg"`
Visible SceneItemVisibleCmd `cmd:"" help:"Get scene item visibility." aliases:"v"` Visible SceneItemVisibleCmd `cmd:"" help:"Get scene item visibility." aliases:"v"`
Transform SceneItemTransformCmd `cmd:"" help:"Transform scene item." aliases:"t"`
} }
// SceneItemListCmd provides a command to list all scene items in a scene. // SceneItemListCmd provides a command to list all scene items in a scene.
type SceneItemListCmd struct { type SceneItemListCmd struct {
SceneName string `arg:"" help:"Scene name."` SceneName string `arg:"" help:"Name of the scene to list items from." default:""`
} }
// Run executes the command to list all scene items in a scene. // Run executes the command to list all scene items in a scene.
func (cmd *SceneItemListCmd) Run(ctx *context) error { func (cmd *SceneItemListCmd) Run(ctx *context) error {
if cmd.SceneName == "" {
currentScene, err := ctx.Client.Scenes.GetCurrentProgramScene()
if err != nil {
return fmt.Errorf("failed to get current program scene: %w", err)
}
cmd.SceneName = currentScene.SceneName
}
resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams(). resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams().
WithSceneName(cmd.SceneName)) WithSceneName(cmd.SceneName))
if err != nil { if err != nil {
return fmt.Errorf("failed to get scene item list: %w", err) return fmt.Errorf("failed to get scene item list: %w", err)
} }
for _, item := range resp.SceneItems {
fmt.Fprintf(ctx.Out, "Item ID: %d, Source Name: %s\n", item.SceneItemID, item.SourceName) if len(resp.SceneItems) == 0 {
fmt.Fprintf(ctx.Out, "No scene items found in scene '%s'.\n", cmd.SceneName)
return nil
} }
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignLeft)
t.SetHeaders("Item Name")
for _, item := range resp.SceneItems {
t.AddRow(item.SourceName)
}
t.Render()
return nil return nil
} }
@@ -85,6 +107,13 @@ func (cmd *SceneItemShowCmd) Run(ctx *context) error {
if err != nil { if err != nil {
return err return err
} }
if cmd.Parent != "" {
fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' is now visible.\n", cmd.ItemName, cmd.Parent)
} else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now visible.\n", cmd.ItemName, cmd.SceneName)
}
return nil return nil
} }
@@ -110,6 +139,13 @@ func (cmd *SceneItemHideCmd) Run(ctx *context) error {
if err != nil { if err != nil {
return err return err
} }
if cmd.Parent != "" {
fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' is now hidden.\n", cmd.ItemName, cmd.Parent)
} else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now hidden.\n", cmd.ItemName, cmd.SceneName)
}
return nil return nil
} }
@@ -151,6 +187,13 @@ func (cmd *SceneItemToggleCmd) Run(ctx *context) error {
if err != nil { if err != nil {
return err return err
} }
if itemEnabled {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now hidden.\n", cmd.ItemName, cmd.SceneName)
} else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now visible.\n", cmd.ItemName, cmd.SceneName)
}
return nil return nil
} }
@@ -181,3 +224,110 @@ func (cmd *SceneItemVisibleCmd) Run(ctx *context) error {
} }
return nil return nil
} }
// SceneItemTransformCmd provides a command to transform a scene item.
type SceneItemTransformCmd struct {
SceneName string `arg:"" help:"Scene name."`
ItemName string `arg:"" help:"Item name."`
Parent string `flag:"" help:"Parent group name."`
Alignment float64 `flag:"" help:"Alignment of the scene item."`
BoundsAlignment float64 `flag:"" help:"Bounds alignment of the scene item."`
BoundsHeight float64 `flag:"" help:"Bounds height of the scene item." default:"1.0"`
BoundsType string `flag:"" help:"Bounds type of the scene item." default:"OBS_BOUNDS_NONE"`
BoundsWidth float64 `flag:"" help:"Bounds width of the scene item." default:"1.0"`
CropToBounds bool `flag:"" help:"Whether to crop the scene item to bounds."`
CropBottom float64 `flag:"" help:"Crop bottom value of the scene item."`
CropLeft float64 `flag:"" help:"Crop left value of the scene item."`
CropRight float64 `flag:"" help:"Crop right value of the scene item."`
CropTop float64 `flag:"" help:"Crop top value of the scene item."`
PositionX float64 `flag:"" help:"X position of the scene item."`
PositionY float64 `flag:"" help:"Y position of the scene item."`
Rotation float64 `flag:"" help:"Rotation of the scene item."`
ScaleX float64 `flag:"" help:"X scale of the scene item."`
ScaleY float64 `flag:"" help:"Y scale of the scene item."`
}
// Run executes the command to transform a scene item.
func (cmd *SceneItemTransformCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent)
if err != nil {
return err
}
// Get the current transform of the scene item
resp, err := ctx.Client.SceneItems.GetSceneItemTransform(sceneitems.NewGetSceneItemTransformParams().
WithSceneName(sceneName).
WithSceneItemId(sceneItemID))
if err != nil {
return err
}
// Update the transform with the provided values
transform := resp.SceneItemTransform
if cmd.Alignment != 0 {
transform.Alignment = cmd.Alignment
}
if cmd.BoundsAlignment != 0 {
transform.BoundsAlignment = cmd.BoundsAlignment
}
if cmd.BoundsHeight != 0 {
transform.BoundsHeight = cmd.BoundsHeight
}
if cmd.BoundsType != "" {
transform.BoundsType = cmd.BoundsType
}
if cmd.BoundsWidth != 0 {
transform.BoundsWidth = cmd.BoundsWidth
}
if cmd.CropToBounds {
transform.CropToBounds = cmd.CropToBounds
}
if cmd.CropBottom != 0 {
transform.CropBottom = cmd.CropBottom
}
if cmd.CropLeft != 0 {
transform.CropLeft = cmd.CropLeft
}
if cmd.CropRight != 0 {
transform.CropRight = cmd.CropRight
}
if cmd.CropTop != 0 {
transform.CropTop = cmd.CropTop
}
if cmd.PositionX != 0 {
transform.PositionX = cmd.PositionX
}
if cmd.PositionY != 0 {
transform.PositionY = cmd.PositionY
}
if cmd.Rotation != 0 {
transform.Rotation = cmd.Rotation
}
if cmd.ScaleX != 0 {
transform.ScaleX = cmd.ScaleX
}
if cmd.ScaleY != 0 {
transform.ScaleY = cmd.ScaleY
}
_, err = ctx.Client.SceneItems.SetSceneItemTransform(sceneitems.NewSetSceneItemTransformParams().
WithSceneName(sceneName).
WithSceneItemId(sceneItemID).
WithSceneItemTransform(transform))
if err != nil {
return err
}
if cmd.Parent != "" {
fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' transformed.\n", cmd.ItemName, cmd.Parent)
} else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' transformed.\n", cmd.ItemName, cmd.SceneName)
}
return nil
}

32
sceneitem_test.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"bytes"
"strings"
"testing"
)
func TestSceneItemList(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmd := &SceneItemListCmd{
SceneName: "gobs-test",
}
err := cmd.Run(context)
if err != nil {
t.Fatalf("Failed to list scene items: %v", err)
}
if !strings.Contains(out.String(), "gobs-test-input") {
t.Fatalf("Expected output to contain 'gobs-test-input', got '%s'", out.String())
}
if !strings.Contains(out.String(), "gobs-test-input-2") {
t.Fatalf("Expected output to contain 'gobs-test-input-2', got '%s'", out.String())
}
}

View File

@@ -17,10 +17,22 @@ type StreamStartCmd struct{} // size = 0x0
// Run executes the command to start streaming. // Run executes the command to start streaming.
func (cmd *StreamStartCmd) Run(ctx *context) error { func (cmd *StreamStartCmd) Run(ctx *context) error {
_, err := ctx.Client.Stream.StartStream() // Check if the stream is already active
status, err := ctx.Client.Stream.GetStreamStatus()
if err != nil { if err != nil {
return err return err
} }
if status.OutputActive {
fmt.Fprintln(ctx.Out, "Stream is already active.")
return nil
}
_, err = ctx.Client.Stream.StartStream()
if err != nil {
return err
}
fmt.Fprintln(ctx.Out, "Streaming started successfully.")
return nil return nil
} }
@@ -29,10 +41,22 @@ type StreamStopCmd struct{} // size = 0x0
// Run executes the command to stop streaming. // Run executes the command to stop streaming.
func (cmd *StreamStopCmd) Run(ctx *context) error { func (cmd *StreamStopCmd) Run(ctx *context) error {
_, err := ctx.Client.Stream.StopStream() // Check if the stream is already inactive
status, err := ctx.Client.Stream.GetStreamStatus()
if err != nil { if err != nil {
return err return err
} }
if !status.OutputActive {
fmt.Fprintln(ctx.Out, "Stream is already inactive.")
return nil
}
_, err = ctx.Client.Stream.StopStream()
if err != nil {
return err
}
fmt.Fprintln(ctx.Out, "Streaming stopped successfully.")
return nil return nil
} }
@@ -41,19 +65,15 @@ type StreamToggleCmd struct{} // size = 0x0
// Run executes the command to toggle streaming. // Run executes the command to toggle streaming.
func (cmd *StreamToggleCmd) Run(ctx *context) error { func (cmd *StreamToggleCmd) Run(ctx *context) error {
status, err := ctx.Client.Stream.GetStreamStatus() status, err := ctx.Client.Stream.ToggleStream()
if err != nil { if err != nil {
return err return err
} }
if status.OutputActive { if status.OutputActive {
_, err = ctx.Client.Stream.StopStream() fmt.Fprintln(ctx.Out, "Streaming started successfully.")
fmt.Fprintf(ctx.Out, "Stopping stream...\n")
} else { } else {
_, err = ctx.Client.Stream.StartStream() fmt.Fprintln(ctx.Out, "Streaming stopped successfully.")
fmt.Fprintf(ctx.Out, "Starting stream...\n")
}
if err != nil {
return err
} }
return nil return nil
} }

131
stream_test.go Normal file
View File

@@ -0,0 +1,131 @@
package main
import (
"bytes"
"strings"
"testing"
"time"
)
func TestStreamStart(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdStatus := &StreamStatusCmd{}
err := cmdStatus.Run(context)
if err != nil {
t.Fatalf("Failed to get stream status: %v", err)
}
var active bool
if strings.Contains(out.String(), "Output active: true") {
active = true
}
// Reset output buffer for the next command
out.Reset()
cmdStart := &StreamStartCmd{}
err = cmdStart.Run(context)
if err != nil {
t.Fatalf("Failed to start stream: %v", err)
}
time.Sleep(1 * time.Second) // Wait for the stream to start
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())
}
}
}
func TestStreamStop(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdStatus := &StreamStatusCmd{}
err := cmdStatus.Run(context)
if err != nil {
t.Fatalf("Failed to get stream status: %v", err)
}
var active bool
if strings.Contains(out.String(), "Output active: true") {
active = true
}
// Reset output buffer for the next command
out.Reset()
cmdStop := &StreamStopCmd{}
err = cmdStop.Run(context)
if err != nil {
t.Fatalf("Failed to stop stream: %v", err)
}
time.Sleep(1 * time.Second) // Wait for the stream to stop
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())
}
}
}
func TestStreamToggle(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdStatus := &StreamStatusCmd{}
err := cmdStatus.Run(context)
if err != nil {
t.Fatalf("Failed to get stream status: %v", err)
}
var active bool
if strings.Contains(out.String(), "Output active: true") {
active = true
}
// Reset output buffer for the next command
out.Reset()
cmdToggle := &StreamToggleCmd{}
err = cmdToggle.Run(context)
if err != nil {
t.Fatalf("Failed to toggle stream: %v", err)
}
time.Sleep(1 * time.Second) // Wait for the stream to toggle
if active {
if out.String() != "Streaming stopped successfully.\n" {
t.Fatalf("Expected 'Streaming stopped successfully.', got: %s", out.String())
}
} else {
if out.String() != "Streaming started successfully.\n" {
t.Fatalf("Expected 'Streaming started successfully.', got: %s", out.String())
}
}
}

View File

@@ -23,6 +23,8 @@ func (cmd *StudioModeEnableCmd) Run(ctx *context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to enable studio mode: %w", err) return fmt.Errorf("failed to enable studio mode: %w", err)
} }
fmt.Fprintln(ctx.Out, "Studio mode is now enabled")
return nil return nil
} }
@@ -35,6 +37,8 @@ func (cmd *StudioModeDisableCmd) Run(ctx *context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to disable studio mode: %w", err) return fmt.Errorf("failed to disable studio mode: %w", err)
} }
fmt.Fprintln(ctx.Out, "Studio mode is now disabled")
return nil return nil
} }

68
studiomode_test.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import (
"bytes"
"testing"
)
func TestStudioModeEnable(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdEnable := &StudioModeEnableCmd{}
err := cmdEnable.Run(context)
if err != nil {
t.Fatalf("failed to enable studio mode: %v", err)
}
if out.String() != "Studio mode is now enabled\n" {
t.Fatalf("expected 'Studio mode is now enabled', got: %s", out.String())
}
// Reset output buffer for the next command
out.Reset()
cmdStatus := &StudioModeStatusCmd{}
err = cmdStatus.Run(context)
if err != nil {
t.Fatalf("failed to get studio mode status: %v", err)
}
if out.String() != "Studio mode is enabled\n" {
t.Fatalf("expected 'Studio mode is enabled', got: %s", out.String())
}
}
func TestStudioModeDisable(t *testing.T) {
client, disconnect := getClient(t)
defer disconnect()
var out bytes.Buffer
context := &context{
Client: client,
Out: &out,
}
cmdDisable := &StudioModeDisableCmd{}
err := cmdDisable.Run(context)
if err != nil {
t.Fatalf("failed to disable studio mode: %v", err)
}
if out.String() != "Studio mode is now disabled\n" {
t.Fatalf("expected 'Studio mode is now disabled', got: %s", out.String())
}
// Reset output buffer for the next command
out.Reset()
cmdStatus := &StudioModeStatusCmd{}
err = cmdStatus.Run(context)
if err != nil {
t.Fatalf("failed to get studio mode status: %v", err)
}
if out.String() != "Studio mode is disabled\n" {
t.Fatalf("expected 'Studio mode is disabled', got: %s", out.String())
}
}

22
util.go Normal file
View File

@@ -0,0 +1,22 @@
// Package util provides utility functions for the application.
package main
import "strings"
func snakeCaseToTitleCase(snake string) string {
words := strings.Split(snake, "_")
for i, word := range words {
if len(word) > 0 {
words[i] = strings.ToUpper(word[:1]) + word[1:]
}
}
return strings.Join(words, " ")
}
func getEnabledMark(enabled bool) string {
if enabled {
return "\u2713" // green check mark
}
return "\u274c" // red cross mark
}

20
util_test.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import "testing"
func TestSnakeCaseToTitleCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"hello_world", "Hello World"},
{"snake_case_to_title_case", "Snake Case To Title Case"},
}
for _, test := range tests {
result := snakeCaseToTitleCase(test.input)
if result != test.expected {
t.Errorf("Expected '%s' but got '%s'", test.expected, result)
}
}
}

30
version_test.go Normal file
View 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())
}
}

View File

@@ -6,17 +6,17 @@ import (
// VirtualCamCmd handles the virtual camera commands. // VirtualCamCmd handles the virtual camera commands.
type VirtualCamCmd struct { type VirtualCamCmd struct {
Start StartVirtualCamCmd `help:"Start virtual camera." cmd:"" aliases:"s"` Start VirtualCamStartCmd `help:"Start virtual camera." cmd:"" aliases:"s"`
Stop StopVirtualCamCmd `help:"Stop virtual camera." cmd:"" aliases:"st"` Stop VirtualCamStopCmd `help:"Stop virtual camera." cmd:"" aliases:"st"`
Toggle ToggleVirtualCamCmd `help:"Toggle virtual camera." cmd:"" aliases:"tg"` Toggle VirtualCamToggleCmd `help:"Toggle virtual camera." cmd:"" aliases:"tg"`
Status StatusVirtualCamCmd `help:"Get virtual camera status." cmd:"" aliases:"ss"` Status VirtualCamStatusCmd `help:"Get virtual camera status." cmd:"" aliases:"ss"`
} }
// StartVirtualCamCmd starts the virtual camera. // VirtualCamStartCmd starts the virtual camera.
type StartVirtualCamCmd struct{} // size = 0x0 type VirtualCamStartCmd struct{} // size = 0x0
// Run executes the command to start the virtual camera. // Run executes the command to start the virtual camera.
func (c *StartVirtualCamCmd) Run(ctx *context) error { func (c *VirtualCamStartCmd) Run(ctx *context) error {
_, err := ctx.Client.Outputs.StartVirtualCam() _, err := ctx.Client.Outputs.StartVirtualCam()
if err != nil { if err != nil {
return fmt.Errorf("failed to start virtual camera: %w", err) return fmt.Errorf("failed to start virtual camera: %w", err)
@@ -25,11 +25,11 @@ func (c *StartVirtualCamCmd) Run(ctx *context) error {
return nil return nil
} }
// StopVirtualCamCmd stops the virtual camera. // VirtualCamStopCmd stops the virtual camera.
type StopVirtualCamCmd struct{} // size = 0x0 type VirtualCamStopCmd struct{} // size = 0x0
// Run executes the command to stop the virtual camera. // Run executes the command to stop the virtual camera.
func (c *StopVirtualCamCmd) Run(ctx *context) error { func (c *VirtualCamStopCmd) Run(ctx *context) error {
_, err := ctx.Client.Outputs.StopVirtualCam() _, err := ctx.Client.Outputs.StopVirtualCam()
if err != nil { if err != nil {
return fmt.Errorf("failed to stop virtual camera: %w", err) return fmt.Errorf("failed to stop virtual camera: %w", err)
@@ -38,23 +38,29 @@ func (c *StopVirtualCamCmd) Run(ctx *context) error {
return nil return nil
} }
// ToggleVirtualCamCmd toggles the virtual camera. // VirtualCamToggleCmd toggles the virtual camera.
type ToggleVirtualCamCmd struct{} // size = 0x0 type VirtualCamToggleCmd struct{} // size = 0x0
// Run executes the command to toggle the virtual camera. // Run executes the command to toggle the virtual camera.
func (c *ToggleVirtualCamCmd) Run(ctx *context) error { func (c *VirtualCamToggleCmd) Run(ctx *context) error {
_, err := ctx.Client.Outputs.ToggleVirtualCam() resp, err := ctx.Client.Outputs.ToggleVirtualCam()
if err != nil { if err != nil {
return fmt.Errorf("failed to toggle virtual camera: %w", err) return fmt.Errorf("failed to toggle virtual camera: %w", err)
} }
if resp.OutputActive {
fmt.Fprintln(ctx.Out, "Virtual camera is now active.")
} else {
fmt.Fprintln(ctx.Out, "Virtual camera is now inactive.")
}
return nil return nil
} }
// StatusVirtualCamCmd retrieves the status of the virtual camera. // VirtualCamStatusCmd retrieves the status of the virtual camera.
type StatusVirtualCamCmd struct{} // size = 0x0 type VirtualCamStatusCmd struct{} // size = 0x0
// Run executes the command to get the status of the virtual camera. // Run executes the command to get the status of the virtual camera.
func (c *StatusVirtualCamCmd) Run(ctx *context) error { func (c *VirtualCamStatusCmd) Run(ctx *context) error {
status, err := ctx.Client.Outputs.GetVirtualCamStatus() status, err := ctx.Client.Outputs.GetVirtualCamStatus()
if err != nil { if err != nil {
return fmt.Errorf("failed to get virtual camera status: %w", err) return fmt.Errorf("failed to get virtual camera status: %w", err)