Compare commits
No commits in common. "main" and "listenbrainz" have entirely different histories.
main
...
listenbrai
15
.gitea/workflows/push.yaml
Normal file
15
.gitea/workflows/push.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
name: builder
|
||||
run-name: ${{ gitea.actor }} is building
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
cache: true
|
||||
- run: go mod tidy
|
||||
- run: go build -o gspot
|
28
.gitea/workflows/releaser.yaml
Normal file
28
.gitea/workflows/releaser.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
name: deployer
|
||||
run-name: ${{ gitea.actor }} is releasing
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
go-releaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup up go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
cache: true
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: nightly
|
||||
args: release --clean
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ACCESS_TOKEN_GITEA}}
|
19
.gitignore
vendored
19
.gitignore
vendored
@ -1,17 +1,2 @@
|
||||
bin/
|
||||
bin/*
|
||||
/gospt
|
||||
gospt_zsh
|
||||
gospt_bash
|
||||
gospt_fish
|
||||
completions
|
||||
|
||||
.idea/*
|
||||
.idea
|
||||
|
||||
*.log
|
||||
*.out
|
||||
|
||||
*.tmp
|
||||
|
||||
dist/
|
||||
dist
|
||||
gspot
|
||||
|
@ -1,8 +1,3 @@
|
||||
run:
|
||||
deadline: 10m
|
||||
skip-dirs:
|
||||
- hack
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
@ -19,7 +14,7 @@ linters:
|
||||
- unconvert
|
||||
- unused
|
||||
|
||||
linters-settings:
|
||||
linters-settings:
|
||||
gocritic:
|
||||
# Which checks should be enabled; can't be combined with 'disabled-checks';
|
||||
# See https://go-critic.github.io/overview#checks-overview
|
||||
@ -36,7 +31,6 @@ linters-settings:
|
||||
- paramTypeCombine
|
||||
- importShadow
|
||||
- commentFormatting
|
||||
- rangeValCopy
|
||||
|
||||
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
|
||||
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
|
||||
@ -50,6 +44,8 @@ linters-settings:
|
||||
hugeParam:
|
||||
# size in bytes that makes the warning trigger (default 80)
|
||||
sizeThreshold: 1000
|
||||
rangeValCopy:
|
||||
sizeThreshold: 1024
|
||||
rangeExprCopy:
|
||||
# size in bytes that makes the warning trigger (default 512)
|
||||
sizeThreshold: 512
|
||||
@ -71,8 +67,6 @@ linters-settings:
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
gofmt:
|
||||
auto-fix: false
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
@ -85,3 +79,4 @@ issues:
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "SA(1019|1029|5011)"
|
||||
|
||||
|
@ -18,7 +18,7 @@ builds:
|
||||
- goos: windows
|
||||
goarch: "386"
|
||||
ldflags:
|
||||
- -s -w -X git.asdf.cafe/abs3nt/gospt/src.cmd.Version={{.Version}}
|
||||
- -s -w -X git.asdf.cafe/abs3nt/gspot/src/components/cli.Version={{.Version}}
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
@ -30,32 +30,29 @@ archives:
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- completions/*
|
||||
rlcp: true
|
||||
- goos: windows
|
||||
format: zip
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
name_template: "checksums.txt"
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
groups:
|
||||
- title: Added
|
||||
regexp: '^.*?ADD(\([[:word:]]+\))??!?:.+$'
|
||||
order: 0
|
||||
- title: 'Bug fixes'
|
||||
regexp: '^.*?BUG(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: 'Enhancements'
|
||||
regexp: '^.*?IMPROVED(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: 'Docs'
|
||||
regexp: '^.*?DOC(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: 'CI'
|
||||
regexp: '^.*?CI(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: Others
|
||||
order: 999
|
||||
- title: Added
|
||||
regexp: '^.*?ADD(\([[:word:]]+\))??!?:.+$'
|
||||
order: 0
|
||||
- title: "Bug fixes"
|
||||
regexp: '^.*?BUG(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: "Enhancements"
|
||||
regexp: '^.*?IMPROVED(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: "Docs"
|
||||
regexp: '^.*?DOC(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: "CI"
|
||||
regexp: '^.*?CI(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: Others
|
||||
order: 999
|
||||
|
@ -1,18 +0,0 @@
|
||||
steps:
|
||||
build:
|
||||
image: golang:1.22
|
||||
commands:
|
||||
- go mod tidy
|
||||
- go build -o gospt
|
||||
- mkdir completions
|
||||
- ./gospt completion zsh > completions/gospt_zsh
|
||||
- ./gospt completion bash > completions/gospt_bash
|
||||
- ./gospt completion fish > completions/gospt_fish
|
||||
|
||||
publish:
|
||||
image: goreleaser/goreleaser
|
||||
commands:
|
||||
- goreleaser release --clean
|
||||
secrets: [ gitea_token ]
|
||||
when:
|
||||
event: tag
|
4
LICENSE
4
LICENSE
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 abs3nt
|
||||
Copyright (c) 2024 abs3nt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
33
Makefile
33
Makefile
@ -1,32 +1,21 @@
|
||||
build: gospt
|
||||
build:
|
||||
go build -ldflags="-X 'git.asdf.cafe/abs3nt/gspot/src/components/cli.Version=$(shell git show -s --date=short --pretty='format:%h (%ad)' HEAD)'" -o dist/ .
|
||||
|
||||
gospt: $(shell find . -name '*.go')
|
||||
go build -o gospt .
|
||||
|
||||
completions:
|
||||
mkdir -p completions
|
||||
gospt completion zsh > completions/_gospt
|
||||
gospt completion bash > completions/gospt
|
||||
gospt completion fish > completions/gospt.fish
|
||||
|
||||
run:
|
||||
go run main.go
|
||||
run: build
|
||||
./dist/gspot
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
clean:
|
||||
rm -f gospt
|
||||
rm -rf completions
|
||||
rm -rf dist
|
||||
|
||||
uninstall:
|
||||
rm -f /usr/bin/gospt
|
||||
rm -f /usr/share/zsh/site-functions/_gospt
|
||||
rm -f /usr/share/bash-completion/completions/gospt
|
||||
rm -f /usr/share/fish/vendor_completions.d/gospt.fish
|
||||
rm -f /usr/bin/gspot
|
||||
rm -f /usr/share/zsh/site-functions/_gspot
|
||||
rm -f /usr/share/bash-completion/completions/gspot
|
||||
|
||||
install:
|
||||
cp gospt /usr/bin
|
||||
gospt completion zsh > /usr/share/zsh/site-functions/_gospt
|
||||
gospt completion bash > /usr/share/bash-completion/completions/gospt
|
||||
gospt completion fish > /usr/share/fish/vendor_completions.d/gospt.fish
|
||||
cp ./dist/gspot /usr/bin
|
||||
cp ./completions/_gspot /usr/share/zsh/site-functions/_gspot
|
||||
cp ./completions/gspot /usr/share/bash-completion/completionsgspotg
|
||||
|
59
README.md
59
README.md
@ -1,51 +1,43 @@
|
||||
IF YOU ARE ON GITHUB.COM GO HERE INSTEAD: https://git.asdf.cafe/abs3nt/gospt :)
|
||||
IF YOU ARE ON GITHUB.COM GO HERE INSTEAD: https://git.asdf.cafe/abs3nt/gspot
|
||||
|
||||
|
||||
If you open an issue or PR on github I won't see it please use git. Register on asdf and open your PRs there
|
||||
If you open an issue or PR on github I won't see it please use git.asdf.cafe. Register on asdf and open your PRs there
|
||||
|
||||
This project is still under heavy development and some things might not work or not work as intended. Don't hesitate to open an issue to let me know.
|
||||
|
||||
---
|
||||
[![status-badge](https://ci.asdf.cafe/api/badges/abs3nt/gospt/status.svg)](https://ci.asdf.cafe/abs3nt/gospt)
|
||||
![video](/assets/gospt.gif)
|
||||
|
||||
[![status-badge](https://ci.asdf.cafe/api/badges/abs3nt/gspot/status.svg)](https://ci.asdf.cafe/abs3nt/gspot)
|
||||
|
||||
# To install (with a package manager):
|
||||
|
||||
## Archlinux ([AUR])
|
||||
```yay -S gospt```
|
||||
|
||||
or
|
||||
|
||||
```yay -S gospt-git```
|
||||
|
||||
## NetBSD ([Official repositories])
|
||||
```pkgin install gospt```
|
||||
`yay -S gspot-git`
|
||||
|
||||
# To build from source by pulling and building the binary
|
||||
|
||||
`git clone https://git.asdf.cafe/abs3nt/gspot`
|
||||
|
||||
```git clone https://git.asdf.cafe/abs3nt/gospt```
|
||||
`cd gspot`
|
||||
|
||||
```cd gospt```
|
||||
`make build && sudo make install`
|
||||
|
||||
|
||||
```make build && sudo make install```
|
||||
|
||||
[AUR]: https://aur.archlinux.org/packages/gospt
|
||||
[Official repositories]: http://cvsweb.netbsd.org/bsdweb.cgi/pkgsrc/audio/gospt/
|
||||
[AUR]: https://aur.archlinux.org/packages/gspot-git
|
||||
|
||||
# Configuration
|
||||
|
||||
go here https://developer.spotify.com/dashboard/applications to make a spotify application. you will need a client ID and a client secret. Set your redirect uri like this:
|
||||
|
||||
```http://localhost:8888/callback```
|
||||
`http://localhost:8888/callback`
|
||||
|
||||
add your information to ~/.config/gospt/client.yml like this
|
||||
add your information to ~/.config/gspot/gspot.yml like this
|
||||
|
||||
```
|
||||
client_id: "idgoeshere"
|
||||
client_secret: "secretgoeshere"
|
||||
port: "8888"
|
||||
```
|
||||
|
||||
if you dont want to store your secret in the file in plaintext you can use a command to retreive it:
|
||||
|
||||
```
|
||||
@ -54,30 +46,37 @@ client_secret_cmd: "secret spotify_secret"
|
||||
|
||||
you should have either client_secret or client_secret_cmd
|
||||
|
||||
you can enable debug logging by adding
|
||||
|
||||
then run
|
||||
```
|
||||
log_level: "debug"
|
||||
log_output: "file"
|
||||
```
|
||||
|
||||
```gospt```
|
||||
it will log to ~/.config/gspot/gspot.log
|
||||
|
||||
you will be asked to login, you will only have to do this the first time. After login you will be asked to select your default device, this will also only happen once. To reset your device run ```gospot setdevice```
|
||||
## RUNNING
|
||||
|
||||
`gspot`
|
||||
|
||||
you will be asked to login, you will only have to do this the first time. After login you will be asked to select your default device.
|
||||
|
||||
helpful keybinds are shown in the bottom of the screen, hit ? to see all of them
|
||||
|
||||
To use the custom radio feature:
|
||||
|
||||
```gospt radio```
|
||||
`gspot radio`
|
||||
|
||||
|
||||
or hit ctrl+r on any track in the TUI. This will start an extended radio. To replenish the current radio run ```gospt refillradio``` and all the songs already listened will be removed and that number of new recomendations will be added.
|
||||
or hit ctrl+r on any track in the TUI. This will start an extended radio. To replenish the current radio run `gspot refillradio` and all the songs already listened will be removed and that number of new recomendations will be added.
|
||||
|
||||
This radio uses slightly different logic than the standard spotify radio to give a longer playlist and more recomendation. With a cronjob you can schedule refill to run to have an infinite and morphing radio station.
|
||||
|
||||
To view help:
|
||||
|
||||
```gospt --help```
|
||||
`gspot --help`
|
||||
|
||||
Very open to contributations feel free to open a PR
|
||||
|
||||
[tmux plugin](https://git.asdf.cafe/abs3nt/tmux-gospt)
|
||||
[tmux plugin](https://git.asdf.cafe/abs3nt/tmux-gspot)
|
||||
|
||||
[wiki](https://git.asdf.cafe/abs3nt/gospt/wiki)
|
||||
[wiki](https://git.asdf.cafe/abs3nt/gspot/wiki)
|
||||
|
BIN
assets/gospt.gif
BIN
assets/gospt.gif
Binary file not shown.
Before Width: | Height: | Size: 1.7 MiB |
BIN
assets/gospt.png
BIN
assets/gospt.png
Binary file not shown.
Before Width: | Height: | Size: 323 KiB |
@ -1,18 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(clearRadioCmd)
|
||||
}
|
||||
|
||||
var clearRadioCmd = &cobra.Command{
|
||||
Use: "clearradio",
|
||||
Short: "Wipes the radio playlist and creates an empty one",
|
||||
Long: `Wipes the radio playlist and creates an empty one, mostly for debugging or if something goes wrong`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.ClearRadio(ctx)
|
||||
},
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var completionCmd = &cobra.Command{
|
||||
Hidden: true,
|
||||
Use: "completion [bash|zsh|fish|powershell]",
|
||||
Short: "Generate completion script",
|
||||
Long: fmt.Sprintf(`To load completions:
|
||||
|
||||
Bash:
|
||||
|
||||
$ source <(%[1]s completion bash)
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
# Linux:
|
||||
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
|
||||
# macOS:
|
||||
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
|
||||
|
||||
Zsh:
|
||||
|
||||
# If shell completion is not already enabled in your environment,
|
||||
# you will need to enable it. You can execute the following once:
|
||||
|
||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
|
||||
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
|
||||
fish:
|
||||
|
||||
$ %[1]s completion fish | source
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
|
||||
`, rootCmd.Name()),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgs: []string{"bash", "zsh", "fish"},
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
rootCmd.GenBashCompletion(os.Stdout)
|
||||
case "zsh":
|
||||
rootCmd.GenZshCompletion(os.Stdout)
|
||||
case "fish":
|
||||
rootCmd.GenFishCompletion(os.Stdout, true)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(completionCmd)
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(devicesCmd)
|
||||
}
|
||||
|
||||
var devicesCmd = &cobra.Command{
|
||||
Use: "devices",
|
||||
Short: "Prints out devices",
|
||||
Long: `Prints out devices`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.Devices(ctx)
|
||||
},
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(downloadCoverCmd)
|
||||
}
|
||||
|
||||
var downloadCoverCmd = &cobra.Command{
|
||||
Use: "download_cover",
|
||||
Aliases: []string{"dl"},
|
||||
Short: "Returns url for currently playing song art",
|
||||
Long: `Returns url for currently playing song art`,
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1)),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.DownloadCover(ctx, args)
|
||||
},
|
||||
}
|
19
cmd/like.go
19
cmd/like.go
@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(likeCmd)
|
||||
}
|
||||
|
||||
var likeCmd = &cobra.Command{
|
||||
Use: "like",
|
||||
Aliases: []string{"l"},
|
||||
Short: "Likes song",
|
||||
Long: `Likes song`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.Like(ctx)
|
||||
},
|
||||
}
|
28
cmd/link.go
28
cmd/link.go
@ -1,28 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// linkCmd represents the link command
|
||||
var linkCmd = &cobra.Command{
|
||||
Use: "link",
|
||||
Aliases: []string{"yy"},
|
||||
Short: "Print link to currently playing song",
|
||||
Long: `Print link to currently playing song`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
link, err := commands.Link(ctx)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(link)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(linkCmd)
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// linkCmd represents the link command
|
||||
var linkContextCmd = &cobra.Command{
|
||||
Use: "linkcontext",
|
||||
Aliases: []string{"lc"},
|
||||
Short: "Get url to current context(album, playlist)",
|
||||
Long: `Get url to current context(album, playlist)`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
link, err := commands.LinkContext(ctx)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(link)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(linkContextCmd)
|
||||
}
|
22
cmd/mute.go
22
cmd/mute.go
@ -1,22 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(muteCmd)
|
||||
}
|
||||
|
||||
var muteCmd = &cobra.Command{
|
||||
Use: "mute",
|
||||
Short: "mutes playback",
|
||||
Long: `Mutes the spotify device, playback will continue`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := commands.SetVolume(ctx, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
30
cmd/next.go
30
cmd/next.go
@ -1,30 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(nextCmd)
|
||||
}
|
||||
|
||||
var nextCmd = &cobra.Command{
|
||||
Use: "next {amount}",
|
||||
Aliases: []string{"n", "skip"},
|
||||
Args: cobra.MatchAll(cobra.RangeArgs(0, 1)),
|
||||
Short: "Skip to next song or skip the specified number of tracks",
|
||||
Long: `Skip to next song of skip the specified number of tracks`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
skipAmt := 1
|
||||
if len(args) >= 1 {
|
||||
var err error
|
||||
skipAmt, err = strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return commands.Next(ctx, skipAmt, false)
|
||||
},
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(nowPlayingCmd)
|
||||
}
|
||||
|
||||
var nowPlayingCmd = &cobra.Command{
|
||||
Use: "nowplaying",
|
||||
Aliases: []string{"now"},
|
||||
Short: "Shows song and artist of currently playing song",
|
||||
Long: `Shows song and artist of currently playing song, useful for scripting`,
|
||||
Args: cobra.MatchAll(cobra.RangeArgs(0, 1)),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.NowPlaying(ctx, args)
|
||||
},
|
||||
}
|
19
cmd/pause.go
19
cmd/pause.go
@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(pauseCmd)
|
||||
}
|
||||
|
||||
var pauseCmd = &cobra.Command{
|
||||
Use: "pause",
|
||||
Short: "Pauses spotify",
|
||||
Aliases: []string{"pa"},
|
||||
Long: `Pauses currently playing song on spotify`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.Pause(ctx)
|
||||
},
|
||||
}
|
19
cmd/play.go
19
cmd/play.go
@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(playCmd)
|
||||
}
|
||||
|
||||
var playCmd = &cobra.Command{
|
||||
Use: "play",
|
||||
Aliases: []string{"pl", "start", "s"},
|
||||
Short: "Plays spotify",
|
||||
Long: `Plays queued song on spotify, uses last used device and activates it if needed`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.Play(ctx)
|
||||
},
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(playurlCmd)
|
||||
}
|
||||
|
||||
var playurlCmd = &cobra.Command{
|
||||
Use: "playurl",
|
||||
Short: "Plays song from provided url",
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1)),
|
||||
Long: `Plays song from provided url`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.PlayUrl(ctx, args)
|
||||
},
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(previousCmd)
|
||||
}
|
||||
|
||||
var previousCmd = &cobra.Command{
|
||||
Use: "previous",
|
||||
Aliases: []string{"b", "prev", "back"},
|
||||
Short: "goes to previous song",
|
||||
Long: `if song is playing it will start over, if close to begining of song it will go to previous song`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.Previous(ctx)
|
||||
},
|
||||
}
|
19
cmd/radio.go
19
cmd/radio.go
@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(radioCmd)
|
||||
}
|
||||
|
||||
var radioCmd = &cobra.Command{
|
||||
Use: "radio",
|
||||
Aliases: []string{"r"},
|
||||
Short: "Starts radio",
|
||||
Long: `Starts radio`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return commands.Radio(ctx)
|
||||
},
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(refillRadioCmd)
|
||||
}
|
||||
|
||||
var refillRadioCmd = &cobra.Command{
|
||||
Use: "refillradio",
|
||||
Aliases: []string{"rr"},
|
||||
Short: "Refills the radio",
|
||||
Long: `Deletes all songs up to your position in the radio and adds that many songs to the end of the radio`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return commands.RefillRadio(ctx)
|
||||
},
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(repeatCmd)
|
||||
}
|
||||
|
||||
var repeatCmd = &cobra.Command{
|
||||
Use: "repeat",
|
||||
Short: "Toggles repeat",
|
||||
Long: `Switches between repeating your current context or not, spotifyd does not support single track loops`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.Repeat(ctx)
|
||||
},
|
||||
}
|
103
cmd/root.go
103
cmd/root.go
@ -1,103 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
cmds "git.asdf.cafe/abs3nt/gospt/src/commands"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gospt/src/config"
|
||||
"git.asdf.cafe/abs3nt/gospt/src/gctx"
|
||||
"tuxpa.in/a/zlog"
|
||||
|
||||
"github.com/cristalhq/aconfig"
|
||||
"github.com/cristalhq/aconfig/aconfigyaml"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// Used for flags.
|
||||
ctx *gctx.Context
|
||||
commands *cmds.Commands
|
||||
cfgFile string
|
||||
verbose bool
|
||||
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "gospt",
|
||||
Short: "A spotify TUI and CLI to manage playback, browse library, and generate radios",
|
||||
Long: `A spotify TUI and CLI to manage playback, borwse library, and generate radios written in go`,
|
||||
}
|
||||
)
|
||||
|
||||
// Execute executes the root command.
|
||||
func Execute(defCmd string) {
|
||||
if len(os.Args) == 1 {
|
||||
args := append([]string{defCmd}, os.Args[1:]...)
|
||||
rootCmd.SetArgs(args)
|
||||
}
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
zlog.SetGlobalLevel(zlog.DebugLevel)
|
||||
if len(os.Args) > 1 {
|
||||
if os.Args[1] == "completion" || os.Args[1] == "__complete" {
|
||||
return
|
||||
}
|
||||
}
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose logging")
|
||||
cobra.OnInitialize(func() {
|
||||
if verbose {
|
||||
zlog.SetGlobalLevel(zlog.TraceLevel)
|
||||
}
|
||||
})
|
||||
ctx = gctx.NewContext(context.Background())
|
||||
commands = &cmds.Commands{Context: ctx}
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
configDir, _ := os.UserConfigDir()
|
||||
cfgFile = filepath.Join(configDir, "gospt/client.yml")
|
||||
yamlDecoder := aconfigyaml.New()
|
||||
|
||||
loader := aconfig.LoaderFor(&config.Values, aconfig.Config{
|
||||
AllowUnknownFields: true,
|
||||
AllowUnknownEnvs: true,
|
||||
AllowUnknownFlags: true,
|
||||
SkipFlags: true,
|
||||
DontGenerateTags: true,
|
||||
MergeFiles: true,
|
||||
EnvPrefix: "",
|
||||
FlagPrefix: "",
|
||||
Files: []string{
|
||||
cfgFile,
|
||||
},
|
||||
FileDecoders: map[string]aconfig.FileDecoder{
|
||||
".yml": yamlDecoder,
|
||||
},
|
||||
})
|
||||
if err := loader.Load(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if config.Values.ClientSecretCmd != "" {
|
||||
args := strings.Fields(config.Values.ClientSecretCmd)
|
||||
cmd := args[0]
|
||||
secret_command := exec.Command(cmd)
|
||||
if len(args) > 1 {
|
||||
secret_command.Args = args
|
||||
}
|
||||
secret, err := secret_command.Output()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
config.Values.ClientSecret = strings.TrimSpace(string(secret))
|
||||
}
|
||||
}
|
47
cmd/seek.go
47
cmd/seek.go
@ -1,47 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(seekCmd)
|
||||
}
|
||||
|
||||
var seekCmd = &cobra.Command{
|
||||
Use: "seek {forward/backward/songposition in seconds}",
|
||||
Short: "seek forward/backward or to a given second",
|
||||
Aliases: []string{"s"},
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Long: `Seeks forward or backward, or seeks to a given position in seconds`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if args[0] == "forward" || args[0] == "f" {
|
||||
err := commands.Seek(ctx, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if args[0] == "backward" || args[0] == "b" {
|
||||
err := commands.Seek(ctx, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
pos, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pos = pos * 1000
|
||||
err = commands.SetPosition(ctx, pos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"git.asdf.cafe/abs3nt/gospt/src/tui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(setDeviceCmd)
|
||||
}
|
||||
|
||||
var setDeviceCmd = &cobra.Command{
|
||||
Use: "setdevice",
|
||||
Short: "Shows tui to pick active device",
|
||||
Long: `Allows setting or changing the active spotify device, shown in a tui`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
tui.StartTea(ctx, commands, "devices")
|
||||
},
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(shuffleCmd)
|
||||
}
|
||||
|
||||
var shuffleCmd = &cobra.Command{
|
||||
Use: "shuffle",
|
||||
Short: "Toggles shuffle",
|
||||
Long: `Enables shuffle if it is currently disabled or disables it if it is currently active`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.Shuffle(ctx)
|
||||
},
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(statusCmd)
|
||||
}
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Returns player status in json",
|
||||
Long: `Returns all player status in json, useful for scripting`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.Status(ctx)
|
||||
},
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(togglePlayCmd)
|
||||
}
|
||||
|
||||
var togglePlayCmd = &cobra.Command{
|
||||
Use: "toggleplay",
|
||||
Aliases: []string{"t"},
|
||||
Short: "Toggles the play state of spotify",
|
||||
Long: `If you are playing a song it will pause and if a song is paused it will play`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.TogglePlay(ctx)
|
||||
},
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gospt/src/tui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(tracksCmd)
|
||||
}
|
||||
|
||||
var tracksCmd = &cobra.Command{
|
||||
Use: "tracks",
|
||||
Short: "Opens saved tracks",
|
||||
Long: `Uses TUI to open a list of saved tracks`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
configDir, _ := os.UserConfigDir()
|
||||
if commands.ActiveDeviceExists(ctx) {
|
||||
return tui.StartTea(ctx, commands, "tracks")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(configDir, "gospt/device.json")); err != nil {
|
||||
return tui.StartTea(ctx, commands, "devices")
|
||||
}
|
||||
return tui.StartTea(ctx, commands, "tracks")
|
||||
},
|
||||
}
|
30
cmd/tui.go
30
cmd/tui.go
@ -1,30 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gospt/src/tui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(tuiCmd)
|
||||
}
|
||||
|
||||
var tuiCmd = &cobra.Command{
|
||||
Use: "tui",
|
||||
Short: "Default command, launches the main menu",
|
||||
Long: `Default command. this is what will run if no other commands are present. Shows the main menu.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
configDir, _ := os.UserConfigDir()
|
||||
if commands.ActiveDeviceExists(ctx) {
|
||||
return tui.StartTea(ctx, commands, "main")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(configDir, "gospt/device.json")); err != nil {
|
||||
return tui.StartTea(ctx, commands, "devices")
|
||||
}
|
||||
return tui.StartTea(ctx, commands, "main")
|
||||
},
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(unlikeCmd)
|
||||
}
|
||||
|
||||
var unlikeCmd = &cobra.Command{
|
||||
Use: "unlike",
|
||||
Aliases: []string{"u"},
|
||||
Short: "unlikes song",
|
||||
Long: `unlikes song`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
commands.Unlike(ctx)
|
||||
},
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(unmuteCmd)
|
||||
}
|
||||
|
||||
var unmuteCmd = &cobra.Command{
|
||||
Use: "unmute",
|
||||
Short: "unmutes playback",
|
||||
Long: `unmutes the spotify device, playback will continue`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := commands.SetVolume(ctx, 100)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Version = "v0.0.47"
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Prints current verison",
|
||||
Run: version,
|
||||
}
|
||||
|
||||
func version(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Gospt: %s\n", Version)
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(volumeCmd)
|
||||
}
|
||||
|
||||
var volumeCmd = &cobra.Command{
|
||||
Use: "volume",
|
||||
Short: "sets the volume",
|
||||
Aliases: []string{"v"},
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Long: `Sets the volume to the given percent [0-100] or increases/decreases by 5 percent if you say up or down`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if args[0] == "up" {
|
||||
err := commands.ChangeVolume(ctx, 5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if args[0] == "down" {
|
||||
err := commands.ChangeVolume(ctx, -5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
vol, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = commands.SetVolume(ctx, vol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// youtubeLinkCmd represents the youtube-link command
|
||||
var youtubeLinkCmd = &cobra.Command{
|
||||
Use: "youtube-link",
|
||||
Aliases: []string{"yl"},
|
||||
Short: "Print youtube link to currently playing song",
|
||||
Long: `Print youtube link to currently playing song`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
link, err := commands.YoutubeLink(ctx)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(link)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(youtubeLinkCmd)
|
||||
}
|
16
completions/_gspot
Executable file
16
completions/_gspot
Executable file
@ -0,0 +1,16 @@
|
||||
#compdef gspot
|
||||
|
||||
local -a opts
|
||||
local cur
|
||||
cur=${words[-1]}
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}")
|
||||
else
|
||||
opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-shell-completion)}")
|
||||
fi
|
||||
|
||||
if [[ "${opts[1]}" != "" ]]; then
|
||||
_describe 'values' opts
|
||||
else
|
||||
_files
|
||||
fi
|
35
completions/gspot
Executable file
35
completions/gspot
Executable file
@ -0,0 +1,35 @@
|
||||
#! /bin/bash
|
||||
|
||||
: ${PROG:=$(basename ${BASH_SOURCE})}
|
||||
|
||||
# Macs have bash3 for which the bash-completion package doesn't include
|
||||
# _init_completion. This is a minimal version of that function.
|
||||
_cli_init_completion() {
|
||||
COMPREPLY=()
|
||||
_get_comp_words_by_ref "$@" cur prev words cword
|
||||
}
|
||||
|
||||
_cli_bash_autocomplete() {
|
||||
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
|
||||
local cur opts base words
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if declare -F _init_completion >/dev/null 2>&1; then
|
||||
_init_completion -n "=:" || return
|
||||
else
|
||||
_cli_init_completion -n "=:" || return
|
||||
fi
|
||||
words=("${words[@]:0:$cword}")
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
requestComp="${words[*]} ${cur} --generate-shell-completion"
|
||||
else
|
||||
requestComp="${words[*]} --generate-shell-completion"
|
||||
fi
|
||||
opts=$(eval "${requestComp}" 2>/dev/null)
|
||||
COMPREPLY=($(compgen -W "${opts}" -- ${cur}))
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
|
||||
unset PROG
|
102
go.mod
102
go.mod
@ -1,83 +1,83 @@
|
||||
module git.asdf.cafe/abs3nt/gospt
|
||||
module git.asdf.cafe/abs3nt/gspot
|
||||
|
||||
go 1.21
|
||||
go 1.22.3
|
||||
|
||||
require (
|
||||
git.asdf.cafe/abs3nt/gunner v0.0.1
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.26.6
|
||||
github.com/charmbracelet/lipgloss v0.12.1
|
||||
github.com/cristalhq/aconfig v0.18.5
|
||||
github.com/cristalhq/aconfig/aconfigyaml v0.17.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/charmbracelet/bubbles v0.20.0
|
||||
github.com/charmbracelet/bubbletea v1.1.2
|
||||
github.com/charmbracelet/lipgloss v0.13.1
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/lmittmann/tint v1.0.5
|
||||
github.com/rivo/tview v0.0.0-20241016194538-c5e4fb24af13
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1
|
||||
github.com/zmb3/spotify/v2 v2.4.2
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/sync v0.7.0
|
||||
google.golang.org/api v0.188.0
|
||||
modernc.org/sqlite v1.30.2
|
||||
tuxpa.in/a/zlog v1.61.0
|
||||
go.uber.org/fx v1.23.0
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/oauth2 v0.23.0
|
||||
golang.org/x/sync v0.8.0
|
||||
google.golang.org/api v0.203.0
|
||||
modernc.org/sqlite v1.33.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.7.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
|
||||
cloud.google.com/go/compute v1.25.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.4.0 // indirect
|
||||
cloud.google.com/go/auth v0.9.9 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.2 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.4 // indirect
|
||||
github.com/charmbracelet/x/input v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.1.0 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.4.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||
github.com/cristalhq/aconfig v0.18.6 // indirect
|
||||
github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.7.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/zerolog v1.31.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.52.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
||||
go.uber.org/dig v1.18.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/term v0.25.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
|
||||
modernc.org/libc v1.61.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
320
go.sum
320
go.sum
@ -13,29 +13,18 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38=
|
||||
cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4=
|
||||
cloud.google.com/go/auth v0.7.0 h1:kf/x9B3WTbBUHkC+1VS8wwwli9TzhSt0vSTVBmMR8Ts=
|
||||
cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
|
||||
cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ=
|
||||
cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute v1.23.1 h1:V97tBoDaZHb6leicZ1G6DLK2BAaZLJ/7+9BB/En3hR0=
|
||||
cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78=
|
||||
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
|
||||
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c=
|
||||
cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M=
|
||||
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
|
||||
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
@ -48,6 +37,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.asdf.cafe/abs3nt/gunner v0.0.1 h1:N6kCe7fH83kzm1Sjp/5uZbl8FM5s7KoYCfmhO8qyQbA=
|
||||
git.asdf.cafe/abs3nt/gunner v0.0.1/go.mod h1:Q4zhiPfmffCVAb5xIzZn6Momm91uf/deqRVd2/vdjd4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
@ -55,51 +46,28 @@ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||
github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s=
|
||||
github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk=
|
||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/charmbracelet/bubbletea v1.1.2 h1:naQXF2laRxyLyil/i7fxdpiz1/k06IKquhm4vBfHsIc=
|
||||
github.com/charmbracelet/bubbletea v1.1.2/go.mod h1:9HIU/hBV24qKjlehyj8z1r/tR9TYTQEag+cWZnuXo8E=
|
||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
|
||||
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
|
||||
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
|
||||
github.com/charmbracelet/lipgloss v0.11.1 h1:a8KgVPHa7kOoP95vm2tQQrjD2AKhbWmfr4uJ2RW6kNk=
|
||||
github.com/charmbracelet/lipgloss v0.11.1/go.mod h1:beLlcmkF7MWA+5UrKKIRo/VJ21xGXr7YJ9miWfdMRIU=
|
||||
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
|
||||
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
|
||||
github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk=
|
||||
github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
|
||||
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/ansi v0.1.3 h1:RBh/eleNWML5R524mjUF0yVRePTwqN9tPtV+DPgO5Lw=
|
||||
github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
|
||||
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
|
||||
github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
|
||||
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
|
||||
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
|
||||
github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4=
|
||||
github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
||||
github.com/charmbracelet/lipgloss v0.13.1 h1:Oik/oqDTMVA01GetT4JdEC033dNzWoQHdWnHnQmXE2A=
|
||||
github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U=
|
||||
github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU=
|
||||
github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cristalhq/aconfig v0.17.0/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
|
||||
github.com/cristalhq/aconfig v0.18.5 h1:QqXH/Gy2c4QUQJTV2BN8UAuL/rqZ3IwhvxeC8OgzquA=
|
||||
github.com/cristalhq/aconfig v0.18.5/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
|
||||
github.com/cristalhq/aconfig/aconfigyaml v0.17.1 h1:xCCbRKVmKrft9gQj3gHOq6U5PduasvlXEIsxtyzmFZ0=
|
||||
github.com/cristalhq/aconfig/aconfigyaml v0.17.1/go.mod h1:5DTsjHkvQ6hfbyxfG32roB1lF0U82rROtFaLxibL8V8=
|
||||
github.com/cristalhq/aconfig v0.18.6 h1:8KRBznzdjUUiaa7HeIpYbMx1uPE1/xOBEU1ajsnmNME=
|
||||
github.com/cristalhq/aconfig v0.18.6/go.mod h1:9ogrGEt9yU5V4pif/ThkVUfhj8JkdV+iDeahZGgfnDU=
|
||||
github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 h1:HG2ql5fGe4FLL2fUv6o+o0YRyF1mWEcYkNfWGWD82k4=
|
||||
github.com/cristalhq/aconfig/aconfigdotenv v0.17.1/go.mod h1:gQIKkh+HkVcODvMNz/cLbH65Pk9b0r4tfolCOsI8G9I=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
@ -113,15 +81,21 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -151,10 +125,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@ -166,6 +136,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@ -180,88 +151,71 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw=
|
||||
github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/tview v0.0.0-20241016194538-c5e4fb24af13 h1:SG5LUOAzLU9svb9HTLJI2WnLHQDEe86fXWJ4h2fQg0s=
|
||||
github.com/rivo/tview v0.0.0-20241016194538-c5e4fb24af13/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
|
||||
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@ -269,10 +223,11 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1 h1:1fJU+bltkwN8lF4Sni/X0i1d8XwPIrS82ivZ8qsp/q4=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@ -286,14 +241,24 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||
go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
|
||||
go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
|
||||
go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -301,12 +266,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -317,6 +279,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -339,8 +303,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -373,22 +337,17 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
|
||||
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -399,10 +358,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -431,53 +388,40 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@ -520,8 +464,8 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -542,12 +486,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.148.0 h1:HBq4TZlN4/1pNcu0geJZ/Q50vIwIXT532UIMYoo0vOs=
|
||||
google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU=
|
||||
google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo=
|
||||
google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk=
|
||||
google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
|
||||
google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
|
||||
google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU=
|
||||
google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@ -555,8 +495,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@ -586,16 +524,11 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a h1:fwgW9j3vHirt4ObdHoYNwuO24BEZjSzbh+zPaNWoiY8=
|
||||
google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:EMfReVxb80Dq1hhioy0sOsY9jCE46YDgHlJ7fWVUWRE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b h1:04+jVzTs2XBnOZcPsLnmrTGqltqJbZQ1Ey26hjYdQQ0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@ -609,12 +542,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@ -627,10 +556,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -646,18 +574,18 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
|
||||
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.17.10 h1:6wrtRozgrhCxieCeJh85QsxkX/2FFrT9hdaWPlbn4Zo=
|
||||
modernc.org/ccgo/v4 v4.17.10/go.mod h1:0NBHgsqTTpm9cA5z2ccErvGZmtntSM9qD2kFAs6pjXM=
|
||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
|
||||
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M=
|
||||
modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ=
|
||||
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 h1:IYXPPTTjjoSHvUClZIYexDiO7g+4x+XveKT4gCIAwiY=
|
||||
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
|
||||
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
@ -666,10 +594,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk=
|
||||
modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
|
||||
modernc.org/sqlite v1.30.2 h1:IPVVkhLu5mMVnS1dQgh3h0SAACRWcVk7aoLP9Us3UCk=
|
||||
modernc.org/sqlite v1.30.2/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
|
||||
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
|
||||
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
@ -677,5 +603,5 @@ modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
tuxpa.in/a/zlog v1.61.0 h1:7wrS6G4QwpnOmgHRQknrr7IgiMXrfGpekkU0PjM9FhE=
|
||||
tuxpa.in/a/zlog v1.61.0/go.mod h1:CNpMe8laDHLSypx/DyxfX1S0oyxUydeo3aGTEbtRBhg=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
66
main.go
66
main.go
@ -1,10 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.asdf.cafe/abs3nt/gospt/cmd"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxevent"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/cache"
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/cli"
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/logger"
|
||||
"git.asdf.cafe/abs3nt/gspot/src/services"
|
||||
)
|
||||
|
||||
func main() {
|
||||
defCmd := "tui"
|
||||
cmd.Execute(defCmd)
|
||||
var s fx.Shutdowner
|
||||
app := fx.New(
|
||||
fx.WithLogger(func(logger *slog.Logger) fxevent.Logger {
|
||||
l := &fxevent.SlogLogger{Logger: logger}
|
||||
l.UseLogLevel(slog.LevelDebug)
|
||||
return l
|
||||
}),
|
||||
fx.Populate(&s),
|
||||
services.Config,
|
||||
fx.Provide(
|
||||
Context,
|
||||
cache.NewCache,
|
||||
commands.NewCommander,
|
||||
logger.NewLogger,
|
||||
),
|
||||
fx.Invoke(
|
||||
cli.Run,
|
||||
),
|
||||
)
|
||||
app.Run()
|
||||
}
|
||||
|
||||
type AsyncInit func(func(ctx context.Context) error)
|
||||
|
||||
var ErrContextShutdown = errors.New("shutdown")
|
||||
|
||||
func Context(
|
||||
lc fx.Lifecycle,
|
||||
s fx.Shutdowner,
|
||||
log *slog.Logger,
|
||||
) (context.Context, AsyncInit) {
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
ctx, cn := context.WithCancelCause(context.Background())
|
||||
lc.Append(fx.Hook{
|
||||
OnStop: func(ctx context.Context) error {
|
||||
cn(fmt.Errorf("%w: %w", context.Canceled, ErrContextShutdown))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return ctx, func(fn func(ctx context.Context) error) {
|
||||
go func() {
|
||||
err := fn(ctx)
|
||||
if err != nil {
|
||||
log.Error("Failed to run async hook", "err", err)
|
||||
s.Shutdown()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": ["config:recommended"]
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -2,25 +2,44 @@ package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"tuxpa.in/a/zlog/log"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
Root string
|
||||
}
|
||||
|
||||
type CacheEntry struct {
|
||||
Expire time.Time `json:"e"`
|
||||
Value string `json:"v"`
|
||||
}
|
||||
|
||||
func DefaultCache() *Cache {
|
||||
return &Cache{
|
||||
Root: filepath.Join(os.TempDir(), "gospt.cache"),
|
||||
type CacheResult struct {
|
||||
fx.Out
|
||||
|
||||
Cache *Cache
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
Root string
|
||||
Log *slog.Logger
|
||||
}
|
||||
|
||||
type CacheParams struct {
|
||||
fx.In
|
||||
|
||||
Log *slog.Logger
|
||||
}
|
||||
|
||||
func NewCache(p CacheParams) CacheResult {
|
||||
c := &Cache{
|
||||
Root: filepath.Join(os.TempDir(), "gspot.cache"),
|
||||
Log: p.Log,
|
||||
}
|
||||
return CacheResult{
|
||||
Cache: c,
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +60,7 @@ func (c *Cache) save(m map[string]CacheEntry) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Trace().Str("tosave", string(payload)).Msg("saving cache")
|
||||
slog.Debug("CACHE", "saving", string(payload))
|
||||
err = os.WriteFile(c.Root, payload, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -52,7 +71,7 @@ func (c *Cache) save(m map[string]CacheEntry) error {
|
||||
func (c *Cache) GetOrDo(key string, do func() (string, error), ttl time.Duration) (string, error) {
|
||||
conf, err := c.load()
|
||||
if err != nil {
|
||||
log.Trace().Err(err).Msg("cache failed read")
|
||||
slog.Debug("CACHE", "failed read", err)
|
||||
return c.Do(key, do, ttl)
|
||||
}
|
||||
val, ok := conf[key]
|
||||
@ -85,10 +104,10 @@ func (c *Cache) Put(key string, value string, ttl time.Duration) (string, error)
|
||||
Expire: time.Now().Add(ttl),
|
||||
Value: value,
|
||||
}
|
||||
log.Trace().Str("key", key).Str("val", value).Msg("saving new cache key")
|
||||
slog.Debug("CACHE", "new item", fmt.Sprintf("%s: %s", key, value))
|
||||
err = c.save(conf)
|
||||
if err != nil {
|
||||
log.Trace().Err(err).Msg("cache failed save")
|
||||
slog.Debug("CACHE", "failed to save", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
451
src/components/cli/cli.go
Normal file
451
src/components/cli/cli.go
Normal file
@ -0,0 +1,451 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/zmb3/spotify/v2"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/tui"
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/tuitview"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
func Run(c *commands.Commander, s fx.Shutdowner) {
|
||||
app := &cli.Command{
|
||||
Name: "gspot",
|
||||
EnableShellCompletion: true,
|
||||
Version: Version,
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unknown command: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return tui.StartTea(c, "main")
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "play",
|
||||
Aliases: []string{"pl", "start", "s"},
|
||||
Usage: "Plays spotify",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Play()
|
||||
},
|
||||
Category: "Playback",
|
||||
},
|
||||
{
|
||||
Name: "playurl",
|
||||
Aliases: []string{"plu"},
|
||||
Usage: "Plays a spotify url",
|
||||
ArgsUsage: "url",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if !cmd.Args().Present() {
|
||||
return fmt.Errorf("no url provided")
|
||||
}
|
||||
if cmd.NArg() > 1 {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.PlayURL(cmd.Args().First())
|
||||
},
|
||||
Category: "Playback",
|
||||
},
|
||||
{
|
||||
Name: "pause",
|
||||
Aliases: []string{"pa"},
|
||||
Usage: "Pauses spotify",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Pause()
|
||||
},
|
||||
Category: "Playback",
|
||||
},
|
||||
{
|
||||
Name: "toggleplay",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Toggles play/pause",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.TogglePlay()
|
||||
},
|
||||
Category: "Playback",
|
||||
},
|
||||
{
|
||||
Name: "link",
|
||||
Aliases: []string{"yy"},
|
||||
Usage: "Prints the current song's spotify link",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.PrintLink()
|
||||
},
|
||||
Category: "Sharing",
|
||||
},
|
||||
{
|
||||
Name: "linkcontext",
|
||||
Aliases: []string{"lc"},
|
||||
Usage: "Prints the current album or playlist",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.PrintLinkContext()
|
||||
},
|
||||
Category: "Sharing",
|
||||
},
|
||||
{
|
||||
Name: "youtube-link",
|
||||
Aliases: []string{"yl"},
|
||||
Usage: "Prints the current song's youtube link",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.PrintYoutubeLink()
|
||||
},
|
||||
Category: "Sharing",
|
||||
},
|
||||
{
|
||||
Name: "next",
|
||||
Aliases: []string{"n", "skip"},
|
||||
Usage: "Skips to the next song",
|
||||
ArgsUsage: "amount",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() > 1 {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
if cmd.NArg() > 0 {
|
||||
amt, err := strconv.Atoi(cmd.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Next(amt, false)
|
||||
}
|
||||
return c.Next(1, false)
|
||||
},
|
||||
Category: "Playback",
|
||||
},
|
||||
{
|
||||
Name: "previous",
|
||||
Aliases: []string{"b", "prev", "back"},
|
||||
Usage: "Skips to the previous song",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Previous()
|
||||
},
|
||||
Category: "Playback",
|
||||
},
|
||||
{
|
||||
Name: "like",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "Likes the current song",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Like()
|
||||
},
|
||||
Category: "Library Management",
|
||||
},
|
||||
{
|
||||
Name: "unlike",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Unlikes the current song",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.UnLike()
|
||||
},
|
||||
Category: "Library Management",
|
||||
},
|
||||
{
|
||||
Name: "nowplaying",
|
||||
Aliases: []string{"now"},
|
||||
Usage: "Prints the current song",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
DefaultText: "false",
|
||||
Usage: "bypass cache",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.NowPlaying(cmd.Bool("force"))
|
||||
},
|
||||
Category: "Info",
|
||||
},
|
||||
{
|
||||
Name: "volume",
|
||||
Aliases: []string{"v"},
|
||||
Usage: "Control the volume",
|
||||
Category: "Playback",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "up",
|
||||
Usage: "Increase the volume",
|
||||
ArgsUsage: "percent",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() > 1 {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
amt, err := strconv.Atoi(cmd.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.ChangeVolume(amt)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "down",
|
||||
Aliases: []string{"dn"},
|
||||
Usage: "Decrease the volume",
|
||||
ArgsUsage: "percent",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() > 1 {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
amt, err := strconv.Atoi(cmd.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.ChangeVolume(-amt)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "mute",
|
||||
Aliases: []string{"m"},
|
||||
Usage: "Mute",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Mute()
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "unmute",
|
||||
Aliases: []string{"um"},
|
||||
Usage: "Unmute",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.UnMute()
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "togglemute",
|
||||
Aliases: []string{"tm"},
|
||||
Usage: "Toggle mute",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.ToggleMute()
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "download_cover",
|
||||
Usage: "Downloads the cover of the current song",
|
||||
Aliases: []string{"dl"},
|
||||
ArgsUsage: "path",
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
if cmd.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() > 1 {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.DownloadCover(cmd.Args().First())
|
||||
},
|
||||
Category: "Info",
|
||||
},
|
||||
{
|
||||
Name: "radio",
|
||||
Usage: "Starts a radio from the current song",
|
||||
Aliases: []string{"r"},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Radio()
|
||||
},
|
||||
Category: "Radio",
|
||||
},
|
||||
{
|
||||
Name: "clearradio",
|
||||
Usage: "Clears the radio queue",
|
||||
Aliases: []string{"cr"},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.ClearRadio()
|
||||
},
|
||||
Category: "Radio",
|
||||
},
|
||||
{
|
||||
Name: "refillradio",
|
||||
Usage: "Refills the radio queue with similar songs",
|
||||
Aliases: []string{"rr"},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.RefillRadio()
|
||||
},
|
||||
Category: "Radio",
|
||||
},
|
||||
{
|
||||
Name: "status",
|
||||
Usage: "Prints the current status",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Status()
|
||||
},
|
||||
Category: "Info",
|
||||
},
|
||||
{
|
||||
Name: "devices",
|
||||
Usage: "Lists available devices",
|
||||
Aliases: []string{"d"},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.ListDevices()
|
||||
},
|
||||
Category: "Info",
|
||||
},
|
||||
{
|
||||
Name: "setdevice",
|
||||
Usage: "Set the active device",
|
||||
ArgsUsage: "<device_id>",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() == 0 {
|
||||
return fmt.Errorf("no device id provided")
|
||||
}
|
||||
if cmd.NArg() > 1 {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.SetDevice(spotify.ID(cmd.Args().First()))
|
||||
},
|
||||
Category: "Playback",
|
||||
},
|
||||
{
|
||||
Name: "repeat",
|
||||
Usage: "Toggle repeat mode",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Repeat()
|
||||
},
|
||||
Category: "Playback",
|
||||
},
|
||||
{
|
||||
Name: "shuffle",
|
||||
Usage: "Toggle shuffle mode",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Shuffle()
|
||||
},
|
||||
Category: "Playback",
|
||||
},
|
||||
{
|
||||
Name: "tui",
|
||||
Usage: "Starts the TUI",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return tui.StartTea(c, "main")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tview",
|
||||
Usage: "Starts the TUI using tview (experimental)",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
// start tview tui
|
||||
return tuitview.TuitView(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "seek",
|
||||
Usage: "Seek to a position in the song",
|
||||
Aliases: []string{"sk"},
|
||||
Category: "Playback",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() > 1 {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
pos, err := strconv.Atoi(cmd.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetPosition(pos)
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "forward",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Seek forward",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Seek(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "backward",
|
||||
Aliases: []string{"b"},
|
||||
Usage: "Seek backward",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Present() {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
||||
}
|
||||
return c.Seek(false)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := app.Run(c.Context, os.Args); err != nil {
|
||||
c.Log.Error("COMMANDER", "run error", err)
|
||||
s.Shutdown(fx.ExitCode(1))
|
||||
}
|
||||
s.Shutdown()
|
||||
}
|
37
src/components/commands/activate_device.go
Normal file
37
src/components/commands/activate_device.go
Normal file
@ -0,0 +1,37 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
func (c *Commander) activateDevice() (spotify.ID, error) {
|
||||
var device *spotify.PlayerDevice
|
||||
configDir, _ := os.UserConfigDir()
|
||||
if _, err := os.Stat(filepath.Join(configDir, "gspot/device.json")); err == nil {
|
||||
deviceFile, err := os.Open(filepath.Join(configDir, "gspot/device.json"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer deviceFile.Close()
|
||||
deviceValue, err := io.ReadAll(deviceFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = json.Unmarshal(deviceValue, &device)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = c.Client().TransferPlayback(c.Context, device.ID, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
c.Log.Error("COMMANDER", "failed to activated device", "YOU MUST RUN gspot setdevice FIRST")
|
||||
}
|
||||
return device.ID, nil
|
||||
}
|
18
src/components/commands/album.go
Normal file
18
src/components/commands/album.go
Normal file
@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
func (c *Commander) AlbumTracks(album spotify.ID, page int) (*spotify.SimpleTrackPage, error) {
|
||||
tracks, err := c.Client().
|
||||
GetAlbumTracks(c.Context, album, spotify.Limit(50), spotify.Offset((page-1)*50), spotify.Market(spotify.CountryUSA))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
func (c *Commander) UserAlbums(page int) (*spotify.SavedAlbumPage, error) {
|
||||
return c.Client().CurrentUsersAlbums(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
|
||||
}
|
12
src/components/commands/artist.go
Normal file
12
src/components/commands/artist.go
Normal file
@ -0,0 +1,12 @@
|
||||
package commands
|
||||
|
||||
import "github.com/zmb3/spotify/v2"
|
||||
|
||||
func (c *Commander) ArtistAlbums(artist spotify.ID, page int) (*spotify.SimpleAlbumPage, error) {
|
||||
albums, err := c.Client().
|
||||
GetArtistAlbums(c.Context, artist, []spotify.AlbumType{1, 2, 3, 4}, spotify.Market(spotify.CountryUSA), spotify.Limit(50), spotify.Offset((page-1)*50))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return albums, nil
|
||||
}
|
75
src/components/commands/commander.go
Normal file
75
src/components/commands/commander.go
Normal file
@ -0,0 +1,75 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/zmb3/spotify/v2"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/cache"
|
||||
"git.asdf.cafe/abs3nt/gspot/src/config"
|
||||
"git.asdf.cafe/abs3nt/gspot/src/services"
|
||||
)
|
||||
|
||||
type CommanderResult struct {
|
||||
fx.Out
|
||||
|
||||
Commander *Commander
|
||||
}
|
||||
|
||||
type CommanderParams struct {
|
||||
fx.In
|
||||
|
||||
Context context.Context
|
||||
Log *slog.Logger
|
||||
Cache *cache.Cache
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
type Commander struct {
|
||||
Context context.Context
|
||||
User *spotify.PrivateUser
|
||||
Log *slog.Logger
|
||||
Cache *cache.Cache
|
||||
mu sync.RWMutex
|
||||
cl *spotify.Client
|
||||
conf *config.Config
|
||||
}
|
||||
|
||||
func NewCommander(p CommanderParams) CommanderResult {
|
||||
c := &Commander{
|
||||
Context: p.Context,
|
||||
Log: p.Log,
|
||||
Cache: p.Cache,
|
||||
conf: p.Config,
|
||||
}
|
||||
return CommanderResult{
|
||||
Commander: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commander) Client() *spotify.Client {
|
||||
c.mu.Lock()
|
||||
if c.cl == nil {
|
||||
c.cl = c.connectClient()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.cl
|
||||
}
|
||||
|
||||
func (c *Commander) connectClient() *spotify.Client {
|
||||
client, err := services.GetClient(c.conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
currentUser, err := client.CurrentUser(c.Context)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.User = currentUser
|
||||
return client
|
||||
}
|
53
src/components/commands/devices.go
Normal file
53
src/components/commands/devices.go
Normal file
@ -0,0 +1,53 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
func (c *Commander) ListDevices() error {
|
||||
devices, err := c.Client().PlayerDevices(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return PrintDevices(devices)
|
||||
}
|
||||
|
||||
func PrintDevices(devices []spotify.PlayerDevice) error {
|
||||
out, err := json.MarshalIndent(devices, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(out))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) SetDevice(device spotify.ID) error {
|
||||
err := c.Client().TransferPlayback(c.Context, device, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
devices, err := c.Client().PlayerDevices(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range devices {
|
||||
if d.ID == device {
|
||||
out, err := json.MarshalIndent(d, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configDir, _ := os.UserConfigDir()
|
||||
err = os.WriteFile(filepath.Join(configDir, "gspot/device.json"), out, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("device not found")
|
||||
}
|
26
src/components/commands/downloadCover.go
Normal file
26
src/components/commands/downloadCover.go
Normal file
@ -0,0 +1,26 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (c *Commander) DownloadCover(path string) error {
|
||||
if path == "" {
|
||||
path = "cover.png"
|
||||
}
|
||||
destinationPath := filepath.Clean(path)
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(destinationPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = state.Item.Album.Images[0].Download(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
7
src/components/commands/errors.go
Normal file
7
src/components/commands/errors.go
Normal file
@ -0,0 +1,7 @@
|
||||
package commands
|
||||
|
||||
import "strings"
|
||||
|
||||
func isNoActiveError(err error) bool {
|
||||
return strings.Contains(err.Error(), "No active device found")
|
||||
}
|
9
src/components/commands/like.go
Normal file
9
src/components/commands/like.go
Normal file
@ -0,0 +1,9 @@
|
||||
package commands
|
||||
|
||||
func (c *Commander) Like() error {
|
||||
playing, err := c.Client().PlayerCurrentlyPlaying(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Client().AddTracksToLibrary(c.Context, playing.Item.ID)
|
||||
}
|
21
src/components/commands/link.go
Normal file
21
src/components/commands/link.go
Normal file
@ -0,0 +1,21 @@
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (c *Commander) PrintLink() error {
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(state.Item.ExternalURLs["spotify"])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) PrintLinkContext() error {
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(state.PlaybackContext.ExternalURLs["spotify"])
|
||||
return nil
|
||||
}
|
117
src/components/commands/next.go
Normal file
117
src/components/commands/next.go
Normal file
@ -0,0 +1,117 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
func (c *Commander) Next(amt int, inqueue bool) error {
|
||||
if inqueue {
|
||||
for i := 0; i < amt; i++ {
|
||||
err := c.Client().Next(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if amt == 1 {
|
||||
err := c.Client().Next(c.Context)
|
||||
if err != nil {
|
||||
if isNoActiveError(err) {
|
||||
deviceID, err := c.activateDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().NextOpt(c.Context, &spotify.PlayOptions{
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// found := false
|
||||
// playingIndex := 0
|
||||
current, err := c.Client().PlayerCurrentlyPlaying(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
playbackContext := current.PlaybackContext.Type
|
||||
switch playbackContext {
|
||||
case "playlist":
|
||||
found := false
|
||||
currentTrackIndex := 0
|
||||
page := 1
|
||||
for !found {
|
||||
playlist, err := c.Client().
|
||||
GetPlaylistItems(
|
||||
c.Context,
|
||||
spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]),
|
||||
spotify.Limit(50),
|
||||
spotify.Offset((page-1)*50),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for idx, track := range playlist.Items {
|
||||
if track.Track.Track.ID == current.Item.ID {
|
||||
currentTrackIndex = idx + (50 * (page - 1))
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
page++
|
||||
}
|
||||
pos := currentTrackIndex + amt
|
||||
return c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackContext: ¤t.PlaybackContext.URI,
|
||||
PlaybackOffset: &spotify.PlaybackOffset{
|
||||
Position: &pos,
|
||||
},
|
||||
})
|
||||
case "album":
|
||||
found := false
|
||||
currentTrackIndex := 0
|
||||
page := 1
|
||||
for !found {
|
||||
playlist, err := c.Client().
|
||||
GetAlbumTracks(
|
||||
c.Context,
|
||||
spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]),
|
||||
spotify.Limit(50),
|
||||
spotify.Offset((page-1)*50),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for idx, track := range playlist.Tracks {
|
||||
if track.ID == current.Item.ID {
|
||||
currentTrackIndex = idx + (50 * (page - 1))
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
page++
|
||||
}
|
||||
pos := currentTrackIndex + amt
|
||||
return c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackContext: ¤t.PlaybackContext.URI,
|
||||
PlaybackOffset: &spotify.PlaybackOffset{
|
||||
Position: &pos,
|
||||
},
|
||||
})
|
||||
default:
|
||||
for i := 0; i < amt; i++ {
|
||||
err := c.Client().Next(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
50
src/components/commands/nowPlaying.go
Normal file
50
src/components/commands/nowPlaying.go
Normal file
@ -0,0 +1,50 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
func (c *Commander) NowPlaying(force bool) error {
|
||||
if force {
|
||||
current, err := c.Client().PlayerCurrentlyPlaying(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
str := FormatSong(current)
|
||||
fmt.Println(str)
|
||||
_, err = c.Cache.Put("now_playing", str, 5*time.Second)
|
||||
return err
|
||||
}
|
||||
song, err := c.Cache.GetOrDo("now_playing", func() (string, error) {
|
||||
current, err := c.Client().PlayerCurrentlyPlaying(c.Context)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
str := FormatSong(current)
|
||||
return str, nil
|
||||
}, 5*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(song)
|
||||
return nil
|
||||
}
|
||||
|
||||
func FormatSong(current *spotify.CurrentlyPlaying) string {
|
||||
out := "▶"
|
||||
if !current.Playing || current == nil {
|
||||
out = "⏸"
|
||||
}
|
||||
if current != nil {
|
||||
if current.Item != nil {
|
||||
out += fmt.Sprintf(" %s", current.Item.Name)
|
||||
if len(current.Item.Artists) > 0 {
|
||||
out += fmt.Sprintf(" - %s", current.Item.Artists[0].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
5
src/components/commands/pause.go
Normal file
5
src/components/commands/pause.go
Normal file
@ -0,0 +1,5 @@
|
||||
package commands
|
||||
|
||||
func (c *Commander) Pause() error {
|
||||
return c.Client().Pause(c.Context)
|
||||
}
|
177
src/components/commands/play.go
Normal file
177
src/components/commands/play.go
Normal file
@ -0,0 +1,177 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
func (c *Commander) Play() error {
|
||||
err := c.Client().Play(c.Context)
|
||||
if err != nil {
|
||||
if isNoActiveError(err) {
|
||||
deviceID, err := c.activateDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) PlayLikedSongs(position int) error {
|
||||
c.Log.Debug("Playing liked songs")
|
||||
err := c.ClearRadio()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
playlist, _, err := c.GetRadioPlaylist("Saved Songs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Log.Debug("got playlist", "id", playlist.ID)
|
||||
songs, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(position))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toAdd := []spotify.ID{}
|
||||
for _, song := range songs.Tracks {
|
||||
toAdd = append(toAdd, song.ID)
|
||||
}
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, playlist.ID, toAdd...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Log.Debug("added songs to playlist")
|
||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackContext: &playlist.URI,
|
||||
})
|
||||
if err != nil {
|
||||
if isNoActiveError(err) {
|
||||
c.Log.Debug("need to activate device")
|
||||
deviceID, err := c.activateDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackContext: &playlist.URI,
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Log.Debug("starting loop")
|
||||
for page := 2; page <= 5; page++ {
|
||||
c.Log.Debug("doing loop", "page", page)
|
||||
songs, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset((50*(page-1))+position))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toAdd := []spotify.ID{}
|
||||
for _, song := range songs.Tracks {
|
||||
toAdd = append(toAdd, song.ID)
|
||||
}
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, playlist.ID, toAdd...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.Log.Debug("done")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commander) PlayURL(urlString string) error {
|
||||
url, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
splittUrl := strings.Split(url.Path, "/")
|
||||
if len(splittUrl) < 3 {
|
||||
return fmt.Errorf("invalid url")
|
||||
}
|
||||
trackID := splittUrl[2]
|
||||
err = c.Client().QueueSong(c.Context, spotify.ID(trackID))
|
||||
if err != nil {
|
||||
if isNoActiveError(err) {
|
||||
deviceID, err := c.activateDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().QueueSongOpt(c.Context, spotify.ID(trackID), &spotify.PlayOptions{
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().NextOpt(c.Context, &spotify.PlayOptions{
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = c.Client().Next(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) PlaySongInPlaylist(context *spotify.URI, offset *int) error {
|
||||
e := c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
|
||||
PlaybackContext: context,
|
||||
})
|
||||
if e != nil {
|
||||
if isNoActiveError(e) {
|
||||
deviceID, err := c.activateDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
|
||||
PlaybackContext: context,
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
if isNoActiveError(err) {
|
||||
deviceID, err := c.activateDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
|
||||
PlaybackContext: context,
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
err = c.Client().Play(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
23
src/components/commands/playlist.go
Normal file
23
src/components/commands/playlist.go
Normal file
@ -0,0 +1,23 @@
|
||||
package commands
|
||||
|
||||
import "github.com/zmb3/spotify/v2"
|
||||
|
||||
func (c *Commander) Playlists(page int) (*spotify.SimplePlaylistPage, error) {
|
||||
return c.Client().CurrentUsersPlaylists(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
|
||||
}
|
||||
|
||||
func (c *Commander) PlaylistTracks(playlist spotify.ID, page int) (*spotify.PlaylistItemPage, error) {
|
||||
return c.Client().GetPlaylistItems(c.Context, playlist, spotify.Limit(50), spotify.Offset((page-1)*50))
|
||||
}
|
||||
|
||||
func (c *Commander) DeleteTracksFromPlaylist(tracks []spotify.ID, playlist spotify.ID) error {
|
||||
_, err := c.Client().RemoveTracksFromPlaylist(c.Context, playlist, tracks...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) TrackList(page int) (*spotify.SavedTrackPage, error) {
|
||||
return c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
|
||||
}
|
25
src/components/commands/previous.go
Normal file
25
src/components/commands/previous.go
Normal file
@ -0,0 +1,25 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
func (c *Commander) Previous() error {
|
||||
err := c.Client().Previous(c.Context)
|
||||
if err != nil {
|
||||
if isNoActiveError(err) {
|
||||
deviceID, err := c.activateDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().PreviousOpt(c.Context, &spotify.PlayOptions{
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
25
src/components/commands/queue.go
Normal file
25
src/components/commands/queue.go
Normal file
@ -0,0 +1,25 @@
|
||||
package commands
|
||||
|
||||
import "github.com/zmb3/spotify/v2"
|
||||
|
||||
func (c *Commander) QueueSong(id spotify.ID) error {
|
||||
err := c.Client().QueueSong(c.Context, id)
|
||||
if err != nil {
|
||||
if isNoActiveError(err) {
|
||||
deviceID, err := c.activateDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().QueueSongOpt(c.Context, id, &spotify.PlayOptions{
|
||||
DeviceID: &deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
625
src/components/commands/radio.go
Normal file
625
src/components/commands/radio.go
Normal file
@ -0,0 +1,625 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/zmb3/spotify/v2"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func (c *Commander) Radio() error {
|
||||
currentSong, err := c.Client().PlayerCurrentlyPlaying(c.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current song: %w", err)
|
||||
}
|
||||
if currentSong.Item != nil {
|
||||
return c.RadioGivenSong(currentSong.Item.SimpleTrack, currentSong.Progress)
|
||||
}
|
||||
_, err = c.activateDevice()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to activate device: %w", err)
|
||||
}
|
||||
tracks, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(10))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current users tracks: %w", err)
|
||||
}
|
||||
return c.RadioGivenSong(tracks.Tracks[rand.Intn(len(tracks.Tracks))].SimpleTrack, 0)
|
||||
}
|
||||
|
||||
func (c *Commander) RadioFromPlaylist(playlist spotify.SimplePlaylist) error {
|
||||
playlistPage, err := c.Client().GetPlaylistItems(
|
||||
c.Context,
|
||||
playlist.ID,
|
||||
spotify.Limit(50),
|
||||
spotify.Offset(0),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pageSongs := playlistPage.Items
|
||||
rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
|
||||
seedCount := 5
|
||||
if len(pageSongs) < seedCount {
|
||||
seedCount = len(pageSongs)
|
||||
}
|
||||
seedIds := []spotify.ID{}
|
||||
for idx, song := range pageSongs {
|
||||
if idx >= seedCount {
|
||||
break
|
||||
}
|
||||
seedIds = append(seedIds, song.Track.Track.ID)
|
||||
}
|
||||
return c.RadioGivenList(seedIds[:seedCount], playlist.Name)
|
||||
}
|
||||
|
||||
func (c *Commander) RadioFromSavedTracks() error {
|
||||
savedSongs, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if savedSongs.Total == 0 {
|
||||
return fmt.Errorf("you have no saved songs")
|
||||
}
|
||||
pages := int(math.Ceil(float64(savedSongs.Total) / 50))
|
||||
randomPage := 1
|
||||
if pages > 1 {
|
||||
randomPage = rand.Intn(pages-1) + 1
|
||||
}
|
||||
trackPage, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(randomPage*50))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pageSongs := trackPage.Tracks
|
||||
rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
|
||||
seedCount := 4
|
||||
seedIds := []spotify.ID{}
|
||||
for idx, song := range pageSongs {
|
||||
if idx >= seedCount {
|
||||
break
|
||||
}
|
||||
seedIds = append(seedIds, song.ID)
|
||||
}
|
||||
seedIds = append(seedIds, savedSongs.Tracks[0].ID)
|
||||
return c.RadioGivenList(seedIds, "Saved Tracks")
|
||||
}
|
||||
|
||||
func (c *Commander) RadioGivenArtist(artist spotify.SimpleArtist) error {
|
||||
seed := spotify.Seeds{
|
||||
Artists: []spotify.ID{artist.ID},
|
||||
}
|
||||
recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recomendationIds := []spotify.ID{}
|
||||
for _, song := range recomendations.Tracks {
|
||||
recomendationIds = append(recomendationIds, song.ID)
|
||||
}
|
||||
err = c.ClearRadio()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
radioPlaylist, db, err := c.GetRadioPlaylist(artist.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queue := []spotify.ID{}
|
||||
for _, rec := range recomendationIds {
|
||||
exists, err := c.SongExists(db, rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
_, err := db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(rec)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queue = append(queue, rec)
|
||||
}
|
||||
}
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackContext: &radioPlaylist.URI,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().Repeat(c.Context, "context")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < 4; i++ {
|
||||
id := rand.Intn(len(recomendationIds)-2) + 1
|
||||
seed := spotify.Seeds{
|
||||
Tracks: []spotify.ID{recomendationIds[id]},
|
||||
}
|
||||
additionalRecs, err := c.Client().
|
||||
GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
additionalRecsIds := []spotify.ID{}
|
||||
for _, song := range additionalRecs.Tracks {
|
||||
exists, err := c.SongExists(db, song.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
_, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
additionalRecsIds = append(additionalRecsIds, song.ID)
|
||||
}
|
||||
}
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos spotify.Numeric) error {
|
||||
start := time.Now().UnixMilli()
|
||||
seed := spotify.Seeds{
|
||||
Tracks: []spotify.ID{song.ID},
|
||||
}
|
||||
recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get recommendations: %w", err)
|
||||
}
|
||||
recomendationIds := []spotify.ID{}
|
||||
for _, song := range recomendations.Tracks {
|
||||
recomendationIds = append(recomendationIds, song.ID)
|
||||
}
|
||||
err = c.ClearRadio()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
radioPlaylist, db, err := c.GetRadioPlaylist(song.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queue := []spotify.ID{song.ID}
|
||||
for _, rec := range recomendationIds {
|
||||
exists, err := c.SongExists(db, rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
_, err := db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(rec)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queue = append(queue, rec)
|
||||
}
|
||||
}
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delay := time.Now().UnixMilli() - start
|
||||
if pos != 0 {
|
||||
pos = pos + spotify.Numeric(delay)
|
||||
}
|
||||
err = c.PlayRadio(radioPlaylist, int(pos))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().Repeat(c.Context, "context")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < 4; i++ {
|
||||
id := rand.Intn(len(recomendationIds)-2) + 1
|
||||
seed := spotify.Seeds{
|
||||
Tracks: []spotify.ID{recomendationIds[id]},
|
||||
}
|
||||
additionalRecs, err := c.Client().
|
||||
GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
additionalRecsIds := []spotify.ID{}
|
||||
for _, song := range additionalRecs.Tracks {
|
||||
exists, err := c.SongExists(db, song.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
_, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
additionalRecsIds = append(additionalRecsIds, song.ID)
|
||||
}
|
||||
}
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) PlayRadio(radioPlaylist *spotify.FullPlaylist, pos int) error {
|
||||
err := c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackContext: &radioPlaylist.URI,
|
||||
PositionMs: spotify.Numeric(pos),
|
||||
})
|
||||
if err != nil {
|
||||
if isNoActiveError(err) {
|
||||
deviceID, err := c.activateDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||
PlaybackContext: &radioPlaylist.URI,
|
||||
DeviceID: &deviceID,
|
||||
PositionMs: spotify.Numeric(pos),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) ClearRadio() error {
|
||||
radioPlaylist, db, err := c.GetRadioPlaylist("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().UnfollowPlaylist(c.Context, radioPlaylist.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = db.Query("DROP TABLE IF EXISTS radio")
|
||||
configDir, _ := os.UserConfigDir()
|
||||
os.Remove(filepath.Join(configDir, "gspot/radio.json"))
|
||||
_ = c.Client().Pause(c.Context)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) GetRadioPlaylist(name string) (*spotify.FullPlaylist, *sql.DB, error) {
|
||||
configDir, _ := os.UserConfigDir()
|
||||
playlistFile, err := os.ReadFile(filepath.Join(configDir, "gspot/radio.json"))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return c.CreateRadioPlaylist(name)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var playlist *spotify.FullPlaylist
|
||||
err = json.Unmarshal(playlistFile, &playlist)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
db, err := sql.Open("sqlite", filepath.Join(configDir, "gspot/radio.db"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return playlist, db, nil
|
||||
}
|
||||
|
||||
func (c *Commander) CreateRadioPlaylist(name string) (*spotify.FullPlaylist, *sql.DB, error) {
|
||||
// private flag doesnt work
|
||||
configDir, _ := os.UserConfigDir()
|
||||
playlist, err := c.Client().
|
||||
CreatePlaylistForUser(c.Context, c.User.ID, name+" - autoradio", "Automanaged radio playlist", false, false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
raw, err := json.MarshalIndent(playlist, "", " ")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(configDir, "gspot/radio.json"), raw, 0o600)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
db, err := sql.Open("sqlite", filepath.Join(configDir, "gspot/radio.db"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_, _ = db.QueryContext(c.Context, "DROP TABLE IF EXISTS radio")
|
||||
_, _ = db.QueryContext(c.Context, "CREATE TABLE IF NOT EXISTS radio (id string PRIMARY KEY)")
|
||||
return playlist, db, nil
|
||||
}
|
||||
|
||||
func (c *Commander) SongExists(db *sql.DB, song spotify.ID) (bool, error) {
|
||||
songID := string(song)
|
||||
sqlStmt := `SELECT id FROM radio WHERE id = ?`
|
||||
err := db.QueryRow(sqlStmt, songID).Scan(&songID)
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Commander) RefillRadio() error {
|
||||
status, err := c.Client().PlayerCurrentlyPlaying(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paused := false
|
||||
if !status.Playing {
|
||||
paused = true
|
||||
}
|
||||
toRemove := []spotify.ID{}
|
||||
radioPlaylist, db, err := c.GetRadioPlaylist("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
playlistItems, err := c.Client().GetPlaylistItems(c.Context, radioPlaylist.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("orig playlist items: %w", err)
|
||||
}
|
||||
|
||||
if status.PlaybackContext.URI != radioPlaylist.URI || paused {
|
||||
return c.RadioFromPlaylist(radioPlaylist.SimplePlaylist)
|
||||
}
|
||||
|
||||
page := 0
|
||||
for {
|
||||
tracks, err := c.Client().GetPlaylistItems(c.Context, radioPlaylist.ID, spotify.Limit(50), spotify.Offset(page*50))
|
||||
if err != nil {
|
||||
return fmt.Errorf("tracks: %w", err)
|
||||
}
|
||||
if len(tracks.Items) == 0 {
|
||||
break
|
||||
}
|
||||
for _, track := range tracks.Items {
|
||||
if track.Track.Track.ID == status.Item.ID {
|
||||
break
|
||||
}
|
||||
toRemove = append(toRemove, track.Track.Track.ID)
|
||||
}
|
||||
page++
|
||||
}
|
||||
if len(toRemove) > 0 {
|
||||
var trackGroups []spotify.ID
|
||||
for idx, item := range toRemove {
|
||||
if idx%100 == 0 && idx != 0 {
|
||||
_, err = c.Client().RemoveTracksFromPlaylist(c.Context, radioPlaylist.ID, trackGroups...)
|
||||
trackGroups = []spotify.ID{}
|
||||
}
|
||||
trackGroups = append(trackGroups, item)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error clearing playlist: %w", err)
|
||||
}
|
||||
}
|
||||
_, err := c.Client().RemoveTracksFromPlaylist(c.Context, radioPlaylist.ID, trackGroups...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
toAdd := 500 - (int(playlistItems.Total) - len(toRemove))
|
||||
playlistItems, err = c.Client().GetPlaylistItems(c.Context, radioPlaylist.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("playlist items: %w", err)
|
||||
}
|
||||
total := playlistItems.Total
|
||||
pages := int(math.Ceil(float64(total) / 50))
|
||||
randomPage := 1
|
||||
if pages > 1 {
|
||||
randomPage = rand.Intn(pages-1) + 1
|
||||
}
|
||||
playlistPage, err := c.Client().
|
||||
GetPlaylistItems(c.Context, radioPlaylist.ID, spotify.Limit(50), spotify.Offset((randomPage-1)*50))
|
||||
if err != nil {
|
||||
return fmt.Errorf("playlist page: %w", err)
|
||||
}
|
||||
pageSongs := playlistPage.Items
|
||||
rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
|
||||
seedCount := 5
|
||||
if len(pageSongs) < seedCount {
|
||||
seedCount = len(pageSongs)
|
||||
}
|
||||
seedIds := []spotify.ID{}
|
||||
for idx, song := range pageSongs {
|
||||
if idx >= seedCount {
|
||||
break
|
||||
}
|
||||
seedIds = append(seedIds, song.Track.Track.ID)
|
||||
}
|
||||
seed := spotify.Seeds{
|
||||
Tracks: seedIds,
|
||||
}
|
||||
recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(95))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recomendationIds := []spotify.ID{}
|
||||
for _, song := range recomendations.Tracks {
|
||||
exists, err := c.SongExists(db, song.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("err check song existnce: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
recomendationIds = append(recomendationIds, song.ID)
|
||||
}
|
||||
}
|
||||
queue := []spotify.ID{}
|
||||
for idx, rec := range recomendationIds {
|
||||
if idx > toAdd {
|
||||
break
|
||||
}
|
||||
_, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", rec.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queue = append(queue, rec)
|
||||
}
|
||||
toAdd -= len(queue)
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add tracks: %w", err)
|
||||
}
|
||||
err = c.Client().Repeat(c.Context, "context")
|
||||
if err != nil {
|
||||
return fmt.Errorf("repeat: %w", err)
|
||||
}
|
||||
for toAdd > 0 {
|
||||
id := rand.Intn(len(recomendationIds)-2) + 1
|
||||
seed := spotify.Seeds{
|
||||
Tracks: []spotify.ID{recomendationIds[id]},
|
||||
}
|
||||
additionalRecs, err := c.Client().
|
||||
GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
|
||||
if err != nil {
|
||||
return fmt.Errorf("get recs: %w", err)
|
||||
}
|
||||
additionalRecsIds := []spotify.ID{}
|
||||
for idx, song := range additionalRecs.Tracks {
|
||||
exists, err := c.SongExists(db, song.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check song existence: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
if idx > toAdd {
|
||||
break
|
||||
}
|
||||
additionalRecsIds = append(additionalRecsIds, song.ID)
|
||||
queue = append(queue, song.ID)
|
||||
}
|
||||
}
|
||||
toAdd -= len(queue)
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add tracks to playlist: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) RadioFromAlbum(album spotify.SimpleAlbum) error {
|
||||
tracks, err := c.AlbumTracks(album.ID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
total := tracks.Total
|
||||
if total == 0 {
|
||||
return fmt.Errorf("this playlist is empty")
|
||||
}
|
||||
pages := int(math.Ceil(float64(total) / 50))
|
||||
randomPage := 1
|
||||
if pages > 1 {
|
||||
randomPage = rand.Intn(pages-1) + 1
|
||||
}
|
||||
albumTrackPage, err := c.AlbumTracks(album.ID, randomPage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pageSongs := albumTrackPage.Tracks
|
||||
rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
|
||||
seedCount := 5
|
||||
if len(pageSongs) < seedCount {
|
||||
seedCount = len(pageSongs)
|
||||
}
|
||||
seedIds := []spotify.ID{}
|
||||
for idx, song := range pageSongs {
|
||||
if idx >= seedCount {
|
||||
break
|
||||
}
|
||||
seedIds = append(seedIds, song.ID)
|
||||
}
|
||||
return c.RadioGivenList(seedIds[:seedCount], album.Name)
|
||||
}
|
||||
|
||||
func (c *Commander) RadioGivenList(songs []spotify.ID, name string) error {
|
||||
seed := spotify.Seeds{
|
||||
Tracks: songs,
|
||||
}
|
||||
recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recomendationIds := []spotify.ID{}
|
||||
for _, song := range recomendations.Tracks {
|
||||
recomendationIds = append(recomendationIds, song.ID)
|
||||
}
|
||||
err = c.ClearRadio()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
radioPlaylist, db, err := c.GetRadioPlaylist(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queue := []spotify.ID{songs[0]}
|
||||
for _, rec := range recomendationIds {
|
||||
exists, err := c.SongExists(db, rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
_, err := db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(rec)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queue = append(queue, rec)
|
||||
}
|
||||
}
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.PlayRadio(radioPlaylist, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < 4; i++ {
|
||||
id := rand.Intn(len(recomendationIds)-2) + 1
|
||||
seed := spotify.Seeds{
|
||||
Tracks: []spotify.ID{recomendationIds[id]},
|
||||
}
|
||||
additionalRecs, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
additionalRecsIds := []spotify.ID{}
|
||||
for _, song := range additionalRecs.Tracks {
|
||||
exists, err := c.SongExists(db, song.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
_, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
additionalRecsIds = append(additionalRecsIds, song.ID)
|
||||
}
|
||||
}
|
||||
_, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
19
src/components/commands/repeat.go
Normal file
19
src/components/commands/repeat.go
Normal file
@ -0,0 +1,19 @@
|
||||
package commands
|
||||
|
||||
func (c *Commander) Repeat() error {
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newState := "off"
|
||||
if state.RepeatState == "off" {
|
||||
newState = "context"
|
||||
}
|
||||
// spotifyd only supports binary value for repeat, context or off, change when/if spotifyd is better
|
||||
err = c.Client().Repeat(c.Context, newState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Log.Info("COMMANDER", "Repeat set to", newState)
|
||||
return nil
|
||||
}
|
12
src/components/commands/search.go
Normal file
12
src/components/commands/search.go
Normal file
@ -0,0 +1,12 @@
|
||||
package commands
|
||||
|
||||
import "github.com/zmb3/spotify/v2"
|
||||
|
||||
func (c *Commander) Search(search string, page int) (*spotify.SearchResult, error) {
|
||||
result, err := c.Client().
|
||||
Search(c.Context, search, spotify.SearchTypeAlbum|spotify.SearchTypeArtist|spotify.SearchTypeTrack|spotify.SearchTypePlaylist, spotify.Limit(50), spotify.Offset((page-1)*50))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
25
src/components/commands/seek.go
Normal file
25
src/components/commands/seek.go
Normal file
@ -0,0 +1,25 @@
|
||||
package commands
|
||||
|
||||
func (c *Commander) Seek(fwd bool) error {
|
||||
current, err := c.Client().PlayerCurrentlyPlaying(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newPos := current.Progress + 5000
|
||||
if !fwd {
|
||||
newPos = current.Progress - 5000
|
||||
}
|
||||
err = c.Client().Seek(c.Context, int(newPos))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) SetPosition(pos int) error {
|
||||
err := c.Client().Seek(c.Context, pos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
14
src/components/commands/shuffle.go
Normal file
14
src/components/commands/shuffle.go
Normal file
@ -0,0 +1,14 @@
|
||||
package commands
|
||||
|
||||
func (c *Commander) Shuffle() error {
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client().Shuffle(c.Context, !state.ShuffleState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Log.Info("COMMANDER", "shuffle state", !state.ShuffleState)
|
||||
return nil
|
||||
}
|
38
src/components/commands/status.go
Normal file
38
src/components/commands/status.go
Normal file
@ -0,0 +1,38 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
func (c *Commander) Status() error {
|
||||
state, err := c.Cache.GetOrDo("state", func() (string, error) {
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
str, err := c.FormatState(state)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return str, nil
|
||||
}, 5*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(state)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commander) FormatState(state *spotify.PlayerState) (string, error) {
|
||||
state.Item.AvailableMarkets = []string{}
|
||||
state.Item.Album.AvailableMarkets = []string{}
|
||||
out, err := json.MarshalIndent(state, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return (string(out)), nil
|
||||
}
|
12
src/components/commands/toggle_play.go
Normal file
12
src/components/commands/toggle_play.go
Normal file
@ -0,0 +1,12 @@
|
||||
package commands
|
||||
|
||||
func (c *Commander) TogglePlay() error {
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return c.Play()
|
||||
}
|
||||
if state.Playing {
|
||||
return c.Pause()
|
||||
}
|
||||
return c.Play()
|
||||
}
|
9
src/components/commands/unlike.go
Normal file
9
src/components/commands/unlike.go
Normal file
@ -0,0 +1,9 @@
|
||||
package commands
|
||||
|
||||
func (c *Commander) UnLike() error {
|
||||
playing, err := c.Client().PlayerCurrentlyPlaying(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Client().RemoveTracksFromLibrary(c.Context, playing.Item.ID)
|
||||
}
|
35
src/components/commands/volume.go
Normal file
35
src/components/commands/volume.go
Normal file
@ -0,0 +1,35 @@
|
||||
package commands
|
||||
|
||||
func (c *Commander) ChangeVolume(amount int) error {
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newVolume := int(state.Device.Volume) + amount
|
||||
if newVolume > 100 {
|
||||
newVolume = 100
|
||||
}
|
||||
if newVolume < 0 {
|
||||
newVolume = 0
|
||||
}
|
||||
return c.Client().Volume(c.Context, newVolume)
|
||||
}
|
||||
|
||||
func (c *Commander) Mute() error {
|
||||
return c.ChangeVolume(-100)
|
||||
}
|
||||
|
||||
func (c *Commander) UnMute() error {
|
||||
return c.ChangeVolume(100)
|
||||
}
|
||||
|
||||
func (c *Commander) ToggleMute() error {
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state.Device.Volume == 0 {
|
||||
return c.ChangeVolume(100)
|
||||
}
|
||||
return c.ChangeVolume(-100)
|
||||
}
|
17
src/components/commands/youtube-link.go
Normal file
17
src/components/commands/youtube-link.go
Normal file
@ -0,0 +1,17 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/youtube"
|
||||
)
|
||||
|
||||
func (c *Commander) PrintYoutubeLink() error {
|
||||
state, err := c.Client().PlayerState(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link := youtube.Search(state.Item.Artists[0].Name + state.Item.Name)
|
||||
fmt.Println(link)
|
||||
return nil
|
||||
}
|
67
src/components/logger/logger.go
Normal file
67
src/components/logger/logger.go
Normal file
@ -0,0 +1,67 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/lmittmann/tint"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/config"
|
||||
)
|
||||
|
||||
type LoggerResult struct {
|
||||
fx.Out
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
type LoggerParams struct {
|
||||
fx.In
|
||||
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
func NewLogger(p LoggerParams) LoggerResult {
|
||||
lvl := slog.LevelInfo
|
||||
configLevel := strings.ToUpper(p.Config.LogLevel)
|
||||
switch configLevel {
|
||||
case "INFO":
|
||||
lvl = slog.LevelInfo
|
||||
case "WARN":
|
||||
lvl = slog.LevelWarn
|
||||
case "ERROR":
|
||||
lvl = slog.LevelError
|
||||
case "DEBUG":
|
||||
lvl = slog.LevelDebug
|
||||
}
|
||||
if strings.ToUpper(p.Config.LogOutput) == "FILE" {
|
||||
fp := ""
|
||||
p, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
p, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
fp = filepath.Join(p, ".config", "gspot", "gspot.log")
|
||||
} else {
|
||||
fp = filepath.Join(p, "gspot", "gspot.log")
|
||||
}
|
||||
f, err := os.Create(fp)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return LoggerResult{
|
||||
Logger: slog.New(slog.NewJSONHandler(f, &slog.HandlerOptions{
|
||||
Level: lvl.Level(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
return LoggerResult{
|
||||
Logger: slog.New(tint.NewHandler(os.Stdout, &tint.Options{
|
||||
Level: lvl.Level(),
|
||||
TimeFormat: "[15:04:05.000]",
|
||||
})),
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
func (m *mainModel) LoadMoreItems() {
|
||||
@ -15,7 +16,7 @@ func (m *mainModel) LoadMoreItems() {
|
||||
}()
|
||||
switch m.mode {
|
||||
case "artist":
|
||||
albums, err := m.commands.ArtistAlbums(m.ctx, m.artist.ID, (page + 1))
|
||||
albums, err := m.commands.ArtistAlbums(m.artist.ID, (page + 1))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -31,29 +32,38 @@ func (m *mainModel) LoadMoreItems() {
|
||||
for _, item := range items {
|
||||
m.list.InsertItem(len(m.list.Items())+1, item)
|
||||
}
|
||||
main_updates <- m
|
||||
mainUpdates <- m
|
||||
return
|
||||
case "artists":
|
||||
artists, err := m.commands.UserArtists(m.ctx, (page + 1))
|
||||
artists, err := m.commands.Client().CurrentUsersFollowedArtists(
|
||||
m.commands.Context,
|
||||
spotify.Limit(50),
|
||||
spotify.Offset((page)*50),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
items := []list.Item{}
|
||||
for _, artist := range artists.Artists {
|
||||
items = append(items, mainItem{
|
||||
Name: artist.Name,
|
||||
ID: artist.ID,
|
||||
Desc: fmt.Sprintf("%d followers, genres: %s, popularity: %d", artist.Followers.Count, artist.Genres, artist.Popularity),
|
||||
Name: artist.Name,
|
||||
ID: artist.ID,
|
||||
Desc: fmt.Sprintf(
|
||||
"%d followers, genres: %s, popularity: %d",
|
||||
artist.Followers.Count,
|
||||
artist.Genres,
|
||||
artist.Popularity,
|
||||
),
|
||||
SpotifyItem: artist.SimpleArtist,
|
||||
})
|
||||
}
|
||||
for _, item := range items {
|
||||
m.list.InsertItem(len(m.list.Items())+1, item)
|
||||
}
|
||||
main_updates <- m
|
||||
mainUpdates <- m
|
||||
return
|
||||
case "album":
|
||||
tracks, err := m.commands.AlbumTracks(m.ctx, m.album.ID, (page + 1))
|
||||
tracks, err := m.commands.AlbumTracks(m.album.ID, (page + 1))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -70,10 +80,10 @@ func (m *mainModel) LoadMoreItems() {
|
||||
for _, item := range items {
|
||||
m.list.InsertItem(len(m.list.Items())+1, item)
|
||||
}
|
||||
main_updates <- m
|
||||
mainUpdates <- m
|
||||
return
|
||||
case "albums":
|
||||
albums, err := m.commands.UserAlbums(m.ctx, (page + 1))
|
||||
albums, err := m.commands.UserAlbums(page + 1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -89,10 +99,10 @@ func (m *mainModel) LoadMoreItems() {
|
||||
for _, item := range items {
|
||||
m.list.InsertItem(len(m.list.Items())+1, item)
|
||||
}
|
||||
main_updates <- m
|
||||
mainUpdates <- m
|
||||
return
|
||||
case "main":
|
||||
playlists, err := m.commands.Playlists(m.ctx, (page + 1))
|
||||
playlists, err := m.commands.Playlists(page + 1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -107,10 +117,10 @@ func (m *mainModel) LoadMoreItems() {
|
||||
for _, item := range items {
|
||||
m.list.InsertItem(len(m.list.Items())+1, item)
|
||||
}
|
||||
main_updates <- m
|
||||
mainUpdates <- m
|
||||
return
|
||||
case "playlist":
|
||||
playlistItems, err := m.commands.PlaylistTracks(m.ctx, m.playlist.ID, (page + 1))
|
||||
playlistItems, err := m.commands.PlaylistTracks(m.playlist.ID, (page + 1))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -121,16 +131,18 @@ func (m *mainModel) LoadMoreItems() {
|
||||
Artist: item.Track.Track.Artists[0],
|
||||
Duration: item.Track.Track.TimeDuration().Round(time.Second).String(),
|
||||
ID: item.Track.Track.ID,
|
||||
Desc: item.Track.Track.Artists[0].Name + " - " + item.Track.Track.TimeDuration().Round(time.Second).String(),
|
||||
Desc: item.Track.Track.Artists[0].Name + " - " + item.Track.Track.TimeDuration().
|
||||
Round(time.Second).
|
||||
String(),
|
||||
})
|
||||
}
|
||||
for _, item := range items {
|
||||
m.list.InsertItem(len(m.list.Items())+1, item)
|
||||
}
|
||||
main_updates <- m
|
||||
mainUpdates <- m
|
||||
return
|
||||
case "tracks":
|
||||
tracks, err := m.commands.TrackList(m.ctx, (page + 1))
|
||||
tracks, err := m.commands.TrackList(page + 1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -147,7 +159,7 @@ func (m *mainModel) LoadMoreItems() {
|
||||
for _, item := range items {
|
||||
m.list.InsertItem(len(m.list.Items())+1, item)
|
||||
}
|
||||
main_updates <- m
|
||||
mainUpdates <- m
|
||||
return
|
||||
}
|
||||
}
|
@ -14,8 +14,7 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/zmb3/spotify/v2"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gospt/src/commands"
|
||||
"git.asdf.cafe/abs3nt/gospt/src/gctx"
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -23,7 +22,7 @@ var (
|
||||
DocStyle = lipgloss.NewStyle().Margin(0, 2).Border(lipgloss.DoubleBorder(), true, true, true, true)
|
||||
currentlyPlaying *spotify.CurrentlyPlaying
|
||||
playbackContext string
|
||||
main_updates chan *mainModel
|
||||
mainUpdates chan *mainModel
|
||||
page = 1
|
||||
loading = false
|
||||
showingMessage = false
|
||||
@ -77,8 +76,7 @@ func (i mainItem) FilterValue() string { return i.Title() + i.Desc }
|
||||
type mainModel struct {
|
||||
list list.Model
|
||||
input textinput.Model
|
||||
ctx *gctx.Context
|
||||
commands *commands.Commands
|
||||
commands *commands.Commander
|
||||
mode Mode
|
||||
playlist spotify.SimplePlaylist
|
||||
artist spotify.SimpleArtist
|
||||
@ -95,37 +93,92 @@ func (m *mainModel) PlayRadio() {
|
||||
selectedItem := m.list.SelectedItem().(mainItem).SpotifyItem
|
||||
switch item := selectedItem.(type) {
|
||||
case spotify.SimplePlaylist:
|
||||
go HandlePlaylistRadio(m.ctx, m.commands, item)
|
||||
go func() {
|
||||
err := m.commands.RadioFromPlaylist(item)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case *spotify.SavedTrackPage:
|
||||
go HandleLibraryRadio(m.ctx, m.commands)
|
||||
go func() {
|
||||
err := m.commands.RadioFromSavedTracks()
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case spotify.SimpleAlbum:
|
||||
go HandleAlbumRadio(m.ctx, m.commands, item)
|
||||
go func() {
|
||||
err := m.commands.RadioFromAlbum(item)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case spotify.FullAlbum:
|
||||
go HandleAlbumRadio(m.ctx, m.commands, item.SimpleAlbum)
|
||||
go func() {
|
||||
err := m.commands.RadioFromAlbum(item.SimpleAlbum)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case spotify.SimpleArtist:
|
||||
go HandleArtistRadio(m.ctx, m.commands, item)
|
||||
go func() {
|
||||
err := m.commands.RadioGivenArtist(item)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case spotify.FullArtist:
|
||||
go HandleArtistRadio(m.ctx, m.commands, item.SimpleArtist)
|
||||
go func() {
|
||||
err := m.commands.RadioGivenArtist(item.SimpleArtist)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case spotify.SimpleTrack:
|
||||
go HandleRadio(m.ctx, m.commands, item)
|
||||
go func() {
|
||||
err := m.commands.RadioGivenSong(item, 0)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case spotify.FullTrack:
|
||||
go HandleRadio(m.ctx, m.commands, item.SimpleTrack)
|
||||
go func() {
|
||||
err := m.commands.RadioGivenSong(item.SimpleTrack, 0)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case spotify.PlaylistTrack:
|
||||
go HandleRadio(m.ctx, m.commands, item.Track.SimpleTrack)
|
||||
go func() {
|
||||
err := m.commands.RadioGivenSong(item.Track.SimpleTrack, 0)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case spotify.PlaylistItem:
|
||||
go HandleRadio(m.ctx, m.commands, item.Track.Track.SimpleTrack)
|
||||
go func() {
|
||||
err := m.commands.RadioGivenSong(item.Track.Track.SimpleTrack, 0)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
case spotify.SavedTrack:
|
||||
go HandleRadio(m.ctx, m.commands, item.SimpleTrack)
|
||||
go func() {
|
||||
err := m.commands.RadioGivenSong(item.SimpleTrack, 0)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -137,35 +190,35 @@ func (m *mainModel) GoBack() (tea.Cmd, error) {
|
||||
return tea.Quit, nil
|
||||
case Albums, Artists, Tracks, Playlist, Devices, Search, Queue:
|
||||
m.mode = Main
|
||||
new_items, err := MainView(m.ctx, m.commands)
|
||||
newItems, err := MainView(m.commands)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
case Album:
|
||||
m.mode = Albums
|
||||
new_items, err := AlbumsView(m.ctx, m.commands)
|
||||
newItems, err := AlbumsView(m.commands)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
case Artist:
|
||||
m.mode = Artists
|
||||
new_items, err := ArtistsView(m.ctx, m.commands)
|
||||
newItems, err := ArtistsView(m.commands)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
case ArtistAlbum:
|
||||
m.mode = Artist
|
||||
new_items, err := ArtistAlbumsView(m.ctx, m.artist.ID, m.commands)
|
||||
newItems, err := ArtistAlbumsView(m.artist.ID, m.commands)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
case SearchArtists, SearchTracks, SearchAlbums, SearchPlaylists:
|
||||
m.mode = Search
|
||||
items, result, err := SearchView(m.ctx, m.commands, m.search)
|
||||
items, result, err := SearchView(m.commands, m.search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -173,39 +226,39 @@ func (m *mainModel) GoBack() (tea.Cmd, error) {
|
||||
m.list.SetItems(items)
|
||||
case SearchArtist:
|
||||
m.mode = SearchArtists
|
||||
new_items, err := SearchArtistsView(m.ctx, m.commands, m.searchResults.Artists)
|
||||
newItems, err := SearchArtistsView(m.commands, m.searchResults.Artists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
case SearchArtistAlbum:
|
||||
m.mode = SearchArtist
|
||||
new_items, err := ArtistAlbumsView(m.ctx, m.artist.ID, m.commands)
|
||||
newItems, err := ArtistAlbumsView(m.artist.ID, m.commands)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
case SearchAlbum:
|
||||
m.mode = SearchAlbums
|
||||
new_items, err := SearchAlbumsView(m.ctx, m.commands, m.searchResults.Albums)
|
||||
newItems, err := SearchAlbumsView(m.commands, m.searchResults.Albums)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
case SearchPlaylist:
|
||||
m.mode = SearchPlaylists
|
||||
new_items, err := SearchPlaylistsView(m.ctx, m.commands, m.searchResults.Playlists)
|
||||
newItems, err := SearchPlaylistsView(m.commands, m.searchResults.Playlists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
default:
|
||||
page = 0
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type SpotifyUrl struct {
|
||||
type SpotifyURL struct {
|
||||
ExternalURLs map[string]string
|
||||
}
|
||||
|
||||
@ -214,34 +267,34 @@ func (m *mainModel) CopyToClipboard() error {
|
||||
switch converted := item.(type) {
|
||||
case spotify.SimplePlaylist:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
case *spotify.FullPlaylist:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
case spotify.SimpleAlbum:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
case *spotify.FullAlbum:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
case spotify.SimpleArtist:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
case *spotify.FullArtist:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
case spotify.SimpleTrack:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
case spotify.PlaylistTrack:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.Track.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.Track.ExternalURLs["spotify"])
|
||||
case spotify.SavedTrack:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
case spotify.FullTrack:
|
||||
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||
clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
return clipboard.WriteAll(converted.ExternalURLs["spotify"])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -256,36 +309,45 @@ func (m *mainModel) SendMessage(msg string, duration time.Duration) {
|
||||
}
|
||||
|
||||
func (m *mainModel) QueueItem() error {
|
||||
var id spotify.ID
|
||||
var name string
|
||||
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
|
||||
case spotify.PlaylistTrack:
|
||||
go m.SendMessage("Adding "+item.Track.Name+" to queue", 2*time.Second)
|
||||
go HandleQueueItem(m.ctx, m.commands, item.Track.ID)
|
||||
name = item.Track.Name
|
||||
id = item.Track.ID
|
||||
case spotify.SavedTrack:
|
||||
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
|
||||
go HandleQueueItem(m.ctx, m.commands, item.ID)
|
||||
name = item.Name
|
||||
id = item.ID
|
||||
case spotify.SimpleTrack:
|
||||
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
|
||||
go HandleQueueItem(m.ctx, m.commands, item.ID)
|
||||
name = item.Name
|
||||
id = item.ID
|
||||
case spotify.FullTrack:
|
||||
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
|
||||
go HandleQueueItem(m.ctx, m.commands, item.ID)
|
||||
name = item.Name
|
||||
id = item.ID
|
||||
case *spotify.FullTrack:
|
||||
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
|
||||
go HandleQueueItem(m.ctx, m.commands, item.ID)
|
||||
name = item.Name
|
||||
id = item.ID
|
||||
case *spotify.SimpleTrack:
|
||||
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
|
||||
go HandleQueueItem(m.ctx, m.commands, item.ID)
|
||||
name = item.Name
|
||||
id = item.ID
|
||||
case *spotify.SimplePlaylist:
|
||||
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
|
||||
go HandleQueueItem(m.ctx, m.commands, item.ID)
|
||||
name = item.Name
|
||||
id = item.ID
|
||||
}
|
||||
go m.SendMessage("Adding "+name+" to queue", 2*time.Second)
|
||||
go func() {
|
||||
err := m.commands.QueueSong(id)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
if m.mode == Queue {
|
||||
go func() {
|
||||
new_items, err := QueueView(m.ctx, m.commands)
|
||||
newItems, err := QueueView(m.commands)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
@ -298,12 +360,15 @@ func (m *mainModel) DeleteTrackFromPlaylist() error {
|
||||
track := m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.PlaylistTrack).Track
|
||||
go m.SendMessage("Deleteing "+track.Name+" from "+m.playlist.Name, 2*time.Second)
|
||||
go func() {
|
||||
HandleDeleteTrackFromPlaylist(m.ctx, m.commands, track.ID, m.playlist.ID)
|
||||
new_items, err := PlaylistView(m.ctx, m.commands, m.playlist)
|
||||
err := m.commands.DeleteTracksFromPlaylist([]spotify.ID{track.ID}, m.playlist.ID)
|
||||
if err != nil {
|
||||
return
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
newItems, err := PlaylistView(m.commands, m.playlist)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
m.list.SetItems(newItems)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@ -313,12 +378,15 @@ func (m *mainModel) SelectItem() error {
|
||||
case Queue:
|
||||
page = 1
|
||||
go func() {
|
||||
HandleNextInQueue(m.ctx, m.commands, m.list.Index())
|
||||
new_items, err := QueueView(m.ctx, m.commands)
|
||||
err := m.commands.Next(m.list.Index(), true)
|
||||
if err != nil {
|
||||
return
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
newItems, err := QueueView(m.commands)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
}()
|
||||
case Search:
|
||||
@ -326,176 +394,202 @@ func (m *mainModel) SelectItem() error {
|
||||
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
|
||||
case *spotify.FullArtistPage:
|
||||
m.mode = SearchArtists
|
||||
new_items, err := SearchArtistsView(m.ctx, m.commands, item)
|
||||
newItems, err := SearchArtistsView(m.commands, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case *spotify.SimpleAlbumPage:
|
||||
m.mode = SearchAlbums
|
||||
new_items, err := SearchAlbumsView(m.ctx, m.commands, item)
|
||||
newItems, err := SearchAlbumsView(m.commands, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case *spotify.SimplePlaylistPage:
|
||||
m.mode = SearchPlaylists
|
||||
new_items, err := SearchPlaylistsView(m.ctx, m.commands, item)
|
||||
newItems, err := SearchPlaylistsView(m.commands, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case *spotify.FullTrackPage:
|
||||
m.mode = SearchTracks
|
||||
new_items, err := SearchTracksView(m.ctx, m.commands, item)
|
||||
newItems, err := SearchTracksView(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
}
|
||||
case SearchArtists:
|
||||
page = 1
|
||||
m.mode = SearchArtist
|
||||
m.artist = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleArtist)
|
||||
new_items, err := ArtistAlbumsView(m.ctx, m.artist.ID, m.commands)
|
||||
newItems, err := ArtistAlbumsView(m.artist.ID, m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case SearchArtist:
|
||||
page = 1
|
||||
m.mode = SearchArtistAlbum
|
||||
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
|
||||
new_items, err := AlbumTracksView(m.ctx, m.album.ID, m.commands)
|
||||
newItems, err := AlbumTracksView(m.album.ID, m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case SearchAlbums:
|
||||
page = 1
|
||||
m.mode = SearchAlbum
|
||||
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
|
||||
new_items, err := AlbumTracksView(m.ctx, m.album.ID, m.commands)
|
||||
newItems, err := AlbumTracksView(m.album.ID, m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case SearchPlaylists:
|
||||
page = 1
|
||||
m.mode = SearchPlaylist
|
||||
playlist := m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimplePlaylist)
|
||||
m.playlist = playlist
|
||||
new_items, err := PlaylistView(m.ctx, m.commands, playlist)
|
||||
newItems, err := PlaylistView(m.commands, playlist)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case Main:
|
||||
page = 1
|
||||
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
|
||||
case spotify.Queue:
|
||||
m.mode = Queue
|
||||
new_items, err := QueueView(m.ctx, m.commands)
|
||||
newItems, err := QueueView(m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case *spotify.FullArtistCursorPage:
|
||||
m.mode = Artists
|
||||
new_items, err := ArtistsView(m.ctx, m.commands)
|
||||
newItems, err := ArtistsView(m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case *spotify.SavedAlbumPage:
|
||||
m.mode = Albums
|
||||
new_items, err := AlbumsView(m.ctx, m.commands)
|
||||
newItems, err := AlbumsView(m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case spotify.SimplePlaylist:
|
||||
m.mode = Playlist
|
||||
m.playlist = item
|
||||
new_items, err := PlaylistView(m.ctx, m.commands, item)
|
||||
newItems, err := PlaylistView(m.commands, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case *spotify.SavedTrackPage:
|
||||
m.mode = Tracks
|
||||
new_items, err := SavedTracksView(m.ctx, m.commands)
|
||||
newItems, err := SavedTracksView(m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
}
|
||||
case Albums:
|
||||
page = 1
|
||||
m.mode = Album
|
||||
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
|
||||
new_items, err := AlbumTracksView(m.ctx, m.album.ID, m.commands)
|
||||
newItems, err := AlbumTracksView(m.album.ID, m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case Artist:
|
||||
m.mode = ArtistAlbum
|
||||
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
|
||||
new_items, err := AlbumTracksView(m.ctx, m.album.ID, m.commands)
|
||||
newItems, err := AlbumTracksView(m.album.ID, m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case Artists:
|
||||
m.mode = Artist
|
||||
m.artist = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleArtist)
|
||||
new_items, err := ArtistAlbumsView(m.ctx, m.artist.ID, m.commands)
|
||||
newItems, err := ArtistAlbumsView(m.artist.ID, m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
case Album, ArtistAlbum, SearchArtistAlbum, SearchAlbum:
|
||||
pos := m.list.Cursor() + (m.list.Paginator.Page * m.list.Paginator.TotalPages)
|
||||
go HandlePlayWithContext(m.ctx, m.commands, &m.album.URI, &pos)
|
||||
go func() {
|
||||
_ = m.commands.PlaySongInPlaylist(&m.album.URI, &pos)
|
||||
}()
|
||||
case Playlist, SearchPlaylist:
|
||||
pos := m.list.Cursor() + (m.list.Paginator.Page * m.list.Paginator.PerPage)
|
||||
go HandlePlayWithContext(m.ctx, m.commands, &m.playlist.URI, &pos)
|
||||
go func() {
|
||||
_ = m.commands.PlaySongInPlaylist(&m.playlist.URI, &pos)
|
||||
}()
|
||||
case Tracks:
|
||||
go HandlePlayLikedSong(m.ctx, m.commands, m.list.Cursor()+(m.list.Paginator.Page*m.list.Paginator.PerPage))
|
||||
go func() {
|
||||
err := m.commands.PlayLikedSongs(m.list.Cursor() + (m.list.Paginator.Page * m.list.Paginator.PerPage))
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
case SearchTracks:
|
||||
go HandlePlayTrack(m.ctx, m.commands, m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.FullTrack).ID)
|
||||
go func() {
|
||||
err := m.commands.QueueSong(m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.FullTrack).ID)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
return
|
||||
}
|
||||
err = m.commands.Next(1, false)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
return
|
||||
}
|
||||
}()
|
||||
case Devices:
|
||||
go HandleSetDevice(m.ctx, m.commands, m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.PlayerDevice))
|
||||
go m.SendMessage("Setting device to "+m.list.SelectedItem().FilterValue(), 2*time.Second)
|
||||
go func() {
|
||||
err := m.commands.SetDevice(m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.PlayerDevice).ID)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
} else {
|
||||
m.SendMessage("Setting device to "+m.list.SelectedItem().FilterValue(), 2*time.Second)
|
||||
}
|
||||
}()
|
||||
m.mode = "main"
|
||||
new_items, err := MainView(m.ctx, m.commands)
|
||||
newItems, err := MainView(m.commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mainModel) Init() tea.Cmd {
|
||||
main_updates = make(chan *mainModel)
|
||||
mainUpdates = make(chan *mainModel)
|
||||
return Tick()
|
||||
}
|
||||
|
||||
@ -508,10 +602,13 @@ func Tick() tea.Cmd {
|
||||
}
|
||||
|
||||
func (m *mainModel) TickPlayback() {
|
||||
playing, _ := m.commands.Client().PlayerCurrentlyPlaying(m.ctx)
|
||||
playing, _ := m.commands.Client().PlayerCurrentlyPlaying(m.commands.Context)
|
||||
if playing != nil && playing.Playing && playing.Item != nil {
|
||||
if currentlyPlaying == nil || currentlyPlaying.Item == nil ||
|
||||
currentlyPlaying.Item.ID != playing.Item.ID {
|
||||
playbackContext, _ = m.getContext(playing)
|
||||
}
|
||||
currentlyPlaying = playing
|
||||
playbackContext, _ = m.getContext(playing)
|
||||
}
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
quit := make(chan struct{})
|
||||
@ -519,10 +616,14 @@ func (m *mainModel) TickPlayback() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
playing, _ := m.commands.Client().PlayerCurrentlyPlaying(m.ctx)
|
||||
m.commands.Log.Debug("TICKING PLAYBACK")
|
||||
playing, _ := m.commands.Client().PlayerCurrentlyPlaying(m.commands.Context)
|
||||
if playing != nil && playing.Playing && playing.Item != nil {
|
||||
if currentlyPlaying == nil || currentlyPlaying.Item == nil ||
|
||||
currentlyPlaying.Item.ID != playing.Item.ID {
|
||||
playbackContext, _ = m.getContext(playing)
|
||||
}
|
||||
currentlyPlaying = playing
|
||||
playbackContext, _ = m.getContext(playing)
|
||||
}
|
||||
case <-quit:
|
||||
ticker.Stop()
|
||||
@ -541,7 +642,7 @@ func (m *mainModel) View() string {
|
||||
|
||||
func (m *mainModel) Typing(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
if msg.String() == "enter" {
|
||||
items, result, err := SearchView(m.ctx, m.commands, m.input.Value())
|
||||
items, result, err := SearchView(m.commands, m.input.Value())
|
||||
if err != nil {
|
||||
return false, tea.Quit
|
||||
}
|
||||
@ -564,26 +665,29 @@ func (m *mainModel) Typing(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
|
||||
func (m *mainModel) getContext(playing *spotify.CurrentlyPlaying) (string, error) {
|
||||
context := playing.PlaybackContext
|
||||
uri_split := strings.Split(string(context.URI), ":")
|
||||
if len(uri_split) < 3 {
|
||||
uriSplit := strings.Split(string(context.URI), ":")
|
||||
if len(uriSplit) < 3 {
|
||||
return "", fmt.Errorf("NO URI")
|
||||
}
|
||||
id := strings.Split(string(context.URI), ":")[2]
|
||||
switch context.Type {
|
||||
case "album":
|
||||
album, err := m.commands.Client().GetAlbum(m.ctx, spotify.ID(id))
|
||||
m.commands.Log.Debug("ALBUM CONTEXT")
|
||||
album, err := m.commands.Client().GetAlbum(m.commands.Context, spotify.ID(id))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return album.Name, nil
|
||||
case "playlist":
|
||||
playlist, err := m.commands.Client().GetPlaylist(m.ctx, spotify.ID(id))
|
||||
m.commands.Log.Debug("PLAYLIST CONTEXT")
|
||||
playlist, err := m.commands.Client().GetPlaylist(m.commands.Context, spotify.ID(id))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return playlist.Name, nil
|
||||
case "artist":
|
||||
artist, err := m.commands.Client().GetArtist(m.ctx, spotify.ID(id))
|
||||
m.commands.Log.Debug("ARTIST CONTEXT")
|
||||
artist, err := m.commands.Client().GetArtist(m.commands.Context, spotify.ID(id))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -595,7 +699,7 @@ func (m *mainModel) getContext(playing *spotify.CurrentlyPlaying) (string, error
|
||||
func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// Update list items from LoadMore
|
||||
select {
|
||||
case update := <-main_updates:
|
||||
case update := <-mainUpdates:
|
||||
m.list.SetItems(update.list.Items())
|
||||
default:
|
||||
}
|
||||
@ -617,11 +721,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if m.mode == Queue && len(m.list.Items()) != 0 {
|
||||
if m.list.Items()[0].(mainItem).SpotifyItem.(spotify.FullTrack).Name != playing.Item.Name {
|
||||
go func() {
|
||||
new_items, err := QueueView(m.ctx, m.commands)
|
||||
newItems, err := QueueView(m.commands)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
}()
|
||||
}
|
||||
}
|
||||
@ -652,20 +756,40 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if msg.String() == "c" {
|
||||
err := m.CopyToClipboard()
|
||||
if err != nil {
|
||||
return m, tea.Quit
|
||||
go m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}
|
||||
if msg.String() == ">" {
|
||||
go HandleSeek(m.ctx, m.commands, true)
|
||||
go func() {
|
||||
err := m.commands.Seek(true)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if msg.String() == "<" {
|
||||
go HandleSeek(m.ctx, m.commands, false)
|
||||
go func() {
|
||||
err := m.commands.Seek(false)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if msg.String() == "+" {
|
||||
go HandleVolume(m.ctx, m.commands, true)
|
||||
go func() {
|
||||
err := m.commands.ChangeVolume(10)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if msg.String() == "-" {
|
||||
go HandleVolume(m.ctx, m.commands, false)
|
||||
go func() {
|
||||
err := m.commands.ChangeVolume(-10)
|
||||
if err != nil {
|
||||
m.SendMessage(err.Error(), 5*time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
// search input
|
||||
if m.input.Focused() {
|
||||
@ -682,11 +806,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// enter device selection
|
||||
if msg.String() == "d" {
|
||||
m.mode = Devices
|
||||
new_items, err := DeviceView(m.ctx, m.commands)
|
||||
newItems, err := DeviceView(m.commands)
|
||||
if err != nil {
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.list.SetItems(new_items)
|
||||
m.list.SetItems(newItems)
|
||||
m.list.ResetSelected()
|
||||
}
|
||||
// go back
|
||||
@ -723,18 +847,19 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
// handle mouse
|
||||
case tea.MouseMsg:
|
||||
if msg.Type == 5 {
|
||||
case tea.MouseButton:
|
||||
if msg == 5 {
|
||||
m.list.CursorUp()
|
||||
}
|
||||
if msg.Type == 6 {
|
||||
m.list.CursorDown()
|
||||
if msg == 6 {
|
||||
m.list.CursorUp()
|
||||
}
|
||||
|
||||
// window size -1 to handle search bar
|
||||
case tea.WindowSizeMsg:
|
||||
h, v := DocStyle.GetFrameSize()
|
||||
m.list.SetSize(msg.Width-h, msg.Height-v-1)
|
||||
DocStyle.Width(msg.Width - h)
|
||||
}
|
||||
|
||||
// return
|
||||
@ -743,36 +868,35 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func InitMain(ctx *gctx.Context, c *commands.Commands, mode Mode) (tea.Model, error) {
|
||||
func InitMain(c *commands.Commander, mode Mode) (tea.Model, error) {
|
||||
prog := progress.New(progress.WithColorProfile(2), progress.WithoutPercentage())
|
||||
var err error
|
||||
lipgloss.SetColorProfile(2)
|
||||
items := []list.Item{}
|
||||
switch mode {
|
||||
case Main:
|
||||
items, err = MainView(ctx, c)
|
||||
items, err = MainView(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case Devices:
|
||||
items, err = DeviceView(ctx, c)
|
||||
items, err = DeviceView(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case Tracks:
|
||||
items, err = SavedTracksView(ctx, c)
|
||||
items, err = SavedTracksView(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m := &mainModel{
|
||||
list: list.New(items, list.NewDefaultDelegate(), 0, 0),
|
||||
ctx: ctx,
|
||||
commands: c,
|
||||
mode: mode,
|
||||
progress: prog,
|
||||
}
|
||||
m.list.Title = "GOSPT"
|
||||
m.list.Title = "GSPOT"
|
||||
go m.TickPlayback()
|
||||
Tick()
|
||||
m.list.DisableQuitKeybindings()
|
@ -1,15 +1,14 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"git.asdf.cafe/abs3nt/gospt/src/commands"
|
||||
"git.asdf.cafe/abs3nt/gospt/src/gctx"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
|
||||
)
|
||||
|
||||
// StartTea the entry point for the UI. Initializes the model.
|
||||
func StartTea(ctx *gctx.Context, cmd *commands.Commands, mode string) error {
|
||||
m, err := InitMain(ctx, cmd, Mode(mode))
|
||||
func StartTea(cmd *commands.Commander, mode string) error {
|
||||
m, err := InitMain(cmd, Mode(mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
@ -5,19 +5,18 @@ import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gospt/src/commands"
|
||||
"git.asdf.cafe/abs3nt/gospt/src/gctx"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/zmb3/spotify/v2"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
|
||||
)
|
||||
|
||||
const regex = `<.*?>`
|
||||
|
||||
func DeviceView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
|
||||
func DeviceView(commands *commands.Commander) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
devices, err := commands.Client().PlayerDevices(ctx)
|
||||
devices, err := commands.Client().PlayerDevices(commands.Context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -31,19 +30,21 @@ func DeviceView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, er
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func QueueView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
|
||||
func QueueView(commands *commands.Commander) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
tracks, err := commands.UserQueue(ctx)
|
||||
tracks, err := commands.Client().GetQueue(commands.Context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tracks.CurrentlyPlaying.Name != "" {
|
||||
items = append(items, mainItem{
|
||||
Name: tracks.CurrentlyPlaying.Name,
|
||||
Artist: tracks.CurrentlyPlaying.Artists[0],
|
||||
Duration: tracks.CurrentlyPlaying.TimeDuration().Round(time.Second).String(),
|
||||
ID: tracks.CurrentlyPlaying.ID,
|
||||
Desc: tracks.CurrentlyPlaying.Artists[0].Name + " - " + tracks.CurrentlyPlaying.TimeDuration().Round(time.Second).String(),
|
||||
Name: tracks.CurrentlyPlaying.Name,
|
||||
Artist: tracks.CurrentlyPlaying.Artists[0],
|
||||
Duration: tracks.CurrentlyPlaying.TimeDuration().Round(time.Second).String(),
|
||||
ID: tracks.CurrentlyPlaying.ID,
|
||||
Desc: tracks.CurrentlyPlaying.Artists[0].Name + " - " + tracks.CurrentlyPlaying.TimeDuration().
|
||||
Round(time.Second).
|
||||
String(),
|
||||
SpotifyItem: tracks.CurrentlyPlaying,
|
||||
})
|
||||
}
|
||||
@ -60,28 +61,35 @@ func QueueView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, err
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func PlaylistView(ctx *gctx.Context, commands *commands.Commands, playlist spotify.SimplePlaylist) ([]list.Item, error) {
|
||||
func PlaylistView(commands *commands.Commander, playlist spotify.SimplePlaylist) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
playlistItems, err := commands.PlaylistTracks(ctx, playlist.ID, 1)
|
||||
playlistItems, err := commands.Client().GetPlaylistItems(
|
||||
commands.Context,
|
||||
playlist.ID,
|
||||
spotify.Limit(50),
|
||||
spotify.Offset(0),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range playlistItems.Items {
|
||||
items = append(items, mainItem{
|
||||
Name: item.Track.Track.Name,
|
||||
Artist: item.Track.Track.Artists[0],
|
||||
Duration: item.Track.Track.TimeDuration().Round(time.Second).String(),
|
||||
ID: item.Track.Track.ID,
|
||||
Desc: item.Track.Track.Artists[0].Name + " - " + item.Track.Track.TimeDuration().Round(time.Second).String(),
|
||||
Name: item.Track.Track.Name,
|
||||
Artist: item.Track.Track.Artists[0],
|
||||
Duration: item.Track.Track.TimeDuration().Round(time.Second).String(),
|
||||
ID: item.Track.Track.ID,
|
||||
Desc: item.Track.Track.Artists[0].Name + " - " + item.Track.Track.TimeDuration().
|
||||
Round(time.Second).
|
||||
String(),
|
||||
SpotifyItem: item,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func ArtistsView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
|
||||
func ArtistsView(commands *commands.Commander) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
artists, err := commands.UserArtists(ctx, 1)
|
||||
artists, err := commands.Client().CurrentUsersFollowedArtists(commands.Context, spotify.Limit(50), spotify.Offset(0))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -96,7 +104,10 @@ func ArtistsView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, e
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func SearchArtistsView(ctx *gctx.Context, commands *commands.Commands, artists *spotify.FullArtistPage) ([]list.Item, error) {
|
||||
func SearchArtistsView(
|
||||
commands *commands.Commander,
|
||||
artists *spotify.FullArtistPage,
|
||||
) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
for _, artist := range artists.Artists {
|
||||
items = append(items, mainItem{
|
||||
@ -109,10 +120,10 @@ func SearchArtistsView(ctx *gctx.Context, commands *commands.Commands, artists *
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func SearchView(ctx *gctx.Context, commands *commands.Commands, search string) ([]list.Item, *SearchResults, error) {
|
||||
func SearchView(commands *commands.Commander, search string) ([]list.Item, *SearchResults, error) {
|
||||
items := []list.Item{}
|
||||
|
||||
result, err := commands.Search(ctx, search, 1)
|
||||
result, err := commands.Search(search, 1)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -132,51 +143,62 @@ func SearchView(ctx *gctx.Context, commands *commands.Commands, search string) (
|
||||
return items, results, nil
|
||||
}
|
||||
|
||||
func AlbumsView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
|
||||
func AlbumsView(commands *commands.Commander) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
albums, err := commands.UserAlbums(ctx, 1)
|
||||
albums, err := commands.Client().CurrentUsersAlbums(commands.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, album := range albums.Albums {
|
||||
items = append(items, mainItem{
|
||||
Name: album.Name,
|
||||
ID: album.ID,
|
||||
Desc: fmt.Sprintf("%s by %s, %d tracks, released %d", album.AlbumType, album.Artists[0].Name, album.Tracks.Total, album.ReleaseDateTime().Year()),
|
||||
Name: album.Name,
|
||||
ID: album.ID,
|
||||
Desc: fmt.Sprintf(
|
||||
"%s by %s, %d tracks, released %d",
|
||||
album.AlbumType,
|
||||
album.Artists[0].Name,
|
||||
album.Tracks.Total,
|
||||
album.ReleaseDateTime().Year(),
|
||||
),
|
||||
SpotifyItem: album.SimpleAlbum,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func SearchPlaylistsView(ctx *gctx.Context, commands *commands.Commands, playlists *spotify.SimplePlaylistPage) ([]list.Item, error) {
|
||||
func SearchPlaylistsView(commands *commands.Commander, playlists *spotify.SimplePlaylistPage) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
for _, playlist := range playlists.Playlists {
|
||||
items = append(items, mainItem{
|
||||
Name: playlist.Name,
|
||||
Desc: stripHtmlRegex(playlist.Description),
|
||||
Desc: stripHTMLRegex(playlist.Description),
|
||||
SpotifyItem: playlist,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func SearchAlbumsView(ctx *gctx.Context, commands *commands.Commands, albums *spotify.SimpleAlbumPage) ([]list.Item, error) {
|
||||
func SearchAlbumsView(commands *commands.Commander, albums *spotify.SimpleAlbumPage) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
for _, album := range albums.Albums {
|
||||
items = append(items, mainItem{
|
||||
Name: album.Name,
|
||||
ID: album.ID,
|
||||
Desc: fmt.Sprintf("%s by %s, released %d", album.AlbumType, album.Artists[0].Name, album.ReleaseDateTime().Year()),
|
||||
Name: album.Name,
|
||||
ID: album.ID,
|
||||
Desc: fmt.Sprintf(
|
||||
"%s by %s, released %d",
|
||||
album.AlbumType,
|
||||
album.Artists[0].Name,
|
||||
album.ReleaseDateTime().Year(),
|
||||
),
|
||||
SpotifyItem: album,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func ArtistAlbumsView(ctx *gctx.Context, album spotify.ID, commands *commands.Commands) ([]list.Item, error) {
|
||||
func ArtistAlbumsView(album spotify.ID, commands *commands.Commander) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
albums, err := commands.ArtistAlbums(ctx, album, 1)
|
||||
albums, err := commands.ArtistAlbums(album, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -191,9 +213,9 @@ func ArtistAlbumsView(ctx *gctx.Context, album spotify.ID, commands *commands.Co
|
||||
return items, err
|
||||
}
|
||||
|
||||
func AlbumTracksView(ctx *gctx.Context, album spotify.ID, commands *commands.Commands) ([]list.Item, error) {
|
||||
func AlbumTracksView(album spotify.ID, commands *commands.Commander) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
tracks, err := commands.AlbumTracks(ctx, album, 1)
|
||||
tracks, err := commands.AlbumTracks(album, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -210,7 +232,7 @@ func AlbumTracksView(ctx *gctx.Context, album spotify.ID, commands *commands.Com
|
||||
return items, err
|
||||
}
|
||||
|
||||
func SearchTracksView(ctx *gctx.Context, commands *commands.Commands, tracks *spotify.FullTrackPage) ([]list.Item, error) {
|
||||
func SearchTracksView(tracks *spotify.FullTrackPage) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
for _, track := range tracks.Tracks {
|
||||
items = append(items, mainItem{
|
||||
@ -225,9 +247,9 @@ func SearchTracksView(ctx *gctx.Context, commands *commands.Commands, tracks *sp
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func SavedTracksView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
|
||||
func SavedTracksView(commands *commands.Commander) ([]list.Item, error) {
|
||||
items := []list.Item{}
|
||||
tracks, err := commands.TrackList(ctx, 1)
|
||||
tracks, err := commands.Client().CurrentUsersTracks(commands.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -244,30 +266,31 @@ func SavedTracksView(ctx *gctx.Context, commands *commands.Commands) ([]list.Ite
|
||||
return items, err
|
||||
}
|
||||
|
||||
func MainView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
|
||||
func MainView(c *commands.Commander) ([]list.Item, error) {
|
||||
c.Log.Debug("SWITCHING TO MAIN VIEW")
|
||||
wg := errgroup.Group{}
|
||||
var saved_items *spotify.SavedTrackPage
|
||||
var savedItems *spotify.SavedTrackPage
|
||||
var playlists *spotify.SimplePlaylistPage
|
||||
var artists *spotify.FullArtistCursorPage
|
||||
var albums *spotify.SavedAlbumPage
|
||||
|
||||
wg.Go(func() (err error) {
|
||||
saved_items, err = commands.TrackList(ctx, 1)
|
||||
savedItems, err = c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(0))
|
||||
return
|
||||
})
|
||||
|
||||
wg.Go(func() (err error) {
|
||||
playlists, err = commands.Playlists(ctx, 1)
|
||||
playlists, err = c.Client().CurrentUsersPlaylists(c.Context, spotify.Limit(50), spotify.Offset(0))
|
||||
return
|
||||
})
|
||||
|
||||
wg.Go(func() (err error) {
|
||||
artists, err = commands.UserArtists(ctx, 1)
|
||||
artists, err = c.Client().CurrentUsersFollowedArtists(c.Context, spotify.Limit(50), spotify.Offset(0))
|
||||
return
|
||||
})
|
||||
|
||||
wg.Go(func() (err error) {
|
||||
albums, err = commands.UserAlbums(ctx, 1)
|
||||
albums, err = c.Client().CurrentUsersAlbums(c.Context, spotify.Limit(50), spotify.Offset(0))
|
||||
return
|
||||
})
|
||||
|
||||
@ -277,11 +300,11 @@ func MainView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, erro
|
||||
}
|
||||
|
||||
items := []list.Item{}
|
||||
if saved_items != nil && saved_items.Total != 0 {
|
||||
if savedItems != nil && savedItems.Total != 0 {
|
||||
items = append(items, mainItem{
|
||||
Name: "Saved Tracks",
|
||||
Desc: fmt.Sprintf("%d saved songs", saved_items.Total),
|
||||
SpotifyItem: saved_items,
|
||||
Desc: fmt.Sprintf("%d saved songs", savedItems.Total),
|
||||
SpotifyItem: savedItems,
|
||||
})
|
||||
}
|
||||
if albums != nil && albums.Total != 0 {
|
||||
@ -307,7 +330,7 @@ func MainView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, erro
|
||||
for _, playlist := range playlists.Playlists {
|
||||
items = append(items, mainItem{
|
||||
Name: playlist.Name,
|
||||
Desc: stripHtmlRegex(playlist.Description),
|
||||
Desc: stripHTMLRegex(playlist.Description),
|
||||
SpotifyItem: playlist,
|
||||
})
|
||||
}
|
||||
@ -315,7 +338,7 @@ func MainView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, erro
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func stripHtmlRegex(s string) string {
|
||||
func stripHTMLRegex(s string) string {
|
||||
r := regexp.MustCompile(regex)
|
||||
return r.ReplaceAllString(s, "")
|
||||
}
|
102
src/components/tuitview/tuitview.go
Normal file
102
src/components/tuitview/tuitview.go
Normal file
@ -0,0 +1,102 @@
|
||||
package tuitview
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/zmb3/spotify/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
tracksLoading = atomic.Bool{}
|
||||
playlistsLoading = atomic.Bool{}
|
||||
tracksPage = 1
|
||||
playlistsPage = 1
|
||||
)
|
||||
|
||||
func TuitView(cmd *commands.Commander) error {
|
||||
tracksLoading.Store(false)
|
||||
playlistsLoading.Store(false)
|
||||
playlistsList := tview.NewList().ShowSecondaryText(false)
|
||||
playlistsList.SetBorder(true).SetTitle("Playlists")
|
||||
savedTracksList := tview.NewList().ShowSecondaryText(false)
|
||||
savedTracksList.SetWrapAround(false)
|
||||
savedTracksList.SetBorder(true).SetTitle("Tracks")
|
||||
savedTracksList.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
||||
go cmd.PlayLikedSongs(index)
|
||||
})
|
||||
flex := tview.NewFlex().AddItem(playlistsList, 0, 1, false).AddItem(savedTracksList, 0, 2, true)
|
||||
playlists, err := cmd.Playlists(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, playlist := range playlists.Playlists {
|
||||
playlistsList.AddItem(playlist.Name, "", 0, func() {
|
||||
playlistTracksList := tview.NewList().ShowSecondaryText(false)
|
||||
playlistTracksList.SetBorder(true).SetTitle(playlist.Name)
|
||||
playlistTracksList.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
||||
go cmd.PlaySongInPlaylist((*spotify.URI)(&secondaryText), &index)
|
||||
})
|
||||
tracks, err := cmd.PlaylistTracks(playlist.ID, 1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, track := range tracks.Items {
|
||||
playlistTracksList.AddItem(track.Track.Track.Name+" - "+track.Track.Track.Artists[0].Name, string(playlist.URI), 0, nil)
|
||||
}
|
||||
flex.Clear()
|
||||
flex.AddItem(playlistsList, 0, 1, false)
|
||||
flex.AddItem(playlistTracksList, 0, 2, false)
|
||||
})
|
||||
}
|
||||
tracks, err := cmd.TrackList(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, track := range tracks.Tracks {
|
||||
savedTracksList.AddItem(track.Name+" - "+track.Artists[0].Name, "", 0, nil)
|
||||
}
|
||||
playlistsList.SetChangedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
||||
if playlistsList.GetItemCount()%50 != 0 {
|
||||
return
|
||||
}
|
||||
if playlistsList.GetItemCount()-index < 40 {
|
||||
go func() {
|
||||
if playlistsLoading.Load() {
|
||||
return
|
||||
}
|
||||
playlistsLoading.Store(true)
|
||||
defer playlistsLoading.Store(false)
|
||||
playlistsPage++
|
||||
newPlaylists, _ := cmd.Playlists(playlistsPage)
|
||||
for _, playlist := range newPlaylists.Playlists {
|
||||
savedTracksList.AddItem(playlist.Name, "", 0, nil)
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
savedTracksList.SetChangedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
||||
if savedTracksList.GetItemCount()%50 != 0 {
|
||||
return
|
||||
}
|
||||
if savedTracksList.GetItemCount()-index < 40 {
|
||||
go func() {
|
||||
if tracksLoading.Load() {
|
||||
return
|
||||
}
|
||||
tracksLoading.Store(true)
|
||||
defer tracksLoading.Store(false)
|
||||
tracksPage++
|
||||
tracks, _ := cmd.TrackList(tracksPage)
|
||||
for _, track := range tracks.Tracks {
|
||||
savedTracksList.AddItem(track.Name+" - "+track.Artists[0].Name, "", 0, nil)
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
if err := tview.NewApplication().EnableMouse(true).SetRoot(flex, true).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -94,7 +94,7 @@ func Search(query string) string {
|
||||
ctx := context.Background()
|
||||
|
||||
confDir, _ := os.UserConfigDir()
|
||||
b, err := os.ReadFile(filepath.Join(confDir, "gospt", "client_secret.json"))
|
||||
b, err := os.ReadFile(filepath.Join(confDir, "gspot", "client_secret.json"))
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to read client secret file: %v", err)
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
package config
|
||||
|
||||
var Values struct {
|
||||
ClientId string `yaml:"client_id"`
|
||||
type Config struct {
|
||||
ClientID string `yaml:"client_id"`
|
||||
ClientSecret string `yaml:"client_secret"`
|
||||
ClientSecretCmd string `yaml:"client_secret_cmd"`
|
||||
Port string `yaml:"port"`
|
||||
LogLevel string `yaml:"log_level" default:"info"`
|
||||
LogOutput string `yaml:"log_output" default:"stdout"`
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
package gctx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"tuxpa.in/a/zlog"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
zlog.Logger
|
||||
Debug zlog.Logger
|
||||
|
||||
context.Context
|
||||
}
|
||||
|
||||
func (c *Context) Err() error {
|
||||
return c.Context.Err()
|
||||
}
|
||||
|
||||
func (c *Context) Println(args ...any) {
|
||||
c.Info().Msg(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func NewContext(ctx context.Context) *Context {
|
||||
out := &Context{
|
||||
Context: ctx,
|
||||
Logger: zlog.New(os.Stderr),
|
||||
Debug: zlog.New(os.Stderr),
|
||||
}
|
||||
return out
|
||||
}
|
180
src/listenbrainz/listenbrainz.go
Normal file
180
src/listenbrainz/listenbrainz.go
Normal file
@ -0,0 +1,180 @@
|
||||
package listenbrainz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type ListenBrainz struct {
|
||||
client *resty.Client
|
||||
|
||||
labs *resty.Client
|
||||
}
|
||||
|
||||
func NewListenBrainz(
|
||||
Endpoint string,
|
||||
ApiKey string,
|
||||
) *ListenBrainz {
|
||||
c := resty.New().SetBaseURL(Endpoint)
|
||||
if ApiKey != "" {
|
||||
c = c.SetHeader("Authorization", "Token "+ApiKey)
|
||||
}
|
||||
return &ListenBrainz{
|
||||
client: c,
|
||||
labs: resty.New().SetBaseURL("https://labs.api.listenbrainz.org"),
|
||||
}
|
||||
}
|
||||
|
||||
type RadioApiResponse struct {
|
||||
Payload struct {
|
||||
Feedback []string `json:"feedback"`
|
||||
Jspf struct {
|
||||
Playlist struct {
|
||||
Annotation string `json:"annotation"`
|
||||
Creator string `json:"creator"`
|
||||
Extension struct {
|
||||
HTTPSMusicbrainzOrgDocJspfPlaylist struct {
|
||||
Public bool `json:"public"`
|
||||
} `json:"https://musicbrainz.org/doc/jspf#playlist"`
|
||||
} `json:"extension"`
|
||||
Title string `json:"title"`
|
||||
Track []ApiTrack `json:"track"`
|
||||
} `json:"playlist"`
|
||||
} `json:"jspf"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
type ApiTrack struct {
|
||||
Album string `json:"album"`
|
||||
Creator string `json:"creator"`
|
||||
Duration int `json:"duration,omitempty"`
|
||||
Extension struct {
|
||||
HTTPSMusicbrainzOrgDocJspfTrack struct {
|
||||
ArtistIdentifiers []string `json:"artist_identifiers"`
|
||||
ReleaseIdentifier string `json:"release_identifier"`
|
||||
} `json:"https://musicbrainz.org/doc/jspf#track"`
|
||||
} `json:"extension"`
|
||||
Identifier []string `json:"identifier"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func (o *ListenBrainz) RequestRadio(ctx context.Context, req *RadioRequest) (*RadioTracksResponse, error) {
|
||||
var res RadioApiResponse
|
||||
resp, err := o.client.R().
|
||||
SetResult(&res).
|
||||
SetQueryParam("prompt", req.Prompt).
|
||||
SetQueryParam("mode", req.Prompt).
|
||||
Get("/1/explore/lb-radio")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case resp.StatusCode() == 200:
|
||||
default:
|
||||
return nil, fmt.Errorf("radio request code %d: %s", resp.StatusCode(), resp.Status())
|
||||
}
|
||||
tracks := res.Payload.Jspf.Playlist.Track
|
||||
return &RadioTracksResponse{
|
||||
Tracks: tracks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RadioTracksResponse struct {
|
||||
Tracks []ApiTrack
|
||||
}
|
||||
|
||||
type RadioRequest struct {
|
||||
Prompt string
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type MatchTracksParams struct {
|
||||
Tracks []ApiTrack
|
||||
}
|
||||
|
||||
func (o *ListenBrainz) MatchTracks(ctx context.Context, params *MatchTracksParams) error {
|
||||
// first try to get the mbid from the recording id
|
||||
o.labs.R().Get("/")
|
||||
return nil
|
||||
}
|
||||
|
||||
type MatchedTrack struct {
|
||||
Mbid string
|
||||
SpotifyId string
|
||||
Strategy string
|
||||
}
|
||||
|
||||
type TrackMatch struct {
|
||||
RecordingMbid string `json:"recording_mbid"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
ReleaseName string `json:"release_name"`
|
||||
TrackName string `json:"track_name"`
|
||||
SpotifyTrackIds []string `json:"spotify_track_ids"`
|
||||
}
|
||||
|
||||
var ErrNoMatch = fmt.Errorf("no match")
|
||||
|
||||
func (o *ListenBrainz) MatchTrack(ctx context.Context, track *ApiTrack) (*MatchedTrack, error) {
|
||||
// refuse to match a track with no identifiers
|
||||
if len(track.Identifier) == 0 {
|
||||
return nil, fmt.Errorf("%w: no identifier", ErrNoMatch)
|
||||
}
|
||||
var matches []TrackMatch
|
||||
// there are mbids, so try to get the first one that is valid
|
||||
resp, err := o.labs.R().
|
||||
SetResult(&matches).
|
||||
SetQueryParam("recording_mbid", track.Identifier[0]).
|
||||
Get("/spotify-id-from-mbid/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return nil, fmt.Errorf("labs request code %d: %s", resp.StatusCode(), resp.Status())
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("%w: no mbid", ErrNoMatch)
|
||||
}
|
||||
// for each match, see if ther eis a spotify id, and if there is, we are done!
|
||||
for _, match := range matches {
|
||||
if len(match.SpotifyTrackIds) == 0 {
|
||||
continue
|
||||
}
|
||||
return &MatchedTrack{
|
||||
Mbid: match.RecordingMbid,
|
||||
SpotifyId: match.SpotifyTrackIds[0],
|
||||
Strategy: "exact-match",
|
||||
}, nil
|
||||
}
|
||||
for _, match := range matches {
|
||||
var submatch []TrackMatch
|
||||
resp, err := o.labs.R().
|
||||
SetResult(&submatch).
|
||||
SetQueryParam("artist_name", match.ArtistName).
|
||||
SetQueryParam("release_name", match.ReleaseName).
|
||||
SetQueryParam("track_name", match.TrackName).
|
||||
Get("/spotify-id-from-track/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return nil, fmt.Errorf("labs request code %d: %s", resp.StatusCode(), resp.Status())
|
||||
}
|
||||
if len(submatch) == 0 {
|
||||
return nil, fmt.Errorf("%w: no tracks found", ErrNoMatch)
|
||||
}
|
||||
for _, submatch := range submatch {
|
||||
if len(submatch.SpotifyTrackIds) == 0 {
|
||||
continue
|
||||
}
|
||||
return &MatchedTrack{
|
||||
Mbid: match.RecordingMbid,
|
||||
SpotifyId: submatch.SpotifyTrackIds[0],
|
||||
Strategy: "track-match",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: no tracks found", ErrNoMatch)
|
||||
}
|
79
src/listenbrainz/radio.go
Normal file
79
src/listenbrainz/radio.go
Normal file
@ -0,0 +1,79 @@
|
||||
package listenbrainz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type RadioPromptBuilder struct {
|
||||
px RadioParameters
|
||||
}
|
||||
|
||||
func (o *RadioPromptBuilder) Add(name string, values ...string) *RadioPromptBuilder {
|
||||
o.px = append(o.px, RadioParameter{
|
||||
Name: name,
|
||||
Values: values,
|
||||
})
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *RadioPromptBuilder) AddParameter(p RadioParameter) *RadioPromptBuilder {
|
||||
o.px = append(o.px, p)
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *RadioPromptBuilder) AddWithCount(name string, count int, values ...string) *RadioPromptBuilder {
|
||||
o.px = append(o.px, RadioParameter{
|
||||
Name: name,
|
||||
Count: count,
|
||||
Values: values,
|
||||
})
|
||||
return o
|
||||
}
|
||||
func (o *RadioPromptBuilder) AddWithOption(name string, option string, values ...string) *RadioPromptBuilder {
|
||||
o.px = append(o.px, RadioParameter{
|
||||
Name: name,
|
||||
Option: option,
|
||||
Values: values,
|
||||
})
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *RadioPromptBuilder) String() string {
|
||||
val, _ := o.px.MarshalText()
|
||||
return string(val)
|
||||
}
|
||||
|
||||
type RadioParameters []RadioParameter
|
||||
|
||||
func (r RadioParameters) MarshalText() ([]byte, error) {
|
||||
o := &bytes.Buffer{}
|
||||
for pidx, v := range r {
|
||||
o.WriteString(v.Name)
|
||||
o.WriteString(":(")
|
||||
for idx, vv := range v.Values {
|
||||
o.WriteString(vv)
|
||||
if len(v.Values) > 1 && idx != len(v.Values)-1 {
|
||||
o.WriteString(",")
|
||||
}
|
||||
}
|
||||
o.WriteString(")")
|
||||
if v.Count > 0 {
|
||||
o.WriteString(":" + strconv.Itoa(v.Count))
|
||||
}
|
||||
if v.Option != "" {
|
||||
o.WriteString(":" + v.Option)
|
||||
}
|
||||
if len(r) > 1 && pidx != len(r)-1 {
|
||||
o.WriteString(" ")
|
||||
}
|
||||
}
|
||||
return o.Bytes(), nil
|
||||
}
|
||||
|
||||
type RadioParameter struct {
|
||||
Name string `json:"name"`
|
||||
Values []string `json:"value"`
|
||||
Count int `json:"count"`
|
||||
Option string `json:"options"`
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package auth
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -8,16 +8,15 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tuxpa.in/a/zlog/log"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gospt/src/config"
|
||||
"git.asdf.cafe/abs3nt/gospt/src/gctx"
|
||||
|
||||
"github.com/zmb3/spotify/v2"
|
||||
spotifyauth "github.com/zmb3/spotify/v2/auth"
|
||||
"golang.org/x/exp/slog"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/config"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -33,17 +32,27 @@ func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
return fn(req)
|
||||
}
|
||||
|
||||
func GetClient(ctx *gctx.Context) (*spotify.Client, error) {
|
||||
if config.Values.ClientId == "" || config.Values.ClientSecret == "" || config.Values.Port == "" {
|
||||
fmt.Println("PLEASE WRITE YOUR CONFIG FILE IN", filepath.Join(configDir, "gospt/client.yml"))
|
||||
fmt.Println("GO HERE TO AND MAKE AN APPLICATION: https://developer.spotify.com/dashboard/applications")
|
||||
fmt.Print("\nclient_id: \"idgoesherelikethis\"\nclient_secret: \"secretgoesherelikethis\"\nport:\"8888\"\n\n")
|
||||
return nil, fmt.Errorf("\nINVALID CONFIG")
|
||||
func GetClient(conf *config.Config) (c *spotify.Client, err error) {
|
||||
if conf.ClientID == "" || (conf.ClientSecret == "" && conf.ClientSecretCmd == "") || conf.Port == "" {
|
||||
return nil, fmt.Errorf("INVALID CONFIG")
|
||||
}
|
||||
if conf.ClientSecretCmd != "" {
|
||||
args := strings.Fields(conf.ClientSecretCmd)
|
||||
cmd := args[0]
|
||||
secretCommand := exec.Command(cmd)
|
||||
if len(args) > 1 {
|
||||
secretCommand.Args = args
|
||||
}
|
||||
secret, err := secretCommand.Output()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conf.ClientSecret = strings.TrimSpace(string(secret))
|
||||
}
|
||||
auth = spotifyauth.New(
|
||||
spotifyauth.WithClientID(config.Values.ClientId),
|
||||
spotifyauth.WithClientSecret(config.Values.ClientSecret),
|
||||
spotifyauth.WithRedirectURL(fmt.Sprintf("http://localhost:%s/callback", config.Values.Port)),
|
||||
spotifyauth.WithClientID(conf.ClientID),
|
||||
spotifyauth.WithClientSecret(conf.ClientSecret),
|
||||
spotifyauth.WithRedirectURL(fmt.Sprintf("http://localhost:%s/callback", conf.Port)),
|
||||
spotifyauth.WithScopes(
|
||||
spotifyauth.ScopeImageUpload,
|
||||
spotifyauth.ScopePlaylistReadPrivate,
|
||||
@ -64,8 +73,8 @@ func GetClient(ctx *gctx.Context) (*spotify.Client, error) {
|
||||
spotifyauth.ScopeStreaming,
|
||||
),
|
||||
)
|
||||
if _, err := os.Stat(filepath.Join(configDir, "gospt/auth.json")); err == nil {
|
||||
authFilePath := filepath.Join(configDir, "gospt/auth.json")
|
||||
if _, err := os.Stat(filepath.Join(configDir, "gspot/auth.json")); err == nil {
|
||||
authFilePath := filepath.Join(configDir, "gspot/auth.json")
|
||||
authFile, err := os.Open(authFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -76,19 +85,19 @@ func GetClient(ctx *gctx.Context) (*spotify.Client, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.Context = context.WithValue(ctx.Context, oauth2.HTTPClient, &http.Client{
|
||||
authCtx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{
|
||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||
log.Trace().Interface("path", r.URL.Path).Msg("request")
|
||||
slog.Debug("ROUND_TRIPPER", "request", r.URL.Path)
|
||||
return http.DefaultTransport.RoundTrip(r)
|
||||
}),
|
||||
})
|
||||
authClient := auth.Client(ctx, tok)
|
||||
client := spotify.New(authClient)
|
||||
new_token, err := client.Token()
|
||||
authClient := auth.Client(authCtx, tok)
|
||||
client := spotify.New(authClient, spotify.WithRetry(true))
|
||||
newToken, err := client.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := json.MarshalIndent(new_token, "", " ")
|
||||
out, err := json.MarshalIndent(newToken, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -101,29 +110,29 @@ func GetClient(ctx *gctx.Context) (*spotify.Client, error) {
|
||||
// first start an HTTP server
|
||||
http.HandleFunc("/callback", completeAuth)
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("Got request for:", r.URL.String())
|
||||
slog.Info("AUTHENTICATOR", "received request", r.URL.String())
|
||||
})
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", config.Values.Port),
|
||||
Addr: fmt.Sprintf(":%s", conf.Port),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
go func() {
|
||||
server.ListenAndServe()
|
||||
_ = server.ListenAndServe()
|
||||
}()
|
||||
url := auth.AuthURL(state)
|
||||
fmt.Println(url)
|
||||
slog.Info("AUTH", "url", url)
|
||||
cmd := exec.Command("xdg-open", url)
|
||||
cmd.Start()
|
||||
_ = cmd.Start()
|
||||
// wait for auth to complete
|
||||
client := <-ch
|
||||
|
||||
server.Shutdown(ctx)
|
||||
_ = server.Shutdown(context.Background())
|
||||
// use the client to make calls that require authorization
|
||||
user, err := client.CurrentUser(ctx)
|
||||
user, err := client.CurrentUser(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("You are logged in as:", user.ID)
|
||||
slog.Info("AUTH", "You are logged in as:", user.ID)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@ -134,18 +143,20 @@ func completeAuth(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if st := r.FormValue("state"); st != state {
|
||||
http.NotFound(w, r)
|
||||
log.Fatalf("State mismatch: %s != %s\n", st, state)
|
||||
slog.Error("State mismatch: %s != %s\n", st, state)
|
||||
os.Exit(1)
|
||||
}
|
||||
out, err := json.MarshalIndent(tok, "", " ")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
slog.Error("AUTHENTICATOR", "failed to unmarshal", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(configDir, "gospt/auth.json"), out, 0o600)
|
||||
err = os.WriteFile(filepath.Join(configDir, "gspot/auth.json"), out, 0o600)
|
||||
if err != nil {
|
||||
panic("FAILED TO SAVE AUTH")
|
||||
slog.Error("AUTHENTICATOR", "failed to save auth", err)
|
||||
}
|
||||
// use the token to get an authenticated client
|
||||
client := spotify.New(auth.Client(r.Context(), tok))
|
||||
client := spotify.New(auth.Client(r.Context(), tok), spotify.WithRetry(true))
|
||||
fmt.Fprintf(w, "Login Completed!")
|
||||
ch <- client
|
||||
}
|
18
src/services/config.go
Normal file
18
src/services/config.go
Normal file
@ -0,0 +1,18 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"git.asdf.cafe/abs3nt/gunner"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gspot/src/config"
|
||||
)
|
||||
|
||||
var Config = fx.Options(
|
||||
fx.Provide(
|
||||
func() *config.Config {
|
||||
c := &config.Config{}
|
||||
gunner.LoadApp(c, "gspot")
|
||||
return c
|
||||
},
|
||||
),
|
||||
)
|
@ -1,121 +0,0 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"github.com/zmb3/spotify/v2"
|
||||
|
||||
"git.asdf.cafe/abs3nt/gospt/src/commands"
|
||||
"git.asdf.cafe/abs3nt/gospt/src/gctx"
|
||||
)
|
||||
|
||||
func HandlePlayWithContext(ctx *gctx.Context, commands *commands.Commands, uri *spotify.URI, pos *int) {
|
||||
err := commands.PlaySongInPlaylist(ctx, uri, pos)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRadio(ctx *gctx.Context, commands *commands.Commands, song spotify.SimpleTrack) {
|
||||
err := commands.RadioGivenSong(ctx, song, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleAlbumRadio(ctx *gctx.Context, commands *commands.Commands, album spotify.SimpleAlbum) {
|
||||
err := commands.RadioFromAlbum(ctx, album)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSeek(ctx *gctx.Context, commands *commands.Commands, fwd bool) {
|
||||
err := commands.Seek(ctx, fwd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleVolume(ctx *gctx.Context, commands *commands.Commands, up bool) {
|
||||
vol := 10
|
||||
if !up {
|
||||
vol = -10
|
||||
}
|
||||
err := commands.ChangeVolume(ctx, vol)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleArtistRadio(ctx *gctx.Context, commands *commands.Commands, artist spotify.SimpleArtist) {
|
||||
err := commands.RadioGivenArtist(ctx, artist)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleAlbumArtist(ctx *gctx.Context, commands *commands.Commands, artist spotify.SimpleArtist) {
|
||||
err := commands.RadioGivenArtist(ctx, artist)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandlePlaylistRadio(ctx *gctx.Context, commands *commands.Commands, playlist spotify.SimplePlaylist) {
|
||||
err := commands.RadioFromPlaylist(ctx, playlist)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleLibraryRadio(ctx *gctx.Context, commands *commands.Commands) {
|
||||
err := commands.RadioFromSavedTracks(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandlePlayLikedSong(ctx *gctx.Context, commands *commands.Commands, position int) {
|
||||
err := commands.PlayLikedSongs(ctx, position)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandlePlayTrack(ctx *gctx.Context, commands *commands.Commands, track spotify.ID) {
|
||||
err := commands.QueueSong(ctx, track)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = commands.Next(ctx, 1, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleNextInQueue(ctx *gctx.Context, commands *commands.Commands, amt int) {
|
||||
err := commands.Next(ctx, amt, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleQueueItem(ctx *gctx.Context, commands *commands.Commands, item spotify.ID) {
|
||||
err := commands.QueueSong(ctx, item)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleDeleteTrackFromPlaylist(ctx *gctx.Context, commands *commands.Commands, item, playlist spotify.ID) {
|
||||
err := commands.DeleteTracksFromPlaylist(ctx, []spotify.ID{item}, playlist)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSetDevice(ctx *gctx.Context, commands *commands.Commands, player spotify.PlayerDevice) {
|
||||
err := commands.SetDevice(ctx, player)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user