commit 2037a64e983e9075caac16ad06a267ae33b48c6c Author: onyx-and-iris Date: Thu Apr 24 11:54:51 2025 +0100 first commit diff --git a/.github/workflows/golang-ci.yml b/.github/workflows/golang-ci.yml new file mode 100644 index 0000000..bd13d40 --- /dev/null +++ b/.github/workflows/golang-ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ "main" ] + paths: + - '**.go' + pull_request: + branches: [ "main" ] + paths: + - '**.go' +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + timeout-minutes: 3 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + - name: Install golangci-lint + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + - name: Run golangci-lint + run: golangci-lint run ./... \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4429e32 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: goreleaser + +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v5 + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7dd53ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Auto-generated .gitignore by gignore: github.com/onyx-and-iris/gignore + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# End of gignore: github.com/onyx-and-iris/gignore + +.envrc +*_test.go \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..0843388 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,52 @@ +run: + # timeout for analysis, e.g. 30s, 3m, default is 1m + timeout: 3m + # exclude test files + tests: true + +linters: + # Set to true runs only fast linters. + # Good option for 'lint on save', pre-commit hook or CI. + fast: true + + disable-all: true + + enable: + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - gofmt + - gofumpt + - misspell + - unparam + - gosec + - asciicheck + - errname + - gci + - godot + - goimports + - revive + +linters-settings: + gofmt: + rewrite-rules: + - pattern: 'interface{}' + replacement: 'any' + - pattern: 'a[b:len(a)]' + replacement: 'a[b:]' + + misspell: + locale: UK + + errcheck: + check-type-assertions: true + +issues: + max-same-issues: 0 + max-issues-per-linter: 0 + exclude-use-default: false + exclude: + # gosec: Duplicated errcheck checks + - G104 diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..54450c7 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,48 @@ +version: 2 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + +archives: + - formats: ['tar.gz'] + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + formats: ['zip'] + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +release: + footer: >- + + --- + + Released by [GoReleaser](https://github.com/goreleaser/goreleaser). diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f7c81be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +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/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +# [0.1.0] - 2025-04-24 + +### Added + +- Initial release. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1cef21 --- /dev/null +++ b/README.md @@ -0,0 +1,401 @@ +# gobs-cli + +A command line interface for OBS Websocket v5 + +For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md) + +## Configuration + +#### Flags + +Pass `--host`, `--port` and `--password` as flags to the root command, for example: + +```console +gobs-cli --host=localhost --port=4455 --password= --help +``` + +#### Environment Variables + +Load connection details from your environment: + +```bash +#!/usr/bin/env bash + +export OBS_HOST=localhost +export OBS_PORT=4455 +export OBS_PASSWORD= +export OBS_TIMEOUT=5 +``` + +## Commands + +### VersionCmd + +```console +gobs-cli version +``` + +### SceneCmd + +- list: List all scenes. + +```console +gobs-cli scene list +``` + +- current: Get the current scene. + - flags: + + *optional* + - --preview: Preview scene. + +```console +gobs-cli scene current + +gobs-cli scene current --preview +``` + +- switch: Switch to a scene. + - flags: + + *optional* + - --preview: Preview scene. + - args: SceneName + +```console +gobs-cli scene switch LIVE + +gobs-cli scene switch --preview LIVE +``` + +### SceneItemCmd + +- list: List all scene items. + - args: SceneName + +```console +gobs-cli sceneitem list LIVE +``` + +- show: Show scene item. + - flags: + + *optional* + - --parent: Parent group name. + - args: SceneName ItemName + +```console +gobs-cli sceneitem show START "Colour Source" +``` + +- hide: Hide scene item. + - flags: + + *optional* + - --parent: Parent group name. + - args: SceneName ItemName + +```console +gobs-cli sceneitem hide START "Colour Source" +``` + +- toggle: Toggle scene item. + - flags: + + *optional* + - --parent: Parent group name. + - args: SceneName ItemName + +```console +gobs-cli sceneitem toggle --parent=test_group START "Colour Source 3" +``` + +- visible: Get scene item visibility. + - flags: + + *optional* + - --parent: Parent group name. + - args: SceneName ItemName + +```console +gobs-cli sceneitem visible --parent=test_group START "Colour Source 4" +``` + +### GroupCmd + +- list: List all groups. + - args: SceneName + +```console +gobs-cli group list START +``` + +- show: Show group details. + - args: SceneName GroupName + +```console +gobs-cli group show START "test_group" +``` + +- hide: Hide group. + - args: SceneName GroupName + +```console +gobs-cli group hide START "test_group" +``` + +- toggle: Toggle group. + - args: SceneName GroupName + +```console +gobs-cli group toggle START "test_group" +``` + +- status: Get group status. + - args: SceneName GroupName + +```console +gobs-cli group status START "test_group" +``` + +### InputCmd + +- list: List all inputs. + - flags: + + *optional* + - --input: List all inputs. + - --output: List all outputs. + - --colour: List all colour sources. + +```console +gobs-cli input list + +gobs-cli input list --input --colour +``` + +- mute: Mute input. + - args: InputName + +```console +gobs-cli input mute "Mic/Aux" +``` + +- unmute: Unmute input. + - args: InputName + +```console +gobs-cli input unmute "Mic/Aux" +``` + +- toggle: Toggle input. + - args: InputName + +```console +gobs-cli input toggle "Mic/Aux" +``` + +### RecordCmd + +- start: Start recording. + +```console +gobs-cli record start +``` + +- stop: Stop recording. + +```console +gobs-cli record stop +``` + +- status: Get recording status. + +```console +gobs-cli record status +``` + +- toggle: Toggle recording. + +```console +gobs-cli record toggle +``` + +- pause: Pause recording. + +```console +gobs-cli record pause +``` + +- resume: Resume recording. + +```console +gobs-cli record resume +``` + +### StreamCmd + +- start: Start streaming. + +```console +gobs-cli stream start +``` + +- stop: Stop streaming. + +```console +gobs-cli stream stop +``` + +- status: Get streaming status. + +```console +gobs-cli stream status +``` + +- toggle: Toggle streaming. + +```console +gobs-cli stream toggle +``` + +### SceneCollectionCmd + +- list: List scene collections. + +```console +gobs-cli scenecollection list +``` + +- current: Get current scene collection. + +```console +gobs-cli scenecollection current +``` + +- switch: "Switch scene collection. + - args: Name + +```console +gobs-cli scenecollection switch test-collection +``` + +- create: Create scene collection. + - args: Name + +```console +gobs-cli scenecollection create test-collection +``` + +### ProfileCmd + +- list: List profiles. + +```console +gobs-cli profile list +``` + +- current: Get current profile. + +```console +gobs-cli profile current +``` + +- switch: Switch profile. + - args: Name + +```console +gobs-cli profile switch test-collection +``` + +- create: Create profile. + - args: Name + +```console +gobs-cli profile create test-collection +``` + +- remove: Remove profile. + - args: Name + +```console +gobs-cli profile create test-collection +``` + +### ReplayBufferCmd + +- start: Start replay buffer. + +```console +gobs-cli replaybuffer start +``` + +- stop: Stop replay buffer. + +```console +gobs-cli replaybuffer stop +``` + +- status: Get replay buffer status. + +```console +gobs-cli replaybuffer status +``` + +- save: Save replay buffer. + +```console +gobs-cli replaybuffer save +``` + +### StudioModeCmd + +- enable: Enable studio mode. + +```console +gobs-cli studiomode enable +``` + +- disable: Disable studio mode. + +```console +gobs-cli studiomode disable +``` + +- toggle: Toggle studio mode. + +```console +gobs-cli studiomode toggle +``` + +- status: Get studio mode status. + +```console +gobs-cli studiomode status +``` + +### VirtualCamCmd + +- start: Start virtual camera. + +```console +gobs-cli virtualcam start +``` + +- stop: Stop virtual camera. + +```console +gobs-cli virtualcam stop +``` + +- toggle: Toggle virtual camera. + +```console +gobs-cli virtualcam toggle +``` + +- status: Get virtual camera status. + +```console +gobs-cli virtualcam status +``` \ No newline at end of file diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..76956d4 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,52 @@ +version: '3' + +vars: + PROGRAM: gobs-cli + SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}' + BIN_DIR: bin + +tasks: + default: + desc: Build the gobs-cli project + cmds: + - task: build + + build: + desc: Build the gobs-cli project + deps: [vet] + cmds: + - task: build-windows + - task: build-linux + + vet: + desc: Vet the code + deps: [fmt] + cmds: + - go vet ./... + + fmt: + desc: Fmt the code + cmds: + - go fmt ./... + + build-windows: + desc: Build the gobs-cli project for Windows + cmds: + - GOOS=windows GOARCH=amd64 go build -o {{.BIN_DIR}}/{{.PROGRAM}}_windows_amd64.exe + internal: true + + build-linux: + desc: Build the gobs-cli project for Linux + cmds: + - GOOS=linux GOARCH=amd64 go build -o {{.BIN_DIR}}/{{.PROGRAM}}_linux_amd64 + internal: true + + test: + desc: Run tests + cmds: + - go test ./... + + clean: + desc: Clean the build artifacts + cmds: + - '{{.SHELL}} rm -r {{.BIN_DIR}}' diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..86091ef --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/onyx-and-iris/gobs-cli + +go 1.24.0 + +require ( + github.com/alecthomas/kong v1.10.0 + github.com/andreykaipov/goobs v1.5.6 +) + +require ( + github.com/buger/jsonparser v1.1.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/logutils v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mmcloughlin/profile v0.1.1 // indirect + github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9cf9be2 --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +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/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +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/andreykaipov/goobs v1.5.6 h1:eIkEqYN99+2VJvmlY/56Ah60nkRKS6efMQvpM3oUgPQ= +github.com/andreykaipov/goobs v1.5.6/go.mod h1:iSZP93FJ4d9X/U1x4DD4IyILLtig+vViqZWBGjLywcY= +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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +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/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +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/mmcloughlin/profile v0.1.1 h1:jhDmAqPyebOsVDOCICJoINoLb/AnLBaUw58nFzxWS2w= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/group.go b/group.go new file mode 100644 index 0000000..3e93ddd --- /dev/null +++ b/group.go @@ -0,0 +1,173 @@ +package main + +import ( + "fmt" + + "github.com/andreykaipov/goobs/api/requests/sceneitems" +) + +// GroupCmd provides commands to manage groups in OBS Studio. +type GroupCmd struct { + List GroupListCmd `cmd:"" help:"List all groups." aliases:"ls"` + Show GroupShowCmd `cmd:"" help:"Show group details." aliases:"sh"` + Hide GroupHideCmd `cmd:"" help:"Hide group." aliases:"h"` + Toggle GroupToggleCmd `cmd:"" help:"Toggle group." aliases:"tg"` + Status GroupStatusCmd `cmd:"" help:"Get group status." aliases:"ss"` +} + +// GroupListCmd provides a command to list all groups in a scene. +type GroupListCmd struct { + SceneName string `arg:"" help:"Name of the scene to list groups from."` +} + +// Run executes the command to list all groups in a scene. +func (cmd *GroupListCmd) Run(ctx *context) error { + resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams(). + WithSceneName(cmd.SceneName)) + if err != nil { + return fmt.Errorf("failed to get scene item list: %w", err) + } + for _, item := range resp.SceneItems { + if item.IsGroup { + fmt.Fprintf(ctx.Out, "Group ID: %d, Source Name: %s\n", item.SceneItemID, item.SourceName) + } + } + return nil +} + +// GroupShowCmd provides a command to show a group in a scene. +type GroupShowCmd struct { + SceneName string `arg:"" help:"Name of the scene to show group from."` + GroupName string `arg:"" help:"Name of the group to show."` +} + +// Run executes the command to show a group in a scene. +func (cmd *GroupShowCmd) Run(ctx *context) error { + resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams(). + WithSceneName(cmd.SceneName)) + if err != nil { + return fmt.Errorf("failed to get scene item list: %w", err) + } + + var found bool + for _, item := range resp.SceneItems { + if item.IsGroup && item.SourceName == cmd.GroupName { + _, err := ctx.Client.SceneItems.SetSceneItemEnabled(sceneitems.NewSetSceneItemEnabledParams(). + WithSceneName(cmd.SceneName). + WithSceneItemId(item.SceneItemID). + WithSceneItemEnabled(true)) + if err != nil { + return fmt.Errorf("failed to set scene item enabled: %w", err) + } + fmt.Fprintf(ctx.Out, "Group %s is now shown.\n", cmd.GroupName) + found = true + break + } + } + if !found { + return fmt.Errorf("group '%s' not found", cmd.GroupName) + } + return nil +} + +// GroupHideCmd provides a command to hide a group in a scene. +type GroupHideCmd struct { + SceneName string `arg:"" help:"Name of the scene to hide group from."` + GroupName string `arg:"" help:"Name of the group to hide."` +} + +// Run executes the command to hide a group in a scene. +func (cmd *GroupHideCmd) Run(ctx *context) error { + resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams(). + WithSceneName(cmd.SceneName)) + if err != nil { + return fmt.Errorf("failed to get scene item list: %w", err) + } + + var found bool + for _, item := range resp.SceneItems { + if item.IsGroup && item.SourceName == cmd.GroupName { + _, err := ctx.Client.SceneItems.SetSceneItemEnabled(sceneitems.NewSetSceneItemEnabledParams(). + WithSceneName(cmd.SceneName). + WithSceneItemId(item.SceneItemID). + WithSceneItemEnabled(false)) + if err != nil { + return fmt.Errorf("failed to set scene item enabled: %w", err) + } + fmt.Fprintf(ctx.Out, "Group %s is now hidden.\n", cmd.GroupName) + found = true + break + } + } + if !found { + return fmt.Errorf("group '%s' not found", cmd.GroupName) + } + return nil +} + +// GroupToggleCmd provides a command to toggle a group in a scene. +type GroupToggleCmd struct { + SceneName string `arg:"" help:"Name of the scene to toggle group from."` + GroupName string `arg:"" help:"Name of the group to toggle."` +} + +// Run executes the command to toggle a group in a scene. +func (cmd *GroupToggleCmd) Run(ctx *context) error { + resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams(). + WithSceneName(cmd.SceneName)) + if err != nil { + return fmt.Errorf("failed to get scene item list: %w", err) + } + + var found bool + for _, item := range resp.SceneItems { + if item.IsGroup && item.SourceName == cmd.GroupName { + newState := !item.SceneItemEnabled + _, err := ctx.Client.SceneItems.SetSceneItemEnabled(sceneitems.NewSetSceneItemEnabledParams(). + WithSceneName(cmd.SceneName). + WithSceneItemId(item.SceneItemID). + WithSceneItemEnabled(newState)) + if err != nil { + return fmt.Errorf("failed to set scene item enabled: %w", err) + } + if newState { + fmt.Fprintf(ctx.Out, "Group %s is now shown.\n", cmd.GroupName) + } else { + fmt.Fprintf(ctx.Out, "Group %s is now hidden.\n", cmd.GroupName) + } + found = true + break + } + } + if !found { + return fmt.Errorf("group '%s' not found", cmd.GroupName) + } + + return nil +} + +// GroupStatusCmd provides a command to get the status of a group in a scene. +type GroupStatusCmd struct { + SceneName string `arg:"" help:"Name of the scene to get group status from."` + GroupName string `arg:"" help:"Name of the group to get status."` +} + +// Run executes the command to get the status of a group in a scene. +func (cmd *GroupStatusCmd) Run(ctx *context) error { + resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams(). + WithSceneName(cmd.SceneName)) + if err != nil { + return fmt.Errorf("failed to get scene item list: %w", err) + } + for _, item := range resp.SceneItems { + if item.IsGroup && item.SourceName == cmd.GroupName { + if item.SceneItemEnabled { + fmt.Fprintf(ctx.Out, "Group %s is shown.\n", cmd.GroupName) + } else { + fmt.Fprintf(ctx.Out, "Group %s is hidden.\n", cmd.GroupName) + } + return nil + } + } + return fmt.Errorf("group '%s' not found", cmd.GroupName) +} diff --git a/input.go b/input.go new file mode 100644 index 0000000..cc0301a --- /dev/null +++ b/input.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/andreykaipov/goobs/api/requests/inputs" +) + +// InputCmd provides commands to manage inputs in OBS Studio. +type InputCmd struct { + List InputListCmd `cmd:"" help:"List all inputs." aliases:"ls"` + Mute InputMuteCmd `cmd:"" help:"Mute input." aliases:"m"` + Unmute InputUnmuteCmd `cmd:"" help:"Unmute input." aliases:"um"` + Toggle InputToggleCmd `cmd:"" help:"Toggle input." aliases:"tg"` +} + +// InputListCmd provides a command to list all inputs. +type InputListCmd struct { + Input bool `flag:"" help:"List all inputs." aliases:"i"` + Output bool `flag:"" help:"List all outputs." aliases:"o"` + Colour bool `flag:"" help:"List all colour sources." aliases:"c"` +} + +// Run executes the command to list all inputs. +func (cmd *InputListCmd) Run(ctx *context) error { + resp, err := ctx.Client.Inputs.GetInputList(inputs.NewGetInputListParams()) + if err != nil { + return err + } + for _, input := range resp.Inputs { + if cmd.Input && strings.Contains(input.InputKind, "input") { + fmt.Fprintln(ctx.Out, "Input:", input.InputName) + } + if cmd.Output && strings.Contains(input.InputKind, "output") { + fmt.Fprintln(ctx.Out, "Output:", input.InputName) + } + if cmd.Colour && strings.Contains(input.InputKind, "color") { // nolint + fmt.Fprintln(ctx.Out, "Colour Source:", input.InputName) + } + + if !cmd.Input && !cmd.Output && !cmd.Colour { + fmt.Fprintln(ctx.Out, "Source:", input.InputName) + } + } + return nil +} + +// InputMuteCmd provides a command to mute an input. +type InputMuteCmd struct { + InputName string `arg:"" help:"Name of the input to mute."` +} + +// Run executes the command to mute an input. +func (cmd *InputMuteCmd) Run(ctx *context) error { + _, err := ctx.Client.Inputs.SetInputMute( + inputs.NewSetInputMuteParams().WithInputName(cmd.InputName).WithInputMuted(true), + ) + if err != nil { + return fmt.Errorf("failed to mute input: %w", err) + } + + fmt.Fprintf(ctx.Out, "Muted input: %s\n", cmd.InputName) + return nil +} + +// InputUnmuteCmd provides a command to unmute an input. +type InputUnmuteCmd struct { + InputName string `arg:"" help:"Name of the input to unmute."` +} + +// Run executes the command to unmute an input. +func (cmd *InputUnmuteCmd) Run(ctx *context) error { + _, err := ctx.Client.Inputs.SetInputMute( + inputs.NewSetInputMuteParams().WithInputName(cmd.InputName).WithInputMuted(false), + ) + if err != nil { + return fmt.Errorf("failed to unmute input: %w", err) + } + + fmt.Fprintf(ctx.Out, "Unmuted input: %s\n", cmd.InputName) + return nil +} + +// InputToggleCmd provides a command to toggle the mute state of an input. +type InputToggleCmd struct { + InputName string `arg:"" help:"Name of the input to toggle."` +} + +// Run executes the command to toggle the mute state of an input. +func (cmd *InputToggleCmd) Run(ctx *context) error { + // Get the current mute state of the input + resp, err := ctx.Client.Inputs.GetInputMute( + inputs.NewGetInputMuteParams().WithInputName(cmd.InputName), + ) + if err != nil { + return fmt.Errorf("failed to get input mute state: %w", err) + } + // Toggle the mute state + newMuteState := !resp.InputMuted + _, err = ctx.Client.Inputs.SetInputMute( + inputs.NewSetInputMuteParams().WithInputName(cmd.InputName).WithInputMuted(newMuteState), + ) + if err != nil { + return fmt.Errorf("failed to toggle input mute state: %w", err) + } + + if newMuteState { + fmt.Fprintf(ctx.Out, "Muted input: %s\n", cmd.InputName) + } else { + fmt.Fprintf(ctx.Out, "Unmuted input: %s\n", cmd.InputName) + } + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..517d39a --- /dev/null +++ b/main.go @@ -0,0 +1,94 @@ +// Package main provides a command-line interface (CLI) tool for interacting with OBS WebSocket. +// It allows users to manage various aspects of OBS, such as scenes, inputs, recording, streaming, +// and more, by leveraging the goobs library for communication with the OBS WebSocket server. +package main + +import ( + "fmt" + "io" + "os" + "time" + + "github.com/alecthomas/kong" + "github.com/andreykaipov/goobs" +) + +// ObsConfig holds the configuration for connecting to the OBS WebSocket server. +type ObsConfig struct { + Host string `flag:"host" help:"Host to connect to." default:"localhost" env:"OBS_HOST"` + Port int `flag:"port" help:"Port to connect to." default:"4455" env:"OBS_PORT"` + Password string `flag:"password" help:"Password for authentication." default:"" env:"OBS_PASSWORD"` + Timeout int `flag:"timeout" help:"Timeout in seconds." default:"5" env:"OBS_TIMEOUT"` +} + +// cli is the main command line interface structure. +// It embeds the ObsConfig struct to inherit its fields and flags. +type cli struct { + ObsConfig `embed:"" help:"OBS WebSocket configuration."` + + Version VersionCmd `help:"Show version." cmd:"" aliases:"v"` + Scene SceneCmd `help:"Manage scenes." cmd:"" aliases:"sc"` + Sceneitem SceneItemCmd `help:"Manage scene items." cmd:"" aliases:"si"` + Group GroupCmd `help:"Manage groups." cmd:"" aliases:"g"` + Input InputCmd `help:"Manage inputs." cmd:"" aliases:"i"` + Record RecordCmd `help:"Manage recording." cmd:"" aliases:"rec"` + Stream StreamCmd `help:"Manage streaming." cmd:"" aliases:"st"` + Scenecollection SceneCollectionCmd `help:"Manage scene collections." cmd:"" aliases:"scn"` + Profile ProfileCmd `help:"Manage profiles." cmd:"" aliases:"p"` + Replaybuffer ReplayBufferCmd `help:"Manage replay buffer." cmd:"" aliases:"rb"` + Studiomode StudioModeCmd `help:"Manage studio mode." cmd:"" aliases:"sm"` + Virtualcam VirtualCamCmd `help:"Manage virtual camera." cmd:"" aliases:"vc"` +} + +type context struct { + Client *goobs.Client + Out io.Writer +} + +func main() { + var client *goobs.Client + cli := cli{} + ctx := kong.Parse( + &cli, + kong.Name("GOBS-CLI"), + kong.Description("A command line tool to interact with OBS Websocket."), + ) + + client, err := connectObs(cli.ObsConfig) + if err != nil { + ctx.FatalIfErrorf(err) + } + + ctx.Bind(&context{ + Client: client, + Out: os.Stdout, + }) + + ctx.FatalIfErrorf(run(ctx, client)) +} + +// connectObs creates a new OBS client and connects to the OBS WebSocket server. +func connectObs(cfg ObsConfig) (*goobs.Client, error) { + client, err := goobs.New( + fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), + goobs.WithPassword(cfg.Password), + goobs.WithResponseTimeout(time.Duration(cfg.Timeout)*time.Second), + ) + if err != nil { + return nil, err + } + return client, nil +} + +// run executes the command line interface. +// It disconnects the OBS client after the command is executed. +func run(ctx *kong.Context, client *goobs.Client) error { + defer func() error { + if err := client.Disconnect(); err != nil { + return fmt.Errorf("failed to disconnect from OBS: %w", err) + } + return nil + }() + + return ctx.Run() +} diff --git a/profile.go b/profile.go new file mode 100644 index 0000000..96f4cab --- /dev/null +++ b/profile.go @@ -0,0 +1,131 @@ +package main + +import ( + "fmt" + "slices" + + "github.com/andreykaipov/goobs/api/requests/config" +) + +// ProfileCmd provides commands to manage profiles in OBS Studio. +type ProfileCmd struct { + List ListProfileCmd `help:"List profiles." cmd:"" aliases:"ls"` + Current CurrentProfileCmd `help:"Get current profile." cmd:"" aliases:"c"` + Switch SwitchProfileCmd `help:"Switch profile." cmd:"" aliases:"sw"` + Create CreateProfileCmd `help:"Create profile." cmd:"" aliases:"cr"` + Remove RemoveProfileCmd `help:"Remove profile." cmd:"" aliases:"rm"` +} + +// ListProfileCmd provides a command to list all profiles. +type ListProfileCmd struct{} // size = 0x0 + +// Run executes the command to list all profiles. +func (cmd *ListProfileCmd) Run(ctx *context) error { + profiles, err := ctx.Client.Config.GetProfileList() + if err != nil { + return err + } + + for _, profile := range profiles.Profiles { + fmt.Fprintln(ctx.Out, profile) + } + + return nil +} + +// CurrentProfileCmd provides a command to get the current profile. +type CurrentProfileCmd struct{} // size = 0x0 + +// Run executes the command to get the current profile. +func (cmd *CurrentProfileCmd) Run(ctx *context) error { + profiles, err := ctx.Client.Config.GetProfileList() + if err != nil { + return err + } + fmt.Fprintf(ctx.Out, "Current profile: %s\n", profiles.CurrentProfileName) + + return nil +} + +// SwitchProfileCmd provides a command to switch to a different profile. +type SwitchProfileCmd struct { + Name string `arg:"" help:"Name of the profile to switch to." required:""` +} + +// Run executes the command to switch to a different profile. +func (cmd *SwitchProfileCmd) Run(ctx *context) error { + profiles, err := ctx.Client.Config.GetProfileList() + if err != nil { + return err + } + current := profiles.CurrentProfileName + + if current == cmd.Name { + return nil + } + + _, err = ctx.Client.Config.SetCurrentProfile(config.NewSetCurrentProfileParams().WithProfileName(cmd.Name)) + if err != nil { + return err + } + + fmt.Fprintf(ctx.Out, "Switched from profile %s to %s\n", current, cmd.Name) + + return nil +} + +// CreateProfileCmd provides a command to create a new profile. +type CreateProfileCmd struct { + Name string `arg:"" help:"Name of the profile to create." required:""` +} + +// Run executes the command to create a new profile. +func (cmd *CreateProfileCmd) Run(ctx *context) error { + profiles, err := ctx.Client.Config.GetProfileList() + if err != nil { + return err + } + + if slices.Contains(profiles.Profiles, cmd.Name) { + return fmt.Errorf("profile %s already exists", cmd.Name) + } + + _, err = ctx.Client.Config.CreateProfile(config.NewCreateProfileParams().WithProfileName(cmd.Name)) + if err != nil { + return err + } + + fmt.Fprintf(ctx.Out, "Created profile: %s\n", cmd.Name) + + return nil +} + +// RemoveProfileCmd provides a command to remove an existing profile. +type RemoveProfileCmd struct { + Name string `arg:"" help:"Name of the profile to delete." required:""` +} + +// Run executes the command to remove an existing profile. +func (cmd *RemoveProfileCmd) Run(ctx *context) error { + profiles, err := ctx.Client.Config.GetProfileList() + if err != nil { + return err + } + + if !slices.Contains(profiles.Profiles, cmd.Name) { + return fmt.Errorf("profile %s does not exist", cmd.Name) + } + + if profiles.CurrentProfileName == cmd.Name { + return fmt.Errorf("cannot delete current profile %s", cmd.Name) + } + + _, err = ctx.Client.Config.RemoveProfile(config.NewRemoveProfileParams().WithProfileName(cmd.Name)) + if err != nil { + return err + } + + fmt.Fprintf(ctx.Out, "Deleted profile: %s\n", cmd.Name) + + return nil +} diff --git a/record.go b/record.go new file mode 100644 index 0000000..ff980a8 --- /dev/null +++ b/record.go @@ -0,0 +1,119 @@ +package main + +import ( + "fmt" +) + +// RecordCmd handles the recording commands. +type RecordCmd struct { + Start RecordStartCmd `cmd:"" help:"Start recording." aliases:"s"` + Stop RecordStopCmd `cmd:"" help:"Stop recording." aliases:"st"` + Toggle RecordToggleCmd `cmd:"" help:"Toggle recording." aliases:"tg"` + Pause RecordPauseCmd `cmd:"" help:"Pause recording." aliases:"p"` + Resume RecordResumeCmd `cmd:"" help:"Resume recording." aliases:"r"` +} + +// RecordStartCmd starts the recording. +type RecordStartCmd struct{} // size = 0x0 + +// Run executes the command to start recording. +func (cmd *RecordStartCmd) Run(ctx *context) error { + _, err := ctx.Client.Record.StartRecord() + if err != nil { + return err + } + fmt.Fprintln(ctx.Out, "Recording started successfully.") + return nil +} + +// RecordStopCmd stops the recording. +type RecordStopCmd struct{} // size = 0x0 + +// Run executes the command to stop recording. +func (cmd *RecordStopCmd) Run(ctx *context) error { + _, err := ctx.Client.Record.StopRecord() + if err != nil { + return err + } + fmt.Fprintln(ctx.Out, "Recording stopped successfully.") + return nil +} + +// RecordToggleCmd toggles the recording state. +type RecordToggleCmd struct{} // size = 0x0 + +// Run executes the command to toggle recording. +func (cmd *RecordToggleCmd) Run(ctx *context) error { + // Check if recording is in progress + status, err := ctx.Client.Record.GetRecordStatus() + if err != nil { + return err + } + + if status.OutputActive { + _, err = ctx.Client.Record.StopRecord() + if err != nil { + return err + } + fmt.Fprintln(ctx.Out, "Recording stopped successfully.") + } else { + _, err = ctx.Client.Record.StartRecord() + if err != nil { + return err + } + fmt.Fprintln(ctx.Out, "Recording started successfully.") + } + return nil +} + +// RecordPauseCmd pauses the recording. +type RecordPauseCmd struct{} // size = 0x0 + +// Run executes the command to pause recording. +func (cmd *RecordPauseCmd) Run(ctx *context) error { + // Check if recording in progress and not already paused + status, err := ctx.Client.Record.GetRecordStatus() + if err != nil { + return err + } + if !status.OutputActive { + return fmt.Errorf("recording is not in progress") + } + if status.OutputPaused { + return fmt.Errorf("recording is already paused") + } + + _, err = ctx.Client.Record.PauseRecord() + if err != nil { + return err + } + + fmt.Fprintln(ctx.Out, "Recording paused successfully.") + return nil +} + +// RecordResumeCmd resumes the recording. +type RecordResumeCmd struct{} // size = 0x0 + +// Run executes the command to resume recording. +func (cmd *RecordResumeCmd) Run(ctx *context) error { + // Check if recording in progress and not already resumed + status, err := ctx.Client.Record.GetRecordStatus() + if err != nil { + return err + } + if !status.OutputActive { + return fmt.Errorf("recording is not in progress") + } + if !status.OutputPaused { + return fmt.Errorf("recording is not paused") + } + + _, err = ctx.Client.Record.ResumeRecord() + if err != nil { + return err + } + + fmt.Fprintln(ctx.Out, "Recording resumed successfully.") + return nil +} diff --git a/replaybuffer.go b/replaybuffer.go new file mode 100644 index 0000000..b776e43 --- /dev/null +++ b/replaybuffer.go @@ -0,0 +1,63 @@ +package main + +import ( + "fmt" +) + +// ReplayBufferCmd handles the recording commands. +type ReplayBufferCmd struct { + Start ReplayBufferStartCmd `help:"Start replay buffer." cmd:"" aliases:"s"` + Stop ReplayBufferStopCmd `help:"Stop replay buffer." cmd:"" aliases:"st"` + Status ReplayBufferStatusCmd `help:"Get replay buffer status." cmd:"" aliases:"ss"` + Save ReplayBufferSaveCmd `help:"Save replay buffer." cmd:"" aliases:"sv"` +} + +// ReplayBufferStartCmd starts the replay buffer. +type ReplayBufferStartCmd struct{} // size = 0x0 + +// Run executes the command to start the replay buffer. +func (cmd *ReplayBufferStartCmd) Run(ctx *context) error { + _, err := ctx.Client.Outputs.StartReplayBuffer() + return err +} + +// ReplayBufferStopCmd stops the replay buffer. +type ReplayBufferStopCmd struct{} // size = 0x0 + +// Run executes the command to stop the replay buffer. +func (cmd *ReplayBufferStopCmd) Run(ctx *context) error { + _, err := ctx.Client.Outputs.StopReplayBuffer() + return err +} + +// ReplayBufferStatusCmd retrieves the status of the replay buffer. +type ReplayBufferStatusCmd struct{} // size = 0x0 + +// Run executes the command to get the replay buffer status. +func (cmd *ReplayBufferStatusCmd) Run(ctx *context) error { + status, err := ctx.Client.Outputs.GetReplayBufferStatus() + if err != nil { + return err + } + + if status.OutputActive { + fmt.Fprintln(ctx.Out, "Replay buffer is active.") + } else { + fmt.Fprintln(ctx.Out, "Replay buffer is not active.") + } + return nil +} + +// ReplayBufferSaveCmd saves the replay buffer. +type ReplayBufferSaveCmd struct{} // size = 0x0 + +// Run executes the command to save the replay buffer. +func (cmd *ReplayBufferSaveCmd) Run(ctx *context) error { + _, err := ctx.Client.Outputs.SaveReplayBuffer() + if err != nil { + return fmt.Errorf("failed to save replay buffer: %w", err) + } + + fmt.Fprintln(ctx.Out, "Replay buffer saved") + return nil +} diff --git a/scene.go b/scene.go new file mode 100644 index 0000000..a2fbb48 --- /dev/null +++ b/scene.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "slices" + + "github.com/andreykaipov/goobs/api/requests/scenes" +) + +// SceneCmd provides commands to manage scenes in OBS Studio. +type SceneCmd struct { + List SceneListCmd `cmd:"" help:"List all scenes." aliases:"ls"` + Current SceneCurrentCmd `cmd:"" help:"Get the current scene." aliases:"c"` + Switch SceneSwitchCmd `cmd:"" help:"Switch to a scene." aliases:"sw"` +} + +// SceneListCmd provides a command to list all scenes. +type SceneListCmd struct{} // size = 0x0 + +// Run executes the command to list all scenes. +func (cmd *SceneListCmd) Run(ctx *context) error { + scenes, err := ctx.Client.Scenes.GetSceneList() + if err != nil { + return err + } + + slices.Reverse(scenes.Scenes) + for _, scene := range scenes.Scenes { + fmt.Fprintln(ctx.Out, scene.SceneName) + } + return nil +} + +// SceneCurrentCmd provides a command to get the current scene. +type SceneCurrentCmd struct { + Preview bool `flag:"" help:"Preview scene."` +} + +// Run executes the command to get the current scene. +func (cmd *SceneCurrentCmd) Run(ctx *context) error { + if cmd.Preview { + scene, err := ctx.Client.Scenes.GetCurrentPreviewScene() + if err != nil { + return err + } + fmt.Fprintln(ctx.Out, scene.SceneName) + } else { + scene, err := ctx.Client.Scenes.GetCurrentProgramScene() + if err != nil { + return err + } + fmt.Fprintln(ctx.Out, scene.SceneName) + } + return nil +} + +// SceneSwitchCmd provides a command to switch to a different scene. +type SceneSwitchCmd struct { + Preview bool `flag:"" help:"Preview scene."` + NewScene string ` help:"Scene name to switch to." arg:""` +} + +// Run executes the command to switch to a different scene. +func (cmd *SceneSwitchCmd) Run(ctx *context) error { + if cmd.Preview { + _, err := ctx.Client.Scenes.SetCurrentPreviewScene(scenes.NewSetCurrentPreviewSceneParams(). + WithSceneName(cmd.NewScene)) + if err != nil { + return err + } + + fmt.Fprintln(ctx.Out, "Switched to preview scene:", cmd.NewScene) + } else { + _, err := ctx.Client.Scenes.SetCurrentProgramScene(scenes.NewSetCurrentProgramSceneParams(). + WithSceneName(cmd.NewScene)) + if err != nil { + return err + } + + fmt.Fprintln(ctx.Out, "Switched to program scene:", cmd.NewScene) + } + return nil +} diff --git a/scenecollection.go b/scenecollection.go new file mode 100644 index 0000000..7663ef3 --- /dev/null +++ b/scenecollection.go @@ -0,0 +1,93 @@ +package main + +import ( + "fmt" + + "github.com/andreykaipov/goobs/api/requests/config" +) + +// SceneCollectionCmd provides commands to manage scene collections in OBS Studio. +type SceneCollectionCmd struct { + List ListSceneCollectionCmd `help:"List scene collections." cmd:"" aliases:"ls"` + Current CurrentSceneCollectionCmd `help:"Get current scene collection." cmd:"" aliases:"c"` + Switch SwitchSceneCollectionCmd `help:"Switch scene collection." cmd:"" aliases:"sw"` + Create CreateSceneCollectionCmd `help:"Create scene collection." cmd:"" aliases:"cr"` +} + +// ListSceneCollectionCmd provides a command to list all scene collections. +type ListSceneCollectionCmd struct{} // size = 0x0 + +// Run executes the command to list all scene collections. +func (cmd *ListSceneCollectionCmd) Run(ctx *context) error { + collections, err := ctx.Client.Config.GetSceneCollectionList() + if err != nil { + return fmt.Errorf("failed to get scene collection list: %w", err) + } + + for _, collection := range collections.SceneCollections { + fmt.Fprintln(ctx.Out, collection) + } + + return nil +} + +// CurrentSceneCollectionCmd provides a command to get the current scene collection. +type CurrentSceneCollectionCmd struct{} // size = 0x0 + +// Run executes the command to get the current scene collection. +func (cmd *CurrentSceneCollectionCmd) Run(ctx *context) error { + collections, err := ctx.Client.Config.GetSceneCollectionList() + if err != nil { + return fmt.Errorf("failed to get scene collection list: %w", err) + } + fmt.Fprintln(ctx.Out, collections.CurrentSceneCollectionName) + + return nil +} + +// SwitchSceneCollectionCmd provides a command to switch to a different scene collection. +type SwitchSceneCollectionCmd struct { + Name string `arg:"" help:"Name of the scene collection to switch to." required:""` +} + +// Run executes the command to switch to a different scene collection. +func (cmd *SwitchSceneCollectionCmd) Run(ctx *context) error { + collections, err := ctx.Client.Config.GetSceneCollectionList() + if err != nil { + return err + } + current := collections.CurrentSceneCollectionName + + if current == cmd.Name { + return fmt.Errorf("scene collection %s is already active", cmd.Name) + } + + _, err = ctx.Client.Config.SetCurrentSceneCollection( + config.NewSetCurrentSceneCollectionParams().WithSceneCollectionName(cmd.Name), + ) + if err != nil { + return fmt.Errorf("failed to switch scene collection: %w", err) + } + + fmt.Fprintf(ctx.Out, "Switched to scene collection: %s\n", cmd.Name) + + return nil +} + +// CreateSceneCollectionCmd provides a command to create a new scene collection. +type CreateSceneCollectionCmd struct { + Name string `arg:"" help:"Name of the scene collection to create." required:""` +} + +// Run executes the command to create a new scene collection. +func (cmd *CreateSceneCollectionCmd) Run(ctx *context) error { + _, err := ctx.Client.Config.CreateSceneCollection( + config.NewCreateSceneCollectionParams().WithSceneCollectionName(cmd.Name), + ) + if err != nil { + return fmt.Errorf("failed to create scene collection: %w", err) + } + + fmt.Fprintf(ctx.Out, "Created scene collection: %s\n", cmd.Name) + return nil +} diff --git a/sceneitem.go b/sceneitem.go new file mode 100644 index 0000000..3fc826c --- /dev/null +++ b/sceneitem.go @@ -0,0 +1,183 @@ +package main + +import ( + "fmt" + + "github.com/andreykaipov/goobs" + "github.com/andreykaipov/goobs/api/requests/sceneitems" +) + +// SceneItemCmd provides commands to manage scene items in OBS Studio. +type SceneItemCmd struct { + List SceneItemListCmd `cmd:"" help:"List all scene items." aliases:"ls"` + Show SceneItemShowCmd `cmd:"" help:"Show scene item." aliases:"sh"` + Hide SceneItemHideCmd `cmd:"" help:"Hide scene item." aliases:"h"` + Toggle SceneItemToggleCmd `cmd:"" help:"Toggle scene item." aliases:"tg"` + Visible SceneItemVisibleCmd `cmd:"" help:"Get scene item visibility." aliases:"v"` +} + +// SceneItemListCmd provides a command to list all scene items in a scene. +type SceneItemListCmd struct { + SceneName string `arg:"" help:"Scene name."` +} + +// Run executes the command to list all scene items in a scene. +func (cmd *SceneItemListCmd) Run(ctx *context) error { + resp, err := ctx.Client.SceneItems.GetSceneItemList(sceneitems.NewGetSceneItemListParams(). + WithSceneName(cmd.SceneName)) + if err != nil { + 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) + } + return nil +} + +func getSceneNameAndItemID( + client *goobs.Client, + sceneName string, + itemName string, + parent string, +) (string, int, error) { + if parent != "" { + resp, err := client.SceneItems.GetGroupSceneItemList(sceneitems.NewGetGroupSceneItemListParams(). + WithSceneName(parent)) + if err != nil { + return "", 0, err + } + for _, item := range resp.SceneItems { + if item.SourceName == itemName { + return parent, int(item.SceneItemID), nil + } + } + return "", 0, fmt.Errorf("item '%s' not found in scene '%s'", itemName, sceneName) + } + + itemID, err := client.SceneItems.GetSceneItemId(sceneitems.NewGetSceneItemIdParams(). + WithSceneName(sceneName). + WithSourceName(itemName)) + if err != nil { + return "", 0, err + } + return sceneName, int(itemID.SceneItemId), nil +} + +// SceneItemShowCmd provides a command to show a scene item. +type SceneItemShowCmd struct { + Parent string `flag:"" help:"Parent group name."` + + SceneName string `arg:"" help:"Scene name."` + ItemName string `arg:"" help:"Item name."` +} + +// Run executes the command to show a scene item. +func (cmd *SceneItemShowCmd) Run(ctx *context) error { + sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent) + if err != nil { + return err + } + + _, err = ctx.Client.SceneItems.SetSceneItemEnabled(sceneitems.NewSetSceneItemEnabledParams(). + WithSceneName(sceneName). + WithSceneItemId(sceneItemID). + WithSceneItemEnabled(true)) + if err != nil { + return err + } + return nil +} + +// SceneItemHideCmd provides a command to hide a scene item. +type SceneItemHideCmd struct { + Parent string `flag:"" help:"Parent group name."` + + SceneName string `arg:"" help:"Scene name."` + ItemName string `arg:"" help:"Item name."` +} + +// Run executes the command to hide a scene item. +func (cmd *SceneItemHideCmd) Run(ctx *context) error { + sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent) + if err != nil { + return err + } + + _, err = ctx.Client.SceneItems.SetSceneItemEnabled(sceneitems.NewSetSceneItemEnabledParams(). + WithSceneName(sceneName). + WithSceneItemId(sceneItemID). + WithSceneItemEnabled(false)) + if err != nil { + return err + } + return nil +} + +// getItemEnabled retrieves the enabled status of a scene item. +func getItemEnabled(client *goobs.Client, sceneName string, itemID int) (bool, error) { + item, err := client.SceneItems.GetSceneItemEnabled(sceneitems.NewGetSceneItemEnabledParams(). + WithSceneName(sceneName). + WithSceneItemId(itemID)) + if err != nil { + return false, err + } + return item.SceneItemEnabled, nil +} + +// SceneItemToggleCmd provides a command to toggle the visibility of a scene item. +type SceneItemToggleCmd struct { + Parent string `flag:"" help:"Parent group name."` + + SceneName string `arg:"" help:"Scene name."` + ItemName string `arg:"" help:"Item name."` +} + +// Run executes the command to toggle the visibility of a scene item. +func (cmd *SceneItemToggleCmd) Run(ctx *context) error { + sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent) + if err != nil { + return err + } + + itemEnabled, err := getItemEnabled(ctx.Client, sceneName, sceneItemID) + if err != nil { + return err + } + + _, err = ctx.Client.SceneItems.SetSceneItemEnabled(sceneitems.NewSetSceneItemEnabledParams(). + WithSceneName(sceneName). + WithSceneItemId(sceneItemID). + WithSceneItemEnabled(!itemEnabled)) + if err != nil { + return err + } + return nil +} + +// SceneItemVisibleCmd provides a command to check the visibility of a scene item. +type SceneItemVisibleCmd struct { + Parent string `flag:"" help:"Parent group name."` + + SceneName string `arg:"" help:"Scene name."` + ItemName string `arg:"" help:"Item name."` +} + +// Run executes the command to check the visibility of a scene item. +func (cmd *SceneItemVisibleCmd) Run(ctx *context) error { + sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Parent) + if err != nil { + return err + } + + itemEnabled, err := getItemEnabled(ctx.Client, sceneName, sceneItemID) + if err != nil { + return err + } + + if itemEnabled { + fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is visible.\n", cmd.ItemName, cmd.SceneName) + } else { + fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is hidden.\n", cmd.ItemName, cmd.SceneName) + } + return nil +} diff --git a/stream.go b/stream.go new file mode 100644 index 0000000..f34a964 --- /dev/null +++ b/stream.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" +) + +// StreamCmd handles the streaming commands. +type StreamCmd struct { + Start StreamStartCmd `cmd:"" help:"Start streaming." aliases:"s"` + Stop StreamStopCmd `cmd:"" help:"Stop streaming." aliases:"st"` + Toggle StreamToggleCmd `cmd:"" help:"Toggle streaming." aliases:"tg"` + Status StreamStatusCmd `cmd:"" help:"Get streaming status." aliases:"ss"` +} + +// StreamStartCmd starts the stream. +type StreamStartCmd struct{} // size = 0x0 + +// Run executes the command to start streaming. +func (cmd *StreamStartCmd) Run(ctx *context) error { + _, err := ctx.Client.Stream.StartStream() + if err != nil { + return err + } + return nil +} + +// StreamStopCmd stops the stream. +type StreamStopCmd struct{} // size = 0x0 + +// Run executes the command to stop streaming. +func (cmd *StreamStopCmd) Run(ctx *context) error { + _, err := ctx.Client.Stream.StopStream() + if err != nil { + return err + } + return nil +} + +// StreamToggleCmd toggles the stream status. +type StreamToggleCmd struct{} // size = 0x0 + +// Run executes the command to toggle streaming. +func (cmd *StreamToggleCmd) Run(ctx *context) error { + status, err := ctx.Client.Stream.GetStreamStatus() + if err != nil { + return err + } + if status.OutputActive { + _, err = ctx.Client.Stream.StopStream() + fmt.Fprintf(ctx.Out, "Stopping stream...\n") + } else { + _, err = ctx.Client.Stream.StartStream() + fmt.Fprintf(ctx.Out, "Starting stream...\n") + } + if err != nil { + return err + } + return nil +} + +// StreamStatusCmd retrieves the status of the stream. +type StreamStatusCmd struct{} // size = 0x0 + +// Run executes the command to get the stream status. +func (cmd *StreamStatusCmd) Run(ctx *context) error { + status, err := ctx.Client.Stream.GetStreamStatus() + if err != nil { + return err + } + fmt.Fprintf(ctx.Out, "Output active: %v\n", status.OutputActive) + if status.OutputActive { + seconds := status.OutputDuration / 1000 + minutes := int(seconds / 60) + secondsInt := int(seconds) % 60 + if minutes > 0 { + fmt.Fprintf(ctx.Out, "Output duration: %d minutes and %d seconds\n", minutes, secondsInt) + } else { + fmt.Fprintf(ctx.Out, "Output duration: %d seconds\n", secondsInt) + } + } + return nil +} diff --git a/studiomode.go b/studiomode.go new file mode 100644 index 0000000..e234793 --- /dev/null +++ b/studiomode.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + + "github.com/andreykaipov/goobs/api/requests/ui" +) + +// StudioModeCmd provides commands to manage studio mode in OBS Studio. +type StudioModeCmd struct { + Enable StudioModeEnableCmd `cmd:"enable" help:"Enable studio mode." aliases:"on"` + Disable StudioModeDisableCmd `cmd:"disable" help:"Disable studio mode." aliases:"off"` + Toggle StudioModeToggleCmd `cmd:"toggle" help:"Toggle studio mode." aliases:"tg"` + Status StudioModeStatusCmd `cmd:"status" help:"Get studio mode status." aliases:"ss"` +} + +// StudioModeEnableCmd provides a command to enable studio mode. +type StudioModeEnableCmd struct{} // size = 0x0 + +// Run executes the command to enable studio mode. +func (cmd *StudioModeEnableCmd) Run(ctx *context) error { + _, err := ctx.Client.Ui.SetStudioModeEnabled(ui.NewSetStudioModeEnabledParams().WithStudioModeEnabled(true)) + if err != nil { + return fmt.Errorf("failed to enable studio mode: %w", err) + } + return nil +} + +// StudioModeDisableCmd provides a command to disable studio mode. +type StudioModeDisableCmd struct{} // size = 0x0 + +// Run executes the command to disable studio mode. +func (cmd *StudioModeDisableCmd) Run(ctx *context) error { + _, err := ctx.Client.Ui.SetStudioModeEnabled(ui.NewSetStudioModeEnabledParams().WithStudioModeEnabled(false)) + if err != nil { + return fmt.Errorf("failed to disable studio mode: %w", err) + } + return nil +} + +// StudioModeToggleCmd provides a command to toggle studio mode. +type StudioModeToggleCmd struct{} // size = 0x0 + +// Run executes the command to toggle studio mode. +func (cmd *StudioModeToggleCmd) Run(ctx *context) error { + status, err := ctx.Client.Ui.GetStudioModeEnabled(&ui.GetStudioModeEnabledParams{}) + if err != nil { + return fmt.Errorf("failed to get studio mode status: %w", err) + } + + newStatus := !status.StudioModeEnabled + _, err = ctx.Client.Ui.SetStudioModeEnabled(ui.NewSetStudioModeEnabledParams().WithStudioModeEnabled(newStatus)) + if err != nil { + return fmt.Errorf("failed to toggle studio mode: %w", err) + } + + if newStatus { + fmt.Fprintln(ctx.Out, "Studio mode is now enabled") + } else { + fmt.Fprintln(ctx.Out, "Studio mode is now disabled") + } + + return nil +} + +// StudioModeStatusCmd provides a command to get the status of studio mode. +type StudioModeStatusCmd struct{} // size = 0x0 + +// Run executes the command to get the status of studio mode. +func (cmd *StudioModeStatusCmd) Run(ctx *context) error { + status, err := ctx.Client.Ui.GetStudioModeEnabled(&ui.GetStudioModeEnabledParams{}) + if err != nil { + return fmt.Errorf("failed to get studio mode status: %w", err) + } + if status.StudioModeEnabled { + fmt.Fprintln(ctx.Out, "Studio mode is enabled") + } else { + fmt.Fprintln(ctx.Out, "Studio mode is disabled") + } + return nil +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..c9ac075 --- /dev/null +++ b/version.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" +) + +// VersionCmd handles the version command. +type VersionCmd struct{} // size = 0x0 + +// Run executes the command to get the OBS client version. +func (cmd *VersionCmd) Run(ctx *context) error { + version, err := ctx.Client.General.GetVersion() + if err != nil { + return err + } + fmt.Fprintf( + ctx.Out, + "OBS Client Version: %s with Websocket Version: %s\n", + version.ObsVersion, + version.ObsWebSocketVersion, + ) + + return nil +} diff --git a/virtualcam.go b/virtualcam.go new file mode 100644 index 0000000..a834f67 --- /dev/null +++ b/virtualcam.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" +) + +// VirtualCamCmd handles the virtual camera commands. +type VirtualCamCmd struct { + Start StartVirtualCamCmd `help:"Start virtual camera." cmd:"" aliases:"s"` + Stop StopVirtualCamCmd `help:"Stop virtual camera." cmd:"" aliases:"st"` + Toggle ToggleVirtualCamCmd `help:"Toggle virtual camera." cmd:"" aliases:"tg"` + Status StatusVirtualCamCmd `help:"Get virtual camera status." cmd:"" aliases:"ss"` +} + +// StartVirtualCamCmd starts the virtual camera. +type StartVirtualCamCmd struct{} // size = 0x0 + +// Run executes the command to start the virtual camera. +func (c *StartVirtualCamCmd) Run(ctx *context) error { + _, err := ctx.Client.Outputs.StartVirtualCam() + if err != nil { + return fmt.Errorf("failed to start virtual camera: %w", err) + } + fmt.Fprintln(ctx.Out, "Virtual camera started.") + return nil +} + +// StopVirtualCamCmd stops the virtual camera. +type StopVirtualCamCmd struct{} // size = 0x0 + +// Run executes the command to stop the virtual camera. +func (c *StopVirtualCamCmd) Run(ctx *context) error { + _, err := ctx.Client.Outputs.StopVirtualCam() + if err != nil { + return fmt.Errorf("failed to stop virtual camera: %w", err) + } + fmt.Fprintln(ctx.Out, "Virtual camera stopped.") + return nil +} + +// ToggleVirtualCamCmd toggles the virtual camera. +type ToggleVirtualCamCmd struct{} // size = 0x0 + +// Run executes the command to toggle the virtual camera. +func (c *ToggleVirtualCamCmd) Run(ctx *context) error { + _, err := ctx.Client.Outputs.ToggleVirtualCam() + if err != nil { + return fmt.Errorf("failed to toggle virtual camera: %w", err) + } + return nil +} + +// StatusVirtualCamCmd retrieves the status of the virtual camera. +type StatusVirtualCamCmd struct{} // size = 0x0 + +// Run executes the command to get the status of the virtual camera. +func (c *StatusVirtualCamCmd) Run(ctx *context) error { + status, err := ctx.Client.Outputs.GetVirtualCamStatus() + if err != nil { + return fmt.Errorf("failed to get virtual camera status: %w", err) + } + + if status.OutputActive { + fmt.Fprintln(ctx.Out, "Virtual camera is active.") + } else { + fmt.Fprintln(ctx.Out, "Virtual camera is inactive.") + } + return nil +}