diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afee479..f662725 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,42 +5,57 @@ on: branches: [master, develop] pull_request: branches: [master] + workflow_dispatch: + inputs: + force_run: + description: 'Force workflow run' + required: true + type: choice + options: [yes, no] + +permissions: + actions: read + contents: read + statuses: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: Go: name: Go runs-on: ubuntu-latest - env: - SRC_DIR: src/github.com/${{ github.repository }} - GO111MODULE: auto - strategy: matrix: - go: [ '1.17.x', '1.18.x' ] + go: [ '1.21.x', '1.22.x' ] steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - id: go - - - name: Setup PATH - run: | - echo "GOPATH=${{ github.workspace }}" >> "$GITHUB_ENV" - echo "GOBIN=${{ github.workspace }}/bin" >> "$GITHUB_ENV" - echo "${{ github.workspace }}/bin" >> "$GITHUB_PATH" - - - name: Checkout - uses: actions/checkout@v3 - with: - path: ${{env.SRC_DIR}} - name: Download dependencies - working-directory: ${{env.SRC_DIR}} run: make deps - name: Build binary - working-directory: ${{env.SRC_DIR}} run: make all + + Typos: + name: Typos + runs-on: ubuntu-latest + + needs: Go + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check spelling + continue-on-error: true + uses: crate-ci/typos@master diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0fa9f18..bfc4df5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -20,14 +20,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 2 - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: go - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03de5cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor +icecli diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..55aead8 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,2 @@ +[files] +extend-exclude = ["go.sum"] diff --git a/Makefile b/Makefile index c87839b..248c9e1 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ ################################################################################ -# This Makefile generated by GoMakeGen 1.5.1 using next command: +# This Makefile generated by GoMakeGen 2.3.0 using next command: # gomakegen --mod . # # More info: https://kaos.sh/gomakegen @@ -9,15 +9,25 @@ export GO111MODULE=on +ifdef VERBOSE ## Print verbose information (Flag) +VERBOSE_FLAG = -v +endif + +COMPAT ?= 1.18 +MAKEDIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +GITREV ?= $(shell test -s $(MAKEDIR)/.git && git rev-parse --short HEAD) + +################################################################################ + .DEFAULT_GOAL := help -.PHONY = fmt vet all clean deps mod-init mod-update mod-vendor help +.PHONY = fmt vet all clean deps update init vendor mod-init mod-update mod-download mod-vendor help ################################################################################ all: icecli ## Build all binaries -icecli: ## Build icecli binary - go build icecli.go +icecli: + go build $(VERBOSE_FLAG) -ldflags="-X main.gitrev=$(GITREV)" icecli.go install: ## Install all binaries cp icecli /usr/bin/icecli @@ -25,32 +35,66 @@ install: ## Install all binaries uninstall: ## Uninstall all binaries rm -f /usr/bin/icecli -deps: mod-update ## Download dependencies +init: mod-init ## Initialize new module -mod-init: ## Initialize new module - go mod init - go mod tidy +deps: mod-download ## Download dependencies + +update: mod-update ## Update dependencies to the latest versions -mod-update: ## Download modules to local cache +vendor: mod-vendor ## Make vendored copy of dependencies + +mod-init: +ifdef MODULE_PATH ## Module path for initialization (String) + go mod init $(MODULE_PATH) +else + go mod init +endif + +ifdef COMPAT ## Compatible Go version (String) + go mod tidy $(VERBOSE_FLAG) -compat=$(COMPAT) -go=$(COMPAT) +else + go mod tidy $(VERBOSE_FLAG) +endif + +mod-update: +ifdef UPDATE_ALL ## Update all dependencies (Flag) + go get -u $(VERBOSE_FLAG) all +else + go get -u $(VERBOSE_FLAG) ./... +endif + +ifdef COMPAT + go mod tidy $(VERBOSE_FLAG) -compat=$(COMPAT) +else + go mod tidy $(VERBOSE_FLAG) +endif + + test -d vendor && rm -rf vendor && go mod vendor $(VERBOSE_FLAG) || : + +mod-download: go mod download -mod-vendor: ## Make vendored copy of dependencies - go mod vendor +mod-vendor: + rm -rf vendor && go mod vendor $(VERBOSE_FLAG) fmt: ## Format source code with gofmt find . -name "*.go" -exec gofmt -s -w {} \; -vet: ## Runs go vet over sources +vet: ## Runs 'go vet' over sources go vet -composites=false -printfuncs=LPrintf,TLPrintf,TPrintf,log.Debug,log.Info,log.Warn,log.Error,log.Critical,log.Print ./... clean: ## Remove generated files rm -f icecli help: ## Show this info - @echo -e '\n\033[1mSupported targets:\033[0m\n' + @echo -e '\n\033[1mTargets:\033[0m\n' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ - | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-12s\033[0m %s\n", $$1, $$2}' + | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-14s\033[0m %s\n", $$1, $$2}' + @echo -e '\n\033[1mVariables:\033[0m\n' + @grep -E '^ifdef [A-Z_]+ .*?## .*$$' $(abspath $(lastword $(MAKEFILE_LIST))) \ + | sed 's/ifdef //' \ + | awk 'BEGIN {FS = " .*?## "}; {printf " \033[32m%-14s\033[0m %s\n", $$1, $$2}' @echo -e '' - @echo -e '\033[90mGenerated by GoMakeGen 1.5.1\033[0m\n' + @echo -e '\033[90mGenerated by GoMakeGen 2.3.0\033[0m\n' ################################################################################ diff --git a/README.md b/README.md index 3e33209..8ed0ec8 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ #### From source -To build the `icecli` from scratch, make sure you have a working Go 1.16+ workspace (_[instructions](https://golang.org/doc/install)_), then: +To build the `icecli` from scratch, make sure you have a working Go 1.18+ workspace (_[instructions](https://go.dev/doc/install)_), then: -``` -go install github.com/essentialkaos/icecli +```bash +go install github.com/essentialkaos/icecli@latest ``` #### Prebuilt binaries diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..a60aa18 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,748 @@ +package cli + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/essentialkaos/ek/v12/fmtc" + "github.com/essentialkaos/ek/v12/fmtutil" + "github.com/essentialkaos/ek/v12/fmtutil/table" + "github.com/essentialkaos/ek/v12/options" + "github.com/essentialkaos/ek/v12/support" + "github.com/essentialkaos/ek/v12/support/deps" + "github.com/essentialkaos/ek/v12/support/pkgs" + "github.com/essentialkaos/ek/v12/terminal/tty" + "github.com/essentialkaos/ek/v12/timeutil" + "github.com/essentialkaos/ek/v12/usage" + "github.com/essentialkaos/ek/v12/usage/completion/bash" + "github.com/essentialkaos/ek/v12/usage/completion/fish" + "github.com/essentialkaos/ek/v12/usage/completion/zsh" + "github.com/essentialkaos/ek/v12/usage/man" + "github.com/essentialkaos/ek/v12/usage/update" + + ic "github.com/essentialkaos/go-icecast/v2" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +const ( + APP = "icecli" + DESC = "Icecast CLI" + VER = "1.1.0" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +const ( + CMD_HELP = "help" + CMD_STATS = "stats" + CMD_KILL_CLIENT = "kill-client" + CMD_KILL_SOURCE = "kill-source" + CMD_LIST_CLIENTS = "list-clients" + CMD_LIST_MOUNTS = "list-mounts" + CMD_MOVE_CLIENTS = "move-clients" + CMD_UPDATE_META = "update-meta" +) + +const ( + OPT_HOST = "H:host" + OPT_USER = "U:user" + OPT_PASS = "P:password" + OPT_NO_COLOR = "nc:no-color" + OPT_HELP = "h:help" + OPT_VER = "v:version" + + OPT_VERB_VER = "vv:verbose-version" + OPT_COMPLETION = "completion" + OPT_GENERATE_MAN = "generate-man" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// optMap is map with options +var optMap = options.Map{ + OPT_HOST: {Value: "http://127.0.0.1:8000", Alias: "url"}, + OPT_USER: {Value: "admin", Alias: "login"}, + OPT_PASS: {Value: "hackme", Alias: "pass"}, + OPT_NO_COLOR: {Type: options.BOOL}, + OPT_HELP: {Type: options.BOOL}, + OPT_VER: {Type: options.MIXED}, + + OPT_VERB_VER: {Type: options.BOOL}, + OPT_COMPLETION: {}, + OPT_GENERATE_MAN: {Type: options.BOOL}, +} + +// colorTagApp contains color tag for app name +var colorTagApp string + +// colorTagVer contains color tag for app version +var colorTagVer string + +// client is icecast API client +var client *ic.API + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Run is main application function +func Run(gitRev string, gomod []byte) { + preConfigureUI() + + args, errs := options.Parse(optMap) + + if len(errs) != 0 { + printError("Options parsing errors:") + + for _, err := range errs { + printError(" %v", err) + } + + os.Exit(1) + } + + configureUI() + + switch { + case options.Has(OPT_COMPLETION): + os.Exit(printCompletion()) + case options.Has(OPT_GENERATE_MAN): + printMan() + os.Exit(0) + case options.GetB(OPT_VER): + genAbout(gitRev).Print(options.GetS(OPT_VER)) + os.Exit(0) + case options.GetB(OPT_VERB_VER): + support.Collect(APP, VER). + WithRevision(gitRev). + WithDeps(deps.Extract(gomod)). + WithPackages(pkgs.Collect("icecast,icecast2,icecast-kh")). + Print() + os.Exit(0) + case len(args) == 0, options.GetB(OPT_HELP): + genUsage().Print() + os.Exit(0) + } + + if args.Get(0).ToLower().String() == CMD_HELP { + checkForRequiredArgs(args, 1) + showHelp(args.Get(0).String()) + } else { + execCommand(args) + } +} + +// preConfigureUI preconfigures UI based on information about user terminal +func preConfigureUI() { + if !tty.IsTTY() { + fmtc.DisableColors = true + } + + fmtutil.SeparatorSymbol = "–" + fmtutil.SeparatorFullscreen = true + fmtutil.SizeSeparator = " " + table.SeparatorSymbol = "–" + table.HeaderCapitalize = true + + switch { + case fmtc.Is256ColorsSupported(): + colorTagApp, colorTagVer = "{*}{#27}", "{#27}" + default: + colorTagApp, colorTagVer = "{*}{b}", "{b}" + } +} + +// configureUI configures user interface +func configureUI() { + if options.GetB(OPT_NO_COLOR) { + fmtc.DisableColors = true + } +} + +// execCommand executes command +func execCommand(args options.Arguments) { + var err error + + client, err = ic.NewAPI( + options.GetS(OPT_HOST), + options.GetS(OPT_USER), + options.GetS(OPT_PASS), + ) + + if err != nil { + printErrorExit(err.Error()) + } + + cmd := args.Get(0).ToLower().String() + + switch cmd { + case CMD_STATS: + showServerStats() + case CMD_LIST_MOUNTS: + listMounts() + case CMD_LIST_CLIENTS: + checkForRequiredArgs(args, 1) + listClients(args.Get(1).String()) + case CMD_MOVE_CLIENTS: + checkForRequiredArgs(args, 2) + moveClients( + args.Get(1).String(), + args.Get(2).String(), + ) + case CMD_UPDATE_META: + checkForRequiredArgs(args, 3) + updateMeta( + args.Get(1).String(), + args.Get(2).String(), + args.Get(3).String(), + ) + case CMD_KILL_CLIENT: + checkForRequiredArgs(args, 2) + killClient( + args.Get(1).String(), + args.Get(2).String(), + ) + case CMD_KILL_SOURCE: + checkForRequiredArgs(args, 1) + killSource(args.Get(1).String()) + default: + printError("Unknown or unsupported command %q", cmd) + os.Exit(1) + } +} + +// showHelp prints command usage info +func showHelp(command string) { + switch command { + case CMD_STATS: + helpCmdStats() + case CMD_LIST_MOUNTS: + helpCmdListMounts() + case CMD_LIST_CLIENTS: + helpCmdListClients() + case CMD_MOVE_CLIENTS: + helpCmdMoveClients() + case CMD_UPDATE_META: + helpCmdUpdateMeta() + case CMD_KILL_CLIENT: + helpCmdKillClient() + case CMD_KILL_SOURCE: + helpCmdKillSource() + default: + genUsage().Print() + } +} + +// showServerStats prints server stats +func showServerStats() { + stats, err := client.GetStats() + + if err != nil { + printErrorExit(err.Error()) + } + + fmtc.NewLine() + printServerHeader(stats.Info.ID) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Sources", fmtutil.PrettyNum(stats.Stats.Sources)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Banned IPs", fmtutil.PrettyNum(stats.Stats.BannedIPs)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Clients", fmtutil.PrettyNum(stats.Stats.Clients)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Connections", fmtutil.PrettyNum(stats.Stats.Connections)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listeners", fmtutil.PrettyNum(stats.Stats.Listeners)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Stats", fmtutil.PrettyNum(stats.Stats.Stats)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Client Connections", fmtutil.PrettyNum(stats.Stats.ClientConnections)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "File Connections", fmtutil.PrettyNum(stats.Stats.FileConnections)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listener Connections", fmtutil.PrettyNum(stats.Stats.ListenerConnections)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Stats Connections", fmtutil.PrettyNum(stats.Stats.StatsConnections)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Source Client Connections", fmtutil.PrettyNum(stats.Stats.SourceClientConnections)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Source Relay Connections", fmtutil.PrettyNum(stats.Stats.SourceRelayConnections)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Source Total Connections", fmtutil.PrettyNum(stats.Stats.SourceTotalConnections)) + fmtc.Printf( + " {*}%-28s{!} {s}|{!} %s {s-}(%s){!}\n", "Stream Bytes Read", + fmtutil.PrettyNum(stats.Stats.StreamBytesRead), + fmtutil.PrettySize(stats.Stats.StreamBytesRead), + ) + fmtc.Printf( + " {*}%-28s{!} {s}|{!} %s {s-}(%s){!}\n", "Stream Bytes Sent", + fmtutil.PrettyNum(stats.Stats.StreamBytesSent), + fmtutil.PrettySize(stats.Stats.StreamBytesSent), + ) + + for path, source := range stats.Sources { + showSeparator(false) + fmtc.Printf(" {*y}%s{!} {s-}(online: %s){!}\n", path, timeutil.PrettyDuration(time.Since(source.StreamStarted))) + showSeparator(false) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Source IP", source.SourceIP) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Name", formatString(source.Info.Name)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Genre", formatString(source.Genre)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Description", formatString(source.Info.Description)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Type", formatString(source.Info.Type)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "URL", formatString(source.Info.URL)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listen URL", formatString(source.ListenURL)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "SubType", formatString(source.Info.SubType)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %t\n", "Public", source.Public) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "User-Agent", formatString(source.UserAgent)) + showSeparator(true) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Bitrate", fmtutil.PrettyNum(source.AudioInfo.Bitrate)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Channels", fmtutil.PrettyNum(source.AudioInfo.Channels)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s Hz\n", "SampleRate", fmtutil.PrettyNum(source.AudioInfo.SampleRate)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "CodecID", fmtutil.PrettyNum(source.AudioInfo.CodecID)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "RawInfo", formatString(source.AudioInfo.RawInfo)) + showSeparator(true) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Artist", formatString(source.Track.Artist)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Title", formatString(source.Track.Title)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Artwork", formatString(source.Track.Artwork)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Metadata URL", formatString(source.Track.MetadataURL)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "RawInfo", formatString(source.Track.RawInfo)) + fmtc.Printf( + " {*}%-28s{!} {s}|{!} %s {s-}(%s ago){!}\n", "Metadata Updated", + timeutil.Format(source.MetadataUpdated, "%Y/%m/%d %H:%M:%S"), + timeutil.PrettyDuration(time.Since(source.MetadataUpdated)), + ) + showSeparator(true) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listeners", fmtutil.PrettyNum(source.Stats.Listeners)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listener Peak", fmtutil.PrettyNum(source.Stats.ListenerPeak)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Max Listeners", fmtutil.PrettyNum(source.Stats.MaxListeners)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Slow Listeners", fmtutil.PrettyNum(source.Stats.SlowListeners)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listener Connections", fmtutil.PrettyNum(source.Stats.ListenerConnections)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Connected", fmtutil.PrettyNum(source.Stats.Connected)) + fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Queue Size", fmtutil.PrettyNum(source.Stats.QueueSize)) + + fmtc.Printf( + " {*}%-28s{!} {s}|{!} %s {s-}(%s/s){!}\n", "Incoming Bitrate", + fmtutil.PrettyNum(source.Stats.IncomingBitrate), + fmtutil.PrettySize(source.Stats.IncomingBitrate), + ) + + fmtc.Printf( + " {*}%-28s{!} {s}|{!} %s {s-}(%s/s){!}\n", "Outgoing Bitrate", + fmtutil.PrettyNum(source.Stats.OutgoingBitrate), + fmtutil.PrettySize(source.Stats.OutgoingBitrate), + ) + + fmtc.Printf( + " {*}%-28s{!} {s}|{!} %s {s-}(%s){!}\n", "Total Bytes Read", + fmtutil.PrettyNum(source.Stats.TotalBytesRead), + fmtutil.PrettySize(source.Stats.TotalBytesRead), + ) + + fmtc.Printf( + " {*}%-28s{!} {s}|{!} %s {s-}(%s){!}\n", "Total Bytes Sent", + fmtutil.PrettyNum(source.Stats.TotalBytesSent), + fmtutil.PrettySize(source.Stats.TotalBytesSent), + ) + } + + showSeparator(false) + fmtc.NewLine() +} + +// listMounts prints list of all connected mount points +func listMounts() { + mounts, err := client.ListMounts() + + if err != nil { + printErrorExit(err.Error()) + } + + if len(mounts) == 0 { + fmtc.Println("{y}No mounts found{!}") + return + } + + t := table.NewTable("path", "listeners", "connected", "content-type") + t.SetAlignments(table.ALIGN_LEFT, table.ALIGN_RIGHT, table.ALIGN_RIGHT) + t.SetSizes(20, 10, 10) + + fmtc.NewLine() + + for _, m := range mounts { + t.Print( + m.Path, fmtutil.PrettyNum(m.Listeners), + timeutil.ShortDuration(m.Connected), m.ContentType, + ) + } + + t.Separator() + + fmtc.NewLine() +} + +// listClients prints info about clients (listeners) connected to given mount point +func listClients(mount string) { + mount = formatMount(mount) + listeners, err := client.ListClients(mount) + + if err != nil { + printErrorExit(err.Error()) + } + + if len(listeners) == 0 { + fmtc.Println("{y}No listeners found{!}") + return + } + + t := table.NewTable("id", "ip", "lag", "connected", "user-agent") + t.SetAlignments(table.ALIGN_RIGHT, table.ALIGN_RIGHT, table.ALIGN_RIGHT, table.ALIGN_RIGHT) + t.SetSizes(6, 14, 10, 9) + + fmtc.NewLine() + + for _, l := range listeners { + t.Print( + l.ID, l.IP, fmtutil.PrettySize(l.Lag), + timeutil.ShortDuration(l.Connected), + l.UserAgent, + ) + } + + t.Separator() + + fmtc.NewLine() +} + +// moveClients moves clients from one mount point to another +func moveClients(fromMount, toMount string) { + fromMount = formatMount(fromMount) + toMount = formatMount(toMount) + + err := client.MoveClients(fromMount, toMount) + + if err != nil { + printErrorExit(err.Error()) + } + + fmtc.Printf("{g}Clients successfully moved from %s to %s{!}\n", fromMount, toMount) +} + +// updateMeta updates metadata for given mount point +func updateMeta(mount, artist, title string) { + mount = formatMount(mount) + + err := client.UpdateMeta(mount, ic.TrackMeta{ + Artist: artist, + Title: title, + }) + + if err != nil { + printErrorExit(err.Error()) + } + + fmtc.Printf("{g}Metadata successfully updated for %s{!}\n", mount) +} + +// killClient detaches client with given ID from the mount point +func killClient(mount, clientID string) { + mount = formatMount(mount) + id, err := strconv.Atoi(clientID) + + if err != nil { + printErrorExit(err.Error()) + } + + err = client.KillClient(mount, id) + + if err != nil { + printErrorExit(err.Error()) + } + + fmtc.Printf("{g}Cliend %d successfully detached from %s{!}\n", id, mount) +} + +// killSource detaches source from given mount point +func killSource(mount string) { + mount = formatMount(mount) + + err := client.KillSource(mount) + + if err != nil { + printErrorExit(err.Error()) + } + + fmtc.Printf("{g}Source successfully detached from %s{!}\n", mount) +} + +// printServerHeader prints header with icecast info +func printServerHeader(id string) { + showSeparator(false) + + if id == "" { + fmtc.Printf(" {*}{#45}Icecast Server{!} on {*}%s{!}\n", options.GetS(OPT_HOST)) + } else { + fmtc.Printf(" {*}{#45}Icecast Server{!} on {*}%s{!} {s-}(%s){!}\n", options.GetS(OPT_HOST), id) + } + + showSeparator(false) +} + +// showSeparator prints separator +func showSeparator(shadow bool) { + if shadow { + fmtutil.SeparatorColorTag = "{s-}" + } else { + fmtutil.SeparatorColorTag = "{s}" + } + + fmtutil.Separator(true) +} + +// formatString formats string for stats info +func formatString(s string) string { + if s == "" { + return fmtc.Sprintf("{s-}—{!}") + } + + return s +} + +// formatMount formats mount name +func formatMount(mount string) string { + if !strings.HasPrefix(mount, "/") { + return "/" + mount + } + + return mount +} + +// checks command for required args num +func checkForRequiredArgs(args options.Arguments, required int) { + if len(args) >= required+1 { + return + } + + printErrorExit( + "Wrong number of arguments for %s command", + args.Get(0).ToLower().String(), + ) +} + +// printError prints error message to console +func printError(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) +} + +// printErrorExit prints error message to console and exit with error code +func printErrorExit(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) + os.Exit(1) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// helpCmdStats shows help for "stats" command +func helpCmdStats() { + fmtc.NewLine() + fmtc.Println("{*}Description:{!}\n") + fmtc.Println(" Shows internal statistics kept by the Icecast server.\n") + fmtc.Println("{*}Usage:{!}\n") + fmtc.Printf(" {c*}%s{!} {y}%s{!}\n\n", APP, CMD_STATS) + fmtc.Println("{*}Examples:{!}\n") + fmtc.Printf(" %s %s\n", APP, CMD_STATS) + fmtc.NewLine() +} + +// helpCmdListMounts shows help for "list-mounts" command +func helpCmdListMounts() { + fmtc.NewLine() + fmtc.Println("{*}Description:{!}\n") + fmtc.Println(" Shows all the currently connected mountpoints.\n") + fmtc.Println("{*}Usage:{!}\n") + fmtc.Printf(" {c*}%s{!} {y}%s{!}\n\n", APP, CMD_LIST_MOUNTS) + fmtc.Println("{*}Examples:{!}\n") + fmtc.Printf(" %s %s\n", APP, CMD_LIST_MOUNTS) + fmtc.NewLine() +} + +// helpCmdListClients shows help for "list-clients" command +func helpCmdListClients() { + fmtc.NewLine() + fmtc.Println("{*}Description:{!}\n") + fmtc.Println(" Shows all the clients currently connected to a specific mountpoint.") + fmtc.NewLine() + fmtc.Println("{*}Usage:{!}\n") + fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}mount{!}\n", APP, CMD_LIST_CLIENTS) + fmtc.NewLine() + fmtc.Println("{*}Arguments:{!}\n") + fmtc.Println(" {g}mount{!} - Mount name {s-}(with or without leading slash){!}") + fmtc.NewLine() + fmtc.Println("{*}Examples:{!}\n") + fmtc.Printf(" %s %s /source1.ogg \n", APP, CMD_LIST_CLIENTS) + fmtc.Printf(" %s %s source1.ogg \n", APP, CMD_LIST_CLIENTS) + fmtc.NewLine() +} + +// helpCmdMoveClients shows help for "move-clients" command +func helpCmdMoveClients() { + fmtc.NewLine() + fmtc.Println("{*}Description:{!}\n") + fmtc.Println(" This command provides the ability to migrate currently connected listeners") + fmtc.Println(" from one mountpoint to another.") + fmtc.NewLine() + fmtc.Println("{*}Usage:{!}\n") + fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}from-mount to-mount{!}\n", APP, CMD_MOVE_CLIENTS) + fmtc.NewLine() + fmtc.Println("{*}Arguments:{!}\n") + fmtc.Println(" {g}from-mount{!} - Source mount name {s-}(with or without leading slash){!}") + fmtc.Println(" {g}to-mount {!} - Target mount name {s-}(with or without leading slash){!}") + fmtc.NewLine() + fmtc.Println("{*}Examples:{!}\n") + fmtc.Printf(" %s %s /source1.ogg /source2.ogg\n", APP, CMD_MOVE_CLIENTS) + fmtc.Printf(" %s %s source1.aac source2.aac \n", APP, CMD_MOVE_CLIENTS) + fmtc.NewLine() +} + +// helpCmdUpdateMeta shows help for "update-meta" command +func helpCmdUpdateMeta() { + fmtc.NewLine() + fmtc.Println("{*}Description:{!}\n") + fmtc.Println(" This command provides the ability for either a source client or any external") + fmtc.Println(" program to update the metadata information for a particular mountpoint.") + fmtc.NewLine() + fmtc.Println("{*}Usage:{!}\n") + fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}mount artist title{!}\n", APP, CMD_UPDATE_META) + fmtc.NewLine() + fmtc.Println("{*}Arguments:{!}\n") + fmtc.Println(" {g}mount {!} - Mount name {s-}(with or without leading slash){!}") + fmtc.Println(" {g}artist{!} - Track artist name") + fmtc.Println(" {g}title {!} - Track title") + fmtc.NewLine() + fmtc.Println("{*}Examples:{!}\n") + fmtc.Printf(" %s %s \"Wretch 32\" \"Traktor (Brookes Brothers Remix)\"\n", APP, CMD_UPDATE_META) + fmtc.NewLine() +} + +// helpCmdKillClient shows help for "kill-client" command +func helpCmdKillClient() { + fmtc.NewLine() + fmtc.Println("{*}Description:{!}\n") + fmtc.Println(" Disconnects a specific listener of a currently connected mountpoint.") + fmtc.NewLine() + fmtc.Println("{*}Usage:{!}\n") + fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}mount{!}\n", APP, CMD_KILL_CLIENT) + fmtc.NewLine() + fmtc.Println("{*}Arguments:{!}\n") + fmtc.Println(" {g}mount {!} - Mount name {s-}(with or without leading slash){!}") + fmtc.Println(" {g}client-id{!} - Client ID") + fmtc.NewLine() + fmtc.Println("{*}Examples:{!}\n") + fmtc.Printf(" %s %s /source1.ogg 457\n", APP, CMD_KILL_CLIENT) + fmtc.Printf(" %s %s source1.ogg 457\n", APP, CMD_KILL_CLIENT) + fmtc.NewLine() +} + +// helpCmdKillSource shows help for "kill-source" command +func helpCmdKillSource() { + fmtc.NewLine() + fmtc.Println("{*}Description:{!}\n") + fmtc.Println(" Disconnects a specific mountpoint from the server.") + fmtc.NewLine() + fmtc.Println("{*}Usage:{!}\n") + fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}mount{!}\n", APP, CMD_KILL_SOURCE) + fmtc.NewLine() + fmtc.Println("{*}Arguments:{!}\n") + fmtc.Println(" {g}mount{!} - Mount name {s-}(with or without leading slash){!}") + fmtc.NewLine() + fmtc.Println("{*}Examples:{!}\n") + fmtc.Printf(" %s %s /source1.ogg \n", APP, CMD_KILL_SOURCE) + fmtc.Printf(" %s %s source1.ogg \n", APP, CMD_KILL_SOURCE) + fmtc.NewLine() +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// printCompletion prints completion for given shell +func printCompletion() int { + switch options.GetS(OPT_COMPLETION) { + case "bash": + fmt.Print(bash.Generate(genUsage(), APP)) + case "fish": + fmt.Print(fish.Generate(genUsage(), APP)) + case "zsh": + fmt.Print(zsh.Generate(genUsage(), optMap, APP)) + default: + return 1 + } + + return 0 +} + +// printMan prints man page +func printMan() { + fmt.Println( + man.Generate( + genUsage(), + genAbout(""), + ), + ) +} + +// genUsage generates usage info +func genUsage() *usage.Info { + info := usage.NewInfo("icecli", "arguments…") + + info.AddCommand(CMD_STATS, "Show Icecast statistics") + info.AddCommand(CMD_LIST_MOUNTS, "List mount points") + info.AddCommand(CMD_LIST_CLIENTS, "List clients", "mount") + info.AddCommand(CMD_MOVE_CLIENTS, "Move clients between mounts", "from-mount", "to-mount") + info.AddCommand(CMD_UPDATE_META, "Update meta for mount", "mount", "artist", "title") + info.AddCommand(CMD_KILL_CLIENT, "Kill client connection", "mount", "client-id") + info.AddCommand(CMD_KILL_SOURCE, "Kill source connection", "mount") + info.AddCommand(CMD_HELP, "Show detailed info about command usage", "command") + + info.AddOption(OPT_HOST, "URL of Icecast instance {s-}(default: http://127.0.0.1:8000){!}", "host") + info.AddOption(OPT_USER, "Admin username {s-}(default: admin){!}", "username") + info.AddOption(OPT_PASS, "Admin password {s-}(default: hackme){!}", "password") + info.AddOption(OPT_NO_COLOR, "Disable colors in output") + info.AddOption(OPT_HELP, "Show this help message") + info.AddOption(OPT_VER, "Show version") + + info.AddExample( + CMD_STATS+" -H 127.0.0.1:10000", + "Show stats for server on 127.0.0.1:10000", + ) + + info.AddExample( + CMD_KILL_CLIENT+" -P mYsUpPaPaSs /stream3 361", + "Detach client with ID 361 from /stream3", + ) + + info.AddExample( + CMD_LIST_CLIENTS+" -H 127.0.0.1:10000 -U super_admin -P mYsUpPaPaSs /stream3", + "List clients on /stream3", + ) + + return info +} + +// genAbout generates info about version +func genAbout(gitRev string) *usage.About { + about := &usage.About{ + App: APP, + Version: VER, + Desc: DESC, + Year: 2009, + Owner: "ESSENTIAL KAOS", + + AppNameColorTag: colorTagApp, + VersionColorTag: colorTagVer, + DescSeparator: "{s}—{!}", + + License: "Apache License, Version 2.0 ", + BugTracker: "https://github.com/essentialkaos/icecli", + } + + if gitRev != "" { + about.Build = "git:" + gitRev + about.UpdateChecker = usage.UpdateChecker{"essentialkaos/icecli", update.GitHubChecker} + } + + return about +} diff --git a/go.mod b/go.mod index 9070fac..6237b1b 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,17 @@ module github.com/essentialkaos/icecli -go 1.17 +go 1.18 require ( - github.com/essentialkaos/ek/v12 v12.43.0 + github.com/essentialkaos/ek/v12 v12.113.1 github.com/essentialkaos/go-icecast/v2 v2.0.6 ) require ( - github.com/andybalholm/brotli v1.0.4 // indirect - github.com/klauspost/compress v1.15.0 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/essentialkaos/depsy v1.1.0 // indirect + github.com/klauspost/compress v1.17.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.34.0 // indirect + github.com/valyala/fasthttp v1.52.0 // indirect + golang.org/x/sys v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index 05f1713..ac7370c 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,20 @@ -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/essentialkaos/check v1.2.1 h1:avvyFy/1acUNwfxwuOLsHeCjfXtMygtbu0lVDr3nxFs= -github.com/essentialkaos/check v1.2.1/go.mod h1:PhxzfJWlf5L/skuyhzBLIvjMB5Xu9TIyDIsqpY5MvB8= -github.com/essentialkaos/ek/v12 v12.43.0 h1:lnwrfGYQFJ3EjEF4ydW2qnshDcXCR2OnNRk0kwlFpFE= -github.com/essentialkaos/ek/v12 v12.43.0/go.mod h1:Cv/tOZshmFg4pMJnBkg4aW/WyYhzzc41qzZIfk5RSi4= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/essentialkaos/check v1.4.0 h1:kWdFxu9odCxUqo1NNFNJmguGrDHgwi3A8daXX1nkuKk= +github.com/essentialkaos/depsy v1.1.0 h1:U6dp687UkQwXlZU17Hg2KMxbp3nfZAoZ8duaeUFYvJI= +github.com/essentialkaos/depsy v1.1.0/go.mod h1:kpiTAV17dyByVnrbNaMcZt2jRwvuXClUYOzpyJQwtG8= +github.com/essentialkaos/ek/v12 v12.113.1 h1:3opV9dwRpIQq1fqg5mkaSEt6ogECL4VLzrH/829qeYg= +github.com/essentialkaos/ek/v12 v12.113.1/go.mod h1:SslW97Se34YQKc08Ume2V/8h/HPTgLS1+Iok64cNF/U= github.com/essentialkaos/go-icecast/v2 v2.0.6 h1:QqMwPT+TNN6R5xaQKsLINubtvdDg8LvcEhpynn3TVkI= github.com/essentialkaos/go-icecast/v2 v2.0.6/go.mod h1:EEzBSggEHWafABzwhKCRqFrxEPf1x2OHcu863EbD4/0= -github.com/essentialkaos/go-linenoise/v3 v3.3.5/go.mod h1:g4X3LhT83XT4h7xwrCLclAdMkJvS9qWBQTGNdS6y4vo= -github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= -github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= -github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= +github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/icecli.go b/icecli.go index 6f48b56..00a07fb 100644 --- a/icecli.go +++ b/icecli.go @@ -2,667 +2,27 @@ package main // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2022 ESSENTIAL KAOS // +// Copyright (c) 2024 ESSENTIAL KAOS // // Apache License, Version 2.0 // // // // ////////////////////////////////////////////////////////////////////////////////// // import ( - "fmt" - "os" - "strconv" - "strings" - "time" + _ "embed" - "github.com/essentialkaos/ek/v12/fmtc" - "github.com/essentialkaos/ek/v12/fmtutil" - "github.com/essentialkaos/ek/v12/fmtutil/table" - "github.com/essentialkaos/ek/v12/options" - "github.com/essentialkaos/ek/v12/timeutil" - "github.com/essentialkaos/ek/v12/usage" - "github.com/essentialkaos/ek/v12/usage/completion/bash" - "github.com/essentialkaos/ek/v12/usage/completion/fish" - "github.com/essentialkaos/ek/v12/usage/completion/zsh" - "github.com/essentialkaos/ek/v12/usage/update" - - ic "github.com/essentialkaos/go-icecast/v2" + CLI "github.com/essentialkaos/icecli/cli" ) // ////////////////////////////////////////////////////////////////////////////////// // -const ( - APP = "icecli" - DESC = "Icecast CLI" - VER = "1.0.1" -) +//go:embed go.mod +var gomod []byte -const ( - CMD_HELP = "help" - CMD_STATS = "stats" - CMD_KILL_CLIENT = "kill-client" - CMD_KILL_SOURCE = "kill-source" - CMD_LIST_CLIENTS = "list-clients" - CMD_LIST_MOUNTS = "list-mounts" - CMD_MOVE_CLIENTS = "move-clients" - CMD_UPDATE_META = "update-meta" -) - -const ( - OPT_HOST = "H:host" - OPT_USER = "U:user" - OPT_PASS = "P:password" - OPT_NO_COLOR = "nc:no-color" - OPT_HELP = "h:help" - OPT_VER = "v:version" - - OPT_COMPLETION = "completion" -) +// gitrev is short hash of the latest git commit +var gitrev string // ////////////////////////////////////////////////////////////////////////////////// // -var optMap = options.Map{ - OPT_HOST: {Value: "http://127.0.0.1:8000", Alias: "url"}, - OPT_USER: {Value: "admin", Alias: "login"}, - OPT_PASS: {Value: "hackme", Alias: "pass"}, - OPT_NO_COLOR: {Type: options.BOOL}, - OPT_HELP: {Type: options.BOOL, Alias: "u:usage"}, - OPT_VER: {Type: options.BOOL, Alias: "ver"}, - - OPT_COMPLETION: {}, -} - -var client *ic.API - -// ////////////////////////////////////////////////////////////////////////////////// // - -// main is main func func main() { - args, errs := options.Parse(optMap) - - if len(errs) != 0 { - printError("Options parsing errors:") - - for _, err := range errs { - printError(" %v", err) - } - - os.Exit(1) - } - - configureUI() - - if options.Has(OPT_COMPLETION) { - genCompletion() - } - - if options.GetB(OPT_VER) { - showAbout() - return - } - - if options.GetB(OPT_HELP) || len(args) == 0 { - showUsage() - return - } - - if args[0] == CMD_HELP { - checkForRequiredArgs(args, 1) - showHelp(args[1]) - } else { - execCommand(args) - } -} - -// configureUI configures user interface -func configureUI() { - if options.GetB(OPT_NO_COLOR) { - fmtc.DisableColors = true - } - - fmtutil.SeparatorSymbol = "–" - fmtutil.SeparatorFullscreen = true - fmtutil.SizeSeparator = " " - table.SeparatorSymbol = "–" - table.HeaderCapitalize = true -} - -// execCommand executes command -func execCommand(args []string) { - var err error - - client, err = ic.NewAPI( - options.GetS(OPT_HOST), - options.GetS(OPT_USER), - options.GetS(OPT_PASS), - ) - - if err != nil { - printErrorExit(err.Error()) - } - - switch args[0] { - case CMD_STATS: - showServerStats() - case CMD_LIST_MOUNTS: - listMounts() - case CMD_LIST_CLIENTS: - checkForRequiredArgs(args, 1) - listClients(args[1]) - case CMD_MOVE_CLIENTS: - checkForRequiredArgs(args, 2) - moveClients(args[1], args[2]) - case CMD_UPDATE_META: - checkForRequiredArgs(args, 3) - updateMeta(args[1], args[2], args[3]) - case CMD_KILL_CLIENT: - checkForRequiredArgs(args, 2) - killClient(args[1], args[2]) - case CMD_KILL_SOURCE: - checkForRequiredArgs(args, 1) - killSource(args[1]) - default: - showUsage() - } -} - -// showHelp prints command usage info -func showHelp(command string) { - switch command { - case CMD_STATS: - helpCmdStats() - case CMD_LIST_MOUNTS: - helpCmdListMounts() - case CMD_LIST_CLIENTS: - helpCmdListClients() - case CMD_MOVE_CLIENTS: - helpCmdMoveClients() - case CMD_UPDATE_META: - helpCmdUpdateMeta() - case CMD_KILL_CLIENT: - helpCmdKillClient() - case CMD_KILL_SOURCE: - helpCmdKillSource() - default: - showUsage() - } -} - -// showServerStats prints server stats -func showServerStats() { - stats, err := client.GetStats() - - if err != nil { - printErrorExit(err.Error()) - } - - fmtc.NewLine() - printServerHeader(stats.Info.ID) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Sources", fmtutil.PrettyNum(stats.Stats.Sources)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Banned IPs", fmtutil.PrettyNum(stats.Stats.BannedIPs)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Clients", fmtutil.PrettyNum(stats.Stats.Clients)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Connections", fmtutil.PrettyNum(stats.Stats.Connections)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listeners", fmtutil.PrettyNum(stats.Stats.Listeners)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Stats", fmtutil.PrettyNum(stats.Stats.Stats)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Client Connections", fmtutil.PrettyNum(stats.Stats.ClientConnections)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "File Connections", fmtutil.PrettyNum(stats.Stats.FileConnections)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listener Connections", fmtutil.PrettyNum(stats.Stats.ListenerConnections)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Stats Connections", fmtutil.PrettyNum(stats.Stats.StatsConnections)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Source Client Connections", fmtutil.PrettyNum(stats.Stats.SourceClientConnections)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Source Relay Connections", fmtutil.PrettyNum(stats.Stats.SourceRelayConnections)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Source Total Connections", fmtutil.PrettyNum(stats.Stats.SourceTotalConnections)) - fmtc.Printf( - " {*}%-28s{!} {s}|{!} %s {s-}(%s){!}\n", "Stream Bytes Read", - fmtutil.PrettyNum(stats.Stats.StreamBytesRead), - fmtutil.PrettySize(stats.Stats.StreamBytesRead), - ) - fmtc.Printf( - " {*}%-28s{!} {s}|{!} %s {s-}(%s){!}\n", "Stream Bytes Sent", - fmtutil.PrettyNum(stats.Stats.StreamBytesSent), - fmtutil.PrettySize(stats.Stats.StreamBytesSent), - ) - - for path, source := range stats.Sources { - showSeparator(false) - fmtc.Printf(" {*y}%s{!} {s-}(online: %s){!}\n", path, timeutil.PrettyDuration(time.Since(source.StreamStarted))) - showSeparator(false) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Source IP", source.SourceIP) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Name", formatString(source.Info.Name)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Genre", formatString(source.Genre)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Description", formatString(source.Info.Description)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Type", formatString(source.Info.Type)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "URL", formatString(source.Info.URL)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listen URL", formatString(source.ListenURL)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "SubType", formatString(source.Info.SubType)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %t\n", "Public", source.Public) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "User-Agent", formatString(source.UserAgent)) - showSeparator(true) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Bitrate", fmtutil.PrettyNum(source.AudioInfo.Bitrate)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Channels", fmtutil.PrettyNum(source.AudioInfo.Channels)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s Hz\n", "SampleRate", fmtutil.PrettyNum(source.AudioInfo.SampleRate)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "CodecID", fmtutil.PrettyNum(source.AudioInfo.CodecID)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "RawInfo", formatString(source.AudioInfo.RawInfo)) - showSeparator(true) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Artist", formatString(source.Track.Artist)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Title", formatString(source.Track.Title)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Artwork", formatString(source.Track.Artwork)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Metadata URL", formatString(source.Track.MetadataURL)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "RawInfo", formatString(source.Track.RawInfo)) - fmtc.Printf( - " {*}%-28s{!} {s}|{!} %s {s-}(%s ago){!}\n", "Metadata Updated", - timeutil.Format(source.MetadataUpdated, "%Y/%m/%d %H:%M:%S"), - timeutil.PrettyDuration(time.Since(source.MetadataUpdated)), - ) - showSeparator(true) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listeners", fmtutil.PrettyNum(source.Stats.Listeners)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listener Peak", fmtutil.PrettyNum(source.Stats.ListenerPeak)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Max Listeners", fmtutil.PrettyNum(source.Stats.MaxListeners)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Slow Listeners", fmtutil.PrettyNum(source.Stats.SlowListeners)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Listener Connections", fmtutil.PrettyNum(source.Stats.ListenerConnections)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Connected", fmtutil.PrettyNum(source.Stats.Connected)) - fmtc.Printf(" {*}%-28s{!} {s}|{!} %s\n", "Queue Size", fmtutil.PrettyNum(source.Stats.QueueSize)) - - fmtc.Printf( - " {*}%-28s{!} {s}|{!} %s {s-}(%s/s){!}\n", "Incoming Bitrate", - fmtutil.PrettyNum(source.Stats.IncomingBitrate), - fmtutil.PrettySize(source.Stats.IncomingBitrate), - ) - - fmtc.Printf( - " {*}%-28s{!} {s}|{!} %s {s-}(%s/s){!}\n", "Outgoing Bitrate", - fmtutil.PrettyNum(source.Stats.OutgoingBitrate), - fmtutil.PrettySize(source.Stats.OutgoingBitrate), - ) - - fmtc.Printf( - " {*}%-28s{!} {s}|{!} %s {s-}(%s){!}\n", "Total Bytes Read", - fmtutil.PrettyNum(source.Stats.TotalBytesRead), - fmtutil.PrettySize(source.Stats.TotalBytesRead), - ) - - fmtc.Printf( - " {*}%-28s{!} {s}|{!} %s {s-}(%s){!}\n", "Total Bytes Sent", - fmtutil.PrettyNum(source.Stats.TotalBytesSent), - fmtutil.PrettySize(source.Stats.TotalBytesSent), - ) - } - - showSeparator(false) - fmtc.NewLine() -} - -// listMounts prints list of all connected mount points -func listMounts() { - mounts, err := client.ListMounts() - - if err != nil { - printErrorExit(err.Error()) - } - - if len(mounts) == 0 { - fmtc.Println("{y}No mounts found{!}") - return - } - - t := table.NewTable("path", "listeners", "connected", "content-type") - t.SetAlignments(table.ALIGN_LEFT, table.ALIGN_RIGHT, table.ALIGN_RIGHT) - t.SetSizes(20, 10, 10) - - fmtc.NewLine() - - for _, m := range mounts { - t.Print( - m.Path, fmtutil.PrettyNum(m.Listeners), - timeutil.ShortDuration(m.Connected), m.ContentType, - ) - } - - t.Separator() - - fmtc.NewLine() -} - -// listClients prints info about clients (listeners) connected to given mount point -func listClients(mount string) { - mount = formatMount(mount) - listeners, err := client.ListClients(mount) - - if err != nil { - printErrorExit(err.Error()) - } - - if len(listeners) == 0 { - fmtc.Println("{y}No listeners found{!}") - return - } - - t := table.NewTable("id", "ip", "lag", "connected", "user-agent") - t.SetAlignments(table.ALIGN_RIGHT, table.ALIGN_RIGHT, table.ALIGN_RIGHT, table.ALIGN_RIGHT) - t.SetSizes(6, 14, 10, 9) - - fmtc.NewLine() - - for _, l := range listeners { - t.Print( - l.ID, l.IP, fmtutil.PrettySize(l.Lag), - timeutil.ShortDuration(l.Connected), - l.UserAgent, - ) - } - - t.Separator() - - fmtc.NewLine() -} - -// moveClients moves clients from one mount point to another -func moveClients(fromMount, toMount string) { - fromMount = formatMount(fromMount) - toMount = formatMount(toMount) - - err := client.MoveClients(fromMount, toMount) - - if err != nil { - printErrorExit(err.Error()) - } - - fmtc.Printf("{g}Clients successfully moved from %s to %s{!}\n", fromMount, toMount) -} - -// updateMeta updates metadata for given mount point -func updateMeta(mount, artist, title string) { - mount = formatMount(mount) - - err := client.UpdateMeta(mount, ic.TrackMeta{ - Artist: artist, - Title: title, - }) - - if err != nil { - printErrorExit(err.Error()) - } - - fmtc.Printf("{g}Metadata successfully updated for %s{!}\n", mount) -} - -// killClient detaches client with given ID from the mount point -func killClient(mount, clientID string) { - mount = formatMount(mount) - id, err := strconv.Atoi(clientID) - - if err != nil { - printErrorExit(err.Error()) - } - - err = client.KillClient(mount, id) - - if err != nil { - printErrorExit(err.Error()) - } - - fmtc.Printf("{g}Cliend %d successfully detached from %s{!}\n", id, mount) -} - -// killSource detaches source from given mount point -func killSource(mount string) { - mount = formatMount(mount) - - err := client.KillSource(mount) - - if err != nil { - printErrorExit(err.Error()) - } - - fmtc.Printf("{g}Source successfully detached from %s{!}\n", mount) -} - -// printServerHeader prints header with icecast info -func printServerHeader(id string) { - showSeparator(false) - - if id == "" { - fmtc.Printf(" {*}{#45}Icecast Server{!} on {*}%s{!}\n", options.GetS(OPT_HOST)) - } else { - fmtc.Printf(" {*}{#45}Icecast Server{!} on {*}%s{!} {s-}(%s){!}\n", options.GetS(OPT_HOST), id) - } - - showSeparator(false) -} - -// showSeparator prints separator -func showSeparator(shadow bool) { - if shadow { - fmtutil.SeparatorColorTag = "{s-}" - } else { - fmtutil.SeparatorColorTag = "{s}" - } - - fmtutil.Separator(true) -} - -// formatString formats string for stats info -func formatString(s string) string { - if s == "" { - return fmtc.Sprintf("{s-}—{!}") - } - - return s -} - -// formatMount formats mount name -func formatMount(mount string) string { - if !strings.HasPrefix(mount, "/") { - return "/" + mount - } - - return mount -} - -// checks command for required args num -func checkForRequiredArgs(args []string, required int) { - if len(args) >= required+1 { - return - } - - printErrorExit("Wrong number of arguments for %s command", args[0]) -} - -// printError prints error message to console -func printError(f string, a ...interface{}) { - fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) -} - -// printErrorExit prints error message to console and exit with error code -func printErrorExit(f string, a ...interface{}) { - fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) - os.Exit(1) -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -func helpCmdStats() { - fmtc.NewLine() - fmtc.Println("{*}Description:{!}\n") - fmtc.Println(" Shows internal statistics kept by the Icecast server.\n") - fmtc.Println("{*}Usage:{!}\n") - fmtc.Printf(" {c*}%s{!} {y}%s{!}\n\n", APP, CMD_STATS) - fmtc.Println("{*}Examples:{!}\n") - fmtc.Printf(" %s %s\n", APP, CMD_STATS) - fmtc.NewLine() -} - -func helpCmdListMounts() { - fmtc.NewLine() - fmtc.Println("{*}Description:{!}\n") - fmtc.Println(" Shows all the currently connected mountpoints.\n") - fmtc.Println("{*}Usage:{!}\n") - fmtc.Printf(" {c*}%s{!} {y}%s{!}\n\n", APP, CMD_LIST_MOUNTS) - fmtc.Println("{*}Examples:{!}\n") - fmtc.Printf(" %s %s\n", APP, CMD_LIST_MOUNTS) - fmtc.NewLine() -} - -func helpCmdListClients() { - fmtc.NewLine() - fmtc.Println("{*}Description:{!}\n") - fmtc.Println(" Shows all the clients currently connected to a specific mountpoint.") - fmtc.NewLine() - fmtc.Println("{*}Usage:{!}\n") - fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}mount{!}\n", APP, CMD_LIST_CLIENTS) - fmtc.NewLine() - fmtc.Println("{*}Arguments:{!}\n") - fmtc.Println(" {g}mount{!} - Mount name {s-}(with or without leading slash){!}") - fmtc.NewLine() - fmtc.Println("{*}Examples:{!}\n") - fmtc.Printf(" %s %s /source1.ogg \n", APP, CMD_LIST_CLIENTS) - fmtc.Printf(" %s %s source1.ogg \n", APP, CMD_LIST_CLIENTS) - fmtc.NewLine() -} - -func helpCmdMoveClients() { - fmtc.NewLine() - fmtc.Println("{*}Description:{!}\n") - fmtc.Println(" This command provides the ability to migrate currently connected listeners") - fmtc.Println(" from one mountpoint to another.") - fmtc.NewLine() - fmtc.Println("{*}Usage:{!}\n") - fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}from-mount to-mount{!}\n", APP, CMD_MOVE_CLIENTS) - fmtc.NewLine() - fmtc.Println("{*}Arguments:{!}\n") - fmtc.Println(" {g}from-mount{!} - Source mount name {s-}(with or without leading slash){!}") - fmtc.Println(" {g}to-mount {!} - Target mount name {s-}(with or without leading slash){!}") - fmtc.NewLine() - fmtc.Println("{*}Examples:{!}\n") - fmtc.Printf(" %s %s /source1.ogg /source2.ogg\n", APP, CMD_MOVE_CLIENTS) - fmtc.Printf(" %s %s source1.aac source2.aac \n", APP, CMD_MOVE_CLIENTS) - fmtc.NewLine() -} - -func helpCmdUpdateMeta() { - fmtc.NewLine() - fmtc.Println("{*}Description:{!}\n") - fmtc.Println(" This command provides the ability for either a source client or any external") - fmtc.Println(" program to update the metadata information for a particular mountpoint.") - fmtc.NewLine() - fmtc.Println("{*}Usage:{!}\n") - fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}mount artist title{!}\n", APP, CMD_UPDATE_META) - fmtc.NewLine() - fmtc.Println("{*}Arguments:{!}\n") - fmtc.Println(" {g}mount {!} - Mount name {s-}(with or without leading slash){!}") - fmtc.Println(" {g}artist{!} - Track artist name") - fmtc.Println(" {g}title {!} - Track title") - fmtc.NewLine() - fmtc.Println("{*}Examples:{!}\n") - fmtc.Printf(" %s %s \"Wretch 32\" \"Traktor (Brookes Brothers Remix)\"\n", APP, CMD_UPDATE_META) - fmtc.NewLine() -} - -func helpCmdKillClient() { - fmtc.NewLine() - fmtc.Println("{*}Description:{!}\n") - fmtc.Println(" Disconnects a specific listener of a currently connected mountpoint.") - fmtc.NewLine() - fmtc.Println("{*}Usage:{!}\n") - fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}mount{!}\n", APP, CMD_KILL_CLIENT) - fmtc.NewLine() - fmtc.Println("{*}Arguments:{!}\n") - fmtc.Println(" {g}mount {!} - Mount name {s-}(with or without leading slash){!}") - fmtc.Println(" {g}client-id{!} - Client ID") - fmtc.NewLine() - fmtc.Println("{*}Examples:{!}\n") - fmtc.Printf(" %s %s /source1.ogg 457\n", APP, CMD_KILL_CLIENT) - fmtc.Printf(" %s %s source1.ogg 457\n", APP, CMD_KILL_CLIENT) - fmtc.NewLine() -} - -func helpCmdKillSource() { - fmtc.NewLine() - fmtc.Println("{*}Description:{!}\n") - fmtc.Println(" Disconnects a specific mountpoint from the server.") - fmtc.NewLine() - fmtc.Println("{*}Usage:{!}\n") - fmtc.Printf(" {c*}%s{!} {y}%s{!} {g}mount{!}\n", APP, CMD_KILL_SOURCE) - fmtc.NewLine() - fmtc.Println("{*}Arguments:{!}\n") - fmtc.Println(" {g}mount{!} - Mount name {s-}(with or without leading slash){!}") - fmtc.NewLine() - fmtc.Println("{*}Examples:{!}\n") - fmtc.Printf(" %s %s /source1.ogg \n", APP, CMD_KILL_SOURCE) - fmtc.Printf(" %s %s source1.ogg \n", APP, CMD_KILL_SOURCE) - fmtc.NewLine() -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -// showUsage print usage info -func showUsage() { - genUsage().Render() -} - -// genUsage generates usage info -func genUsage() *usage.Info { - info := usage.NewInfo("icecli", "arguments…") - - info.AddCommand(CMD_STATS, "Show Icecast statistics") - info.AddCommand(CMD_LIST_MOUNTS, "List mount points") - info.AddCommand(CMD_LIST_CLIENTS, "List clients", "mount") - info.AddCommand(CMD_MOVE_CLIENTS, "Move clients between mounts", "from-mount", "to-mount") - info.AddCommand(CMD_UPDATE_META, "Update meta for mount", "mount", "artist", "title") - info.AddCommand(CMD_KILL_CLIENT, "Kill client connection", "mount", "client-id") - info.AddCommand(CMD_KILL_SOURCE, "Kill source connection", "mount") - info.AddCommand(CMD_HELP, "Show detailed info about command usage", "command") - - info.AddOption(OPT_HOST, "URL of Icecast instance {s-}(default: http://127.0.0.1:8000){!}", "host") - info.AddOption(OPT_USER, "Admin username {s-}(default: admin){!}", "username") - info.AddOption(OPT_PASS, "Admin password {s-}(default: hackme){!}", "password") - info.AddOption(OPT_NO_COLOR, "Disable colors in output") - info.AddOption(OPT_HELP, "Show this help message") - info.AddOption(OPT_VER, "Show version") - - info.AddExample( - CMD_STATS+" -H 127.0.0.1:10000", - "Show stats for server on 127.0.0.1:10000", - ) - - info.AddExample( - CMD_KILL_CLIENT+" -P mYsUpPaPaSs /stream3 361", - "Detach client with ID 361 from /stream3", - ) - - info.AddExample( - CMD_LIST_CLIENTS+" -H 127.0.0.1:10000 -U super_admin -P mYsUpPaPaSs /stream3", - "List clients on /stream3", - ) - - return info -} - -// genCompletion generates completion for different shells -func genCompletion() { - switch options.GetS(OPT_COMPLETION) { - case "bash": - fmt.Printf(bash.Generate(genUsage(), APP)) - case "fish": - fmt.Printf(fish.Generate(genUsage(), APP)) - case "zsh": - fmt.Printf(zsh.Generate(genUsage(), optMap, APP)) - default: - os.Exit(1) - } - - os.Exit(0) -} - -// showAbout shows info about version -func showAbout() { - about := &usage.About{ - App: APP, - Version: VER, - Desc: DESC, - Year: 2006, - Owner: "ESSENTIAL KAOS", - License: "Apache License, Version 2.0 ", - UpdateChecker: usage.UpdateChecker{"essentialkaos/icecli", update.GitHubChecker}, - } - - about.Render() + CLI.Run(gitrev, gomod) }