From 2037a64e983e9075caac16ad06a267ae33b48c6c Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Thu, 24 Apr 2025 11:54:51 +0100 Subject: [PATCH] first commit --- .github/workflows/golang-ci.yml | 29 +++ .github/workflows/release.yml | 31 +++ .gitignore | 30 +++ .golangci.yml | 52 +++++ .goreleaser.yaml | 48 ++++ CHANGELOG.md | 12 + README.md | 401 ++++++++++++++++++++++++++++++++ Taskfile.yaml | 52 +++++ go.mod | 17 ++ go.sum | 30 +++ group.go | 173 ++++++++++++++ input.go | 114 +++++++++ main.go | 94 ++++++++ profile.go | 131 +++++++++++ record.go | 119 ++++++++++ replaybuffer.go | 63 +++++ scene.go | 83 +++++++ scenecollection.go | 93 ++++++++ sceneitem.go | 183 +++++++++++++++ stream.go | 82 +++++++ studiomode.go | 81 +++++++ version.go | 24 ++ virtualcam.go | 69 ++++++ 23 files changed, 2011 insertions(+) create mode 100644 .github/workflows/golang-ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .goreleaser.yaml create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 Taskfile.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 group.go create mode 100644 input.go create mode 100644 main.go create mode 100644 profile.go create mode 100644 record.go create mode 100644 replaybuffer.go create mode 100644 scene.go create mode 100644 scenecollection.go create mode 100644 sceneitem.go create mode 100644 stream.go create mode 100644 studiomode.go create mode 100644 version.go create mode 100644 virtualcam.go 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 +}